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. @@ -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/).
## [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
### Added

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

@ -19,6 +19,11 @@ namespace ErsatzTV.Application.Artists.Queries @@ -19,6 +19,11 @@ namespace ErsatzTV.Application.Artists.Queries
public Task<List<NamedMediaItemViewModel>> Handle(
GetAllArtists request,
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 @@ -20,5 +20,5 @@ namespace ErsatzTV.Application.Channels.Commands
int WatermarkHorizontalMargin,
int WatermarkVerticalMargin,
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; @@ -7,39 +7,40 @@ using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.Channels.Mapper;
using Microsoft.EntityFrameworkCore;
using static LanguageExt.Prelude;
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 IFFmpegProfileRepository _ffmpegProfileRepository;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public CreateChannelHandler(
IChannelRepository channelRepository,
IFFmpegProfileRepository ffmpegProfileRepository)
{
_channelRepository = channelRepository;
_ffmpegProfileRepository = ffmpegProfileRepository;
}
public CreateChannelHandler(IDbContextFactory<TvContext> dbContextFactory) => _dbContextFactory = dbContextFactory;
public Task<Either<BaseError, ChannelViewModel>> Handle(
public async Task<Either<BaseError, CreateChannelResult>> Handle(
CreateChannel request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(PersistChannel)
.Bind(v => v.ToEitherAsync());
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Validation<BaseError, Channel> validation = await Validate(dbContext, request);
return await validation.Apply(c => PersistChannel(dbContext, c));
}
private Task<ChannelViewModel> PersistChannel(Channel c) =>
_channelRepository.Add(c).Map(ProjectToViewModel);
private static async Task<CreateChannelResult> PersistChannel(TvContext dbContext, Channel channel)
{
await dbContext.Channels.AddAsync(channel);
await dbContext.SaveChangesAsync();
return new CreateChannelResult(channel.Id);
}
private async Task<Validation<BaseError, Channel>> Validate(CreateChannel request) =>
(ValidateName(request), await ValidateNumber(request), await FFmpegProfileMustExist(request),
private async Task<Validation<BaseError, Channel>> Validate(TvContext dbContext, CreateChannel request) =>
(ValidateName(request), await ValidateNumber(dbContext, request),
await FFmpegProfileMustExist(dbContext, request),
ValidatePreferredLanguage(request))
.Apply(
(name, number, ffmpegProfileId, preferredLanguageCode) =>
@ -85,20 +86,21 @@ namespace ErsatzTV.Application.Channels.Commands @@ -85,20 +86,21 @@ namespace ErsatzTV.Application.Channels.Commands
return channel;
});
private Validation<BaseError, string> ValidateName(CreateChannel createChannel) =>
private static Validation<BaseError, string> ValidateName(CreateChannel createChannel) =>
createChannel.NotEmpty(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)
.Filter(
lc => string.IsNullOrWhiteSpace(lc) || CultureInfo.GetCultures(CultureTypes.NeutralCultures).Any(
ci => string.Equals(ci.ThreeLetterISOLanguageName, lc, StringComparison.OrdinalIgnoreCase)))
.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>>(
_ => BaseError.New("Channel number must be unique"),
() =>
@ -112,9 +114,14 @@ namespace ErsatzTV.Application.Channels.Commands @@ -112,9 +114,14 @@ namespace ErsatzTV.Application.Channels.Commands
});
}
private async Task<Validation<BaseError, int>> FFmpegProfileMustExist(CreateChannel createChannel) =>
(await _ffmpegProfileRepository.Get(createChannel.FFmpegProfileId))
.ToValidation<BaseError>($"FFmpegProfile {createChannel.FFmpegProfileId} does not exist.")
.Map(c => c.Id);
private static Task<Validation<BaseError, int>> FFmpegProfileMustExist(
TvContext dbContext,
CreateChannel createChannel) =>
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 @@ @@ -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 @@ @@ -1,6 +1,5 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
@ -15,19 +14,7 @@ namespace ErsatzTV.Application.Configuration.Commands @@ -15,19 +14,7 @@ namespace ErsatzTV.Application.Configuration.Commands
public async Task<Unit> Handle(SaveConfigElementByKey request, CancellationToken cancellationToken)
{
Option<ConfigElement> maybeElement = await _configElementRepository.Get(request.Key);
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);
});
await _configElementRepository.Upsert(request.Key, request.Value);
return Unit.Default;
}
}

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

