Initial commit
This commit is contained in:
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"isRoot": true,
|
||||||
|
"tools": {
|
||||||
|
"jetbrains.resharper.globaltools": {
|
||||||
|
"version": "2020.3.2",
|
||||||
|
"commands": [
|
||||||
|
"jb"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"swashbuckle.aspnetcore.cli": {
|
||||||
|
"version": "5.6.2",
|
||||||
|
"commands": [
|
||||||
|
"swagger"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
docs/
|
||||||
|
sdk/
|
||||||
|
**/.vs/
|
||||||
|
**/bin/
|
||||||
|
**/obj/
|
||||||
|
.idea/
|
||||||
|
Dockerfile
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
|
||||||
|
[*]
|
||||||
|
charset=utf-8-bom
|
||||||
|
end_of_line=lf
|
||||||
|
trim_trailing_whitespace=false
|
||||||
|
insert_final_newline=false
|
||||||
|
indent_style=space
|
||||||
|
indent_size=4
|
||||||
|
|
||||||
|
# Microsoft .NET properties
|
||||||
|
csharp_new_line_before_members_in_object_initializers=false
|
||||||
|
csharp_preferred_modifier_order=public, private, protected, internal, new, abstract, virtual, sealed, override, static, readonly, extern, unsafe, volatile, async:suggestion
|
||||||
|
csharp_style_expression_bodied_accessors=true:suggestion
|
||||||
|
csharp_style_expression_bodied_constructors=true:none
|
||||||
|
csharp_style_expression_bodied_methods=true:none
|
||||||
|
csharp_style_expression_bodied_properties=true:suggestion
|
||||||
|
csharp_style_var_elsewhere=false:suggestion
|
||||||
|
csharp_style_var_for_built_in_types=false:suggestion
|
||||||
|
csharp_style_var_when_type_is_apparent=true:suggestion
|
||||||
|
dotnet_naming_rule.local_constants_rule.severity=warning
|
||||||
|
dotnet_naming_rule.local_constants_rule.style=all_upper_style
|
||||||
|
dotnet_naming_rule.local_constants_rule.symbols=local_constants_symbols
|
||||||
|
dotnet_naming_style.all_upper_style.capitalization=all_upper
|
||||||
|
dotnet_naming_style.all_upper_style.word_separator=_
|
||||||
|
dotnet_naming_symbols.local_constants_symbols.applicable_accessibilities=*
|
||||||
|
dotnet_naming_symbols.local_constants_symbols.applicable_kinds=local
|
||||||
|
dotnet_naming_symbols.local_constants_symbols.required_modifiers=const
|
||||||
|
dotnet_style_parentheses_in_arithmetic_binary_operators=never_if_unnecessary:none
|
||||||
|
dotnet_style_parentheses_in_other_binary_operators=never_if_unnecessary:none
|
||||||
|
dotnet_style_parentheses_in_relational_binary_operators=never_if_unnecessary:none
|
||||||
|
dotnet_style_predefined_type_for_locals_parameters_members=true:suggestion
|
||||||
|
dotnet_style_predefined_type_for_member_access=true:suggestion
|
||||||
|
dotnet_style_qualification_for_event=false:suggestion
|
||||||
|
dotnet_style_qualification_for_field=false:suggestion
|
||||||
|
dotnet_style_qualification_for_method=false:suggestion
|
||||||
|
dotnet_style_qualification_for_property=false:suggestion
|
||||||
|
dotnet_style_require_accessibility_modifiers=for_non_interface_members:suggestion
|
||||||
|
|
||||||
|
# ReSharper properties
|
||||||
|
resharper_autodetect_indent_settings=true
|
||||||
|
resharper_braces_for_for=required
|
||||||
|
resharper_braces_for_foreach=required
|
||||||
|
resharper_braces_for_ifelse=required
|
||||||
|
resharper_braces_for_while=required
|
||||||
|
resharper_csharp_insert_final_newline=true
|
||||||
|
resharper_csharp_max_attribute_length_for_same_line=0
|
||||||
|
resharper_csharp_place_accessorholder_attribute_on_same_line=never
|
||||||
|
resharper_csharp_place_field_attribute_on_same_line=if_owner_is_single_line
|
||||||
|
resharper_csharp_wrap_after_declaration_lpar=true
|
||||||
|
resharper_csharp_wrap_after_invocation_lpar=true
|
||||||
|
resharper_csharp_wrap_arguments_style=chop_if_long
|
||||||
|
resharper_csharp_wrap_parameters_style=chop_if_long
|
||||||
|
resharper_enforce_line_ending_style=true
|
||||||
|
resharper_for_built_in_types=use_var_when_evident
|
||||||
|
resharper_space_within_single_line_array_initializer_braces=true
|
||||||
|
resharper_use_indent_from_vs=false
|
||||||
|
resharper_wrap_array_initializer_style=chop_if_long
|
||||||
|
|
||||||
|
# ReSharper inspection severities
|
||||||
|
resharper_arrange_redundant_parentheses_highlighting=hint
|
||||||
|
resharper_arrange_this_qualifier_highlighting=hint
|
||||||
|
resharper_arrange_type_member_modifiers_highlighting=hint
|
||||||
|
resharper_arrange_type_modifiers_highlighting=hint
|
||||||
|
resharper_built_in_type_reference_style_for_member_access_highlighting=hint
|
||||||
|
resharper_built_in_type_reference_style_highlighting=hint
|
||||||
|
resharper_redundant_base_qualifier_highlighting=warning
|
||||||
|
resharper_suggest_var_or_type_built_in_types_highlighting=hint
|
||||||
|
resharper_suggest_var_or_type_elsewhere_highlighting=hint
|
||||||
|
resharper_suggest_var_or_type_simple_types_highlighting=hint
|
||||||
|
resharper_web_config_module_not_resolved_highlighting=warning
|
||||||
|
resharper_web_config_type_not_resolved_highlighting=warning
|
||||||
|
resharper_web_config_wrong_module_highlighting=warning
|
||||||
|
|
||||||
|
[{*.har,*.inputactions,*.jsb2,*.jsb3,*.json,.babelrc,.eslintrc,.stylelintrc,bowerrc,jest.config}]
|
||||||
|
indent_style=space
|
||||||
|
indent_size=2
|
||||||
|
|
||||||
|
[*.{appxmanifest,asax,ascx,aspx,build,cg,cginc,compute,cs,cshtml,dtd,fs,fsi,fsscript,fsx,hlsl,hlsli,hlslinc,master,ml,mli,nuspec,razor,resw,resx,shader,skin,usf,ush,vb,xaml,xamlx,xoml,xsd}]
|
||||||
|
indent_style=space
|
||||||
|
indent_size=4
|
||||||
|
tab_width=4
|
||||||
+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
|
||||||
+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 @@
|
|||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Channels
|
||||||
|
{
|
||||||
|
public record ChannelViewModel(
|
||||||
|
int Id,
|
||||||
|
int Number,
|
||||||
|
string Name,
|
||||||
|
int FFmpegProfileId,
|
||||||
|
string Logo,
|
||||||
|
StreamingMode StreamingMode);
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Channels.Commands
|
||||||
|
{
|
||||||
|
public record CreateChannel
|
||||||
|
(
|
||||||
|
string Name,
|
||||||
|
int Number,
|
||||||
|
int FFmpegProfileId,
|
||||||
|
string Logo,
|
||||||
|
StreamingMode StreamingMode) : IRequest<Either<BaseError, ChannelViewModel>>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
using static ErsatzTV.Application.Channels.Mapper;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Channels.Commands
|
||||||
|
{
|
||||||
|
public class CreateChannelHandler : IRequestHandler<CreateChannel, Either<BaseError, ChannelViewModel>>
|
||||||
|
{
|
||||||
|
private readonly IChannelRepository _channelRepository;
|
||||||
|
private readonly IFFmpegProfileRepository _ffmpegProfileRepository;
|
||||||
|
|
||||||
|
public CreateChannelHandler(
|
||||||
|
IChannelRepository channelRepository,
|
||||||
|
IFFmpegProfileRepository ffmpegProfileRepository)
|
||||||
|
{
|
||||||
|
_channelRepository = channelRepository;
|
||||||
|
_ffmpegProfileRepository = ffmpegProfileRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<Either<BaseError, ChannelViewModel>> Handle(
|
||||||
|
CreateChannel request,
|
||||||
|
CancellationToken cancellationToken) =>
|
||||||
|
Validate(request)
|
||||||
|
.MapT(PersistChannel)
|
||||||
|
.Bind(v => v.ToEitherAsync());
|
||||||
|
|
||||||
|
private Task<ChannelViewModel> PersistChannel(Channel c) =>
|
||||||
|
_channelRepository.Add(c).Map(ProjectToViewModel);
|
||||||
|
|
||||||
|
private async Task<Validation<BaseError, Channel>> Validate(CreateChannel request) =>
|
||||||
|
(ValidateName(request), ValidateNumber(request), await FFmpegProfileMustExist(request))
|
||||||
|
.Apply(
|
||||||
|
(name, number, ffmpegProfileId) => new Channel(Guid.NewGuid())
|
||||||
|
{
|
||||||
|
Name = name, Number = number, FFmpegProfileId = ffmpegProfileId,
|
||||||
|
StreamingMode = request.StreamingMode
|
||||||
|
});
|
||||||
|
|
||||||
|
private Validation<BaseError, string> ValidateName(CreateChannel createChannel) =>
|
||||||
|
createChannel.NotEmpty(c => c.Name)
|
||||||
|
.Bind(_ => createChannel.NotLongerThan(50)(c => c.Name));
|
||||||
|
|
||||||
|
// TODO: validate number does not exist?
|
||||||
|
private Validation<BaseError, int> ValidateNumber(CreateChannel createChannel) =>
|
||||||
|
createChannel.AtLeast(1)(c => c.Number);
|
||||||
|
|
||||||
|
private async Task<Validation<BaseError, int>> FFmpegProfileMustExist(CreateChannel createChannel) =>
|
||||||
|
(await _ffmpegProfileRepository.Get(createChannel.FFmpegProfileId))
|
||||||
|
.ToValidation<BaseError>($"FFmpegProfile {createChannel.FFmpegProfileId} does not exist.")
|
||||||
|
.Map(c => c.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Channels.Commands
|
||||||
|
{
|
||||||
|
public record DeleteChannel(int ChannelId) : IRequest<Either<BaseError, Task>>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Channels.Commands
|
||||||
|
{
|
||||||
|
public class DeleteChannelHandler : IRequestHandler<DeleteChannel, Either<BaseError, Task>>
|
||||||
|
{
|
||||||
|
private readonly IChannelRepository _channelRepository;
|
||||||
|
|
||||||
|
public DeleteChannelHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository;
|
||||||
|
|
||||||
|
public async Task<Either<BaseError, Task>> Handle(DeleteChannel request, CancellationToken cancellationToken) =>
|
||||||
|
(await ChannelMustExist(request))
|
||||||
|
.Map(DoDeletion)
|
||||||
|
.ToEither<Task>();
|
||||||
|
|
||||||
|
private Task DoDeletion(int channelId) => _channelRepository.Delete(channelId);
|
||||||
|
|
||||||
|
private async Task<Validation<BaseError, int>> ChannelMustExist(DeleteChannel deleteChannel) =>
|
||||||
|
(await _channelRepository.Get(deleteChannel.ChannelId))
|
||||||
|
.ToValidation<BaseError>($"Channel {deleteChannel.ChannelId} does not exist.")
|
||||||
|
.Map(c => c.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Channels.Commands
|
||||||
|
{
|
||||||
|
public record UpdateChannel
|
||||||
|
(
|
||||||
|
int ChannelId,
|
||||||
|
string Name,
|
||||||
|
int Number,
|
||||||
|
int FFmpegProfileId,
|
||||||
|
string Logo,
|
||||||
|
StreamingMode StreamingMode) : IRequest<Either<BaseError, ChannelViewModel>>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
using static ErsatzTV.Application.Channels.Mapper;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Channels.Commands
|
||||||
|
{
|
||||||
|
public class UpdateChannelHandler : IRequestHandler<UpdateChannel, Either<BaseError, ChannelViewModel>>
|
||||||
|
{
|
||||||
|
private readonly IChannelRepository _channelRepository;
|
||||||
|
|
||||||
|
public UpdateChannelHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository;
|
||||||
|
|
||||||
|
public Task<Either<BaseError, ChannelViewModel>> Handle(
|
||||||
|
UpdateChannel request,
|
||||||
|
CancellationToken cancellationToken) =>
|
||||||
|
Validate(request)
|
||||||
|
.MapT(c => ApplyUpdateRequest(c, request))
|
||||||
|
.Bind(v => v.ToEitherAsync());
|
||||||
|
|
||||||
|
private async Task<ChannelViewModel> ApplyUpdateRequest(Channel c, UpdateChannel update)
|
||||||
|
{
|
||||||
|
c.Name = update.Name;
|
||||||
|
c.Number = update.Number;
|
||||||
|
c.FFmpegProfileId = update.FFmpegProfileId;
|
||||||
|
c.Logo = update.Logo;
|
||||||
|
c.StreamingMode = update.StreamingMode;
|
||||||
|
await _channelRepository.Update(c);
|
||||||
|
return ProjectToViewModel(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Validation<BaseError, Channel>> Validate(UpdateChannel request) =>
|
||||||
|
(await ChannelMustExist(request), ValidateName(request), await ValidateNumber(request))
|
||||||
|
.Apply((channelToUpdate, _, _) => channelToUpdate);
|
||||||
|
|
||||||
|
private Task<Validation<BaseError, Channel>> ChannelMustExist(UpdateChannel updateChannel) =>
|
||||||
|
_channelRepository.Get(updateChannel.ChannelId)
|
||||||
|
.Map(v => v.ToValidation<BaseError>("Channel does not exist."));
|
||||||
|
|
||||||
|
private Validation<BaseError, string> ValidateName(UpdateChannel updateChannel) =>
|
||||||
|
updateChannel.NotEmpty(c => c.Name)
|
||||||
|
.Bind(_ => updateChannel.NotLongerThan(50)(c => c.Name));
|
||||||
|
|
||||||
|
private async Task<Validation<BaseError, int>> ValidateNumber(UpdateChannel updateChannel)
|
||||||
|
{
|
||||||
|
Option<Channel> match = await _channelRepository.GetByNumber(updateChannel.Number);
|
||||||
|
int matchId = match.Map(c => c.Id).IfNone(updateChannel.ChannelId);
|
||||||
|
if (matchId == updateChannel.ChannelId)
|
||||||
|
{
|
||||||
|
return updateChannel.AtLeast(1)(c => c.Number);
|
||||||
|
}
|
||||||
|
|
||||||
|
return BaseError.New("Channel number must be unique");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Channels
|
||||||
|
{
|
||||||
|
internal static class Mapper
|
||||||
|
{
|
||||||
|
internal static ChannelViewModel ProjectToViewModel(Channel channel) =>
|
||||||
|
new(channel.Id, channel.Number, channel.Name, channel.FFmpegProfileId, channel.Logo, channel.StreamingMode);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Channels.Queries
|
||||||
|
{
|
||||||
|
public record GetAllChannels : IRequest<List<ChannelViewModel>>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
using static ErsatzTV.Application.Channels.Mapper;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Channels.Queries
|
||||||
|
{
|
||||||
|
public class GetAllChannelsHandler : IRequestHandler<GetAllChannels, List<ChannelViewModel>>
|
||||||
|
{
|
||||||
|
private readonly IChannelRepository _channelRepository;
|
||||||
|
|
||||||
|
public GetAllChannelsHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository;
|
||||||
|
|
||||||
|
public Task<List<ChannelViewModel>> Handle(GetAllChannels request, CancellationToken cancellationToken) =>
|
||||||
|
_channelRepository.GetAll().Map(channels => channels.Map(ProjectToViewModel).ToList());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Channels.Queries
|
||||||
|
{
|
||||||
|
public record GetChannelById(int Id) : IRequest<Option<ChannelViewModel>>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
using static ErsatzTV.Application.Channels.Mapper;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Channels.Queries
|
||||||
|
{
|
||||||
|
public class GetChannelByIdHandler : IRequestHandler<GetChannelById, Option<ChannelViewModel>>
|
||||||
|
{
|
||||||
|
private readonly IChannelRepository _channelRepository;
|
||||||
|
|
||||||
|
public GetChannelByIdHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository;
|
||||||
|
|
||||||
|
public Task<Option<ChannelViewModel>> Handle(GetChannelById request, CancellationToken cancellationToken) =>
|
||||||
|
_channelRepository.Get(request.Id)
|
||||||
|
.MapT(ProjectToViewModel);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
using ErsatzTV.Core.Iptv;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Channels.Queries
|
||||||
|
{
|
||||||
|
public record GetChannelGuide(string Scheme, string Host) : IRequest<ChannelGuide>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using ErsatzTV.Core.Iptv;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Channels.Queries
|
||||||
|
{
|
||||||
|
public class GetChannelGuideHandler : IRequestHandler<GetChannelGuide, ChannelGuide>
|
||||||
|
{
|
||||||
|
private readonly IChannelRepository _channelRepository;
|
||||||
|
|
||||||
|
public GetChannelGuideHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository;
|
||||||
|
|
||||||
|
public Task<ChannelGuide> Handle(GetChannelGuide request, CancellationToken cancellationToken) =>
|
||||||
|
_channelRepository.GetAllForGuide()
|
||||||
|
.Map(channels => new ChannelGuide(request.Scheme, request.Host, channels));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using ErsatzTV.Core.Hdhr;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Channels.Queries
|
||||||
|
{
|
||||||
|
public record GetChannelLineup(string Scheme, string Host) : IRequest<List<LineupItem>>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core.Hdhr;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Channels.Queries
|
||||||
|
{
|
||||||
|
public class GetChannelLineupHandler : IRequestHandler<GetChannelLineup, List<LineupItem>>
|
||||||
|
{
|
||||||
|
private readonly IChannelRepository _channelRepository;
|
||||||
|
|
||||||
|
public GetChannelLineupHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository;
|
||||||
|
|
||||||
|
public Task<List<LineupItem>> Handle(GetChannelLineup request, CancellationToken cancellationToken) =>
|
||||||
|
_channelRepository.GetAll()
|
||||||
|
.Map(channels => channels.Map(c => new LineupItem(request.Scheme, request.Host, c)).ToList());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
using ErsatzTV.Core.Iptv;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Channels.Queries
|
||||||
|
{
|
||||||
|
public record GetChannelPlaylist(string Scheme, string Host) : IRequest<ChannelPlaylist>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using ErsatzTV.Core.Iptv;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Channels.Queries
|
||||||
|
{
|
||||||
|
public class GetChannelPlaylistHandler : IRequestHandler<GetChannelPlaylist, ChannelPlaylist>
|
||||||
|
{
|
||||||
|
private readonly IChannelRepository _channelRepository;
|
||||||
|
|
||||||
|
public GetChannelPlaylistHandler(IChannelRepository channelRepository) =>
|
||||||
|
_channelRepository = channelRepository;
|
||||||
|
|
||||||
|
public Task<ChannelPlaylist> Handle(GetChannelPlaylist request, CancellationToken cancellationToken) =>
|
||||||
|
_channelRepository.GetAll()
|
||||||
|
.Map(channels => new ChannelPlaylist(request.Scheme, request.Host, channels));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net5.0</TargetFramework>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="MediatR" Version="9.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="5.0.0" />
|
||||||
|
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||||
|
<PackageReference Include="Winista.MimeDetect" Version="1.0.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\ErsatzTV.Core\ErsatzTV.Core.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using ErsatzTV.Core;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||||
|
{
|
||||||
|
public record CreateFFmpegProfile(
|
||||||
|
string Name,
|
||||||
|
int ThreadCount,
|
||||||
|
bool Transcode,
|
||||||
|
int ResolutionId,
|
||||||
|
bool NormalizeResolution,
|
||||||
|
string VideoCodec,
|
||||||
|
bool NormalizeVideoCodec,
|
||||||
|
int VideoBitrate,
|
||||||
|
int VideoBufferSize,
|
||||||
|
string AudioCodec,
|
||||||
|
bool NormalizeAudioCodec,
|
||||||
|
int AudioBitrate,
|
||||||
|
int AudioBufferSize,
|
||||||
|
int AudioVolume,
|
||||||
|
int AudioChannels,
|
||||||
|
int AudioSampleRate,
|
||||||
|
bool NormalizeAudio) : IRequest<Either<BaseError, FFmpegProfileViewModel>>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
using static ErsatzTV.Application.FFmpegProfiles.Mapper;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||||
|
{
|
||||||
|
public class
|
||||||
|
CreateFFmpegProfileHandler : IRequestHandler<CreateFFmpegProfile, Either<BaseError, FFmpegProfileViewModel>>
|
||||||
|
{
|
||||||
|
private readonly IFFmpegProfileRepository _ffmpegProfileRepository;
|
||||||
|
private readonly IResolutionRepository _resolutionRepository;
|
||||||
|
|
||||||
|
public CreateFFmpegProfileHandler(
|
||||||
|
IFFmpegProfileRepository ffmpegProfileRepository,
|
||||||
|
IResolutionRepository resolutionRepository)
|
||||||
|
{
|
||||||
|
_ffmpegProfileRepository = ffmpegProfileRepository;
|
||||||
|
_resolutionRepository = resolutionRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<Either<BaseError, FFmpegProfileViewModel>> Handle(
|
||||||
|
CreateFFmpegProfile request,
|
||||||
|
CancellationToken cancellationToken) =>
|
||||||
|
Validate(request)
|
||||||
|
.MapT(PersistFFmpegProfile)
|
||||||
|
.Bind(v => v.ToEitherAsync());
|
||||||
|
|
||||||
|
private Task<FFmpegProfileViewModel> PersistFFmpegProfile(FFmpegProfile ffmpegProfile) =>
|
||||||
|
_ffmpegProfileRepository.Add(ffmpegProfile).Map(ProjectToViewModel);
|
||||||
|
|
||||||
|
private async Task<Validation<BaseError, FFmpegProfile>> Validate(CreateFFmpegProfile request) =>
|
||||||
|
(ValidateName(request), ValidateThreadCount(request), await ResolutionMustExist(request))
|
||||||
|
.Apply(
|
||||||
|
(name, threadCount, resolutionId) => new FFmpegProfile
|
||||||
|
{
|
||||||
|
Name = name,
|
||||||
|
ThreadCount = threadCount,
|
||||||
|
Transcode = request.Transcode,
|
||||||
|
ResolutionId = resolutionId,
|
||||||
|
NormalizeResolution = request.NormalizeResolution,
|
||||||
|
VideoCodec = request.VideoCodec,
|
||||||
|
NormalizeVideoCodec = request.NormalizeVideoCodec,
|
||||||
|
VideoBitrate = request.VideoBitrate,
|
||||||
|
VideoBufferSize = request.VideoBufferSize,
|
||||||
|
AudioCodec = request.AudioCodec,
|
||||||
|
NormalizeAudioCodec = request.NormalizeAudioCodec,
|
||||||
|
AudioBitrate = request.AudioBitrate,
|
||||||
|
AudioBufferSize = request.AudioBufferSize,
|
||||||
|
AudioVolume = request.AudioVolume,
|
||||||
|
AudioChannels = request.AudioChannels,
|
||||||
|
AudioSampleRate = request.AudioSampleRate,
|
||||||
|
NormalizeAudio = request.NormalizeAudio
|
||||||
|
});
|
||||||
|
|
||||||
|
private Validation<BaseError, string> ValidateName(CreateFFmpegProfile createFFmpegProfile) =>
|
||||||
|
createFFmpegProfile.NotEmpty(x => x.Name)
|
||||||
|
.Bind(_ => createFFmpegProfile.NotLongerThan(50)(x => x.Name));
|
||||||
|
|
||||||
|
private Validation<BaseError, int> ValidateThreadCount(CreateFFmpegProfile createFFmpegProfile) =>
|
||||||
|
createFFmpegProfile.AtLeast(1)(p => p.ThreadCount);
|
||||||
|
|
||||||
|
private async Task<Validation<BaseError, int>> ResolutionMustExist(CreateFFmpegProfile createFFmpegProfile) =>
|
||||||
|
(await _resolutionRepository.Get(createFFmpegProfile.ResolutionId))
|
||||||
|
.ToValidation<BaseError>($"[Resolution] {createFFmpegProfile.ResolutionId} does not exist.")
|
||||||
|
.Map(c => c.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||||
|
{
|
||||||
|
public record DeleteFFmpegProfile(int FFmpegProfileId) : IRequest<Either<BaseError, Task>>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||||
|
{
|
||||||
|
public class DeleteFFmpegProfileHandler : IRequestHandler<DeleteFFmpegProfile, Either<BaseError, Task>>
|
||||||
|
{
|
||||||
|
private readonly IFFmpegProfileRepository _ffmpegProfileRepository;
|
||||||
|
|
||||||
|
public DeleteFFmpegProfileHandler(IFFmpegProfileRepository ffmpegProfileRepository) =>
|
||||||
|
_ffmpegProfileRepository = ffmpegProfileRepository;
|
||||||
|
|
||||||
|
public async Task<Either<BaseError, Task>> Handle(
|
||||||
|
DeleteFFmpegProfile request,
|
||||||
|
CancellationToken cancellationToken) =>
|
||||||
|
(await FFmpegProfileMustExist(request))
|
||||||
|
.Map(DoDeletion)
|
||||||
|
.ToEither<Task>();
|
||||||
|
|
||||||
|
private Task DoDeletion(int channelId) => _ffmpegProfileRepository.Delete(channelId);
|
||||||
|
|
||||||
|
private async Task<Validation<BaseError, int>> FFmpegProfileMustExist(
|
||||||
|
DeleteFFmpegProfile deleteFFmpegProfile) =>
|
||||||
|
(await _ffmpegProfileRepository.Get(deleteFFmpegProfile.FFmpegProfileId))
|
||||||
|
.ToValidation<BaseError>($"FFmpegProfile {deleteFFmpegProfile.FFmpegProfileId} does not exist.")
|
||||||
|
.Map(c => c.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Requests a new ffmpeg profile (view model) that contains
|
||||||
|
/// appropriate default values.
|
||||||
|
/// </summary>
|
||||||
|
public record NewFFmpegProfile : IRequest<FFmpegProfileViewModel>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
using static LanguageExt.Prelude;
|
||||||
|
using static ErsatzTV.Application.FFmpegProfiles.Mapper;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||||
|
{
|
||||||
|
public class NewFFmpegProfileHandler : IRequestHandler<NewFFmpegProfile, FFmpegProfileViewModel>
|
||||||
|
{
|
||||||
|
private readonly IConfigElementRepository _configElementRepository;
|
||||||
|
private readonly IResolutionRepository _resolutionRepository;
|
||||||
|
|
||||||
|
public NewFFmpegProfileHandler(
|
||||||
|
IResolutionRepository resolutionRepository,
|
||||||
|
IConfigElementRepository configElementRepository)
|
||||||
|
{
|
||||||
|
_resolutionRepository = resolutionRepository;
|
||||||
|
_configElementRepository = configElementRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<FFmpegProfileViewModel> Handle(NewFFmpegProfile request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
int defaultResolutionId = await _configElementRepository
|
||||||
|
.GetValue<int>(ConfigElementKey.FFmpegDefaultResolutionId)
|
||||||
|
.IfNoneAsync(0);
|
||||||
|
|
||||||
|
List<Resolution> allResolutions = await _resolutionRepository.GetAll();
|
||||||
|
|
||||||
|
Option<Resolution> maybeDefaultResolution = allResolutions.Find(r => r.Id == defaultResolutionId);
|
||||||
|
Resolution defaultResolution = maybeDefaultResolution.Match(identity, () => allResolutions.Head());
|
||||||
|
|
||||||
|
return ProjectToViewModel(FFmpegProfile.New("New Profile", defaultResolution));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using ErsatzTV.Core;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||||
|
{
|
||||||
|
public record UpdateFFmpegProfile(
|
||||||
|
int FFmpegProfileId,
|
||||||
|
string Name,
|
||||||
|
int ThreadCount,
|
||||||
|
bool Transcode,
|
||||||
|
int ResolutionId,
|
||||||
|
bool NormalizeResolution,
|
||||||
|
string VideoCodec,
|
||||||
|
bool NormalizeVideoCodec,
|
||||||
|
int VideoBitrate,
|
||||||
|
int VideoBufferSize,
|
||||||
|
string AudioCodec,
|
||||||
|
bool NormalizeAudioCodec,
|
||||||
|
int AudioBitrate,
|
||||||
|
int AudioBufferSize,
|
||||||
|
int AudioVolume,
|
||||||
|
int AudioChannels,
|
||||||
|
int AudioSampleRate,
|
||||||
|
bool NormalizeAudio) : IRequest<Either<BaseError, FFmpegProfileViewModel>>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
using static ErsatzTV.Application.FFmpegProfiles.Mapper;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||||
|
{
|
||||||
|
public class
|
||||||
|
UpdateFFmpegProfileHandler : IRequestHandler<UpdateFFmpegProfile, Either<BaseError, FFmpegProfileViewModel>>
|
||||||
|
{
|
||||||
|
private readonly IFFmpegProfileRepository _ffmpegProfileRepository;
|
||||||
|
private readonly IResolutionRepository _resolutionRepository;
|
||||||
|
|
||||||
|
public UpdateFFmpegProfileHandler(
|
||||||
|
IFFmpegProfileRepository ffmpegProfileRepository,
|
||||||
|
IResolutionRepository resolutionRepository)
|
||||||
|
{
|
||||||
|
_ffmpegProfileRepository = ffmpegProfileRepository;
|
||||||
|
_resolutionRepository = resolutionRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<Either<BaseError, FFmpegProfileViewModel>> Handle(
|
||||||
|
UpdateFFmpegProfile request,
|
||||||
|
CancellationToken cancellationToken) =>
|
||||||
|
Validate(request)
|
||||||
|
.MapT(c => ApplyUpdateRequest(c, request))
|
||||||
|
.Bind(v => v.ToEitherAsync());
|
||||||
|
|
||||||
|
private async Task<FFmpegProfileViewModel> ApplyUpdateRequest(FFmpegProfile p, UpdateFFmpegProfile update)
|
||||||
|
{
|
||||||
|
p.Name = update.Name;
|
||||||
|
p.ThreadCount = update.ThreadCount;
|
||||||
|
p.Transcode = update.Transcode;
|
||||||
|
p.ResolutionId = update.ResolutionId;
|
||||||
|
p.NormalizeResolution = update.NormalizeResolution;
|
||||||
|
p.VideoCodec = update.VideoCodec;
|
||||||
|
p.NormalizeVideoCodec = update.NormalizeVideoCodec;
|
||||||
|
p.VideoBitrate = update.VideoBitrate;
|
||||||
|
p.VideoBufferSize = update.VideoBufferSize;
|
||||||
|
p.AudioCodec = update.AudioCodec;
|
||||||
|
p.NormalizeAudioCodec = update.NormalizeAudioCodec;
|
||||||
|
p.AudioBitrate = update.AudioBitrate;
|
||||||
|
p.AudioBufferSize = update.AudioBufferSize;
|
||||||
|
p.AudioVolume = update.AudioVolume;
|
||||||
|
p.AudioChannels = update.AudioChannels;
|
||||||
|
p.AudioSampleRate = update.AudioSampleRate;
|
||||||
|
p.NormalizeAudio = update.NormalizeAudio;
|
||||||
|
await _ffmpegProfileRepository.Update(p);
|
||||||
|
return ProjectToViewModel(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Validation<BaseError, FFmpegProfile>> Validate(UpdateFFmpegProfile request) =>
|
||||||
|
(await FFmpegProfileMustExist(request), ValidateName(request), ValidateThreadCount(request),
|
||||||
|
await ResolutionMustExist(request))
|
||||||
|
.Apply((ffmpegProfileToUpdate, _, _, _) => ffmpegProfileToUpdate);
|
||||||
|
|
||||||
|
private async Task<Validation<BaseError, FFmpegProfile>> FFmpegProfileMustExist(
|
||||||
|
UpdateFFmpegProfile updateFFmpegProfile) =>
|
||||||
|
(await _ffmpegProfileRepository.Get(updateFFmpegProfile.FFmpegProfileId))
|
||||||
|
.ToValidation<BaseError>("FFmpegProfile does not exist.");
|
||||||
|
|
||||||
|
private Validation<BaseError, string> ValidateName(UpdateFFmpegProfile updateFFmpegProfile) =>
|
||||||
|
updateFFmpegProfile.NotEmpty(x => x.Name)
|
||||||
|
.Bind(_ => updateFFmpegProfile.NotLongerThan(50)(x => x.Name));
|
||||||
|
|
||||||
|
private Validation<BaseError, int> ValidateThreadCount(UpdateFFmpegProfile updateFFmpegProfile) =>
|
||||||
|
updateFFmpegProfile.AtLeast(1)(p => p.ThreadCount);
|
||||||
|
|
||||||
|
private async Task<Validation<BaseError, int>> ResolutionMustExist(UpdateFFmpegProfile updateFFmpegProfile) =>
|
||||||
|
(await _resolutionRepository.Get(updateFFmpegProfile.ResolutionId))
|
||||||
|
.ToValidation<BaseError>($"[Resolution] {updateFFmpegProfile.ResolutionId} does not exist.")
|
||||||
|
.Map(c => c.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||||
|
{
|
||||||
|
public record UpdateFFmpegSettings(FFmpegSettingsViewModel Settings) : IRequest;
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
using Unit = MediatR.Unit;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||||
|
{
|
||||||
|
public class UpdateFFmpegSettingsHandler : IRequestHandler<UpdateFFmpegSettings>
|
||||||
|
{
|
||||||
|
private readonly IConfigElementRepository _configElementRepository;
|
||||||
|
|
||||||
|
public UpdateFFmpegSettingsHandler(IConfigElementRepository configElementRepository) =>
|
||||||
|
_configElementRepository = configElementRepository;
|
||||||
|
|
||||||
|
public async Task<Unit> Handle(UpdateFFmpegSettings request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Option<ConfigElement> ffmpegPath = await _configElementRepository.Get(ConfigElementKey.FFmpegPath);
|
||||||
|
Option<ConfigElement> ffprobePath = await _configElementRepository.Get(ConfigElementKey.FFprobePath);
|
||||||
|
Option<ConfigElement> defaultFFmpegProfileId =
|
||||||
|
await _configElementRepository.Get(ConfigElementKey.FFmpegDefaultProfileId);
|
||||||
|
|
||||||
|
ffmpegPath.Match(
|
||||||
|
ce =>
|
||||||
|
{
|
||||||
|
ce.Value = request.Settings.FFmpegPath;
|
||||||
|
_configElementRepository.Update(ce);
|
||||||
|
},
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
var ce = new ConfigElement
|
||||||
|
{ Key = ConfigElementKey.FFmpegPath.Key, Value = request.Settings.FFmpegPath };
|
||||||
|
_configElementRepository.Add(ce);
|
||||||
|
});
|
||||||
|
|
||||||
|
ffprobePath.Match(
|
||||||
|
ce =>
|
||||||
|
{
|
||||||
|
ce.Value = request.Settings.FFprobePath;
|
||||||
|
_configElementRepository.Update(ce);
|
||||||
|
},
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
var ce = new ConfigElement
|
||||||
|
{ Key = ConfigElementKey.FFprobePath.Key, Value = request.Settings.FFprobePath };
|
||||||
|
_configElementRepository.Add(ce);
|
||||||
|
});
|
||||||
|
|
||||||
|
defaultFFmpegProfileId.Match(
|
||||||
|
ce =>
|
||||||
|
{
|
||||||
|
ce.Value = request.Settings.DefaultFFmpegProfileId.ToString();
|
||||||
|
_configElementRepository.Update(ce);
|
||||||
|
},
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
var ce = new ConfigElement
|
||||||
|
{
|
||||||
|
Key = ConfigElementKey.FFmpegDefaultProfileId.Key,
|
||||||
|
Value = request.Settings.DefaultFFmpegProfileId.ToString()
|
||||||
|
};
|
||||||
|
_configElementRepository.Add(ce);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Unit.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using ErsatzTV.Application.Resolutions;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.FFmpegProfiles
|
||||||
|
{
|
||||||
|
public record FFmpegProfileViewModel(
|
||||||
|
int Id,
|
||||||
|
string Name,
|
||||||
|
int ThreadCount,
|
||||||
|
bool Transcode,
|
||||||
|
ResolutionViewModel Resolution,
|
||||||
|
bool NormalizeResolution,
|
||||||
|
string VideoCodec,
|
||||||
|
bool NormalizeVideoCodec,
|
||||||
|
int VideoBitrate,
|
||||||
|
int VideoBufferSize,
|
||||||
|
string AudioCodec,
|
||||||
|
bool NormalizeAudioCodec,
|
||||||
|
int AudioBitrate,
|
||||||
|
int AudioBufferSize,
|
||||||
|
int AudioVolume,
|
||||||
|
int AudioChannels,
|
||||||
|
int AudioSampleRate,
|
||||||
|
bool NormalizeAudio);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace ErsatzTV.Application.FFmpegProfiles
|
||||||
|
{
|
||||||
|
public class FFmpegSettingsViewModel
|
||||||
|
{
|
||||||
|
public string FFmpegPath { get; set; }
|
||||||
|
public string FFprobePath { get; set; }
|
||||||
|
public int DefaultFFmpegProfileId { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using ErsatzTV.Application.Resolutions;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.FFmpegProfiles
|
||||||
|
{
|
||||||
|
internal static class Mapper
|
||||||
|
{
|
||||||
|
internal static FFmpegProfileViewModel ProjectToViewModel(FFmpegProfile profile) =>
|
||||||
|
new(
|
||||||
|
profile.Id,
|
||||||
|
profile.Name,
|
||||||
|
profile.ThreadCount,
|
||||||
|
profile.Transcode,
|
||||||
|
Project(profile.Resolution),
|
||||||
|
profile.NormalizeResolution,
|
||||||
|
profile.VideoCodec,
|
||||||
|
profile.NormalizeVideoCodec,
|
||||||
|
profile.VideoBitrate,
|
||||||
|
profile.VideoBufferSize,
|
||||||
|
profile.AudioCodec,
|
||||||
|
profile.NormalizeAudioCodec,
|
||||||
|
profile.AudioBitrate,
|
||||||
|
profile.AudioBufferSize,
|
||||||
|
profile.AudioVolume,
|
||||||
|
profile.AudioChannels,
|
||||||
|
profile.AudioSampleRate,
|
||||||
|
profile.NormalizeAudio);
|
||||||
|
|
||||||
|
private static ResolutionViewModel Project(Resolution resolution) =>
|
||||||
|
new(resolution.Id, resolution.Name, resolution.Width, resolution.Height);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.FFmpegProfiles.Queries
|
||||||
|
{
|
||||||
|
public record GetAllFFmpegProfiles : IRequest<List<FFmpegProfileViewModel>>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using MediatR;
|
||||||
|
using static ErsatzTV.Application.FFmpegProfiles.Mapper;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.FFmpegProfiles.Queries
|
||||||
|
{
|
||||||
|
public class GetAllFFmpegProfilesHandler : IRequestHandler<GetAllFFmpegProfiles, List<FFmpegProfileViewModel>>
|
||||||
|
{
|
||||||
|
private readonly IFFmpegProfileRepository _ffmpegProfileRepository;
|
||||||
|
|
||||||
|
public GetAllFFmpegProfilesHandler(IFFmpegProfileRepository ffmpegProfileRepository) =>
|
||||||
|
_ffmpegProfileRepository = ffmpegProfileRepository;
|
||||||
|
|
||||||
|
public async Task<List<FFmpegProfileViewModel>> Handle(
|
||||||
|
GetAllFFmpegProfiles request,
|
||||||
|
CancellationToken cancellationToken) =>
|
||||||
|
(await _ffmpegProfileRepository.GetAll()).Map(ProjectToViewModel).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.FFmpegProfiles.Queries
|
||||||
|
{
|
||||||
|
public record GetFFmpegProfileById(int Id) : IRequest<Option<FFmpegProfileViewModel>>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
using static ErsatzTV.Application.FFmpegProfiles.Mapper;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.FFmpegProfiles.Queries
|
||||||
|
{
|
||||||
|
public class GetFFmpegProfileByIdHandler : IRequestHandler<GetFFmpegProfileById, Option<FFmpegProfileViewModel>>
|
||||||
|
{
|
||||||
|
private readonly IFFmpegProfileRepository _ffmpegProfileRepository;
|
||||||
|
|
||||||
|
public GetFFmpegProfileByIdHandler(IFFmpegProfileRepository ffmpegProfileRepository) =>
|
||||||
|
_ffmpegProfileRepository = ffmpegProfileRepository;
|
||||||
|
|
||||||
|
public Task<Option<FFmpegProfileViewModel>> Handle(
|
||||||
|
GetFFmpegProfileById request,
|
||||||
|
CancellationToken cancellationToken) =>
|
||||||
|
_ffmpegProfileRepository.Get(request.Id)
|
||||||
|
.MapT(ProjectToViewModel);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.FFmpegProfiles.Queries
|
||||||
|
{
|
||||||
|
public record GetFFmpegSettings : IRequest<FFmpegSettingsViewModel>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.FFmpegProfiles.Queries
|
||||||
|
{
|
||||||
|
public class GetFFmpegSettingsHandler : IRequestHandler<GetFFmpegSettings, FFmpegSettingsViewModel>
|
||||||
|
{
|
||||||
|
private readonly IConfigElementRepository _configElementRepository;
|
||||||
|
|
||||||
|
public GetFFmpegSettingsHandler(IConfigElementRepository configElementRepository) =>
|
||||||
|
_configElementRepository = configElementRepository;
|
||||||
|
|
||||||
|
public async Task<FFmpegSettingsViewModel> Handle(
|
||||||
|
GetFFmpegSettings request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Option<string> ffmpegPath = await _configElementRepository.GetValue<string>(ConfigElementKey.FFmpegPath);
|
||||||
|
Option<string> ffprobePath = await _configElementRepository.GetValue<string>(ConfigElementKey.FFprobePath);
|
||||||
|
Option<int> defaultFFmpegProfileId =
|
||||||
|
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegDefaultProfileId);
|
||||||
|
|
||||||
|
return new FFmpegSettingsViewModel
|
||||||
|
{
|
||||||
|
FFmpegPath = ffmpegPath.IfNone(string.Empty),
|
||||||
|
FFprobePath = ffprobePath.IfNone(string.Empty),
|
||||||
|
DefaultFFmpegProfileId = defaultFFmpegProfileId.IfNone(0)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace ErsatzTV.Application
|
||||||
|
{
|
||||||
|
public interface IBackgroundServiceRequest
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace ErsatzTV.Application
|
||||||
|
{
|
||||||
|
public interface IPlexBackgroundServiceRequest
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using ErsatzTV.Core;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Images.Commands
|
||||||
|
{
|
||||||
|
// ReSharper disable once SuggestBaseTypeForParameter
|
||||||
|
public record SaveImageToDisk(byte[] Buffer) : IRequest<Either<BaseError, string>>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Images.Commands
|
||||||
|
{
|
||||||
|
public class SaveImageToDiskHandler : IRequestHandler<SaveImageToDisk, Either<BaseError, string>>
|
||||||
|
{
|
||||||
|
private static readonly SHA1CryptoServiceProvider Crypto;
|
||||||
|
|
||||||
|
static SaveImageToDiskHandler() => Crypto = new SHA1CryptoServiceProvider();
|
||||||
|
|
||||||
|
public async Task<Either<BaseError, string>> Handle(
|
||||||
|
SaveImageToDisk request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
byte[] hash = Crypto.ComputeHash(request.Buffer);
|
||||||
|
string hex = BitConverter.ToString(hash).Replace("-", string.Empty);
|
||||||
|
|
||||||
|
string fileName = Path.Combine(FileSystemLayout.ImageCacheFolder, hex);
|
||||||
|
|
||||||
|
if (!Directory.Exists(FileSystemLayout.ImageCacheFolder))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(FileSystemLayout.ImageCacheFolder);
|
||||||
|
}
|
||||||
|
|
||||||
|
await File.WriteAllBytesAsync(fileName, request.Buffer, cancellationToken);
|
||||||
|
return hex;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return BaseError.New(ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
namespace ErsatzTV.Application.Images
|
||||||
|
{
|
||||||
|
// ReSharper disable once SuggestBaseTypeForParameter
|
||||||
|
public record ImageViewModel(byte[] Contents, string MimeType);
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using ErsatzTV.Core;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Images.Queries
|
||||||
|
{
|
||||||
|
public record GetImageContents(string FileName) : IRequest<Either<BaseError, ImageViewModel>>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using Winista.Mime;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Images.Queries
|
||||||
|
{
|
||||||
|
public class GetImageContentsHandler : IRequestHandler<GetImageContents, Either<BaseError, ImageViewModel>>
|
||||||
|
{
|
||||||
|
private static readonly MimeTypes MimeTypes = new();
|
||||||
|
private readonly IMemoryCache _memoryCache;
|
||||||
|
|
||||||
|
public GetImageContentsHandler(IMemoryCache memoryCache) => _memoryCache = memoryCache;
|
||||||
|
|
||||||
|
public async Task<Either<BaseError, ImageViewModel>> Handle(
|
||||||
|
GetImageContents request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _memoryCache.GetOrCreateAsync(
|
||||||
|
request.FileName,
|
||||||
|
async entry =>
|
||||||
|
{
|
||||||
|
entry.SlidingExpiration = TimeSpan.FromHours(1);
|
||||||
|
|
||||||
|
string fileName = Path.Combine(FileSystemLayout.ImageCacheFolder, request.FileName);
|
||||||
|
byte[] contents = await File.ReadAllBytesAsync(fileName, cancellationToken);
|
||||||
|
MimeType mimeType = MimeTypes.GetMimeType(contents);
|
||||||
|
return new ImageViewModel(contents, mimeType.Name);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return BaseError.New(ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using ErsatzTV.Core;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Commands
|
||||||
|
{
|
||||||
|
public record CreateSimpleMediaCollection
|
||||||
|
(string Name) : IRequest<Either<BaseError, MediaCollectionViewModel>>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
using static ErsatzTV.Application.MediaCollections.Mapper;
|
||||||
|
using static LanguageExt.Prelude;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Commands
|
||||||
|
{
|
||||||
|
public class CreateSimpleMediaCollectionHandler : IRequestHandler<CreateSimpleMediaCollection,
|
||||||
|
Either<BaseError, MediaCollectionViewModel>>
|
||||||
|
{
|
||||||
|
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||||
|
|
||||||
|
public CreateSimpleMediaCollectionHandler(IMediaCollectionRepository mediaCollectionRepository) =>
|
||||||
|
_mediaCollectionRepository = mediaCollectionRepository;
|
||||||
|
|
||||||
|
public Task<Either<BaseError, MediaCollectionViewModel>> Handle(
|
||||||
|
CreateSimpleMediaCollection request,
|
||||||
|
CancellationToken cancellationToken) =>
|
||||||
|
Validate(request).MapT(PersistCollection).Bind(v => v.ToEitherAsync());
|
||||||
|
|
||||||
|
private Task<MediaCollectionViewModel> PersistCollection(SimpleMediaCollection c) =>
|
||||||
|
_mediaCollectionRepository.Add(c).Map(ProjectToViewModel);
|
||||||
|
|
||||||
|
private Task<Validation<BaseError, SimpleMediaCollection>> Validate(CreateSimpleMediaCollection request) =>
|
||||||
|
ValidateName(request).MapT(name => new SimpleMediaCollection { Name = name });
|
||||||
|
|
||||||
|
private async Task<Validation<BaseError, string>> ValidateName(CreateSimpleMediaCollection createCollection)
|
||||||
|
{
|
||||||
|
List<string> allNames = await _mediaCollectionRepository.GetSimpleMediaCollections()
|
||||||
|
.Map(list => list.Map(c => c.Name).ToList());
|
||||||
|
|
||||||
|
Validation<BaseError, string> result1 = createCollection.NotEmpty(c => c.Name)
|
||||||
|
.Bind(_ => createCollection.NotLongerThan(50)(c => c.Name));
|
||||||
|
|
||||||
|
var result2 = Optional(createCollection.Name)
|
||||||
|
.Filter(name => !allNames.Contains(name))
|
||||||
|
.ToValidation<BaseError>("Media collection name must be unique");
|
||||||
|
|
||||||
|
return (result1, result2).Apply((_, _) => createCollection.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Commands
|
||||||
|
{
|
||||||
|
public record DeleteSimpleMediaCollection(int SimpleMediaCollectionId) : IRequest<Either<BaseError, Task>>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Commands
|
||||||
|
{
|
||||||
|
public class
|
||||||
|
DeleteSimpleMediaCollectionHandler : IRequestHandler<DeleteSimpleMediaCollection, Either<BaseError, Task>>
|
||||||
|
{
|
||||||
|
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||||
|
|
||||||
|
public DeleteSimpleMediaCollectionHandler(IMediaCollectionRepository mediaCollectionRepository) =>
|
||||||
|
_mediaCollectionRepository = mediaCollectionRepository;
|
||||||
|
|
||||||
|
public async Task<Either<BaseError, Task>> Handle(
|
||||||
|
DeleteSimpleMediaCollection request,
|
||||||
|
CancellationToken cancellationToken) =>
|
||||||
|
(await SimpleMediaCollectionMustExist(request))
|
||||||
|
.Map(DoDeletion)
|
||||||
|
.ToEither<Task>();
|
||||||
|
|
||||||
|
private Task DoDeletion(int mediaCollectionId) => _mediaCollectionRepository.Delete(mediaCollectionId);
|
||||||
|
|
||||||
|
private async Task<Validation<BaseError, int>> SimpleMediaCollectionMustExist(
|
||||||
|
DeleteSimpleMediaCollection deleteMediaCollection) =>
|
||||||
|
(await _mediaCollectionRepository.GetSimpleMediaCollection(deleteMediaCollection.SimpleMediaCollectionId))
|
||||||
|
.ToValidation<BaseError>(
|
||||||
|
$"SimpleMediaCollection {deleteMediaCollection.SimpleMediaCollectionId} does not exist.")
|
||||||
|
.Map(c => c.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using ErsatzTV.Application.MediaItems;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Commands
|
||||||
|
{
|
||||||
|
public record ReplaceSimpleMediaCollectionItems
|
||||||
|
(int MediaCollectionId, List<int> MediaItemIds) : IRequest<Either<BaseError, List<MediaItemViewModel>>>;
|
||||||
|
}
|
||||||
+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 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using LanguageExt;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Commands
|
||||||
|
{
|
||||||
|
public class
|
||||||
|
UpdateSimpleMediaCollectionHandler : MediatR.IRequestHandler<UpdateSimpleMediaCollection,
|
||||||
|
Either<BaseError, Unit>>
|
||||||
|
{
|
||||||
|
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||||
|
|
||||||
|
public UpdateSimpleMediaCollectionHandler(IMediaCollectionRepository mediaCollectionRepository) =>
|
||||||
|
_mediaCollectionRepository = mediaCollectionRepository;
|
||||||
|
|
||||||
|
public Task<Either<BaseError, Unit>> Handle(
|
||||||
|
UpdateSimpleMediaCollection request,
|
||||||
|
CancellationToken cancellationToken) =>
|
||||||
|
Validate(request)
|
||||||
|
.MapT(c => ApplyUpdateRequest(c, request))
|
||||||
|
.Bind(v => v.ToEitherAsync());
|
||||||
|
|
||||||
|
private async Task<Unit> ApplyUpdateRequest(SimpleMediaCollection c, UpdateSimpleMediaCollection update)
|
||||||
|
{
|
||||||
|
c.Name = update.Name;
|
||||||
|
await _mediaCollectionRepository.Update(c);
|
||||||
|
return Unit.Default;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Validation<BaseError, SimpleMediaCollection>>
|
||||||
|
Validate(UpdateSimpleMediaCollection request) =>
|
||||||
|
(await SimpleMediaCollectionMustExist(request), ValidateName(request))
|
||||||
|
.Apply((simpleMediaCollectionToUpdate, _) => simpleMediaCollectionToUpdate);
|
||||||
|
|
||||||
|
private Task<Validation<BaseError, SimpleMediaCollection>> SimpleMediaCollectionMustExist(
|
||||||
|
UpdateSimpleMediaCollection updateSimpleMediaCollection) =>
|
||||||
|
_mediaCollectionRepository.GetSimpleMediaCollection(updateSimpleMediaCollection.MediaCollectionId)
|
||||||
|
.Map(v => v.ToValidation<BaseError>("SimpleMediaCollection does not exist."));
|
||||||
|
|
||||||
|
private Validation<BaseError, string> ValidateName(UpdateSimpleMediaCollection updateSimpleMediaCollection) =>
|
||||||
|
updateSimpleMediaCollection.NotEmpty(c => c.Name)
|
||||||
|
.Bind(_ => updateSimpleMediaCollection.NotLongerThan(50)(c => c.Name));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using ErsatzTV.Core;
|
||||||
|
using LanguageExt;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Commands
|
||||||
|
{
|
||||||
|
public record UpdateSimpleMediaCollection
|
||||||
|
(int MediaCollectionId, string Name) : MediatR.IRequest<Either<BaseError, Unit>>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using ErsatzTV.Core.AggregateModels;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections
|
||||||
|
{
|
||||||
|
internal static class Mapper
|
||||||
|
{
|
||||||
|
internal static MediaCollectionViewModel ProjectToViewModel(MediaCollection mediaCollection) =>
|
||||||
|
new(mediaCollection.Id, mediaCollection.Name);
|
||||||
|
|
||||||
|
internal static MediaCollectionSummaryViewModel ProjectToViewModel(
|
||||||
|
MediaCollectionSummary mediaCollectionSummary) =>
|
||||||
|
new(
|
||||||
|
mediaCollectionSummary.Id,
|
||||||
|
mediaCollectionSummary.Name,
|
||||||
|
mediaCollectionSummary.ItemCount,
|
||||||
|
mediaCollectionSummary.IsSimple);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
namespace ErsatzTV.Application.MediaCollections
|
||||||
|
{
|
||||||
|
public record MediaCollectionSummaryViewModel(int Id, string Name, int ItemCount, bool IsSimple);
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
namespace ErsatzTV.Application.MediaCollections
|
||||||
|
{
|
||||||
|
public record MediaCollectionViewModel(int Id, string Name);
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Queries
|
||||||
|
{
|
||||||
|
public record GetAllMediaCollections : IRequest<List<MediaCollectionViewModel>>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
using static ErsatzTV.Application.MediaCollections.Mapper;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Queries
|
||||||
|
{
|
||||||
|
public class GetAllMediaCollectionsHandler : IRequestHandler<GetAllMediaCollections, List<MediaCollectionViewModel>>
|
||||||
|
{
|
||||||
|
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||||
|
|
||||||
|
public GetAllMediaCollectionsHandler(IMediaCollectionRepository mediaCollectionRepository) =>
|
||||||
|
_mediaCollectionRepository = mediaCollectionRepository;
|
||||||
|
|
||||||
|
public Task<List<MediaCollectionViewModel>> Handle(
|
||||||
|
GetAllMediaCollections request,
|
||||||
|
CancellationToken cancellationToken) =>
|
||||||
|
_mediaCollectionRepository.GetAll().Map(list => list.Map(ProjectToViewModel).ToList());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Queries
|
||||||
|
{
|
||||||
|
public record GetAllSimpleMediaCollections : IRequest<List<MediaCollectionViewModel>>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using MediatR;
|
||||||
|
using static ErsatzTV.Application.MediaCollections.Mapper;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Queries
|
||||||
|
{
|
||||||
|
public class
|
||||||
|
GetAllSimpleMediaCollectionsHandler : IRequestHandler<GetAllSimpleMediaCollections,
|
||||||
|
List<MediaCollectionViewModel>>
|
||||||
|
{
|
||||||
|
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||||
|
|
||||||
|
public GetAllSimpleMediaCollectionsHandler(IMediaCollectionRepository mediaCollectionRepository) =>
|
||||||
|
_mediaCollectionRepository = mediaCollectionRepository;
|
||||||
|
|
||||||
|
public async Task<List<MediaCollectionViewModel>> Handle(
|
||||||
|
GetAllSimpleMediaCollections request,
|
||||||
|
CancellationToken cancellationToken) =>
|
||||||
|
(await _mediaCollectionRepository.GetSimpleMediaCollections()).Map(ProjectToViewModel).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Queries
|
||||||
|
{
|
||||||
|
public record GetMediaCollectionSummaries(string SearchString) : IRequest<List<MediaCollectionSummaryViewModel>>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
using static ErsatzTV.Application.MediaCollections.Mapper;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Queries
|
||||||
|
{
|
||||||
|
public class
|
||||||
|
GetMediaCollectionSummariesHandler : IRequestHandler<GetMediaCollectionSummaries,
|
||||||
|
List<MediaCollectionSummaryViewModel>>
|
||||||
|
{
|
||||||
|
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||||
|
|
||||||
|
public GetMediaCollectionSummariesHandler(IMediaCollectionRepository mediaCollectionRepository) =>
|
||||||
|
_mediaCollectionRepository = mediaCollectionRepository;
|
||||||
|
|
||||||
|
public Task<List<MediaCollectionSummaryViewModel>> Handle(
|
||||||
|
GetMediaCollectionSummaries request,
|
||||||
|
CancellationToken cancellationToken) =>
|
||||||
|
_mediaCollectionRepository.GetSummaries(request.SearchString)
|
||||||
|
.Map(list => list.Map(ProjectToViewModel).ToList());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Queries
|
||||||
|
{
|
||||||
|
public record GetSimpleMediaCollectionById(int Id) : IRequest<Option<MediaCollectionViewModel>>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
using static ErsatzTV.Application.MediaCollections.Mapper;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Queries
|
||||||
|
{
|
||||||
|
public class
|
||||||
|
GetSimpleMediaCollectionByIdHandler : IRequestHandler<GetSimpleMediaCollectionById,
|
||||||
|
Option<MediaCollectionViewModel>>
|
||||||
|
{
|
||||||
|
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||||
|
|
||||||
|
public GetSimpleMediaCollectionByIdHandler(IMediaCollectionRepository mediaCollectionRepository) =>
|
||||||
|
_mediaCollectionRepository = mediaCollectionRepository;
|
||||||
|
|
||||||
|
public Task<Option<MediaCollectionViewModel>> Handle(
|
||||||
|
GetSimpleMediaCollectionById request,
|
||||||
|
CancellationToken cancellationToken) =>
|
||||||
|
_mediaCollectionRepository.GetSimpleMediaCollection(request.Id)
|
||||||
|
.MapT(ProjectToViewModel);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using ErsatzTV.Application.MediaItems;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Queries
|
||||||
|
{
|
||||||
|
public record GetSimpleMediaCollectionItems(int Id) : IRequest<Option<IEnumerable<MediaItemViewModel>>>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Application.MediaItems;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
using static ErsatzTV.Application.MediaItems.Mapper;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Queries
|
||||||
|
{
|
||||||
|
public class GetSimpleMediaCollectionItemsHandler : IRequestHandler<GetSimpleMediaCollectionItems,
|
||||||
|
Option<IEnumerable<MediaItemViewModel>>>
|
||||||
|
{
|
||||||
|
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||||
|
|
||||||
|
public GetSimpleMediaCollectionItemsHandler(IMediaCollectionRepository mediaCollectionRepository) =>
|
||||||
|
_mediaCollectionRepository = mediaCollectionRepository;
|
||||||
|
|
||||||
|
public Task<Option<IEnumerable<MediaItemViewModel>>> Handle(
|
||||||
|
GetSimpleMediaCollectionItems request,
|
||||||
|
CancellationToken cancellationToken) =>
|
||||||
|
_mediaCollectionRepository.GetSimpleMediaCollectionItems(request.Id)
|
||||||
|
.MapT(mediaItems => mediaItems.Map(ProjectToViewModel));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
namespace ErsatzTV.Application.MediaItems
|
||||||
|
{
|
||||||
|
public record AggregateMediaItemViewModel(string Source, string Title, int Count, string Duration);
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using ErsatzTV.Core;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaItems.Commands
|
||||||
|
{
|
||||||
|
public record CreateMediaItem(int MediaSourceId, string Path) : IRequest<Either<BaseError, MediaItemViewModel>>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Interfaces.Metadata;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
using static LanguageExt.Prelude;
|
||||||
|
using static ErsatzTV.Application.MediaItems.Mapper;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaItems.Commands
|
||||||
|
{
|
||||||
|
public class CreateMediaItemHandler : IRequestHandler<CreateMediaItem, Either<BaseError, MediaItemViewModel>>
|
||||||
|
{
|
||||||
|
private readonly IConfigElementRepository _configElementRepository;
|
||||||
|
private readonly ILocalMetadataProvider _localMetadataProvider;
|
||||||
|
private readonly ILocalStatisticsProvider _localStatisticsProvider;
|
||||||
|
private readonly IMediaItemRepository _mediaItemRepository;
|
||||||
|
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||||
|
private readonly ISmartCollectionBuilder _smartCollectionBuilder;
|
||||||
|
|
||||||
|
public CreateMediaItemHandler(
|
||||||
|
IMediaItemRepository mediaItemRepository,
|
||||||
|
IMediaSourceRepository mediaSourceRepository,
|
||||||
|
IConfigElementRepository configElementRepository,
|
||||||
|
ISmartCollectionBuilder smartCollectionBuilder,
|
||||||
|
ILocalMetadataProvider localMetadataProvider,
|
||||||
|
ILocalStatisticsProvider localStatisticsProvider)
|
||||||
|
{
|
||||||
|
_mediaItemRepository = mediaItemRepository;
|
||||||
|
_mediaSourceRepository = mediaSourceRepository;
|
||||||
|
_configElementRepository = configElementRepository;
|
||||||
|
_smartCollectionBuilder = smartCollectionBuilder;
|
||||||
|
_localMetadataProvider = localMetadataProvider;
|
||||||
|
_localStatisticsProvider = localStatisticsProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<Either<BaseError, MediaItemViewModel>> Handle(
|
||||||
|
CreateMediaItem request,
|
||||||
|
CancellationToken cancellationToken) =>
|
||||||
|
Validate(request)
|
||||||
|
.MapT(PersistMediaItem)
|
||||||
|
.Bind(v => v.ToEitherAsync());
|
||||||
|
|
||||||
|
private async Task<MediaItemViewModel> PersistMediaItem(RequestParameters parameters)
|
||||||
|
{
|
||||||
|
await _mediaItemRepository.Add(parameters.MediaItem);
|
||||||
|
|
||||||
|
await _localStatisticsProvider.RefreshStatistics(parameters.FFprobePath, parameters.MediaItem);
|
||||||
|
await _localMetadataProvider.RefreshMetadata(parameters.MediaItem);
|
||||||
|
await _smartCollectionBuilder.RefreshSmartCollections(parameters.MediaItem);
|
||||||
|
|
||||||
|
return ProjectToViewModel(parameters.MediaItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Validation<BaseError, RequestParameters>> Validate(CreateMediaItem request) =>
|
||||||
|
(await ValidateMediaSource(request), PathMustExist(request), await ValidateFFprobePath())
|
||||||
|
.Apply(
|
||||||
|
(mediaSourceId, path, ffprobePath) => new RequestParameters(
|
||||||
|
ffprobePath,
|
||||||
|
new MediaItem
|
||||||
|
{
|
||||||
|
MediaSourceId = mediaSourceId,
|
||||||
|
Path = path
|
||||||
|
}));
|
||||||
|
|
||||||
|
private async Task<Validation<BaseError, int>> ValidateMediaSource(CreateMediaItem createMediaItem) =>
|
||||||
|
(await MediaSourceMustExist(createMediaItem)).Bind(MediaSourceMustBeLocal);
|
||||||
|
|
||||||
|
private async Task<Validation<BaseError, MediaSource>> MediaSourceMustExist(CreateMediaItem createMediaItem) =>
|
||||||
|
(await _mediaSourceRepository.Get(createMediaItem.MediaSourceId))
|
||||||
|
.ToValidation<BaseError>($"[MediaSource] {createMediaItem.MediaSourceId} does not exist.");
|
||||||
|
|
||||||
|
private Validation<BaseError, int> MediaSourceMustBeLocal(MediaSource mediaSource) =>
|
||||||
|
Some(mediaSource)
|
||||||
|
.Filter(ms => ms is LocalMediaSource)
|
||||||
|
.ToValidation<BaseError>($"[MediaSource] {mediaSource.Id} must be a local media source")
|
||||||
|
.Map(ms => ms.Id);
|
||||||
|
|
||||||
|
private Validation<BaseError, string> PathMustExist(CreateMediaItem createMediaItem) =>
|
||||||
|
Some(createMediaItem.Path)
|
||||||
|
.Filter(File.Exists)
|
||||||
|
.ToValidation<BaseError>("[Path] does not exist on the file system");
|
||||||
|
|
||||||
|
private Task<Validation<BaseError, string>> ValidateFFprobePath() =>
|
||||||
|
_configElementRepository.GetValue<string>(ConfigElementKey.FFprobePath)
|
||||||
|
.FilterT(File.Exists)
|
||||||
|
.Map(
|
||||||
|
ffprobePath =>
|
||||||
|
ffprobePath.ToValidation<BaseError>("FFprobe path does not exist on the file system"));
|
||||||
|
|
||||||
|
private record RequestParameters(string FFprobePath, MediaItem MediaItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaItems.Commands
|
||||||
|
{
|
||||||
|
public record DeleteMediaItem(int MediaItemId) : IRequest<Either<BaseError, Task>>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaItems.Commands
|
||||||
|
{
|
||||||
|
public class DeleteMediaItemHandler : IRequestHandler<DeleteMediaItem, Either<BaseError, Task>>
|
||||||
|
{
|
||||||
|
private readonly IMediaItemRepository _mediaItemRepository;
|
||||||
|
|
||||||
|
public DeleteMediaItemHandler(IMediaItemRepository mediaItemRepository) =>
|
||||||
|
_mediaItemRepository = mediaItemRepository;
|
||||||
|
|
||||||
|
public async Task<Either<BaseError, Task>> Handle(
|
||||||
|
DeleteMediaItem request,
|
||||||
|
CancellationToken cancellationToken) =>
|
||||||
|
(await MediaItemMustExist(request))
|
||||||
|
.Map(DoDeletion)
|
||||||
|
.ToEither<Task>();
|
||||||
|
|
||||||
|
private Task DoDeletion(int mediaItemId) => _mediaItemRepository.Delete(mediaItemId);
|
||||||
|
|
||||||
|
private async Task<Validation<BaseError, int>> MediaItemMustExist(DeleteMediaItem deleteMediaItem) =>
|
||||||
|
(await _mediaItemRepository.Get(deleteMediaItem.MediaItemId))
|
||||||
|
.ToValidation<BaseError>($"MediaItem {deleteMediaItem.MediaItemId} does not exist.")
|
||||||
|
.Map(c => c.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using ErsatzTV.Core;
|
||||||
|
using LanguageExt;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaItems.Commands
|
||||||
|
{
|
||||||
|
public record RefreshMediaItem(int MediaItemId) : MediatR.IRequest<Either<BaseError, Unit>>,
|
||||||
|
IBackgroundServiceRequest;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace ErsatzTV.Application.MediaItems.Commands
|
||||||
|
{
|
||||||
|
public record RefreshMediaItemCollections : RefreshMediaItem
|
||||||
|
{
|
||||||
|
public RefreshMediaItemCollections(int mediaItemId) : base(mediaItemId)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Interfaces.Metadata;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using LanguageExt;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaItems.Commands
|
||||||
|
{
|
||||||
|
public class
|
||||||
|
RefreshMediaItemCollectionsHandler : MediatR.IRequestHandler<RefreshMediaItemCollections,
|
||||||
|
Either<BaseError, Unit>>
|
||||||
|
{
|
||||||
|
private readonly IMediaItemRepository _mediaItemRepository;
|
||||||
|
private readonly ISmartCollectionBuilder _smartCollectionBuilder;
|
||||||
|
|
||||||
|
public RefreshMediaItemCollectionsHandler(
|
||||||
|
IMediaItemRepository mediaItemRepository,
|
||||||
|
ISmartCollectionBuilder smartCollectionBuilder)
|
||||||
|
{
|
||||||
|
_mediaItemRepository = mediaItemRepository;
|
||||||
|
_smartCollectionBuilder = smartCollectionBuilder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<Either<BaseError, Unit>> Handle(
|
||||||
|
RefreshMediaItemCollections request,
|
||||||
|
CancellationToken cancellationToken) =>
|
||||||
|
Validate(request)
|
||||||
|
.MapT(RefreshCollections)
|
||||||
|
.Bind(v => v.ToEitherAsync());
|
||||||
|
|
||||||
|
private Task<Validation<BaseError, MediaItem>> Validate(RefreshMediaItemCollections request) =>
|
||||||
|
MediaItemMustExist(request);
|
||||||
|
|
||||||
|
private Task<Validation<BaseError, MediaItem>> MediaItemMustExist(
|
||||||
|
RefreshMediaItemCollections refreshMediaItemCollections) =>
|
||||||
|
_mediaItemRepository.Get(refreshMediaItemCollections.MediaItemId)
|
||||||
|
.Map(
|
||||||
|
maybeItem => maybeItem.ToValidation<BaseError>(
|
||||||
|
$"[MediaItem] {refreshMediaItemCollections.MediaItemId} does not exist."));
|
||||||
|
|
||||||
|
private Task<Unit> RefreshCollections(MediaItem mediaItem) =>
|
||||||
|
_smartCollectionBuilder.RefreshSmartCollections(mediaItem).ToUnit();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace ErsatzTV.Application.MediaItems.Commands
|
||||||
|
{
|
||||||
|
public record RefreshMediaItemMetadata : RefreshMediaItem
|
||||||
|
{
|
||||||
|
public RefreshMediaItemMetadata(int mediaItemId) : base(mediaItemId)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Interfaces.Metadata;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using LanguageExt;
|
||||||
|
using static LanguageExt.Prelude;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaItems.Commands
|
||||||
|
{
|
||||||
|
public class
|
||||||
|
RefreshMediaItemMetadataHandler : MediatR.IRequestHandler<RefreshMediaItemMetadata, Either<BaseError, Unit>>
|
||||||
|
{
|
||||||
|
private readonly ILocalMetadataProvider _localMetadataProvider;
|
||||||
|
private readonly IMediaItemRepository _mediaItemRepository;
|
||||||
|
|
||||||
|
public RefreshMediaItemMetadataHandler(
|
||||||
|
IMediaItemRepository mediaItemRepository,
|
||||||
|
ILocalMetadataProvider localMetadataProvider)
|
||||||
|
{
|
||||||
|
_mediaItemRepository = mediaItemRepository;
|
||||||
|
_localMetadataProvider = localMetadataProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<Either<BaseError, Unit>> Handle(
|
||||||
|
RefreshMediaItemMetadata request,
|
||||||
|
CancellationToken cancellationToken) =>
|
||||||
|
Validate(request)
|
||||||
|
.MapT(RefreshMetadata)
|
||||||
|
.Bind(v => v.ToEitherAsync());
|
||||||
|
|
||||||
|
private Task<Validation<BaseError, MediaItem>> Validate(RefreshMediaItemMetadata request) =>
|
||||||
|
MediaItemMustExist(request).BindT(PathMustExist);
|
||||||
|
|
||||||
|
private Task<Validation<BaseError, MediaItem>> MediaItemMustExist(
|
||||||
|
RefreshMediaItemMetadata refreshMediaItemMetadata) =>
|
||||||
|
_mediaItemRepository.Get(refreshMediaItemMetadata.MediaItemId)
|
||||||
|
.Map(
|
||||||
|
maybeItem => maybeItem.ToValidation<BaseError>(
|
||||||
|
$"[MediaItem] {refreshMediaItemMetadata.MediaItemId} does not exist."));
|
||||||
|
|
||||||
|
private Validation<BaseError, MediaItem> PathMustExist(MediaItem mediaItem) =>
|
||||||
|
Some(mediaItem)
|
||||||
|
.Filter(item => File.Exists(item.Path))
|
||||||
|
.ToValidation<BaseError>($"[Path] '{mediaItem.Path}' does not exist on the file system");
|
||||||
|
|
||||||
|
private Task<Unit> RefreshMetadata(MediaItem mediaItem) =>
|
||||||
|
_localMetadataProvider.RefreshMetadata(mediaItem).ToUnit();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace ErsatzTV.Application.MediaItems.Commands
|
||||||
|
{
|
||||||
|
public record RefreshMediaItemStatistics : RefreshMediaItem
|
||||||
|
{
|
||||||
|
public RefreshMediaItemStatistics(int mediaItemId) : base(mediaItemId)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Interfaces.Metadata;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using LanguageExt;
|
||||||
|
using static LanguageExt.Prelude;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaItems.Commands
|
||||||
|
{
|
||||||
|
public class
|
||||||
|
RefreshMediaItemStatisticsHandler : MediatR.IRequestHandler<RefreshMediaItemStatistics, Either<BaseError, Unit>>
|
||||||
|
{
|
||||||
|
private readonly IConfigElementRepository _configElementRepository;
|
||||||
|
private readonly ILocalStatisticsProvider _localStatisticsProvider;
|
||||||
|
private readonly IMediaItemRepository _mediaItemRepository;
|
||||||
|
|
||||||
|
public RefreshMediaItemStatisticsHandler(
|
||||||
|
IMediaItemRepository mediaItemRepository,
|
||||||
|
IConfigElementRepository configElementRepository,
|
||||||
|
ILocalStatisticsProvider localStatisticsProvider)
|
||||||
|
{
|
||||||
|
_mediaItemRepository = mediaItemRepository;
|
||||||
|
_configElementRepository = configElementRepository;
|
||||||
|
_localStatisticsProvider = localStatisticsProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<Either<BaseError, Unit>> Handle(
|
||||||
|
RefreshMediaItemStatistics request,
|
||||||
|
CancellationToken cancellationToken) =>
|
||||||
|
Validate(request)
|
||||||
|
.MapT(RefreshStatistics)
|
||||||
|
.Bind(v => v.ToEitherAsync());
|
||||||
|
|
||||||
|
private async Task<Validation<BaseError, RefreshParameters>> Validate(RefreshMediaItemStatistics request) =>
|
||||||
|
(await MediaItemMustExist(request).BindT(PathMustExist), await ValidateFFprobePath())
|
||||||
|
.Apply((mediaItem, ffprobePath) => new RefreshParameters(mediaItem, ffprobePath));
|
||||||
|
|
||||||
|
private Task<Validation<BaseError, MediaItem>> MediaItemMustExist(
|
||||||
|
RefreshMediaItemStatistics refreshMediaItemStatistics) =>
|
||||||
|
_mediaItemRepository.Get(refreshMediaItemStatistics.MediaItemId)
|
||||||
|
.Map(
|
||||||
|
maybeItem => maybeItem.ToValidation<BaseError>(
|
||||||
|
$"[MediaItem] {refreshMediaItemStatistics.MediaItemId} does not exist."));
|
||||||
|
|
||||||
|
private Validation<BaseError, MediaItem> PathMustExist(MediaItem mediaItem) =>
|
||||||
|
Some(mediaItem)
|
||||||
|
.Filter(item => File.Exists(item.Path))
|
||||||
|
.ToValidation<BaseError>($"[Path] '{mediaItem.Path}' does not exist on the file system");
|
||||||
|
|
||||||
|
private Task<Validation<BaseError, string>> ValidateFFprobePath() =>
|
||||||
|
_configElementRepository.GetValue<string>(ConfigElementKey.FFprobePath)
|
||||||
|
.FilterT(File.Exists)
|
||||||
|
.Map(
|
||||||
|
ffprobePath =>
|
||||||
|
ffprobePath.ToValidation<BaseError>("FFprobe path does not exist on the file system"));
|
||||||
|
|
||||||
|
private Task<Unit> RefreshStatistics(RefreshParameters parameters) =>
|
||||||
|
_localStatisticsProvider.RefreshStatistics(parameters.FFprobePath, parameters.MediaItem).ToUnit();
|
||||||
|
|
||||||
|
private record RefreshParameters(MediaItem MediaItem, string FFprobePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaItems
|
||||||
|
{
|
||||||
|
internal static class Mapper
|
||||||
|
{
|
||||||
|
internal static MediaItemViewModel ProjectToViewModel(MediaItem mediaItem) =>
|
||||||
|
new(
|
||||||
|
mediaItem.Id,
|
||||||
|
mediaItem.MediaSourceId,
|
||||||
|
mediaItem.Path);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
namespace ErsatzTV.Application.MediaItems
|
||||||
|
{
|
||||||
|
public record MediaItemViewModel(int Id, int MediaSourceId, string Path);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaItems.Queries
|
||||||
|
{
|
||||||
|
public record GetAggregateMediaItems
|
||||||
|
(MediaType MediaType, string SearchString) : IRequest<List<AggregateMediaItemViewModel>>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaItems.Queries
|
||||||
|
{
|
||||||
|
public class
|
||||||
|
GetAggregateMediaItemsHandler : IRequestHandler<GetAggregateMediaItems, List<AggregateMediaItemViewModel>>
|
||||||
|
{
|
||||||
|
private readonly IMediaItemRepository _mediaItemRepository;
|
||||||
|
|
||||||
|
public GetAggregateMediaItemsHandler(IMediaItemRepository mediaItemRepository) =>
|
||||||
|
_mediaItemRepository = mediaItemRepository;
|
||||||
|
|
||||||
|
public async Task<List<AggregateMediaItemViewModel>> Handle(
|
||||||
|
GetAggregateMediaItems request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
IEnumerable<MediaItem> allItems = await _mediaItemRepository.GetAll(request.MediaType);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(request.SearchString))
|
||||||
|
{
|
||||||
|
allItems = allItems.Filter(i => i.Metadata?.Title.Contains(request.SearchString) == true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return allItems.GroupBy(c => new { c.Source.Name, c.Metadata.Title }).Map(
|
||||||
|
group => new AggregateMediaItemViewModel(
|
||||||
|
group.Key.Name,
|
||||||
|
group.Key.Title,
|
||||||
|
group.Count(),
|
||||||
|
group.Count() == 1 ? DisplayDuration(group.Head()) : string.Empty))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string DisplayDuration(MediaItem mediaItem) => string.Format(
|
||||||
|
mediaItem.Metadata?.Duration.TotalHours >= 1 ? @"{0:h\:mm\:ss}" : @"{0:mm\:ss}",
|
||||||
|
mediaItem.Metadata?.Duration);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaItems.Queries
|
||||||
|
{
|
||||||
|
public record GetAllMediaItems : IRequest<List<MediaItemViewModel>>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using MediatR;
|
||||||
|
using static ErsatzTV.Application.MediaItems.Mapper;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaItems.Queries
|
||||||
|
{
|
||||||
|
public class GetAllMediaItemsHandler : IRequestHandler<GetAllMediaItems, List<MediaItemViewModel>>
|
||||||
|
{
|
||||||
|
private readonly IMediaItemRepository _mediaItemRepository;
|
||||||
|
|
||||||
|
public GetAllMediaItemsHandler(IMediaItemRepository mediaItemRepository) =>
|
||||||
|
_mediaItemRepository = mediaItemRepository;
|
||||||
|
|
||||||
|
public async Task<List<MediaItemViewModel>> Handle(
|
||||||
|
GetAllMediaItems request,
|
||||||
|
CancellationToken cancellationToken) =>
|
||||||
|
(await _mediaItemRepository.GetAll()).Map(ProjectToViewModel).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaItems.Queries
|
||||||
|
{
|
||||||
|
public record GetMediaItemById(int Id) : IRequest<Option<MediaItemViewModel>>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
using static ErsatzTV.Application.MediaItems.Mapper;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaItems.Queries
|
||||||
|
{
|
||||||
|
public class GetMediaItemByIdHandler : IRequestHandler<GetMediaItemById, Option<MediaItemViewModel>>
|
||||||
|
{
|
||||||
|
private readonly IMediaItemRepository _mediaItemRepository;
|
||||||
|
|
||||||
|
public GetMediaItemByIdHandler(IMediaItemRepository mediaItemRepository) =>
|
||||||
|
_mediaItemRepository = mediaItemRepository;
|
||||||
|
|
||||||
|
public Task<Option<MediaItemViewModel>> Handle(
|
||||||
|
GetMediaItemById request,
|
||||||
|
CancellationToken cancellationToken) =>
|
||||||
|
_mediaItemRepository.Get(request.Id)
|
||||||
|
.MapT(ProjectToViewModel);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaSources.Commands
|
||||||
|
{
|
||||||
|
public record CreateLocalMediaSource
|
||||||
|
(string Name, MediaType MediaType, string Folder) : IRequest<Either<BaseError, MediaSourceViewModel>>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
using static ErsatzTV.Application.MediaSources.Mapper;
|
||||||
|
using static LanguageExt.Prelude;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaSources.Commands
|
||||||
|
{
|
||||||
|
public class CreateLocalMediaSourceHandler : IRequestHandler<CreateLocalMediaSource,
|
||||||
|
Either<BaseError, MediaSourceViewModel>>
|
||||||
|
{
|
||||||
|
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||||
|
|
||||||
|
public CreateLocalMediaSourceHandler(IMediaSourceRepository mediaSourceRepository) =>
|
||||||
|
_mediaSourceRepository = mediaSourceRepository;
|
||||||
|
|
||||||
|
public Task<Either<BaseError, MediaSourceViewModel>> Handle(
|
||||||
|
CreateLocalMediaSource request,
|
||||||
|
CancellationToken cancellationToken) =>
|
||||||
|
Validate(request).MapT(PersistLocalMediaSource).Bind(v => v.ToEitherAsync());
|
||||||
|
|
||||||
|
private Task<MediaSourceViewModel> PersistLocalMediaSource(LocalMediaSource c) =>
|
||||||
|
_mediaSourceRepository.Add(c).Map(ProjectToViewModel);
|
||||||
|
|
||||||
|
private async Task<Validation<BaseError, LocalMediaSource>> Validate(CreateLocalMediaSource request) =>
|
||||||
|
(await ValidateName(request), await ValidateFolder(request))
|
||||||
|
.Apply(
|
||||||
|
(name, folder) =>
|
||||||
|
new LocalMediaSource
|
||||||
|
{
|
||||||
|
Name = name,
|
||||||
|
MediaType = request.MediaType,
|
||||||
|
Folder = folder
|
||||||
|
});
|
||||||
|
|
||||||
|
private async Task<Validation<BaseError, string>> ValidateName(CreateLocalMediaSource createCollection)
|
||||||
|
{
|
||||||
|
List<string> allNames = await _mediaSourceRepository.GetAll()
|
||||||
|
.Map(list => list.Map(c => c.Name).ToList());
|
||||||
|
|
||||||
|
Validation<BaseError, string> result1 = createCollection.NotEmpty(c => c.Name)
|
||||||
|
.Bind(_ => createCollection.NotLongerThan(50)(c => c.Name));
|
||||||
|
|
||||||
|
var result2 = Optional(createCollection.Name)
|
||||||
|
.Filter(name => !allNames.Contains(name))
|
||||||
|
.ToValidation<BaseError>("Media source name must be unique");
|
||||||
|
|
||||||
|
return (result1, result2).Apply((_, _) => createCollection.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Validation<BaseError, string>> ValidateFolder(CreateLocalMediaSource createCollection)
|
||||||
|
{
|
||||||
|
List<string> allFolders = await _mediaSourceRepository.GetAll()
|
||||||
|
.Map(list => list.OfType<LocalMediaSource>().Map(c => c.Folder).ToList());
|
||||||
|
|
||||||
|
|
||||||
|
return Optional(createCollection.Folder)
|
||||||
|
.Filter(folder => allFolders.ForAll(f => !AreSubPaths(f, folder)))
|
||||||
|
.ToValidation<BaseError>("Folder must not belong to another media source");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool AreSubPaths(string path1, string path2)
|
||||||
|
{
|
||||||
|
string one = path1 + Path.DirectorySeparatorChar;
|
||||||
|
string two = path2 + Path.DirectorySeparatorChar;
|
||||||
|
return one == two || one.StartsWith(two, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
two.StartsWith(one, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaSources.Commands
|
||||||
|
{
|
||||||
|
public record DeleteLocalMediaSource(int LocalMediaSourceId) : IRequest<Either<BaseError, Task>>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaSources.Commands
|
||||||
|
{
|
||||||
|
public class
|
||||||
|
DeleteLocalMediaSourceHandler : IRequestHandler<DeleteLocalMediaSource, Either<BaseError, Task>>
|
||||||
|
{
|
||||||
|
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||||
|
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||||
|
|
||||||
|
public DeleteLocalMediaSourceHandler(
|
||||||
|
IMediaSourceRepository mediaSourceRepository,
|
||||||
|
IMediaCollectionRepository mediaCollectionRepository)
|
||||||
|
{
|
||||||
|
_mediaSourceRepository = mediaSourceRepository;
|
||||||
|
_mediaCollectionRepository = mediaCollectionRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Either<BaseError, Task>> Handle(
|
||||||
|
DeleteLocalMediaSource request,
|
||||||
|
CancellationToken cancellationToken) =>
|
||||||
|
(await MediaSourceMustExist(request))
|
||||||
|
.Map(DoDeletion)
|
||||||
|
.ToEither<Task>();
|
||||||
|
|
||||||
|
private async Task DoDeletion(LocalMediaSource mediaSource)
|
||||||
|
{
|
||||||
|
await _mediaSourceRepository.Delete(mediaSource.Id);
|
||||||
|
if (mediaSource.MediaType == MediaType.TvShow)
|
||||||
|
{
|
||||||
|
await _mediaCollectionRepository.DeleteEmptyTelevisionCollections();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Validation<BaseError, LocalMediaSource>> MediaSourceMustExist(
|
||||||
|
DeleteLocalMediaSource deleteMediaSource) =>
|
||||||
|
(await _mediaSourceRepository.Get(deleteMediaSource.LocalMediaSourceId))
|
||||||
|
.OfType<LocalMediaSource>()
|
||||||
|
.HeadOrNone()
|
||||||
|
.ToValidation<BaseError>(
|
||||||
|
$"Local media source {deleteMediaSource.LocalMediaSourceId} does not exist.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using ErsatzTV.Core;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaSources.Commands
|
||||||
|
{
|
||||||
|
public record ScanLocalMediaSource(int MediaSourceId) : IRequest<Either<BaseError, string>>,
|
||||||
|
IBackgroundServiceRequest;
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Interfaces.Metadata;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaSources.Commands
|
||||||
|
{
|
||||||
|
public class ScanLocalMediaSourceHandler : IRequestHandler<ScanLocalMediaSource, Either<BaseError, string>>
|
||||||
|
{
|
||||||
|
private readonly IConfigElementRepository _configElementRepository;
|
||||||
|
private readonly ILocalMediaScanner _localMediaScanner;
|
||||||
|
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||||
|
|
||||||
|
public ScanLocalMediaSourceHandler(
|
||||||
|
IMediaSourceRepository mediaSourceRepository,
|
||||||
|
IConfigElementRepository configElementRepository,
|
||||||
|
ILocalMediaScanner localMediaScanner)
|
||||||
|
{
|
||||||
|
_mediaSourceRepository = mediaSourceRepository;
|
||||||
|
_configElementRepository = configElementRepository;
|
||||||
|
_localMediaScanner = localMediaScanner;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<Either<BaseError, string>>
|
||||||
|
Handle(ScanLocalMediaSource request, CancellationToken cancellationToken) =>
|
||||||
|
Validate(request)
|
||||||
|
.MapT(
|
||||||
|
p => _localMediaScanner.ScanLocalMediaSource(p.LocalMediaSource, p.FFprobePath)
|
||||||
|
.Map(_ => p.LocalMediaSource.Name))
|
||||||
|
.Bind(v => v.ToEitherAsync());
|
||||||
|
|
||||||
|
private async Task<Validation<BaseError, RequestParameters>> Validate(ScanLocalMediaSource request) =>
|
||||||
|
(await LocalMediaSourceMustExist(request), await ValidateFFprobePath())
|
||||||
|
.Apply((localMediaSource, ffprobePath) => new RequestParameters(localMediaSource, ffprobePath));
|
||||||
|
|
||||||
|
private Task<Validation<BaseError, LocalMediaSource>> LocalMediaSourceMustExist(
|
||||||
|
ScanLocalMediaSource request) =>
|
||||||
|
_mediaSourceRepository.Get(request.MediaSourceId)
|
||||||
|
.Map(maybeMediaSource => maybeMediaSource.Map(ms => ms as LocalMediaSource))
|
||||||
|
.Map(v => v.ToValidation<BaseError>($"Local media source {request.MediaSourceId} does not exist."));
|
||||||
|
|
||||||
|
private Task<Validation<BaseError, string>> ValidateFFprobePath() =>
|
||||||
|
_configElementRepository.GetValue<string>(ConfigElementKey.FFprobePath)
|
||||||
|
.FilterT(File.Exists)
|
||||||
|
.Map(
|
||||||
|
ffprobePath =>
|
||||||
|
ffprobePath.ToValidation<BaseError>("FFprobe path does not exist on the file system"));
|
||||||
|
|
||||||
|
private record RequestParameters(LocalMediaSource LocalMediaSource, string FFprobePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using ErsatzTV.Core;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaSources.Commands
|
||||||
|
{
|
||||||
|
public record StartPlexPinFlow : IRequest<Either<BaseError, string>>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Channels;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Interfaces.Plex;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
using static LanguageExt.Prelude;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaSources.Commands
|
||||||
|
{
|
||||||
|
public class StartPlexPinFlowHandler : IRequestHandler<StartPlexPinFlow, Either<BaseError, string>>
|
||||||
|
{
|
||||||
|
private readonly ChannelWriter<IPlexBackgroundServiceRequest> _channel;
|
||||||
|
private readonly IPlexTvApiClient _plexTvApiClient;
|
||||||
|
|
||||||
|
public StartPlexPinFlowHandler(
|
||||||
|
IPlexTvApiClient plexTvApiClient,
|
||||||
|
ChannelWriter<IPlexBackgroundServiceRequest> channel)
|
||||||
|
{
|
||||||
|
_plexTvApiClient = plexTvApiClient;
|
||||||
|
_channel = channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<Either<BaseError, string>> Handle(
|
||||||
|
StartPlexPinFlow request,
|
||||||
|
CancellationToken cancellationToken) =>
|
||||||
|
_plexTvApiClient.StartPinFlow().Bind(
|
||||||
|
result => result.Match(
|
||||||
|
Left: error => Task.FromResult(Left<BaseError, string>(error)),
|
||||||
|
Right: async pin =>
|
||||||
|
{
|
||||||
|
await _channel.WriteAsync(new TryCompletePlexPinFlow(pin), cancellationToken);
|
||||||
|
return Right<BaseError, string>(pin.Url);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
using ErsatzTV.Core;
|
||||||
|
using LanguageExt;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaSources.Commands
|
||||||
|
{
|
||||||
|
public record SynchronizePlexLibraries(int PlexMediaSourceId) : MediatR.IRequest<Either<BaseError, Unit>>;
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user