Browse Source

refactor dbcontext lifetime (#258)

* refactor create playout handler

* refactor get all playouts handler

* refactor delete playout handler

* remove dead code

* ignore unnamed artists for collections

* more repository cleanup

* more schedule items refactoring

* more playout refactoring

* refactor playout builder

* refactor ffmpeg profiles

* more ffmpeg profile refactoring

* rework resolutions

* refactor media collections

* refactor config elements

* update changelog

* more cleanup
pull/259/head
Jason Dove 5 years ago committed by GitHub
parent
commit
0fb5bfde58
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      CHANGELOG.md
  2. 7
      ErsatzTV.Application/Artists/Queries/GetAllArtistsHandler.cs
  3. 2
      ErsatzTV.Application/Channels/Commands/CreateChannel.cs
  4. 65
      ErsatzTV.Application/Channels/Commands/CreateChannelHandler.cs
  5. 4
      ErsatzTV.Application/Channels/Commands/CreateChannelResult.cs
  6. 15
      ErsatzTV.Application/Configuration/Commands/SaveConfigElementByKeyHandler.cs
  7. 22
      ErsatzTV.Application/Configuration/Commands/UpdateLibraryRefreshIntervalHandler.cs
  8. 4
      ErsatzTV.Application/EntityIdResult.cs
  9. 1
      ErsatzTV.Application/ErsatzTV.Application.csproj
  10. 2
      ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfile.cs
  11. 62
      ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfileHandler.cs
  12. 4
      ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfileResult.cs
  13. 6
      ErsatzTV.Application/FFmpegProfiles/Commands/DeleteFFmpegProfile.cs
  14. 43
      ErsatzTV.Application/FFmpegProfiles/Commands/DeleteFFmpegProfileHandler.cs
  15. 23
      ErsatzTV.Application/FFmpegProfiles/Commands/NewFFmpegProfileHandler.cs
  16. 2
      ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfile.cs
  17. 71
      ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfileHandler.cs
  18. 4
      ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfileResult.cs
  19. 32
      ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegSettingsHandler.cs
  20. 20
      ErsatzTV.Application/FFmpegProfiles/Queries/GetAllFFmpegProfilesHandler.cs
  21. 21
      ErsatzTV.Application/FFmpegProfiles/Queries/GetFFmpegProfileByIdHandler.cs
  22. 17
      ErsatzTV.Application/HDHR/Commands/UpdateHDHRTunerCountHandler.cs
  23. 60
      ErsatzTV.Application/MediaCards/Queries/GetCollectionCardsHandler.cs
  24. 64
      ErsatzTV.Application/MediaCollections/Commands/AddArtistToCollectionHandler.cs
  25. 65
      ErsatzTV.Application/MediaCollections/Commands/AddEpisodeToCollectionHandler.cs
  26. 59
      ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollectionHandler.cs
  27. 64
      ErsatzTV.Application/MediaCollections/Commands/AddMovieToCollectionHandler.cs
  28. 64
      ErsatzTV.Application/MediaCollections/Commands/AddMusicVideoToCollectionHandler.cs
  29. 65
      ErsatzTV.Application/MediaCollections/Commands/AddSeasonToCollectionHandler.cs
  30. 67
      ErsatzTV.Application/MediaCollections/Commands/AddShowToCollectionHandler.cs
  31. 49
      ErsatzTV.Application/MediaCollections/Commands/CreateCollectionHandler.cs
  32. 5
      ErsatzTV.Application/MediaCollections/Commands/DeleteCollection.cs
  33. 45
      ErsatzTV.Application/MediaCollections/Commands/DeleteCollectionHandler.cs
  34. 41
      ErsatzTV.Application/MediaCollections/Commands/RemoveItemsFromCollectionHandler.cs
  35. 58
      ErsatzTV.Application/MediaCollections/Commands/UpdateCollectionCustomOrderHandler.cs
  36. 46
      ErsatzTV.Application/MediaCollections/Commands/UpdateCollectionHandler.cs
  37. 20
      ErsatzTV.Application/MediaCollections/Queries/GetAllCollectionsHandler.cs
  38. 25
      ErsatzTV.Application/MediaCollections/Queries/GetCollectionByIdHandler.cs
  39. 9
      ErsatzTV.Application/MediaCollections/Queries/GetCollectionItems.cs
  40. 26
      ErsatzTV.Application/MediaCollections/Queries/GetCollectionItemsHandler.cs
  41. 27
      ErsatzTV.Application/MediaCollections/Queries/GetPagedCollectionsHandler.cs
  42. 3
      ErsatzTV.Application/MediaItems/Mapper.cs
  43. 9
      ErsatzTV.Application/MediaItems/MediaItemSearchResultViewModel.cs
  44. 4
      ErsatzTV.Application/MediaItems/MediaItemViewModel.cs
  45. 7
      ErsatzTV.Application/MediaItems/Queries/GetAllMediaItems.cs
  46. 24
      ErsatzTV.Application/MediaItems/Queries/GetAllMediaItemsHandler.cs
  47. 7
      ErsatzTV.Application/MediaItems/Queries/GetMediaItemById.cs
  48. 23
      ErsatzTV.Application/MediaItems/Queries/GetMediaItemByIdHandler.cs
  49. 16
      ErsatzTV.Application/MediaSources/Mapper.cs
  50. 6
      ErsatzTV.Application/MediaSources/Queries/CountMediaItemsById.cs
  51. 18
      ErsatzTV.Application/MediaSources/Queries/CountMediaItemsByIdHandler.cs
  52. 7
      ErsatzTV.Application/MediaSources/Queries/GetAllMediaSources.cs
  53. 23
      ErsatzTV.Application/MediaSources/Queries/GetAllMediaSourcesHandler.cs
  54. 7
      ErsatzTV.Application/MediaSources/Queries/GetMediaSourceById.cs
  55. 23
      ErsatzTV.Application/MediaSources/Queries/GetMediaSourceByIdHandler.cs
  56. 52
      ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs
  57. 2
      ErsatzTV.Application/Playouts/Commands/CreatePlayout.cs
  58. 86
      ErsatzTV.Application/Playouts/Commands/CreatePlayoutHandler.cs
  59. 4
      ErsatzTV.Application/Playouts/Commands/CreatePlayoutResponse.cs
  60. 6
      ErsatzTV.Application/Playouts/Commands/DeletePlayout.cs
  61. 44
      ErsatzTV.Application/Playouts/Commands/DeletePlayoutHandler.cs
  62. 13
      ErsatzTV.Application/Playouts/Commands/UpdatePlayout.cs
  63. 75
      ErsatzTV.Application/Playouts/Commands/UpdatePlayoutHandler.cs
  64. 13
      ErsatzTV.Application/Playouts/Mapper.cs
  65. 8
      ErsatzTV.Application/Playouts/PlayoutNameViewModel.cs
  66. 2
      ErsatzTV.Application/Playouts/Queries/GetAllPlayouts.cs
  67. 26
      ErsatzTV.Application/Playouts/Queries/GetAllPlayoutsHandler.cs
  68. 7
      ErsatzTV.Application/Playouts/Queries/GetPlayoutById.cs
  69. 24
      ErsatzTV.Application/Playouts/Queries/GetPlayoutByIdHandler.cs
  70. 45
      ErsatzTV.Application/Playouts/Queries/GetPlayoutItemsByIdHandler.cs
  71. 35
      ErsatzTV.Application/ProgramSchedules/Commands/AddProgramScheduleItemHandler.cs
  72. 2
      ErsatzTV.Application/ProgramSchedules/Commands/CreateProgramSchedule.cs
  73. 89
      ErsatzTV.Application/ProgramSchedules/Commands/CreateProgramScheduleHandler.cs
  74. 4
      ErsatzTV.Application/ProgramSchedules/Commands/CreateProgramScheduleResult.cs
  75. 5
      ErsatzTV.Application/ProgramSchedules/Commands/DeleteProgramSchedule.cs
  76. 43
      ErsatzTV.Application/ProgramSchedules/Commands/DeleteProgramScheduleHandler.cs
  77. 24
      ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs
  78. 37
      ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItemsHandler.cs
  79. 2
      ErsatzTV.Application/ProgramSchedules/Commands/UpdateProgramSchedule.cs
  80. 91
      ErsatzTV.Application/ProgramSchedules/Commands/UpdateProgramScheduleHandler.cs
  81. 4
      ErsatzTV.Application/ProgramSchedules/Commands/UpdateProgramScheduleResult.cs
  82. 26
      ErsatzTV.Application/ProgramSchedules/Queries/GetAllProgramSchedulesHandler.cs
  83. 24
      ErsatzTV.Application/ProgramSchedules/Queries/GetProgramScheduleByIdHandler.cs
  84. 3
      ErsatzTV.Application/ProgramSchedules/Queries/GetProgramScheduleItems.cs
  85. 45
      ErsatzTV.Application/ProgramSchedules/Queries/GetProgramScheduleItemsHandler.cs
  86. 20
      ErsatzTV.Application/Resolutions/Queries/GetAllResolutionsHandler.cs
  87. 18
      ErsatzTV.Application/Search/Commands/RebuildSearchIndexHandler.cs
  88. 73
      ErsatzTV.Application/Streaming/Queries/FFmpegProcessHandler.cs
  89. 14
      ErsatzTV.Application/Streaming/Queries/GetConcatProcessByChannelNumberHandler.cs
  90. 84
      ErsatzTV.Application/Streaming/Queries/GetHlsPlaylistByChannelNumberHandler.cs
  91. 53
      ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs
  92. 19
      ErsatzTV.Application/Television/Mapper.cs
  93. 7
      ErsatzTV.Application/Television/Queries/GetTelevisionEpisodeById.cs
  94. 24
      ErsatzTV.Application/Television/Queries/GetTelevisionEpisodeByIdHandler.cs
  95. 10
      ErsatzTV.Application/Television/TelevisionEpisodeViewModel.cs
  96. 18
      ErsatzTV.Core.Tests/Fakes/FakeMediaCollectionRepository.cs
  97. 2
      ErsatzTV.Core/FFmpeg/FFmpegLocator.cs
  98. 1
      ErsatzTV.Core/Interfaces/Metadata/ILocalFileSystem.cs
  99. 2
      ErsatzTV.Core/Interfaces/Repositories/IChannelRepository.cs
  100. 3
      ErsatzTV.Core/Interfaces/Repositories/IConfigElementRepository.cs
  101. Some files were not shown because too many files have changed in this diff Show More

6
CHANGELOG.md

@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased] ## [Unreleased]
### Changed
- Remove unused API and SDK project; may reintroduce in the future but for now they have fallen out of date
### Fixed
- Fix crash adding or editing schedule items due to Artist with no name
- Fix many potential sources of inconsistent data in UI
## [0.0.45-prealpha] - 2021-06-12 ## [0.0.45-prealpha] - 2021-06-12
### Added ### Added

7
ErsatzTV.Application/Artists/Queries/GetAllArtistsHandler.cs

@ -19,6 +19,11 @@ namespace ErsatzTV.Application.Artists.Queries
public Task<List<NamedMediaItemViewModel>> Handle( public Task<List<NamedMediaItemViewModel>> Handle(
GetAllArtists request, GetAllArtists request,
CancellationToken cancellationToken) => CancellationToken cancellationToken) =>
_artistRepository.GetAllArtists().Map(list => list.Map(ProjectToViewModel).ToList()); _artistRepository.GetAllArtists()
.Map(
list => list.Filter(
a => !string.IsNullOrWhiteSpace(
a.ArtistMetadata.HeadOrNone().Match(am => am.Title, () => string.Empty))))
.Map(list => list.Map(ProjectToViewModel).ToList());
} }
} }

2
ErsatzTV.Application/Channels/Commands/CreateChannel.cs

@ -20,5 +20,5 @@ namespace ErsatzTV.Application.Channels.Commands
int WatermarkHorizontalMargin, int WatermarkHorizontalMargin,
int WatermarkVerticalMargin, int WatermarkVerticalMargin,
int WatermarkFrequencyMinutes, int WatermarkFrequencyMinutes,
int WatermarkDurationSeconds) : IRequest<Either<BaseError, ChannelViewModel>>; int WatermarkDurationSeconds) : IRequest<Either<BaseError, CreateChannelResult>>;
} }

65
ErsatzTV.Application/Channels/Commands/CreateChannelHandler.cs

@ -7,39 +7,40 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt; using LanguageExt;
using MediatR; using MediatR;
using static ErsatzTV.Application.Channels.Mapper; using Microsoft.EntityFrameworkCore;
using static LanguageExt.Prelude; using static LanguageExt.Prelude;
namespace ErsatzTV.Application.Channels.Commands namespace ErsatzTV.Application.Channels.Commands
{ {
public class CreateChannelHandler : IRequestHandler<CreateChannel, Either<BaseError, ChannelViewModel>> public class CreateChannelHandler : IRequestHandler<CreateChannel, Either<BaseError, CreateChannelResult>>
{ {
private readonly IChannelRepository _channelRepository; private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IFFmpegProfileRepository _ffmpegProfileRepository;
public CreateChannelHandler( public CreateChannelHandler(IDbContextFactory<TvContext> dbContextFactory) => _dbContextFactory = dbContextFactory;
IChannelRepository channelRepository,
IFFmpegProfileRepository ffmpegProfileRepository)
{
_channelRepository = channelRepository;
_ffmpegProfileRepository = ffmpegProfileRepository;
}
public Task<Either<BaseError, ChannelViewModel>> Handle( public async Task<Either<BaseError, CreateChannelResult>> Handle(
CreateChannel request, CreateChannel request,
CancellationToken cancellationToken) => CancellationToken cancellationToken)
Validate(request) {
.MapT(PersistChannel) await using TvContext dbContext = _dbContextFactory.CreateDbContext();
.Bind(v => v.ToEitherAsync()); Validation<BaseError, Channel> validation = await Validate(dbContext, request);
return await validation.Apply(c => PersistChannel(dbContext, c));
}
private Task<ChannelViewModel> PersistChannel(Channel c) => private static async Task<CreateChannelResult> PersistChannel(TvContext dbContext, Channel channel)
_channelRepository.Add(c).Map(ProjectToViewModel); {
await dbContext.Channels.AddAsync(channel);
await dbContext.SaveChangesAsync();
return new CreateChannelResult(channel.Id);
}
private async Task<Validation<BaseError, Channel>> Validate(CreateChannel request) => private async Task<Validation<BaseError, Channel>> Validate(TvContext dbContext, CreateChannel request) =>
(ValidateName(request), await ValidateNumber(request), await FFmpegProfileMustExist(request), (ValidateName(request), await ValidateNumber(dbContext, request),
await FFmpegProfileMustExist(dbContext, request),
ValidatePreferredLanguage(request)) ValidatePreferredLanguage(request))
.Apply( .Apply(
(name, number, ffmpegProfileId, preferredLanguageCode) => (name, number, ffmpegProfileId, preferredLanguageCode) =>
@ -85,20 +86,21 @@ namespace ErsatzTV.Application.Channels.Commands
return channel; return channel;
}); });
private Validation<BaseError, string> ValidateName(CreateChannel createChannel) => private static Validation<BaseError, string> ValidateName(CreateChannel createChannel) =>
createChannel.NotEmpty(c => c.Name) createChannel.NotEmpty(c => c.Name)
.Bind(_ => createChannel.NotLongerThan(50)(c => c.Name)); .Bind(_ => createChannel.NotLongerThan(50)(c => c.Name));
private Validation<BaseError, string> ValidatePreferredLanguage(CreateChannel createChannel) => private static Validation<BaseError, string> ValidatePreferredLanguage(CreateChannel createChannel) =>
Optional(createChannel.PreferredLanguageCode ?? string.Empty) Optional(createChannel.PreferredLanguageCode ?? string.Empty)
.Filter( .Filter(
lc => string.IsNullOrWhiteSpace(lc) || CultureInfo.GetCultures(CultureTypes.NeutralCultures).Any( lc => string.IsNullOrWhiteSpace(lc) || CultureInfo.GetCultures(CultureTypes.NeutralCultures).Any(
ci => string.Equals(ci.ThreeLetterISOLanguageName, lc, StringComparison.OrdinalIgnoreCase))) ci => string.Equals(ci.ThreeLetterISOLanguageName, lc, StringComparison.OrdinalIgnoreCase)))
.ToValidation<BaseError>("Preferred language code is invalid"); .ToValidation<BaseError>("Preferred language code is invalid");
private async Task<Validation<BaseError, string>> ValidateNumber(CreateChannel createChannel) private static async Task<Validation<BaseError, string>> ValidateNumber(TvContext dbContext, CreateChannel createChannel)
{ {
Option<Channel> maybeExistingChannel = await _channelRepository.GetByNumber(createChannel.Number); Option<Channel> maybeExistingChannel = await dbContext.Channels
.SelectOneAsync(c => c.Number, c => c.Number == createChannel.Number);
return maybeExistingChannel.Match<Validation<BaseError, string>>( return maybeExistingChannel.Match<Validation<BaseError, string>>(
_ => BaseError.New("Channel number must be unique"), _ => BaseError.New("Channel number must be unique"),
() => () =>
@ -112,9 +114,14 @@ namespace ErsatzTV.Application.Channels.Commands
}); });
} }
private async Task<Validation<BaseError, int>> FFmpegProfileMustExist(CreateChannel createChannel) => private static Task<Validation<BaseError, int>> FFmpegProfileMustExist(
(await _ffmpegProfileRepository.Get(createChannel.FFmpegProfileId)) TvContext dbContext,
.ToValidation<BaseError>($"FFmpegProfile {createChannel.FFmpegProfileId} does not exist.") CreateChannel createChannel) =>
.Map(c => c.Id); dbContext.FFmpegProfiles
.CountAsync(p => p.Id == createChannel.FFmpegProfileId)
.Map(Optional)
.Filter(c => c > 0)
.MapT(_ => createChannel.FFmpegProfileId)
.Map(o => o.ToValidation<BaseError>($"FFmpegProfile {createChannel.FFmpegProfileId} does not exist."));
} }
} }

4
ErsatzTV.Application/Channels/Commands/CreateChannelResult.cs

@ -0,0 +1,4 @@
namespace ErsatzTV.Application.Channels.Commands
{
public record CreateChannelResult(int ChannelId) : EntityIdResult(ChannelId);
}

15
ErsatzTV.Application/Configuration/Commands/SaveConfigElementByKeyHandler.cs

@ -1,6 +1,5 @@
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt; using LanguageExt;
@ -15,19 +14,7 @@ namespace ErsatzTV.Application.Configuration.Commands
public async Task<Unit> Handle(SaveConfigElementByKey request, CancellationToken cancellationToken) public async Task<Unit> Handle(SaveConfigElementByKey request, CancellationToken cancellationToken)
{ {
Option<ConfigElement> maybeElement = await _configElementRepository.Get(request.Key); await _configElementRepository.Upsert(request.Key, request.Value);
await maybeElement.Match(
ce =>
{
ce.Value = request.Value;
return _configElementRepository.Update(ce);
},
() =>
{
var ce = new ConfigElement { Key = request.Key.Key, Value = request.Value };
return _configElementRepository.Add(ce);
});
return Unit.Default; return Unit.Default;
} }
} }

22
ErsatzTV.Application/Configuration/Commands/UpdateLibraryRefreshIntervalHandler.cs

@ -8,9 +8,8 @@ using static LanguageExt.Prelude;
namespace ErsatzTV.Application.Configuration.Commands namespace ErsatzTV.Application.Configuration.Commands
{ {
public class public class UpdateLibraryRefreshIntervalHandler :
UpdateLibraryRefreshIntervalHandler : MediatR.IRequestHandler<UpdateLibraryRefreshInterval, MediatR.IRequestHandler<UpdateLibraryRefreshInterval, Either<BaseError, Unit>>
Either<BaseError, Unit>>
{ {
private readonly IConfigElementRepository _configElementRepository; private readonly IConfigElementRepository _configElementRepository;
@ -21,27 +20,14 @@ namespace ErsatzTV.Application.Configuration.Commands
UpdateLibraryRefreshInterval request, UpdateLibraryRefreshInterval request,
CancellationToken cancellationToken) => CancellationToken cancellationToken) =>
Validate(request) Validate(request)
.MapT(_ => Upsert(ConfigElementKey.LibraryRefreshInterval, request.LibraryRefreshInterval.ToString())) .MapT(_ => _configElementRepository.Upsert(ConfigElementKey.LibraryRefreshInterval, request.LibraryRefreshInterval))
.Bind(v => v.ToEitherAsync()); .Bind(v => v.ToEitherAsync());
private Task<Validation<BaseError, Unit>> Validate(UpdateLibraryRefreshInterval request) => private static Task<Validation<BaseError, Unit>> Validate(UpdateLibraryRefreshInterval request) =>
Optional(request.LibraryRefreshInterval) Optional(request.LibraryRefreshInterval)
.Filter(lri => lri > 0) .Filter(lri => lri > 0)
.Map(_ => Unit.Default) .Map(_ => Unit.Default)
.ToValidation<BaseError>("Tuner count must be greater than zero") .ToValidation<BaseError>("Tuner count must be greater than zero")
.AsTask(); .AsTask();
private Task<Unit> Upsert(ConfigElementKey key, string value) =>
_configElementRepository.Get(key).Match(
ce =>
{
ce.Value = value;
return _configElementRepository.Update(ce);
},
() =>
{
var ce = new ConfigElement { Key = key.Key, Value = value };
return _configElementRepository.Add(ce);
}).ToUnit();
} }
} }

4
ErsatzTV.Application/EntityIdResult.cs

@ -0,0 +1,4 @@
namespace ErsatzTV.Application
{
public record EntityIdResult(int Id);
}

1
ErsatzTV.Application/ErsatzTV.Application.csproj

@ -23,6 +23,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\ErsatzTV.Core\ErsatzTV.Core.csproj" /> <ProjectReference Include="..\ErsatzTV.Core\ErsatzTV.Core.csproj" />
<ProjectReference Include="..\ErsatzTV.Infrastructure\ErsatzTV.Infrastructure.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

2
ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfile.cs

@ -21,5 +21,5 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
bool NormalizeLoudness, bool NormalizeLoudness,
int AudioChannels, int AudioChannels,
int AudioSampleRate, int AudioSampleRate,
bool NormalizeAudio) : IRequest<Either<BaseError, FFmpegProfileViewModel>>; bool NormalizeAudio) : IRequest<Either<BaseError, CreateFFmpegProfileResult>>;
} }

62
ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfileHandler.cs

@ -2,39 +2,42 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt; using LanguageExt;
using MediatR; using MediatR;
using static ErsatzTV.Application.FFmpegProfiles.Mapper; using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.FFmpegProfiles.Commands namespace ErsatzTV.Application.FFmpegProfiles.Commands
{ {
public class public class CreateFFmpegProfileHandler :
CreateFFmpegProfileHandler : IRequestHandler<CreateFFmpegProfile, Either<BaseError, FFmpegProfileViewModel>> IRequestHandler<CreateFFmpegProfile, Either<BaseError, CreateFFmpegProfileResult>>
{ {
private readonly IFFmpegProfileRepository _ffmpegProfileRepository; private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IResolutionRepository _resolutionRepository;
public CreateFFmpegProfileHandler( public CreateFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFactory) =>
IFFmpegProfileRepository ffmpegProfileRepository, _dbContextFactory = dbContextFactory;
IResolutionRepository resolutionRepository)
{
_ffmpegProfileRepository = ffmpegProfileRepository;
_resolutionRepository = resolutionRepository;
}
public Task<Either<BaseError, FFmpegProfileViewModel>> Handle( public async Task<Either<BaseError, CreateFFmpegProfileResult>> Handle(
CreateFFmpegProfile request, CreateFFmpegProfile request,
CancellationToken cancellationToken) => CancellationToken cancellationToken)
Validate(request) {
.MapT(PersistFFmpegProfile) await using TvContext dbContext = _dbContextFactory.CreateDbContext();
.Bind(v => v.ToEitherAsync()); Validation<BaseError, FFmpegProfile> validation = await Validate(dbContext, request);
return await validation.Apply(profile => PersistFFmpegProfile(dbContext, profile));
}
private Task<FFmpegProfileViewModel> PersistFFmpegProfile(FFmpegProfile ffmpegProfile) => private static async Task<CreateFFmpegProfileResult> PersistFFmpegProfile(
_ffmpegProfileRepository.Add(ffmpegProfile).Map(ProjectToViewModel); TvContext dbContext,
FFmpegProfile ffmpegProfile)
{
await dbContext.FFmpegProfiles.AddAsync(ffmpegProfile);
await dbContext.SaveChangesAsync();
return new CreateFFmpegProfileResult(ffmpegProfile.Id);
}
private async Task<Validation<BaseError, FFmpegProfile>> Validate(CreateFFmpegProfile request) => private async Task<Validation<BaseError, FFmpegProfile>> Validate(TvContext dbContext, CreateFFmpegProfile request) =>
(ValidateName(request), ValidateThreadCount(request), await ResolutionMustExist(request)) (ValidateName(request), ValidateThreadCount(request), await ResolutionMustExist(dbContext, request))
.Apply( .Apply(
(name, threadCount, resolutionId) => new FFmpegProfile (name, threadCount, resolutionId) => new FFmpegProfile
{ {
@ -56,16 +59,19 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
NormalizeAudio = request.NormalizeAudio NormalizeAudio = request.NormalizeAudio
}); });
private Validation<BaseError, string> ValidateName(CreateFFmpegProfile createFFmpegProfile) => private static Validation<BaseError, string> ValidateName(CreateFFmpegProfile createFFmpegProfile) =>
createFFmpegProfile.NotEmpty(x => x.Name) createFFmpegProfile.NotEmpty(x => x.Name)
.Bind(_ => createFFmpegProfile.NotLongerThan(50)(x => x.Name)); .Bind(_ => createFFmpegProfile.NotLongerThan(50)(x => x.Name));
private Validation<BaseError, int> ValidateThreadCount(CreateFFmpegProfile createFFmpegProfile) => private static Validation<BaseError, int> ValidateThreadCount(CreateFFmpegProfile createFFmpegProfile) =>
createFFmpegProfile.AtLeast(0)(p => p.ThreadCount); createFFmpegProfile.AtLeast(0)(p => p.ThreadCount);
private async Task<Validation<BaseError, int>> ResolutionMustExist(CreateFFmpegProfile createFFmpegProfile) => private static Task<Validation<BaseError, int>> ResolutionMustExist(
(await _resolutionRepository.Get(createFFmpegProfile.ResolutionId)) TvContext dbContext,
.ToValidation<BaseError>($"[Resolution] {createFFmpegProfile.ResolutionId} does not exist.") CreateFFmpegProfile createFFmpegProfile) =>
.Map(c => c.Id); dbContext.Resolutions
.SelectOneAsync(r => r.Id, r => r.Id == createFFmpegProfile.ResolutionId)
.MapT(r => r.Id)
.Map(o => o.ToValidation<BaseError>($"[Resolution] {createFFmpegProfile.ResolutionId} does not exist"));
} }
} }

