mirror of https://github.com/ErsatzTV/ErsatzTV.git
commit
4d52e115b5
493 changed files with 31399 additions and 0 deletions
@ -0,0 +1,18 @@ |
|||||||
|
{ |
||||||
|
"version": 1, |
||||||
|
"isRoot": true, |
||||||
|
"tools": { |
||||||
|
"jetbrains.resharper.globaltools": { |
||||||
|
"version": "2020.3.2", |
||||||
|
"commands": [ |
||||||
|
"jb" |
||||||
|
] |
||||||
|
}, |
||||||
|
"swashbuckle.aspnetcore.cli": { |
||||||
|
"version": "5.6.2", |
||||||
|
"commands": [ |
||||||
|
"swagger" |
||||||
|
] |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,7 @@ |
|||||||
|
docs/ |
||||||
|
sdk/ |
||||||
|
**/.vs/ |
||||||
|
**/bin/ |
||||||
|
**/obj/ |
||||||
|
.idea/ |
||||||
|
Dockerfile |
||||||
@ -0,0 +1,81 @@ |
|||||||
|
|
||||||
|
[*] |
||||||
|
charset=utf-8-bom |
||||||
|
end_of_line=lf |
||||||
|
trim_trailing_whitespace=false |
||||||
|
insert_final_newline=false |
||||||
|
indent_style=space |
||||||
|
indent_size=4 |
||||||
|
|
||||||
|
# Microsoft .NET properties |
||||||
|
csharp_new_line_before_members_in_object_initializers=false |
||||||
|
csharp_preferred_modifier_order=public, private, protected, internal, new, abstract, virtual, sealed, override, static, readonly, extern, unsafe, volatile, async:suggestion |
||||||
|
csharp_style_expression_bodied_accessors=true:suggestion |
||||||
|
csharp_style_expression_bodied_constructors=true:none |
||||||
|
csharp_style_expression_bodied_methods=true:none |
||||||
|
csharp_style_expression_bodied_properties=true:suggestion |
||||||
|
csharp_style_var_elsewhere=false:suggestion |
||||||
|
csharp_style_var_for_built_in_types=false:suggestion |
||||||
|
csharp_style_var_when_type_is_apparent=true:suggestion |
||||||
|
dotnet_naming_rule.local_constants_rule.severity=warning |
||||||
|
dotnet_naming_rule.local_constants_rule.style=all_upper_style |
||||||
|
dotnet_naming_rule.local_constants_rule.symbols=local_constants_symbols |
||||||
|
dotnet_naming_style.all_upper_style.capitalization=all_upper |
||||||
|
dotnet_naming_style.all_upper_style.word_separator=_ |
||||||
|
dotnet_naming_symbols.local_constants_symbols.applicable_accessibilities=* |
||||||
|
dotnet_naming_symbols.local_constants_symbols.applicable_kinds=local |
||||||
|
dotnet_naming_symbols.local_constants_symbols.required_modifiers=const |
||||||
|
dotnet_style_parentheses_in_arithmetic_binary_operators=never_if_unnecessary:none |
||||||
|
dotnet_style_parentheses_in_other_binary_operators=never_if_unnecessary:none |
||||||
|
dotnet_style_parentheses_in_relational_binary_operators=never_if_unnecessary:none |
||||||
|
dotnet_style_predefined_type_for_locals_parameters_members=true:suggestion |
||||||
|
dotnet_style_predefined_type_for_member_access=true:suggestion |
||||||
|
dotnet_style_qualification_for_event=false:suggestion |
||||||
|
dotnet_style_qualification_for_field=false:suggestion |
||||||
|
dotnet_style_qualification_for_method=false:suggestion |
||||||
|
dotnet_style_qualification_for_property=false:suggestion |
||||||
|
dotnet_style_require_accessibility_modifiers=for_non_interface_members:suggestion |
||||||
|
|
||||||
|
# ReSharper properties |
||||||
|
resharper_autodetect_indent_settings=true |
||||||
|
resharper_braces_for_for=required |
||||||
|
resharper_braces_for_foreach=required |
||||||
|
resharper_braces_for_ifelse=required |
||||||
|
resharper_braces_for_while=required |
||||||
|
resharper_csharp_insert_final_newline=true |
||||||
|
resharper_csharp_max_attribute_length_for_same_line=0 |
||||||
|
resharper_csharp_place_accessorholder_attribute_on_same_line=never |
||||||
|
resharper_csharp_place_field_attribute_on_same_line=if_owner_is_single_line |
||||||
|
resharper_csharp_wrap_after_declaration_lpar=true |
||||||
|
resharper_csharp_wrap_after_invocation_lpar=true |
||||||
|
resharper_csharp_wrap_arguments_style=chop_if_long |
||||||
|
resharper_csharp_wrap_parameters_style=chop_if_long |
||||||
|
resharper_enforce_line_ending_style=true |
||||||
|
resharper_for_built_in_types=use_var_when_evident |
||||||
|
resharper_space_within_single_line_array_initializer_braces=true |
||||||
|
resharper_use_indent_from_vs=false |
||||||
|
resharper_wrap_array_initializer_style=chop_if_long |
||||||
|
|
||||||
|
# ReSharper inspection severities |
||||||
|
resharper_arrange_redundant_parentheses_highlighting=hint |
||||||
|
resharper_arrange_this_qualifier_highlighting=hint |
||||||
|
resharper_arrange_type_member_modifiers_highlighting=hint |
||||||
|
resharper_arrange_type_modifiers_highlighting=hint |
||||||
|
resharper_built_in_type_reference_style_for_member_access_highlighting=hint |
||||||
|
resharper_built_in_type_reference_style_highlighting=hint |
||||||
|
resharper_redundant_base_qualifier_highlighting=warning |
||||||
|
resharper_suggest_var_or_type_built_in_types_highlighting=hint |
||||||
|
resharper_suggest_var_or_type_elsewhere_highlighting=hint |
||||||
|
resharper_suggest_var_or_type_simple_types_highlighting=hint |
||||||
|
resharper_web_config_module_not_resolved_highlighting=warning |
||||||
|
resharper_web_config_type_not_resolved_highlighting=warning |
||||||
|
resharper_web_config_wrong_module_highlighting=warning |
||||||
|
|
||||||
|
[{*.har,*.inputactions,*.jsb2,*.jsb3,*.json,.babelrc,.eslintrc,.stylelintrc,bowerrc,jest.config}] |
||||||
|
indent_style=space |
||||||
|
indent_size=2 |
||||||
|
|
||||||
|
[*.{appxmanifest,asax,ascx,aspx,build,cg,cginc,compute,cs,cshtml,dtd,fs,fsi,fsscript,fsx,hlsl,hlsli,hlslinc,master,ml,mli,nuspec,razor,resw,resx,shader,skin,usf,ush,vb,xaml,xamlx,xoml,xsd}] |
||||||
|
indent_style=space |
||||||
|
indent_size=4 |
||||||
|
tab_width=4 |
||||||
@ -0,0 +1,42 @@ |
|||||||
|
*.swp |
||||||
|
*.*~ |
||||||
|
project.lock.json |
||||||
|
.DS_Store |
||||||
|
*.pyc |
||||||
|
nupkg/ |
||||||
|
|
||||||
|
# Visual Studio Code |
||||||
|
.vscode |
||||||
|
|
||||||
|
# Rider |
||||||
|
.idea |
||||||
|
|
||||||
|
# User-specific files |
||||||
|
*.suo |
||||||
|
*.user |
||||||
|
*.userosscache |
||||||
|
*.sln.docstates |
||||||
|
|
||||||
|
# Build results |
||||||
|
[Dd]ebug/ |
||||||
|
[Dd]ebugPublic/ |
||||||
|
[Rr]elease/ |
||||||
|
[Rr]eleases/ |
||||||
|
x64/ |
||||||
|
x86/ |
||||||
|
build/ |
||||||
|
bld/ |
||||||
|
[Bb]in/ |
||||||
|
[Oo]bj/ |
||||||
|
[Oo]ut/ |
||||||
|
msbuild.log |
||||||
|
msbuild.err |
||||||
|
msbuild.wrn |
||||||
|
|
||||||
|
# Visual Studio 2015 |
||||||
|
.vs/ |
||||||
|
|
||||||
|
*.sqlite3* |
||||||
|
core |
||||||
|
|
||||||
|
scripts/generate-api-sdk/swagger.json |
||||||
@ -0,0 +1,23 @@ |
|||||||
|
FROM mcr.microsoft.com/dotnet/aspnet:5.0-focal-amd64 AS runtime-base |
||||||
|
RUN apt-get update && apt-get install -y ffmpeg |
||||||
|
|
||||||
|
# https://hub.docker.com/_/microsoft-dotnet |
||||||
|
FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build |
||||||
|
WORKDIR /source |
||||||
|
|
||||||
|
# copy csproj and restore as distinct layers |
||||||
|
COPY *.sln . |
||||||
|
COPY ErsatzTV/*.csproj ./ErsatzTV/ |
||||||
|
COPY ErsatzTV.Tests/*.csproj ./ErsatzTV.Tests/ |
||||||
|
RUN dotnet restore -r linux-x64 |
||||||
|
|
||||||
|
# copy everything else and build app |
||||||
|
COPY ErsatzTV/. ./ErsatzTV/ |
||||||
|
WORKDIR /source/ErsatzTV |
||||||
|
RUN dotnet publish -c release -o /app -r linux-x64 --self-contained false --no-restore |
||||||
|
|
||||||
|
# final stage/image |
||||||
|
FROM runtime-base |
||||||
|
WORKDIR /app |
||||||
|
COPY --from=build /app ./ |
||||||
|
ENTRYPOINT ["./ErsatzTV"] |
||||||
@ -0,0 +1,12 @@ |
|||||||
|
using ErsatzTV.Core.Domain; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Channels |
||||||
|
{ |
||||||
|
public record ChannelViewModel( |
||||||
|
int Id, |
||||||
|
int Number, |
||||||
|
string Name, |
||||||
|
int FFmpegProfileId, |
||||||
|
string Logo, |
||||||
|
StreamingMode StreamingMode); |
||||||
|
} |
||||||
@ -0,0 +1,15 @@ |
|||||||
|
using ErsatzTV.Core; |
||||||
|
using ErsatzTV.Core.Domain; |
||||||
|
using LanguageExt; |
||||||
|
using MediatR; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Channels.Commands |
||||||
|
{ |
||||||
|
public record CreateChannel |
||||||
|
( |
||||||
|
string Name, |
||||||
|
int Number, |
||||||
|
int FFmpegProfileId, |
||||||
|
string Logo, |
||||||
|
StreamingMode StreamingMode) : IRequest<Either<BaseError, ChannelViewModel>>; |
||||||
|
} |
||||||
@ -0,0 +1,58 @@ |
|||||||
|
using System; |
||||||
|
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.Channels.Mapper; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Channels.Commands |
||||||
|
{ |
||||||
|
public class CreateChannelHandler : IRequestHandler<CreateChannel, Either<BaseError, ChannelViewModel>> |
||||||
|
{ |
||||||
|
private readonly IChannelRepository _channelRepository; |
||||||
|
private readonly IFFmpegProfileRepository _ffmpegProfileRepository; |
||||||
|
|
||||||
|
public CreateChannelHandler( |
||||||
|
IChannelRepository channelRepository, |
||||||
|
IFFmpegProfileRepository ffmpegProfileRepository) |
||||||
|
{ |
||||||
|
_channelRepository = channelRepository; |
||||||
|
_ffmpegProfileRepository = ffmpegProfileRepository; |
||||||
|
} |
||||||
|
|
||||||
|
public Task<Either<BaseError, ChannelViewModel>> Handle( |
||||||
|
CreateChannel request, |
||||||
|
CancellationToken cancellationToken) => |
||||||
|
Validate(request) |
||||||
|
.MapT(PersistChannel) |
||||||
|
.Bind(v => v.ToEitherAsync()); |
||||||
|
|
||||||
|
private Task<ChannelViewModel> PersistChannel(Channel c) => |
||||||
|
_channelRepository.Add(c).Map(ProjectToViewModel); |
||||||
|
|
||||||
|
private async Task<Validation<BaseError, Channel>> Validate(CreateChannel request) => |
||||||
|
(ValidateName(request), ValidateNumber(request), await FFmpegProfileMustExist(request)) |
||||||
|
.Apply( |
||||||
|
(name, number, ffmpegProfileId) => new Channel(Guid.NewGuid()) |
||||||
|
{ |
||||||
|
Name = name, Number = number, FFmpegProfileId = ffmpegProfileId, |
||||||
|
StreamingMode = request.StreamingMode |
||||||
|
}); |
||||||
|
|
||||||
|
private Validation<BaseError, string> ValidateName(CreateChannel createChannel) => |
||||||
|
createChannel.NotEmpty(c => c.Name) |
||||||
|
.Bind(_ => createChannel.NotLongerThan(50)(c => c.Name)); |
||||||
|
|
||||||
|
// TODO: validate number does not exist?
|
||||||
|
private Validation<BaseError, int> ValidateNumber(CreateChannel createChannel) => |
||||||
|
createChannel.AtLeast(1)(c => c.Number); |
||||||
|
|
||||||
|
private async Task<Validation<BaseError, int>> FFmpegProfileMustExist(CreateChannel createChannel) => |
||||||
|
(await _ffmpegProfileRepository.Get(createChannel.FFmpegProfileId)) |
||||||
|
.ToValidation<BaseError>($"FFmpegProfile {createChannel.FFmpegProfileId} 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.Channels.Commands |
||||||
|
{ |
||||||
|
public record DeleteChannel(int ChannelId) : IRequest<Either<BaseError, Task>>; |
||||||
|
} |
||||||
@ -0,0 +1,28 @@ |
|||||||
|
using System.Threading; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Core; |
||||||
|
using ErsatzTV.Core.Interfaces.Repositories; |
||||||
|
using LanguageExt; |
||||||
|
using MediatR; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Channels.Commands |
||||||
|
{ |
||||||
|
public class DeleteChannelHandler : IRequestHandler<DeleteChannel, Either<BaseError, Task>> |
||||||
|
{ |
||||||
|
private readonly IChannelRepository _channelRepository; |
||||||
|
|
||||||
|
public DeleteChannelHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository; |
||||||
|
|
||||||
|
public async Task<Either<BaseError, Task>> Handle(DeleteChannel request, CancellationToken cancellationToken) => |
||||||
|
(await ChannelMustExist(request)) |
||||||
|
.Map(DoDeletion) |
||||||
|
.ToEither<Task>(); |
||||||
|
|
||||||
|
private Task DoDeletion(int channelId) => _channelRepository.Delete(channelId); |
||||||
|
|
||||||
|
private async Task<Validation<BaseError, int>> ChannelMustExist(DeleteChannel deleteChannel) => |
||||||
|
(await _channelRepository.Get(deleteChannel.ChannelId)) |
||||||
|
.ToValidation<BaseError>($"Channel {deleteChannel.ChannelId} does not exist.") |
||||||
|
.Map(c => c.Id); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,16 @@ |
|||||||
|
using ErsatzTV.Core; |
||||||
|
using ErsatzTV.Core.Domain; |
||||||
|
using LanguageExt; |
||||||
|
using MediatR; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Channels.Commands |
||||||
|
{ |
||||||
|
public record UpdateChannel |
||||||
|
( |
||||||
|
int ChannelId, |
||||||
|
string Name, |
||||||
|
int Number, |
||||||
|
int FFmpegProfileId, |
||||||
|
string Logo, |
||||||
|
StreamingMode StreamingMode) : IRequest<Either<BaseError, ChannelViewModel>>; |
||||||
|
} |
||||||
@ -0,0 +1,60 @@ |
|||||||
|
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.Channels.Mapper; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Channels.Commands |
||||||
|
{ |
||||||
|
public class UpdateChannelHandler : IRequestHandler<UpdateChannel, Either<BaseError, ChannelViewModel>> |
||||||
|
{ |
||||||
|
private readonly IChannelRepository _channelRepository; |
||||||
|
|
||||||
|
public UpdateChannelHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository; |
||||||
|
|
||||||
|
public Task<Either<BaseError, ChannelViewModel>> Handle( |
||||||
|
UpdateChannel request, |
||||||
|
CancellationToken cancellationToken) => |
||||||
|
Validate(request) |
||||||
|
.MapT(c => ApplyUpdateRequest(c, request)) |
||||||
|
.Bind(v => v.ToEitherAsync()); |
||||||
|
|
||||||
|
private async Task<ChannelViewModel> ApplyUpdateRequest(Channel c, UpdateChannel update) |
||||||
|
{ |
||||||
|
c.Name = update.Name; |
||||||
|
c.Number = update.Number; |
||||||
|
c.FFmpegProfileId = update.FFmpegProfileId; |
||||||
|
c.Logo = update.Logo; |
||||||
|
c.StreamingMode = update.StreamingMode; |
||||||
|
await _channelRepository.Update(c); |
||||||
|
return ProjectToViewModel(c); |
||||||
|
} |
||||||
|
|
||||||
|
private async Task<Validation<BaseError, Channel>> Validate(UpdateChannel request) => |
||||||
|
(await ChannelMustExist(request), ValidateName(request), await ValidateNumber(request)) |
||||||
|
.Apply((channelToUpdate, _, _) => channelToUpdate); |
||||||
|
|
||||||
|
private Task<Validation<BaseError, Channel>> ChannelMustExist(UpdateChannel updateChannel) => |
||||||
|
_channelRepository.Get(updateChannel.ChannelId) |
||||||
|
.Map(v => v.ToValidation<BaseError>("Channel does not exist.")); |
||||||
|
|
||||||
|
private Validation<BaseError, string> ValidateName(UpdateChannel updateChannel) => |
||||||
|
updateChannel.NotEmpty(c => c.Name) |
||||||
|
.Bind(_ => updateChannel.NotLongerThan(50)(c => c.Name)); |
||||||
|
|
||||||
|
private async Task<Validation<BaseError, int>> ValidateNumber(UpdateChannel updateChannel) |
||||||
|
{ |
||||||
|
Option<Channel> match = await _channelRepository.GetByNumber(updateChannel.Number); |
||||||
|
int matchId = match.Map(c => c.Id).IfNone(updateChannel.ChannelId); |
||||||
|
if (matchId == updateChannel.ChannelId) |
||||||
|
{ |
||||||
|
return updateChannel.AtLeast(1)(c => c.Number); |
||||||
|
} |
||||||
|
|
||||||
|
return BaseError.New("Channel number must be unique"); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,10 @@ |
|||||||
|
using ErsatzTV.Core.Domain; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Channels |
||||||
|
{ |
||||||
|
internal static class Mapper |
||||||
|
{ |
||||||
|
internal static ChannelViewModel ProjectToViewModel(Channel channel) => |
||||||
|
new(channel.Id, channel.Number, channel.Name, channel.FFmpegProfileId, channel.Logo, channel.StreamingMode); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,7 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
using MediatR; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Channels.Queries |
||||||
|
{ |
||||||
|
public record GetAllChannels : IRequest<List<ChannelViewModel>>; |
||||||
|
} |
||||||
@ -0,0 +1,21 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
using System.Linq; |
||||||
|
using System.Threading; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Core.Interfaces.Repositories; |
||||||
|
using LanguageExt; |
||||||
|
using MediatR; |
||||||
|
using static ErsatzTV.Application.Channels.Mapper; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Channels.Queries |
||||||
|
{ |
||||||
|
public class GetAllChannelsHandler : IRequestHandler<GetAllChannels, List<ChannelViewModel>> |
||||||
|
{ |
||||||
|
private readonly IChannelRepository _channelRepository; |
||||||
|
|
||||||
|
public GetAllChannelsHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository; |
||||||
|
|
||||||
|
public Task<List<ChannelViewModel>> Handle(GetAllChannels request, CancellationToken cancellationToken) => |
||||||
|
_channelRepository.GetAll().Map(channels => channels.Map(ProjectToViewModel).ToList()); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,7 @@ |
|||||||
|
using LanguageExt; |
||||||
|
using MediatR; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Channels.Queries |
||||||
|
{ |
||||||
|
public record GetChannelById(int Id) : IRequest<Option<ChannelViewModel>>; |
||||||
|
} |
||||||
@ -0,0 +1,20 @@ |
|||||||
|
using System.Threading; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Core.Interfaces.Repositories; |
||||||
|
using LanguageExt; |
||||||
|
using MediatR; |
||||||
|
using static ErsatzTV.Application.Channels.Mapper; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Channels.Queries |
||||||
|
{ |
||||||
|
public class GetChannelByIdHandler : IRequestHandler<GetChannelById, Option<ChannelViewModel>> |
||||||
|
{ |
||||||
|
private readonly IChannelRepository _channelRepository; |
||||||
|
|
||||||
|
public GetChannelByIdHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository; |
||||||
|
|
||||||
|
public Task<Option<ChannelViewModel>> Handle(GetChannelById request, CancellationToken cancellationToken) => |
||||||
|
_channelRepository.Get(request.Id) |
||||||
|
.MapT(ProjectToViewModel); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,7 @@ |
|||||||
|
using ErsatzTV.Core.Iptv; |
||||||
|
using MediatR; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Channels.Queries |
||||||
|
{ |
||||||
|
public record GetChannelGuide(string Scheme, string Host) : IRequest<ChannelGuide>; |
||||||
|
} |
||||||
@ -0,0 +1,20 @@ |
|||||||
|
using System.Threading; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Core.Interfaces.Repositories; |
||||||
|
using ErsatzTV.Core.Iptv; |
||||||
|
using LanguageExt; |
||||||
|
using MediatR; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Channels.Queries |
||||||
|
{ |
||||||
|
public class GetChannelGuideHandler : IRequestHandler<GetChannelGuide, ChannelGuide> |
||||||
|
{ |
||||||
|
private readonly IChannelRepository _channelRepository; |
||||||
|
|
||||||
|
public GetChannelGuideHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository; |
||||||
|
|
||||||
|
public Task<ChannelGuide> Handle(GetChannelGuide request, CancellationToken cancellationToken) => |
||||||
|
_channelRepository.GetAllForGuide() |
||||||
|
.Map(channels => new ChannelGuide(request.Scheme, request.Host, channels)); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,8 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
using ErsatzTV.Core.Hdhr; |
||||||
|
using MediatR; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Channels.Queries |
||||||
|
{ |
||||||
|
public record GetChannelLineup(string Scheme, string Host) : IRequest<List<LineupItem>>; |
||||||
|
} |
||||||
@ -0,0 +1,22 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
using System.Linq; |
||||||
|
using System.Threading; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Core.Hdhr; |
||||||
|
using ErsatzTV.Core.Interfaces.Repositories; |
||||||
|
using LanguageExt; |
||||||
|
using MediatR; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Channels.Queries |
||||||
|
{ |
||||||
|
public class GetChannelLineupHandler : IRequestHandler<GetChannelLineup, List<LineupItem>> |
||||||
|
{ |
||||||
|
private readonly IChannelRepository _channelRepository; |
||||||
|
|
||||||
|
public GetChannelLineupHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository; |
||||||
|
|
||||||
|
public Task<List<LineupItem>> Handle(GetChannelLineup request, CancellationToken cancellationToken) => |
||||||
|
_channelRepository.GetAll() |
||||||
|
.Map(channels => channels.Map(c => new LineupItem(request.Scheme, request.Host, c)).ToList()); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,7 @@ |
|||||||
|
using ErsatzTV.Core.Iptv; |
||||||
|
using MediatR; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Channels.Queries |
||||||
|
{ |
||||||
|
public record GetChannelPlaylist(string Scheme, string Host) : IRequest<ChannelPlaylist>; |
||||||
|
} |
||||||
@ -0,0 +1,21 @@ |
|||||||
|
using System.Threading; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Core.Interfaces.Repositories; |
||||||
|
using ErsatzTV.Core.Iptv; |
||||||
|
using LanguageExt; |
||||||
|
using MediatR; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Channels.Queries |
||||||
|
{ |
||||||
|
public class GetChannelPlaylistHandler : IRequestHandler<GetChannelPlaylist, ChannelPlaylist> |
||||||
|
{ |
||||||
|
private readonly IChannelRepository _channelRepository; |
||||||
|
|
||||||
|
public GetChannelPlaylistHandler(IChannelRepository channelRepository) => |
||||||
|
_channelRepository = channelRepository; |
||||||
|
|
||||||
|
public Task<ChannelPlaylist> Handle(GetChannelPlaylist request, CancellationToken cancellationToken) => |
||||||
|
_channelRepository.GetAll() |
||||||
|
.Map(channels => new ChannelPlaylist(request.Scheme, request.Host, channels)); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,18 @@ |
|||||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||||
|
|
||||||
|
<PropertyGroup> |
||||||
|
<TargetFramework>net5.0</TargetFramework> |
||||||
|
</PropertyGroup> |
||||||
|
|
||||||
|
<ItemGroup> |
||||||
|
<PackageReference Include="MediatR" Version="9.0.0" /> |
||||||
|
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="5.0.0" /> |
||||||
|
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> |
||||||
|
<PackageReference Include="Winista.MimeDetect" Version="1.0.1" /> |
||||||
|
</ItemGroup> |
||||||
|
|
||||||
|
<ItemGroup> |
||||||
|
<ProjectReference Include="..\ErsatzTV.Core\ErsatzTV.Core.csproj" /> |
||||||
|
</ItemGroup> |
||||||
|
|
||||||
|
</Project> |
||||||
@ -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) |
||||||
|
}; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,6 @@ |
|||||||
|
namespace ErsatzTV.Application |
||||||
|
{ |
||||||
|
public interface IBackgroundServiceRequest |
||||||
|
{ |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,6 @@ |
|||||||
|
namespace ErsatzTV.Application |
||||||
|
{ |
||||||
|
public interface IPlexBackgroundServiceRequest |
||||||
|
{ |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,9 @@ |
|||||||
|
using ErsatzTV.Core; |
||||||
|
using LanguageExt; |
||||||
|
using MediatR; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Images.Commands |
||||||
|
{ |
||||||
|
// ReSharper disable once SuggestBaseTypeForParameter
|
||||||
|
public record SaveImageToDisk(byte[] Buffer) : IRequest<Either<BaseError, string>>; |
||||||
|
} |
||||||
@ -0,0 +1,43 @@ |
|||||||
|
using System; |
||||||
|
using System.IO; |
||||||
|
using System.Security.Cryptography; |
||||||
|
using System.Threading; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Core; |
||||||
|
using LanguageExt; |
||||||
|
using MediatR; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Images.Commands |
||||||
|
{ |
||||||
|
public class SaveImageToDiskHandler : IRequestHandler<SaveImageToDisk, Either<BaseError, string>> |
||||||
|
{ |
||||||
|
private static readonly SHA1CryptoServiceProvider Crypto; |
||||||
|
|
||||||
|
static SaveImageToDiskHandler() => Crypto = new SHA1CryptoServiceProvider(); |
||||||
|
|
||||||
|
public async Task<Either<BaseError, string>> Handle( |
||||||
|
SaveImageToDisk request, |
||||||
|
CancellationToken cancellationToken) |
||||||
|
{ |
||||||
|
try |
||||||
|
{ |
||||||
|
byte[] hash = Crypto.ComputeHash(request.Buffer); |
||||||
|
string hex = BitConverter.ToString(hash).Replace("-", string.Empty); |
||||||
|
|
||||||
|
string fileName = Path.Combine(FileSystemLayout.ImageCacheFolder, hex); |
||||||
|
|
||||||
|
if (!Directory.Exists(FileSystemLayout.ImageCacheFolder)) |
||||||
|
{ |
||||||
|
Directory.CreateDirectory(FileSystemLayout.ImageCacheFolder); |
||||||
|
} |
||||||
|
|
||||||
|
await File.WriteAllBytesAsync(fileName, request.Buffer, cancellationToken); |
||||||
|
return hex; |
||||||
|
} |
||||||
|
catch (Exception ex) |
||||||
|
{ |
||||||
|
return BaseError.New(ex.Message); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,5 @@ |
|||||||
|
namespace ErsatzTV.Application.Images |
||||||
|
{ |
||||||
|
// ReSharper disable once SuggestBaseTypeForParameter
|
||||||
|
public record ImageViewModel(byte[] Contents, string MimeType); |
||||||
|
} |
||||||
@ -0,0 +1,8 @@ |
|||||||
|
using ErsatzTV.Core; |
||||||
|
using LanguageExt; |
||||||
|
using MediatR; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Images.Queries |
||||||
|
{ |
||||||
|
public record GetImageContents(string FileName) : IRequest<Either<BaseError, ImageViewModel>>; |
||||||
|
} |
||||||
@ -0,0 +1,44 @@ |
|||||||
|
using System; |
||||||
|
using System.IO; |
||||||
|
using System.Threading; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Core; |
||||||
|
using LanguageExt; |
||||||
|
using MediatR; |
||||||
|
using Microsoft.Extensions.Caching.Memory; |
||||||
|
using Winista.Mime; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Images.Queries |
||||||
|
{ |
||||||
|
public class GetImageContentsHandler : IRequestHandler<GetImageContents, Either<BaseError, ImageViewModel>> |
||||||
|
{ |
||||||
|
private static readonly MimeTypes MimeTypes = new(); |
||||||
|
private readonly IMemoryCache _memoryCache; |
||||||
|
|
||||||
|
public GetImageContentsHandler(IMemoryCache memoryCache) => _memoryCache = memoryCache; |
||||||
|
|
||||||
|
public async Task<Either<BaseError, ImageViewModel>> Handle( |
||||||
|
GetImageContents request, |
||||||
|
CancellationToken cancellationToken) |
||||||
|
{ |
||||||
|
try |
||||||
|
{ |
||||||
|
return await _memoryCache.GetOrCreateAsync( |
||||||
|
request.FileName, |
||||||
|
async entry => |
||||||
|
{ |
||||||
|
entry.SlidingExpiration = TimeSpan.FromHours(1); |
||||||
|
|
||||||
|
string fileName = Path.Combine(FileSystemLayout.ImageCacheFolder, request.FileName); |
||||||
|
byte[] contents = await File.ReadAllBytesAsync(fileName, cancellationToken); |
||||||
|
MimeType mimeType = MimeTypes.GetMimeType(contents); |
||||||
|
return new ImageViewModel(contents, mimeType.Name); |
||||||
|
}); |
||||||
|
} |
||||||
|
catch (Exception ex) |
||||||
|
{ |
||||||
|
return BaseError.New(ex.Message); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,9 @@ |
|||||||
|
using ErsatzTV.Core; |
||||||
|
using LanguageExt; |
||||||
|
using MediatR; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Commands |
||||||
|
{ |
||||||
|
public record CreateSimpleMediaCollection |
||||||
|
(string Name) : IRequest<Either<BaseError, MediaCollectionViewModel>>; |
||||||
|
} |
||||||
@ -0,0 +1,49 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
using System.Linq; |
||||||
|
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.MediaCollections.Mapper; |
||||||
|
using static LanguageExt.Prelude; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Commands |
||||||
|
{ |
||||||
|
public class CreateSimpleMediaCollectionHandler : IRequestHandler<CreateSimpleMediaCollection, |
||||||
|
Either<BaseError, MediaCollectionViewModel>> |
||||||
|
{ |
||||||
|
private readonly IMediaCollectionRepository _mediaCollectionRepository; |
||||||
|
|
||||||
|
public CreateSimpleMediaCollectionHandler(IMediaCollectionRepository mediaCollectionRepository) => |
||||||
|
_mediaCollectionRepository = mediaCollectionRepository; |
||||||
|
|
||||||
|
public Task<Either<BaseError, MediaCollectionViewModel>> Handle( |
||||||
|
CreateSimpleMediaCollection request, |
||||||
|
CancellationToken cancellationToken) => |
||||||
|
Validate(request).MapT(PersistCollection).Bind(v => v.ToEitherAsync()); |
||||||
|
|
||||||
|
private Task<MediaCollectionViewModel> PersistCollection(SimpleMediaCollection c) => |
||||||
|
_mediaCollectionRepository.Add(c).Map(ProjectToViewModel); |
||||||
|
|
||||||
|
private Task<Validation<BaseError, SimpleMediaCollection>> Validate(CreateSimpleMediaCollection request) => |
||||||
|
ValidateName(request).MapT(name => new SimpleMediaCollection { Name = name }); |
||||||
|
|
||||||
|
private async Task<Validation<BaseError, string>> ValidateName(CreateSimpleMediaCollection createCollection) |
||||||
|
{ |
||||||
|
List<string> allNames = await _mediaCollectionRepository.GetSimpleMediaCollections() |
||||||
|
.Map(list => list.Map(c => c.Name).ToList()); |
||||||
|
|
||||||
|
Validation<BaseError, string> result1 = createCollection.NotEmpty(c => c.Name) |
||||||
|
.Bind(_ => createCollection.NotLongerThan(50)(c => c.Name)); |
||||||
|
|
||||||
|
var result2 = Optional(createCollection.Name) |
||||||
|
.Filter(name => !allNames.Contains(name)) |
||||||
|
.ToValidation<BaseError>("Media collection name must be unique"); |
||||||
|
|
||||||
|
return (result1, result2).Apply((_, _) => createCollection.Name); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,9 @@ |
|||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Core; |
||||||
|
using LanguageExt; |
||||||
|
using MediatR; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Commands |
||||||
|
{ |
||||||
|
public record DeleteSimpleMediaCollection(int SimpleMediaCollectionId) : IRequest<Either<BaseError, Task>>; |
||||||
|
} |
||||||
@ -0,0 +1,34 @@ |
|||||||
|
using System.Threading; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Core; |
||||||
|
using ErsatzTV.Core.Interfaces.Repositories; |
||||||
|
using LanguageExt; |
||||||
|
using MediatR; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Commands |
||||||
|
{ |
||||||
|
public class |
||||||
|
DeleteSimpleMediaCollectionHandler : IRequestHandler<DeleteSimpleMediaCollection, Either<BaseError, Task>> |
||||||
|
{ |
||||||
|
private readonly IMediaCollectionRepository _mediaCollectionRepository; |
||||||
|
|
||||||
|
public DeleteSimpleMediaCollectionHandler(IMediaCollectionRepository mediaCollectionRepository) => |
||||||
|
_mediaCollectionRepository = mediaCollectionRepository; |
||||||
|
|
||||||
|
public async Task<Either<BaseError, Task>> Handle( |
||||||
|
DeleteSimpleMediaCollection request, |
||||||
|
CancellationToken cancellationToken) => |
||||||
|
(await SimpleMediaCollectionMustExist(request)) |
||||||
|
.Map(DoDeletion) |
||||||
|
.ToEither<Task>(); |
||||||
|
|
||||||
|
private Task DoDeletion(int mediaCollectionId) => _mediaCollectionRepository.Delete(mediaCollectionId); |
||||||
|
|
||||||
|
private async Task<Validation<BaseError, int>> SimpleMediaCollectionMustExist( |
||||||
|
DeleteSimpleMediaCollection deleteMediaCollection) => |
||||||
|
(await _mediaCollectionRepository.GetSimpleMediaCollection(deleteMediaCollection.SimpleMediaCollectionId)) |
||||||
|
.ToValidation<BaseError>( |
||||||
|
$"SimpleMediaCollection {deleteMediaCollection.SimpleMediaCollectionId} does not exist.") |
||||||
|
.Map(c => c.Id); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,11 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
using ErsatzTV.Application.MediaItems; |
||||||
|
using ErsatzTV.Core; |
||||||
|
using LanguageExt; |
||||||
|
using MediatR; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Commands |
||||||
|
{ |
||||||
|
public record ReplaceSimpleMediaCollectionItems |
||||||
|
(int MediaCollectionId, List<int> MediaItemIds) : IRequest<Either<BaseError, List<MediaItemViewModel>>>; |
||||||
|
} |
||||||
@ -0,0 +1,65 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
using System.Linq; |
||||||
|
using System.Threading; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Application.MediaItems; |
||||||
|
using ErsatzTV.Core; |
||||||
|
using ErsatzTV.Core.Domain; |
||||||
|
using ErsatzTV.Core.Interfaces.Repositories; |
||||||
|
using LanguageExt; |
||||||
|
using LanguageExt.UnsafeValueAccess; |
||||||
|
using MediatR; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Commands |
||||||
|
{ |
||||||
|
public class ReplaceSimpleMediaCollectionItemsHandler : IRequestHandler<ReplaceSimpleMediaCollectionItems, |
||||||
|
Either<BaseError, List<MediaItemViewModel>>> |
||||||
|
{ |
||||||
|
private readonly IMediaCollectionRepository _mediaCollectionRepository; |
||||||
|
private readonly IMediaItemRepository _mediaItemRepository; |
||||||
|
|
||||||
|
public ReplaceSimpleMediaCollectionItemsHandler( |
||||||
|
IMediaCollectionRepository mediaCollectionRepository, |
||||||
|
IMediaItemRepository mediaItemRepository) |
||||||
|
{ |
||||||
|
_mediaCollectionRepository = mediaCollectionRepository; |
||||||
|
_mediaItemRepository = mediaItemRepository; |
||||||
|
} |
||||||
|
|
||||||
|
public Task<Either<BaseError, List<MediaItemViewModel>>> Handle( |
||||||
|
ReplaceSimpleMediaCollectionItems request, |
||||||
|
CancellationToken cancellationToken) => |
||||||
|
Validate(request) |
||||||
|
.MapT(mediaItems => PersistItems(request, mediaItems)) |
||||||
|
.Bind(v => v.ToEitherAsync()); |
||||||
|
|
||||||
|
private async Task<List<MediaItemViewModel>> PersistItems( |
||||||
|
ReplaceSimpleMediaCollectionItems request, |
||||||
|
List<MediaItem> mediaItems) |
||||||
|
{ |
||||||
|
await _mediaCollectionRepository.ReplaceItems(request.MediaCollectionId, mediaItems); |
||||||
|
return mediaItems.Map(MediaItems.Mapper.ProjectToViewModel).ToList(); |
||||||
|
} |
||||||
|
|
||||||
|
private Task<Validation<BaseError, List<MediaItem>>> Validate(ReplaceSimpleMediaCollectionItems request) => |
||||||
|
MediaCollectionMustExist(request).BindT(_ => MediaItemsMustExist(request)); |
||||||
|
|
||||||
|
private async Task<Validation<BaseError, SimpleMediaCollection>> MediaCollectionMustExist( |
||||||
|
ReplaceSimpleMediaCollectionItems request) => |
||||||
|
(await _mediaCollectionRepository.GetSimpleMediaCollection(request.MediaCollectionId)) |
||||||
|
.ToValidation<BaseError>("[MediaCollectionId] does not exist."); |
||||||
|
|
||||||
|
private async Task<Validation<BaseError, List<MediaItem>>> MediaItemsMustExist( |
||||||
|
ReplaceSimpleMediaCollectionItems replaceItems) |
||||||
|
{ |
||||||
|
var allMediaItems = (await replaceItems.MediaItemIds.Map(i => _mediaItemRepository.Get(i)).Sequence()) |
||||||
|
.ToList(); |
||||||
|
if (allMediaItems.Any(x => x.IsNone)) |
||||||
|
{ |
||||||
|
return BaseError.New("[MediaItemId] does not exist"); |
||||||
|
} |
||||||
|
|
||||||
|
return allMediaItems.Sequence().ValueUnsafe().ToList(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,47 @@ |
|||||||
|
using System.Threading; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Core; |
||||||
|
using ErsatzTV.Core.Domain; |
||||||
|
using ErsatzTV.Core.Interfaces.Repositories; |
||||||
|
using LanguageExt; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Commands |
||||||
|
{ |
||||||
|
public class |
||||||
|
UpdateSimpleMediaCollectionHandler : MediatR.IRequestHandler<UpdateSimpleMediaCollection, |
||||||
|
Either<BaseError, Unit>> |
||||||
|
{ |
||||||
|
private readonly IMediaCollectionRepository _mediaCollectionRepository; |
||||||
|
|
||||||
|
public UpdateSimpleMediaCollectionHandler(IMediaCollectionRepository mediaCollectionRepository) => |
||||||
|
_mediaCollectionRepository = mediaCollectionRepository; |
||||||
|
|
||||||
|
public Task<Either<BaseError, Unit>> Handle( |
||||||
|
UpdateSimpleMediaCollection request, |
||||||
|
CancellationToken cancellationToken) => |
||||||
|
Validate(request) |
||||||
|
.MapT(c => ApplyUpdateRequest(c, request)) |
||||||
|
.Bind(v => v.ToEitherAsync()); |
||||||
|
|
||||||
|
private async Task<Unit> ApplyUpdateRequest(SimpleMediaCollection c, UpdateSimpleMediaCollection update) |
||||||
|
{ |
||||||
|
c.Name = update.Name; |
||||||
|
await _mediaCollectionRepository.Update(c); |
||||||
|
return Unit.Default; |
||||||
|
} |
||||||
|
|
||||||
|
private async Task<Validation<BaseError, SimpleMediaCollection>> |
||||||
|
Validate(UpdateSimpleMediaCollection request) => |
||||||
|
(await SimpleMediaCollectionMustExist(request), ValidateName(request)) |
||||||
|
.Apply((simpleMediaCollectionToUpdate, _) => simpleMediaCollectionToUpdate); |
||||||
|
|
||||||
|
private Task<Validation<BaseError, SimpleMediaCollection>> SimpleMediaCollectionMustExist( |
||||||
|
UpdateSimpleMediaCollection updateSimpleMediaCollection) => |
||||||
|
_mediaCollectionRepository.GetSimpleMediaCollection(updateSimpleMediaCollection.MediaCollectionId) |
||||||
|
.Map(v => v.ToValidation<BaseError>("SimpleMediaCollection does not exist.")); |
||||||
|
|
||||||
|
private Validation<BaseError, string> ValidateName(UpdateSimpleMediaCollection updateSimpleMediaCollection) => |
||||||
|
updateSimpleMediaCollection.NotEmpty(c => c.Name) |
||||||
|
.Bind(_ => updateSimpleMediaCollection.NotLongerThan(50)(c => c.Name)); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,8 @@ |
|||||||
|
using ErsatzTV.Core; |
||||||
|
using LanguageExt; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Commands |
||||||
|
{ |
||||||
|
public record UpdateSimpleMediaCollection |
||||||
|
(int MediaCollectionId, string Name) : MediatR.IRequest<Either<BaseError, Unit>>; |
||||||
|
} |
||||||
@ -0,0 +1,19 @@ |
|||||||
|
using ErsatzTV.Core.AggregateModels; |
||||||
|
using ErsatzTV.Core.Domain; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections |
||||||
|
{ |
||||||
|
internal static class Mapper |
||||||
|
{ |
||||||
|
internal static MediaCollectionViewModel ProjectToViewModel(MediaCollection mediaCollection) => |
||||||
|
new(mediaCollection.Id, mediaCollection.Name); |
||||||
|
|
||||||
|
internal static MediaCollectionSummaryViewModel ProjectToViewModel( |
||||||
|
MediaCollectionSummary mediaCollectionSummary) => |
||||||
|
new( |
||||||
|
mediaCollectionSummary.Id, |
||||||
|
mediaCollectionSummary.Name, |
||||||
|
mediaCollectionSummary.ItemCount, |
||||||
|
mediaCollectionSummary.IsSimple); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,4 @@ |
|||||||
|
namespace ErsatzTV.Application.MediaCollections |
||||||
|
{ |
||||||
|
public record MediaCollectionSummaryViewModel(int Id, string Name, int ItemCount, bool IsSimple); |
||||||
|
} |
||||||
@ -0,0 +1,4 @@ |
|||||||
|
namespace ErsatzTV.Application.MediaCollections |
||||||
|
{ |
||||||
|
public record MediaCollectionViewModel(int Id, string Name); |
||||||
|
} |
||||||
@ -0,0 +1,7 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
using MediatR; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Queries |
||||||
|
{ |
||||||
|
public record GetAllMediaCollections : IRequest<List<MediaCollectionViewModel>>; |
||||||
|
} |
||||||
@ -0,0 +1,24 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
using System.Linq; |
||||||
|
using System.Threading; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Core.Interfaces.Repositories; |
||||||
|
using LanguageExt; |
||||||
|
using MediatR; |
||||||
|
using static ErsatzTV.Application.MediaCollections.Mapper; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Queries |
||||||
|
{ |
||||||
|
public class GetAllMediaCollectionsHandler : IRequestHandler<GetAllMediaCollections, List<MediaCollectionViewModel>> |
||||||
|
{ |
||||||
|
private readonly IMediaCollectionRepository _mediaCollectionRepository; |
||||||
|
|
||||||
|
public GetAllMediaCollectionsHandler(IMediaCollectionRepository mediaCollectionRepository) => |
||||||
|
_mediaCollectionRepository = mediaCollectionRepository; |
||||||
|
|
||||||
|
public Task<List<MediaCollectionViewModel>> Handle( |
||||||
|
GetAllMediaCollections request, |
||||||
|
CancellationToken cancellationToken) => |
||||||
|
_mediaCollectionRepository.GetAll().Map(list => list.Map(ProjectToViewModel).ToList()); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,7 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
using MediatR; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Queries |
||||||
|
{ |
||||||
|
public record GetAllSimpleMediaCollections : IRequest<List<MediaCollectionViewModel>>; |
||||||
|
} |
||||||
@ -0,0 +1,25 @@ |
|||||||
|
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.MediaCollections.Mapper; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Queries |
||||||
|
{ |
||||||
|
public class |
||||||
|
GetAllSimpleMediaCollectionsHandler : IRequestHandler<GetAllSimpleMediaCollections, |
||||||
|
List<MediaCollectionViewModel>> |
||||||
|
{ |
||||||
|
private readonly IMediaCollectionRepository _mediaCollectionRepository; |
||||||
|
|
||||||
|
public GetAllSimpleMediaCollectionsHandler(IMediaCollectionRepository mediaCollectionRepository) => |
||||||
|
_mediaCollectionRepository = mediaCollectionRepository; |
||||||
|
|
||||||
|
public async Task<List<MediaCollectionViewModel>> Handle( |
||||||
|
GetAllSimpleMediaCollections request, |
||||||
|
CancellationToken cancellationToken) => |
||||||
|
(await _mediaCollectionRepository.GetSimpleMediaCollections()).Map(ProjectToViewModel).ToList(); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,7 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
using MediatR; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Queries |
||||||
|
{ |
||||||
|
public record GetMediaCollectionSummaries(string SearchString) : IRequest<List<MediaCollectionSummaryViewModel>>; |
||||||
|
} |
||||||
@ -0,0 +1,27 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
using System.Linq; |
||||||
|
using System.Threading; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Core.Interfaces.Repositories; |
||||||
|
using LanguageExt; |
||||||
|
using MediatR; |
||||||
|
using static ErsatzTV.Application.MediaCollections.Mapper; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Queries |
||||||
|
{ |
||||||
|
public class |
||||||
|
GetMediaCollectionSummariesHandler : IRequestHandler<GetMediaCollectionSummaries, |
||||||
|
List<MediaCollectionSummaryViewModel>> |
||||||
|
{ |
||||||
|
private readonly IMediaCollectionRepository _mediaCollectionRepository; |
||||||
|
|
||||||
|
public GetMediaCollectionSummariesHandler(IMediaCollectionRepository mediaCollectionRepository) => |
||||||
|
_mediaCollectionRepository = mediaCollectionRepository; |
||||||
|
|
||||||
|
public Task<List<MediaCollectionSummaryViewModel>> Handle( |
||||||
|
GetMediaCollectionSummaries request, |
||||||
|
CancellationToken cancellationToken) => |
||||||
|
_mediaCollectionRepository.GetSummaries(request.SearchString) |
||||||
|
.Map(list => list.Map(ProjectToViewModel).ToList()); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,7 @@ |
|||||||
|
using LanguageExt; |
||||||
|
using MediatR; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Queries |
||||||
|
{ |
||||||
|
public record GetSimpleMediaCollectionById(int Id) : IRequest<Option<MediaCollectionViewModel>>; |
||||||
|
} |
||||||
@ -0,0 +1,25 @@ |
|||||||
|
using System.Threading; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Core.Interfaces.Repositories; |
||||||
|
using LanguageExt; |
||||||
|
using MediatR; |
||||||
|
using static ErsatzTV.Application.MediaCollections.Mapper; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Queries |
||||||
|
{ |
||||||
|
public class |
||||||
|
GetSimpleMediaCollectionByIdHandler : IRequestHandler<GetSimpleMediaCollectionById, |
||||||
|
Option<MediaCollectionViewModel>> |
||||||
|
{ |
||||||
|
private readonly IMediaCollectionRepository _mediaCollectionRepository; |
||||||
|
|
||||||
|
public GetSimpleMediaCollectionByIdHandler(IMediaCollectionRepository mediaCollectionRepository) => |
||||||
|
_mediaCollectionRepository = mediaCollectionRepository; |
||||||
|
|
||||||
|
public Task<Option<MediaCollectionViewModel>> Handle( |
||||||
|
GetSimpleMediaCollectionById request, |
||||||
|
CancellationToken cancellationToken) => |
||||||
|
_mediaCollectionRepository.GetSimpleMediaCollection(request.Id) |
||||||
|
.MapT(ProjectToViewModel); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,9 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
using ErsatzTV.Application.MediaItems; |
||||||
|
using LanguageExt; |
||||||
|
using MediatR; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Queries |
||||||
|
{ |
||||||
|
public record GetSimpleMediaCollectionItems(int Id) : IRequest<Option<IEnumerable<MediaItemViewModel>>>; |
||||||
|
} |
||||||
@ -0,0 +1,26 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
using System.Threading; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Application.MediaItems; |
||||||
|
using ErsatzTV.Core.Interfaces.Repositories; |
||||||
|
using LanguageExt; |
||||||
|
using MediatR; |
||||||
|
using static ErsatzTV.Application.MediaItems.Mapper; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Queries |
||||||
|
{ |
||||||
|
public class GetSimpleMediaCollectionItemsHandler : IRequestHandler<GetSimpleMediaCollectionItems, |
||||||
|
Option<IEnumerable<MediaItemViewModel>>> |
||||||
|
{ |
||||||
|
private readonly IMediaCollectionRepository _mediaCollectionRepository; |
||||||
|
|
||||||
|
public GetSimpleMediaCollectionItemsHandler(IMediaCollectionRepository mediaCollectionRepository) => |
||||||
|
_mediaCollectionRepository = mediaCollectionRepository; |
||||||
|
|
||||||
|
public Task<Option<IEnumerable<MediaItemViewModel>>> Handle( |
||||||
|
GetSimpleMediaCollectionItems request, |
||||||
|
CancellationToken cancellationToken) => |
||||||
|
_mediaCollectionRepository.GetSimpleMediaCollectionItems(request.Id) |
||||||
|
.MapT(mediaItems => mediaItems.Map(ProjectToViewModel)); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,4 @@ |
|||||||
|
namespace ErsatzTV.Application.MediaItems |
||||||
|
{ |
||||||
|
public record AggregateMediaItemViewModel(string Source, string Title, int Count, string Duration); |
||||||
|
} |
||||||
@ -0,0 +1,8 @@ |
|||||||
|
using ErsatzTV.Core; |
||||||
|
using LanguageExt; |
||||||
|
using MediatR; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaItems.Commands |
||||||
|
{ |
||||||
|
public record CreateMediaItem(int MediaSourceId, string Path) : IRequest<Either<BaseError, MediaItemViewModel>>; |
||||||
|
} |
||||||
@ -0,0 +1,96 @@ |
|||||||
|
using System.IO; |
||||||
|
using System.Threading; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Core; |
||||||
|
using ErsatzTV.Core.Domain; |
||||||
|
using ErsatzTV.Core.Interfaces.Metadata; |
||||||
|
using ErsatzTV.Core.Interfaces.Repositories; |
||||||
|
using LanguageExt; |
||||||
|
using MediatR; |
||||||
|
using static LanguageExt.Prelude; |
||||||
|
using static ErsatzTV.Application.MediaItems.Mapper; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaItems.Commands |
||||||
|
{ |
||||||
|
public class CreateMediaItemHandler : IRequestHandler<CreateMediaItem, Either<BaseError, MediaItemViewModel>> |
||||||
|
{ |
||||||
|
private readonly IConfigElementRepository _configElementRepository; |
||||||
|
private readonly ILocalMetadataProvider _localMetadataProvider; |
||||||
|
private readonly ILocalStatisticsProvider _localStatisticsProvider; |
||||||
|
private readonly IMediaItemRepository _mediaItemRepository; |
||||||
|
private readonly IMediaSourceRepository _mediaSourceRepository; |
||||||
|
private readonly ISmartCollectionBuilder _smartCollectionBuilder; |
||||||
|
|
||||||
|
public CreateMediaItemHandler( |
||||||
|
IMediaItemRepository mediaItemRepository, |
||||||
|
IMediaSourceRepository mediaSourceRepository, |
||||||
|
IConfigElementRepository configElementRepository, |
||||||
|
ISmartCollectionBuilder smartCollectionBuilder, |
||||||
|
ILocalMetadataProvider localMetadataProvider, |
||||||
|
ILocalStatisticsProvider localStatisticsProvider) |
||||||
|
{ |
||||||
|
_mediaItemRepository = mediaItemRepository; |
||||||
|
_mediaSourceRepository = mediaSourceRepository; |
||||||
|
_configElementRepository = configElementRepository; |
||||||
|
_smartCollectionBuilder = smartCollectionBuilder; |
||||||
|
_localMetadataProvider = localMetadataProvider; |
||||||
|
_localStatisticsProvider = localStatisticsProvider; |
||||||
|
} |
||||||
|
|
||||||
|
public Task<Either<BaseError, MediaItemViewModel>> Handle( |
||||||
|
CreateMediaItem request, |
||||||
|
CancellationToken cancellationToken) => |
||||||
|
Validate(request) |
||||||
|
.MapT(PersistMediaItem) |
||||||
|
.Bind(v => v.ToEitherAsync()); |
||||||
|
|
||||||
|
private async Task<MediaItemViewModel> PersistMediaItem(RequestParameters parameters) |
||||||
|
{ |
||||||
|
await _mediaItemRepository.Add(parameters.MediaItem); |
||||||
|
|
||||||
|
await _localStatisticsProvider.RefreshStatistics(parameters.FFprobePath, parameters.MediaItem); |
||||||
|
await _localMetadataProvider.RefreshMetadata(parameters.MediaItem); |
||||||
|
await _smartCollectionBuilder.RefreshSmartCollections(parameters.MediaItem); |
||||||
|
|
||||||
|
return ProjectToViewModel(parameters.MediaItem); |
||||||
|
} |
||||||
|
|
||||||
|
private async Task<Validation<BaseError, RequestParameters>> Validate(CreateMediaItem request) => |
||||||
|
(await ValidateMediaSource(request), PathMustExist(request), await ValidateFFprobePath()) |
||||||
|
.Apply( |
||||||
|
(mediaSourceId, path, ffprobePath) => new RequestParameters( |
||||||
|
ffprobePath, |
||||||
|
new MediaItem |
||||||
|
{ |
||||||
|
MediaSourceId = mediaSourceId, |
||||||
|
Path = path |
||||||
|
})); |
||||||
|
|
||||||
|
private async Task<Validation<BaseError, int>> ValidateMediaSource(CreateMediaItem createMediaItem) => |
||||||
|
(await MediaSourceMustExist(createMediaItem)).Bind(MediaSourceMustBeLocal); |
||||||
|
|
||||||
|
private async Task<Validation<BaseError, MediaSource>> MediaSourceMustExist(CreateMediaItem createMediaItem) => |
||||||
|
(await _mediaSourceRepository.Get(createMediaItem.MediaSourceId)) |
||||||
|
.ToValidation<BaseError>($"[MediaSource] {createMediaItem.MediaSourceId} does not exist."); |
||||||
|
|
||||||
|
private Validation<BaseError, int> MediaSourceMustBeLocal(MediaSource mediaSource) => |
||||||
|
Some(mediaSource) |
||||||
|
.Filter(ms => ms is LocalMediaSource) |
||||||
|
.ToValidation<BaseError>($"[MediaSource] {mediaSource.Id} must be a local media source") |
||||||
|
.Map(ms => ms.Id); |
||||||
|
|
||||||
|
private Validation<BaseError, string> PathMustExist(CreateMediaItem createMediaItem) => |
||||||
|
Some(createMediaItem.Path) |
||||||
|
.Filter(File.Exists) |
||||||
|
.ToValidation<BaseError>("[Path] does not exist on the file system"); |
||||||
|
|
||||||
|
private Task<Validation<BaseError, string>> ValidateFFprobePath() => |
||||||
|
_configElementRepository.GetValue<string>(ConfigElementKey.FFprobePath) |
||||||
|
.FilterT(File.Exists) |
||||||
|
.Map( |
||||||
|
ffprobePath => |
||||||
|
ffprobePath.ToValidation<BaseError>("FFprobe path does not exist on the file system")); |
||||||
|
|
||||||
|
private record RequestParameters(string FFprobePath, MediaItem MediaItem); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,9 @@ |
|||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Core; |
||||||
|
using LanguageExt; |
||||||
|
using MediatR; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaItems.Commands |
||||||
|
{ |
||||||
|
public record DeleteMediaItem(int MediaItemId) : IRequest<Either<BaseError, Task>>; |
||||||
|
} |
||||||
@ -0,0 +1,31 @@ |
|||||||
|
using System.Threading; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Core; |
||||||
|
using ErsatzTV.Core.Interfaces.Repositories; |
||||||
|
using LanguageExt; |
||||||
|
using MediatR; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaItems.Commands |
||||||
|
{ |
||||||
|
public class DeleteMediaItemHandler : IRequestHandler<DeleteMediaItem, Either<BaseError, Task>> |
||||||
|
{ |
||||||
|
private readonly IMediaItemRepository _mediaItemRepository; |
||||||
|
|
||||||
|
public DeleteMediaItemHandler(IMediaItemRepository mediaItemRepository) => |
||||||
|
_mediaItemRepository = mediaItemRepository; |
||||||
|
|
||||||
|
public async Task<Either<BaseError, Task>> Handle( |
||||||
|
DeleteMediaItem request, |
||||||
|
CancellationToken cancellationToken) => |
||||||
|
(await MediaItemMustExist(request)) |
||||||
|
.Map(DoDeletion) |
||||||
|
.ToEither<Task>(); |
||||||
|
|
||||||
|
private Task DoDeletion(int mediaItemId) => _mediaItemRepository.Delete(mediaItemId); |
||||||
|
|
||||||
|
private async Task<Validation<BaseError, int>> MediaItemMustExist(DeleteMediaItem deleteMediaItem) => |
||||||
|
(await _mediaItemRepository.Get(deleteMediaItem.MediaItemId)) |
||||||
|
.ToValidation<BaseError>($"MediaItem {deleteMediaItem.MediaItemId} does not exist.") |
||||||
|
.Map(c => c.Id); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,8 @@ |
|||||||
|
using ErsatzTV.Core; |
||||||
|
using LanguageExt; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaItems.Commands |
||||||
|
{ |
||||||
|
public record RefreshMediaItem(int MediaItemId) : MediatR.IRequest<Either<BaseError, Unit>>, |
||||||
|
IBackgroundServiceRequest; |
||||||
|
} |
||||||
@ -0,0 +1,9 @@ |
|||||||
|
namespace ErsatzTV.Application.MediaItems.Commands |
||||||
|
{ |
||||||
|
public record RefreshMediaItemCollections : RefreshMediaItem |
||||||
|
{ |
||||||
|
public RefreshMediaItemCollections(int mediaItemId) : base(mediaItemId) |
||||||
|
{ |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,46 @@ |
|||||||
|
using System.Threading; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Core; |
||||||
|
using ErsatzTV.Core.Domain; |
||||||
|
using ErsatzTV.Core.Interfaces.Metadata; |
||||||
|
using ErsatzTV.Core.Interfaces.Repositories; |
||||||
|
using LanguageExt; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaItems.Commands |
||||||
|
{ |
||||||
|
public class |
||||||
|
RefreshMediaItemCollectionsHandler : MediatR.IRequestHandler<RefreshMediaItemCollections, |
||||||
|
Either<BaseError, Unit>> |
||||||
|
{ |
||||||
|
private readonly IMediaItemRepository _mediaItemRepository; |
||||||
|
private readonly ISmartCollectionBuilder _smartCollectionBuilder; |
||||||
|
|
||||||
|
public RefreshMediaItemCollectionsHandler( |
||||||
|
IMediaItemRepository mediaItemRepository, |
||||||
|
ISmartCollectionBuilder smartCollectionBuilder) |
||||||
|
{ |
||||||
|
_mediaItemRepository = mediaItemRepository; |
||||||
|
_smartCollectionBuilder = smartCollectionBuilder; |
||||||
|
} |
||||||
|
|
||||||
|
public Task<Either<BaseError, Unit>> Handle( |
||||||
|
RefreshMediaItemCollections request, |
||||||
|
CancellationToken cancellationToken) => |
||||||
|
Validate(request) |
||||||
|
.MapT(RefreshCollections) |
||||||
|
.Bind(v => v.ToEitherAsync()); |
||||||
|
|
||||||
|
private Task<Validation<BaseError, MediaItem>> Validate(RefreshMediaItemCollections request) => |
||||||
|
MediaItemMustExist(request); |
||||||
|
|
||||||
|
private Task<Validation<BaseError, MediaItem>> MediaItemMustExist( |
||||||
|
RefreshMediaItemCollections refreshMediaItemCollections) => |
||||||
|
_mediaItemRepository.Get(refreshMediaItemCollections.MediaItemId) |
||||||
|
.Map( |
||||||
|
maybeItem => maybeItem.ToValidation<BaseError>( |
||||||
|
$"[MediaItem] {refreshMediaItemCollections.MediaItemId} does not exist.")); |
||||||
|
|
||||||
|
private Task<Unit> RefreshCollections(MediaItem mediaItem) => |
||||||
|
_smartCollectionBuilder.RefreshSmartCollections(mediaItem).ToUnit(); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,9 @@ |
|||||||
|
namespace ErsatzTV.Application.MediaItems.Commands |
||||||
|
{ |
||||||
|
public record RefreshMediaItemMetadata : RefreshMediaItem |
||||||
|
{ |
||||||
|
public RefreshMediaItemMetadata(int mediaItemId) : base(mediaItemId) |
||||||
|
{ |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,52 @@ |
|||||||
|
using System.IO; |
||||||
|
using System.Threading; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Core; |
||||||
|
using ErsatzTV.Core.Domain; |
||||||
|
using ErsatzTV.Core.Interfaces.Metadata; |
||||||
|
using ErsatzTV.Core.Interfaces.Repositories; |
||||||
|
using LanguageExt; |
||||||
|
using static LanguageExt.Prelude; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaItems.Commands |
||||||
|
{ |
||||||
|
public class |
||||||
|
RefreshMediaItemMetadataHandler : MediatR.IRequestHandler<RefreshMediaItemMetadata, Either<BaseError, Unit>> |
||||||
|
{ |
||||||
|
private readonly ILocalMetadataProvider _localMetadataProvider; |
||||||
|
private readonly IMediaItemRepository _mediaItemRepository; |
||||||
|
|
||||||
|
public RefreshMediaItemMetadataHandler( |
||||||
|
IMediaItemRepository mediaItemRepository, |
||||||
|
ILocalMetadataProvider localMetadataProvider) |
||||||
|
{ |
||||||
|
_mediaItemRepository = mediaItemRepository; |
||||||
|
_localMetadataProvider = localMetadataProvider; |
||||||
|
} |
||||||
|
|
||||||
|
public Task<Either<BaseError, Unit>> Handle( |
||||||
|
RefreshMediaItemMetadata request, |
||||||
|
CancellationToken cancellationToken) => |
||||||
|
Validate(request) |
||||||
|
.MapT(RefreshMetadata) |
||||||
|
.Bind(v => v.ToEitherAsync()); |
||||||
|
|
||||||
|
private Task<Validation<BaseError, MediaItem>> Validate(RefreshMediaItemMetadata request) => |
||||||
|
MediaItemMustExist(request).BindT(PathMustExist); |
||||||
|
|
||||||
|
private Task<Validation<BaseError, MediaItem>> MediaItemMustExist( |
||||||
|
RefreshMediaItemMetadata refreshMediaItemMetadata) => |
||||||
|
_mediaItemRepository.Get(refreshMediaItemMetadata.MediaItemId) |
||||||
|
.Map( |
||||||
|
maybeItem => maybeItem.ToValidation<BaseError>( |
||||||
|
$"[MediaItem] {refreshMediaItemMetadata.MediaItemId} does not exist.")); |
||||||
|
|
||||||
|
private Validation<BaseError, MediaItem> PathMustExist(MediaItem mediaItem) => |
||||||
|
Some(mediaItem) |
||||||
|
.Filter(item => File.Exists(item.Path)) |
||||||
|
.ToValidation<BaseError>($"[Path] '{mediaItem.Path}' does not exist on the file system"); |
||||||
|
|
||||||
|
private Task<Unit> RefreshMetadata(MediaItem mediaItem) => |
||||||
|
_localMetadataProvider.RefreshMetadata(mediaItem).ToUnit(); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,9 @@ |
|||||||
|
namespace ErsatzTV.Application.MediaItems.Commands |
||||||
|
{ |
||||||
|
public record RefreshMediaItemStatistics : RefreshMediaItem |
||||||
|
{ |
||||||
|
public RefreshMediaItemStatistics(int mediaItemId) : base(mediaItemId) |
||||||
|
{ |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,65 @@ |
|||||||
|
using System.IO; |
||||||
|
using System.Threading; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Core; |
||||||
|
using ErsatzTV.Core.Domain; |
||||||
|
using ErsatzTV.Core.Interfaces.Metadata; |
||||||
|
using ErsatzTV.Core.Interfaces.Repositories; |
||||||
|
using LanguageExt; |
||||||
|
using static LanguageExt.Prelude; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaItems.Commands |
||||||
|
{ |
||||||
|
public class |
||||||
|
RefreshMediaItemStatisticsHandler : MediatR.IRequestHandler<RefreshMediaItemStatistics, Either<BaseError, Unit>> |
||||||
|
{ |
||||||
|
private readonly IConfigElementRepository _configElementRepository; |
||||||
|
private readonly ILocalStatisticsProvider _localStatisticsProvider; |
||||||
|
private readonly IMediaItemRepository _mediaItemRepository; |
||||||
|
|
||||||
|
public RefreshMediaItemStatisticsHandler( |
||||||
|
IMediaItemRepository mediaItemRepository, |
||||||
|
IConfigElementRepository configElementRepository, |
||||||
|
ILocalStatisticsProvider localStatisticsProvider) |
||||||
|
{ |
||||||
|
_mediaItemRepository = mediaItemRepository; |
||||||
|
_configElementRepository = configElementRepository; |
||||||
|
_localStatisticsProvider = localStatisticsProvider; |
||||||
|
} |
||||||
|
|
||||||
|
public Task<Either<BaseError, Unit>> Handle( |
||||||
|
RefreshMediaItemStatistics request, |
||||||
|
CancellationToken cancellationToken) => |
||||||
|
Validate(request) |
||||||
|
.MapT(RefreshStatistics) |
||||||
|
.Bind(v => v.ToEitherAsync()); |
||||||
|
|
||||||
|
private async Task<Validation<BaseError, RefreshParameters>> Validate(RefreshMediaItemStatistics request) => |
||||||
|
(await MediaItemMustExist(request).BindT(PathMustExist), await ValidateFFprobePath()) |
||||||
|
.Apply((mediaItem, ffprobePath) => new RefreshParameters(mediaItem, ffprobePath)); |
||||||
|
|
||||||
|
private Task<Validation<BaseError, MediaItem>> MediaItemMustExist( |
||||||
|
RefreshMediaItemStatistics refreshMediaItemStatistics) => |
||||||
|
_mediaItemRepository.Get(refreshMediaItemStatistics.MediaItemId) |
||||||
|
.Map( |
||||||
|
maybeItem => maybeItem.ToValidation<BaseError>( |
||||||
|
$"[MediaItem] {refreshMediaItemStatistics.MediaItemId} does not exist.")); |
||||||
|
|
||||||
|
private Validation<BaseError, MediaItem> PathMustExist(MediaItem mediaItem) => |
||||||
|
Some(mediaItem) |
||||||
|
.Filter(item => File.Exists(item.Path)) |
||||||
|
.ToValidation<BaseError>($"[Path] '{mediaItem.Path}' does not exist on the file system"); |
||||||
|
|
||||||
|
private Task<Validation<BaseError, string>> ValidateFFprobePath() => |
||||||
|
_configElementRepository.GetValue<string>(ConfigElementKey.FFprobePath) |
||||||
|
.FilterT(File.Exists) |
||||||
|
.Map( |
||||||
|
ffprobePath => |
||||||
|
ffprobePath.ToValidation<BaseError>("FFprobe path does not exist on the file system")); |
||||||
|
|
||||||
|
private Task<Unit> RefreshStatistics(RefreshParameters parameters) => |
||||||
|
_localStatisticsProvider.RefreshStatistics(parameters.FFprobePath, parameters.MediaItem).ToUnit(); |
||||||
|
|
||||||
|
private record RefreshParameters(MediaItem MediaItem, string FFprobePath); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,13 @@ |
|||||||
|
using ErsatzTV.Core.Domain; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaItems |
||||||
|
{ |
||||||
|
internal static class Mapper |
||||||
|
{ |
||||||
|
internal static MediaItemViewModel ProjectToViewModel(MediaItem mediaItem) => |
||||||
|
new( |
||||||
|
mediaItem.Id, |
||||||
|
mediaItem.MediaSourceId, |
||||||
|
mediaItem.Path); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,4 @@ |
|||||||
|
namespace ErsatzTV.Application.MediaItems |
||||||
|
{ |
||||||
|
public record MediaItemViewModel(int Id, int MediaSourceId, string Path); |
||||||
|
} |
||||||
@ -0,0 +1,9 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
using ErsatzTV.Core.Domain; |
||||||
|
using MediatR; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaItems.Queries |
||||||
|
{ |
||||||
|
public record GetAggregateMediaItems |
||||||
|
(MediaType MediaType, string SearchString) : IRequest<List<AggregateMediaItemViewModel>>; |
||||||
|
} |
||||||
@ -0,0 +1,43 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
using System.Linq; |
||||||
|
using System.Threading; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Core.Domain; |
||||||
|
using ErsatzTV.Core.Interfaces.Repositories; |
||||||
|
using MediatR; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaItems.Queries |
||||||
|
{ |
||||||
|
public class |
||||||
|
GetAggregateMediaItemsHandler : IRequestHandler<GetAggregateMediaItems, List<AggregateMediaItemViewModel>> |
||||||
|
{ |
||||||
|
private readonly IMediaItemRepository _mediaItemRepository; |
||||||
|
|
||||||
|
public GetAggregateMediaItemsHandler(IMediaItemRepository mediaItemRepository) => |
||||||
|
_mediaItemRepository = mediaItemRepository; |
||||||
|
|
||||||
|
public async Task<List<AggregateMediaItemViewModel>> Handle( |
||||||
|
GetAggregateMediaItems request, |
||||||
|
CancellationToken cancellationToken) |
||||||
|
{ |
||||||
|
IEnumerable<MediaItem> allItems = await _mediaItemRepository.GetAll(request.MediaType); |
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(request.SearchString)) |
||||||
|
{ |
||||||
|
allItems = allItems.Filter(i => i.Metadata?.Title.Contains(request.SearchString) == true); |
||||||
|
} |
||||||
|
|
||||||
|
return allItems.GroupBy(c => new { c.Source.Name, c.Metadata.Title }).Map( |
||||||
|
group => new AggregateMediaItemViewModel( |
||||||
|
group.Key.Name, |
||||||
|
group.Key.Title, |
||||||
|
group.Count(), |
||||||
|
group.Count() == 1 ? DisplayDuration(group.Head()) : string.Empty)) |
||||||
|
.ToList(); |
||||||
|
} |
||||||
|
|
||||||
|
private static string DisplayDuration(MediaItem mediaItem) => string.Format( |
||||||
|
mediaItem.Metadata?.Duration.TotalHours >= 1 ? @"{0:h\:mm\:ss}" : @"{0:mm\:ss}", |
||||||
|
mediaItem.Metadata?.Duration); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,7 @@ |
|||||||
|
using System.Collections.Generic; |
||||||
|
using MediatR; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaItems.Queries |
||||||
|
{ |
||||||
|
public record GetAllMediaItems : IRequest<List<MediaItemViewModel>>; |
||||||
|
} |
||||||
@ -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.MediaItems.Mapper; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaItems.Queries |
||||||
|
{ |
||||||
|
public class GetAllMediaItemsHandler : IRequestHandler<GetAllMediaItems, List<MediaItemViewModel>> |
||||||
|
{ |
||||||
|
private readonly IMediaItemRepository _mediaItemRepository; |
||||||
|
|
||||||
|
public GetAllMediaItemsHandler(IMediaItemRepository mediaItemRepository) => |
||||||
|
_mediaItemRepository = mediaItemRepository; |
||||||
|
|
||||||
|
public async Task<List<MediaItemViewModel>> Handle( |
||||||
|
GetAllMediaItems request, |
||||||
|
CancellationToken cancellationToken) => |
||||||
|
(await _mediaItemRepository.GetAll()).Map(ProjectToViewModel).ToList(); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,7 @@ |
|||||||
|
using LanguageExt; |
||||||
|
using MediatR; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaItems.Queries |
||||||
|
{ |
||||||
|
public record GetMediaItemById(int Id) : IRequest<Option<MediaItemViewModel>>; |
||||||
|
} |
||||||
@ -0,0 +1,23 @@ |
|||||||
|
using System.Threading; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Core.Interfaces.Repositories; |
||||||
|
using LanguageExt; |
||||||
|
using MediatR; |
||||||
|
using static ErsatzTV.Application.MediaItems.Mapper; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaItems.Queries |
||||||
|
{ |
||||||
|
public class GetMediaItemByIdHandler : IRequestHandler<GetMediaItemById, Option<MediaItemViewModel>> |
||||||
|
{ |
||||||
|
private readonly IMediaItemRepository _mediaItemRepository; |
||||||
|
|
||||||
|
public GetMediaItemByIdHandler(IMediaItemRepository mediaItemRepository) => |
||||||
|
_mediaItemRepository = mediaItemRepository; |
||||||
|
|
||||||
|
public Task<Option<MediaItemViewModel>> Handle( |
||||||
|
GetMediaItemById request, |
||||||
|
CancellationToken cancellationToken) => |
||||||
|
_mediaItemRepository.Get(request.Id) |
||||||
|
.MapT(ProjectToViewModel); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,10 @@ |
|||||||
|
using ErsatzTV.Core; |
||||||
|
using ErsatzTV.Core.Domain; |
||||||
|
using LanguageExt; |
||||||
|
using MediatR; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaSources.Commands |
||||||
|
{ |
||||||
|
public record CreateLocalMediaSource |
||||||
|
(string Name, MediaType MediaType, string Folder) : IRequest<Either<BaseError, MediaSourceViewModel>>; |
||||||
|
} |
||||||
@ -0,0 +1,78 @@ |
|||||||
|
using System; |
||||||
|
using System.Collections.Generic; |
||||||
|
using System.IO; |
||||||
|
using System.Linq; |
||||||
|
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.MediaSources.Mapper; |
||||||
|
using static LanguageExt.Prelude; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaSources.Commands |
||||||
|
{ |
||||||
|
public class CreateLocalMediaSourceHandler : IRequestHandler<CreateLocalMediaSource, |
||||||
|
Either<BaseError, MediaSourceViewModel>> |
||||||
|
{ |
||||||
|
private readonly IMediaSourceRepository _mediaSourceRepository; |
||||||
|
|
||||||
|
public CreateLocalMediaSourceHandler(IMediaSourceRepository mediaSourceRepository) => |
||||||
|
_mediaSourceRepository = mediaSourceRepository; |
||||||
|
|
||||||
|
public Task<Either<BaseError, MediaSourceViewModel>> Handle( |
||||||
|
CreateLocalMediaSource request, |
||||||
|
CancellationToken cancellationToken) => |
||||||
|
Validate(request).MapT(PersistLocalMediaSource).Bind(v => v.ToEitherAsync()); |
||||||
|
|
||||||
|
private Task<MediaSourceViewModel> PersistLocalMediaSource(LocalMediaSource c) => |
||||||
|
_mediaSourceRepository.Add(c).Map(ProjectToViewModel); |
||||||
|
|
||||||
|
private async Task<Validation<BaseError, LocalMediaSource>> Validate(CreateLocalMediaSource request) => |
||||||
|
(await ValidateName(request), await ValidateFolder(request)) |
||||||
|
.Apply( |
||||||
|
(name, folder) => |
||||||
|
new LocalMediaSource |
||||||
|
{ |
||||||
|
Name = name, |
||||||
|
MediaType = request.MediaType, |
||||||
|
Folder = folder |
||||||
|
}); |
||||||
|
|
||||||
|
private async Task<Validation<BaseError, string>> ValidateName(CreateLocalMediaSource createCollection) |
||||||
|
{ |
||||||
|
List<string> allNames = await _mediaSourceRepository.GetAll() |
||||||
|
.Map(list => list.Map(c => c.Name).ToList()); |
||||||
|
|
||||||
|
Validation<BaseError, string> result1 = createCollection.NotEmpty(c => c.Name) |
||||||
|
.Bind(_ => createCollection.NotLongerThan(50)(c => c.Name)); |
||||||
|
|
||||||
|
var result2 = Optional(createCollection.Name) |
||||||
|
.Filter(name => !allNames.Contains(name)) |
||||||
|
.ToValidation<BaseError>("Media source name must be unique"); |
||||||
|
|
||||||
|
return (result1, result2).Apply((_, _) => createCollection.Name); |
||||||
|
} |
||||||
|
|
||||||
|
private async Task<Validation<BaseError, string>> ValidateFolder(CreateLocalMediaSource createCollection) |
||||||
|
{ |
||||||
|
List<string> allFolders = await _mediaSourceRepository.GetAll() |
||||||
|
.Map(list => list.OfType<LocalMediaSource>().Map(c => c.Folder).ToList()); |
||||||
|
|
||||||
|
|
||||||
|
return Optional(createCollection.Folder) |
||||||
|
.Filter(folder => allFolders.ForAll(f => !AreSubPaths(f, folder))) |
||||||
|
.ToValidation<BaseError>("Folder must not belong to another media source"); |
||||||
|
} |
||||||
|
|
||||||
|
private static bool AreSubPaths(string path1, string path2) |
||||||
|
{ |
||||||
|
string one = path1 + Path.DirectorySeparatorChar; |
||||||
|
string two = path2 + Path.DirectorySeparatorChar; |
||||||
|
return one == two || one.StartsWith(two, StringComparison.OrdinalIgnoreCase) || |
||||||
|
two.StartsWith(one, StringComparison.OrdinalIgnoreCase); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,9 @@ |
|||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Core; |
||||||
|
using LanguageExt; |
||||||
|
using MediatR; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaSources.Commands |
||||||
|
{ |
||||||
|
public record DeleteLocalMediaSource(int LocalMediaSourceId) : IRequest<Either<BaseError, Task>>; |
||||||
|
} |
||||||
@ -0,0 +1,50 @@ |
|||||||
|
using System.Linq; |
||||||
|
using System.Threading; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Core; |
||||||
|
using ErsatzTV.Core.Domain; |
||||||
|
using ErsatzTV.Core.Interfaces.Repositories; |
||||||
|
using LanguageExt; |
||||||
|
using MediatR; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaSources.Commands |
||||||
|
{ |
||||||
|
public class |
||||||
|
DeleteLocalMediaSourceHandler : IRequestHandler<DeleteLocalMediaSource, Either<BaseError, Task>> |
||||||
|
{ |
||||||
|
private readonly IMediaCollectionRepository _mediaCollectionRepository; |
||||||
|
private readonly IMediaSourceRepository _mediaSourceRepository; |
||||||
|
|
||||||
|
public DeleteLocalMediaSourceHandler( |
||||||
|
IMediaSourceRepository mediaSourceRepository, |
||||||
|
IMediaCollectionRepository mediaCollectionRepository) |
||||||
|
{ |
||||||
|
_mediaSourceRepository = mediaSourceRepository; |
||||||
|
_mediaCollectionRepository = mediaCollectionRepository; |
||||||
|
} |
||||||
|
|
||||||
|
public async Task<Either<BaseError, Task>> Handle( |
||||||
|
DeleteLocalMediaSource request, |
||||||
|
CancellationToken cancellationToken) => |
||||||
|
(await MediaSourceMustExist(request)) |
||||||
|
.Map(DoDeletion) |
||||||
|
.ToEither<Task>(); |
||||||
|
|
||||||
|
private async Task DoDeletion(LocalMediaSource mediaSource) |
||||||
|
{ |
||||||
|
await _mediaSourceRepository.Delete(mediaSource.Id); |
||||||
|
if (mediaSource.MediaType == MediaType.TvShow) |
||||||
|
{ |
||||||
|
await _mediaCollectionRepository.DeleteEmptyTelevisionCollections(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private async Task<Validation<BaseError, LocalMediaSource>> MediaSourceMustExist( |
||||||
|
DeleteLocalMediaSource deleteMediaSource) => |
||||||
|
(await _mediaSourceRepository.Get(deleteMediaSource.LocalMediaSourceId)) |
||||||
|
.OfType<LocalMediaSource>() |
||||||
|
.HeadOrNone() |
||||||
|
.ToValidation<BaseError>( |
||||||
|
$"Local media source {deleteMediaSource.LocalMediaSourceId} does not exist."); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,9 @@ |
|||||||
|
using ErsatzTV.Core; |
||||||
|
using LanguageExt; |
||||||
|
using MediatR; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaSources.Commands |
||||||
|
{ |
||||||
|
public record ScanLocalMediaSource(int MediaSourceId) : IRequest<Either<BaseError, string>>, |
||||||
|
IBackgroundServiceRequest; |
||||||
|
} |
||||||
@ -0,0 +1,56 @@ |
|||||||
|
using System.IO; |
||||||
|
using System.Threading; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Core; |
||||||
|
using ErsatzTV.Core.Domain; |
||||||
|
using ErsatzTV.Core.Interfaces.Metadata; |
||||||
|
using ErsatzTV.Core.Interfaces.Repositories; |
||||||
|
using LanguageExt; |
||||||
|
using MediatR; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaSources.Commands |
||||||
|
{ |
||||||
|
public class ScanLocalMediaSourceHandler : IRequestHandler<ScanLocalMediaSource, Either<BaseError, string>> |
||||||
|
{ |
||||||
|
private readonly IConfigElementRepository _configElementRepository; |
||||||
|
private readonly ILocalMediaScanner _localMediaScanner; |
||||||
|
private readonly IMediaSourceRepository _mediaSourceRepository; |
||||||
|
|
||||||
|
public ScanLocalMediaSourceHandler( |
||||||
|
IMediaSourceRepository mediaSourceRepository, |
||||||
|
IConfigElementRepository configElementRepository, |
||||||
|
ILocalMediaScanner localMediaScanner) |
||||||
|
{ |
||||||
|
_mediaSourceRepository = mediaSourceRepository; |
||||||
|
_configElementRepository = configElementRepository; |
||||||
|
_localMediaScanner = localMediaScanner; |
||||||
|
} |
||||||
|
|
||||||
|
public Task<Either<BaseError, string>> |
||||||
|
Handle(ScanLocalMediaSource request, CancellationToken cancellationToken) => |
||||||
|
Validate(request) |
||||||
|
.MapT( |
||||||
|
p => _localMediaScanner.ScanLocalMediaSource(p.LocalMediaSource, p.FFprobePath) |
||||||
|
.Map(_ => p.LocalMediaSource.Name)) |
||||||
|
.Bind(v => v.ToEitherAsync()); |
||||||
|
|
||||||
|
private async Task<Validation<BaseError, RequestParameters>> Validate(ScanLocalMediaSource request) => |
||||||
|
(await LocalMediaSourceMustExist(request), await ValidateFFprobePath()) |
||||||
|
.Apply((localMediaSource, ffprobePath) => new RequestParameters(localMediaSource, ffprobePath)); |
||||||
|
|
||||||
|
private Task<Validation<BaseError, LocalMediaSource>> LocalMediaSourceMustExist( |
||||||
|
ScanLocalMediaSource request) => |
||||||
|
_mediaSourceRepository.Get(request.MediaSourceId) |
||||||
|
.Map(maybeMediaSource => maybeMediaSource.Map(ms => ms as LocalMediaSource)) |
||||||
|
.Map(v => v.ToValidation<BaseError>($"Local media source {request.MediaSourceId} does not exist.")); |
||||||
|
|
||||||
|
private Task<Validation<BaseError, string>> ValidateFFprobePath() => |
||||||
|
_configElementRepository.GetValue<string>(ConfigElementKey.FFprobePath) |
||||||
|
.FilterT(File.Exists) |
||||||
|
.Map( |
||||||
|
ffprobePath => |
||||||
|
ffprobePath.ToValidation<BaseError>("FFprobe path does not exist on the file system")); |
||||||
|
|
||||||
|
private record RequestParameters(LocalMediaSource LocalMediaSource, string FFprobePath); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,8 @@ |
|||||||
|
using ErsatzTV.Core; |
||||||
|
using LanguageExt; |
||||||
|
using MediatR; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaSources.Commands |
||||||
|
{ |
||||||
|
public record StartPlexPinFlow : IRequest<Either<BaseError, string>>; |
||||||
|
} |
||||||
@ -0,0 +1,38 @@ |
|||||||
|
using System.Threading; |
||||||
|
using System.Threading.Channels; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using ErsatzTV.Core; |
||||||
|
using ErsatzTV.Core.Interfaces.Plex; |
||||||
|
using LanguageExt; |
||||||
|
using MediatR; |
||||||
|
using static LanguageExt.Prelude; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaSources.Commands |
||||||
|
{ |
||||||
|
public class StartPlexPinFlowHandler : IRequestHandler<StartPlexPinFlow, Either<BaseError, string>> |
||||||
|
{ |
||||||
|
private readonly ChannelWriter<IPlexBackgroundServiceRequest> _channel; |
||||||
|
private readonly IPlexTvApiClient _plexTvApiClient; |
||||||
|
|
||||||
|
public StartPlexPinFlowHandler( |
||||||
|
IPlexTvApiClient plexTvApiClient, |
||||||
|
ChannelWriter<IPlexBackgroundServiceRequest> channel) |
||||||
|
{ |
||||||
|
_plexTvApiClient = plexTvApiClient; |
||||||
|
_channel = channel; |
||||||
|
} |
||||||
|
|
||||||
|
public Task<Either<BaseError, string>> Handle( |
||||||
|
StartPlexPinFlow request, |
||||||
|
CancellationToken cancellationToken) => |
||||||
|
_plexTvApiClient.StartPinFlow().Bind( |
||||||
|
result => result.Match( |
||||||
|
Left: error => Task.FromResult(Left<BaseError, string>(error)), |
||||||
|
Right: async pin => |
||||||
|
{ |
||||||
|
await _channel.WriteAsync(new TryCompletePlexPinFlow(pin), cancellationToken); |
||||||
|
return Right<BaseError, string>(pin.Url); |
||||||
|
}) |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,7 @@ |
|||||||
|
using ErsatzTV.Core; |
||||||
|
using LanguageExt; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaSources.Commands |
||||||
|
{ |
||||||
|
public record SynchronizePlexLibraries(int PlexMediaSourceId) : MediatR.IRequest<Either<BaseError, Unit>>; |
||||||
|
} |
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue