mirror of https://github.com/ErsatzTV/ErsatzTV.git
commit
4d52e115b5
493 changed files with 31399 additions and 0 deletions
@ -0,0 +1,18 @@
@@ -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 @@
@@ -0,0 +1,7 @@
|
||||
docs/ |
||||
sdk/ |
||||
**/.vs/ |
||||
**/bin/ |
||||
**/obj/ |
||||
.idea/ |
||||
Dockerfile |
@ -0,0 +1,81 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -0,0 +1,6 @@
|
||||
using MediatR; |
||||
|
||||
namespace ErsatzTV.Application.FFmpegProfiles.Commands |
||||
{ |
||||
public record UpdateFFmpegSettings(FFmpegSettingsViewModel Settings) : IRequest; |
||||
} |
@ -0,0 +1,70 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -0,0 +1,6 @@
|
||||
using MediatR; |
||||
|
||||
namespace ErsatzTV.Application.FFmpegProfiles.Queries |
||||
{ |
||||
public record GetFFmpegSettings : IRequest<FFmpegSettingsViewModel>; |
||||
} |
@ -0,0 +1,34 @@
@@ -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 @@
@@ -0,0 +1,6 @@
|
||||
namespace ErsatzTV.Application |
||||
{ |
||||
public interface IBackgroundServiceRequest |
||||
{ |
||||
} |
||||
} |
@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
|
||||
namespace ErsatzTV.Application |
||||
{ |
||||
public interface IPlexBackgroundServiceRequest |
||||
{ |
||||
} |
||||
} |
@ -0,0 +1,9 @@
@@ -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 @@
@@ -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 @@
@@ -0,0 +1,5 @@
|
||||
namespace ErsatzTV.Application.Images |
||||
{ |
||||
// ReSharper disable once SuggestBaseTypeForParameter
|
||||
public record ImageViewModel(byte[] Contents, string MimeType); |
||||
} |
@ -0,0 +1,8 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -0,0 +1,4 @@
|
||||
namespace ErsatzTV.Application.MediaCollections |
||||
{ |
||||
public record MediaCollectionSummaryViewModel(int Id, string Name, int ItemCount, bool IsSimple); |
||||
} |
@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
|
||||
namespace ErsatzTV.Application.MediaCollections |
||||
{ |
||||
public record MediaCollectionViewModel(int Id, string Name); |
||||
} |
@ -0,0 +1,7 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -0,0 +1,4 @@
|
||||
namespace ErsatzTV.Application.MediaItems |
||||
{ |
||||
public record AggregateMediaItemViewModel(string Source, string Title, int Count, string Duration); |
||||
} |
@ -0,0 +1,8 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -0,0 +1,9 @@
|
||||
namespace ErsatzTV.Application.MediaItems.Commands |
||||
{ |
||||
public record RefreshMediaItemCollections : RefreshMediaItem |
||||
{ |
||||
public RefreshMediaItemCollections(int mediaItemId) : base(mediaItemId) |
||||
{ |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,46 @@
@@ -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 @@
@@ -0,0 +1,9 @@
|
||||
namespace ErsatzTV.Application.MediaItems.Commands |
||||
{ |
||||
public record RefreshMediaItemMetadata : RefreshMediaItem |
||||
{ |
||||
public RefreshMediaItemMetadata(int mediaItemId) : base(mediaItemId) |
||||
{ |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,52 @@
@@ -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 @@
@@ -0,0 +1,9 @@
|
||||
namespace ErsatzTV.Application.MediaItems.Commands |
||||
{ |
||||
public record RefreshMediaItemStatistics : RefreshMediaItem |
||||
{ |
||||
public RefreshMediaItemStatistics(int mediaItemId) : base(mediaItemId) |
||||
{ |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,65 @@
@@ -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 @@
@@ -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 @@
@@ -0,0 +1,4 @@
|
||||
namespace ErsatzTV.Application.MediaItems |
||||
{ |
||||
public record MediaItemViewModel(int Id, int MediaSourceId, string Path); |
||||
} |
@ -0,0 +1,9 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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