4
ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfileResult.cs

@ -0,0 +1,4 @@
namespace ErsatzTV.Application.FFmpegProfiles.Commands
{
public record CreateFFmpegProfileResult(int FFmpegProfileId) : EntityIdResult(FFmpegProfileId);
}

6
ErsatzTV.Application/FFmpegProfiles/Commands/DeleteFFmpegProfile.cs

@ -1,9 +1,7 @@
using System.Threading.Tasks; using ErsatzTV.Core;
using ErsatzTV.Core;
using LanguageExt; using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.FFmpegProfiles.Commands namespace ErsatzTV.Application.FFmpegProfiles.Commands
{ {
public record DeleteFFmpegProfile(int FFmpegProfileId) : IRequest<Either<BaseError, Task>>; public record DeleteFFmpegProfile(int FFmpegProfileId) : MediatR.IRequest<Either<BaseError, Unit>>;
} }

43
ErsatzTV.Application/FFmpegProfiles/Commands/DeleteFFmpegProfileHandler.cs

@ -1,32 +1,43 @@
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Domain;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt; using LanguageExt;
using MediatR; using MediatR;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.FFmpegProfiles.Commands namespace ErsatzTV.Application.FFmpegProfiles.Commands
{ {
public class DeleteFFmpegProfileHandler : IRequestHandler<DeleteFFmpegProfile, Either<BaseError, Task>> public class DeleteFFmpegProfileHandler : IRequestHandler<DeleteFFmpegProfile, Either<BaseError, LanguageExt.Unit>>
{ {
private readonly IFFmpegProfileRepository _ffmpegProfileRepository; private readonly IDbContextFactory<TvContext> _dbContextFactory;
public DeleteFFmpegProfileHandler(IFFmpegProfileRepository ffmpegProfileRepository) => public DeleteFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_ffmpegProfileRepository = ffmpegProfileRepository; _dbContextFactory = dbContextFactory;
public async Task<Either<BaseError, Task>> Handle( public async Task<Either<BaseError, LanguageExt.Unit>> Handle(
DeleteFFmpegProfile request, DeleteFFmpegProfile request,
CancellationToken cancellationToken) => CancellationToken cancellationToken)
(await FFmpegProfileMustExist(request)) {
.Map(DoDeletion) await using TvContext dbContext = _dbContextFactory.CreateDbContext();
.ToEither<Task>(); Validation<BaseError, FFmpegProfile> validation = await FFmpegProfileMustExist(dbContext, request);
return await validation.Apply(p => DoDeletion(dbContext, p));
}
private Task DoDeletion(int channelId) => _ffmpegProfileRepository.Delete(channelId); private static async Task<LanguageExt.Unit> DoDeletion(TvContext dbContext, FFmpegProfile ffmpegProfile)
{
dbContext.FFmpegProfiles.Remove(ffmpegProfile);
await dbContext.SaveChangesAsync();
return LanguageExt.Unit.Default;
}
private async Task<Validation<BaseError, int>> FFmpegProfileMustExist( private static Task<Validation<BaseError, FFmpegProfile>> FFmpegProfileMustExist(
DeleteFFmpegProfile deleteFFmpegProfile) => TvContext dbContext,
(await _ffmpegProfileRepository.Get(deleteFFmpegProfile.FFmpegProfileId)) DeleteFFmpegProfile request) =>
.ToValidation<BaseError>($"FFmpegProfile {deleteFFmpegProfile.FFmpegProfileId} does not exist.") dbContext.FFmpegProfiles
.Map(c => c.Id); .SelectOneAsync(p => p.Id, p => p.Id == request.FFmpegProfileId)
.Map(o => o.ToValidation<BaseError>($"FFmpegProfile {request.FFmpegProfileId} does not exist"));
} }
} }

23
ErsatzTV.Application/FFmpegProfiles/Commands/NewFFmpegProfileHandler.cs

@ -2,9 +2,11 @@
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt; using LanguageExt;
using MediatR; using MediatR;
using Microsoft.EntityFrameworkCore;
using static LanguageExt.Prelude; using static LanguageExt.Prelude;
using static ErsatzTV.Application.FFmpegProfiles.Mapper; using static ErsatzTV.Application.FFmpegProfiles.Mapper;
@ -12,24 +14,21 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
{ {
public class NewFFmpegProfileHandler : IRequestHandler<NewFFmpegProfile, FFmpegProfileViewModel> public class NewFFmpegProfileHandler : IRequestHandler<NewFFmpegProfile, FFmpegProfileViewModel>
{ {
private readonly IConfigElementRepository _configElementRepository; private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IResolutionRepository _resolutionRepository;
public NewFFmpegProfileHandler( public NewFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFactory) =>
IResolutionRepository resolutionRepository, _dbContextFactory = dbContextFactory;
IConfigElementRepository configElementRepository)
{
_resolutionRepository = resolutionRepository;
_configElementRepository = configElementRepository;
}
public async Task<FFmpegProfileViewModel> Handle(NewFFmpegProfile request, CancellationToken cancellationToken) public async Task<FFmpegProfileViewModel> Handle(NewFFmpegProfile request, CancellationToken cancellationToken)
{ {
int defaultResolutionId = await _configElementRepository await using TvContext dbContext = _dbContextFactory.CreateDbContext();
int defaultResolutionId = await dbContext.ConfigElements
.GetValue<int>(ConfigElementKey.FFmpegDefaultResolutionId) .GetValue<int>(ConfigElementKey.FFmpegDefaultResolutionId)
.IfNoneAsync(0); .IfNoneAsync(0);
List<Resolution> allResolutions = await _resolutionRepository.GetAll(); List<Resolution> allResolutions = await dbContext.Resolutions
.ToListAsync(cancellationToken);
Option<Resolution> maybeDefaultResolution = allResolutions.Find(r => r.Id == defaultResolutionId); Option<Resolution> maybeDefaultResolution = allResolutions.Find(r => r.Id == defaultResolutionId);
Resolution defaultResolution = maybeDefaultResolution.Match(identity, () => allResolutions.Head()); Resolution defaultResolution = maybeDefaultResolution.Match(identity, () => allResolutions.Head());

2
ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfile.cs

@ -22,5 +22,5 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
bool NormalizeLoudness, bool NormalizeLoudness,
int AudioChannels, int AudioChannels,
int AudioSampleRate, int AudioSampleRate,
bool NormalizeAudio) : IRequest<Either<BaseError, FFmpegProfileViewModel>>; bool NormalizeAudio) : IRequest<Either<BaseError, UpdateFFmpegProfileResult>>;
} }

71
ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfileHandler.cs

@ -2,35 +2,35 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt; using LanguageExt;
using MediatR; using MediatR;
using static ErsatzTV.Application.FFmpegProfiles.Mapper; using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.FFmpegProfiles.Commands namespace ErsatzTV.Application.FFmpegProfiles.Commands
{ {
public class public class
UpdateFFmpegProfileHandler : IRequestHandler<UpdateFFmpegProfile, Either<BaseError, FFmpegProfileViewModel>> UpdateFFmpegProfileHandler : IRequestHandler<UpdateFFmpegProfile, Either<BaseError, UpdateFFmpegProfileResult>>
{ {
private readonly IFFmpegProfileRepository _ffmpegProfileRepository; private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IResolutionRepository _resolutionRepository;
public UpdateFFmpegProfileHandler( public UpdateFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFactory) =>
IFFmpegProfileRepository ffmpegProfileRepository, _dbContextFactory = dbContextFactory;
IResolutionRepository resolutionRepository)
{
_ffmpegProfileRepository = ffmpegProfileRepository;
_resolutionRepository = resolutionRepository;
}
public Task<Either<BaseError, FFmpegProfileViewModel>> Handle( public async Task<Either<BaseError, UpdateFFmpegProfileResult>> Handle(
UpdateFFmpegProfile request, UpdateFFmpegProfile request,
CancellationToken cancellationToken) => CancellationToken cancellationToken)
Validate(request) {
.MapT(c => ApplyUpdateRequest(c, request)) await using TvContext dbContext = _dbContextFactory.CreateDbContext();
.Bind(v => v.ToEitherAsync()); Validation<BaseError, FFmpegProfile> validation = await Validate(dbContext, request);
return await validation.Apply(p => ApplyUpdateRequest(dbContext, p, request));
}
private async Task<FFmpegProfileViewModel> ApplyUpdateRequest(FFmpegProfile p, UpdateFFmpegProfile update) private async Task<UpdateFFmpegProfileResult> ApplyUpdateRequest(
TvContext dbContext,
FFmpegProfile p,
UpdateFFmpegProfile update)
{ {
p.Name = update.Name; p.Name = update.Name;
p.ThreadCount = update.ThreadCount; p.ThreadCount = update.ThreadCount;
@ -48,30 +48,37 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
p.AudioChannels = update.AudioChannels; p.AudioChannels = update.AudioChannels;
p.AudioSampleRate = update.AudioSampleRate; p.AudioSampleRate = update.AudioSampleRate;
p.NormalizeAudio = update.NormalizeAudio; p.NormalizeAudio = update.NormalizeAudio;
await _ffmpegProfileRepository.Update(p); await dbContext.SaveChangesAsync();
return ProjectToViewModel(p); return new UpdateFFmpegProfileResult(p.Id);
} }
private async Task<Validation<BaseError, FFmpegProfile>> Validate(UpdateFFmpegProfile request) => private static async Task<Validation<BaseError, FFmpegProfile>> Validate(
(await FFmpegProfileMustExist(request), ValidateName(request), ValidateThreadCount(request), TvContext dbContext,
await ResolutionMustExist(request)) UpdateFFmpegProfile request) =>
(await FFmpegProfileMustExist(dbContext, request), ValidateName(request), ValidateThreadCount(request),
await ResolutionMustExist(dbContext, request))
.Apply((ffmpegProfileToUpdate, _, _, _) => ffmpegProfileToUpdate); .Apply((ffmpegProfileToUpdate, _, _, _) => ffmpegProfileToUpdate);
private async Task<Validation<BaseError, FFmpegProfile>> FFmpegProfileMustExist( private static Task<Validation<BaseError, FFmpegProfile>> FFmpegProfileMustExist(
TvContext dbContext,
UpdateFFmpegProfile updateFFmpegProfile) => UpdateFFmpegProfile updateFFmpegProfile) =>
(await _ffmpegProfileRepository.Get(updateFFmpegProfile.FFmpegProfileId)) dbContext.FFmpegProfiles
.ToValidation<BaseError>("FFmpegProfile does not exist."); .SelectOneAsync(p => p.Id, p => p.Id == updateFFmpegProfile.FFmpegProfileId)
.Map(o => o.ToValidation<BaseError>("FFmpegProfile does not exist."));
private Validation<BaseError, string> ValidateName(UpdateFFmpegProfile updateFFmpegProfile) => private static Validation<BaseError, string> ValidateName(UpdateFFmpegProfile updateFFmpegProfile) =>
updateFFmpegProfile.NotEmpty(x => x.Name) updateFFmpegProfile.NotEmpty(x => x.Name)
.Bind(_ => updateFFmpegProfile.NotLongerThan(50)(x => x.Name)); .Bind(_ => updateFFmpegProfile.NotLongerThan(50)(x => x.Name));
private Validation<BaseError, int> ValidateThreadCount(UpdateFFmpegProfile updateFFmpegProfile) => private static Validation<BaseError, int> ValidateThreadCount(UpdateFFmpegProfile updateFFmpegProfile) =>
updateFFmpegProfile.AtLeast(0)(p => p.ThreadCount); updateFFmpegProfile.AtLeast(0)(p => p.ThreadCount);
private async Task<Validation<BaseError, int>> ResolutionMustExist(UpdateFFmpegProfile updateFFmpegProfile) => private static Task<Validation<BaseError, int>> ResolutionMustExist(
(await _resolutionRepository.Get(updateFFmpegProfile.ResolutionId)) TvContext dbContext,
.ToValidation<BaseError>($"[Resolution] {updateFFmpegProfile.ResolutionId} does not exist.") UpdateFFmpegProfile updateFFmpegProfile) =>
.Map(c => c.Id); dbContext.Resolutions
.SelectOneAsync(r => r.Id, r => r.Id == updateFFmpegProfile.ResolutionId)
.MapT(r => r.Id)
.Map(o => o.ToValidation<BaseError>($"[Resolution] {updateFFmpegProfile.ResolutionId} does not exist"));
} }
} }

4
ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfileResult.cs

@ -0,0 +1,4 @@
namespace ErsatzTV.Application.FFmpegProfiles.Commands
{
public record UpdateFFmpegProfileResult(int FFmpegProfileId) : EntityIdResult(FFmpegProfileId);
}

32
ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegSettingsHandler.cs

@ -86,35 +86,25 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
private async Task<Unit> ApplyUpdate(UpdateFFmpegSettings request) private async Task<Unit> ApplyUpdate(UpdateFFmpegSettings request)
{ {
await Upsert(ConfigElementKey.FFmpegPath, request.Settings.FFmpegPath); await _configElementRepository.Upsert(ConfigElementKey.FFmpegPath, request.Settings.FFmpegPath);
await Upsert(ConfigElementKey.FFprobePath, request.Settings.FFprobePath); await _configElementRepository.Upsert(ConfigElementKey.FFprobePath, request.Settings.FFprobePath);
await Upsert(ConfigElementKey.FFmpegDefaultProfileId, request.Settings.DefaultFFmpegProfileId.ToString()); await _configElementRepository.Upsert(
await Upsert(ConfigElementKey.FFmpegSaveReports, request.Settings.SaveReports.ToString()); ConfigElementKey.FFmpegDefaultProfileId,
request.Settings.DefaultFFmpegProfileId.ToString());
await _configElementRepository.Upsert(
ConfigElementKey.FFmpegSaveReports,
request.Settings.SaveReports.ToString());
if (request.Settings.SaveReports && !Directory.Exists(FileSystemLayout.FFmpegReportsFolder)) if (request.Settings.SaveReports && !Directory.Exists(FileSystemLayout.FFmpegReportsFolder))
{ {
Directory.CreateDirectory(FileSystemLayout.FFmpegReportsFolder); Directory.CreateDirectory(FileSystemLayout.FFmpegReportsFolder);
} }
await Upsert(ConfigElementKey.FFmpegPreferredLanguageCode, request.Settings.PreferredLanguageCode); await _configElementRepository.Upsert(
ConfigElementKey.FFmpegPreferredLanguageCode,
request.Settings.PreferredLanguageCode);
return Unit.Default; return Unit.Default;
} }
private async Task Upsert(ConfigElementKey key, string value)
{
Option<ConfigElement> maybeElement = await _configElementRepository.Get(key);
await maybeElement.Match(
ce =>
{
ce.Value = value;
return _configElementRepository.Update(ce);
},
() =>
{
var ce = new ConfigElement { Key = key.Key, Value = value };
return _configElementRepository.Add(ce);
});
}
} }
} }

20
ErsatzTV.Application/FFmpegProfiles/Queries/GetAllFFmpegProfilesHandler.cs

@ -2,22 +2,30 @@
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Infrastructure.Data;
using LanguageExt;
using MediatR; using MediatR;
using Microsoft.EntityFrameworkCore;
using static ErsatzTV.Application.FFmpegProfiles.Mapper; using static ErsatzTV.Application.FFmpegProfiles.Mapper;
namespace ErsatzTV.Application.FFmpegProfiles.Queries namespace ErsatzTV.Application.FFmpegProfiles.Queries
{ {
public class GetAllFFmpegProfilesHandler : IRequestHandler<GetAllFFmpegProfiles, List<FFmpegProfileViewModel>> public class GetAllFFmpegProfilesHandler : IRequestHandler<GetAllFFmpegProfiles, List<FFmpegProfileViewModel>>
{ {
private readonly IFFmpegProfileRepository _ffmpegProfileRepository; private readonly IDbContextFactory<TvContext> _dbContextFactory;
public GetAllFFmpegProfilesHandler(IFFmpegProfileRepository ffmpegProfileRepository) => public GetAllFFmpegProfilesHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_ffmpegProfileRepository = ffmpegProfileRepository; _dbContextFactory = dbContextFactory;
public async Task<List<FFmpegProfileViewModel>> Handle( public async Task<List<FFmpegProfileViewModel>> Handle(
GetAllFFmpegProfiles request, GetAllFFmpegProfiles request,
CancellationToken cancellationToken) => CancellationToken cancellationToken)
(await _ffmpegProfileRepository.GetAll()).Map(ProjectToViewModel).ToList(); {
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
return await dbContext.FFmpegProfiles
.Include(p => p.Resolution)
.ToListAsync(cancellationToken)
.Map(list => list.Map(ProjectToViewModel).ToList());
}
} }
} }

21
ErsatzTV.Application/FFmpegProfiles/Queries/GetFFmpegProfileByIdHandler.cs

@ -1,23 +1,30 @@
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt; using LanguageExt;
using MediatR; using MediatR;
using Microsoft.EntityFrameworkCore;
using static ErsatzTV.Application.FFmpegProfiles.Mapper; using static ErsatzTV.Application.FFmpegProfiles.Mapper;
namespace ErsatzTV.Application.FFmpegProfiles.Queries namespace ErsatzTV.Application.FFmpegProfiles.Queries
{ {
public class GetFFmpegProfileByIdHandler : IRequestHandler<GetFFmpegProfileById, Option<FFmpegProfileViewModel>> public class GetFFmpegProfileByIdHandler : IRequestHandler<GetFFmpegProfileById, Option<FFmpegProfileViewModel>>
{ {
private readonly IFFmpegProfileRepository _ffmpegProfileRepository; private readonly IDbContextFactory<TvContext> _dbContextFactory;
public GetFFmpegProfileByIdHandler(IFFmpegProfileRepository ffmpegProfileRepository) => public GetFFmpegProfileByIdHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_ffmpegProfileRepository = ffmpegProfileRepository; _dbContextFactory = dbContextFactory;
public Task<Option<FFmpegProfileViewModel>> Handle( public async Task<Option<FFmpegProfileViewModel>> Handle(
GetFFmpegProfileById request, GetFFmpegProfileById request,
CancellationToken cancellationToken) => CancellationToken cancellationToken)
_ffmpegProfileRepository.Get(request.Id) {
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
return await dbContext.FFmpegProfiles
.Include(p => p.Resolution)
.SelectOneAsync(p => p.Id, p => p.Id == request.Id)
.MapT(ProjectToViewModel); .MapT(ProjectToViewModel);
}
} }
} }

17
ErsatzTV.Application/HDHR/Commands/UpdateHDHRTunerCountHandler.cs

@ -19,27 +19,14 @@ namespace ErsatzTV.Application.HDHR.Commands
UpdateHDHRTunerCount request, UpdateHDHRTunerCount request,
CancellationToken cancellationToken) => CancellationToken cancellationToken) =>
Validate(request) Validate(request)
.MapT(_ => Upsert(ConfigElementKey.HDHRTunerCount, request.TunerCount.ToString())) .MapT(_ => _configElementRepository.Upsert(ConfigElementKey.HDHRTunerCount, request.TunerCount.ToString()))
.Bind(v => v.ToEitherAsync()); .Bind(v => v.ToEitherAsync());
private Task<Validation<BaseError, Unit>> Validate(UpdateHDHRTunerCount request) => private static Task<Validation<BaseError, Unit>> Validate(UpdateHDHRTunerCount request) =>
Optional(request.TunerCount) Optional(request.TunerCount)
.Filter(tc => tc > 0) .Filter(tc => tc > 0)
.Map(_ => Unit.Default) .Map(_ => Unit.Default)
.ToValidation<BaseError>("Tuner count must be greater than zero") .ToValidation<BaseError>("Tuner count must be greater than zero")
.AsTask(); .AsTask();
private Task<Unit> Upsert(ConfigElementKey key, string value) =>
_configElementRepository.Get(key).Match(
ce =>
{
ce.Value = value;
return _configElementRepository.Update(ce);
},
() =>
{
var ce = new ConfigElement { Key = key.Key, Value = value };
return _configElementRepository.Add(ce);
}).ToUnit();
} }
} }

60
ErsatzTV.Application/MediaCards/Queries/GetCollectionCardsHandler.cs