@ -8,9 +8,8 @@ using static LanguageExt.Prelude; @@ -8,9 +8,8 @@ using static LanguageExt.Prelude;
namespace ErsatzTV.Application.Configuration.Commands
{
public class
UpdateLibraryRefreshIntervalHandler : MediatR.IRequestHandler<UpdateLibraryRefreshInterval,
Either<BaseError, Unit>>
public class UpdateLibraryRefreshIntervalHandler :
MediatR.IRequestHandler<UpdateLibraryRefreshInterval, Either<BaseError, Unit>>
{
private readonly IConfigElementRepository _configElementRepository;
@ -21,27 +20,14 @@ namespace ErsatzTV.Application.Configuration.Commands @@ -21,27 +20,14 @@ namespace ErsatzTV.Application.Configuration.Commands
UpdateLibraryRefreshInterval request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(_ => Upsert(ConfigElementKey.LibraryRefreshInterval, request.LibraryRefreshInterval.ToString()))
.MapT(_ => _configElementRepository.Upsert(ConfigElementKey.LibraryRefreshInterval, request.LibraryRefreshInterval))
.Bind(v => v.ToEitherAsync());
private Task<Validation<BaseError, Unit>> Validate(UpdateLibraryRefreshInterval request) =>
private static Task<Validation<BaseError, Unit>> Validate(UpdateLibraryRefreshInterval request) =>
Optional(request.LibraryRefreshInterval)
.Filter(lri => lri > 0)
.Map(_ => Unit.Default)
.ToValidation<BaseError>("Tuner count must be greater than zero")
.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 @@ @@ -0,0 +1,4 @@
namespace ErsatzTV.Application
{
public record EntityIdResult(int Id);
}

1
ErsatzTV.Application/ErsatzTV.Application.csproj

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

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

@ -21,5 +21,5 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands @@ -21,5 +21,5 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
bool NormalizeLoudness,
int AudioChannels,
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 @@ @@ -2,39 +2,42 @@
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.FFmpegProfiles.Mapper;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.FFmpegProfiles.Commands
{
public class
CreateFFmpegProfileHandler : IRequestHandler<CreateFFmpegProfile, Either<BaseError, FFmpegProfileViewModel>>
public class CreateFFmpegProfileHandler :
IRequestHandler<CreateFFmpegProfile, Either<BaseError, CreateFFmpegProfileResult>>
{
private readonly IFFmpegProfileRepository _ffmpegProfileRepository;
private readonly IResolutionRepository _resolutionRepository;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public CreateFFmpegProfileHandler(
IFFmpegProfileRepository ffmpegProfileRepository,
IResolutionRepository resolutionRepository)
{
_ffmpegProfileRepository = ffmpegProfileRepository;
_resolutionRepository = resolutionRepository;
}
public CreateFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public Task<Either<BaseError, FFmpegProfileViewModel>> Handle(
public async Task<Either<BaseError, CreateFFmpegProfileResult>> Handle(
CreateFFmpegProfile request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(PersistFFmpegProfile)
.Bind(v => v.ToEitherAsync());
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Validation<BaseError, FFmpegProfile> validation = await Validate(dbContext, request);
return await validation.Apply(profile => PersistFFmpegProfile(dbContext, profile));
}
private Task<FFmpegProfileViewModel> PersistFFmpegProfile(FFmpegProfile ffmpegProfile) =>
_ffmpegProfileRepository.Add(ffmpegProfile).Map(ProjectToViewModel);
private static async Task<CreateFFmpegProfileResult> PersistFFmpegProfile(
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) =>
(ValidateName(request), ValidateThreadCount(request), await ResolutionMustExist(request))
private async Task<Validation<BaseError, FFmpegProfile>> Validate(TvContext dbContext, CreateFFmpegProfile request) =>
(ValidateName(request), ValidateThreadCount(request), await ResolutionMustExist(dbContext, request))
.Apply(
(name, threadCount, resolutionId) => new FFmpegProfile
{
@ -56,16 +59,19 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands @@ -56,16 +59,19 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
NormalizeAudio = request.NormalizeAudio
});
private Validation<BaseError, string> ValidateName(CreateFFmpegProfile createFFmpegProfile) =>
private static Validation<BaseError, string> ValidateName(CreateFFmpegProfile createFFmpegProfile) =>
createFFmpegProfile.NotEmpty(x => x.Name)
.Bind(_ => createFFmpegProfile.NotLongerThan(50)(x => x.Name));
private Validation<BaseError, int> ValidateThreadCount(CreateFFmpegProfile createFFmpegProfile) =>
private static Validation<BaseError, int> ValidateThreadCount(CreateFFmpegProfile createFFmpegProfile) =>
createFFmpegProfile.AtLeast(0)(p => p.ThreadCount);
private async Task<Validation<BaseError, int>> ResolutionMustExist(CreateFFmpegProfile createFFmpegProfile) =>
(await _resolutionRepository.Get(createFFmpegProfile.ResolutionId))
.ToValidation<BaseError>($"[Resolution] {createFFmpegProfile.ResolutionId} does not exist.")
.Map(c => c.Id);
private static Task<Validation<BaseError, int>> ResolutionMustExist(
TvContext dbContext,
CreateFFmpegProfile createFFmpegProfile) =>
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 @@ @@ -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 @@ @@ -1,9 +1,7 @@
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core;
using LanguageExt;
using MediatR;
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 @@ @@ -1,32 +1,43 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Domain;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt;
using MediatR;
using Microsoft.EntityFrameworkCore;
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) =>
_ffmpegProfileRepository = ffmpegProfileRepository;
public DeleteFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public async Task<Either<BaseError, Task>> Handle(
public async Task<Either<BaseError, LanguageExt.Unit>> Handle(
DeleteFFmpegProfile request,
CancellationToken cancellationToken) =>
(await FFmpegProfileMustExist(request))
.Map(DoDeletion)
.ToEither<Task>();
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
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(
DeleteFFmpegProfile deleteFFmpegProfile) =>
(await _ffmpegProfileRepository.Get(deleteFFmpegProfile.FFmpegProfileId))
.ToValidation<BaseError>($"FFmpegProfile {deleteFFmpegProfile.FFmpegProfileId} does not exist.")
.Map(c => c.Id);
private static Task<Validation<BaseError, FFmpegProfile>> FFmpegProfileMustExist(
TvContext dbContext,
DeleteFFmpegProfile request) =>
dbContext.FFmpegProfiles
.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 @@ @@ -2,9 +2,11 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt;
using MediatR;
using Microsoft.EntityFrameworkCore;
using static LanguageExt.Prelude;
using static ErsatzTV.Application.FFmpegProfiles.Mapper;
@ -12,24 +14,21 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands @@ -12,24 +14,21 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
{
public class NewFFmpegProfileHandler : IRequestHandler<NewFFmpegProfile, FFmpegProfileViewModel>
{
private readonly IConfigElementRepository _configElementRepository;
private readonly IResolutionRepository _resolutionRepository;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public NewFFmpegProfileHandler(
IResolutionRepository resolutionRepository,
IConfigElementRepository configElementRepository)
{
_resolutionRepository = resolutionRepository;
_configElementRepository = configElementRepository;
}
public NewFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
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)
.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);
Resolution defaultResolution = maybeDefaultResolution.Match(identity, () => allResolutions.Head());

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

@ -22,5 +22,5 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands @@ -22,5 +22,5 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
bool NormalizeLoudness,
int AudioChannels,
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 @@ @@ -2,35 +2,35 @@
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.FFmpegProfiles.Mapper;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.FFmpegProfiles.Commands
{
public class
UpdateFFmpegProfileHandler : IRequestHandler<UpdateFFmpegProfile, Either<BaseError, FFmpegProfileViewModel>>
UpdateFFmpegProfileHandler : IRequestHandler<UpdateFFmpegProfile, Either<BaseError, UpdateFFmpegProfileResult>>
{
private readonly IFFmpegProfileRepository _ffmpegProfileRepository;
private readonly IResolutionRepository _resolutionRepository;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public UpdateFFmpegProfileHandler(
IFFmpegProfileRepository ffmpegProfileRepository,
IResolutionRepository resolutionRepository)
{
_ffmpegProfileRepository = ffmpegProfileRepository;
_resolutionRepository = resolutionRepository;
}
public UpdateFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public Task<Either<BaseError, FFmpegProfileViewModel>> Handle(
public async Task<Either<BaseError, UpdateFFmpegProfileResult>> Handle(
UpdateFFmpegProfile request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(c => ApplyUpdateRequest(c, request))
.Bind(v => v.ToEitherAsync());
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
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.ThreadCount = update.ThreadCount;
@ -48,30 +48,37 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands @@ -48,30 +48,37 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
p.AudioChannels = update.AudioChannels;
p.AudioSampleRate = update.AudioSampleRate;
p.NormalizeAudio = update.NormalizeAudio;
await _ffmpegProfileRepository.Update(p);
return ProjectToViewModel(p);
await dbContext.SaveChangesAsync();
return new UpdateFFmpegProfileResult(p.Id);
}
private async Task<Validation<BaseError, FFmpegProfile>> Validate(UpdateFFmpegProfile request) =>
(await FFmpegProfileMustExist(request), ValidateName(request), ValidateThreadCount(request),
await ResolutionMustExist(request))
private static async Task<Validation<BaseError, FFmpegProfile>> Validate(
TvContext dbContext,
UpdateFFmpegProfile request) =>
(await FFmpegProfileMustExist(dbContext, request), ValidateName(request), ValidateThreadCount(request),
await ResolutionMustExist(dbContext, request))
.Apply((ffmpegProfileToUpdate, _, _, _) => ffmpegProfileToUpdate);
private async Task<Validation<BaseError, FFmpegProfile>> FFmpegProfileMustExist(
private static Task<Validation<BaseError, FFmpegProfile>> FFmpegProfileMustExist(
TvContext dbContext,
UpdateFFmpegProfile updateFFmpegProfile) =>
(await _ffmpegProfileRepository.Get(updateFFmpegProfile.FFmpegProfileId))
.ToValidation<BaseError>("FFmpegProfile does not exist.");
dbContext.FFmpegProfiles
.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)
.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);
private async Task<Validation<BaseError, int>> ResolutionMustExist(UpdateFFmpegProfile updateFFmpegProfile) =>
(await _resolutionRepository.Get(updateFFmpegProfile.ResolutionId))
.ToValidation<BaseError>($"[Resolution] {updateFFmpegProfile.ResolutionId} does not exist.")
.Map(c => c.Id);
private static Task<Validation<BaseError, int>> ResolutionMustExist(
TvContext dbContext,
UpdateFFmpegProfile updateFFmpegProfile) =>
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 @@ @@ -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 @@ -86,35 +86,25 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
private async Task<Unit> ApplyUpdate(UpdateFFmpegSettings request)
{
await Upsert(ConfigElementKey.FFmpegPath, request.Settings.FFmpegPath);
await Upsert(ConfigElementKey.FFprobePath, request.Settings.FFprobePath);
await Upsert(ConfigElementKey.FFmpegDefaultProfileId, request.Settings.DefaultFFmpegProfileId.ToString());
await Upsert(ConfigElementKey.FFmpegSaveReports, request.Settings.SaveReports.ToString());
await _configElementRepository.Upsert(ConfigElementKey.FFmpegPath, request.Settings.FFmpegPath);
await _configElementRepository.Upsert(ConfigElementKey.FFprobePath, request.Settings.FFprobePath);
await _configElementRepository.Upsert(
ConfigElementKey.FFmpegDefaultProfileId,
request.Settings.DefaultFFmpegProfileId.ToString());
await _configElementRepository.Upsert(
ConfigElementKey.FFmpegSaveReports,
request.Settings.SaveReports.ToString());
if (request.Settings.SaveReports && !Directory.Exists(FileSystemLayout.FFmpegReportsFolder))
{
Directory.CreateDirectory(FileSystemLayout.FFmpegReportsFolder);
}
await Upsert(ConfigElementKey.FFmpegPreferredLanguageCode, request.Settings.PreferredLanguageCode);
await _configElementRepository.Upsert(
ConfigElementKey.FFmpegPreferredLanguageCode,
request.Settings.PreferredLanguageCode);
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 @@ @@ -2,22 +2,30 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data;
using LanguageExt;
using MediatR;
using Microsoft.EntityFrameworkCore;
using static ErsatzTV.Application.FFmpegProfiles.Mapper;
namespace ErsatzTV.Application.FFmpegProfiles.Queries
{
public class GetAllFFmpegProfilesHandler : IRequestHandler<GetAllFFmpegProfiles, List<FFmpegProfileViewModel>>
{
private readonly IFFmpegProfileRepository _ffmpegProfileRepository;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public GetAllFFmpegProfilesHandler(IFFmpegProfileRepository ffmpegProfileRepository) =>
_ffmpegProfileRepository = ffmpegProfileRepository;
public GetAllFFmpegProfilesHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public async Task<List<FFmpegProfileViewModel>> Handle(
GetAllFFmpegProfiles request,
CancellationToken cancellationToken) =>
(await _ffmpegProfileRepository.GetAll()).Map(ProjectToViewModel).ToList();
CancellationToken cancellationToken)
{
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 @@ @@ -1,23 +1,30 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt;
using MediatR;
using Microsoft.EntityFrameworkCore;
using static ErsatzTV.Application.FFmpegProfiles.Mapper;
namespace ErsatzTV.Application.FFmpegProfiles.Queries
{
public class GetFFmpegProfileByIdHandler : IRequestHandler<GetFFmpegProfileById, Option<FFmpegProfileViewModel>>
{
private readonly IFFmpegProfileRepository _ffmpegProfileRepository;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public GetFFmpegProfileByIdHandler(IFFmpegProfileRepository ffmpegProfileRepository) =>
_ffmpegProfileRepository = ffmpegProfileRepository;
public GetFFmpegProfileByIdHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public Task<Option<FFmpegProfileViewModel>> Handle(
public async Task<Option<FFmpegProfileViewModel>> Handle(
GetFFmpegProfileById request,
CancellationToken cancellationToken) =>
_ffmpegProfileRepository.Get(request.Id)
CancellationToken cancellationToken)
{
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);
}
}
}

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

@ -19,27 +19,14 @@ namespace ErsatzTV.Application.HDHR.Commands @@ -19,27 +19,14 @@ namespace ErsatzTV.Application.HDHR.Commands
UpdateHDHRTunerCount request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(_ => Upsert(ConfigElementKey.HDHRTunerCount, request.TunerCount.ToString()))
.MapT(_ => _configElementRepository.Upsert(ConfigElementKey.HDHRTunerCount, request.TunerCount.ToString()))
.Bind(v => v.ToEitherAsync());
private Task<Validation<BaseError, Unit>> Validate(UpdateHDHRTunerCount request) =>
private static Task<Validation<BaseError, Unit>> Validate(UpdateHDHRTunerCount request) =>
Optional(request.TunerCount)
.Filter(tc => tc > 0)
.Map(_ => Unit.Default)
.ToValidation<BaseError>("Tuner count must be greater than zero")
.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; @@ -3,23 +3,26 @@ using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt;
using MediatR;
using Microsoft.EntityFrameworkCore;
using static ErsatzTV.Application.MediaCards.Mapper;
namespace ErsatzTV.Application.MediaCards.Queries
{
public class GetCollectionCardsHandler : IRequestHandler<GetCollectionCards,
Either<BaseError, CollectionCardResultsViewModel>>
public class GetCollectionCardsHandler :
IRequestHandler<GetCollectionCards, Either<BaseError, CollectionCardResultsViewModel>>
{
private readonly IMediaCollectionRepository _collectionRepository;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IMediaSourceRepository _mediaSourceRepository;
public GetCollectionCardsHandler(
IMediaCollectionRepository collectionRepository,
IDbContextFactory<TvContext> dbContextFactory,
IMediaSourceRepository mediaSourceRepository)
{
_collectionRepository = collectionRepository;
_dbContextFactory = dbContextFactory;
_mediaSourceRepository = mediaSourceRepository;
}
@ -27,14 +30,57 @@ namespace ErsatzTV.Application.MediaCards.Queries @@ -27,14 +30,57 @@ namespace ErsatzTV.Application.MediaCards.Queries
GetCollectionCards request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Option<JellyfinMediaSource> maybeJellyfin = await _mediaSourceRepository.GetAllJellyfin()
.Map(list => list.HeadOrNone());
Option<EmbyMediaSource> maybeEmby = await _mediaSourceRepository.GetAllEmby()
.Map(list => list.HeadOrNone());
return await _collectionRepository
.GetCollectionWithItemsUntracked(request.Id)
return await dbContext.Collections
.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")))
.MapT(c => ProjectToViewModel(c, maybeJellyfin, maybeEmby));
}

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

@ -5,41 +5,47 @@ using ErsatzTV.Application.Playouts.Commands; @@ -5,41 +5,47 @@ using ErsatzTV.Application.Playouts.Commands;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.MediaCollections.Commands
{
public class
AddArtistToCollectionHandler : MediatR.IRequestHandler<AddArtistToCollection, Either<BaseError, Unit>>
public class AddArtistToCollectionHandler :
MediatR.IRequestHandler<AddArtistToCollection, Either<BaseError, Unit>>
{
private readonly IArtistRepository _artistRepository;
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IMediaCollectionRepository _mediaCollectionRepository;
public AddArtistToCollectionHandler(
IDbContextFactory<TvContext> dbContextFactory,
IMediaCollectionRepository mediaCollectionRepository,
IArtistRepository artistRepository,
ChannelWriter<IBackgroundServiceRequest> channel)
{
_dbContextFactory = dbContextFactory;
_mediaCollectionRepository = mediaCollectionRepository;
_artistRepository = artistRepository;
_channel = channel;
}
public Task<Either<BaseError, Unit>> Handle(
public async Task<Either<BaseError, Unit>> Handle(
AddArtistToCollection request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(_ => ApplyAddArtistRequest(request))
.Bind(v => v.ToEitherAsync());
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
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
foreach (int playoutId in await _mediaCollectionRepository
.PlayoutIdsUsingCollection(request.CollectionId))
.PlayoutIdsUsingCollection(parameters.Collection.Id))
{
await _channel.WriteAsync(new BuildPlayout(playoutId, true));
}
@ -48,21 +54,27 @@ namespace ErsatzTV.Application.MediaCollections.Commands @@ -48,21 +54,27 @@ namespace ErsatzTV.Application.MediaCollections.Commands
return Unit.Default;
}
private async Task<Validation<BaseError, Unit>> Validate(AddArtistToCollection request) =>
(await CollectionMustExist(request), await ValidateArtist(request))
.Apply((_, _) => Unit.Default);
private static async Task<Validation<BaseError, Parameters>> Validate(
TvContext dbContext,
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) =>
_mediaCollectionRepository.GetCollectionWithItems(request.CollectionId)
.MapT(_ => Unit.Default)
.Map(v => v.ToValidation<BaseError>("Collection does not exist."));
private static Task<Validation<BaseError, Collection>> CollectionMustExist(
TvContext dbContext,
AddArtistToCollection request) =>
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) =>
LoadArtist(request)
.MapT(_ => Unit.Default)
.Map(v => v.ToValidation<BaseError>("Music video does not exist"));
private static Task<Validation<BaseError, Artist>> ValidateArtist(
TvContext dbContext,
AddArtistToCollection request) =>
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) =>
_artistRepository.GetArtist(request.ArtistId);
private record Parameters(Collection Collection, Artist Artist);
}
}

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

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

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

@ -1,43 +1,54 @@ @@ -1,43 +1,54 @@
using System.Linq;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using ErsatzTV.Application.Playouts.Commands;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt;
using Microsoft.EntityFrameworkCore;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.MediaCollections.Commands
{
public class
AddItemsToCollectionHandler : MediatR.IRequestHandler<AddItemsToCollection, Either<BaseError, Unit>>
public class AddItemsToCollectionHandler :
MediatR.IRequestHandler<AddItemsToCollection, Either<BaseError, Unit>>
{
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly IMovieRepository _movieRepository;
private readonly ITelevisionRepository _televisionRepository;
public AddItemsToCollectionHandler(
IDbContextFactory<TvContext> dbContextFactory,
IMediaCollectionRepository mediaCollectionRepository,
IMovieRepository movieRepository,
ITelevisionRepository televisionRepository,
ChannelWriter<IBackgroundServiceRequest> channel)
{
_dbContextFactory = dbContextFactory;
_mediaCollectionRepository = mediaCollectionRepository;
_movieRepository = movieRepository;
_televisionRepository = televisionRepository;
_channel = channel;
}
public Task<Either<BaseError, Unit>> Handle(
public async Task<Either<BaseError, Unit>> Handle(
AddItemsToCollection request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(_ => ApplyAddItemsRequest(request))
.Bind(v => v.ToEitherAsync());
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
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
.Append(request.ShowIds)
@ -46,7 +57,14 @@ namespace ErsatzTV.Application.MediaCollections.Commands @@ -46,7 +57,14 @@ namespace ErsatzTV.Application.MediaCollections.Commands
.Append(request.MusicVideoIds)
.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
foreach (int playoutId in await _mediaCollectionRepository
@ -59,15 +77,20 @@ namespace ErsatzTV.Application.MediaCollections.Commands @@ -59,15 +77,20 @@ namespace ErsatzTV.Application.MediaCollections.Commands
return Unit.Default;
}
private async Task<Validation<BaseError, Unit>> Validate(AddItemsToCollection request) =>
(await CollectionMustExist(request), await ValidateMovies(request), await ValidateShows(request),
private async Task<Validation<BaseError, Collection>> Validate(TvContext dbContext, AddItemsToCollection request) =>
(await CollectionMustExist(dbContext, request),
await ValidateMovies(request),
await ValidateShows(request),
await ValidateEpisodes(request))
.Apply((_, _, _, _) => Unit.Default);
.Apply((collection, _, _, _) => collection);
private Task<Validation<BaseError, Unit>> CollectionMustExist(AddItemsToCollection request) =>
_mediaCollectionRepository.GetCollectionWithItems(request.CollectionId)
.MapT(_ => Unit.Default)
.Map(v => v.ToValidation<BaseError>("Collection does not exist."));
private static Task<Validation<BaseError, Collection>> CollectionMustExist(
TvContext dbContext,
AddItemsToCollection request) =>
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) =>
_movieRepository.AllMoviesExist(request.MovieIds)
@ -88,6 +111,6 @@ namespace ErsatzTV.Application.MediaCollections.Commands @@ -88,6 +111,6 @@ namespace ErsatzTV.Application.MediaCollections.Commands
.Map(Optional)
.Filter(v => v == true)
.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; @@ -5,41 +5,47 @@ using ErsatzTV.Application.Playouts.Commands;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.MediaCollections.Commands
{
public class
AddMovieToCollectionHandler : MediatR.IRequestHandler<AddMovieToCollection, Either<BaseError, Unit>>
public class AddMovieToCollectionHandler :
MediatR.IRequestHandler<AddMovieToCollection, Either<BaseError, Unit>>
{
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly IMovieRepository _movieRepository;
public AddMovieToCollectionHandler(
IDbContextFactory<TvContext> dbContextFactory,
IMediaCollectionRepository mediaCollectionRepository,
IMovieRepository movieRepository,
ChannelWriter<IBackgroundServiceRequest> channel)
{
_dbContextFactory = dbContextFactory;
_mediaCollectionRepository = mediaCollectionRepository;
_movieRepository = movieRepository;
_channel = channel;
}
public Task<Either<BaseError, Unit>> Handle(
public async Task<Either<BaseError, Unit>> Handle(
AddMovieToCollection request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(_ => ApplyAddMoviesRequest(request))
.Bind(v => v.ToEitherAsync());
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
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
foreach (int playoutId in await _mediaCollectionRepository
.PlayoutIdsUsingCollection(request.CollectionId))
.PlayoutIdsUsingCollection(parameters.Collection.Id))
{
await _channel.WriteAsync(new BuildPlayout(playoutId, true));
}
@ -48,21 +54,27 @@ namespace ErsatzTV.Application.MediaCollections.Commands @@ -48,21 +54,27 @@ namespace ErsatzTV.Application.MediaCollections.Commands
return Unit.Default;
}
private async Task<Validation<BaseError, Unit>> Validate(AddMovieToCollection request) =>
(await CollectionMustExist(request), await ValidateMovies(request))
.Apply((_, _) => Unit.Default);
private static async Task<Validation<BaseError, Parameters>> Validate(
TvContext dbContext,
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) =>
_mediaCollectionRepository.GetCollectionWithItems(request.CollectionId)
.MapT(_ => Unit.Default)
.Map(v => v.ToValidation<BaseError>("Collection does not exist."));
private static Task<Validation<BaseError, Collection>> CollectionMustExist(
TvContext dbContext,
AddMovieToCollection request) =>
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) =>
LoadMovie(request)
.MapT(_ => Unit.Default)
.Map(v => v.ToValidation<BaseError>("Movie does not exist"));
private static Task<Validation<BaseError, Movie>> ValidateMovie(
TvContext dbContext,
AddMovieToCollection request) =>
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) =>
_movieRepository.GetMovie(request.MovieId);
private record Parameters(Collection Collection, Movie Movie);
}
}

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

@ -5,41 +5,47 @@ using ErsatzTV.Application.Playouts.Commands; @@ -5,41 +5,47 @@ using ErsatzTV.Application.Playouts.Commands;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.MediaCollections.Commands
{
public class
AddMusicVideoToCollectionHandler : MediatR.IRequestHandler<AddMusicVideoToCollection, Either<BaseError, Unit>>
public class AddMusicVideoToCollectionHandler :
MediatR.IRequestHandler<AddMusicVideoToCollection, Either<BaseError, Unit>>
{
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly IMusicVideoRepository _musicVideoRepository;
public AddMusicVideoToCollectionHandler(
IDbContextFactory<TvContext> dbContextFactory,
IMediaCollectionRepository mediaCollectionRepository,
IMusicVideoRepository musicVideoRepository,
ChannelWriter<IBackgroundServiceRequest> channel)
{
_dbContextFactory = dbContextFactory;
_mediaCollectionRepository = mediaCollectionRepository;
_musicVideoRepository = musicVideoRepository;
_channel = channel;
}
public Task<Either<BaseError, Unit>> Handle(
public async Task<Either<BaseError, Unit>> Handle(
AddMusicVideoToCollection request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(_ => ApplyAddMusicVideoRequest(request))
.Bind(v => v.ToEitherAsync());
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
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
foreach (int playoutId in await _mediaCollectionRepository
.PlayoutIdsUsingCollection(request.CollectionId))
.PlayoutIdsUsingCollection(parameters.Collection.Id))
{
await _channel.WriteAsync(new BuildPlayout(playoutId, true));
}
@ -48,21 +54,27 @@ namespace ErsatzTV.Application.MediaCollections.Commands @@ -48,21 +54,27 @@ namespace ErsatzTV.Application.MediaCollections.Commands
return Unit.Default;
}
private async Task<Validation<BaseError, Unit>> Validate(AddMusicVideoToCollection request) =>
(await CollectionMustExist(request), await ValidateMusicVideo(request))
.Apply((_, _) => Unit.Default);
private static async Task<Validation<BaseError, Parameters>> Validate(
TvContext dbContext,
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) =>
_mediaCollectionRepository.GetCollectionWithItems(request.CollectionId)
.MapT(_ => Unit.Default)
.Map(v => v.ToValidation<BaseError>("Collection does not exist."));
private static Task<Validation<BaseError, Collection>> CollectionMustExist(
TvContext dbContext,
AddMusicVideoToCollection request) =>
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) =>
LoadMusicVideo(request)
.MapT(_ => Unit.Default)
.Map(v => v.ToValidation<BaseError>("Music video does not exist"));
private static Task<Validation<BaseError, MusicVideo>> ValidateMusicVideo(
TvContext dbContext,
AddMusicVideoToCollection request) =>
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) =>
_musicVideoRepository.GetMusicVideo(request.MusicVideoId);
private record Parameters(Collection Collection, MusicVideo MusicVideo);
}
}

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

@ -5,41 +5,47 @@ using ErsatzTV.Application.Playouts.Commands; @@ -5,41 +5,47 @@ using ErsatzTV.Application.Playouts.Commands;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.MediaCollections.Commands
{
public class
AddSeasonToCollectionHandler : MediatR.IRequestHandler<AddSeasonToCollection, Either<BaseError, Unit>>
public class AddSeasonToCollectionHandler :
MediatR.IRequestHandler<AddSeasonToCollection, Either<BaseError, Unit>>
{
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly ITelevisionRepository _televisionRepository;
public AddSeasonToCollectionHandler(
IDbContextFactory<TvContext> dbContextFactory,
IMediaCollectionRepository mediaCollectionRepository,
ITelevisionRepository televisionRepository,
ChannelWriter<IBackgroundServiceRequest> channel)
{
_dbContextFactory = dbContextFactory;
_mediaCollectionRepository = mediaCollectionRepository;
_televisionRepository = televisionRepository;
_channel = channel;
}
public Task<Either<BaseError, Unit>> Handle(
public async Task<Either<BaseError, Unit>> Handle(
AddSeasonToCollection request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(_ => ApplyAddTelevisionSeasonRequest(request))
.Bind(v => v.ToEitherAsync());
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
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
foreach (int playoutId in await _mediaCollectionRepository
.PlayoutIdsUsingCollection(request.CollectionId))
.PlayoutIdsUsingCollection(parameters.Collection.Id))
{
await _channel.WriteAsync(new BuildPlayout(playoutId, true));
}
@ -48,22 +54,27 @@ namespace ErsatzTV.Application.MediaCollections.Commands @@ -48,22 +54,27 @@ namespace ErsatzTV.Application.MediaCollections.Commands
return Unit.Default;
}
private async Task<Validation<BaseError, Unit>> Validate(AddSeasonToCollection request) =>
(await CollectionMustExist(request), await ValidateSeason(request))
.Apply((_, _) => Unit.Default);
private Task<Validation<BaseError, Unit>> CollectionMustExist(AddSeasonToCollection request) =>
_mediaCollectionRepository.GetCollectionWithItems(request.CollectionId)
.MapT(_ => Unit.Default)
.Map(v => v.ToValidation<BaseError>("Collection does not exist."));
private static async Task<Validation<BaseError, Parameters>> Validate(
TvContext dbContext,
AddSeasonToCollection request) =>
(await CollectionMustExist(dbContext, request), await ValidateSeason(dbContext, request))
.Apply((collection, episode) => new Parameters(collection, episode));
private Task<Validation<BaseError, Unit>> ValidateSeason(AddSeasonToCollection request) =>
LoadTelevisionSeason(request)
.MapT(_ => Unit.Default)
.Map(v => v.ToValidation<BaseError>("Season does not exist"));
private static Task<Validation<BaseError, Collection>> CollectionMustExist(
TvContext dbContext,
AddSeasonToCollection request) =>
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) =>
_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; @@ -5,65 +5,76 @@ using ErsatzTV.Application.Playouts.Commands;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt;
using Microsoft.EntityFrameworkCore;
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 IDbContextFactory<TvContext> _dbContextFactory;
private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly ITelevisionRepository _televisionRepository;
public AddShowToCollectionHandler(
IDbContextFactory<TvContext> dbContextFactory,
IMediaCollectionRepository mediaCollectionRepository,
ITelevisionRepository televisionRepository,
ChannelWriter<IBackgroundServiceRequest> channel)
{
_dbContextFactory = dbContextFactory;
_mediaCollectionRepository = mediaCollectionRepository;
_televisionRepository = televisionRepository;
_channel = channel;
}
public Task<Either<BaseError, Unit>> Handle(
public async Task<Either<BaseError, Unit>> Handle(
AddShowToCollection request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(_ => ApplyAddTelevisionShowRequest(request))
.Bind(v => v.ToEitherAsync());
private async Task<Unit> ApplyAddTelevisionShowRequest(AddShowToCollection request)
CancellationToken cancellationToken)
{
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
foreach (int playoutId in await _mediaCollectionRepository
.PlayoutIdsUsingCollection(request.CollectionId))
.PlayoutIdsUsingCollection(parameters.Collection.Id))
{
await _channel.WriteAsync(new BuildPlayout(playoutId, true));
}
}
return result;
return Unit.Default;
}
private async Task<Validation<BaseError, Unit>> Validate(AddShowToCollection request) =>
(await CollectionMustExist(request), await ValidateShow(request))
.Apply((_, _) => Unit.Default);
private static async Task<Validation<BaseError, Parameters>> Validate(
TvContext dbContext,
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) =>
_mediaCollectionRepository.GetCollectionWithItems(request.CollectionId)
.MapT(_ => Unit.Default)
.Map(v => v.ToValidation<BaseError>("Collection does not exist."));
private static Task<Validation<BaseError, Collection>> CollectionMustExist(
TvContext dbContext,
AddShowToCollection request) =>
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) =>
LoadTelevisionShow(request)
.MapT(_ => Unit.Default)
.Map(v => v.ToValidation<BaseError>("Show does not exist"));
private static Task<Validation<BaseError, Show>> ValidateShow(
TvContext dbContext,
AddShowToCollection request) =>
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) =>
_televisionRepository.GetShow(request.ShowId);
private record Parameters(Collection Collection, Show Show);
}
}

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

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

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

@ -1,9 +1,8 @@ @@ -1,9 +1,8 @@
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core;
using LanguageExt;
using MediatR;
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 @@ @@ -1,33 +1,42 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Domain;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt;
using MediatR;
using Microsoft.EntityFrameworkCore;
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) =>
_mediaCollectionRepository = mediaCollectionRepository;
public DeleteCollectionHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public async Task<Either<BaseError, Task>> Handle(
public async Task<Either<BaseError, Unit>> Handle(
DeleteCollection request,
CancellationToken cancellationToken) =>
(await CollectionMustExist(request))
.Map(DoDeletion)
.ToEither<Task>();
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
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(
DeleteCollection deleteMediaCollection) =>
(await _mediaCollectionRepository.Get(deleteMediaCollection.CollectionId))
.ToValidation<BaseError>(
$"Collection {deleteMediaCollection.CollectionId} does not exist.")
.Map(c => c.Id);
private static Task<Unit> DoDeletion(TvContext dbContext, Collection collection)
{
dbContext.Collections.Remove(collection);
return dbContext.SaveChangesAsync().ToUnit();
}
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; @@ -6,32 +6,41 @@ using ErsatzTV.Application.Playouts.Commands;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.MediaCollections.Commands
{
public class
RemoveItemsFromCollectionHandler : MediatR.IRequestHandler<RemoveItemsFromCollection, Either<BaseError, Unit>>
public class RemoveItemsFromCollectionHandler :
MediatR.IRequestHandler<RemoveItemsFromCollection, Either<BaseError, Unit>>
{
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IMediaCollectionRepository _mediaCollectionRepository;
public RemoveItemsFromCollectionHandler(
IDbContextFactory<TvContext> dbContextFactory,
IMediaCollectionRepository mediaCollectionRepository,
ChannelWriter<IBackgroundServiceRequest> channel)
{
_dbContextFactory = dbContextFactory;
_mediaCollectionRepository = mediaCollectionRepository;
_channel = channel;
}
public Task<Either<BaseError, Unit>> Handle(
public async Task<Either<BaseError, Unit>> Handle(
RemoveItemsFromCollection request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(collection => ApplyRemoveItemsRequest(request, collection))
.Bind(v => v.ToEitherAsync());
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Validation<BaseError, Collection> validation = await Validate(dbContext, request);
return await validation.Apply(c => ApplyRemoveItemsRequest(dbContext, request, c));
}
private async Task<Unit> ApplyRemoveItemsRequest(
TvContext dbContext,
RemoveItemsFromCollection request,
Collection collection)
{
@ -41,7 +50,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands @@ -41,7 +50,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands
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
foreach (int playoutId in await _mediaCollectionRepository.PlayoutIdsUsingCollection(collection.Id))
@ -53,13 +62,17 @@ namespace ErsatzTV.Application.MediaCollections.Commands @@ -53,13 +62,17 @@ namespace ErsatzTV.Application.MediaCollections.Commands
return Unit.Default;
}
private Task<Validation<BaseError, Collection>> Validate(
private static Task<Validation<BaseError, Collection>> Validate(
TvContext dbContext,
RemoveItemsFromCollection request) =>
CollectionMustExist(request);
CollectionMustExist(dbContext, request);
private Task<Validation<BaseError, Collection>> CollectionMustExist(
RemoveItemsFromCollection updateCollection) =>
_mediaCollectionRepository.GetCollectionWithItems(updateCollection.MediaCollectionId)
.Map(v => v.ToValidation<BaseError>("Collection does not exist."));
private static Task<Validation<BaseError, Collection>> CollectionMustExist(
TvContext dbContext,
RemoveItemsFromCollection request) =>
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; @@ -6,47 +6,60 @@ using ErsatzTV.Application.Playouts.Commands;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.MediaCollections.Commands
{
public class
UpdateCollectionCustomOrderHandler : MediatR.IRequestHandler<UpdateCollectionCustomOrder,
Either<BaseError, Unit>>
public class UpdateCollectionCustomOrderHandler :
MediatR.IRequestHandler<UpdateCollectionCustomOrder, Either<BaseError, Unit>>
{
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IMediaCollectionRepository _mediaCollectionRepository;
public UpdateCollectionCustomOrderHandler(
IDbContextFactory<TvContext> dbContextFactory,
IMediaCollectionRepository mediaCollectionRepository,
ChannelWriter<IBackgroundServiceRequest> channel)
{
_dbContextFactory = dbContextFactory;
_mediaCollectionRepository = mediaCollectionRepository;
_channel = channel;
}
public Task<Either<BaseError, Unit>> Handle(
public async Task<Either<BaseError, Unit>> Handle(
UpdateCollectionCustomOrder request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(c => ApplyUpdateRequest(c, request))
.Bind(v => v.ToEitherAsync());
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
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)
{
Option<CollectionItem> maybeCollectionItem =
c.CollectionItems.FirstOrDefault(ci => ci.MediaItemId == updateItem.MediaItemId);
Option<CollectionItem> maybeCollectionItem = c.CollectionItems
.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
foreach (int playoutId in await _mediaCollectionRepository.PlayoutIdsUsingCollection(
request.CollectionId))
foreach (int playoutId in await _mediaCollectionRepository
.PlayoutIdsUsingCollection(request.CollectionId))
{
await _channel.WriteAsync(new BuildPlayout(playoutId, true));
}
@ -55,12 +68,17 @@ namespace ErsatzTV.Application.MediaCollections.Commands @@ -55,12 +68,17 @@ namespace ErsatzTV.Application.MediaCollections.Commands
return Unit.Default;
}
private Task<Validation<BaseError, Collection>> Validate(UpdateCollectionCustomOrder request) =>
CollectionMustExist(request);
private static Task<Validation<BaseError, Collection>> Validate(
TvContext dbContext,
UpdateCollectionCustomOrder request) =>
CollectionMustExist(dbContext, request);
private Task<Validation<BaseError, Collection>> CollectionMustExist(
private static Task<Validation<BaseError, Collection>> CollectionMustExist(
TvContext dbContext,
UpdateCollectionCustomOrder request) =>
_mediaCollectionRepository.Get(request.CollectionId)
.Map(v => v.ToValidation<BaseError>("Collection does not exist."));
dbContext.Collections
.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; @@ -5,36 +5,47 @@ using ErsatzTV.Application.Playouts.Commands;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.MediaCollections.Commands
{
public class UpdateCollectionHandler : MediatR.IRequestHandler<UpdateCollection, Either<BaseError, Unit>>
{
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IMediaCollectionRepository _mediaCollectionRepository;
public UpdateCollectionHandler(
IDbContextFactory<TvContext> dbContextFactory,
IMediaCollectionRepository mediaCollectionRepository,
ChannelWriter<IBackgroundServiceRequest> channel)
{
_dbContextFactory = dbContextFactory;
_mediaCollectionRepository = mediaCollectionRepository;
_channel = channel;
}
public Task<Either<BaseError, Unit>> Handle(
public async Task<Either<BaseError, Unit>> Handle(
UpdateCollection request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(c => ApplyUpdateRequest(c, request))
.Bind(v => v.ToEitherAsync());
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
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;
await request.UseCustomPlaybackOrder.IfSomeAsync(
useCustomPlaybackOrder => c.UseCustomPlaybackOrder = useCustomPlaybackOrder);
if (await _mediaCollectionRepository.Update(c) && request.UseCustomPlaybackOrder.IsSome)
foreach (bool useCustomPlaybackOrder in request.UseCustomPlaybackOrder)
{
c.UseCustomPlaybackOrder = useCustomPlaybackOrder;
}
if (await dbContext.SaveChangesAsync() > 0 && request.UseCustomPlaybackOrder.IsSome)
{
// rebuild all playouts that use this collection
foreach (int playoutId in await _mediaCollectionRepository.PlayoutIdsUsingCollection(
@ -47,17 +58,20 @@ namespace ErsatzTV.Application.MediaCollections.Commands @@ -47,17 +58,20 @@ namespace ErsatzTV.Application.MediaCollections.Commands
return Unit.Default;
}
private async Task<Validation<BaseError, Collection>>
Validate(UpdateCollection request) =>
(await CollectionMustExist(request), ValidateName(request))
private static async Task<Validation<BaseError, Collection>> Validate(
TvContext dbContext,
UpdateCollection request) =>
(await CollectionMustExist(dbContext, request), ValidateName(request))
.Apply((collectionToUpdate, _) => collectionToUpdate);
private Task<Validation<BaseError, Collection>> CollectionMustExist(
private static Task<Validation<BaseError, Collection>> CollectionMustExist(
TvContext dbContext,
UpdateCollection updateCollection) =>
_mediaCollectionRepository.Get(updateCollection.CollectionId)
.Map(v => v.ToValidation<BaseError>("Collection does not exist."));
dbContext.Collections
.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)
.Bind(_ => updateSimpleMediaCollection.NotLongerThan(50)(c => c.Name));
}

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

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

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

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

3
ErsatzTV.Application/MediaItems/Mapper.cs

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

9
ErsatzTV.Application/MediaItems/MediaItemSearchResultViewModel.cs

@ -1,9 +0,0 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -2,41 +2,57 @@
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt;
using static LanguageExt.Prelude;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Playouts.Commands
{
public class BuildPlayoutHandler : MediatR.IRequestHandler<BuildPlayout, Either<BaseError, Unit>>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
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;
}
public Task<Either<BaseError, Unit>> Handle(BuildPlayout request, CancellationToken cancellationToken) =>
Validate(request)
.Map(v => v.ToEither<Playout>())
.BindT(playout => ApplyUpdateRequest(request, playout));
public async Task<Either<BaseError, Unit>> Handle(BuildPlayout request, CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
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 _playoutRepository.Update(result);
return unit;
await _playoutBuilder.BuildPlayoutItems(playout, request.Rebuild);
await dbContext.SaveChangesAsync();
return Unit.Default;
}
private Task<Validation<BaseError, Playout>> Validate(BuildPlayout request) =>
PlayoutMustExist(request);
private static Task<Validation<BaseError, Playout>> Validate(TvContext dbContext, BuildPlayout request) =>
PlayoutMustExist(dbContext, request);
private async Task<Validation<BaseError, Playout>> PlayoutMustExist(BuildPlayout buildPlayout) =>
(await _playoutRepository.GetFull(buildPlayout.PlayoutId))
.ToValidation<BaseError>("Playout does not exist.");
private static Task<Validation<BaseError, Playout>> PlayoutMustExist(
TvContext dbContext,
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 @@ -8,5 +8,5 @@ namespace ErsatzTV.Application.Playouts.Commands
public record CreatePlayout(
int ChannelId,
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; @@ -4,51 +4,50 @@ using System.Threading.Channels;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.Playouts.Mapper;
using Microsoft.EntityFrameworkCore;
using static LanguageExt.Prelude;
using Channel = ErsatzTV.Core.Domain.Channel;
namespace ErsatzTV.Application.Playouts.Commands
{
public class
CreatePlayoutHandler : IRequestHandler<CreatePlayout, Either<BaseError, PlayoutViewModel>>
public class CreatePlayoutHandler : IRequestHandler<CreatePlayout, Either<BaseError, CreatePlayoutResponse>>
{
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly IChannelRepository _channelRepository;
private readonly IPlayoutRepository _playoutRepository;
private readonly IProgramScheduleRepository _programScheduleRepository;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public CreatePlayoutHandler(
IPlayoutRepository playoutRepository,
IChannelRepository channelRepository,
IProgramScheduleRepository programScheduleRepository,
ChannelWriter<IBackgroundServiceRequest> channel)
ChannelWriter<IBackgroundServiceRequest> channel,
IDbContextFactory<TvContext> dbContextFactory)
{
_playoutRepository = playoutRepository;
_channelRepository = channelRepository;
_programScheduleRepository = programScheduleRepository;
_channel = channel;
_dbContextFactory = dbContextFactory;
}
public Task<Either<BaseError, PlayoutViewModel>> Handle(
public async Task<Either<BaseError, CreatePlayoutResponse>> Handle(
CreatePlayout request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(PersistPlayout)
.Bind(v => v.ToEitherAsync());
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
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 _channel.WriteAsync(new BuildPlayout(result.Id));
return result;
await dbContext.Playouts.AddAsync(playout);
await dbContext.SaveChangesAsync();
await _channel.WriteAsync(new BuildPlayout(playout.Id));
return new CreatePlayoutResponse(playout.Id);
}
private async Task<Validation<BaseError, Playout>> Validate(CreatePlayout request) =>
(await ValidateChannel(request), await ProgramScheduleMustExist(request), ValidatePlayoutType(request))
private async Task<Validation<BaseError, Playout>> Validate(TvContext dbContext, CreatePlayout request) =>
(await ValidateChannel(dbContext, request), await ValidateProgramSchedule(dbContext, request),
ValidatePlayoutType(request))
.Apply(
(channel, programSchedule, playoutType) => new Playout
{
@ -57,31 +56,38 @@ namespace ErsatzTV.Application.Playouts.Commands @@ -57,31 +56,38 @@ namespace ErsatzTV.Application.Playouts.Commands
ProgramSchedulePlayoutType = playoutType
});
private Task<Validation<BaseError, Channel>> ValidateChannel(CreatePlayout createPlayout) =>
ChannelMustExist(createPlayout).BindT(ChannelMustNotHavePlayouts);
private async Task<Validation<BaseError, Channel>> ChannelMustExist(CreatePlayout createPlayout) =>
(await _channelRepository.Get(createPlayout.ChannelId))
.ToValidation<BaseError>("Channel does not exist.");
private static Task<Validation<BaseError, Channel>> ValidateChannel(
TvContext dbContext,
CreatePlayout createPlayout) =>
dbContext.Channels
.Include(c => c.Playouts)
.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) =>
Optional(await _channelRepository.CountPlayouts(channel.Id))
private static Validation<BaseError, Channel> ChannelMustNotHavePlayouts(Channel channel) =>
Optional(channel.Playouts.Count)
.Filter(count => count == 0)
.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) =>
(await _programScheduleRepository.GetWithPlayouts(createPlayout.ProgramScheduleId))
.ToValidation<BaseError>("ProgramSchedule does not exist.")
.Bind(ProgramScheduleMustHaveItems);
dbContext.ProgramSchedules
.Include(ps => ps.Items)
.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)
.Filter(ps => ps.Items.Any())
.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)
.Filter(playoutType => playoutType != ProgramSchedulePlayoutType.None)
.ToValidation<BaseError>("[ProgramSchedulePlayoutType] must not be None");

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

@ -0,0 +1,4 @@ @@ -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 @@ @@ -1,9 +1,9 @@
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core;
using LanguageExt;
using MediatR;
using Unit = LanguageExt.Unit;
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 @@ @@ -1,32 +1,42 @@
using System.Threading;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Domain;
using ErsatzTV.Infrastructure.Data;
using LanguageExt;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Unit = LanguageExt.Unit;
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) =>
_playoutRepository = playoutRepository;
public DeletePlayoutHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public async Task<Either<BaseError, Task>> Handle(
public async Task<Either<BaseError, Unit>> Handle(
DeletePlayout request,
CancellationToken cancellationToken) =>
(await PlayoutMustExist(request))
.Map(DoDeletion)
.ToEither<Task>();
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
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(
DeletePlayout deletePlayout) =>
(await _playoutRepository.Get(deletePlayout.PlayoutId))
.ToValidation<BaseError>($"Playout {deletePlayout.PlayoutId} does not exist.")
.Map(c => c.Id);
foreach (Playout playout in maybePlayout)
{
dbContext.Playouts.Remove(playout);
await dbContext.SaveChangesAsync(cancellationToken);
}
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 @@ @@ -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 @@ @@ -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 @@ -6,25 +6,12 @@ namespace ErsatzTV.Application.Playouts
{
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) =>
new(
GetDisplayTitle(playoutItem.MediaItem),
playoutItem.StartOffset,
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)
{
switch (mediaItem)

8
ErsatzTV.Application/Playouts/PlayoutNameViewModel.cs

@ -0,0 +1,8 @@ @@ -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; @@ -3,5 +3,5 @@ using MediatR;
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 @@ @@ -1,24 +1,28 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using ErsatzTV.Infrastructure.Data;
using MediatR;
using static ErsatzTV.Application.Playouts.Mapper;
using Microsoft.EntityFrameworkCore;
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) =>
_playoutRepository = playoutRepository;
public GetAllPlayoutsHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public Task<List<PlayoutViewModel>> Handle(
public async Task<List<PlayoutNameViewModel>> Handle(
GetAllPlayouts request,
CancellationToken cancellationToken) =>
_playoutRepository.GetAll().Map(list => list.Map(ProjectToViewModel).ToList());
CancellationToken cancellationToken)
{
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 @@ @@ -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 @@ @@ -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 @@ @@ -2,24 +2,55 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Domain;
using ErsatzTV.Infrastructure.Data;
using LanguageExt;
using MediatR;
using Microsoft.EntityFrameworkCore;
using static ErsatzTV.Application.Playouts.Mapper;
namespace ErsatzTV.Application.Playouts.Queries
{
public class GetPlayoutItemsByIdHandler : IRequestHandler<GetPlayoutItemsById, List<PlayoutItemViewModel>>
{
private readonly IPlayoutRepository _playoutRepository;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public GetPlayoutItemsByIdHandler(IPlayoutRepository playoutRepository) =>
_playoutRepository = playoutRepository;
public GetPlayoutItemsByIdHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public Task<List<PlayoutItemViewModel>> Handle(
public async Task<List<PlayoutItemViewModel>> Handle(
GetPlayoutItemsById request,
CancellationToken cancellationToken) =>
_playoutRepository.GetPlayoutItems(request.PlayoutId)
CancellationToken cancellationToken)
{
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());
}
}
}

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

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

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

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

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

@ -0,0 +1,4 @@ @@ -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 @@ @@ -1,9 +1,8 @@
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core;
using LanguageExt;
using MediatR;
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 @@ @@ -1,32 +1,43 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Domain;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Unit = LanguageExt.Unit;
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) =>
_programScheduleRepository = programScheduleRepository;
public DeleteProgramScheduleHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public async Task<Either<BaseError, Task>> Handle(
public async Task<Either<BaseError, Unit>> Handle(
DeleteProgramSchedule request,
CancellationToken cancellationToken) =>
(await ProgramScheduleMustExist(request))
.Map(DoDeletion)
.ToEither<Task>();
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
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(
DeleteProgramSchedule deleteProgramSchedule) =>
(await _programScheduleRepository.Get(deleteProgramSchedule.ProgramScheduleId))
.ToValidation<BaseError>($"ProgramSchedule {deleteProgramSchedule.ProgramScheduleId} does not exist.")
.Map(c => c.Id);
private Task<Validation<BaseError, ProgramSchedule>> ProgramScheduleMustExist(
TvContext dbContext,
DeleteProgramSchedule request) =>
dbContext.ProgramSchedules
.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 @@ @@ -2,23 +2,25 @@
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.ProgramSchedules.Commands
{
public abstract class ProgramScheduleItemCommandBase
{
private readonly IProgramScheduleRepository _programScheduleRepository;
protected ProgramScheduleItemCommandBase(IProgramScheduleRepository programScheduleRepository) =>
_programScheduleRepository = programScheduleRepository;
protected async Task<Validation<BaseError, ProgramSchedule>> ProgramScheduleMustExist(int programScheduleId) =>
(await _programScheduleRepository.GetWithPlayouts(programScheduleId))
.ToValidation<BaseError>("[ProgramScheduleId] does not exist.");
protected Validation<BaseError, ProgramSchedule> PlayoutModeMustBeValid(
protected static Task<Validation<BaseError, ProgramSchedule>> ProgramScheduleMustExist(
TvContext dbContext,
int programScheduleId) =>
dbContext.ProgramSchedules
.Include(ps => ps.Items)
.Include(ps => ps.Playouts)
.SelectOneAsync(ps => ps.Id, ps => ps.Id == programScheduleId)
.Map(o => o.ToValidation<BaseError>("[ProgramScheduleId] does not exist."));
protected static Validation<BaseError, ProgramSchedule> PlayoutModeMustBeValid(
IProgramScheduleItemRequest item,
ProgramSchedule programSchedule)
{

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

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

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

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

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

@ -0,0 +1,4 @@ @@ -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 @@ @@ -1,23 +1,33 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data;
using MediatR;
using static ErsatzTV.Application.ProgramSchedules.Mapper;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.ProgramSchedules.Queries
{
public class GetAllProgramSchedulesHandler : IRequestHandler<GetAllProgramSchedules, List<ProgramScheduleViewModel>>
{
private readonly IProgramScheduleRepository _programScheduleRepository;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public GetAllProgramSchedulesHandler(IProgramScheduleRepository programScheduleRepository) =>
_programScheduleRepository = programScheduleRepository;
public GetAllProgramSchedulesHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public async Task<List<ProgramScheduleViewModel>> Handle(
GetAllProgramSchedules request,
CancellationToken cancellationToken) =>
(await _programScheduleRepository.GetAll()).Map(ProjectToViewModel).ToList();
CancellationToken cancellationToken)
{
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 @@ @@ -1,24 +1,30 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt;
using MediatR;
using Microsoft.EntityFrameworkCore;
using static ErsatzTV.Application.ProgramSchedules.Mapper;
namespace ErsatzTV.Application.ProgramSchedules.Queries
{
public class
GetProgramScheduleByIdHandler : IRequestHandler<GetProgramScheduleById, Option<ProgramScheduleViewModel>>
public class GetProgramScheduleByIdHandler :
IRequestHandler<GetProgramScheduleById, Option<ProgramScheduleViewModel>>
{
private readonly IProgramScheduleRepository _programScheduleRepository;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public GetProgramScheduleByIdHandler(IProgramScheduleRepository programScheduleRepository) =>
_programScheduleRepository = programScheduleRepository;
public GetProgramScheduleByIdHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public Task<Option<ProgramScheduleViewModel>> Handle(
public async Task<Option<ProgramScheduleViewModel>> Handle(
GetProgramScheduleById request,
CancellationToken cancellationToken) =>
_programScheduleRepository.Get(request.Id)
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
return await dbContext.ProgramSchedules
.SelectOneAsync(ps => ps.Id, ps => ps.Id == request.Id)
.MapT(ProjectToViewModel);
}
}
}

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

@ -1,8 +1,7 @@ @@ -1,8 +1,7 @@
using System.Collections.Generic;
using LanguageExt;
using MediatR;
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 @@ @@ -1,25 +1,50 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Domain;
using ErsatzTV.Infrastructure.Data;
using LanguageExt;
using MediatR;
using Microsoft.EntityFrameworkCore;
using static ErsatzTV.Application.ProgramSchedules.Mapper;
namespace ErsatzTV.Application.ProgramSchedules.Queries
{
public class GetProgramScheduleItemsHandler : IRequestHandler<GetProgramScheduleItems,
Option<IEnumerable<ProgramScheduleItemViewModel>>>
public class GetProgramScheduleItemsHandler :
IRequestHandler<GetProgramScheduleItems, List<ProgramScheduleItemViewModel>>
{
private readonly IProgramScheduleRepository _programScheduleRepository;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public GetProgramScheduleItemsHandler(IProgramScheduleRepository programScheduleRepository) =>
_programScheduleRepository = programScheduleRepository;
public GetProgramScheduleItemsHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public Task<Option<IEnumerable<ProgramScheduleItemViewModel>>> Handle(
public async Task<List<ProgramScheduleItemViewModel>> Handle(
GetProgramScheduleItems request,
CancellationToken cancellationToken) =>
_programScheduleRepository.GetItems(request.Id)
.MapT(programScheduleItems => programScheduleItems.Map(ProjectToViewModel));
CancellationToken cancellationToken)
{
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 @@ @@ -2,21 +2,29 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data;
using LanguageExt;
using MediatR;
using Microsoft.EntityFrameworkCore;
using static ErsatzTV.Application.Resolutions.Mapper;
namespace ErsatzTV.Application.Resolutions.Queries
{
public class GetAllResolutionsHandler : IRequestHandler<GetAllResolutions, List<ResolutionViewModel>>
{
private readonly IResolutionRepository _resolutionRepository;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public GetAllResolutionsHandler(IResolutionRepository resolutionRepository) =>
_resolutionRepository = resolutionRepository;
public GetAllResolutionsHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public Task<List<ResolutionViewModel>> Handle(GetAllResolutions request, CancellationToken cancellationToken) =>
_resolutionRepository.GetAll().Map(resolutions => resolutions.Map(ProjectToViewModel).ToList());
public async Task<List<ResolutionViewModel>> Handle(
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 @@ -49,23 +49,7 @@ namespace ErsatzTV.Application.Search.Commands
List<int> itemIds = await _searchRepository.GetItemIdsToIndex();
await _searchIndex.Rebuild(_searchRepository, itemIds);
Option<ConfigElement> maybeVersion =
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);
});
await _configElementRepository.Upsert(ConfigElementKey.SearchIndexVersion, _searchIndex.Version);
_logger.LogDebug("Done migrating search index");
}

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

@ -5,9 +5,11 @@ using System.Threading; @@ -5,9 +5,11 @@ using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt;
using MediatR;
using Microsoft.EntityFrameworkCore;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.Streaming.Queries
@ -15,46 +17,55 @@ namespace ErsatzTV.Application.Streaming.Queries @@ -15,46 +17,55 @@ namespace ErsatzTV.Application.Streaming.Queries
public abstract class FFmpegProcessHandler<T> : IRequestHandler<T, Either<BaseError, Process>>
where T : FFmpegProcessRequest
{
private readonly IChannelRepository _channelRepository;
private readonly IConfigElementRepository _configElementRepository;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
protected FFmpegProcessHandler(
IChannelRepository channelRepository,
IConfigElementRepository configElementRepository)
protected FFmpegProcessHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public async Task<Either<BaseError, Process>> Handle(T request, CancellationToken cancellationToken)
{
_channelRepository = channelRepository;
_configElementRepository = configElementRepository;
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
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) =>
Validate(request)
.Map(v => v.ToEither<Tuple<Channel, string>>())
.BindT(tuple => GetProcess(request, tuple.Item1, tuple.Item2));
protected abstract Task<Either<BaseError, Process>> GetProcess(T request, Channel channel, string ffmpegPath);
protected abstract Task<Either<BaseError, Process>> GetProcess(
TvContext dbContext,
T request,
Channel channel,
string ffmpegPath);
private async Task<Validation<BaseError, Tuple<Channel, string>>> Validate(T request) =>
(await ChannelMustExist(request), await FFmpegPathMustExist())
private static async Task<Validation<BaseError, Tuple<Channel, string>>> Validate(
TvContext dbContext,
T request) =>
(await ChannelMustExist(dbContext, request), await FFmpegPathMustExist(dbContext))
.Apply((channel, ffmpegPath) => Tuple(channel, ffmpegPath));
private async Task<Validation<BaseError, Channel>> ChannelMustExist(T request) =>
(await _channelRepository.GetByNumber(request.ChannelNumber))
.Map(
channel =>
{
channel.StreamingMode = request.Mode.ToLowerInvariant() switch
private static Task<Validation<BaseError, Channel>> ChannelMustExist(TvContext dbContext, T request) =>
dbContext.Channels
.Include(c => c.FFmpegProfile)
.ThenInclude(p => p.Resolution)
.Include(c => c.Artwork)
.Include(c => c.Watermark)
.SelectOneAsync(c => c.Number, c => c.Number == request.ChannelNumber)
.MapT(
channel =>
{
"hls-direct" => StreamingMode.HttpLiveStreamingDirect,
"ts" => StreamingMode.TransportStream,
_ => channel.StreamingMode
};
channel.StreamingMode = request.Mode.ToLowerInvariant() switch
{
"hls-direct" => StreamingMode.HttpLiveStreamingDirect,
"ts" => StreamingMode.TransportStream,
_ => channel.StreamingMode
};
return channel;
})
.ToValidation<BaseError>($"Channel number {request.ChannelNumber} does not exist.");
return channel;
})
.Map(o => o.ToValidation<BaseError>($"Channel number {request.ChannelNumber} does not exist."));
private Task<Validation<BaseError, string>> FFmpegPathMustExist() =>
_configElementRepository.GetValue<string>(ConfigElementKey.FFmpegPath)
private static Task<Validation<BaseError, string>> FFmpegPathMustExist(TvContext dbContext) =>
dbContext.ConfigElements.GetValue<string>(ConfigElementKey.FFmpegPath)
.FilterT(File.Exists)
.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; @@ -4,36 +4,36 @@ using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Runtime;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Streaming.Queries
{
public class GetConcatProcessByChannelNumberHandler : FFmpegProcessHandler<GetConcatProcessByChannelNumber>
{
private readonly IConfigElementRepository _configElementRepository;
private readonly FFmpegProcessService _ffmpegProcessService;
private readonly IRuntimeInfo _runtimeInfo;
public GetConcatProcessByChannelNumberHandler(
IChannelRepository channelRepository,
IConfigElementRepository configElementRepository,
IDbContextFactory<TvContext> dbContextFactory,
FFmpegProcessService ffmpegProcessService,
IRuntimeInfo runtimeInfo)
: base(channelRepository, configElementRepository)
: base(dbContextFactory)
{
_configElementRepository = configElementRepository;
_ffmpegProcessService = ffmpegProcessService;
_runtimeInfo = runtimeInfo;
}
protected override async Task<Either<BaseError, Process>> GetProcess(
TvContext dbContext,
GetConcatProcessByChannelNumber request,
Channel channel,
string ffmpegPath)
{
bool saveReports = !_runtimeInfo.IsOSPlatform(OSPlatform.Windows) && await _configElementRepository
bool saveReports = !_runtimeInfo.IsOSPlatform(OSPlatform.Windows) && await dbContext.ConfigElements
.GetValue<bool>(ConfigElementKey.FFmpegSaveReports)
.Map(result => result.IfNone(false));

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

@ -3,73 +3,83 @@ using System.Threading; @@ -3,73 +3,83 @@ using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Serilog;
namespace ErsatzTV.Application.Streaming.Queries
{
public class
GetHlsPlaylistByChannelNumberHandler : IRequestHandler<GetHlsPlaylistByChannelNumber, Either<BaseError, string>>
public class GetHlsPlaylistByChannelNumberHandler :
IRequestHandler<GetHlsPlaylistByChannelNumber, Either<BaseError, string>>
{
private readonly IChannelRepository _channelRepository;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IMemoryCache _memoryCache;
private readonly IPlayoutRepository _playoutRepository;
public GetHlsPlaylistByChannelNumberHandler(
IChannelRepository channelRepository,
IPlayoutRepository playoutRepository,
IDbContextFactory<TvContext> dbContextFactory,
IMemoryCache memoryCache)
{
_channelRepository = channelRepository;
_playoutRepository = playoutRepository;
_dbContextFactory = dbContextFactory;
_memoryCache = memoryCache;
}
public Task<Either<BaseError, string>> Handle(
public async Task<Either<BaseError, string>> Handle(
GetHlsPlaylistByChannelNumber request,
CancellationToken cancellationToken) =>
ChannelMustExist(request)
.Map(v => v.ToEither<Channel>())
.BindT(channel => GetPlaylist(request, channel));
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
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,
Channel channel)
Parameters parameters,
DateTimeOffset now)
{
string mode = string.IsNullOrWhiteSpace(request.Mode)
? string.Empty
: $"&mode={request.Mode}";
DateTimeOffset now = DateTimeOffset.Now;
Option<PlayoutItem> maybePlayoutItem = await _playoutRepository.GetPlayoutItem(channel.Id, now);
return maybePlayoutItem.Match<Either<BaseError, string>>(
playoutItem =>
{
long index = GetIndexForChannel(channel, playoutItem);
double timeRemaining = Math.Abs((playoutItem.FinishOffset - now).TotalSeconds);
return $@"#EXTM3U
long index = GetIndexForChannel(parameters.Channel, parameters.PlayoutItem);
double timeRemaining = Math.Abs((parameters.PlayoutItem.FinishOffset - now).TotalSeconds);
return $@"#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:{index}
#EXT-X-DISCONTINUITY
#EXTINF:{timeRemaining:F2},
{request.Scheme}://{request.Host}/ffmpeg/stream/{request.ChannelNumber}?index={index}{mode}
";
},
() =>
{
// 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}");
});
".AsTask();
}
private async Task<Validation<BaseError, Channel>> ChannelMustExist(GetHlsPlaylistByChannelNumber request) =>
(await _channelRepository.GetByNumber(request.ChannelNumber))
.ToValidation<BaseError>($"Channel number {request.ChannelNumber} does not exist.");
private Task<Validation<BaseError, Parameters>> Validate(
TvContext dbContext,
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)
{
@ -101,5 +111,7 @@ namespace ErsatzTV.Application.Streaming.Queries @@ -101,5 +111,7 @@ namespace ErsatzTV.Application.Streaming.Queries
private record ChannelIndexKey(int ChannelId);
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 @@ @@ -1,5 +1,6 @@
using System;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using ErsatzTV.Core;
@ -10,39 +11,35 @@ using ErsatzTV.Core.Interfaces.Emby; @@ -10,39 +11,35 @@ using ErsatzTV.Core.Interfaces.Emby;
using ErsatzTV.Core.Interfaces.Jellyfin;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Plex;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Runtime;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt;
using Microsoft.EntityFrameworkCore;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.Streaming.Queries
{
public class
GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<GetPlayoutItemProcessByChannelNumber>
public class GetPlayoutItemProcessByChannelNumberHandler :
FFmpegProcessHandler<GetPlayoutItemProcessByChannelNumber>
{
private readonly IConfigElementRepository _configElementRepository;
private readonly IEmbyPathReplacementService _embyPathReplacementService;
private readonly FFmpegProcessService _ffmpegProcessService;
private readonly IJellyfinPathReplacementService _jellyfinPathReplacementService;
private readonly ILocalFileSystem _localFileSystem;
private readonly IPlayoutRepository _playoutRepository;
private readonly IPlexPathReplacementService _plexPathReplacementService;
private readonly IRuntimeInfo _runtimeInfo;
public GetPlayoutItemProcessByChannelNumberHandler(
IChannelRepository channelRepository,
IConfigElementRepository configElementRepository,
IPlayoutRepository playoutRepository,
IDbContextFactory<TvContext> dbContextFactory,
FFmpegProcessService ffmpegProcessService,
ILocalFileSystem localFileSystem,
IPlexPathReplacementService plexPathReplacementService,
IJellyfinPathReplacementService jellyfinPathReplacementService,
IEmbyPathReplacementService embyPathReplacementService,
IRuntimeInfo runtimeInfo)
: base(channelRepository, configElementRepository)
: base(dbContextFactory)
{
_configElementRepository = configElementRepository;
_playoutRepository = playoutRepository;
_ffmpegProcessService = ffmpegProcessService;
_localFileSystem = localFileSystem;
_plexPathReplacementService = plexPathReplacementService;
@ -52,13 +49,32 @@ namespace ErsatzTV.Application.Streaming.Queries @@ -52,13 +49,32 @@ namespace ErsatzTV.Application.Streaming.Queries
}
protected override async Task<Either<BaseError, Process>> GetProcess(
TvContext dbContext,
GetPlayoutItemProcessByChannelNumber _,
Channel channel,
string ffmpegPath)
{
DateTimeOffset now = DateTimeOffset.Now;
Either<BaseError, PlayoutItemWithPath> maybePlayoutItem = await _playoutRepository
.GetPlayoutItem(channel.Id, now)
Either<BaseError, PlayoutItemWithPath> maybePlayoutItem = await dbContext.PlayoutItems
.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()))
.BindT(ValidatePlayoutItemPath);
@ -73,7 +89,7 @@ namespace ErsatzTV.Application.Streaming.Queries @@ -73,7 +89,7 @@ namespace ErsatzTV.Application.Streaming.Queries
_ => 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)
.Map(result => result.IfNone(false));
@ -95,8 +111,13 @@ namespace ErsatzTV.Application.Streaming.Queries @@ -95,8 +111,13 @@ namespace ErsatzTV.Application.Streaming.Queries
Option<TimeSpan> maybeDuration = await Optional(channel.FFmpegProfile.Transcode)
.Filter(transcode => transcode)
.Match(
_ => _playoutRepository.GetNextItemStart(channel.Id, now)
.MapT(nextStart => nextStart - now),
_ => dbContext.PlayoutItems
.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());
switch (error)

19
ErsatzTV.Application/Television/Mapper.cs

@ -57,19 +57,6 @@ namespace ErsatzTV.Application.Television @@ -57,19 +57,6 @@ namespace ErsatzTV.Application.Television
season.Show.ShowMetadata.HeadOrNone().Map(m => GetFanArt(m, maybeJellyfin, maybeEmby))
.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(
Metadata metadata,
Option<JellyfinMediaSource> maybeJellyfin,
@ -82,12 +69,6 @@ namespace ErsatzTV.Application.Television @@ -82,12 +69,6 @@ namespace ErsatzTV.Application.Television
Option<EmbyMediaSource> 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(
Metadata metadata,
ArtworkKind artworkKind,

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

@ -1,7 +0,0 @@ @@ -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 @@ @@ -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 @@ @@ -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; @@ -5,7 +5,6 @@ using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using static LanguageExt.Prelude;
namespace ErsatzTV.Core.Tests.Fakes
{
@ -15,25 +14,10 @@ namespace ErsatzTV.Core.Tests.Fakes @@ -15,25 +14,10 @@ namespace ErsatzTV.Core.Tests.Fakes
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) =>
throw new NotSupportedException();
public Task<List<Collection>> GetAll() => throw new NotSupportedException();
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<MediaItem>> GetItems(int id) => _data[id].ToList().AsTask();
public Task<List<int>> PlayoutIdsUsingCollection(int collectionId) => throw new NotSupportedException();
public Task<bool> IsCustomPlaybackOrder(int collectionId) => false.AsTask();
}

2
ErsatzTV.Core/FFmpeg/FFmpegLocator.cs

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

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

@ -14,7 +14,6 @@ namespace ErsatzTV.Core.Interfaces.Metadata @@ -14,7 +14,6 @@ namespace ErsatzTV.Core.Interfaces.Metadata
IEnumerable<string> ListSubdirectories(string folder);
IEnumerable<string> ListFiles(string folder);
bool FileExists(string path);
Task<byte[]> ReadAllBytes(string path);
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 @@ -7,14 +7,12 @@ namespace ErsatzTV.Core.Interfaces.Repositories
{
public interface IChannelRepository
{
Task<Channel> Add(Channel channel);
Task<Option<Channel>> Get(int id);
Task<Option<Channel>> GetByNumber(string number);
Task<List<Channel>> GetAll();
Task<List<Channel>> GetAllForGuide();
Task<bool> Update(Channel channel);
Task Delete(int channelId);
Task<int> CountPlayouts(int channelId);
Task<Unit> RemoveWatermark(Channel channel);
}
}

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

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