Initial commit

This commit is contained in:
Jason Dove
2021-02-08 21:13:53 -06:00
commit 4d52e115b5
493 changed files with 31399 additions and 0 deletions
@@ -0,0 +1,25 @@
using ErsatzTV.Core;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.FFmpegProfiles.Commands
{
public record CreateFFmpegProfile(
string Name,
int ThreadCount,
bool Transcode,
int ResolutionId,
bool NormalizeResolution,
string VideoCodec,
bool NormalizeVideoCodec,
int VideoBitrate,
int VideoBufferSize,
string AudioCodec,
bool NormalizeAudioCodec,
int AudioBitrate,
int AudioBufferSize,
int AudioVolume,
int AudioChannels,
int AudioSampleRate,
bool NormalizeAudio) : IRequest<Either<BaseError, FFmpegProfileViewModel>>;
}
@@ -0,0 +1,72 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.FFmpegProfiles.Mapper;
namespace ErsatzTV.Application.FFmpegProfiles.Commands
{
public class
CreateFFmpegProfileHandler : IRequestHandler<CreateFFmpegProfile, Either<BaseError, FFmpegProfileViewModel>>
{
private readonly IFFmpegProfileRepository _ffmpegProfileRepository;
private readonly IResolutionRepository _resolutionRepository;
public CreateFFmpegProfileHandler(
IFFmpegProfileRepository ffmpegProfileRepository,
IResolutionRepository resolutionRepository)
{
_ffmpegProfileRepository = ffmpegProfileRepository;
_resolutionRepository = resolutionRepository;
}
public Task<Either<BaseError, FFmpegProfileViewModel>> Handle(
CreateFFmpegProfile request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(PersistFFmpegProfile)
.Bind(v => v.ToEitherAsync());
private Task<FFmpegProfileViewModel> PersistFFmpegProfile(FFmpegProfile ffmpegProfile) =>
_ffmpegProfileRepository.Add(ffmpegProfile).Map(ProjectToViewModel);
private async Task<Validation<BaseError, FFmpegProfile>> Validate(CreateFFmpegProfile request) =>
(ValidateName(request), ValidateThreadCount(request), await ResolutionMustExist(request))
.Apply(
(name, threadCount, resolutionId) => new FFmpegProfile
{
Name = name,
ThreadCount = threadCount,
Transcode = request.Transcode,
ResolutionId = resolutionId,
NormalizeResolution = request.NormalizeResolution,
VideoCodec = request.VideoCodec,
NormalizeVideoCodec = request.NormalizeVideoCodec,
VideoBitrate = request.VideoBitrate,
VideoBufferSize = request.VideoBufferSize,
AudioCodec = request.AudioCodec,
NormalizeAudioCodec = request.NormalizeAudioCodec,
AudioBitrate = request.AudioBitrate,
AudioBufferSize = request.AudioBufferSize,
AudioVolume = request.AudioVolume,
AudioChannels = request.AudioChannels,
AudioSampleRate = request.AudioSampleRate,
NormalizeAudio = request.NormalizeAudio
});
private Validation<BaseError, string> ValidateName(CreateFFmpegProfile createFFmpegProfile) =>
createFFmpegProfile.NotEmpty(x => x.Name)
.Bind(_ => createFFmpegProfile.NotLongerThan(50)(x => x.Name));
private Validation<BaseError, int> ValidateThreadCount(CreateFFmpegProfile createFFmpegProfile) =>
createFFmpegProfile.AtLeast(1)(p => p.ThreadCount);
private async Task<Validation<BaseError, int>> ResolutionMustExist(CreateFFmpegProfile createFFmpegProfile) =>
(await _resolutionRepository.Get(createFFmpegProfile.ResolutionId))
.ToValidation<BaseError>($"[Resolution] {createFFmpegProfile.ResolutionId} does not exist.")
.Map(c => c.Id);
}
}
@@ -0,0 +1,9 @@
using System.Threading.Tasks;
using ErsatzTV.Core;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.FFmpegProfiles.Commands
{
public record DeleteFFmpegProfile(int FFmpegProfileId) : IRequest<Either<BaseError, Task>>;
}
@@ -0,0 +1,32 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.FFmpegProfiles.Commands
{
public class DeleteFFmpegProfileHandler : IRequestHandler<DeleteFFmpegProfile, Either<BaseError, Task>>
{
private readonly IFFmpegProfileRepository _ffmpegProfileRepository;
public DeleteFFmpegProfileHandler(IFFmpegProfileRepository ffmpegProfileRepository) =>
_ffmpegProfileRepository = ffmpegProfileRepository;
public async Task<Either<BaseError, Task>> Handle(
DeleteFFmpegProfile request,
CancellationToken cancellationToken) =>
(await FFmpegProfileMustExist(request))
.Map(DoDeletion)
.ToEither<Task>();
private Task DoDeletion(int channelId) => _ffmpegProfileRepository.Delete(channelId);
private async Task<Validation<BaseError, int>> FFmpegProfileMustExist(
DeleteFFmpegProfile deleteFFmpegProfile) =>
(await _ffmpegProfileRepository.Get(deleteFFmpegProfile.FFmpegProfileId))
.ToValidation<BaseError>($"FFmpegProfile {deleteFFmpegProfile.FFmpegProfileId} does not exist.")
.Map(c => c.Id);
}
}
@@ -0,0 +1,10 @@
using MediatR;
namespace ErsatzTV.Application.FFmpegProfiles.Commands
{
/// <summary>
/// Requests a new ffmpeg profile (view model) that contains
/// appropriate default values.
/// </summary>
public record NewFFmpegProfile : IRequest<FFmpegProfileViewModel>;
}
@@ -0,0 +1,40 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static LanguageExt.Prelude;
using static ErsatzTV.Application.FFmpegProfiles.Mapper;
namespace ErsatzTV.Application.FFmpegProfiles.Commands
{
public class NewFFmpegProfileHandler : IRequestHandler<NewFFmpegProfile, FFmpegProfileViewModel>
{
private readonly IConfigElementRepository _configElementRepository;
private readonly IResolutionRepository _resolutionRepository;
public NewFFmpegProfileHandler(
IResolutionRepository resolutionRepository,
IConfigElementRepository configElementRepository)
{
_resolutionRepository = resolutionRepository;
_configElementRepository = configElementRepository;
}
public async Task<FFmpegProfileViewModel> Handle(NewFFmpegProfile request, CancellationToken cancellationToken)
{
int defaultResolutionId = await _configElementRepository
.GetValue<int>(ConfigElementKey.FFmpegDefaultResolutionId)
.IfNoneAsync(0);
List<Resolution> allResolutions = await _resolutionRepository.GetAll();
Option<Resolution> maybeDefaultResolution = allResolutions.Find(r => r.Id == defaultResolutionId);
Resolution defaultResolution = maybeDefaultResolution.Match(identity, () => allResolutions.Head());
return ProjectToViewModel(FFmpegProfile.New("New Profile", defaultResolution));
}
}
}
@@ -0,0 +1,26 @@
using ErsatzTV.Core;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.FFmpegProfiles.Commands
{
public record UpdateFFmpegProfile(
int FFmpegProfileId,
string Name,
int ThreadCount,
bool Transcode,
int ResolutionId,
bool NormalizeResolution,
string VideoCodec,
bool NormalizeVideoCodec,
int VideoBitrate,
int VideoBufferSize,
string AudioCodec,
bool NormalizeAudioCodec,
int AudioBitrate,
int AudioBufferSize,
int AudioVolume,
int AudioChannels,
int AudioSampleRate,
bool NormalizeAudio) : IRequest<Either<BaseError, FFmpegProfileViewModel>>;
}
@@ -0,0 +1,78 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.FFmpegProfiles.Mapper;
namespace ErsatzTV.Application.FFmpegProfiles.Commands
{
public class
UpdateFFmpegProfileHandler : IRequestHandler<UpdateFFmpegProfile, Either<BaseError, FFmpegProfileViewModel>>
{
private readonly IFFmpegProfileRepository _ffmpegProfileRepository;
private readonly IResolutionRepository _resolutionRepository;
public UpdateFFmpegProfileHandler(
IFFmpegProfileRepository ffmpegProfileRepository,
IResolutionRepository resolutionRepository)
{
_ffmpegProfileRepository = ffmpegProfileRepository;
_resolutionRepository = resolutionRepository;
}
public Task<Either<BaseError, FFmpegProfileViewModel>> Handle(
UpdateFFmpegProfile request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(c => ApplyUpdateRequest(c, request))
.Bind(v => v.ToEitherAsync());
private async Task<FFmpegProfileViewModel> ApplyUpdateRequest(FFmpegProfile p, UpdateFFmpegProfile update)
{
p.Name = update.Name;
p.ThreadCount = update.ThreadCount;
p.Transcode = update.Transcode;
p.ResolutionId = update.ResolutionId;
p.NormalizeResolution = update.NormalizeResolution;
p.VideoCodec = update.VideoCodec;
p.NormalizeVideoCodec = update.NormalizeVideoCodec;
p.VideoBitrate = update.VideoBitrate;
p.VideoBufferSize = update.VideoBufferSize;
p.AudioCodec = update.AudioCodec;
p.NormalizeAudioCodec = update.NormalizeAudioCodec;
p.AudioBitrate = update.AudioBitrate;
p.AudioBufferSize = update.AudioBufferSize;
p.AudioVolume = update.AudioVolume;
p.AudioChannels = update.AudioChannels;
p.AudioSampleRate = update.AudioSampleRate;
p.NormalizeAudio = update.NormalizeAudio;
await _ffmpegProfileRepository.Update(p);
return ProjectToViewModel(p);
}
private async Task<Validation<BaseError, FFmpegProfile>> Validate(UpdateFFmpegProfile request) =>
(await FFmpegProfileMustExist(request), ValidateName(request), ValidateThreadCount(request),
await ResolutionMustExist(request))
.Apply((ffmpegProfileToUpdate, _, _, _) => ffmpegProfileToUpdate);
private async Task<Validation<BaseError, FFmpegProfile>> FFmpegProfileMustExist(
UpdateFFmpegProfile updateFFmpegProfile) =>
(await _ffmpegProfileRepository.Get(updateFFmpegProfile.FFmpegProfileId))
.ToValidation<BaseError>("FFmpegProfile does not exist.");
private Validation<BaseError, string> ValidateName(UpdateFFmpegProfile updateFFmpegProfile) =>
updateFFmpegProfile.NotEmpty(x => x.Name)
.Bind(_ => updateFFmpegProfile.NotLongerThan(50)(x => x.Name));
private Validation<BaseError, int> ValidateThreadCount(UpdateFFmpegProfile updateFFmpegProfile) =>
updateFFmpegProfile.AtLeast(1)(p => p.ThreadCount);
private async Task<Validation<BaseError, int>> ResolutionMustExist(UpdateFFmpegProfile updateFFmpegProfile) =>
(await _resolutionRepository.Get(updateFFmpegProfile.ResolutionId))
.ToValidation<BaseError>($"[Resolution] {updateFFmpegProfile.ResolutionId} does not exist.")
.Map(c => c.Id);
}
}
@@ -0,0 +1,6 @@
using MediatR;
namespace ErsatzTV.Application.FFmpegProfiles.Commands
{
public record UpdateFFmpegSettings(FFmpegSettingsViewModel Settings) : IRequest;
}
@@ -0,0 +1,70 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using Unit = MediatR.Unit;
namespace ErsatzTV.Application.FFmpegProfiles.Commands
{
public class UpdateFFmpegSettingsHandler : IRequestHandler<UpdateFFmpegSettings>
{
private readonly IConfigElementRepository _configElementRepository;
public UpdateFFmpegSettingsHandler(IConfigElementRepository configElementRepository) =>
_configElementRepository = configElementRepository;
public async Task<Unit> Handle(UpdateFFmpegSettings request, CancellationToken cancellationToken)
{
Option<ConfigElement> ffmpegPath = await _configElementRepository.Get(ConfigElementKey.FFmpegPath);
Option<ConfigElement> ffprobePath = await _configElementRepository.Get(ConfigElementKey.FFprobePath);
Option<ConfigElement> defaultFFmpegProfileId =
await _configElementRepository.Get(ConfigElementKey.FFmpegDefaultProfileId);
ffmpegPath.Match(
ce =>
{
ce.Value = request.Settings.FFmpegPath;
_configElementRepository.Update(ce);
},
() =>
{
var ce = new ConfigElement
{ Key = ConfigElementKey.FFmpegPath.Key, Value = request.Settings.FFmpegPath };
_configElementRepository.Add(ce);
});
ffprobePath.Match(
ce =>
{
ce.Value = request.Settings.FFprobePath;
_configElementRepository.Update(ce);
},
() =>
{
var ce = new ConfigElement
{ Key = ConfigElementKey.FFprobePath.Key, Value = request.Settings.FFprobePath };
_configElementRepository.Add(ce);
});
defaultFFmpegProfileId.Match(
ce =>
{
ce.Value = request.Settings.DefaultFFmpegProfileId.ToString();
_configElementRepository.Update(ce);
},
() =>
{
var ce = new ConfigElement
{
Key = ConfigElementKey.FFmpegDefaultProfileId.Key,
Value = request.Settings.DefaultFFmpegProfileId.ToString()
};
_configElementRepository.Add(ce);
});
return Unit.Value;
}
}
}
@@ -0,0 +1,24 @@
using ErsatzTV.Application.Resolutions;
namespace ErsatzTV.Application.FFmpegProfiles
{
public record FFmpegProfileViewModel(
int Id,
string Name,
int ThreadCount,
bool Transcode,
ResolutionViewModel Resolution,
bool NormalizeResolution,
string VideoCodec,
bool NormalizeVideoCodec,
int VideoBitrate,
int VideoBufferSize,
string AudioCodec,
bool NormalizeAudioCodec,
int AudioBitrate,
int AudioBufferSize,
int AudioVolume,
int AudioChannels,
int AudioSampleRate,
bool NormalizeAudio);
}
@@ -0,0 +1,9 @@
namespace ErsatzTV.Application.FFmpegProfiles
{
public class FFmpegSettingsViewModel
{
public string FFmpegPath { get; set; }
public string FFprobePath { get; set; }
public int DefaultFFmpegProfileId { get; set; }
}
}
@@ -0,0 +1,32 @@
using ErsatzTV.Application.Resolutions;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.FFmpegProfiles
{
internal static class Mapper
{
internal static FFmpegProfileViewModel ProjectToViewModel(FFmpegProfile profile) =>
new(
profile.Id,
profile.Name,
profile.ThreadCount,
profile.Transcode,
Project(profile.Resolution),
profile.NormalizeResolution,
profile.VideoCodec,
profile.NormalizeVideoCodec,
profile.VideoBitrate,
profile.VideoBufferSize,
profile.AudioCodec,
profile.NormalizeAudioCodec,
profile.AudioBitrate,
profile.AudioBufferSize,
profile.AudioVolume,
profile.AudioChannels,
profile.AudioSampleRate,
profile.NormalizeAudio);
private static ResolutionViewModel Project(Resolution resolution) =>
new(resolution.Id, resolution.Name, resolution.Width, resolution.Height);
}
}
@@ -0,0 +1,7 @@
using System.Collections.Generic;
using MediatR;
namespace ErsatzTV.Application.FFmpegProfiles.Queries
{
public record GetAllFFmpegProfiles : IRequest<List<FFmpegProfileViewModel>>;
}
@@ -0,0 +1,23 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using MediatR;
using static ErsatzTV.Application.FFmpegProfiles.Mapper;
namespace ErsatzTV.Application.FFmpegProfiles.Queries
{
public class GetAllFFmpegProfilesHandler : IRequestHandler<GetAllFFmpegProfiles, List<FFmpegProfileViewModel>>
{
private readonly IFFmpegProfileRepository _ffmpegProfileRepository;
public GetAllFFmpegProfilesHandler(IFFmpegProfileRepository ffmpegProfileRepository) =>
_ffmpegProfileRepository = ffmpegProfileRepository;
public async Task<List<FFmpegProfileViewModel>> Handle(
GetAllFFmpegProfiles request,
CancellationToken cancellationToken) =>
(await _ffmpegProfileRepository.GetAll()).Map(ProjectToViewModel).ToList();
}
}
@@ -0,0 +1,7 @@
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.FFmpegProfiles.Queries
{
public record GetFFmpegProfileById(int Id) : IRequest<Option<FFmpegProfileViewModel>>;
}
@@ -0,0 +1,23 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.FFmpegProfiles.Mapper;
namespace ErsatzTV.Application.FFmpegProfiles.Queries
{
public class GetFFmpegProfileByIdHandler : IRequestHandler<GetFFmpegProfileById, Option<FFmpegProfileViewModel>>
{
private readonly IFFmpegProfileRepository _ffmpegProfileRepository;
public GetFFmpegProfileByIdHandler(IFFmpegProfileRepository ffmpegProfileRepository) =>
_ffmpegProfileRepository = ffmpegProfileRepository;
public Task<Option<FFmpegProfileViewModel>> Handle(
GetFFmpegProfileById request,
CancellationToken cancellationToken) =>
_ffmpegProfileRepository.Get(request.Id)
.MapT(ProjectToViewModel);
}
}
@@ -0,0 +1,6 @@
using MediatR;
namespace ErsatzTV.Application.FFmpegProfiles.Queries
{
public record GetFFmpegSettings : IRequest<FFmpegSettingsViewModel>;
}
@@ -0,0 +1,34 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.FFmpegProfiles.Queries
{
public class GetFFmpegSettingsHandler : IRequestHandler<GetFFmpegSettings, FFmpegSettingsViewModel>
{
private readonly IConfigElementRepository _configElementRepository;
public GetFFmpegSettingsHandler(IConfigElementRepository configElementRepository) =>
_configElementRepository = configElementRepository;
public async Task<FFmpegSettingsViewModel> Handle(
GetFFmpegSettings request,
CancellationToken cancellationToken)
{
Option<string> ffmpegPath = await _configElementRepository.GetValue<string>(ConfigElementKey.FFmpegPath);
Option<string> ffprobePath = await _configElementRepository.GetValue<string>(ConfigElementKey.FFprobePath);
Option<int> defaultFFmpegProfileId =
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegDefaultProfileId);
return new FFmpegSettingsViewModel
{
FFmpegPath = ffmpegPath.IfNone(string.Empty),
FFprobePath = ffprobePath.IfNone(string.Empty),
DefaultFFmpegProfileId = defaultFFmpegProfileId.IfNone(0)
};
}
}
}