@ -3,23 +3,26 @@ using System.Threading.Tasks;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt; using LanguageExt;
using MediatR; using MediatR;
using Microsoft.EntityFrameworkCore;
using static ErsatzTV.Application.MediaCards.Mapper; using static ErsatzTV.Application.MediaCards.Mapper;
namespace ErsatzTV.Application.MediaCards.Queries namespace ErsatzTV.Application.MediaCards.Queries
{ {
public class GetCollectionCardsHandler : IRequestHandler<GetCollectionCards, public class GetCollectionCardsHandler :
Either<BaseError, CollectionCardResultsViewModel>> IRequestHandler<GetCollectionCards, Either<BaseError, CollectionCardResultsViewModel>>
{ {
private readonly IMediaCollectionRepository _collectionRepository; private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IMediaSourceRepository _mediaSourceRepository; private readonly IMediaSourceRepository _mediaSourceRepository;
public GetCollectionCardsHandler( public GetCollectionCardsHandler(
IMediaCollectionRepository collectionRepository, IDbContextFactory<TvContext> dbContextFactory,
IMediaSourceRepository mediaSourceRepository) IMediaSourceRepository mediaSourceRepository)
{ {
_collectionRepository = collectionRepository; _dbContextFactory = dbContextFactory;
_mediaSourceRepository = mediaSourceRepository; _mediaSourceRepository = mediaSourceRepository;
} }
@ -27,14 +30,57 @@ namespace ErsatzTV.Application.MediaCards.Queries
GetCollectionCards request, GetCollectionCards request,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Option<JellyfinMediaSource> maybeJellyfin = await _mediaSourceRepository.GetAllJellyfin() Option<JellyfinMediaSource> maybeJellyfin = await _mediaSourceRepository.GetAllJellyfin()
.Map(list => list.HeadOrNone()); .Map(list => list.HeadOrNone());
Option<EmbyMediaSource> maybeEmby = await _mediaSourceRepository.GetAllEmby() Option<EmbyMediaSource> maybeEmby = await _mediaSourceRepository.GetAllEmby()
.Map(list => list.HeadOrNone()); .Map(list => list.HeadOrNone());
return await _collectionRepository return await dbContext.Collections
.GetCollectionWithItemsUntracked(request.Id) .AsNoTracking()
.Include(c => c.CollectionItems)
.Include(c => c.MediaItems)
.ThenInclude(i => i.LibraryPath)
.Include(c => c.MediaItems)
.ThenInclude(i => (i as Movie).MovieMetadata)
.ThenInclude(mm => mm.Artwork)
.Include(c => c.MediaItems)
.ThenInclude(i => (i as Artist).ArtistMetadata)
.ThenInclude(mvm => mvm.Artwork)
.Include(c => c.MediaItems)
.ThenInclude(i => (i as MusicVideo).MusicVideoMetadata)
.ThenInclude(mvm => mvm.Artwork)
.Include(c => c.MediaItems)
.ThenInclude(i => (i as MusicVideo).Artist)
.ThenInclude(a => a.ArtistMetadata)
.Include(c => c.MediaItems)
.ThenInclude(i => (i as Show).ShowMetadata)
.ThenInclude(sm => sm.Artwork)
.Include(c => c.MediaItems)
.ThenInclude(i => (i as Season).SeasonMetadata)
.ThenInclude(sm => sm.Artwork)
.Include(c => c.MediaItems)
.ThenInclude(i => (i as Season).Show)
.ThenInclude(s => s.ShowMetadata)
.Include(c => c.MediaItems)
.ThenInclude(i => (i as Episode).EpisodeMetadata)
.ThenInclude(em => em.Artwork)
.Include(c => c.MediaItems)
.ThenInclude(i => (i as Episode).EpisodeMetadata)
.ThenInclude(em => em.Directors)
.Include(c => c.MediaItems)
.ThenInclude(i => (i as Episode).EpisodeMetadata)
.ThenInclude(em => em.Writers)
.Include(c => c.MediaItems)
.ThenInclude(i => (i as Episode).Season)
.ThenInclude(s => s.Show)
.ThenInclude(s => s.ShowMetadata)
.Include(c => c.MediaItems)
.ThenInclude(i => (i as Episode).Season)
.ThenInclude(s => s.SeasonMetadata)
.SelectOneAsync(c => c.Id, c => c.Id == request.Id)
.Map(c => c.ToEither(BaseError.New("Unable to load collection"))) .Map(c => c.ToEither(BaseError.New("Unable to load collection")))
.MapT(c => ProjectToViewModel(c, maybeJellyfin, maybeEmby)); .MapT(c => ProjectToViewModel(c, maybeJellyfin, maybeEmby));
} }

64
ErsatzTV.Application/MediaCollections/Commands/AddArtistToCollectionHandler.cs

@ -5,41 +5,47 @@ using ErsatzTV.Application.Playouts.Commands;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt; using LanguageExt;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.MediaCollections.Commands namespace ErsatzTV.Application.MediaCollections.Commands
{ {
public class public class AddArtistToCollectionHandler :
AddArtistToCollectionHandler : MediatR.IRequestHandler<AddArtistToCollection, Either<BaseError, Unit>> MediatR.IRequestHandler<AddArtistToCollection, Either<BaseError, Unit>>
{ {
private readonly IArtistRepository _artistRepository;
private readonly ChannelWriter<IBackgroundServiceRequest> _channel; private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IMediaCollectionRepository _mediaCollectionRepository; private readonly IMediaCollectionRepository _mediaCollectionRepository;
public AddArtistToCollectionHandler( public AddArtistToCollectionHandler(
IDbContextFactory<TvContext> dbContextFactory,
IMediaCollectionRepository mediaCollectionRepository, IMediaCollectionRepository mediaCollectionRepository,
IArtistRepository artistRepository,
ChannelWriter<IBackgroundServiceRequest> channel) ChannelWriter<IBackgroundServiceRequest> channel)
{ {
_dbContextFactory = dbContextFactory;
_mediaCollectionRepository = mediaCollectionRepository; _mediaCollectionRepository = mediaCollectionRepository;
_artistRepository = artistRepository;
_channel = channel; _channel = channel;
} }
public Task<Either<BaseError, Unit>> Handle( public async Task<Either<BaseError, Unit>> Handle(
AddArtistToCollection request, AddArtistToCollection request,
CancellationToken cancellationToken) => CancellationToken cancellationToken)
Validate(request) {
.MapT(_ => ApplyAddArtistRequest(request)) await using TvContext dbContext = _dbContextFactory.CreateDbContext();
.Bind(v => v.ToEitherAsync()); Validation<BaseError, Parameters> validation = await Validate(dbContext, request);
return await validation.Apply(parameters => ApplyAddArtistRequest(dbContext, parameters));
}
private async Task<Unit> ApplyAddArtistRequest(AddArtistToCollection request) private async Task<Unit> ApplyAddArtistRequest(TvContext dbContext, Parameters parameters)
{ {
if (await _mediaCollectionRepository.AddMediaItem(request.CollectionId, request.ArtistId)) parameters.Collection.MediaItems.Add(parameters.Artist);
if (await dbContext.SaveChangesAsync() > 0)
{ {
// rebuild all playouts that use this collection // rebuild all playouts that use this collection
foreach (int playoutId in await _mediaCollectionRepository foreach (int playoutId in await _mediaCollectionRepository
.PlayoutIdsUsingCollection(request.CollectionId)) .PlayoutIdsUsingCollection(parameters.Collection.Id))
{ {
await _channel.WriteAsync(new BuildPlayout(playoutId, true)); await _channel.WriteAsync(new BuildPlayout(playoutId, true));
} }
@ -48,21 +54,27 @@ namespace ErsatzTV.Application.MediaCollections.Commands
return Unit.Default; return Unit.Default;
} }
private async Task<Validation<BaseError, Unit>> Validate(AddArtistToCollection request) => private static async Task<Validation<BaseError, Parameters>> Validate(
(await CollectionMustExist(request), await ValidateArtist(request)) TvContext dbContext,
.Apply((_, _) => Unit.Default); AddArtistToCollection request) =>
(await CollectionMustExist(dbContext, request), await ValidateArtist(dbContext, request))
.Apply((collection, artist) => new Parameters(collection, artist));
private Task<Validation<BaseError, Unit>> CollectionMustExist(AddArtistToCollection request) => private static Task<Validation<BaseError, Collection>> CollectionMustExist(
_mediaCollectionRepository.GetCollectionWithItems(request.CollectionId) TvContext dbContext,
.MapT(_ => Unit.Default) AddArtistToCollection request) =>
.Map(v => v.ToValidation<BaseError>("Collection does not exist.")); dbContext.Collections
.Include(c => c.MediaItems)
.SelectOneAsync(c => c.Id, c => c.Id == request.CollectionId)
.Map(o => o.ToValidation<BaseError>("Collection does not exist."));
private Task<Validation<BaseError, Unit>> ValidateArtist(AddArtistToCollection request) => private static Task<Validation<BaseError, Artist>> ValidateArtist(
LoadArtist(request) TvContext dbContext,
.MapT(_ => Unit.Default) AddArtistToCollection request) =>
.Map(v => v.ToValidation<BaseError>("Music video does not exist")); dbContext.Artists
.SelectOneAsync(a => a.Id, a => a.Id == request.ArtistId)
.Map(o => o.ToValidation<BaseError>("Music video does not exist"));
private Task<Option<Artist>> LoadArtist(AddArtistToCollection request) => private record Parameters(Collection Collection, Artist Artist);
_artistRepository.GetArtist(request.ArtistId);
} }
} }

65
ErsatzTV.Application/MediaCollections/Commands/AddEpisodeToCollectionHandler.cs

@ -3,42 +3,49 @@ using System.Threading.Channels;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Application.Playouts.Commands; using ErsatzTV.Application.Playouts.Commands;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt; using LanguageExt;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.MediaCollections.Commands namespace ErsatzTV.Application.MediaCollections.Commands
{ {
public class public class AddEpisodeToCollectionHandler :
AddEpisodeToCollectionHandler : MediatR.IRequestHandler<AddEpisodeToCollection, Either<BaseError, Unit>> MediatR.IRequestHandler<AddEpisodeToCollection, Either<BaseError, Unit>>
{ {
private readonly ChannelWriter<IBackgroundServiceRequest> _channel; private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IMediaCollectionRepository _mediaCollectionRepository; private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly ITelevisionRepository _televisionRepository;
public AddEpisodeToCollectionHandler( public AddEpisodeToCollectionHandler(
IDbContextFactory<TvContext> dbContextFactory,
IMediaCollectionRepository mediaCollectionRepository, IMediaCollectionRepository mediaCollectionRepository,
ITelevisionRepository televisionRepository,
ChannelWriter<IBackgroundServiceRequest> channel) ChannelWriter<IBackgroundServiceRequest> channel)
{ {
_dbContextFactory = dbContextFactory;
_mediaCollectionRepository = mediaCollectionRepository; _mediaCollectionRepository = mediaCollectionRepository;
_televisionRepository = televisionRepository;
_channel = channel; _channel = channel;
} }
public Task<Either<BaseError, Unit>> Handle( public async Task<Either<BaseError, Unit>> Handle(
AddEpisodeToCollection request, AddEpisodeToCollection request,
CancellationToken cancellationToken) => CancellationToken cancellationToken)
Validate(request) {
.MapT(_ => ApplyAddTelevisionEpisodeRequest(request)) await using TvContext dbContext = _dbContextFactory.CreateDbContext();
.Bind(v => v.ToEitherAsync()); Validation<BaseError, Parameters> validation = await Validate(dbContext, request);
return await validation.Apply(parameters => ApplyAddTelevisionEpisodeRequest(dbContext, parameters));
}
private async Task<Unit> ApplyAddTelevisionEpisodeRequest(AddEpisodeToCollection request) private async Task<Unit> ApplyAddTelevisionEpisodeRequest(TvContext dbContext, Parameters parameters)
{ {
if (await _mediaCollectionRepository.AddMediaItem(request.CollectionId, request.EpisodeId)) parameters.Collection.MediaItems.Add(parameters.Episode);
if (await dbContext.SaveChangesAsync() > 0)
{ {
// rebuild all playouts that use this collection // rebuild all playouts that use this collection
foreach (int playoutId in await _mediaCollectionRepository foreach (int playoutId in await _mediaCollectionRepository
.PlayoutIdsUsingCollection(request.CollectionId)) .PlayoutIdsUsingCollection(parameters.Collection.Id))
{ {
await _channel.WriteAsync(new BuildPlayout(playoutId, true)); await _channel.WriteAsync(new BuildPlayout(playoutId, true));
} }
@ -47,21 +54,27 @@ namespace ErsatzTV.Application.MediaCollections.Commands
return Unit.Default; return Unit.Default;
} }
private async Task<Validation<BaseError, Unit>> Validate(AddEpisodeToCollection request) => private static async Task<Validation<BaseError, Parameters>> Validate(
(await CollectionMustExist(request), await ValidateEpisode(request)) TvContext dbContext,
.Apply((_, _) => Unit.Default); AddEpisodeToCollection request) =>
(await CollectionMustExist(dbContext, request), await ValidateEpisode(dbContext, request))
.Apply((collection, episode) => new Parameters(collection, episode));
private Task<Validation<BaseError, Unit>> CollectionMustExist(AddEpisodeToCollection request) => private static Task<Validation<BaseError, Collection>> CollectionMustExist(
_mediaCollectionRepository.Get(request.CollectionId) TvContext dbContext,
.MapT(_ => Unit.Default) AddEpisodeToCollection request) =>
.Map(v => v.ToValidation<BaseError>("Collection does not exist.")); dbContext.Collections
.Include(c => c.MediaItems)
.SelectOneAsync(c => c.Id, c => c.Id == request.CollectionId)
.Map(o => o.ToValidation<BaseError>("Collection does not exist."));
private Task<Validation<BaseError, Unit>> ValidateEpisode(AddEpisodeToCollection request) => private static Task<Validation<BaseError, Episode>> ValidateEpisode(
LoadTelevisionEpisode(request) TvContext dbContext,
.MapT(_ => Unit.Default) AddEpisodeToCollection request) =>
.Map(v => v.ToValidation<BaseError>("Episode does not exist")); dbContext.Episodes
.SelectOneAsync(e => e.Id, e => e.Id == request.EpisodeId)
.Map(o => o.ToValidation<BaseError>("Episode does not exist"));
private Task<Option<int>> LoadTelevisionEpisode(AddEpisodeToCollection request) => private record Parameters(Collection Collection, Episode Episode);
_televisionRepository.GetEpisode(request.EpisodeId).MapT(e => e.Id);
} }
} }

59
ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollectionHandler.cs

@ -1,43 +1,54 @@
using System.Linq; using System.Collections.Generic;
using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Channels; using System.Threading.Channels;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Application.Playouts.Commands; using ErsatzTV.Application.Playouts.Commands;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt; using LanguageExt;
using Microsoft.EntityFrameworkCore;
using static LanguageExt.Prelude; using static LanguageExt.Prelude;
namespace ErsatzTV.Application.MediaCollections.Commands namespace ErsatzTV.Application.MediaCollections.Commands
{ {
public class public class AddItemsToCollectionHandler :
AddItemsToCollectionHandler : MediatR.IRequestHandler<AddItemsToCollection, Either<BaseError, Unit>> MediatR.IRequestHandler<AddItemsToCollection, Either<BaseError, Unit>>
{ {
private readonly ChannelWriter<IBackgroundServiceRequest> _channel; private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IMediaCollectionRepository _mediaCollectionRepository; private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly IMovieRepository _movieRepository; private readonly IMovieRepository _movieRepository;
private readonly ITelevisionRepository _televisionRepository; private readonly ITelevisionRepository _televisionRepository;
public AddItemsToCollectionHandler( public AddItemsToCollectionHandler(
IDbContextFactory<TvContext> dbContextFactory,
IMediaCollectionRepository mediaCollectionRepository, IMediaCollectionRepository mediaCollectionRepository,
IMovieRepository movieRepository, IMovieRepository movieRepository,
ITelevisionRepository televisionRepository, ITelevisionRepository televisionRepository,
ChannelWriter<IBackgroundServiceRequest> channel) ChannelWriter<IBackgroundServiceRequest> channel)
{ {
_dbContextFactory = dbContextFactory;
_mediaCollectionRepository = mediaCollectionRepository; _mediaCollectionRepository = mediaCollectionRepository;
_movieRepository = movieRepository; _movieRepository = movieRepository;
_televisionRepository = televisionRepository; _televisionRepository = televisionRepository;
_channel = channel; _channel = channel;
} }
public Task<Either<BaseError, Unit>> Handle( public async Task<Either<BaseError, Unit>> Handle(
AddItemsToCollection request, AddItemsToCollection request,
CancellationToken cancellationToken) => CancellationToken cancellationToken)
Validate(request) {
.MapT(_ => ApplyAddItemsRequest(request)) await using TvContext dbContext = _dbContextFactory.CreateDbContext();
.Bind(v => v.ToEitherAsync());
Validation<BaseError, Collection> validation = await Validate(dbContext, request);
return await validation.Apply(c => ApplyAddItemsRequest(dbContext, c, request));
}
private async Task<Unit> ApplyAddItemsRequest(AddItemsToCollection request) private async Task<Unit> ApplyAddItemsRequest(TvContext dbContext, Collection collection, AddItemsToCollection request)
{ {
var allItems = request.MovieIds var allItems = request.MovieIds
.Append(request.ShowIds) .Append(request.ShowIds)
@ -46,7 +57,14 @@ namespace ErsatzTV.Application.MediaCollections.Commands
.Append(request.MusicVideoIds) .Append(request.MusicVideoIds)
.ToList(); .ToList();
if (await _mediaCollectionRepository.AddMediaItems(request.CollectionId, allItems)) var toAddIds = allItems.Where(item => collection.MediaItems.All(mi => mi.Id != item)).ToList();
List<MediaItem> toAdd = await dbContext.MediaItems
.Filter(mi => toAddIds.Contains(mi.Id))
.ToListAsync();
collection.MediaItems.AddRange(toAdd);
if (await dbContext.SaveChangesAsync() > 0)
{ {
// rebuild all playouts that use this collection // rebuild all playouts that use this collection
foreach (int playoutId in await _mediaCollectionRepository foreach (int playoutId in await _mediaCollectionRepository
@ -59,15 +77,20 @@ namespace ErsatzTV.Application.MediaCollections.Commands
return Unit.Default; return Unit.Default;
} }
private async Task<Validation<BaseError, Unit>> Validate(AddItemsToCollection request) => private async Task<Validation<BaseError, Collection>> Validate(TvContext dbContext, AddItemsToCollection request) =>
(await CollectionMustExist(request), await ValidateMovies(request), await ValidateShows(request), (await CollectionMustExist(dbContext, request),
await ValidateMovies(request),
await ValidateShows(request),
await ValidateEpisodes(request)) await ValidateEpisodes(request))
.Apply((_, _, _, _) => Unit.Default); .Apply((collection, _, _, _) => collection);
private Task<Validation<BaseError, Unit>> CollectionMustExist(AddItemsToCollection request) => private static Task<Validation<BaseError, Collection>> CollectionMustExist(
_mediaCollectionRepository.GetCollectionWithItems(request.CollectionId) TvContext dbContext,
.MapT(_ => Unit.Default) AddItemsToCollection request) =>
.Map(v => v.ToValidation<BaseError>("Collection does not exist.")); dbContext.Collections
.Include(c => c.MediaItems)
.SelectOneAsync(c => c.Id, c => c.Id == request.CollectionId)
.Map(o => o.ToValidation<BaseError>("Collection does not exist."));
private Task<Validation<BaseError, Unit>> ValidateMovies(AddItemsToCollection request) => private Task<Validation<BaseError, Unit>> ValidateMovies(AddItemsToCollection request) =>
_movieRepository.AllMoviesExist(request.MovieIds) _movieRepository.AllMoviesExist(request.MovieIds)
@ -88,6 +111,6 @@ namespace ErsatzTV.Application.MediaCollections.Commands
.Map(Optional) .Map(Optional)
.Filter(v => v == true) .Filter(v => v == true)
.MapT(_ => Unit.Default) .MapT(_ => Unit.Default)
.Map(v => v.ToValidation<BaseError>("Show does not exist")); .Map(v => v.ToValidation<BaseError>("Episode does not exist"));
} }
} }

64
ErsatzTV.Application/MediaCollections/Commands/AddMovieToCollectionHandler.cs

@ -5,41 +5,47 @@ using ErsatzTV.Application.Playouts.Commands;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt; using LanguageExt;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.MediaCollections.Commands namespace ErsatzTV.Application.MediaCollections.Commands
{ {
public class public class AddMovieToCollectionHandler :
AddMovieToCollectionHandler : MediatR.IRequestHandler<AddMovieToCollection, Either<BaseError, Unit>> MediatR.IRequestHandler<AddMovieToCollection, Either<BaseError, Unit>>
{ {
private readonly ChannelWriter<IBackgroundServiceRequest> _channel; private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IMediaCollectionRepository _mediaCollectionRepository; private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly IMovieRepository _movieRepository;
public AddMovieToCollectionHandler( public AddMovieToCollectionHandler(
IDbContextFactory<TvContext> dbContextFactory,
IMediaCollectionRepository mediaCollectionRepository, IMediaCollectionRepository mediaCollectionRepository,
IMovieRepository movieRepository,
ChannelWriter<IBackgroundServiceRequest> channel) ChannelWriter<IBackgroundServiceRequest> channel)
{ {
_dbContextFactory = dbContextFactory;
_mediaCollectionRepository = mediaCollectionRepository; _mediaCollectionRepository = mediaCollectionRepository;
_movieRepository = movieRepository;
_channel = channel; _channel = channel;
} }
public Task<Either<BaseError, Unit>> Handle( public async Task<Either<BaseError, Unit>> Handle(
AddMovieToCollection request, AddMovieToCollection request,
CancellationToken cancellationToken) => CancellationToken cancellationToken)
Validate(request) {
.MapT(_ => ApplyAddMoviesRequest(request)) await using TvContext dbContext = _dbContextFactory.CreateDbContext();
.Bind(v => v.ToEitherAsync()); Validation<BaseError, Parameters> validation = await Validate(dbContext, request);
return await validation.Apply(parameters => ApplyAddMovieRequest(dbContext, parameters));
}
private async Task<Unit> ApplyAddMoviesRequest(AddMovieToCollection request) private async Task<Unit> ApplyAddMovieRequest(TvContext dbContext, Parameters parameters)
{ {
if (await _mediaCollectionRepository.AddMediaItem(request.CollectionId, request.MovieId)) parameters.Collection.MediaItems.Add(parameters.Movie);
if (await dbContext.SaveChangesAsync() > 0)
{ {
// rebuild all playouts that use this collection // rebuild all playouts that use this collection
foreach (int playoutId in await _mediaCollectionRepository foreach (int playoutId in await _mediaCollectionRepository
.PlayoutIdsUsingCollection(request.CollectionId)) .PlayoutIdsUsingCollection(parameters.Collection.Id))
{ {
await _channel.WriteAsync(new BuildPlayout(playoutId, true)); await _channel.WriteAsync(new BuildPlayout(playoutId, true));
} }
@ -48,21 +54,27 @@ namespace ErsatzTV.Application.MediaCollections.Commands
return Unit.Default; return Unit.Default;
} }
private async Task<Validation<BaseError, Unit>> Validate(AddMovieToCollection request) => private static async Task<Validation<BaseError, Parameters>> Validate(
(await CollectionMustExist(request), await ValidateMovies(request)) TvContext dbContext,
.Apply((_, _) => Unit.Default); AddMovieToCollection request) =>
(await CollectionMustExist(dbContext, request), await ValidateMovie(dbContext, request))
.Apply((collection, episode) => new Parameters(collection, episode));
private Task<Validation<BaseError, Unit>> CollectionMustExist(AddMovieToCollection request) => private static Task<Validation<BaseError, Collection>> CollectionMustExist(
_mediaCollectionRepository.GetCollectionWithItems(request.CollectionId) TvContext dbContext,
.MapT(_ => Unit.Default) AddMovieToCollection request) =>
.Map(v => v.ToValidation<BaseError>("Collection does not exist.")); dbContext.Collections
.Include(c => c.MediaItems)
.SelectOneAsync(c => c.Id, c => c.Id == request.CollectionId)
.Map(o => o.ToValidation<BaseError>("Collection does not exist."));
private Task<Validation<BaseError, Unit>> ValidateMovies(AddMovieToCollection request) => private static Task<Validation<BaseError, Movie>> ValidateMovie(
LoadMovie(request) TvContext dbContext,
.MapT(_ => Unit.Default) AddMovieToCollection request) =>
.Map(v => v.ToValidation<BaseError>("Movie does not exist")); dbContext.Movies
.SelectOneAsync(m => m.Id, e => e.Id == request.MovieId)
.Map(o => o.ToValidation<BaseError>("Movie does not exist"));
private Task<Option<Movie>> LoadMovie(AddMovieToCollection request) => private record Parameters(Collection Collection, Movie Movie);
_movieRepository.GetMovie(request.MovieId);
} }
} }

64
ErsatzTV.Application/MediaCollections/Commands/AddMusicVideoToCollectionHandler.cs

@ -5,41 +5,47 @@ using ErsatzTV.Application.Playouts.Commands;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt; using LanguageExt;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.MediaCollections.Commands namespace ErsatzTV.Application.MediaCollections.Commands
{ {
public class public class AddMusicVideoToCollectionHandler :
AddMusicVideoToCollectionHandler : MediatR.IRequestHandler<AddMusicVideoToCollection, Either<BaseError, Unit>> MediatR.IRequestHandler<AddMusicVideoToCollection, Either<BaseError, Unit>>
{ {
private readonly ChannelWriter<IBackgroundServiceRequest> _channel; private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IMediaCollectionRepository _mediaCollectionRepository; private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly IMusicVideoRepository _musicVideoRepository;
public AddMusicVideoToCollectionHandler( public AddMusicVideoToCollectionHandler(
IDbContextFactory<TvContext> dbContextFactory,
IMediaCollectionRepository mediaCollectionRepository, IMediaCollectionRepository mediaCollectionRepository,
IMusicVideoRepository musicVideoRepository,
ChannelWriter<IBackgroundServiceRequest> channel) ChannelWriter<IBackgroundServiceRequest> channel)
{ {
_dbContextFactory = dbContextFactory;
_mediaCollectionRepository = mediaCollectionRepository; _mediaCollectionRepository = mediaCollectionRepository;
_musicVideoRepository = musicVideoRepository;
_channel = channel; _channel = channel;
} }
public Task<Either<BaseError, Unit>> Handle( public async Task<Either<BaseError, Unit>> Handle(
AddMusicVideoToCollection request, AddMusicVideoToCollection request,
CancellationToken cancellationToken) => CancellationToken cancellationToken)
Validate(request) {
.MapT(_ => ApplyAddMusicVideoRequest(request)) await using TvContext dbContext = _dbContextFactory.CreateDbContext();
.Bind(v => v.ToEitherAsync()); Validation<BaseError, Parameters> validation = await Validate(dbContext, request);
return await validation.Apply(parameters => ApplyAddMusicVideoRequest(dbContext, parameters));
}
private async Task<Unit> ApplyAddMusicVideoRequest(AddMusicVideoToCollection request) private async Task<Unit> ApplyAddMusicVideoRequest(TvContext dbContext, Parameters parameters)
{ {
if (await _mediaCollectionRepository.AddMediaItem(request.CollectionId, request.MusicVideoId)) parameters.Collection.MediaItems.Add(parameters.MusicVideo);
if (await dbContext.SaveChangesAsync() > 0)
{ {
// rebuild all playouts that use this collection // rebuild all playouts that use this collection
foreach (int playoutId in await _mediaCollectionRepository foreach (int playoutId in await _mediaCollectionRepository
.PlayoutIdsUsingCollection(request.CollectionId)) .PlayoutIdsUsingCollection(parameters.Collection.Id))
{ {
await _channel.WriteAsync(new BuildPlayout(playoutId, true)); await _channel.WriteAsync(new BuildPlayout(playoutId, true));
} }
@ -48,21 +54,27 @@ namespace ErsatzTV.Application.MediaCollections.Commands
return Unit.Default; return Unit.Default;
} }
private async Task<Validation<BaseError, Unit>> Validate(AddMusicVideoToCollection request) => private static async Task<Validation<BaseError, Parameters>> Validate(
(await CollectionMustExist(request), await ValidateMusicVideo(request)) TvContext dbContext,
.Apply((_, _) => Unit.Default); AddMusicVideoToCollection request) =>
(await CollectionMustExist(dbContext, request), await ValidateMusicVideo(dbContext, request))
.Apply((collection, episode) => new Parameters(collection, episode));
private Task<Validation<BaseError, Unit>> CollectionMustExist(AddMusicVideoToCollection request) => private static Task<Validation<BaseError, Collection>> CollectionMustExist(
_mediaCollectionRepository.GetCollectionWithItems(request.CollectionId) TvContext dbContext,
.MapT(_ => Unit.Default) AddMusicVideoToCollection request) =>
.Map(v => v.ToValidation<BaseError>("Collection does not exist.")); dbContext.Collections
.Include(c => c.MediaItems)
.SelectOneAsync(c => c.Id, c => c.Id == request.CollectionId)
.Map(o => o.ToValidation<BaseError>("Collection does not exist."));
private Task<Validation<BaseError, Unit>> ValidateMusicVideo(AddMusicVideoToCollection request) => private static Task<Validation<BaseError, MusicVideo>> ValidateMusicVideo(
LoadMusicVideo(request) TvContext dbContext,
.MapT(_ => Unit.Default) AddMusicVideoToCollection request) =>
.Map(v => v.ToValidation<BaseError>("Music video does not exist")); dbContext.MusicVideos
.SelectOneAsync(m => m.Id, e => e.Id == request.MusicVideoId)
.Map(o => o.ToValidation<BaseError>("MusicVideo does not exist"));
private Task<Option<MusicVideo>> LoadMusicVideo(AddMusicVideoToCollection request) => private record Parameters(Collection Collection, MusicVideo MusicVideo);
_musicVideoRepository.GetMusicVideo(request.MusicVideoId);
} }
} }

65
ErsatzTV.Application/MediaCollections/Commands/AddSeasonToCollectionHandler.cs

@ -5,41 +5,47 @@ using ErsatzTV.Application.Playouts.Commands;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt; using LanguageExt;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.MediaCollections.Commands namespace ErsatzTV.Application.MediaCollections.Commands
{ {
public class public class AddSeasonToCollectionHandler :
AddSeasonToCollectionHandler : MediatR.IRequestHandler<AddSeasonToCollection, Either<BaseError, Unit>> MediatR.IRequestHandler<AddSeasonToCollection, Either<BaseError, Unit>>
{ {
private readonly ChannelWriter<IBackgroundServiceRequest> _channel; private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IMediaCollectionRepository _mediaCollectionRepository; private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly ITelevisionRepository _televisionRepository;
public AddSeasonToCollectionHandler( public AddSeasonToCollectionHandler(
IDbContextFactory<TvContext> dbContextFactory,
IMediaCollectionRepository mediaCollectionRepository, IMediaCollectionRepository mediaCollectionRepository,
ITelevisionRepository televisionRepository,
ChannelWriter<IBackgroundServiceRequest> channel) ChannelWriter<IBackgroundServiceRequest> channel)
{ {
_dbContextFactory = dbContextFactory;
_mediaCollectionRepository = mediaCollectionRepository; _mediaCollectionRepository = mediaCollectionRepository;
_televisionRepository = televisionRepository;
_channel = channel; _channel = channel;
} }
public Task<Either<BaseError, Unit>> Handle( public async Task<Either<BaseError, Unit>> Handle(
AddSeasonToCollection request, AddSeasonToCollection request,
CancellationToken cancellationToken) => CancellationToken cancellationToken)
Validate(request) {
.MapT(_ => ApplyAddTelevisionSeasonRequest(request)) await using TvContext dbContext = _dbContextFactory.CreateDbContext();
.Bind(v => v.ToEitherAsync()); Validation<BaseError, Parameters> validation = await Validate(dbContext, request);
return await validation.Apply(parameters => ApplyAddSeasonRequest(dbContext, parameters));
}
private async Task<Unit> ApplyAddTelevisionSeasonRequest(AddSeasonToCollection request) private async Task<Unit> ApplyAddSeasonRequest(TvContext dbContext, Parameters parameters)
{ {
if (await _mediaCollectionRepository.AddMediaItem(request.CollectionId, request.SeasonId)) parameters.Collection.MediaItems.Add(parameters.Season);
if (await dbContext.SaveChangesAsync() > 0)
{ {
// rebuild all playouts that use this collection // rebuild all playouts that use this collection
foreach (int playoutId in await _mediaCollectionRepository foreach (int playoutId in await _mediaCollectionRepository
.PlayoutIdsUsingCollection(request.CollectionId)) .PlayoutIdsUsingCollection(parameters.Collection.Id))
{ {
await _channel.WriteAsync(new BuildPlayout(playoutId, true)); await _channel.WriteAsync(new BuildPlayout(playoutId, true));
} }
@ -48,22 +54,27 @@ namespace ErsatzTV.Application.MediaCollections.Commands
return Unit.Default; return Unit.Default;
} }
private async Task<Validation<BaseError, Unit>> Validate(AddSeasonToCollection request) => private static async Task<Validation<BaseError, Parameters>> Validate(
(await CollectionMustExist(request), await ValidateSeason(request)) TvContext dbContext,
.Apply((_, _) => Unit.Default); AddSeasonToCollection request) =>
(await CollectionMustExist(dbContext, request), await ValidateSeason(dbContext, request))
private Task<Validation<BaseError, Unit>> CollectionMustExist(AddSeasonToCollection request) => .Apply((collection, episode) => new Parameters(collection, episode));
_mediaCollectionRepository.GetCollectionWithItems(request.CollectionId)
.MapT(_ => Unit.Default)
.Map(v => v.ToValidation<BaseError>("Collection does not exist."));
private Task<Validation<BaseError, Unit>> ValidateSeason(AddSeasonToCollection request) => private static Task<Validation<BaseError, Collection>> CollectionMustExist(
LoadTelevisionSeason(request) TvContext dbContext,
.MapT(_ => Unit.Default) AddSeasonToCollection request) =>
.Map(v => v.ToValidation<BaseError>("Season does not exist")); dbContext.Collections
.Include(c => c.MediaItems)
.SelectOneAsync(c => c.Id, c => c.Id == request.CollectionId)
.Map(o => o.ToValidation<BaseError>("Collection does not exist."));
private Task<Option<Season>> LoadTelevisionSeason( private static Task<Validation<BaseError, Season>> ValidateSeason(
TvContext dbContext,
AddSeasonToCollection request) => AddSeasonToCollection request) =>
_televisionRepository.GetSeason(request.SeasonId); dbContext.Seasons
.SelectOneAsync(m => m.Id, e => e.Id == request.SeasonId)
.Map(o => o.ToValidation<BaseError>("Season does not exist"));
private record Parameters(Collection Collection, Season Season);
} }
} }

67
ErsatzTV.Application/MediaCollections/Commands/AddShowToCollectionHandler.cs

@ -5,65 +5,76 @@ using ErsatzTV.Application.Playouts.Commands;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt; using LanguageExt;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.MediaCollections.Commands namespace ErsatzTV.Application.MediaCollections.Commands
{ {
public class AddShowToCollectionHandler : MediatR.IRequestHandler<AddShowToCollection, Either<BaseError, Unit>> public class AddShowToCollectionHandler :
MediatR.IRequestHandler<AddShowToCollection, Either<BaseError, Unit>>
{ {
private readonly ChannelWriter<IBackgroundServiceRequest> _channel; private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IMediaCollectionRepository _mediaCollectionRepository; private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly ITelevisionRepository _televisionRepository;
public AddShowToCollectionHandler( public AddShowToCollectionHandler(
IDbContextFactory<TvContext> dbContextFactory,
IMediaCollectionRepository mediaCollectionRepository, IMediaCollectionRepository mediaCollectionRepository,
ITelevisionRepository televisionRepository,
ChannelWriter<IBackgroundServiceRequest> channel) ChannelWriter<IBackgroundServiceRequest> channel)
{ {
_dbContextFactory = dbContextFactory;
_mediaCollectionRepository = mediaCollectionRepository; _mediaCollectionRepository = mediaCollectionRepository;
_televisionRepository = televisionRepository;
_channel = channel; _channel = channel;
} }
public Task<Either<BaseError, Unit>> Handle( public async Task<Either<BaseError, Unit>> Handle(
AddShowToCollection request, AddShowToCollection request,
CancellationToken cancellationToken) => CancellationToken cancellationToken)
Validate(request)
.MapT(_ => ApplyAddTelevisionShowRequest(request))
.Bind(v => v.ToEitherAsync());
private async Task<Unit> ApplyAddTelevisionShowRequest(AddShowToCollection request)
{ {
var result = new Unit(); await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Validation<BaseError, Parameters> validation = await Validate(dbContext, request);
return await validation.Apply(parameters => ApplyAddShowRequest(dbContext, parameters));
}
if (await _mediaCollectionRepository.AddMediaItem(request.CollectionId, request.ShowId)) private async Task<Unit> ApplyAddShowRequest(TvContext dbContext, Parameters parameters)
{
parameters.Collection.MediaItems.Add(parameters.Show);
if (await dbContext.SaveChangesAsync() > 0)
{ {
// rebuild all playouts that use this collection // rebuild all playouts that use this collection
foreach (int playoutId in await _mediaCollectionRepository foreach (int playoutId in await _mediaCollectionRepository
.PlayoutIdsUsingCollection(request.CollectionId)) .PlayoutIdsUsingCollection(parameters.Collection.Id))
{ {
await _channel.WriteAsync(new BuildPlayout(playoutId, true)); await _channel.WriteAsync(new BuildPlayout(playoutId, true));
} }
} }
return result; return Unit.Default;
} }
private async Task<Validation<BaseError, Unit>> Validate(AddShowToCollection request) => private static async Task<Validation<BaseError, Parameters>> Validate(
(await CollectionMustExist(request), await ValidateShow(request)) TvContext dbContext,
.Apply((_, _) => Unit.Default); AddShowToCollection request) =>
(await CollectionMustExist(dbContext, request), await ValidateShow(dbContext, request))
.Apply((collection, episode) => new Parameters(collection, episode));
private Task<Validation<BaseError, Unit>> CollectionMustExist(AddShowToCollection request) => private static Task<Validation<BaseError, Collection>> CollectionMustExist(
_mediaCollectionRepository.GetCollectionWithItems(request.CollectionId) TvContext dbContext,
.MapT(_ => Unit.Default) AddShowToCollection request) =>
.Map(v => v.ToValidation<BaseError>("Collection does not exist.")); dbContext.Collections
.Include(c => c.MediaItems)
.SelectOneAsync(c => c.Id, c => c.Id == request.CollectionId)
.Map(o => o.ToValidation<BaseError>("Collection does not exist."));
private Task<Validation<BaseError, Unit>> ValidateShow(AddShowToCollection request) => private static Task<Validation<BaseError, Show>> ValidateShow(
LoadTelevisionShow(request) TvContext dbContext,
.MapT(_ => Unit.Default) AddShowToCollection request) =>
.Map(v => v.ToValidation<BaseError>("Show does not exist")); dbContext.Shows
.SelectOneAsync(m => m.Id, e => e.Id == request.ShowId)
.Map(o => o.ToValidation<BaseError>("Show does not exist"));
private Task<Option<Show>> LoadTelevisionShow(AddShowToCollection request) => private record Parameters(Collection Collection, Show Show);
_televisionRepository.GetShow(request.ShowId);
} }
} }

49
ErsatzTV.Application/MediaCollections/Commands/CreateCollectionHandler.cs

@ -1,45 +1,60 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Infrastructure.Data;
using LanguageExt; using LanguageExt;
using MediatR; using MediatR;
using Microsoft.EntityFrameworkCore;
using static ErsatzTV.Application.MediaCollections.Mapper; using static ErsatzTV.Application.MediaCollections.Mapper;
using static LanguageExt.Prelude; using static LanguageExt.Prelude;
namespace ErsatzTV.Application.MediaCollections.Commands namespace ErsatzTV.Application.MediaCollections.Commands
{ {
public class public class CreateCollectionHandler :
CreateCollectionHandler : IRequestHandler<CreateCollection, Either<BaseError, MediaCollectionViewModel>> IRequestHandler<CreateCollection, Either<BaseError, MediaCollectionViewModel>>
{ {
private readonly IMediaCollectionRepository _mediaCollectionRepository; private readonly IDbContextFactory<TvContext> _dbContextFactory;
public CreateCollectionHandler(IMediaCollectionRepository mediaCollectionRepository) => public CreateCollectionHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_mediaCollectionRepository = mediaCollectionRepository; _dbContextFactory = dbContextFactory;
public Task<Either<BaseError, MediaCollectionViewModel>> Handle( public async Task<Either<BaseError, MediaCollectionViewModel>> Handle(
CreateCollection request, CreateCollection request,
CancellationToken cancellationToken) => CancellationToken cancellationToken)
Validate(request).MapT(PersistCollection).Bind(v => v.ToEitherAsync()); {
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Validation<BaseError, Collection> validation = await Validate(dbContext, request);
return await validation.Apply(c => PersistCollection(dbContext, c));
}
private Task<MediaCollectionViewModel> PersistCollection(Collection c) => private static async Task<MediaCollectionViewModel> PersistCollection(
_mediaCollectionRepository.Add(c).Map(ProjectToViewModel); TvContext dbContext,
Collection collection)
{
await dbContext.Collections.AddAsync(collection);
await dbContext.SaveChangesAsync();
return ProjectToViewModel(collection);
}
private Task<Validation<BaseError, Collection>> Validate(CreateCollection request) => private static Task<Validation<BaseError, Collection>> Validate(
ValidateName(request).MapT( TvContext dbContext,
CreateCollection request) =>
ValidateName(dbContext, request).MapT(
name => new Collection name => new Collection
{ {
Name = name, Name = name,
MediaItems = new List<MediaItem>() MediaItems = new List<MediaItem>()
}); });
private async Task<Validation<BaseError, string>> ValidateName(CreateCollection createCollection) private static async Task<Validation<BaseError, string>> ValidateName(
TvContext dbContext,
CreateCollection createCollection)
{ {
List<string> allNames = await _mediaCollectionRepository.GetAll() List<string> allNames = await dbContext.Collections
.Map(list => list.Map(c => c.Name).ToList()); .Map(c => c.Name)
.ToListAsync();
Validation<BaseError, string> result1 = createCollection.NotEmpty(c => c.Name) Validation<BaseError, string> result1 = createCollection.NotEmpty(c => c.Name)
.Bind(_ => createCollection.NotLongerThan(50)(c => c.Name)); .Bind(_ => createCollection.NotLongerThan(50)(c => c.Name));

5
ErsatzTV.Application/MediaCollections/Commands/DeleteCollection.cs

@ -1,9 +1,8 @@
using System.Threading.Tasks; using ErsatzTV.Core;
using ErsatzTV.Core;
using LanguageExt; using LanguageExt;
using MediatR; using MediatR;
namespace ErsatzTV.Application.MediaCollections.Commands namespace ErsatzTV.Application.MediaCollections.Commands
{ {
public record DeleteCollection(int CollectionId) : IRequest<Either<BaseError, Task>>; public record DeleteCollection(int CollectionId) : IRequest<Either<BaseError, LanguageExt.Unit>>;
} }

45
ErsatzTV.Application/MediaCollections/Commands/DeleteCollectionHandler.cs

@ -1,33 +1,42 @@
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Domain;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt; using LanguageExt;
using MediatR; using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.MediaCollections.Commands namespace ErsatzTV.Application.MediaCollections.Commands
{ {
public class DeleteCollectionHandler : IRequestHandler<DeleteCollection, Either<BaseError, Task>> public class DeleteCollectionHandler : MediatR.IRequestHandler<DeleteCollection, Either<BaseError, Unit>>
{ {
private readonly IMediaCollectionRepository _mediaCollectionRepository; private readonly IDbContextFactory<TvContext> _dbContextFactory;
public DeleteCollectionHandler(IMediaCollectionRepository mediaCollectionRepository) => public DeleteCollectionHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_mediaCollectionRepository = mediaCollectionRepository; _dbContextFactory = dbContextFactory;
public async Task<Either<BaseError, Task>> Handle( public async Task<Either<BaseError, Unit>> Handle(
DeleteCollection request, DeleteCollection request,
CancellationToken cancellationToken) => CancellationToken cancellationToken)
(await CollectionMustExist(request)) {
.Map(DoDeletion) await using TvContext dbContext = _dbContextFactory.CreateDbContext();
.ToEither<Task>();
private Task DoDeletion(int mediaCollectionId) => _mediaCollectionRepository.Delete(mediaCollectionId); Validation<BaseError, Collection> validation = await CollectionMustExist(dbContext, request);
return await validation.Apply(c => DoDeletion(dbContext, c));
}
private async Task<Validation<BaseError, int>> CollectionMustExist( private static Task<Unit> DoDeletion(TvContext dbContext, Collection collection)
DeleteCollection deleteMediaCollection) => {
(await _mediaCollectionRepository.Get(deleteMediaCollection.CollectionId)) dbContext.Collections.Remove(collection);
.ToValidation<BaseError>( return dbContext.SaveChangesAsync().ToUnit();
$"Collection {deleteMediaCollection.CollectionId} does not exist.") }
.Map(c => c.Id);
private static Task<Validation<BaseError, Collection>> CollectionMustExist(
TvContext dbContext,
DeleteCollection request) =>
dbContext.Collections
.SelectOneAsync(c => c.Id, c => c.Id == request.CollectionId)
.Map(o => o.ToValidation<BaseError>($"Collection {request.CollectionId} does not exist."));
} }
} }

41
ErsatzTV.Application/MediaCollections/Commands/RemoveItemsFromCollectionHandler.cs

@ -6,32 +6,41 @@ using ErsatzTV.Application.Playouts.Commands;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt; using LanguageExt;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.MediaCollections.Commands namespace ErsatzTV.Application.MediaCollections.Commands
{ {
public class public class RemoveItemsFromCollectionHandler :
RemoveItemsFromCollectionHandler : MediatR.IRequestHandler<RemoveItemsFromCollection, Either<BaseError, Unit>> MediatR.IRequestHandler<RemoveItemsFromCollection, Either<BaseError, Unit>>
{ {
private readonly ChannelWriter<IBackgroundServiceRequest> _channel; private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IMediaCollectionRepository _mediaCollectionRepository; private readonly IMediaCollectionRepository _mediaCollectionRepository;
public RemoveItemsFromCollectionHandler( public RemoveItemsFromCollectionHandler(
IDbContextFactory<TvContext> dbContextFactory,
IMediaCollectionRepository mediaCollectionRepository, IMediaCollectionRepository mediaCollectionRepository,
ChannelWriter<IBackgroundServiceRequest> channel) ChannelWriter<IBackgroundServiceRequest> channel)
{ {
_dbContextFactory = dbContextFactory;
_mediaCollectionRepository = mediaCollectionRepository; _mediaCollectionRepository = mediaCollectionRepository;
_channel = channel; _channel = channel;
} }
public Task<Either<BaseError, Unit>> Handle( public async Task<Either<BaseError, Unit>> Handle(
RemoveItemsFromCollection request, RemoveItemsFromCollection request,
CancellationToken cancellationToken) => CancellationToken cancellationToken)
Validate(request) {
.MapT(collection => ApplyRemoveItemsRequest(request, collection)) await using TvContext dbContext = _dbContextFactory.CreateDbContext();
.Bind(v => v.ToEitherAsync()); Validation<BaseError, Collection> validation = await Validate(dbContext, request);
return await validation.Apply(c => ApplyRemoveItemsRequest(dbContext, request, c));
}
private async Task<Unit> ApplyRemoveItemsRequest( private async Task<Unit> ApplyRemoveItemsRequest(
TvContext dbContext,
RemoveItemsFromCollection request, RemoveItemsFromCollection request,
Collection collection) Collection collection)
{ {
@ -41,7 +50,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands
itemsToRemove.ForEach(m => collection.MediaItems.Remove(m)); itemsToRemove.ForEach(m => collection.MediaItems.Remove(m));
if (itemsToRemove.Any() && await _mediaCollectionRepository.Update(collection)) if (itemsToRemove.Any() && await dbContext.SaveChangesAsync() > 0)
{ {
// rebuild all playouts that use this collection // rebuild all playouts that use this collection
foreach (int playoutId in await _mediaCollectionRepository.PlayoutIdsUsingCollection(collection.Id)) foreach (int playoutId in await _mediaCollectionRepository.PlayoutIdsUsingCollection(collection.Id))
@ -53,13 +62,17 @@ namespace ErsatzTV.Application.MediaCollections.Commands
return Unit.Default; return Unit.Default;
} }
private Task<Validation<BaseError, Collection>> Validate( private static Task<Validation<BaseError, Collection>> Validate(
TvContext dbContext,
RemoveItemsFromCollection request) => RemoveItemsFromCollection request) =>
CollectionMustExist(request); CollectionMustExist(dbContext, request);
private Task<Validation<BaseError, Collection>> CollectionMustExist( private static Task<Validation<BaseError, Collection>> CollectionMustExist(
RemoveItemsFromCollection updateCollection) => TvContext dbContext,
_mediaCollectionRepository.GetCollectionWithItems(updateCollection.MediaCollectionId) RemoveItemsFromCollection request) =>
.Map(v => v.ToValidation<BaseError>("Collection does not exist.")); dbContext.Collections
.Include(c => c.MediaItems)
.SelectOneAsync(c => c.Id, c => c.Id == request.MediaCollectionId)
.Map(o => o.ToValidation<BaseError>("Collection does not exist."));
} }
} }

58
ErsatzTV.Application/MediaCollections/Commands/UpdateCollectionCustomOrderHandler.cs

@ -6,47 +6,60 @@ using ErsatzTV.Application.Playouts.Commands;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt; using LanguageExt;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.MediaCollections.Commands namespace ErsatzTV.Application.MediaCollections.Commands
{ {
public class public class UpdateCollectionCustomOrderHandler :
UpdateCollectionCustomOrderHandler : MediatR.IRequestHandler<UpdateCollectionCustomOrder, MediatR.IRequestHandler<UpdateCollectionCustomOrder, Either<BaseError, Unit>>
Either<BaseError, Unit>>
{ {
private readonly ChannelWriter<IBackgroundServiceRequest> _channel; private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IMediaCollectionRepository _mediaCollectionRepository; private readonly IMediaCollectionRepository _mediaCollectionRepository;
public UpdateCollectionCustomOrderHandler( public UpdateCollectionCustomOrderHandler(
IDbContextFactory<TvContext> dbContextFactory,
IMediaCollectionRepository mediaCollectionRepository, IMediaCollectionRepository mediaCollectionRepository,
ChannelWriter<IBackgroundServiceRequest> channel) ChannelWriter<IBackgroundServiceRequest> channel)
{ {
_dbContextFactory = dbContextFactory;
_mediaCollectionRepository = mediaCollectionRepository; _mediaCollectionRepository = mediaCollectionRepository;
_channel = channel; _channel = channel;
} }
public Task<Either<BaseError, Unit>> Handle( public async Task<Either<BaseError, Unit>> Handle(
UpdateCollectionCustomOrder request, UpdateCollectionCustomOrder request,
CancellationToken cancellationToken) => CancellationToken cancellationToken)
Validate(request) {
.MapT(c => ApplyUpdateRequest(c, request)) await using TvContext dbContext = _dbContextFactory.CreateDbContext();
.Bind(v => v.ToEitherAsync()); Validation<BaseError, Collection> validation = await Validate(dbContext, request);
return await validation.Apply(c => ApplyUpdateRequest(dbContext, c, request));
}
private async Task<Unit> ApplyUpdateRequest(Collection c, UpdateCollectionCustomOrder request) private async Task<Unit> ApplyUpdateRequest(
TvContext dbContext,
Collection c,
UpdateCollectionCustomOrder request)
{ {
foreach (MediaItemCustomOrder updateItem in request.MediaItemCustomOrders) foreach (MediaItemCustomOrder updateItem in request.MediaItemCustomOrders)
{ {
Option<CollectionItem> maybeCollectionItem = Option<CollectionItem> maybeCollectionItem = c.CollectionItems
c.CollectionItems.FirstOrDefault(ci => ci.MediaItemId == updateItem.MediaItemId); .FirstOrDefault(ci => ci.MediaItemId == updateItem.MediaItemId);
await maybeCollectionItem.IfSomeAsync(ci => ci.CustomIndex = updateItem.CustomIndex); foreach (CollectionItem collectionItem in maybeCollectionItem)
{
collectionItem.CustomIndex = updateItem.CustomIndex;
}
} }
if (await _mediaCollectionRepository.Update(c)) if (await dbContext.SaveChangesAsync() > 0)
{ {
// rebuild all playouts that use this collection // rebuild all playouts that use this collection
foreach (int playoutId in await _mediaCollectionRepository.PlayoutIdsUsingCollection( foreach (int playoutId in await _mediaCollectionRepository
request.CollectionId)) .PlayoutIdsUsingCollection(request.CollectionId))
{ {
await _channel.WriteAsync(new BuildPlayout(playoutId, true)); await _channel.WriteAsync(new BuildPlayout(playoutId, true));
} }
@ -55,12 +68,17 @@ namespace ErsatzTV.Application.MediaCollections.Commands
return Unit.Default; return Unit.Default;
} }
private Task<Validation<BaseError, Collection>> Validate(UpdateCollectionCustomOrder request) => private static Task<Validation<BaseError, Collection>> Validate(
CollectionMustExist(request); TvContext dbContext,
UpdateCollectionCustomOrder request) =>
CollectionMustExist(dbContext, request);
private Task<Validation<BaseError, Collection>> CollectionMustExist( private static Task<Validation<BaseError, Collection>> CollectionMustExist(
TvContext dbContext,
UpdateCollectionCustomOrder request) => UpdateCollectionCustomOrder request) =>
_mediaCollectionRepository.Get(request.CollectionId) dbContext.Collections
.Map(v => v.ToValidation<BaseError>("Collection does not exist.")); .Include(c => c.CollectionItems)
.SelectOneAsync(c => c.Id, c => c.Id == request.CollectionId)
.Map(o => o.ToValidation<BaseError>("Collection does not exist."));
} }
} }

46
ErsatzTV.Application/MediaCollections/Commands/UpdateCollectionHandler.cs

@ -5,36 +5,47 @@ using ErsatzTV.Application.Playouts.Commands;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt; using LanguageExt;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.MediaCollections.Commands namespace ErsatzTV.Application.MediaCollections.Commands
{ {
public class UpdateCollectionHandler : MediatR.IRequestHandler<UpdateCollection, Either<BaseError, Unit>> public class UpdateCollectionHandler : MediatR.IRequestHandler<UpdateCollection, Either<BaseError, Unit>>
{ {
private readonly ChannelWriter<IBackgroundServiceRequest> _channel; private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IMediaCollectionRepository _mediaCollectionRepository; private readonly IMediaCollectionRepository _mediaCollectionRepository;
public UpdateCollectionHandler( public UpdateCollectionHandler(
IDbContextFactory<TvContext> dbContextFactory,
IMediaCollectionRepository mediaCollectionRepository, IMediaCollectionRepository mediaCollectionRepository,
ChannelWriter<IBackgroundServiceRequest> channel) ChannelWriter<IBackgroundServiceRequest> channel)
{ {
_dbContextFactory = dbContextFactory;
_mediaCollectionRepository = mediaCollectionRepository; _mediaCollectionRepository = mediaCollectionRepository;
_channel = channel; _channel = channel;
} }
public Task<Either<BaseError, Unit>> Handle( public async Task<Either<BaseError, Unit>> Handle(
UpdateCollection request, UpdateCollection request,
CancellationToken cancellationToken) => CancellationToken cancellationToken)
Validate(request) {
.MapT(c => ApplyUpdateRequest(c, request)) await using TvContext dbContext = _dbContextFactory.CreateDbContext();
.Bind(v => v.ToEitherAsync()); Validation<BaseError, Collection> validation = await Validate(dbContext, request);
return await validation.Apply(c => ApplyUpdateRequest(dbContext, c, request));
}
private async Task<Unit> ApplyUpdateRequest(Collection c, UpdateCollection request) private async Task<Unit> ApplyUpdateRequest(TvContext dbContext, Collection c, UpdateCollection request)
{ {
c.Name = request.Name; c.Name = request.Name;
await request.UseCustomPlaybackOrder.IfSomeAsync( foreach (bool useCustomPlaybackOrder in request.UseCustomPlaybackOrder)
useCustomPlaybackOrder => c.UseCustomPlaybackOrder = useCustomPlaybackOrder); {
if (await _mediaCollectionRepository.Update(c) && request.UseCustomPlaybackOrder.IsSome) c.UseCustomPlaybackOrder = useCustomPlaybackOrder;
}
if (await dbContext.SaveChangesAsync() > 0 && request.UseCustomPlaybackOrder.IsSome)
{ {
// rebuild all playouts that use this collection // rebuild all playouts that use this collection
foreach (int playoutId in await _mediaCollectionRepository.PlayoutIdsUsingCollection( foreach (int playoutId in await _mediaCollectionRepository.PlayoutIdsUsingCollection(
@ -47,17 +58,20 @@ namespace ErsatzTV.Application.MediaCollections.Commands
return Unit.Default; return Unit.Default;
} }
private async Task<Validation<BaseError, Collection>> private static async Task<Validation<BaseError, Collection>> Validate(
Validate(UpdateCollection request) => TvContext dbContext,
(await CollectionMustExist(request), ValidateName(request)) UpdateCollection request) =>
(await CollectionMustExist(dbContext, request), ValidateName(request))
.Apply((collectionToUpdate, _) => collectionToUpdate); .Apply((collectionToUpdate, _) => collectionToUpdate);
private Task<Validation<BaseError, Collection>> CollectionMustExist( private static Task<Validation<BaseError, Collection>> CollectionMustExist(
TvContext dbContext,
UpdateCollection updateCollection) => UpdateCollection updateCollection) =>
_mediaCollectionRepository.Get(updateCollection.CollectionId) dbContext.Collections
.Map(v => v.ToValidation<BaseError>("Collection does not exist.")); .SelectOneAsync(c => c.Id, c => c.Id == updateCollection.CollectionId)
.Map(o => o.ToValidation<BaseError>("Collection does not exist."));
private Validation<BaseError, string> ValidateName(UpdateCollection updateSimpleMediaCollection) => private static Validation<BaseError, string> ValidateName(UpdateCollection updateSimpleMediaCollection) =>
updateSimpleMediaCollection.NotEmpty(c => c.Name) updateSimpleMediaCollection.NotEmpty(c => c.Name)
.Bind(_ => updateSimpleMediaCollection.NotLongerThan(50)(c => c.Name)); .Bind(_ => updateSimpleMediaCollection.NotLongerThan(50)(c => c.Name));
} }

20
ErsatzTV.Application/MediaCollections/Queries/GetAllCollectionsHandler.cs

@ -2,23 +2,29 @@
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Infrastructure.Data;
using LanguageExt; using LanguageExt;
using MediatR; using MediatR;
using Microsoft.EntityFrameworkCore;
using static ErsatzTV.Application.MediaCollections.Mapper; using static ErsatzTV.Application.MediaCollections.Mapper;
namespace ErsatzTV.Application.MediaCollections.Queries namespace ErsatzTV.Application.MediaCollections.Queries
{ {
public class GetAllCollectionsHandler : IRequestHandler<GetAllCollections, List<MediaCollectionViewModel>> public class GetAllCollectionsHandler : IRequestHandler<GetAllCollections, List<MediaCollectionViewModel>>
{ {
private readonly IMediaCollectionRepository _mediaCollectionRepository; private readonly IDbContextFactory<TvContext> _dbContextFactory;
public GetAllCollectionsHandler(IMediaCollectionRepository mediaCollectionRepository) => public GetAllCollectionsHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_mediaCollectionRepository = mediaCollectionRepository; _dbContextFactory = dbContextFactory;
public Task<List<MediaCollectionViewModel>> Handle( public async Task<List<MediaCollectionViewModel>> Handle(
GetAllCollections request, GetAllCollections request,
CancellationToken cancellationToken) => CancellationToken cancellationToken)
_mediaCollectionRepository.GetAll().Map(list => list.Map(ProjectToViewModel).ToList()); {
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
return await dbContext.Collections
.ToListAsync(cancellationToken)
.Map(list => list.Map(ProjectToViewModel).ToList());
}
} }
} }

25
ErsatzTV.Application/MediaCollections/Queries/GetCollectionByIdHandler.cs

@ -1,25 +1,30 @@
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt; using LanguageExt;
using MediatR; using MediatR;
using Microsoft.EntityFrameworkCore;
using static ErsatzTV.Application.MediaCollections.Mapper; using static ErsatzTV.Application.MediaCollections.Mapper;
namespace ErsatzTV.Application.MediaCollections.Queries namespace ErsatzTV.Application.MediaCollections.Queries
{ {
public class public class GetCollectionByIdHandler :
GetCollectionByIdHandler : IRequestHandler<GetCollectionById, IRequestHandler<GetCollectionById, Option<MediaCollectionViewModel>>
Option<MediaCollectionViewModel>>
{ {
private readonly IMediaCollectionRepository _mediaCollectionRepository; private readonly IDbContextFactory<TvContext> _dbContextFactory;
public GetCollectionByIdHandler(IMediaCollectionRepository mediaCollectionRepository) => public GetCollectionByIdHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_mediaCollectionRepository = mediaCollectionRepository; _dbContextFactory = dbContextFactory;
public Task<Option<MediaCollectionViewModel>> Handle( public async Task<Option<MediaCollectionViewModel>> Handle(
GetCollectionById request, GetCollectionById request,
CancellationToken cancellationToken) => CancellationToken cancellationToken)
_mediaCollectionRepository.Get(request.Id) {
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
return await dbContext.Collections
.SelectOneAsync(c => c.Id, c => c.Id == request.Id)
.MapT(ProjectToViewModel); .MapT(ProjectToViewModel);
}
} }
} }

9
ErsatzTV.Application/MediaCollections/Queries/GetCollectionItems.cs

@ -1,9 +0,0 @@
using System.Collections.Generic;
using ErsatzTV.Application.MediaItems;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.MediaCollections.Queries
{
public record GetCollectionItems(int Id) : IRequest<Option<IEnumerable<MediaItemViewModel>>>;
}

26
ErsatzTV.Application/MediaCollections/Queries/GetCollectionItemsHandler.cs

@ -1,26 +0,0 @@
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 GetCollectionItemsHandler : IRequestHandler<GetCollectionItems,
Option<IEnumerable<MediaItemViewModel>>>
{
private readonly IMediaCollectionRepository _mediaCollectionRepository;
public GetCollectionItemsHandler(IMediaCollectionRepository mediaCollectionRepository) =>
_mediaCollectionRepository = mediaCollectionRepository;
public Task<Option<IEnumerable<MediaItemViewModel>>> Handle(
GetCollectionItems request,
CancellationToken cancellationToken) =>
_mediaCollectionRepository.GetItems(request.Id)
.MapT(mediaItems => mediaItems.Map(ProjectToViewModel));
}
}

27
ErsatzTV.Application/MediaCollections/Queries/GetPagedCollectionsHandler.cs

@ -1,29 +1,42 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Data;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories; using Dapper;
using ErsatzTV.Infrastructure.Data;
using LanguageExt; using LanguageExt;
using MediatR; using MediatR;
using Microsoft.EntityFrameworkCore;
using static ErsatzTV.Application.MediaCollections.Mapper; using static ErsatzTV.Application.MediaCollections.Mapper;
namespace ErsatzTV.Application.MediaCollections.Queries namespace ErsatzTV.Application.MediaCollections.Queries
{ {
public class GetPagedCollectionsHandler : IRequestHandler<GetPagedCollections, PagedMediaCollectionsViewModel> public class GetPagedCollectionsHandler : IRequestHandler<GetPagedCollections, PagedMediaCollectionsViewModel>
{ {
private readonly IMediaCollectionRepository _mediaCollectionRepository; private readonly IDbConnection _dbConnection;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public GetPagedCollectionsHandler(IMediaCollectionRepository mediaCollectionRepository) => public GetPagedCollectionsHandler(IDbContextFactory<TvContext> dbContextFactory, IDbConnection dbConnection)
_mediaCollectionRepository = mediaCollectionRepository; {
_dbContextFactory = dbContextFactory;
_dbConnection = dbConnection;
}
public async Task<PagedMediaCollectionsViewModel> Handle( public async Task<PagedMediaCollectionsViewModel> Handle(
GetPagedCollections request, GetPagedCollections request,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
int count = await _mediaCollectionRepository.CountAllCollections(); int count = await _dbConnection.QuerySingleAsync<int>(@"SELECT COUNT (*) FROM Collection");
List<MediaCollectionViewModel> page = await _mediaCollectionRepository await using TvContext dbContext = _dbContextFactory.CreateDbContext();
.GetPagedCollections(request.PageNum, request.PageSize) List<MediaCollectionViewModel> page = await dbContext.Collections.FromSqlRaw(
@"SELECT * FROM Collection
ORDER BY Name
LIMIT {0} OFFSET {1}",
request.PageSize,
request.PageNum * request.PageSize)
.ToListAsync(cancellationToken)
.Map(list => list.Map(ProjectToViewModel).ToList()); .Map(list => list.Map(ProjectToViewModel).ToList());
return new PagedMediaCollectionsViewModel(count, page); return new PagedMediaCollectionsViewModel(count, page);

3
ErsatzTV.Application/MediaItems/Mapper.cs

@ -4,9 +4,6 @@ namespace ErsatzTV.Application.MediaItems
{ {
internal static class Mapper internal static class Mapper
{ {
internal static MediaItemViewModel ProjectToViewModel(MediaItem mediaItem) =>
new(mediaItem.Id, mediaItem.LibraryPathId);
internal static NamedMediaItemViewModel ProjectToViewModel(Show show) => internal static NamedMediaItemViewModel ProjectToViewModel(Show show) =>
new(show.Id, show.ShowMetadata.HeadOrNone().Map(sm => $"{sm?.Title} ({sm?.Year})").IfNone("???")); new(show.Id, show.ShowMetadata.HeadOrNone().Map(sm => $"{sm?.Title} ({sm?.Year})").IfNone("???"));

9
ErsatzTV.Application/MediaItems/MediaItemSearchResultViewModel.cs

@ -1,9 +0,0 @@
namespace ErsatzTV.Application.MediaItems
{
public record MediaItemSearchResultViewModel(
int Id,
string Source,
string MediaType,
string Title,
string Duration);
}

4
ErsatzTV.Application/MediaItems/MediaItemViewModel.cs

@ -1,4 +0,0 @@
namespace ErsatzTV.Application.MediaItems
{
public record MediaItemViewModel(int Id, int LibraryPathId);
}

7
ErsatzTV.Application/MediaItems/Queries/GetAllMediaItems.cs

@ -1,7 +0,0 @@
using System.Collections.Generic;
using MediatR;
namespace ErsatzTV.Application.MediaItems.Queries
{
public record GetAllMediaItems : IRequest<List<MediaItemViewModel>>;
}

24
ErsatzTV.Application/MediaItems/Queries/GetAllMediaItemsHandler.cs

@ -1,24 +0,0 @@
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.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 Task<List<MediaItemViewModel>> Handle(
GetAllMediaItems request,
CancellationToken cancellationToken) =>
_mediaItemRepository.GetAll().Map(list => list.Map(ProjectToViewModel).ToList());
}
}

7
ErsatzTV.Application/MediaItems/Queries/GetMediaItemById.cs

@ -1,7 +0,0 @@
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.MediaItems.Queries
{
public record GetMediaItemById(int Id) : IRequest<Option<MediaItemViewModel>>;
}

23
ErsatzTV.Application/MediaItems/Queries/GetMediaItemByIdHandler.cs

@ -1,23 +0,0 @@
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);
}
}

16
ErsatzTV.Application/MediaSources/Mapper.cs

@ -1,16 +0,0 @@
using System;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.MediaSources
{
internal static class Mapper
{
internal static MediaSourceViewModel ProjectToViewModel(MediaSource mediaSource) =>
mediaSource switch
{
LocalMediaSource lms => new LocalMediaSourceViewModel(lms.Id),
PlexMediaSource pms => Plex.Mapper.ProjectToViewModel(pms),
_ => throw new NotSupportedException($"Unsupported media source {mediaSource.GetType().Name}")
};
}
}

6
ErsatzTV.Application/MediaSources/Queries/CountMediaItemsById.cs

@ -1,6 +0,0 @@
using MediatR;
namespace ErsatzTV.Application.MediaSources.Queries
{
public record CountMediaItemsById(int MediaSourceId) : IRequest<int>;
}

18
ErsatzTV.Application/MediaSources/Queries/CountMediaItemsByIdHandler.cs

@ -1,18 +0,0 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using MediatR;
namespace ErsatzTV.Application.MediaSources.Queries
{
public class CountMediaItemsByIdHandler : IRequestHandler<CountMediaItemsById, int>
{
private readonly IMediaSourceRepository _mediaSourceRepository;
public CountMediaItemsByIdHandler(IMediaSourceRepository mediaSourceRepository) =>
_mediaSourceRepository = mediaSourceRepository;
public Task<int> Handle(CountMediaItemsById request, CancellationToken cancellationToken) =>
_mediaSourceRepository.CountMediaItems(request.MediaSourceId);
}
}

7
ErsatzTV.Application/MediaSources/Queries/GetAllMediaSources.cs

@ -1,7 +0,0 @@
using System.Collections.Generic;
using MediatR;
namespace ErsatzTV.Application.MediaSources.Queries
{
public record GetAllMediaSources : IRequest<List<MediaSourceViewModel>>;
}

23
ErsatzTV.Application/MediaSources/Queries/GetAllMediaSourcesHandler.cs

@ -1,23 +0,0 @@
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.MediaSources.Mapper;
namespace ErsatzTV.Application.MediaSources.Queries
{
public class GetAllMediaSourcesHandler : IRequestHandler<GetAllMediaSources, List<MediaSourceViewModel>>
{
private readonly IMediaSourceRepository _mediaSourceRepository;
public GetAllMediaSourcesHandler(IMediaSourceRepository mediaSourceRepository) =>
_mediaSourceRepository = mediaSourceRepository;
public async Task<List<MediaSourceViewModel>> Handle(
GetAllMediaSources request,
CancellationToken cancellationToken) =>
(await _mediaSourceRepository.GetAll()).Map(ProjectToViewModel).ToList();
}
}

7
ErsatzTV.Application/MediaSources/Queries/GetMediaSourceById.cs

@ -1,7 +0,0 @@
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.MediaSources.Queries
{
public record GetMediaSourceById(int Id) : IRequest<Option<MediaSourceViewModel>>;
}

23
ErsatzTV.Application/MediaSources/Queries/GetMediaSourceByIdHandler.cs

@ -1,23 +0,0 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.MediaSources.Mapper;
namespace ErsatzTV.Application.MediaSources.Queries
{
public class GetMediaSourceByIdHandler : IRequestHandler<GetMediaSourceById, Option<MediaSourceViewModel>>
{
private readonly IMediaSourceRepository _mediaSourceRepository;
public GetMediaSourceByIdHandler(IMediaSourceRepository mediaSourceRepository) =>
_mediaSourceRepository = mediaSourceRepository;
public Task<Option<MediaSourceViewModel>> Handle(
GetMediaSourceById request,
CancellationToken cancellationToken) =>
_mediaSourceRepository.Get(request.Id)
.MapT(ProjectToViewModel);
}
}

52
ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs

@ -2,41 +2,57 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Scheduling; using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt; using LanguageExt;
using static LanguageExt.Prelude; using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Playouts.Commands namespace ErsatzTV.Application.Playouts.Commands
{ {
public class BuildPlayoutHandler : MediatR.IRequestHandler<BuildPlayout, Either<BaseError, Unit>> public class BuildPlayoutHandler : MediatR.IRequestHandler<BuildPlayout, Either<BaseError, Unit>>
{ {
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IPlayoutBuilder _playoutBuilder; private readonly IPlayoutBuilder _playoutBuilder;
private readonly IPlayoutRepository _playoutRepository;
public BuildPlayoutHandler(IPlayoutRepository playoutRepository, IPlayoutBuilder playoutBuilder) public BuildPlayoutHandler(IDbContextFactory<TvContext> dbContextFactory, IPlayoutBuilder playoutBuilder)
{ {
_playoutRepository = playoutRepository; _dbContextFactory = dbContextFactory;
_playoutBuilder = playoutBuilder; _playoutBuilder = playoutBuilder;
} }
public Task<Either<BaseError, Unit>> Handle(BuildPlayout request, CancellationToken cancellationToken) => public async Task<Either<BaseError, Unit>> Handle(BuildPlayout request, CancellationToken cancellationToken)
Validate(request) {
.Map(v => v.ToEither<Playout>()) await using TvContext dbContext = _dbContextFactory.CreateDbContext();
.BindT(playout => ApplyUpdateRequest(request, playout)); Validation<BaseError, Playout> validation = await Validate(dbContext, request);
return await validation.Apply(playout => ApplyUpdateRequest(dbContext, request, playout));
}
private async Task<Either<BaseError, Unit>> ApplyUpdateRequest(BuildPlayout request, Playout playout) private async Task<Unit> ApplyUpdateRequest(TvContext dbContext, BuildPlayout request, Playout playout)
{ {
Playout result = await _playoutBuilder.BuildPlayoutItems(playout, request.Rebuild); await _playoutBuilder.BuildPlayoutItems(playout, request.Rebuild);
await _playoutRepository.Update(result); await dbContext.SaveChangesAsync();
return unit; return Unit.Default;
} }
private Task<Validation<BaseError, Playout>> Validate(BuildPlayout request) => private static Task<Validation<BaseError, Playout>> Validate(TvContext dbContext, BuildPlayout request) =>
PlayoutMustExist(request); PlayoutMustExist(dbContext, request);
private async Task<Validation<BaseError, Playout>> PlayoutMustExist(BuildPlayout buildPlayout) => private static Task<Validation<BaseError, Playout>> PlayoutMustExist(
(await _playoutRepository.GetFull(buildPlayout.PlayoutId)) TvContext dbContext,
.ToValidation<BaseError>("Playout does not exist."); BuildPlayout buildPlayout) =>
dbContext.Playouts
.Include(p => p.Channel)
.Include(p => p.Items)
.Include(p => p.ProgramScheduleAnchors)
.ThenInclude(a => a.MediaItem)
.Include(p => p.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.Collection)
.Include(p => p.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.MediaItem)
.SelectOneAsync(p => p.Id, p => p.Id == buildPlayout.PlayoutId)
.Map(o => o.ToValidation<BaseError>("Playout does not exist."));
} }
} }

2
ErsatzTV.Application/Playouts/Commands/CreatePlayout.cs

@ -8,5 +8,5 @@ namespace ErsatzTV.Application.Playouts.Commands
public record CreatePlayout( public record CreatePlayout(
int ChannelId, int ChannelId,
int ProgramScheduleId, int ProgramScheduleId,
ProgramSchedulePlayoutType ProgramSchedulePlayoutType) : IRequest<Either<BaseError, PlayoutViewModel>>; ProgramSchedulePlayoutType ProgramSchedulePlayoutType) : IRequest<Either<BaseError, CreatePlayoutResponse>>;
} }

86
ErsatzTV.Application/Playouts/Commands/CreatePlayoutHandler.cs

@ -4,51 +4,50 @@ using System.Threading.Channels;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt; using LanguageExt;
using MediatR; using MediatR;
using static ErsatzTV.Application.Playouts.Mapper; using Microsoft.EntityFrameworkCore;
using static LanguageExt.Prelude; using static LanguageExt.Prelude;
using Channel = ErsatzTV.Core.Domain.Channel; using Channel = ErsatzTV.Core.Domain.Channel;
namespace ErsatzTV.Application.Playouts.Commands namespace ErsatzTV.Application.Playouts.Commands
{ {
public class public class CreatePlayoutHandler : IRequestHandler<CreatePlayout, Either<BaseError, CreatePlayoutResponse>>
CreatePlayoutHandler : IRequestHandler<CreatePlayout, Either<BaseError, PlayoutViewModel>>
{ {
private readonly ChannelWriter<IBackgroundServiceRequest> _channel; private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly IChannelRepository _channelRepository; private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IPlayoutRepository _playoutRepository;
private readonly IProgramScheduleRepository _programScheduleRepository;
public CreatePlayoutHandler( public CreatePlayoutHandler(
IPlayoutRepository playoutRepository, ChannelWriter<IBackgroundServiceRequest> channel,
IChannelRepository channelRepository, IDbContextFactory<TvContext> dbContextFactory)
IProgramScheduleRepository programScheduleRepository,
ChannelWriter<IBackgroundServiceRequest> channel)
{ {
_playoutRepository = playoutRepository;
_channelRepository = channelRepository;
_programScheduleRepository = programScheduleRepository;
_channel = channel; _channel = channel;
_dbContextFactory = dbContextFactory;
} }
public Task<Either<BaseError, PlayoutViewModel>> Handle( public async Task<Either<BaseError, CreatePlayoutResponse>> Handle(
CreatePlayout request, CreatePlayout request,
CancellationToken cancellationToken) => CancellationToken cancellationToken)
Validate(request) {
.MapT(PersistPlayout) await using TvContext dbContext = _dbContextFactory.CreateDbContext();
.Bind(v => v.ToEitherAsync());
Validation<BaseError, Playout> validation = await Validate(dbContext, request);
return await validation.Apply(playout => PersistPlayout(dbContext, playout));
}
private async Task<PlayoutViewModel> PersistPlayout(Playout c) private async Task<CreatePlayoutResponse> PersistPlayout(TvContext dbContext, Playout playout)
{ {
PlayoutViewModel result = await _playoutRepository.Add(c).Map(ProjectToViewModel); await dbContext.Playouts.AddAsync(playout);
await _channel.WriteAsync(new BuildPlayout(result.Id)); await dbContext.SaveChangesAsync();
return result; await _channel.WriteAsync(new BuildPlayout(playout.Id));
return new CreatePlayoutResponse(playout.Id);
} }
private async Task<Validation<BaseError, Playout>> Validate(CreatePlayout request) => private async Task<Validation<BaseError, Playout>> Validate(TvContext dbContext, CreatePlayout request) =>
(await ValidateChannel(request), await ProgramScheduleMustExist(request), ValidatePlayoutType(request)) (await ValidateChannel(dbContext, request), await ValidateProgramSchedule(dbContext, request),
ValidatePlayoutType(request))
.Apply( .Apply(
(channel, programSchedule, playoutType) => new Playout (channel, programSchedule, playoutType) => new Playout
{ {
@ -57,31 +56,38 @@ namespace ErsatzTV.Application.Playouts.Commands
ProgramSchedulePlayoutType = playoutType ProgramSchedulePlayoutType = playoutType
}); });
private Task<Validation<BaseError, Channel>> ValidateChannel(CreatePlayout createPlayout) => private static Task<Validation<BaseError, Channel>> ValidateChannel(
ChannelMustExist(createPlayout).BindT(ChannelMustNotHavePlayouts); TvContext dbContext,
CreatePlayout createPlayout) =>
private async Task<Validation<BaseError, Channel>> ChannelMustExist(CreatePlayout createPlayout) => dbContext.Channels
(await _channelRepository.Get(createPlayout.ChannelId)) .Include(c => c.Playouts)
.ToValidation<BaseError>("Channel does not exist."); .SelectOneAsync(c => c.Id, c => c.Id == createPlayout.ChannelId)
.Map(o => o.ToValidation<BaseError>("Channel does not exist"))
.BindT(ChannelMustNotHavePlayouts);
private async Task<Validation<BaseError, Channel>> ChannelMustNotHavePlayouts(Channel channel) => private static Validation<BaseError, Channel> ChannelMustNotHavePlayouts(Channel channel) =>
Optional(await _channelRepository.CountPlayouts(channel.Id)) Optional(channel.Playouts.Count)
.Filter(count => count == 0) .Filter(count => count == 0)
.Map(_ => channel) .Map(_ => channel)
.ToValidation<BaseError>("Channel already has one playout."); .ToValidation<BaseError>("Channel already has one playout");
private async Task<Validation<BaseError, ProgramSchedule>> ProgramScheduleMustExist( private static Task<Validation<BaseError, ProgramSchedule>> ValidateProgramSchedule(
TvContext dbContext,
CreatePlayout createPlayout) => CreatePlayout createPlayout) =>
(await _programScheduleRepository.GetWithPlayouts(createPlayout.ProgramScheduleId)) dbContext.ProgramSchedules
.ToValidation<BaseError>("ProgramSchedule does not exist.") .Include(ps => ps.Items)
.Bind(ProgramScheduleMustHaveItems); .SelectOneAsync(ps => ps.Id, ps => ps.Id == createPlayout.ProgramScheduleId)
.Map(o => o.ToValidation<BaseError>("Program schedule does not exist"))
.BindT(ProgramScheduleMustHaveItems);
private Validation<BaseError, ProgramSchedule> ProgramScheduleMustHaveItems(ProgramSchedule programSchedule) => private static Validation<BaseError, ProgramSchedule> ProgramScheduleMustHaveItems(
ProgramSchedule programSchedule) =>
Optional(programSchedule) Optional(programSchedule)
.Filter(ps => ps.Items.Any()) .Filter(ps => ps.Items.Any())
.ToValidation<BaseError>("Program schedule must have items"); .ToValidation<BaseError>("Program schedule must have items");
private Validation<BaseError, ProgramSchedulePlayoutType> ValidatePlayoutType(CreatePlayout createPlayout) => private static Validation<BaseError, ProgramSchedulePlayoutType> ValidatePlayoutType(
CreatePlayout createPlayout) =>
Optional(createPlayout.ProgramSchedulePlayoutType) Optional(createPlayout.ProgramSchedulePlayoutType)
.Filter(playoutType => playoutType != ProgramSchedulePlayoutType.None) .Filter(playoutType => playoutType != ProgramSchedulePlayoutType.None)
.ToValidation<BaseError>("[ProgramSchedulePlayoutType] must not be None"); .ToValidation<BaseError>("[ProgramSchedulePlayoutType] must not be None");

4
ErsatzTV.Application/Playouts/Commands/CreatePlayoutResponse.cs

@ -0,0 +1,4 @@
namespace ErsatzTV.Application.Playouts.Commands
{
public record CreatePlayoutResponse(int PlayoutId);
}

6
ErsatzTV.Application/Playouts/Commands/DeletePlayout.cs

@ -1,9 +1,9 @@
using System.Threading.Tasks; using ErsatzTV.Core;
using ErsatzTV.Core;
using LanguageExt; using LanguageExt;
using MediatR; using MediatR;
using Unit = LanguageExt.Unit;
namespace ErsatzTV.Application.Playouts.Commands namespace ErsatzTV.Application.Playouts.Commands
{ {
public record DeletePlayout(int PlayoutId) : IRequest<Either<BaseError, Task>>; public record DeletePlayout(int PlayoutId) : IRequest<Either<BaseError, Unit>>;
} }

44
ErsatzTV.Application/Playouts/Commands/DeletePlayoutHandler.cs

@ -1,32 +1,42 @@
using System.Threading; using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Domain;
using ErsatzTV.Infrastructure.Data;
using LanguageExt; using LanguageExt;
using MediatR; using MediatR;
using Microsoft.EntityFrameworkCore;
using Unit = LanguageExt.Unit;
namespace ErsatzTV.Application.Playouts.Commands namespace ErsatzTV.Application.Playouts.Commands
{ {
public class DeletePlayoutHandler : IRequestHandler<DeletePlayout, Either<BaseError, Task>> public class DeletePlayoutHandler : IRequestHandler<DeletePlayout, Either<BaseError, Unit>>
{ {
private readonly IPlayoutRepository _playoutRepository; private readonly IDbContextFactory<TvContext> _dbContextFactory;
public DeletePlayoutHandler(IPlayoutRepository playoutRepository) => public DeletePlayoutHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_playoutRepository = playoutRepository; _dbContextFactory = dbContextFactory;
public async Task<Either<BaseError, Task>> Handle( public async Task<Either<BaseError, Unit>> Handle(
DeletePlayout request, DeletePlayout request,
CancellationToken cancellationToken) => CancellationToken cancellationToken)
(await PlayoutMustExist(request)) {
.Map(DoDeletion) await using TvContext dbContext = _dbContextFactory.CreateDbContext();
.ToEither<Task>();
private Task DoDeletion(int playoutId) => _playoutRepository.Delete(playoutId); Option<Playout> maybePlayout = await dbContext.Playouts
.OrderBy(p => p.Id)
.FirstOrDefaultAsync(p => p.Id == request.PlayoutId, cancellationToken);
private async Task<Validation<BaseError, int>> PlayoutMustExist( foreach (Playout playout in maybePlayout)
DeletePlayout deletePlayout) => {
(await _playoutRepository.Get(deletePlayout.PlayoutId)) dbContext.Playouts.Remove(playout);
.ToValidation<BaseError>($"Playout {deletePlayout.PlayoutId} does not exist.") await dbContext.SaveChangesAsync(cancellationToken);
.Map(c => c.Id); }
return maybePlayout
.Map(_ => Unit.Default)
.ToEither(BaseError.New($"Playout {request.PlayoutId} does not exist."));
}
} }
} }

13
ErsatzTV.Application/Playouts/Commands/UpdatePlayout.cs

@ -1,13 +0,0 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Playouts.Commands
{
public record UpdatePlayout(
int PlayoutId,
int ChannelId,
int ProgramScheduleId,
ProgramSchedulePlayoutType ProgramSchedulePlayoutType) : IRequest<Either<BaseError, PlayoutViewModel>>;
}

75
ErsatzTV.Application/Playouts/Commands/UpdatePlayoutHandler.cs

@ -1,75 +0,0 @@
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static LanguageExt.Prelude;
using static ErsatzTV.Application.Playouts.Mapper;
using Channel = ErsatzTV.Core.Domain.Channel;
namespace ErsatzTV.Application.Playouts.Commands
{
public class UpdatePlayoutHandler : IRequestHandler<UpdatePlayout, Either<BaseError, PlayoutViewModel>>
{
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly IChannelRepository _channelRepository;
private readonly IPlayoutRepository _playoutRepository;
private readonly IProgramScheduleRepository _programScheduleRepository;
public UpdatePlayoutHandler(
IPlayoutRepository playoutRepository,
IChannelRepository channelRepository,
IProgramScheduleRepository programScheduleRepository,
ChannelWriter<IBackgroundServiceRequest> channel)
{
_playoutRepository = playoutRepository;
_channelRepository = channelRepository;
_programScheduleRepository = programScheduleRepository;
_channel = channel;
}
public Task<Either<BaseError, PlayoutViewModel>> Handle(
UpdatePlayout request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(c => ApplyUpdateRequest(c, request))
.Bind(v => v.ToEitherAsync());
private async Task<PlayoutViewModel> ApplyUpdateRequest(Playout p, UpdatePlayout update)
{
p.ChannelId = update.ChannelId;
p.ProgramScheduleId = update.ProgramScheduleId;
p.ProgramSchedulePlayoutType = update.ProgramSchedulePlayoutType;
await _playoutRepository.Update(p);
await _channel.WriteAsync(new BuildPlayout(p.Id));
return ProjectToViewModel(p);
}
private async Task<Validation<BaseError, Playout>> Validate(UpdatePlayout request) =>
(await PlayoutMustExist(request), await ChannelMustExist(request), await ProgramScheduleMustExist(request),
ValidatePlayoutType(request))
.Apply(
(playoutToUpdate, _, _, _) => playoutToUpdate);
private async Task<Validation<BaseError, Playout>> PlayoutMustExist(UpdatePlayout updatePlayout) =>
(await _playoutRepository.Get(updatePlayout.PlayoutId))
.ToValidation<BaseError>("Playout does not exist.");
private async Task<Validation<BaseError, Channel>> ChannelMustExist(UpdatePlayout createPlayout) =>
(await _channelRepository.Get(createPlayout.ChannelId))
.ToValidation<BaseError>("Channel does not exist.");
private async Task<Validation<BaseError, ProgramSchedule>>
ProgramScheduleMustExist(UpdatePlayout createPlayout) =>
(await _programScheduleRepository.Get(createPlayout.ProgramScheduleId))
.ToValidation<BaseError>("ProgramSchedule does not exist.");
private Validation<BaseError, ProgramSchedulePlayoutType> ValidatePlayoutType(UpdatePlayout createPlayout) =>
Optional(createPlayout.ProgramSchedulePlayoutType)
.Filter(playoutType => playoutType != ProgramSchedulePlayoutType.None)
.ToValidation<BaseError>("[ProgramSchedulePlayoutType] must not be None");
}
}

13
ErsatzTV.Application/Playouts/Mapper.cs

@ -6,25 +6,12 @@ namespace ErsatzTV.Application.Playouts
{ {
internal static class Mapper internal static class Mapper
{ {
internal static PlayoutViewModel ProjectToViewModel(Playout playout) =>
new(
playout.Id,
Project(playout.Channel),
Project(playout.ProgramSchedule),
playout.ProgramSchedulePlayoutType);
internal static PlayoutItemViewModel ProjectToViewModel(PlayoutItem playoutItem) => internal static PlayoutItemViewModel ProjectToViewModel(PlayoutItem playoutItem) =>
new( new(
GetDisplayTitle(playoutItem.MediaItem), GetDisplayTitle(playoutItem.MediaItem),
playoutItem.StartOffset, playoutItem.StartOffset,
GetDisplayDuration(playoutItem.MediaItem)); GetDisplayDuration(playoutItem.MediaItem));
private static PlayoutChannelViewModel Project(Channel channel) =>
new(channel.Id, channel.Number, channel.Name);
private static PlayoutProgramScheduleViewModel Project(ProgramSchedule programSchedule) =>
new(programSchedule.Id, programSchedule.Name);
private static string GetDisplayTitle(MediaItem mediaItem) private static string GetDisplayTitle(MediaItem mediaItem)
{ {
switch (mediaItem) switch (mediaItem)

8
ErsatzTV.Application/Playouts/PlayoutNameViewModel.cs

@ -0,0 +1,8 @@
namespace ErsatzTV.Application.Playouts
{
public record PlayoutNameViewModel(
int PlayoutId,
string ChannelName,
string ChannelNumber,
string ScheduleName);
}

2
ErsatzTV.Application/Playouts/Queries/GetAllPlayouts.cs

@ -3,5 +3,5 @@ using MediatR;
namespace ErsatzTV.Application.Playouts.Queries namespace ErsatzTV.Application.Playouts.Queries
{ {
public record GetAllPlayouts : IRequest<List<PlayoutViewModel>>; public record GetAllPlayouts : IRequest<List<PlayoutNameViewModel>>;
} }

26
ErsatzTV.Application/Playouts/Queries/GetAllPlayoutsHandler.cs

@ -1,24 +1,28 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Infrastructure.Data;
using LanguageExt;
using MediatR; using MediatR;
using static ErsatzTV.Application.Playouts.Mapper; using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Playouts.Queries namespace ErsatzTV.Application.Playouts.Queries
{ {
public class GetAllPlayoutsHandler : IRequestHandler<GetAllPlayouts, List<PlayoutViewModel>> public class GetAllPlayoutsHandler : IRequestHandler<GetAllPlayouts, List<PlayoutNameViewModel>>
{ {
private readonly IPlayoutRepository _playoutRepository; private readonly IDbContextFactory<TvContext> _dbContextFactory;
public GetAllPlayoutsHandler(IPlayoutRepository playoutRepository) => public GetAllPlayoutsHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_playoutRepository = playoutRepository; _dbContextFactory = dbContextFactory;
public Task<List<PlayoutViewModel>> Handle( public async Task<List<PlayoutNameViewModel>> Handle(
GetAllPlayouts request, GetAllPlayouts request,
CancellationToken cancellationToken) => CancellationToken cancellationToken)
_playoutRepository.GetAll().Map(list => list.Map(ProjectToViewModel).ToList()); {
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
return await dbContext.Playouts
.Filter(p => p.Channel != null && p.ProgramSchedule != null)
.Map(p => new PlayoutNameViewModel(p.Id, p.Channel.Name, p.Channel.Number, p.ProgramSchedule.Name))
.ToListAsync(cancellationToken);
}
} }
} }

7
ErsatzTV.Application/Playouts/Queries/GetPlayoutById.cs

@ -1,7 +0,0 @@
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Playouts.Queries
{
public record GetPlayoutById(int Id) : IRequest<Option<PlayoutViewModel>>;
}

24
ErsatzTV.Application/Playouts/Queries/GetPlayoutByIdHandler.cs

@ -1,24 +0,0 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.Playouts.Mapper;
namespace ErsatzTV.Application.Playouts.Queries
{
public class
GetPlayoutByIdHandler : IRequestHandler<GetPlayoutById, Option<PlayoutViewModel>>
{
private readonly IPlayoutRepository _playoutRepository;
public GetPlayoutByIdHandler(IPlayoutRepository playoutRepository) =>
_playoutRepository = playoutRepository;
public Task<Option<PlayoutViewModel>> Handle(
GetPlayoutById request,
CancellationToken cancellationToken) =>
_playoutRepository.Get(request.Id)
.MapT(ProjectToViewModel);
}
}

45
ErsatzTV.Application/Playouts/Queries/GetPlayoutItemsByIdHandler.cs

@ -2,24 +2,55 @@
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Domain;
using ErsatzTV.Infrastructure.Data;
using LanguageExt; using LanguageExt;
using MediatR; using MediatR;
using Microsoft.EntityFrameworkCore;
using static ErsatzTV.Application.Playouts.Mapper; using static ErsatzTV.Application.Playouts.Mapper;
namespace ErsatzTV.Application.Playouts.Queries namespace ErsatzTV.Application.Playouts.Queries
{ {
public class GetPlayoutItemsByIdHandler : IRequestHandler<GetPlayoutItemsById, List<PlayoutItemViewModel>> public class GetPlayoutItemsByIdHandler : IRequestHandler<GetPlayoutItemsById, List<PlayoutItemViewModel>>
{ {
private readonly IPlayoutRepository _playoutRepository; private readonly IDbContextFactory<TvContext> _dbContextFactory;
public GetPlayoutItemsByIdHandler(IPlayoutRepository playoutRepository) => public GetPlayoutItemsByIdHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_playoutRepository = playoutRepository; _dbContextFactory = dbContextFactory;
public Task<List<PlayoutItemViewModel>> Handle( public async Task<List<PlayoutItemViewModel>> Handle(
GetPlayoutItemsById request, GetPlayoutItemsById request,
CancellationToken cancellationToken) => CancellationToken cancellationToken)
_playoutRepository.GetPlayoutItems(request.PlayoutId) {
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
return await dbContext.PlayoutItems
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as Movie).MovieMetadata)
.ThenInclude(mm => mm.Artwork)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as Movie).MediaVersions)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as MusicVideo).MusicVideoMetadata)
.ThenInclude(mm => mm.Artwork)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as MusicVideo).MediaVersions)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as MusicVideo).Artist)
.ThenInclude(mm => mm.ArtistMetadata)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as Episode).EpisodeMetadata)
.ThenInclude(em => em.Artwork)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as Episode).MediaVersions)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as Episode).Season)
.ThenInclude(s => s.SeasonMetadata)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as Episode).Season.Show)
.ThenInclude(s => s.ShowMetadata)
.Filter(i => i.PlayoutId == request.PlayoutId)
.ToListAsync(cancellationToken)
.Map(list => list.Map(ProjectToViewModel).ToList()); .Map(list => list.Map(ProjectToViewModel).ToList());
}
} }
} }

35
ErsatzTV.Application/ProgramSchedules/Commands/AddProgramScheduleItemHandler.cs

@ -5,36 +5,39 @@ using System.Threading.Tasks;
using ErsatzTV.Application.Playouts.Commands; using ErsatzTV.Application.Playouts.Commands;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Infrastructure.Data;
using LanguageExt; using LanguageExt;
using MediatR; using MediatR;
using Microsoft.EntityFrameworkCore;
using static ErsatzTV.Application.ProgramSchedules.Mapper; using static ErsatzTV.Application.ProgramSchedules.Mapper;
namespace ErsatzTV.Application.ProgramSchedules.Commands namespace ErsatzTV.Application.ProgramSchedules.Commands
{ {
public class AddProgramScheduleItemHandler : ProgramScheduleItemCommandBase, IRequestHandler<AddProgramScheduleItem, public class AddProgramScheduleItemHandler : ProgramScheduleItemCommandBase,
Either<BaseError, ProgramScheduleItemViewModel>> IRequestHandler<AddProgramScheduleItem, Either<BaseError, ProgramScheduleItemViewModel>>
{ {
private readonly ChannelWriter<IBackgroundServiceRequest> _channel; private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly IProgramScheduleRepository _programScheduleRepository; private readonly IDbContextFactory<TvContext> _dbContextFactory;
public AddProgramScheduleItemHandler( public AddProgramScheduleItemHandler(
IProgramScheduleRepository programScheduleRepository, IDbContextFactory<TvContext> dbContextFactory,
ChannelWriter<IBackgroundServiceRequest> channel) ChannelWriter<IBackgroundServiceRequest> channel)
: base(programScheduleRepository)
{ {
_programScheduleRepository = programScheduleRepository; _dbContextFactory = dbContextFactory;
_channel = channel; _channel = channel;
} }
public Task<Either<BaseError, ProgramScheduleItemViewModel>> Handle( public async Task<Either<BaseError, ProgramScheduleItemViewModel>> Handle(
AddProgramScheduleItem request, AddProgramScheduleItem request,
CancellationToken cancellationToken) => CancellationToken cancellationToken)
Validate(request) {
.MapT(programSchedule => PersistItem(request, programSchedule)) await using TvContext dbContext = _dbContextFactory.CreateDbContext();
.Bind(v => v.ToEitherAsync()); Validation<BaseError, ProgramSchedule> validation = await Validate(dbContext, request);
return await validation.Apply(ps => PersistItem(dbContext, request, ps));
}
private async Task<ProgramScheduleItemViewModel> PersistItem( private async Task<ProgramScheduleItemViewModel> PersistItem(
TvContext dbContext,
AddProgramScheduleItem request, AddProgramScheduleItem request,
ProgramSchedule programSchedule) ProgramSchedule programSchedule)
{ {
@ -43,7 +46,7 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
ProgramScheduleItem item = BuildItem(programSchedule, nextIndex, request); ProgramScheduleItem item = BuildItem(programSchedule, nextIndex, request);
programSchedule.Items.Add(item); programSchedule.Items.Add(item);
await _programScheduleRepository.Update(programSchedule); await dbContext.SaveChangesAsync();
// rebuild any playouts that use this schedule // rebuild any playouts that use this schedule
foreach (Playout playout in programSchedule.Playouts) foreach (Playout playout in programSchedule.Playouts)
@ -54,8 +57,10 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
return ProjectToViewModel(item); return ProjectToViewModel(item);
} }
private Task<Validation<BaseError, ProgramSchedule>> Validate(AddProgramScheduleItem request) => private static Task<Validation<BaseError, ProgramSchedule>> Validate(
ProgramScheduleMustExist(request.ProgramScheduleId) TvContext dbContext,
AddProgramScheduleItem request) =>
ProgramScheduleMustExist(dbContext, request.ProgramScheduleId)
.BindT(programSchedule => PlayoutModeMustBeValid(request, programSchedule)); .BindT(programSchedule => PlayoutModeMustBeValid(request, programSchedule));
} }
} }

2
ErsatzTV.Application/ProgramSchedules/Commands/CreateProgramSchedule.cs

@ -9,5 +9,5 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
string Name, string Name,
PlaybackOrder MediaCollectionPlaybackOrder, PlaybackOrder MediaCollectionPlaybackOrder,
bool KeepMultiPartEpisodesTogether, bool KeepMultiPartEpisodesTogether,
bool TreatCollectionsAsShows) : IRequest<Either<BaseError, ProgramScheduleViewModel>>; bool TreatCollectionsAsShows) : IRequest<Either<BaseError, CreateProgramScheduleResult>>;
} }

89
ErsatzTV.Application/ProgramSchedules/Commands/CreateProgramScheduleHandler.cs

@ -1,63 +1,74 @@
using System.Collections.Generic; using System.Threading;
using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Infrastructure.Data;
using LanguageExt; using LanguageExt;
using MediatR; using MediatR;
using static ErsatzTV.Application.ProgramSchedules.Mapper; using Microsoft.EntityFrameworkCore;
using static LanguageExt.Prelude; using static LanguageExt.Prelude;
using ValidationT_AsyncSync_Extensions = LanguageExt.ValidationT_AsyncSync_Extensions;
namespace ErsatzTV.Application.ProgramSchedules.Commands namespace ErsatzTV.Application.ProgramSchedules.Commands
{ {
public class public class CreateProgramScheduleHandler :
CreateProgramScheduleHandler : IRequestHandler<CreateProgramSchedule, IRequestHandler<CreateProgramSchedule, Either<BaseError, CreateProgramScheduleResult>>
Either<BaseError, ProgramScheduleViewModel>>
{ {
private readonly IProgramScheduleRepository _programScheduleRepository; private readonly IDbContextFactory<TvContext> _dbContextFactory;
public CreateProgramScheduleHandler(IProgramScheduleRepository programScheduleRepository) => public CreateProgramScheduleHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_programScheduleRepository = programScheduleRepository; _dbContextFactory = dbContextFactory;
public Task<Either<BaseError, ProgramScheduleViewModel>> Handle( public async Task<Either<BaseError, CreateProgramScheduleResult>> Handle(
CreateProgramSchedule request, CreateProgramSchedule request,
CancellationToken cancellationToken) => CancellationToken cancellationToken)
Validate(request) {
.MapT(PersistProgramSchedule) await using TvContext dbContext = _dbContextFactory.CreateDbContext();
.Bind(v => v.ToEitherAsync());
private Task<ProgramScheduleViewModel> PersistProgramSchedule(ProgramSchedule c) => Validation<BaseError, ProgramSchedule> validation = await Validate(dbContext, request);
_programScheduleRepository.Add(c).Map(ProjectToViewModel); return await validation.Apply(ps => PersistProgramSchedule(dbContext, ps));
}
private Task<Validation<BaseError, ProgramSchedule>> Validate(CreateProgramSchedule request) => private static async Task<CreateProgramScheduleResult> PersistProgramSchedule(
ValidateName(request) TvContext dbContext,
.MapT( ProgramSchedule programSchedule)
name => {
await dbContext.ProgramSchedules.AddAsync(programSchedule);
await dbContext.SaveChangesAsync();
return new CreateProgramScheduleResult(programSchedule.Id);
}
private static Task<Validation<BaseError, ProgramSchedule>> Validate(
TvContext dbContext,
CreateProgramSchedule request) =>
ValidationT_AsyncSync_Extensions.MapT(
ValidateName(dbContext, request),
name =>
{
bool keepMultiPartEpisodesTogether =
request.MediaCollectionPlaybackOrder == PlaybackOrder.Shuffle &&
request.KeepMultiPartEpisodesTogether;
return new ProgramSchedule
{ {
bool keepMultiPartEpisodesTogether = Name = name,
request.MediaCollectionPlaybackOrder == PlaybackOrder.Shuffle && MediaCollectionPlaybackOrder = request.MediaCollectionPlaybackOrder,
request.KeepMultiPartEpisodesTogether; KeepMultiPartEpisodesTogether = keepMultiPartEpisodesTogether,
return new ProgramSchedule TreatCollectionsAsShows = keepMultiPartEpisodesTogether && request.TreatCollectionsAsShows
{ };
Name = name, });
MediaCollectionPlaybackOrder = request.MediaCollectionPlaybackOrder,
KeepMultiPartEpisodesTogether = keepMultiPartEpisodesTogether,
TreatCollectionsAsShows = keepMultiPartEpisodesTogether && request.TreatCollectionsAsShows
};
});
private async Task<Validation<BaseError, string>> ValidateName(CreateProgramSchedule createProgramSchedule) private static async Task<Validation<BaseError, string>> ValidateName(
TvContext dbContext,
CreateProgramSchedule createProgramSchedule)
{ {
List<string> allNames = await _programScheduleRepository.GetAll()
.Map(list => list.Map(c => c.Name).ToList());
Validation<BaseError, string> result1 = createProgramSchedule.NotEmpty(c => c.Name) Validation<BaseError, string> result1 = createProgramSchedule.NotEmpty(c => c.Name)
.Bind(_ => createProgramSchedule.NotLongerThan(50)(c => c.Name)); .Bind(_ => createProgramSchedule.NotLongerThan(50)(c => c.Name));
var result2 = Optional(createProgramSchedule.Name) int duplicateNameCount = await dbContext.ProgramSchedules
.Filter(name => !allNames.Contains(name)) .CountAsync(ps => ps.Name == createProgramSchedule.Name);
var result2 = Optional(duplicateNameCount)
.Filter(count => count == 0)
.ToValidation<BaseError>("Schedule name must be unique"); .ToValidation<BaseError>("Schedule name must be unique");
return (result1, result2).Apply((_, _) => createProgramSchedule.Name); return (result1, result2).Apply((_, _) => createProgramSchedule.Name);

4
ErsatzTV.Application/ProgramSchedules/Commands/CreateProgramScheduleResult.cs

@ -0,0 +1,4 @@
namespace ErsatzTV.Application.ProgramSchedules.Commands
{
public record CreateProgramScheduleResult(int ProgramScheduleId) : EntityIdResult(ProgramScheduleId);
}

5
ErsatzTV.Application/ProgramSchedules/Commands/DeleteProgramSchedule.cs

@ -1,9 +1,8 @@
using System.Threading.Tasks; using ErsatzTV.Core;
using ErsatzTV.Core;
using LanguageExt; using LanguageExt;
using MediatR; using MediatR;
namespace ErsatzTV.Application.ProgramSchedules.Commands namespace ErsatzTV.Application.ProgramSchedules.Commands
{ {
public record DeleteProgramSchedule(int ProgramScheduleId) : IRequest<Either<BaseError, Task>>; public record DeleteProgramSchedule(int ProgramScheduleId) : IRequest<Either<BaseError, LanguageExt.Unit>>;
} }

43
ErsatzTV.Application/ProgramSchedules/Commands/DeleteProgramScheduleHandler.cs

@ -1,32 +1,43 @@
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Domain;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt; using LanguageExt;
using MediatR; using MediatR;
using Microsoft.EntityFrameworkCore;
using Unit = LanguageExt.Unit;
namespace ErsatzTV.Application.ProgramSchedules.Commands namespace ErsatzTV.Application.ProgramSchedules.Commands
{ {
public class DeleteProgramScheduleHandler : IRequestHandler<DeleteProgramSchedule, Either<BaseError, Task>> public class DeleteProgramScheduleHandler : IRequestHandler<DeleteProgramSchedule, Either<BaseError, Unit>>
{ {
private readonly IProgramScheduleRepository _programScheduleRepository; private readonly IDbContextFactory<TvContext> _dbContextFactory;
public DeleteProgramScheduleHandler(IProgramScheduleRepository programScheduleRepository) => public DeleteProgramScheduleHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_programScheduleRepository = programScheduleRepository; _dbContextFactory = dbContextFactory;
public async Task<Either<BaseError, Task>> Handle( public async Task<Either<BaseError, Unit>> Handle(
DeleteProgramSchedule request, DeleteProgramSchedule request,
CancellationToken cancellationToken) => CancellationToken cancellationToken)
(await ProgramScheduleMustExist(request)) {
.Map(DoDeletion) await using TvContext dbContext = _dbContextFactory.CreateDbContext();
.ToEither<Task>(); Validation<BaseError, ProgramSchedule> validation = await ProgramScheduleMustExist(dbContext, request);
return await validation.Apply(ps => DoDeletion(dbContext, ps));
}
private Task DoDeletion(int programScheduleId) => _programScheduleRepository.Delete(programScheduleId); private static Task<Unit> DoDeletion(TvContext dbContext, ProgramSchedule programSchedule)
{
dbContext.ProgramSchedules.Remove(programSchedule);
return dbContext.SaveChangesAsync().ToUnit();
}
private async Task<Validation<BaseError, int>> ProgramScheduleMustExist( private Task<Validation<BaseError, ProgramSchedule>> ProgramScheduleMustExist(
DeleteProgramSchedule deleteProgramSchedule) => TvContext dbContext,
(await _programScheduleRepository.Get(deleteProgramSchedule.ProgramScheduleId)) DeleteProgramSchedule request) =>
.ToValidation<BaseError>($"ProgramSchedule {deleteProgramSchedule.ProgramScheduleId} does not exist.") dbContext.ProgramSchedules
.Map(c => c.Id); .SelectOneAsync(ps => ps.Id, ps => ps.Id == request.ProgramScheduleId)
.Map(o => o.ToValidation<BaseError>($"ProgramSchedule {request.ProgramScheduleId} does not exist."));
} }
} }

24
ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs

@ -2,23 +2,25 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt; using LanguageExt;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.ProgramSchedules.Commands namespace ErsatzTV.Application.ProgramSchedules.Commands
{ {
public abstract class ProgramScheduleItemCommandBase public abstract class ProgramScheduleItemCommandBase
{ {
private readonly IProgramScheduleRepository _programScheduleRepository; protected static Task<Validation<BaseError, ProgramSchedule>> ProgramScheduleMustExist(
TvContext dbContext,
protected ProgramScheduleItemCommandBase(IProgramScheduleRepository programScheduleRepository) => int programScheduleId) =>
_programScheduleRepository = programScheduleRepository; dbContext.ProgramSchedules
.Include(ps => ps.Items)
protected async Task<Validation<BaseError, ProgramSchedule>> ProgramScheduleMustExist(int programScheduleId) => .Include(ps => ps.Playouts)
(await _programScheduleRepository.GetWithPlayouts(programScheduleId)) .SelectOneAsync(ps => ps.Id, ps => ps.Id == programScheduleId)
.ToValidation<BaseError>("[ProgramScheduleId] does not exist."); .Map(o => o.ToValidation<BaseError>("[ProgramScheduleId] does not exist."));
protected Validation<BaseError, ProgramSchedule> PlayoutModeMustBeValid( protected static Validation<BaseError, ProgramSchedule> PlayoutModeMustBeValid(
IProgramScheduleItemRequest item, IProgramScheduleItemRequest item,
ProgramSchedule programSchedule) ProgramSchedule programSchedule)
{ {

37
ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItemsHandler.cs

@ -6,43 +6,46 @@ using System.Threading.Tasks;
using ErsatzTV.Application.Playouts.Commands; using ErsatzTV.Application.Playouts.Commands;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Infrastructure.Data;
using LanguageExt; using LanguageExt;
using MediatR; using MediatR;
using Microsoft.EntityFrameworkCore;
using static ErsatzTV.Application.ProgramSchedules.Mapper; using static ErsatzTV.Application.ProgramSchedules.Mapper;
namespace ErsatzTV.Application.ProgramSchedules.Commands namespace ErsatzTV.Application.ProgramSchedules.Commands
{ {
public class ReplaceProgramScheduleItemsHandler : ProgramScheduleItemCommandBase, public class ReplaceProgramScheduleItemsHandler : ProgramScheduleItemCommandBase,
IRequestHandler<ReplaceProgramScheduleItems, IRequestHandler<ReplaceProgramScheduleItems, Either<BaseError, IEnumerable<ProgramScheduleItemViewModel>>>
Either<BaseError, IEnumerable<ProgramScheduleItemViewModel>>>
{ {
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ChannelWriter<IBackgroundServiceRequest> _channel; private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly IProgramScheduleRepository _programScheduleRepository;
public ReplaceProgramScheduleItemsHandler( public ReplaceProgramScheduleItemsHandler(
IProgramScheduleRepository programScheduleRepository, IDbContextFactory<TvContext> dbContextFactory,
ChannelWriter<IBackgroundServiceRequest> channel) ChannelWriter<IBackgroundServiceRequest> channel)
: base(programScheduleRepository)
{ {
_programScheduleRepository = programScheduleRepository; _dbContextFactory = dbContextFactory;
_channel = channel; _channel = channel;
} }
public Task<Either<BaseError, IEnumerable<ProgramScheduleItemViewModel>>> Handle( public async Task<Either<BaseError, IEnumerable<ProgramScheduleItemViewModel>>> Handle(
ReplaceProgramScheduleItems request, ReplaceProgramScheduleItems request,
CancellationToken cancellationToken) => CancellationToken cancellationToken)
Validate(request) {
.MapT(programSchedule => PersistItems(request, programSchedule)) await using TvContext dbContext = _dbContextFactory.CreateDbContext();
.Bind(v => v.ToEitherAsync()); Validation<BaseError, ProgramSchedule> validation = await Validate(dbContext, request);
return await validation.Apply(ps => PersistItems(dbContext, request, ps));
}
private async Task<IEnumerable<ProgramScheduleItemViewModel>> PersistItems( private async Task<IEnumerable<ProgramScheduleItemViewModel>> PersistItems(
TvContext dbContext,
ReplaceProgramScheduleItems request, ReplaceProgramScheduleItems request,
ProgramSchedule programSchedule) ProgramSchedule programSchedule)
{ {
dbContext.RemoveRange(programSchedule.Items);
programSchedule.Items = request.Items.Map(i => BuildItem(programSchedule, i.Index, i)).ToList(); programSchedule.Items = request.Items.Map(i => BuildItem(programSchedule, i.Index, i)).ToList();
await _programScheduleRepository.Update(programSchedule); await dbContext.SaveChangesAsync();
// rebuild any playouts that use this schedule // rebuild any playouts that use this schedule
foreach (Playout playout in programSchedule.Playouts) foreach (Playout playout in programSchedule.Playouts)
@ -53,12 +56,14 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
return programSchedule.Items.Map(ProjectToViewModel); return programSchedule.Items.Map(ProjectToViewModel);
} }
private Task<Validation<BaseError, ProgramSchedule>> Validate(ReplaceProgramScheduleItems request) => private Task<Validation<BaseError, ProgramSchedule>> Validate(
ProgramScheduleMustExist(request.ProgramScheduleId) TvContext dbContext,
ReplaceProgramScheduleItems request) =>
ProgramScheduleMustExist(dbContext, request.ProgramScheduleId)
.BindT(programSchedule => PlayoutModesMustBeValid(request, programSchedule)) .BindT(programSchedule => PlayoutModesMustBeValid(request, programSchedule))
.BindT(programSchedule => CollectionTypesMustBeValid(request, programSchedule)); .BindT(programSchedule => CollectionTypesMustBeValid(request, programSchedule));
private Validation<BaseError, ProgramSchedule> PlayoutModesMustBeValid( private static Validation<BaseError, ProgramSchedule> PlayoutModesMustBeValid(
ReplaceProgramScheduleItems request, ReplaceProgramScheduleItems request,
ProgramSchedule programSchedule) => ProgramSchedule programSchedule) =>
request.Items.Map(item => PlayoutModeMustBeValid(item, programSchedule)).Sequence() request.Items.Map(item => PlayoutModeMustBeValid(item, programSchedule)).Sequence()

2
ErsatzTV.Application/ProgramSchedules/Commands/UpdateProgramSchedule.cs

@ -11,5 +11,5 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
string Name, string Name,
PlaybackOrder MediaCollectionPlaybackOrder, PlaybackOrder MediaCollectionPlaybackOrder,
bool KeepMultiPartEpisodesTogether, bool KeepMultiPartEpisodesTogether,
bool TreatCollectionsAsShows) : IRequest<Either<BaseError, ProgramScheduleViewModel>>; bool TreatCollectionsAsShows) : IRequest<Either<BaseError, UpdateProgramScheduleResult>>;
} }

91
ErsatzTV.Application/ProgramSchedules/Commands/UpdateProgramScheduleHandler.cs

@ -1,79 +1,94 @@
using System.Threading; using System.Collections.Generic;
using System.Threading;
using System.Threading.Channels; using System.Threading.Channels;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Application.Playouts.Commands; using ErsatzTV.Application.Playouts.Commands;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt; using LanguageExt;
using MediatR; using MediatR;
using static ErsatzTV.Application.ProgramSchedules.Mapper; using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.ProgramSchedules.Commands namespace ErsatzTV.Application.ProgramSchedules.Commands
{ {
public class public class UpdateProgramScheduleHandler :
UpdateProgramScheduleHandler : IRequestHandler<UpdateProgramSchedule, IRequestHandler<UpdateProgramSchedule, Either<BaseError, UpdateProgramScheduleResult>>
Either<BaseError, ProgramScheduleViewModel>>
{ {
private readonly ChannelWriter<IBackgroundServiceRequest> _channel; private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly IProgramScheduleRepository _programScheduleRepository; private readonly IDbContextFactory<TvContext> _dbContextFactory;
public UpdateProgramScheduleHandler( public UpdateProgramScheduleHandler(
IProgramScheduleRepository programScheduleRepository, IDbContextFactory<TvContext> dbContextFactory,
ChannelWriter<IBackgroundServiceRequest> channel) ChannelWriter<IBackgroundServiceRequest> channel)
{ {
_programScheduleRepository = programScheduleRepository; _dbContextFactory = dbContextFactory;
_channel = channel; _channel = channel;
} }
public Task<Either<BaseError, ProgramScheduleViewModel>> Handle( public async Task<Either<BaseError, UpdateProgramScheduleResult>> Handle(
UpdateProgramSchedule request, UpdateProgramSchedule request,
CancellationToken cancellationToken) => CancellationToken cancellationToken)
Validate(request) {
.MapT(c => ApplyUpdateRequest(c, request)) await using TvContext dbContext = _dbContextFactory.CreateDbContext();
.Bind(v => v.ToEitherAsync());
Validation<BaseError, ProgramSchedule> validation = await Validate(dbContext, request);
return await validation.Apply(ps => ApplyUpdateRequest(dbContext, ps, request));
}
private async Task<ProgramScheduleViewModel> ApplyUpdateRequest( private async Task<UpdateProgramScheduleResult> ApplyUpdateRequest(
TvContext dbContext,
ProgramSchedule programSchedule, ProgramSchedule programSchedule,
UpdateProgramSchedule update) UpdateProgramSchedule request)
{ {
// we need to rebuild playouts if the playback order or keep multi-episodes has been modified // we need to rebuild playouts if the playback order or keep multi-episodes has been modified
bool needToRebuildPlayout = bool needToRebuildPlayout =
programSchedule.MediaCollectionPlaybackOrder != update.MediaCollectionPlaybackOrder || programSchedule.MediaCollectionPlaybackOrder != request.MediaCollectionPlaybackOrder ||
programSchedule.KeepMultiPartEpisodesTogether != update.KeepMultiPartEpisodesTogether || programSchedule.KeepMultiPartEpisodesTogether != request.KeepMultiPartEpisodesTogether ||
programSchedule.TreatCollectionsAsShows != update.TreatCollectionsAsShows; programSchedule.TreatCollectionsAsShows != request.TreatCollectionsAsShows;
programSchedule.Name = update.Name; programSchedule.Name = request.Name;
programSchedule.MediaCollectionPlaybackOrder = update.MediaCollectionPlaybackOrder; programSchedule.MediaCollectionPlaybackOrder = request.MediaCollectionPlaybackOrder;
programSchedule.KeepMultiPartEpisodesTogether = programSchedule.KeepMultiPartEpisodesTogether =
update.MediaCollectionPlaybackOrder == PlaybackOrder.Shuffle && request.MediaCollectionPlaybackOrder == PlaybackOrder.Shuffle &&
update.KeepMultiPartEpisodesTogether; request.KeepMultiPartEpisodesTogether;
programSchedule.TreatCollectionsAsShows = programSchedule.KeepMultiPartEpisodesTogether && programSchedule.TreatCollectionsAsShows = programSchedule.KeepMultiPartEpisodesTogether &&
update.TreatCollectionsAsShows; request.TreatCollectionsAsShows;
await _programScheduleRepository.Update(programSchedule);
await dbContext.SaveChangesAsync();
if (needToRebuildPlayout) if (needToRebuildPlayout)
{ {
foreach (Playout playout in programSchedule.Playouts) List<int> playoutIds = await dbContext.Playouts
.Filter(p => p.ProgramScheduleId == programSchedule.Id)
.Map(p => p.Id)
.ToListAsync();
foreach (int playoutId in playoutIds)
{ {
await _channel.WriteAsync(new BuildPlayout(playout.Id, true)); await _channel.WriteAsync(new BuildPlayout(playoutId, true));
} }
} }
return ProjectToViewModel(programSchedule); return new UpdateProgramScheduleResult(programSchedule.Id);
} }
private async Task<Validation<BaseError, ProgramSchedule>> Validate(UpdateProgramSchedule request) => private static async Task<Validation<BaseError, ProgramSchedule>> Validate(
(await ProgramScheduleMustExist(request), ValidateName(request)) TvContext dbContext,
.Apply((programScheduleToUpdate, _) => programScheduleToUpdate); UpdateProgramSchedule request) =>
(await ProgramScheduleMustExist(dbContext, request), ValidateName(request))
.Apply((programSchedule, _) => programSchedule);
private async Task<Validation<BaseError, ProgramSchedule>> ProgramScheduleMustExist( private static Task<Validation<BaseError, ProgramSchedule>> ProgramScheduleMustExist(
UpdateProgramSchedule updateProgramSchedule) => TvContext dbContext,
(await _programScheduleRepository.GetWithPlayouts(updateProgramSchedule.ProgramScheduleId)) UpdateProgramSchedule request) =>
.ToValidation<BaseError>("ProgramSchedule does not exist."); dbContext.ProgramSchedules
.SelectOneAsync(ps => ps.Id, ps => ps.Id == request.ProgramScheduleId)
.Map(o => o.ToValidation<BaseError>("ProgramSchedule does not exist"));
private Validation<BaseError, string> ValidateName(UpdateProgramSchedule updateProgramSchedule) => private static Validation<BaseError, string> ValidateName(UpdateProgramSchedule request) =>
updateProgramSchedule.NotEmpty(c => c.Name) request.NotEmpty(c => c.Name)
.Bind(_ => updateProgramSchedule.NotLongerThan(50)(c => c.Name)); .Bind(_ => request.NotLongerThan(50)(c => c.Name));
} }
} }

4
ErsatzTV.Application/ProgramSchedules/Commands/UpdateProgramScheduleResult.cs

@ -0,0 +1,4 @@
namespace ErsatzTV.Application.ProgramSchedules.Commands
{
public record UpdateProgramScheduleResult(int ProgramScheduleId) : EntityIdResult(ProgramScheduleId);
}

26
ErsatzTV.Application/ProgramSchedules/Queries/GetAllProgramSchedulesHandler.cs

@ -1,23 +1,33 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Infrastructure.Data;
using MediatR; using MediatR;
using static ErsatzTV.Application.ProgramSchedules.Mapper; using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.ProgramSchedules.Queries namespace ErsatzTV.Application.ProgramSchedules.Queries
{ {
public class GetAllProgramSchedulesHandler : IRequestHandler<GetAllProgramSchedules, List<ProgramScheduleViewModel>> public class GetAllProgramSchedulesHandler : IRequestHandler<GetAllProgramSchedules, List<ProgramScheduleViewModel>>
{ {
private readonly IProgramScheduleRepository _programScheduleRepository; private readonly IDbContextFactory<TvContext> _dbContextFactory;
public GetAllProgramSchedulesHandler(IProgramScheduleRepository programScheduleRepository) => public GetAllProgramSchedulesHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_programScheduleRepository = programScheduleRepository; _dbContextFactory = dbContextFactory;
public async Task<List<ProgramScheduleViewModel>> Handle( public async Task<List<ProgramScheduleViewModel>> Handle(
GetAllProgramSchedules request, GetAllProgramSchedules request,
CancellationToken cancellationToken) => CancellationToken cancellationToken)
(await _programScheduleRepository.GetAll()).Map(ProjectToViewModel).ToList(); {
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
return await dbContext.ProgramSchedules
.Map(
ps => new ProgramScheduleViewModel(
ps.Id,
ps.Name,
ps.MediaCollectionPlaybackOrder,
ps.KeepMultiPartEpisodesTogether,
ps.TreatCollectionsAsShows))
.ToListAsync(cancellationToken);
}
} }
} }

24
ErsatzTV.Application/ProgramSchedules/Queries/GetProgramScheduleByIdHandler.cs

@ -1,24 +1,30 @@
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt; using LanguageExt;
using MediatR; using MediatR;
using Microsoft.EntityFrameworkCore;
using static ErsatzTV.Application.ProgramSchedules.Mapper; using static ErsatzTV.Application.ProgramSchedules.Mapper;
namespace ErsatzTV.Application.ProgramSchedules.Queries namespace ErsatzTV.Application.ProgramSchedules.Queries
{ {
public class public class GetProgramScheduleByIdHandler :
GetProgramScheduleByIdHandler : IRequestHandler<GetProgramScheduleById, Option<ProgramScheduleViewModel>> IRequestHandler<GetProgramScheduleById, Option<ProgramScheduleViewModel>>
{ {
private readonly IProgramScheduleRepository _programScheduleRepository; private readonly IDbContextFactory<TvContext> _dbContextFactory;
public GetProgramScheduleByIdHandler(IProgramScheduleRepository programScheduleRepository) => public GetProgramScheduleByIdHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_programScheduleRepository = programScheduleRepository; _dbContextFactory = dbContextFactory;
public Task<Option<ProgramScheduleViewModel>> Handle( public async Task<Option<ProgramScheduleViewModel>> Handle(
GetProgramScheduleById request, GetProgramScheduleById request,
CancellationToken cancellationToken) => CancellationToken cancellationToken)
_programScheduleRepository.Get(request.Id) {
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
return await dbContext.ProgramSchedules
.SelectOneAsync(ps => ps.Id, ps => ps.Id == request.Id)
.MapT(ProjectToViewModel); .MapT(ProjectToViewModel);
}
} }
} }

3
ErsatzTV.Application/ProgramSchedules/Queries/GetProgramScheduleItems.cs

@ -1,8 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using LanguageExt;
using MediatR; using MediatR;
namespace ErsatzTV.Application.ProgramSchedules.Queries namespace ErsatzTV.Application.ProgramSchedules.Queries
{ {
public record GetProgramScheduleItems(int Id) : IRequest<Option<IEnumerable<ProgramScheduleItemViewModel>>>; public record GetProgramScheduleItems(int Id) : IRequest<List<ProgramScheduleItemViewModel>>;
} }

45
ErsatzTV.Application/ProgramSchedules/Queries/GetProgramScheduleItemsHandler.cs

@ -1,25 +1,50 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Domain;
using ErsatzTV.Infrastructure.Data;
using LanguageExt; using LanguageExt;
using MediatR; using MediatR;
using Microsoft.EntityFrameworkCore;
using static ErsatzTV.Application.ProgramSchedules.Mapper; using static ErsatzTV.Application.ProgramSchedules.Mapper;
namespace ErsatzTV.Application.ProgramSchedules.Queries namespace ErsatzTV.Application.ProgramSchedules.Queries
{ {
public class GetProgramScheduleItemsHandler : IRequestHandler<GetProgramScheduleItems, public class GetProgramScheduleItemsHandler :
Option<IEnumerable<ProgramScheduleItemViewModel>>> IRequestHandler<GetProgramScheduleItems, List<ProgramScheduleItemViewModel>>
{ {
private readonly IProgramScheduleRepository _programScheduleRepository; private readonly IDbContextFactory<TvContext> _dbContextFactory;
public GetProgramScheduleItemsHandler(IProgramScheduleRepository programScheduleRepository) => public GetProgramScheduleItemsHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_programScheduleRepository = programScheduleRepository; _dbContextFactory = dbContextFactory;
public Task<Option<IEnumerable<ProgramScheduleItemViewModel>>> Handle( public async Task<List<ProgramScheduleItemViewModel>> Handle(
GetProgramScheduleItems request, GetProgramScheduleItems request,
CancellationToken cancellationToken) => CancellationToken cancellationToken)
_programScheduleRepository.GetItems(request.Id) {
.MapT(programScheduleItems => programScheduleItems.Map(ProjectToViewModel)); await using TvContext dbContext = _dbContextFactory.CreateDbContext();
return await dbContext.ProgramScheduleItems
.Filter(psi => psi.ProgramScheduleId == request.Id)
.Include(i => i.Collection)
.Include(i => i.MediaItem)
.ThenInclude(i => (i as Movie).MovieMetadata)
.ThenInclude(mm => mm.Artwork)
.Include(i => i.MediaItem)
.ThenInclude(i => (i as Season).SeasonMetadata)
.ThenInclude(sm => sm.Artwork)
.Include(i => i.MediaItem)
.ThenInclude(i => (i as Season).Show)
.ThenInclude(s => s.ShowMetadata)
.ThenInclude(sm => sm.Artwork)
.Include(i => i.MediaItem)
.ThenInclude(i => (i as Show).ShowMetadata)
.ThenInclude(sm => sm.Artwork)
.Include(i => i.MediaItem)
.ThenInclude(i => (i as Artist).ArtistMetadata)
.ThenInclude(am => am.Artwork)
.ToListAsync(cancellationToken)
.Map(programScheduleItems => programScheduleItems.Map(ProjectToViewModel).ToList());
}
} }
} }

20
ErsatzTV.Application/Resolutions/Queries/GetAllResolutionsHandler.cs

@ -2,21 +2,29 @@
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Infrastructure.Data;
using LanguageExt; using LanguageExt;
using MediatR; using MediatR;
using Microsoft.EntityFrameworkCore;
using static ErsatzTV.Application.Resolutions.Mapper; using static ErsatzTV.Application.Resolutions.Mapper;
namespace ErsatzTV.Application.Resolutions.Queries namespace ErsatzTV.Application.Resolutions.Queries
{ {
public class GetAllResolutionsHandler : IRequestHandler<GetAllResolutions, List<ResolutionViewModel>> public class GetAllResolutionsHandler : IRequestHandler<GetAllResolutions, List<ResolutionViewModel>>
{ {
private readonly IResolutionRepository _resolutionRepository; private readonly IDbContextFactory<TvContext> _dbContextFactory;
public GetAllResolutionsHandler(IResolutionRepository resolutionRepository) => public GetAllResolutionsHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_resolutionRepository = resolutionRepository; _dbContextFactory = dbContextFactory;
public Task<List<ResolutionViewModel>> Handle(GetAllResolutions request, CancellationToken cancellationToken) => public async Task<List<ResolutionViewModel>> Handle(
_resolutionRepository.GetAll().Map(resolutions => resolutions.Map(ProjectToViewModel).ToList()); GetAllResolutions request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
return await dbContext.Resolutions
.ToListAsync(cancellationToken)
.Map(list => list.Map(ProjectToViewModel).ToList());
}
} }
} }

18
ErsatzTV.Application/Search/Commands/RebuildSearchIndexHandler.cs

@ -49,23 +49,7 @@ namespace ErsatzTV.Application.Search.Commands
List<int> itemIds = await _searchRepository.GetItemIdsToIndex(); List<int> itemIds = await _searchRepository.GetItemIdsToIndex();
await _searchIndex.Rebuild(_searchRepository, itemIds); await _searchIndex.Rebuild(_searchRepository, itemIds);
Option<ConfigElement> maybeVersion = await _configElementRepository.Upsert(ConfigElementKey.SearchIndexVersion, _searchIndex.Version);
await _configElementRepository.Get(ConfigElementKey.SearchIndexVersion);
await maybeVersion.Match(
version =>
{
version.Value = _searchIndex.Version.ToString();
return _configElementRepository.Update(version);
},
() =>
{
var configElement = new ConfigElement
{
Key = ConfigElementKey.SearchIndexVersion.Key,
Value = _searchIndex.Version.ToString()
};
return _configElementRepository.Add(configElement);
});
_logger.LogDebug("Done migrating search index"); _logger.LogDebug("Done migrating search index");
} }

73
ErsatzTV.Application/Streaming/Queries/FFmpegProcessHandler.cs

@ -5,9 +5,11 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt; using LanguageExt;
using MediatR; using MediatR;
using Microsoft.EntityFrameworkCore;
using static LanguageExt.Prelude; using static LanguageExt.Prelude;
namespace ErsatzTV.Application.Streaming.Queries namespace ErsatzTV.Application.Streaming.Queries
@ -15,46 +17,55 @@ namespace ErsatzTV.Application.Streaming.Queries
public abstract class FFmpegProcessHandler<T> : IRequestHandler<T, Either<BaseError, Process>> public abstract class FFmpegProcessHandler<T> : IRequestHandler<T, Either<BaseError, Process>>
where T : FFmpegProcessRequest where T : FFmpegProcessRequest
{ {
private readonly IChannelRepository _channelRepository; private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IConfigElementRepository _configElementRepository;
protected FFmpegProcessHandler( protected FFmpegProcessHandler(IDbContextFactory<TvContext> dbContextFactory) =>
IChannelRepository channelRepository, _dbContextFactory = dbContextFactory;
IConfigElementRepository configElementRepository)
public async Task<Either<BaseError, Process>> Handle(T request, CancellationToken cancellationToken)
{ {
_channelRepository = channelRepository; await using TvContext dbContext = _dbContextFactory.CreateDbContext();
_configElementRepository = configElementRepository; Validation<BaseError, Tuple<Channel, string>> validation = await Validate(dbContext, request);
return await validation.Match(
tuple => GetProcess(dbContext, request, tuple.Item1, tuple.Item2),
error => Task.FromResult<Either<BaseError, Process>>(error.Join()));
} }
public Task<Either<BaseError, Process>> Handle(T request, CancellationToken cancellationToken) => protected abstract Task<Either<BaseError, Process>> GetProcess(
Validate(request) TvContext dbContext,
.Map(v => v.ToEither<Tuple<Channel, string>>()) T request,
.BindT(tuple => GetProcess(request, tuple.Item1, tuple.Item2)); Channel channel,
string ffmpegPath);
protected abstract Task<Either<BaseError, Process>> GetProcess(T request, Channel channel, string ffmpegPath);
private async Task<Validation<BaseError, Tuple<Channel, string>>> Validate(T request) => private static async Task<Validation<BaseError, Tuple<Channel, string>>> Validate(
(await ChannelMustExist(request), await FFmpegPathMustExist()) TvContext dbContext,
T request) =>
(await ChannelMustExist(dbContext, request), await FFmpegPathMustExist(dbContext))
.Apply((channel, ffmpegPath) => Tuple(channel, ffmpegPath)); .Apply((channel, ffmpegPath) => Tuple(channel, ffmpegPath));
private async Task<Validation<BaseError, Channel>> ChannelMustExist(T request) => private static Task<Validation<BaseError, Channel>> ChannelMustExist(TvContext dbContext, T request) =>
(await _channelRepository.GetByNumber(request.ChannelNumber)) dbContext.Channels
.Map( .Include(c => c.FFmpegProfile)
channel => .ThenInclude(p => p.Resolution)
{ .Include(c => c.Artwork)
channel.StreamingMode = request.Mode.ToLowerInvariant() switch .Include(c => c.Watermark)
.SelectOneAsync(c => c.Number, c => c.Number == request.ChannelNumber)
.MapT(
channel =>
{ {
"hls-direct" => StreamingMode.HttpLiveStreamingDirect, channel.StreamingMode = request.Mode.ToLowerInvariant() switch
"ts" => StreamingMode.TransportStream, {
_ => channel.StreamingMode "hls-direct" => StreamingMode.HttpLiveStreamingDirect,
}; "ts" => StreamingMode.TransportStream,
_ => channel.StreamingMode
};
return channel; return channel;
}) })
.ToValidation<BaseError>($"Channel number {request.ChannelNumber} does not exist."); .Map(o => o.ToValidation<BaseError>($"Channel number {request.ChannelNumber} does not exist."));
private Task<Validation<BaseError, string>> FFmpegPathMustExist() => private static Task<Validation<BaseError, string>> FFmpegPathMustExist(TvContext dbContext) =>
_configElementRepository.GetValue<string>(ConfigElementKey.FFmpegPath) dbContext.ConfigElements.GetValue<string>(ConfigElementKey.FFmpegPath)
.FilterT(File.Exists) .FilterT(File.Exists)
.Map(maybePath => maybePath.ToValidation<BaseError>("FFmpeg path does not exist on filesystem")); .Map(maybePath => maybePath.ToValidation<BaseError>("FFmpeg path does not exist on filesystem"));
} }

14
ErsatzTV.Application/Streaming/Queries/GetConcatProcessByChannelNumberHandler.cs

@ -4,36 +4,36 @@ using System.Threading.Tasks;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.FFmpeg; using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Runtime; using ErsatzTV.Core.Interfaces.Runtime;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt; using LanguageExt;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Streaming.Queries namespace ErsatzTV.Application.Streaming.Queries
{ {
public class GetConcatProcessByChannelNumberHandler : FFmpegProcessHandler<GetConcatProcessByChannelNumber> public class GetConcatProcessByChannelNumberHandler : FFmpegProcessHandler<GetConcatProcessByChannelNumber>
{ {
private readonly IConfigElementRepository _configElementRepository;
private readonly FFmpegProcessService _ffmpegProcessService; private readonly FFmpegProcessService _ffmpegProcessService;
private readonly IRuntimeInfo _runtimeInfo; private readonly IRuntimeInfo _runtimeInfo;
public GetConcatProcessByChannelNumberHandler( public GetConcatProcessByChannelNumberHandler(
IChannelRepository channelRepository, IDbContextFactory<TvContext> dbContextFactory,
IConfigElementRepository configElementRepository,
FFmpegProcessService ffmpegProcessService, FFmpegProcessService ffmpegProcessService,
IRuntimeInfo runtimeInfo) IRuntimeInfo runtimeInfo)
: base(channelRepository, configElementRepository) : base(dbContextFactory)
{ {
_configElementRepository = configElementRepository;
_ffmpegProcessService = ffmpegProcessService; _ffmpegProcessService = ffmpegProcessService;
_runtimeInfo = runtimeInfo; _runtimeInfo = runtimeInfo;
} }
protected override async Task<Either<BaseError, Process>> GetProcess( protected override async Task<Either<BaseError, Process>> GetProcess(
TvContext dbContext,
GetConcatProcessByChannelNumber request, GetConcatProcessByChannelNumber request,
Channel channel, Channel channel,
string ffmpegPath) string ffmpegPath)
{ {
bool saveReports = !_runtimeInfo.IsOSPlatform(OSPlatform.Windows) && await _configElementRepository bool saveReports = !_runtimeInfo.IsOSPlatform(OSPlatform.Windows) && await dbContext.ConfigElements
.GetValue<bool>(ConfigElementKey.FFmpegSaveReports) .GetValue<bool>(ConfigElementKey.FFmpegSaveReports)
.Map(result => result.IfNone(false)); .Map(result => result.IfNone(false));

84
ErsatzTV.Application/Streaming/Queries/GetHlsPlaylistByChannelNumberHandler.cs

@ -3,73 +3,83 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt; using LanguageExt;
using MediatR; using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Serilog;
namespace ErsatzTV.Application.Streaming.Queries namespace ErsatzTV.Application.Streaming.Queries
{ {
public class public class GetHlsPlaylistByChannelNumberHandler :
GetHlsPlaylistByChannelNumberHandler : IRequestHandler<GetHlsPlaylistByChannelNumber, Either<BaseError, string>> IRequestHandler<GetHlsPlaylistByChannelNumber, Either<BaseError, string>>
{ {
private readonly IChannelRepository _channelRepository; private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IMemoryCache _memoryCache; private readonly IMemoryCache _memoryCache;
private readonly IPlayoutRepository _playoutRepository;
public GetHlsPlaylistByChannelNumberHandler( public GetHlsPlaylistByChannelNumberHandler(
IChannelRepository channelRepository, IDbContextFactory<TvContext> dbContextFactory,
IPlayoutRepository playoutRepository,
IMemoryCache memoryCache) IMemoryCache memoryCache)
{ {
_channelRepository = channelRepository; _dbContextFactory = dbContextFactory;
_playoutRepository = playoutRepository;
_memoryCache = memoryCache; _memoryCache = memoryCache;
} }
public Task<Either<BaseError, string>> Handle( public async Task<Either<BaseError, string>> Handle(
GetHlsPlaylistByChannelNumber request, GetHlsPlaylistByChannelNumber request,
CancellationToken cancellationToken) => CancellationToken cancellationToken)
ChannelMustExist(request) {
.Map(v => v.ToEither<Channel>()) await using TvContext dbContext = _dbContextFactory.CreateDbContext();
.BindT(channel => GetPlaylist(request, channel)); DateTimeOffset now = DateTimeOffset.Now;
Validation<BaseError, Parameters> validation = await Validate(dbContext, request, now);
return await validation.Apply(parameters => GetPlaylist(dbContext, request, parameters, now));
}
private async Task<Either<BaseError, string>> GetPlaylist( private Task<string> GetPlaylist(
TvContext dbContext,
GetHlsPlaylistByChannelNumber request, GetHlsPlaylistByChannelNumber request,
Channel channel) Parameters parameters,
DateTimeOffset now)
{ {
string mode = string.IsNullOrWhiteSpace(request.Mode) string mode = string.IsNullOrWhiteSpace(request.Mode)
? string.Empty ? string.Empty
: $"&mode={request.Mode}"; : $"&mode={request.Mode}";
DateTimeOffset now = DateTimeOffset.Now; long index = GetIndexForChannel(parameters.Channel, parameters.PlayoutItem);
Option<PlayoutItem> maybePlayoutItem = await _playoutRepository.GetPlayoutItem(channel.Id, now); double timeRemaining = Math.Abs((parameters.PlayoutItem.FinishOffset - now).TotalSeconds);
return maybePlayoutItem.Match<Either<BaseError, string>>( return $@"#EXTM3U
playoutItem =>
{
long index = GetIndexForChannel(channel, playoutItem);
double timeRemaining = Math.Abs((playoutItem.FinishOffset - now).TotalSeconds);
return $@"#EXTM3U
#EXT-X-VERSION:3 #EXT-X-VERSION:3
#EXT-X-TARGETDURATION:10 #EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:{index} #EXT-X-MEDIA-SEQUENCE:{index}
#EXT-X-DISCONTINUITY #EXT-X-DISCONTINUITY
#EXTINF:{timeRemaining:F2}, #EXTINF:{timeRemaining:F2},
{request.Scheme}://{request.Host}/ffmpeg/stream/{request.ChannelNumber}?index={index}{mode} {request.Scheme}://{request.Host}/ffmpeg/stream/{request.ChannelNumber}?index={index}{mode}
"; ".AsTask();
},
() =>
{
// TODO: playlist for error stream
Log.Logger.Error("Unable to locate playout item for m3u8");
return BaseError.New($"Unable to locate playout item for channel {channel.Number}");
});
} }
private async Task<Validation<BaseError, Channel>> ChannelMustExist(GetHlsPlaylistByChannelNumber request) => private Task<Validation<BaseError, Parameters>> Validate(
(await _channelRepository.GetByNumber(request.ChannelNumber)) TvContext dbContext,
.ToValidation<BaseError>($"Channel number {request.ChannelNumber} does not exist."); GetHlsPlaylistByChannelNumber request,
DateTimeOffset now) =>
ChannelMustExist(dbContext, request)
.BindT(channel => PlayoutItemMustExist(dbContext, channel, now));
private static Task<Validation<BaseError, Channel>> ChannelMustExist(
TvContext dbContext,
GetHlsPlaylistByChannelNumber request) =>
dbContext.Channels
.SelectOneAsync(c => c.Number, c => c.Number == request.ChannelNumber)
.Map(o => o.ToValidation<BaseError>($"Channel number {request.ChannelNumber} does not exist."));
private static Task<Validation<BaseError, Parameters>> PlayoutItemMustExist(
TvContext dbContext,
Channel channel,
DateTimeOffset now) =>
dbContext.PlayoutItems
.ForChannelAndTime(channel.Id, now)
.MapT(playoutItem => new Parameters(channel, playoutItem))
.Map(o => o.ToValidation<BaseError>($"Unable to locate playout item for channel {channel.Number}"));
private long GetIndexForChannel(Channel channel, PlayoutItem playoutItem) private long GetIndexForChannel(Channel channel, PlayoutItem playoutItem)
{ {
@ -101,5 +111,7 @@ namespace ErsatzTV.Application.Streaming.Queries
private record ChannelIndexKey(int ChannelId); private record ChannelIndexKey(int ChannelId);
private record ChannelIndexRecord(long StartTicks, long Index); private record ChannelIndexRecord(long StartTicks, long Index);
private record Parameters(Channel Channel, PlayoutItem PlayoutItem);
} }
} }

53
ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs

@ -1,5 +1,6 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Core; using ErsatzTV.Core;
@ -10,39 +11,35 @@ using ErsatzTV.Core.Interfaces.Emby;
using ErsatzTV.Core.Interfaces.Jellyfin; using ErsatzTV.Core.Interfaces.Jellyfin;
using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Plex; using ErsatzTV.Core.Interfaces.Plex;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Runtime; using ErsatzTV.Core.Interfaces.Runtime;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt; using LanguageExt;
using Microsoft.EntityFrameworkCore;
using static LanguageExt.Prelude; using static LanguageExt.Prelude;
namespace ErsatzTV.Application.Streaming.Queries namespace ErsatzTV.Application.Streaming.Queries
{ {
public class public class GetPlayoutItemProcessByChannelNumberHandler :
GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<GetPlayoutItemProcessByChannelNumber> FFmpegProcessHandler<GetPlayoutItemProcessByChannelNumber>
{ {
private readonly IConfigElementRepository _configElementRepository;
private readonly IEmbyPathReplacementService _embyPathReplacementService; private readonly IEmbyPathReplacementService _embyPathReplacementService;
private readonly FFmpegProcessService _ffmpegProcessService; private readonly FFmpegProcessService _ffmpegProcessService;
private readonly IJellyfinPathReplacementService _jellyfinPathReplacementService; private readonly IJellyfinPathReplacementService _jellyfinPathReplacementService;
private readonly ILocalFileSystem _localFileSystem; private readonly ILocalFileSystem _localFileSystem;
private readonly IPlayoutRepository _playoutRepository;
private readonly IPlexPathReplacementService _plexPathReplacementService; private readonly IPlexPathReplacementService _plexPathReplacementService;
private readonly IRuntimeInfo _runtimeInfo; private readonly IRuntimeInfo _runtimeInfo;
public GetPlayoutItemProcessByChannelNumberHandler( public GetPlayoutItemProcessByChannelNumberHandler(
IChannelRepository channelRepository, IDbContextFactory<TvContext> dbContextFactory,
IConfigElementRepository configElementRepository,
IPlayoutRepository playoutRepository,
FFmpegProcessService ffmpegProcessService, FFmpegProcessService ffmpegProcessService,
ILocalFileSystem localFileSystem, ILocalFileSystem localFileSystem,
IPlexPathReplacementService plexPathReplacementService, IPlexPathReplacementService plexPathReplacementService,
IJellyfinPathReplacementService jellyfinPathReplacementService, IJellyfinPathReplacementService jellyfinPathReplacementService,
IEmbyPathReplacementService embyPathReplacementService, IEmbyPathReplacementService embyPathReplacementService,
IRuntimeInfo runtimeInfo) IRuntimeInfo runtimeInfo)
: base(channelRepository, configElementRepository) : base(dbContextFactory)
{ {
_configElementRepository = configElementRepository;
_playoutRepository = playoutRepository;
_ffmpegProcessService = ffmpegProcessService; _ffmpegProcessService = ffmpegProcessService;
_localFileSystem = localFileSystem; _localFileSystem = localFileSystem;
_plexPathReplacementService = plexPathReplacementService; _plexPathReplacementService = plexPathReplacementService;
@ -52,13 +49,32 @@ namespace ErsatzTV.Application.Streaming.Queries
} }
protected override async Task<Either<BaseError, Process>> GetProcess( protected override async Task<Either<BaseError, Process>> GetProcess(
TvContext dbContext,
GetPlayoutItemProcessByChannelNumber _, GetPlayoutItemProcessByChannelNumber _,
Channel channel, Channel channel,
string ffmpegPath) string ffmpegPath)
{ {
DateTimeOffset now = DateTimeOffset.Now; DateTimeOffset now = DateTimeOffset.Now;
Either<BaseError, PlayoutItemWithPath> maybePlayoutItem = await _playoutRepository Either<BaseError, PlayoutItemWithPath> maybePlayoutItem = await dbContext.PlayoutItems
.GetPlayoutItem(channel.Id, now) .Include(i => i.MediaItem)
.ThenInclude(mi => (mi as Episode).MediaVersions)
.ThenInclude(mv => mv.MediaFiles)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as Episode).MediaVersions)
.ThenInclude(mv => mv.Streams)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as Movie).MediaVersions)
.ThenInclude(mv => mv.MediaFiles)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as Movie).MediaVersions)
.ThenInclude(mv => mv.Streams)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as MusicVideo).MediaVersions)
.ThenInclude(mv => mv.MediaFiles)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as MusicVideo).MediaVersions)
.ThenInclude(mv => mv.Streams)
.ForChannelAndTime(channel.Id, now)
.Map(o => o.ToEither<BaseError>(new UnableToLocatePlayoutItem())) .Map(o => o.ToEither<BaseError>(new UnableToLocatePlayoutItem()))
.BindT(ValidatePlayoutItemPath); .BindT(ValidatePlayoutItemPath);
@ -73,7 +89,7 @@ namespace ErsatzTV.Application.Streaming.Queries
_ => throw new ArgumentOutOfRangeException(nameof(playoutItemWithPath)) _ => throw new ArgumentOutOfRangeException(nameof(playoutItemWithPath))
}; };
bool saveReports = !_runtimeInfo.IsOSPlatform(OSPlatform.Windows) && await _configElementRepository bool saveReports = !_runtimeInfo.IsOSPlatform(OSPlatform.Windows) && await dbContext.ConfigElements
.GetValue<bool>(ConfigElementKey.FFmpegSaveReports) .GetValue<bool>(ConfigElementKey.FFmpegSaveReports)
.Map(result => result.IfNone(false)); .Map(result => result.IfNone(false));
@ -95,8 +111,13 @@ namespace ErsatzTV.Application.Streaming.Queries
Option<TimeSpan> maybeDuration = await Optional(channel.FFmpegProfile.Transcode) Option<TimeSpan> maybeDuration = await Optional(channel.FFmpegProfile.Transcode)
.Filter(transcode => transcode) .Filter(transcode => transcode)
.Match( .Match(
_ => _playoutRepository.GetNextItemStart(channel.Id, now) _ => dbContext.PlayoutItems
.MapT(nextStart => nextStart - now), .Filter(pi => pi.Playout.ChannelId == channel.Id)
.Filter(pi => pi.Start > now.UtcDateTime)
.OrderBy(pi => pi.Start)
.FirstOrDefaultAsync()
.Map(Optional)
.MapT(pi => pi.StartOffset - now),
() => Option<TimeSpan>.None.AsTask()); () => Option<TimeSpan>.None.AsTask());
switch (error) switch (error)

19
ErsatzTV.Application/Television/Mapper.cs

@ -57,19 +57,6 @@ namespace ErsatzTV.Application.Television
season.Show.ShowMetadata.HeadOrNone().Map(m => GetFanArt(m, maybeJellyfin, maybeEmby)) season.Show.ShowMetadata.HeadOrNone().Map(m => GetFanArt(m, maybeJellyfin, maybeEmby))
.IfNone(string.Empty)); .IfNone(string.Empty));
internal static TelevisionEpisodeViewModel ProjectToViewModel(Episode episode)
{
Option<EpisodeMetadata> maybeMetadata = episode.EpisodeMetadata.HeadOrNone();
return new TelevisionEpisodeViewModel(
episode.Season.ShowId,
episode.SeasonId,
maybeMetadata.Map(em => em.EpisodeNumber).IfNone(0),
maybeMetadata.Map(m => m.Title ?? string.Empty).IfNone(string.Empty),
maybeMetadata.Map(m => m.Plot ?? string.Empty).IfNone(string.Empty),
maybeMetadata.Map(m => GetThumbnail(m, None, None)).IfNone(string.Empty));
}
private static string GetPoster( private static string GetPoster(
Metadata metadata, Metadata metadata,
Option<JellyfinMediaSource> maybeJellyfin, Option<JellyfinMediaSource> maybeJellyfin,
@ -82,12 +69,6 @@ namespace ErsatzTV.Application.Television
Option<EmbyMediaSource> maybeEmby) => Option<EmbyMediaSource> maybeEmby) =>
GetArtwork(metadata, ArtworkKind.FanArt, maybeJellyfin, maybeEmby); GetArtwork(metadata, ArtworkKind.FanArt, maybeJellyfin, maybeEmby);
private static string GetThumbnail(
Metadata metadata,
Option<JellyfinMediaSource> maybeJellyfin,
Option<EmbyMediaSource> maybeEmby) =>
GetArtwork(metadata, ArtworkKind.Thumbnail, maybeJellyfin, maybeEmby);
private static string GetArtwork( private static string GetArtwork(
Metadata metadata, Metadata metadata,
ArtworkKind artworkKind, ArtworkKind artworkKind,

7
ErsatzTV.Application/Television/Queries/GetTelevisionEpisodeById.cs

@ -1,7 +0,0 @@
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Television.Queries
{
public record GetTelevisionEpisodeById(int EpisodeId) : IRequest<Option<TelevisionEpisodeViewModel>>;
}

24
ErsatzTV.Application/Television/Queries/GetTelevisionEpisodeByIdHandler.cs

@ -1,24 +0,0 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.Television.Mapper;
namespace ErsatzTV.Application.Television.Queries
{
public class
GetTelevisionEpisodeByIdHandler : IRequestHandler<GetTelevisionEpisodeById, Option<TelevisionEpisodeViewModel>>
{
private readonly ITelevisionRepository _televisionRepository;
public GetTelevisionEpisodeByIdHandler(ITelevisionRepository televisionRepository) =>
_televisionRepository = televisionRepository;
public Task<Option<TelevisionEpisodeViewModel>> Handle(
GetTelevisionEpisodeById request,
CancellationToken cancellationToken) =>
_televisionRepository.GetEpisode(request.EpisodeId)
.MapT(ProjectToViewModel);
}
}

10
ErsatzTV.Application/Television/TelevisionEpisodeViewModel.cs

@ -1,10 +0,0 @@
namespace ErsatzTV.Application.Television
{
public record TelevisionEpisodeViewModel(
int ShowId,
int SeasonId,
int Episode,
string Title,
string Plot,
string Poster);
}

18
ErsatzTV.Core.Tests/Fakes/FakeMediaCollectionRepository.cs

@ -5,7 +5,6 @@ using System.Threading.Tasks;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt; using LanguageExt;
using static LanguageExt.Prelude;
namespace ErsatzTV.Core.Tests.Fakes namespace ErsatzTV.Core.Tests.Fakes
{ {
@ -15,25 +14,10 @@ namespace ErsatzTV.Core.Tests.Fakes
public FakeMediaCollectionRepository(Map<int, List<MediaItem>> data) => _data = data; public FakeMediaCollectionRepository(Map<int, List<MediaItem>> data) => _data = data;
public Task<Collection> Add(Collection collection) => throw new NotSupportedException();
public Task<bool> AddMediaItem(int collectionId, int mediaItemId) => throw new NotSupportedException();
public Task<bool> AddMediaItems(int collectionId, List<int> mediaItemIds) => throw new NotSupportedException();
public Task<Option<Collection>> Get(int id) => throw new NotSupportedException();
public Task<Option<Collection>> GetCollectionWithItems(int id) => throw new NotSupportedException();
public Task<Option<Collection>> GetCollectionWithItemsUntracked(int id) => throw new NotSupportedException();
public Task<Option<Collection>> GetCollectionWithCollectionItemsUntracked(int id) => public Task<Option<Collection>> GetCollectionWithCollectionItemsUntracked(int id) =>
throw new NotSupportedException(); throw new NotSupportedException();
public Task<List<Collection>> GetAll() => throw new NotSupportedException(); public Task<List<MediaItem>> GetItems(int id) => _data[id].ToList().AsTask();
public Task<int> CountAllCollections() => throw new NotSupportedException();
public Task<List<Collection>> GetPagedCollections(int pageNumber, int pageSize) =>
throw new NotSupportedException();
public Task<Option<List<MediaItem>>> GetItems(int id) => Some(_data[id].ToList()).AsTask();
Task<bool> IMediaCollectionRepository.Update(Collection collection) => throw new NotSupportedException();
public Task Delete(int collectionId) => throw new NotSupportedException();
public Task<List<int>> PlayoutIdsUsingCollection(int collectionId) => throw new NotSupportedException(); public Task<List<int>> PlayoutIdsUsingCollection(int collectionId) => throw new NotSupportedException();
public Task<bool> IsCustomPlaybackOrder(int collectionId) => false.AsTask(); public Task<bool> IsCustomPlaybackOrder(int collectionId) => false.AsTask();
} }

2
ErsatzTV.Core/FFmpeg/FFmpegLocator.cs

@ -43,7 +43,7 @@ namespace ErsatzTV.Core.FFmpeg
return await maybePath.MatchAsync( return await maybePath.MatchAsync(
async path => async path =>
{ {
await _configElementRepository.Add(new ConfigElement { Key = key.Key, Value = path }); await _configElementRepository.Upsert(key, path);
return Some(path); return Some(path);
}, },
() => None); () => None);

1
ErsatzTV.Core/Interfaces/Metadata/ILocalFileSystem.cs

@ -14,7 +14,6 @@ namespace ErsatzTV.Core.Interfaces.Metadata
IEnumerable<string> ListSubdirectories(string folder); IEnumerable<string> ListSubdirectories(string folder);
IEnumerable<string> ListFiles(string folder); IEnumerable<string> ListFiles(string folder);
bool FileExists(string path); bool FileExists(string path);
Task<byte[]> ReadAllBytes(string path);
Task<Either<BaseError, Unit>> CopyFile(string source, string destination); Task<Either<BaseError, Unit>> CopyFile(string source, string destination);
} }
} }

2
ErsatzTV.Core/Interfaces/Repositories/IChannelRepository.cs

@ -7,14 +7,12 @@ namespace ErsatzTV.Core.Interfaces.Repositories
{ {
public interface IChannelRepository public interface IChannelRepository
{ {
Task<Channel> Add(Channel channel);
Task<Option<Channel>> Get(int id); Task<Option<Channel>> Get(int id);
Task<Option<Channel>> GetByNumber(string number); Task<Option<Channel>> GetByNumber(string number);
Task<List<Channel>> GetAll(); Task<List<Channel>> GetAll();
Task<List<Channel>> GetAllForGuide(); Task<List<Channel>> GetAllForGuide();
Task<bool> Update(Channel channel); Task<bool> Update(Channel channel);
Task Delete(int channelId); Task Delete(int channelId);
Task<int> CountPlayouts(int channelId);
Task<Unit> RemoveWatermark(Channel channel); Task<Unit> RemoveWatermark(Channel channel);
} }
} }

3
ErsatzTV.Core/Interfaces/Repositories/IConfigElementRepository.cs

@ -6,10 +6,9 @@ namespace ErsatzTV.Core.Interfaces.Repositories
{ {
public interface IConfigElementRepository public interface IConfigElementRepository
{ {
Task<ConfigElement> Add(ConfigElement configElement); Task<Unit> Upsert<T>(ConfigElementKey configElementKey, T value);
Task<Option<ConfigElement>> Get(ConfigElementKey key); Task<Option<ConfigElement>> Get(ConfigElementKey key);
Task<Option<T>> GetValue<T>(ConfigElementKey key); Task<Option<T>> GetValue<T>(ConfigElementKey key);
Task Update(ConfigElement configElement);
Task Delete(ConfigElement configElement); Task Delete(ConfigElement configElement);
} }
} }

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save