diff --git a/ErsatzTV.Application/Artists/ArtistViewModel.cs b/ErsatzTV.Application/Artists/ArtistViewModel.cs index 0b37fc210..5b1bb8b4c 100644 --- a/ErsatzTV.Application/Artists/ArtistViewModel.cs +++ b/ErsatzTV.Application/Artists/ArtistViewModel.cs @@ -1,16 +1,14 @@ -using System.Collections.Generic; -using System.Globalization; +using System.Globalization; -namespace ErsatzTV.Application.Artists -{ - public record ArtistViewModel( - string Name, - string Disambiguation, - string Biography, - string Thumbnail, - string FanArt, - List Genres, - List Styles, - List Moods, - List Languages); -} +namespace ErsatzTV.Application.Artists; + +public record ArtistViewModel( + string Name, + string Disambiguation, + string Biography, + string Thumbnail, + string FanArt, + List Genres, + List Styles, + List Moods, + List Languages); \ No newline at end of file diff --git a/ErsatzTV.Application/Artists/Mapper.cs b/ErsatzTV.Application/Artists/Mapper.cs index 45f635c49..34fc5d5c9 100644 --- a/ErsatzTV.Application/Artists/Mapper.cs +++ b/ErsatzTV.Application/Artists/Mapper.cs @@ -1,46 +1,40 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; +using System.Globalization; using ErsatzTV.Core.Domain; -using LanguageExt; -using static LanguageExt.Prelude; -namespace ErsatzTV.Application.Artists +namespace ErsatzTV.Application.Artists; + +internal static class Mapper { - internal static class Mapper + internal static ArtistViewModel ProjectToViewModel(Artist artist, List languages) { - internal static ArtistViewModel ProjectToViewModel(Artist artist, List languages) - { - ArtistMetadata metadata = Optional(artist.ArtistMetadata).Flatten().Head(); - return new ArtistViewModel( - metadata.Title, - metadata.Disambiguation, - metadata.Biography, - Artwork(metadata, ArtworkKind.Thumbnail), - Artwork(metadata, ArtworkKind.FanArt), - metadata.Genres.Map(g => g.Name).ToList(), - metadata.Styles.Map(s => s.Name).ToList(), - metadata.Moods.Map(m => m.Name).ToList(), - LanguagesForArtist(languages)); - } - - private static string Artwork(Metadata metadata, ArtworkKind artworkKind) => - Optional(metadata.Artwork.FirstOrDefault(a => a.ArtworkKind == artworkKind)) - .Match(a => a.Path, string.Empty); - - private static List LanguagesForArtist(List languages) - { - CultureInfo[] allCultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures); - - return languages - .Distinct() - .Map( - lang => allCultures.Filter( - ci => string.Equals(ci.ThreeLetterISOLanguageName, lang, StringComparison.OrdinalIgnoreCase))) - .Sequence() - .Flatten() - .ToList(); - } + ArtistMetadata metadata = Optional(artist.ArtistMetadata).Flatten().Head(); + return new ArtistViewModel( + metadata.Title, + metadata.Disambiguation, + metadata.Biography, + Artwork(metadata, ArtworkKind.Thumbnail), + Artwork(metadata, ArtworkKind.FanArt), + metadata.Genres.Map(g => g.Name).ToList(), + metadata.Styles.Map(s => s.Name).ToList(), + metadata.Moods.Map(m => m.Name).ToList(), + LanguagesForArtist(languages)); } -} + + private static string Artwork(Metadata metadata, ArtworkKind artworkKind) => + Optional(metadata.Artwork.FirstOrDefault(a => a.ArtworkKind == artworkKind)) + .Match(a => a.Path, string.Empty); + + private static List LanguagesForArtist(List languages) + { + CultureInfo[] allCultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures); + + return languages + .Distinct() + .Map( + lang => allCultures.Filter( + ci => string.Equals(ci.ThreeLetterISOLanguageName, lang, StringComparison.OrdinalIgnoreCase))) + .Sequence() + .Flatten() + .ToList(); + } +} \ No newline at end of file diff --git a/ErsatzTV.Application/Artists/Queries/GetAllArtists.cs b/ErsatzTV.Application/Artists/Queries/GetAllArtists.cs index b58873fa8..a63d932de 100644 --- a/ErsatzTV.Application/Artists/Queries/GetAllArtists.cs +++ b/ErsatzTV.Application/Artists/Queries/GetAllArtists.cs @@ -1,8 +1,5 @@ -using System.Collections.Generic; -using ErsatzTV.Application.MediaItems; -using MediatR; +using ErsatzTV.Application.MediaItems; -namespace ErsatzTV.Application.Artists.Queries -{ - public record GetAllArtists : IRequest>; -} +namespace ErsatzTV.Application.Artists; + +public record GetAllArtists : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Artists/Queries/GetAllArtistsHandler.cs b/ErsatzTV.Application/Artists/Queries/GetAllArtistsHandler.cs index d059c08d3..39a0150be 100644 --- a/ErsatzTV.Application/Artists/Queries/GetAllArtistsHandler.cs +++ b/ErsatzTV.Application/Artists/Queries/GetAllArtistsHandler.cs @@ -1,29 +1,22 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Application.MediaItems; +using ErsatzTV.Application.MediaItems; using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -using MediatR; using static ErsatzTV.Application.MediaItems.Mapper; -namespace ErsatzTV.Application.Artists.Queries +namespace ErsatzTV.Application.Artists; + +public class GetAllArtistsHandler : IRequestHandler> { - public class GetAllArtistsHandler : IRequestHandler> - { - private readonly IArtistRepository _artistRepository; + private readonly IArtistRepository _artistRepository; - public GetAllArtistsHandler(IArtistRepository artistRepository) => _artistRepository = artistRepository; + public GetAllArtistsHandler(IArtistRepository artistRepository) => _artistRepository = artistRepository; - public Task> Handle( - GetAllArtists request, - CancellationToken cancellationToken) => - _artistRepository.GetAllArtists() - .Map( - list => list.Filter( - a => !string.IsNullOrWhiteSpace( - a.ArtistMetadata.HeadOrNone().Match(am => am.Title, () => string.Empty)))) - .Map(list => list.Map(ProjectToViewModel).ToList()); - } -} + public Task> Handle( + GetAllArtists request, + CancellationToken cancellationToken) => + _artistRepository.GetAllArtists() + .Map( + list => list.Filter( + a => !string.IsNullOrWhiteSpace( + a.ArtistMetadata.HeadOrNone().Match(am => am.Title, () => string.Empty)))) + .Map(list => list.Map(ProjectToViewModel).ToList()); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Artists/Queries/GetArtistById.cs b/ErsatzTV.Application/Artists/Queries/GetArtistById.cs index 67d15dd6e..54c8e3642 100644 --- a/ErsatzTV.Application/Artists/Queries/GetArtistById.cs +++ b/ErsatzTV.Application/Artists/Queries/GetArtistById.cs @@ -1,7 +1,3 @@ -using LanguageExt; -using MediatR; +namespace ErsatzTV.Application.Artists; -namespace ErsatzTV.Application.Artists.Queries -{ - public record GetArtistById(int ArtistId) : IRequest>; -} +public record GetArtistById(int ArtistId) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Artists/Queries/GetArtistByIdHandler.cs b/ErsatzTV.Application/Artists/Queries/GetArtistByIdHandler.cs index dc676992b..e4e8304c6 100644 --- a/ErsatzTV.Application/Artists/Queries/GetArtistByIdHandler.cs +++ b/ErsatzTV.Application/Artists/Queries/GetArtistByIdHandler.cs @@ -1,38 +1,32 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -using MediatR; using static ErsatzTV.Application.Artists.Mapper; -namespace ErsatzTV.Application.Artists.Queries +namespace ErsatzTV.Application.Artists; + +public class GetArtistByIdHandler : IRequestHandler> { - public class GetArtistByIdHandler : IRequestHandler> + private readonly IArtistRepository _artistRepository; + private readonly ISearchRepository _searchRepository; + + public GetArtistByIdHandler(IArtistRepository artistRepository, ISearchRepository searchRepository) { - private readonly IArtistRepository _artistRepository; - private readonly ISearchRepository _searchRepository; - - public GetArtistByIdHandler(IArtistRepository artistRepository, ISearchRepository searchRepository) - { - _artistRepository = artistRepository; - _searchRepository = searchRepository; - } - - public async Task> Handle( - GetArtistById request, - CancellationToken cancellationToken) - { - Option maybeArtist = await _artistRepository.GetArtist(request.ArtistId); - return await maybeArtist.Match>>( - async artist => - { - List mediaCodes = await _searchRepository.GetLanguagesForArtist(artist); - List languageCodes = await _searchRepository.GetAllLanguageCodes(mediaCodes); - return ProjectToViewModel(artist, languageCodes); - }, - () => Task.FromResult(Option.None)); - } + _artistRepository = artistRepository; + _searchRepository = searchRepository; } -} + + public async Task> Handle( + GetArtistById request, + CancellationToken cancellationToken) + { + Option maybeArtist = await _artistRepository.GetArtist(request.ArtistId); + return await maybeArtist.Match>>( + async artist => + { + List mediaCodes = await _searchRepository.GetLanguagesForArtist(artist); + List languageCodes = await _searchRepository.GetAllLanguageCodes(mediaCodes); + return ProjectToViewModel(artist, languageCodes); + }, + () => Task.FromResult(Option.None)); + } +} \ No newline at end of file diff --git a/ErsatzTV.Application/Channels/ChannelViewModel.cs b/ErsatzTV.Application/Channels/ChannelViewModel.cs index 4a33115ca..04b846b21 100644 --- a/ErsatzTV.Application/Channels/ChannelViewModel.cs +++ b/ErsatzTV.Application/Channels/ChannelViewModel.cs @@ -1,18 +1,17 @@ using ErsatzTV.Core.Domain; -namespace ErsatzTV.Application.Channels -{ - public record ChannelViewModel( - int Id, - string Number, - string Name, - string Group, - string Categories, - int FFmpegProfileId, - string Logo, - string PreferredLanguageCode, - StreamingMode StreamingMode, - int? WatermarkId, - int? FallbackFillerId, - int PlayoutCount); -} +namespace ErsatzTV.Application.Channels; + +public record ChannelViewModel( + int Id, + string Number, + string Name, + string Group, + string Categories, + int FFmpegProfileId, + string Logo, + string PreferredLanguageCode, + StreamingMode StreamingMode, + int? WatermarkId, + int? FallbackFillerId, + int PlayoutCount); \ No newline at end of file diff --git a/ErsatzTV.Application/Channels/Commands/CreateChannel.cs b/ErsatzTV.Application/Channels/Commands/CreateChannel.cs index 033e3e52a..fbfdf75db 100644 --- a/ErsatzTV.Application/Channels/Commands/CreateChannel.cs +++ b/ErsatzTV.Application/Channels/Commands/CreateChannel.cs @@ -1,20 +1,17 @@ using ErsatzTV.Core; using ErsatzTV.Core.Domain; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.Channels.Commands -{ - public record CreateChannel - ( - string Name, - string Number, - string Group, - string Categories, - int FFmpegProfileId, - string Logo, - string PreferredLanguageCode, - StreamingMode StreamingMode, - int? WatermarkId, - int? FallbackFillerId) : IRequest>; -} +namespace ErsatzTV.Application.Channels; + +public record CreateChannel +( + string Name, + string Number, + string Group, + string Categories, + int FFmpegProfileId, + string Logo, + string PreferredLanguageCode, + StreamingMode StreamingMode, + int? WatermarkId, + int? FallbackFillerId) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Channels/Commands/CreateChannelHandler.cs b/ErsatzTV.Application/Channels/Commands/CreateChannelHandler.cs index bd16ff008..a0cbc5caa 100644 --- a/ErsatzTV.Application/Channels/Commands/CreateChannelHandler.cs +++ b/ErsatzTV.Application/Channels/Commands/CreateChannelHandler.cs @@ -1,164 +1,155 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; +using System.Globalization; using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Filler; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; -using LanguageExt; -using MediatR; using Microsoft.EntityFrameworkCore; -using static LanguageExt.Prelude; -namespace ErsatzTV.Application.Channels.Commands +namespace ErsatzTV.Application.Channels; + +public class CreateChannelHandler : IRequestHandler> { - public class CreateChannelHandler : IRequestHandler> + private readonly IDbContextFactory _dbContextFactory; + + public CreateChannelHandler(IDbContextFactory dbContextFactory) => _dbContextFactory = dbContextFactory; + + public async Task> Handle( + CreateChannel request, + CancellationToken cancellationToken) { - private readonly IDbContextFactory _dbContextFactory; - - public CreateChannelHandler(IDbContextFactory dbContextFactory) => _dbContextFactory = dbContextFactory; - - public async Task> Handle( - CreateChannel request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - Validation validation = await Validate(dbContext, request); - return await validation.Apply(c => PersistChannel(dbContext, c)); - } - - private static async Task PersistChannel(TvContext dbContext, Channel channel) - { - await dbContext.Channels.AddAsync(channel); - await dbContext.SaveChangesAsync(); - return new CreateChannelResult(channel.Id); - } - - private static async Task> Validate(TvContext dbContext, CreateChannel request) => - (ValidateName(request), await ValidateNumber(dbContext, request), - await FFmpegProfileMustExist(dbContext, request), - ValidatePreferredLanguage(request), - await WatermarkMustExist(dbContext, request), - await FillerPresetMustExist(dbContext, request)) - .Apply( - (name, number, ffmpegProfileId, preferredLanguageCode, watermarkId, fillerPresetId) => - { - var artwork = new List(); - if (!string.IsNullOrWhiteSpace(request.Logo)) - { - artwork.Add( - new Artwork - { - Path = request.Logo, - ArtworkKind = ArtworkKind.Logo, - DateAdded = DateTime.UtcNow, - DateUpdated = DateTime.UtcNow - }); - } - - var channel = new Channel(Guid.NewGuid()) - { - Name = name, - Number = number, - Group = request.Group, - Categories = request.Categories, - FFmpegProfileId = ffmpegProfileId, - StreamingMode = request.StreamingMode, - Artwork = artwork, - PreferredLanguageCode = preferredLanguageCode - }; - - foreach (int id in watermarkId) - { - channel.WatermarkId = id; - } - - foreach (int id in fillerPresetId) - { - channel.FallbackFillerId = id; - } - - return channel; - }); - - private static Validation ValidateName(CreateChannel createChannel) => - createChannel.NotEmpty(c => c.Name) - .Bind(_ => createChannel.NotLongerThan(50)(c => c.Name)); - - private static Validation 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("Preferred language code is invalid"); - - private static async Task> ValidateNumber(TvContext dbContext, CreateChannel createChannel) - { - Option maybeExistingChannel = await dbContext.Channels - .SelectOneAsync(c => c.Number, c => c.Number == createChannel.Number); - return maybeExistingChannel.Match>( - _ => BaseError.New("Channel number must be unique"), - () => - { - if (Regex.IsMatch(createChannel.Number, Channel.NumberValidator)) - { - return createChannel.Number; - } - - return BaseError.New("Invalid channel number; one decimal is allowed for subchannels"); - }); - } - - private static Task> 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($"FFmpegProfile {createChannel.FFmpegProfileId} does not exist.")); - - private static async Task>> WatermarkMustExist( - TvContext dbContext, - CreateChannel createChannel) - { - if (createChannel.WatermarkId is null) - { - return Option.None; - } - - return await dbContext.ChannelWatermarks - .CountAsync(w => w.Id == createChannel.WatermarkId) - .Map(Optional) - .Filter(c => c > 0) - .MapT(_ => Optional(createChannel.WatermarkId)) - .Map(o => o.ToValidation($"Watermark {createChannel.WatermarkId} does not exist.")); - } - - private static async Task>> FillerPresetMustExist( - TvContext dbContext, - CreateChannel createChannel) - { - if (createChannel.FallbackFillerId is null) - { - return Option.None; - } - - return await dbContext.FillerPresets - .Filter(fp => fp.FillerKind == FillerKind.Fallback) - .CountAsync(w => w.Id == createChannel.FallbackFillerId) - .Map(Optional) - .Filter(c => c > 0) - .MapT(_ => Optional(createChannel.FallbackFillerId)) - .Map( - o => o.ToValidation( - $"Fallback filler {createChannel.FallbackFillerId} does not exist.")); - } + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + Validation validation = await Validate(dbContext, request); + return await LanguageExtensions.Apply(validation, c => PersistChannel(dbContext, c)); } -} + + private static async Task PersistChannel(TvContext dbContext, Channel channel) + { + await dbContext.Channels.AddAsync(channel); + await dbContext.SaveChangesAsync(); + return new CreateChannelResult(channel.Id); + } + + private static async Task> Validate(TvContext dbContext, CreateChannel request) => + (ValidateName(request), await ValidateNumber(dbContext, request), + await FFmpegProfileMustExist(dbContext, request), + ValidatePreferredLanguage(request), + await WatermarkMustExist(dbContext, request), + await FillerPresetMustExist(dbContext, request)) + .Apply( + (name, number, ffmpegProfileId, preferredLanguageCode, watermarkId, fillerPresetId) => + { + var artwork = new List(); + if (!string.IsNullOrWhiteSpace(request.Logo)) + { + artwork.Add( + new Artwork + { + Path = request.Logo, + ArtworkKind = ArtworkKind.Logo, + DateAdded = DateTime.UtcNow, + DateUpdated = DateTime.UtcNow + }); + } + + var channel = new Channel(Guid.NewGuid()) + { + Name = name, + Number = number, + Group = request.Group, + Categories = request.Categories, + FFmpegProfileId = ffmpegProfileId, + StreamingMode = request.StreamingMode, + Artwork = artwork, + PreferredLanguageCode = preferredLanguageCode + }; + + foreach (int id in watermarkId) + { + channel.WatermarkId = id; + } + + foreach (int id in fillerPresetId) + { + channel.FallbackFillerId = id; + } + + return channel; + }); + + private static Validation ValidateName(CreateChannel createChannel) => + createChannel.NotEmpty(c => c.Name) + .Bind(_ => createChannel.NotLongerThan(50)(c => c.Name)); + + private static Validation 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("Preferred language code is invalid"); + + private static async Task> ValidateNumber(TvContext dbContext, CreateChannel createChannel) + { + Option maybeExistingChannel = await dbContext.Channels + .SelectOneAsync(c => c.Number, c => c.Number == createChannel.Number); + return maybeExistingChannel.Match>( + _ => BaseError.New("Channel number must be unique"), + () => + { + if (Regex.IsMatch(createChannel.Number, Channel.NumberValidator)) + { + return createChannel.Number; + } + + return BaseError.New("Invalid channel number; one decimal is allowed for subchannels"); + }); + } + + private static Task> 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($"FFmpegProfile {createChannel.FFmpegProfileId} does not exist.")); + + private static async Task>> WatermarkMustExist( + TvContext dbContext, + CreateChannel createChannel) + { + if (createChannel.WatermarkId is null) + { + return Option.None; + } + + return await dbContext.ChannelWatermarks + .CountAsync(w => w.Id == createChannel.WatermarkId) + .Map(Optional) + .Filter(c => c > 0) + .MapT(_ => Optional(createChannel.WatermarkId)) + .Map(o => o.ToValidation($"Watermark {createChannel.WatermarkId} does not exist.")); + } + + private static async Task>> FillerPresetMustExist( + TvContext dbContext, + CreateChannel createChannel) + { + if (createChannel.FallbackFillerId is null) + { + return Option.None; + } + + return await dbContext.FillerPresets + .Filter(fp => fp.FillerKind == FillerKind.Fallback) + .CountAsync(w => w.Id == createChannel.FallbackFillerId) + .Map(Optional) + .Filter(c => c > 0) + .MapT(_ => Optional(createChannel.FallbackFillerId)) + .Map( + o => o.ToValidation( + $"Fallback filler {createChannel.FallbackFillerId} does not exist.")); + } +} \ No newline at end of file diff --git a/ErsatzTV.Application/Channels/Commands/CreateChannelResult.cs b/ErsatzTV.Application/Channels/Commands/CreateChannelResult.cs index ccd147197..af9178516 100644 --- a/ErsatzTV.Application/Channels/Commands/CreateChannelResult.cs +++ b/ErsatzTV.Application/Channels/Commands/CreateChannelResult.cs @@ -1,4 +1,3 @@ -namespace ErsatzTV.Application.Channels.Commands -{ - public record CreateChannelResult(int ChannelId) : EntityIdResult(ChannelId); -} +namespace ErsatzTV.Application.Channels; + +public record CreateChannelResult(int ChannelId) : EntityIdResult(ChannelId); \ No newline at end of file diff --git a/ErsatzTV.Application/Channels/Commands/DeleteChannel.cs b/ErsatzTV.Application/Channels/Commands/DeleteChannel.cs index 76fd60299..14f6365b6 100644 --- a/ErsatzTV.Application/Channels/Commands/DeleteChannel.cs +++ b/ErsatzTV.Application/Channels/Commands/DeleteChannel.cs @@ -1,9 +1,5 @@ -using System.Threading.Tasks; -using ErsatzTV.Core; -using LanguageExt; -using MediatR; +using ErsatzTV.Core; -namespace ErsatzTV.Application.Channels.Commands -{ - public record DeleteChannel(int ChannelId) : IRequest>; -} +namespace ErsatzTV.Application.Channels; + +public record DeleteChannel(int ChannelId) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Channels/Commands/DeleteChannelHandler.cs b/ErsatzTV.Application/Channels/Commands/DeleteChannelHandler.cs index 83b019d7d..b6534cf27 100644 --- a/ErsatzTV.Application/Channels/Commands/DeleteChannelHandler.cs +++ b/ErsatzTV.Application/Channels/Commands/DeleteChannelHandler.cs @@ -1,28 +1,23 @@ -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.Channels.Commands +namespace ErsatzTV.Application.Channels; + +public class DeleteChannelHandler : IRequestHandler> { - public class DeleteChannelHandler : IRequestHandler> - { - private readonly IChannelRepository _channelRepository; + private readonly IChannelRepository _channelRepository; - public DeleteChannelHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository; + public DeleteChannelHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository; - public async Task> Handle(DeleteChannel request, CancellationToken cancellationToken) => - (await ChannelMustExist(request)) - .Map(DoDeletion) - .ToEither(); + public async Task> Handle(DeleteChannel request, CancellationToken cancellationToken) => + (await ChannelMustExist(request)) + .Map(DoDeletion) + .ToEither(); - private Task DoDeletion(int channelId) => _channelRepository.Delete(channelId); + private Task DoDeletion(int channelId) => _channelRepository.Delete(channelId); - private async Task> ChannelMustExist(DeleteChannel deleteChannel) => - (await _channelRepository.Get(deleteChannel.ChannelId)) - .ToValidation($"Channel {deleteChannel.ChannelId} does not exist.") - .Map(c => c.Id); - } -} + private async Task> ChannelMustExist(DeleteChannel deleteChannel) => + (await _channelRepository.Get(deleteChannel.ChannelId)) + .ToValidation($"Channel {deleteChannel.ChannelId} does not exist.") + .Map(c => c.Id); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Channels/Commands/UpdateChannel.cs b/ErsatzTV.Application/Channels/Commands/UpdateChannel.cs index d4ac52093..fed79c254 100644 --- a/ErsatzTV.Application/Channels/Commands/UpdateChannel.cs +++ b/ErsatzTV.Application/Channels/Commands/UpdateChannel.cs @@ -1,21 +1,18 @@ using ErsatzTV.Core; using ErsatzTV.Core.Domain; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.Channels.Commands -{ - public record UpdateChannel - ( - int ChannelId, - string Name, - string Number, - string Group, - string Categories, - int FFmpegProfileId, - string Logo, - string PreferredLanguageCode, - StreamingMode StreamingMode, - int? WatermarkId, - int? FallbackFillerId) : IRequest>; -} +namespace ErsatzTV.Application.Channels; + +public record UpdateChannel +( + int ChannelId, + string Name, + string Number, + string Group, + string Categories, + int FFmpegProfileId, + string Logo, + string PreferredLanguageCode, + StreamingMode StreamingMode, + int? WatermarkId, + int? FallbackFillerId) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Channels/Commands/UpdateChannelHandler.cs b/ErsatzTV.Application/Channels/Commands/UpdateChannelHandler.cs index 5aee3c713..e2052b9e7 100644 --- a/ErsatzTV.Application/Channels/Commands/UpdateChannelHandler.cs +++ b/ErsatzTV.Application/Channels/Commands/UpdateChannelHandler.cs @@ -1,124 +1,115 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; +using System.Globalization; using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; -using LanguageExt; -using MediatR; using Microsoft.EntityFrameworkCore; using static ErsatzTV.Application.Channels.Mapper; -using static LanguageExt.Prelude; -namespace ErsatzTV.Application.Channels.Commands +namespace ErsatzTV.Application.Channels; + +public class UpdateChannelHandler : IRequestHandler> { - public class UpdateChannelHandler : IRequestHandler> + private readonly IDbContextFactory _dbContextFactory; + + public UpdateChannelHandler(IDbContextFactory dbContextFactory) => + _dbContextFactory = dbContextFactory; + + public async Task> Handle( + UpdateChannel request, + CancellationToken cancellationToken) { - private readonly IDbContextFactory _dbContextFactory; - - public UpdateChannelHandler(IDbContextFactory dbContextFactory) => - _dbContextFactory = dbContextFactory; - - public async Task> Handle( - UpdateChannel request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - Validation validation = await Validate(dbContext, request); - return await validation.Apply(c => ApplyUpdateRequest(dbContext, c, request)); - } - - private async Task ApplyUpdateRequest(TvContext dbContext, Channel c, UpdateChannel update) - { - c.Name = update.Name; - c.Number = update.Number; - c.Group = update.Group; - c.Categories = update.Categories; - c.FFmpegProfileId = update.FFmpegProfileId; - c.PreferredLanguageCode = update.PreferredLanguageCode; - c.Artwork ??= new List(); - - if (!string.IsNullOrWhiteSpace(update.Logo)) - { - Option maybeLogo = - Optional(c.Artwork).Flatten().FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Logo); - - maybeLogo.Match( - artwork => - { - artwork.Path = update.Logo; - artwork.DateUpdated = DateTime.UtcNow; - }, - () => - { - var artwork = new Artwork - { - Path = update.Logo, - DateAdded = DateTime.UtcNow, - DateUpdated = DateTime.UtcNow, - ArtworkKind = ArtworkKind.Logo - }; - c.Artwork.Add(artwork); - }); - } - - c.StreamingMode = update.StreamingMode; - c.WatermarkId = update.WatermarkId; - c.FallbackFillerId = update.FallbackFillerId; - await dbContext.SaveChangesAsync(); - return ProjectToViewModel(c); - } - - private async Task> Validate(TvContext dbContext, UpdateChannel request) => - (await ChannelMustExist(dbContext, request), ValidateName(request), - await ValidateNumber(dbContext, request), - ValidatePreferredLanguage(request)) - .Apply((channelToUpdate, _, _, _) => channelToUpdate); - - private static Task> ChannelMustExist( - TvContext dbContext, - UpdateChannel updateChannel) => - dbContext.Channels - .Include(c => c.Artwork) - .Include(c => c.Watermark) - .SelectOneAsync(c => c.Id, c => c.Id == updateChannel.ChannelId) - .Map(o => o.ToValidation("Channel does not exist.")); - - private static Validation ValidateName(UpdateChannel updateChannel) => - updateChannel.NotEmpty(c => c.Name) - .Bind(_ => updateChannel.NotLongerThan(50)(c => c.Name)); - - private static async Task> ValidateNumber( - TvContext dbContext, - UpdateChannel updateChannel) - { - int matchId = await dbContext.Channels - .SelectOneAsync(c => c.Number, c => c.Number == updateChannel.Number) - .Match(c => c.Id, () => updateChannel.ChannelId); - - if (matchId == updateChannel.ChannelId) - { - if (Regex.IsMatch(updateChannel.Number, Channel.NumberValidator)) - { - return updateChannel.Number; - } - - return BaseError.New("Invalid channel number; one decimal is allowed for subchannels"); - } - - return BaseError.New("Channel number must be unique"); - } - - private static Validation ValidatePreferredLanguage(UpdateChannel updateChannel) => - Optional(updateChannel.PreferredLanguageCode ?? string.Empty) - .Filter( - lc => string.IsNullOrWhiteSpace(lc) || CultureInfo.GetCultures(CultureTypes.NeutralCultures).Any( - ci => string.Equals(ci.ThreeLetterISOLanguageName, lc, StringComparison.OrdinalIgnoreCase))) - .ToValidation("Preferred language code is invalid"); + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + Validation validation = await Validate(dbContext, request); + return await LanguageExtensions.Apply(validation, c => ApplyUpdateRequest(dbContext, c, request)); } -} + + private async Task ApplyUpdateRequest(TvContext dbContext, Channel c, UpdateChannel update) + { + c.Name = update.Name; + c.Number = update.Number; + c.Group = update.Group; + c.Categories = update.Categories; + c.FFmpegProfileId = update.FFmpegProfileId; + c.PreferredLanguageCode = update.PreferredLanguageCode; + c.Artwork ??= new List(); + + if (!string.IsNullOrWhiteSpace(update.Logo)) + { + Option maybeLogo = + Optional(c.Artwork).Flatten().FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Logo); + + maybeLogo.Match( + artwork => + { + artwork.Path = update.Logo; + artwork.DateUpdated = DateTime.UtcNow; + }, + () => + { + var artwork = new Artwork + { + Path = update.Logo, + DateAdded = DateTime.UtcNow, + DateUpdated = DateTime.UtcNow, + ArtworkKind = ArtworkKind.Logo + }; + c.Artwork.Add(artwork); + }); + } + + c.StreamingMode = update.StreamingMode; + c.WatermarkId = update.WatermarkId; + c.FallbackFillerId = update.FallbackFillerId; + await dbContext.SaveChangesAsync(); + return ProjectToViewModel(c); + } + + private async Task> Validate(TvContext dbContext, UpdateChannel request) => + (await ChannelMustExist(dbContext, request), ValidateName(request), + await ValidateNumber(dbContext, request), + ValidatePreferredLanguage(request)) + .Apply((channelToUpdate, _, _, _) => channelToUpdate); + + private static Task> ChannelMustExist( + TvContext dbContext, + UpdateChannel updateChannel) => + dbContext.Channels + .Include(c => c.Artwork) + .Include(c => c.Watermark) + .SelectOneAsync(c => c.Id, c => c.Id == updateChannel.ChannelId) + .Map(o => o.ToValidation("Channel does not exist.")); + + private static Validation ValidateName(UpdateChannel updateChannel) => + updateChannel.NotEmpty(c => c.Name) + .Bind(_ => updateChannel.NotLongerThan(50)(c => c.Name)); + + private static async Task> ValidateNumber( + TvContext dbContext, + UpdateChannel updateChannel) + { + int matchId = await dbContext.Channels + .SelectOneAsync(c => c.Number, c => c.Number == updateChannel.Number) + .Match(c => c.Id, () => updateChannel.ChannelId); + + if (matchId == updateChannel.ChannelId) + { + if (Regex.IsMatch(updateChannel.Number, Channel.NumberValidator)) + { + return updateChannel.Number; + } + + return BaseError.New("Invalid channel number; one decimal is allowed for subchannels"); + } + + return BaseError.New("Channel number must be unique"); + } + + private static Validation ValidatePreferredLanguage(UpdateChannel updateChannel) => + Optional(updateChannel.PreferredLanguageCode ?? string.Empty) + .Filter( + lc => string.IsNullOrWhiteSpace(lc) || CultureInfo.GetCultures(CultureTypes.NeutralCultures).Any( + ci => string.Equals(ci.ThreeLetterISOLanguageName, lc, StringComparison.OrdinalIgnoreCase))) + .ToValidation("Preferred language code is invalid"); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Channels/Mapper.cs b/ErsatzTV.Application/Channels/Mapper.cs index 4024f2e86..b48406ec0 100644 --- a/ErsatzTV.Application/Channels/Mapper.cs +++ b/ErsatzTV.Application/Channels/Mapper.cs @@ -1,32 +1,29 @@ -using System.Linq; -using ErsatzTV.Core.Domain; -using static LanguageExt.Prelude; +using ErsatzTV.Core.Domain; -namespace ErsatzTV.Application.Channels +namespace ErsatzTV.Application.Channels; + +internal static class Mapper { - internal static class Mapper - { - internal static ChannelViewModel ProjectToViewModel(Channel channel) => - new( - channel.Id, - channel.Number, - channel.Name, - channel.Group, - channel.Categories, - channel.FFmpegProfileId, - GetLogo(channel), - channel.PreferredLanguageCode, - channel.StreamingMode, - channel.WatermarkId, - channel.FallbackFillerId, - channel.Playouts?.Count ?? 0); + internal static ChannelViewModel ProjectToViewModel(Channel channel) => + new( + channel.Id, + channel.Number, + channel.Name, + channel.Group, + channel.Categories, + channel.FFmpegProfileId, + GetLogo(channel), + channel.PreferredLanguageCode, + channel.StreamingMode, + channel.WatermarkId, + channel.FallbackFillerId, + channel.Playouts?.Count ?? 0); - private static string GetLogo(Channel channel) => - Optional(channel.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Logo)) - .Match(a => a.Path, string.Empty); + private static string GetLogo(Channel channel) => + Optional(channel.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Logo)) + .Match(a => a.Path, string.Empty); - private static string GetWatermark(Channel channel) => - Optional(channel.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Watermark)) - .Match(a => a.Path, string.Empty); - } -} + private static string GetWatermark(Channel channel) => + Optional(channel.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Watermark)) + .Match(a => a.Path, string.Empty); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Channels/Queries/GetAllChannels.cs b/ErsatzTV.Application/Channels/Queries/GetAllChannels.cs index 99c5d0f08..4c05e0f68 100644 --- a/ErsatzTV.Application/Channels/Queries/GetAllChannels.cs +++ b/ErsatzTV.Application/Channels/Queries/GetAllChannels.cs @@ -1,7 +1,3 @@ -using System.Collections.Generic; -using MediatR; +namespace ErsatzTV.Application.Channels; -namespace ErsatzTV.Application.Channels.Queries -{ - public record GetAllChannels : IRequest>; -} +public record GetAllChannels : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Channels/Queries/GetAllChannelsHandler.cs b/ErsatzTV.Application/Channels/Queries/GetAllChannelsHandler.cs index 7d4f8c1f0..87e891013 100644 --- a/ErsatzTV.Application/Channels/Queries/GetAllChannelsHandler.cs +++ b/ErsatzTV.Application/Channels/Queries/GetAllChannelsHandler.cs @@ -1,21 +1,14 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core.Interfaces.Repositories; -using MediatR; +using ErsatzTV.Core.Interfaces.Repositories; using static ErsatzTV.Application.Channels.Mapper; -using static LanguageExt.Prelude; -namespace ErsatzTV.Application.Channels.Queries +namespace ErsatzTV.Application.Channels; + +public class GetAllChannelsHandler : IRequestHandler> { - public class GetAllChannelsHandler : IRequestHandler> - { - private readonly IChannelRepository _channelRepository; + private readonly IChannelRepository _channelRepository; - public GetAllChannelsHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository; + public GetAllChannelsHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository; - public async Task> Handle(GetAllChannels request, CancellationToken cancellationToken) => - Optional(await _channelRepository.GetAll()).Flatten().Map(ProjectToViewModel).ToList(); - } -} + public async Task> Handle(GetAllChannels request, CancellationToken cancellationToken) => + Optional(await _channelRepository.GetAll()).Flatten().Map(ProjectToViewModel).ToList(); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Channels/Queries/GetChannelById.cs b/ErsatzTV.Application/Channels/Queries/GetChannelById.cs index 696ee2e45..af490209a 100644 --- a/ErsatzTV.Application/Channels/Queries/GetChannelById.cs +++ b/ErsatzTV.Application/Channels/Queries/GetChannelById.cs @@ -1,7 +1,3 @@ -using LanguageExt; -using MediatR; +namespace ErsatzTV.Application.Channels; -namespace ErsatzTV.Application.Channels.Queries -{ - public record GetChannelById(int Id) : IRequest>; -} +public record GetChannelById(int Id) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Channels/Queries/GetChannelByIdHandler.cs b/ErsatzTV.Application/Channels/Queries/GetChannelByIdHandler.cs index 8770c9671..07638286a 100644 --- a/ErsatzTV.Application/Channels/Queries/GetChannelByIdHandler.cs +++ b/ErsatzTV.Application/Channels/Queries/GetChannelByIdHandler.cs @@ -1,20 +1,15 @@ -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -using MediatR; +using ErsatzTV.Core.Interfaces.Repositories; using static ErsatzTV.Application.Channels.Mapper; -namespace ErsatzTV.Application.Channels.Queries +namespace ErsatzTV.Application.Channels; + +public class GetChannelByIdHandler : IRequestHandler> { - public class GetChannelByIdHandler : IRequestHandler> - { - private readonly IChannelRepository _channelRepository; + private readonly IChannelRepository _channelRepository; - public GetChannelByIdHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository; + public GetChannelByIdHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository; - public Task> Handle(GetChannelById request, CancellationToken cancellationToken) => - _channelRepository.Get(request.Id) - .MapT(ProjectToViewModel); - } -} + public Task> Handle(GetChannelById request, CancellationToken cancellationToken) => + _channelRepository.Get(request.Id) + .MapT(ProjectToViewModel); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Channels/Queries/GetChannelFramerate.cs b/ErsatzTV.Application/Channels/Queries/GetChannelFramerate.cs index d2f69d539..7d907e277 100644 --- a/ErsatzTV.Application/Channels/Queries/GetChannelFramerate.cs +++ b/ErsatzTV.Application/Channels/Queries/GetChannelFramerate.cs @@ -1,6 +1,3 @@ -using LanguageExt; -using MediatR; - -namespace ErsatzTV.Application.Channels.Queries; +namespace ErsatzTV.Application.Channels; public record GetChannelFramerate(string ChannelNumber) : IRequest>; diff --git a/ErsatzTV.Application/Channels/Queries/GetChannelFramerateHandler.cs b/ErsatzTV.Application/Channels/Queries/GetChannelFramerateHandler.cs index a68c52019..632bea988 100644 --- a/ErsatzTV.Application/Channels/Queries/GetChannelFramerateHandler.cs +++ b/ErsatzTV.Application/Channels/Queries/GetChannelFramerateHandler.cs @@ -1,18 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; using ErsatzTV.Core.Extensions; using ErsatzTV.Infrastructure.Data; -using LanguageExt; -using MediatR; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using static LanguageExt.Prelude; -namespace ErsatzTV.Application.Channels.Queries; +namespace ErsatzTV.Application.Channels; public class GetChannelFramerateHandler : IRequestHandler> { diff --git a/ErsatzTV.Application/Channels/Queries/GetChannelGuide.cs b/ErsatzTV.Application/Channels/Queries/GetChannelGuide.cs index 5d37ca95c..5b097262e 100644 --- a/ErsatzTV.Application/Channels/Queries/GetChannelGuide.cs +++ b/ErsatzTV.Application/Channels/Queries/GetChannelGuide.cs @@ -1,7 +1,5 @@ using ErsatzTV.Core.Iptv; -using MediatR; -namespace ErsatzTV.Application.Channels.Queries -{ - public record GetChannelGuide(string Scheme, string Host) : IRequest; -} +namespace ErsatzTV.Application.Channels; + +public record GetChannelGuide(string Scheme, string Host) : IRequest; \ No newline at end of file diff --git a/ErsatzTV.Application/Channels/Queries/GetChannelGuideHandler.cs b/ErsatzTV.Application/Channels/Queries/GetChannelGuideHandler.cs index 8b07dadd9..378599358 100644 --- a/ErsatzTV.Application/Channels/Queries/GetChannelGuideHandler.cs +++ b/ErsatzTV.Application/Channels/Queries/GetChannelGuideHandler.cs @@ -1,20 +1,15 @@ -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core.Interfaces.Repositories; +using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Iptv; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.Channels.Queries +namespace ErsatzTV.Application.Channels; + +public class GetChannelGuideHandler : IRequestHandler { - public class GetChannelGuideHandler : IRequestHandler - { - private readonly IChannelRepository _channelRepository; + private readonly IChannelRepository _channelRepository; - public GetChannelGuideHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository; + public GetChannelGuideHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository; - public Task Handle(GetChannelGuide request, CancellationToken cancellationToken) => - _channelRepository.GetAllForGuide() - .Map(channels => new ChannelGuide(request.Scheme, request.Host, channels)); - } -} + public Task Handle(GetChannelGuide request, CancellationToken cancellationToken) => + _channelRepository.GetAllForGuide() + .Map(channels => new ChannelGuide(request.Scheme, request.Host, channels)); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Channels/Queries/GetChannelLineup.cs b/ErsatzTV.Application/Channels/Queries/GetChannelLineup.cs index 979522d53..6201557ca 100644 --- a/ErsatzTV.Application/Channels/Queries/GetChannelLineup.cs +++ b/ErsatzTV.Application/Channels/Queries/GetChannelLineup.cs @@ -1,8 +1,5 @@ -using System.Collections.Generic; -using ErsatzTV.Core.Hdhr; -using MediatR; +using ErsatzTV.Core.Hdhr; -namespace ErsatzTV.Application.Channels.Queries -{ - public record GetChannelLineup(string Scheme, string Host) : IRequest>; -} +namespace ErsatzTV.Application.Channels; + +public record GetChannelLineup(string Scheme, string Host) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Channels/Queries/GetChannelLineupHandler.cs b/ErsatzTV.Application/Channels/Queries/GetChannelLineupHandler.cs index c43e17c21..214f1c243 100644 --- a/ErsatzTV.Application/Channels/Queries/GetChannelLineupHandler.cs +++ b/ErsatzTV.Application/Channels/Queries/GetChannelLineupHandler.cs @@ -1,22 +1,15 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core.Hdhr; +using ErsatzTV.Core.Hdhr; using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.Channels.Queries +namespace ErsatzTV.Application.Channels; + +public class GetChannelLineupHandler : IRequestHandler> { - public class GetChannelLineupHandler : IRequestHandler> - { - private readonly IChannelRepository _channelRepository; + private readonly IChannelRepository _channelRepository; - public GetChannelLineupHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository; + public GetChannelLineupHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository; - public Task> Handle(GetChannelLineup request, CancellationToken cancellationToken) => - _channelRepository.GetAll() - .Map(channels => channels.Map(c => new LineupItem(request.Scheme, request.Host, c)).ToList()); - } -} + public Task> Handle(GetChannelLineup request, CancellationToken cancellationToken) => + _channelRepository.GetAll() + .Map(channels => channels.Map(c => new LineupItem(request.Scheme, request.Host, c)).ToList()); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Channels/Queries/GetChannelPlaylist.cs b/ErsatzTV.Application/Channels/Queries/GetChannelPlaylist.cs index f263e7dc2..ab4667909 100644 --- a/ErsatzTV.Application/Channels/Queries/GetChannelPlaylist.cs +++ b/ErsatzTV.Application/Channels/Queries/GetChannelPlaylist.cs @@ -1,7 +1,5 @@ using ErsatzTV.Core.Iptv; -using MediatR; -namespace ErsatzTV.Application.Channels.Queries -{ - public record GetChannelPlaylist(string Scheme, string Host, string Mode) : IRequest; -} +namespace ErsatzTV.Application.Channels; + +public record GetChannelPlaylist(string Scheme, string Host, string Mode) : IRequest; \ No newline at end of file diff --git a/ErsatzTV.Application/Channels/Queries/GetChannelPlaylistHandler.cs b/ErsatzTV.Application/Channels/Queries/GetChannelPlaylistHandler.cs index 1ae633d4a..66a6576f1 100644 --- a/ErsatzTV.Application/Channels/Queries/GetChannelPlaylistHandler.cs +++ b/ErsatzTV.Application/Channels/Queries/GetChannelPlaylistHandler.cs @@ -1,56 +1,50 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Iptv; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.Channels.Queries +namespace ErsatzTV.Application.Channels; + +public class GetChannelPlaylistHandler : IRequestHandler { - public class GetChannelPlaylistHandler : IRequestHandler + private readonly IChannelRepository _channelRepository; + + public GetChannelPlaylistHandler(IChannelRepository channelRepository) => + _channelRepository = channelRepository; + + public Task Handle(GetChannelPlaylist request, CancellationToken cancellationToken) => + _channelRepository.GetAll() + .Map(channels => EnsureMode(channels, request.Mode)) + .Map(channels => new ChannelPlaylist(request.Scheme, request.Host, channels)); + + private static List EnsureMode(IEnumerable channels, string mode) { - private readonly IChannelRepository _channelRepository; - - public GetChannelPlaylistHandler(IChannelRepository channelRepository) => - _channelRepository = channelRepository; - - public Task Handle(GetChannelPlaylist request, CancellationToken cancellationToken) => - _channelRepository.GetAll() - .Map(channels => EnsureMode(channels, request.Mode)) - .Map(channels => new ChannelPlaylist(request.Scheme, request.Host, channels)); - - private static List EnsureMode(IEnumerable channels, string mode) + var result = new List(); + foreach (Channel channel in channels) { - var result = new List(); - foreach (Channel channel in channels) + switch (mode.ToLowerInvariant()) { - switch (mode.ToLowerInvariant()) - { - case "segmenter": - channel.StreamingMode = StreamingMode.HttpLiveStreamingSegmenter; - result.Add(channel); - break; - case "hls-direct": - channel.StreamingMode = StreamingMode.HttpLiveStreamingDirect; - result.Add(channel); - break; - case "ts-legacy": - channel.StreamingMode = StreamingMode.TransportStream; - result.Add(channel); - break; - case "ts": - channel.StreamingMode = StreamingMode.TransportStreamHybrid; - result.Add(channel); - break; - default: - result.Add(channel); - break; - } + case "segmenter": + channel.StreamingMode = StreamingMode.HttpLiveStreamingSegmenter; + result.Add(channel); + break; + case "hls-direct": + channel.StreamingMode = StreamingMode.HttpLiveStreamingDirect; + result.Add(channel); + break; + case "ts-legacy": + channel.StreamingMode = StreamingMode.TransportStream; + result.Add(channel); + break; + case "ts": + channel.StreamingMode = StreamingMode.TransportStreamHybrid; + result.Add(channel); + break; + default: + result.Add(channel); + break; } - - return result; } + + return result; } -} +} \ No newline at end of file diff --git a/ErsatzTV.Application/Configuration/Commands/SaveConfigElementByKey.cs b/ErsatzTV.Application/Configuration/Commands/SaveConfigElementByKey.cs index 2f814f5c9..3cba41e08 100644 --- a/ErsatzTV.Application/Configuration/Commands/SaveConfigElementByKey.cs +++ b/ErsatzTV.Application/Configuration/Commands/SaveConfigElementByKey.cs @@ -1,7 +1,5 @@ using ErsatzTV.Core.Domain; -using LanguageExt; -namespace ErsatzTV.Application.Configuration.Commands -{ - public record SaveConfigElementByKey(ConfigElementKey Key, string Value) : MediatR.IRequest; -} +namespace ErsatzTV.Application.Configuration; + +public record SaveConfigElementByKey(ConfigElementKey Key, string Value) : MediatR.IRequest; \ No newline at end of file diff --git a/ErsatzTV.Application/Configuration/Commands/SaveConfigElementByKeyHandler.cs b/ErsatzTV.Application/Configuration/Commands/SaveConfigElementByKeyHandler.cs index d2bdb4a72..091d197d7 100644 --- a/ErsatzTV.Application/Configuration/Commands/SaveConfigElementByKeyHandler.cs +++ b/ErsatzTV.Application/Configuration/Commands/SaveConfigElementByKeyHandler.cs @@ -1,21 +1,17 @@ -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; +using ErsatzTV.Core.Interfaces.Repositories; -namespace ErsatzTV.Application.Configuration.Commands +namespace ErsatzTV.Application.Configuration; + +public class SaveConfigElementByKeyHandler : MediatR.IRequestHandler { - public class SaveConfigElementByKeyHandler : MediatR.IRequestHandler + private readonly IConfigElementRepository _configElementRepository; + + public SaveConfigElementByKeyHandler(IConfigElementRepository configElementRepository) => + _configElementRepository = configElementRepository; + + public async Task Handle(SaveConfigElementByKey request, CancellationToken cancellationToken) { - private readonly IConfigElementRepository _configElementRepository; - - public SaveConfigElementByKeyHandler(IConfigElementRepository configElementRepository) => - _configElementRepository = configElementRepository; - - public async Task Handle(SaveConfigElementByKey request, CancellationToken cancellationToken) - { - await _configElementRepository.Upsert(request.Key, request.Value); - return Unit.Default; - } + await _configElementRepository.Upsert(request.Key, request.Value); + return Unit.Default; } -} +} \ No newline at end of file diff --git a/ErsatzTV.Application/Configuration/Commands/UpdateLibraryRefreshInterval.cs b/ErsatzTV.Application/Configuration/Commands/UpdateLibraryRefreshInterval.cs index 4b9a7c012..244b7db80 100644 --- a/ErsatzTV.Application/Configuration/Commands/UpdateLibraryRefreshInterval.cs +++ b/ErsatzTV.Application/Configuration/Commands/UpdateLibraryRefreshInterval.cs @@ -1,7 +1,5 @@ using ErsatzTV.Core; -using LanguageExt; -namespace ErsatzTV.Application.Configuration.Commands -{ - public record UpdateLibraryRefreshInterval(int LibraryRefreshInterval) : MediatR.IRequest>; -} +namespace ErsatzTV.Application.Configuration; + +public record UpdateLibraryRefreshInterval(int LibraryRefreshInterval) : MediatR.IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Configuration/Commands/UpdateLibraryRefreshIntervalHandler.cs b/ErsatzTV.Application/Configuration/Commands/UpdateLibraryRefreshIntervalHandler.cs index 6e232d98c..66236301d 100644 --- a/ErsatzTV.Application/Configuration/Commands/UpdateLibraryRefreshIntervalHandler.cs +++ b/ErsatzTV.Application/Configuration/Commands/UpdateLibraryRefreshIntervalHandler.cs @@ -1,33 +1,28 @@ -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -using static LanguageExt.Prelude; -namespace ErsatzTV.Application.Configuration.Commands +namespace ErsatzTV.Application.Configuration; + +public class UpdateLibraryRefreshIntervalHandler : + MediatR.IRequestHandler> { - public class UpdateLibraryRefreshIntervalHandler : - MediatR.IRequestHandler> - { - private readonly IConfigElementRepository _configElementRepository; + private readonly IConfigElementRepository _configElementRepository; - public UpdateLibraryRefreshIntervalHandler(IConfigElementRepository configElementRepository) => - _configElementRepository = configElementRepository; + public UpdateLibraryRefreshIntervalHandler(IConfigElementRepository configElementRepository) => + _configElementRepository = configElementRepository; - public Task> Handle( - UpdateLibraryRefreshInterval request, - CancellationToken cancellationToken) => - Validate(request) - .MapT(_ => _configElementRepository.Upsert(ConfigElementKey.LibraryRefreshInterval, request.LibraryRefreshInterval)) - .Bind(v => v.ToEitherAsync()); + public Task> Handle( + UpdateLibraryRefreshInterval request, + CancellationToken cancellationToken) => + Validate(request) + .MapT(_ => _configElementRepository.Upsert(ConfigElementKey.LibraryRefreshInterval, request.LibraryRefreshInterval)) + .Bind(v => v.ToEitherAsync()); - private static Task> Validate(UpdateLibraryRefreshInterval request) => - Optional(request.LibraryRefreshInterval) - .Where(lri => lri > 0) - .Map(_ => Unit.Default) - .ToValidation("Tuner count must be greater than zero") - .AsTask(); - } -} + private static Task> Validate(UpdateLibraryRefreshInterval request) => + Optional(request.LibraryRefreshInterval) + .Where(lri => lri > 0) + .Map(_ => Unit.Default) + .ToValidation("Tuner count must be greater than zero") + .AsTask(); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Configuration/Commands/UpdatePlayoutDaysToBuild.cs b/ErsatzTV.Application/Configuration/Commands/UpdatePlayoutDaysToBuild.cs index fba7759e5..1c2fe661d 100644 --- a/ErsatzTV.Application/Configuration/Commands/UpdatePlayoutDaysToBuild.cs +++ b/ErsatzTV.Application/Configuration/Commands/UpdatePlayoutDaysToBuild.cs @@ -1,7 +1,5 @@ using ErsatzTV.Core; -using LanguageExt; -namespace ErsatzTV.Application.Configuration.Commands -{ - public record UpdatePlayoutDaysToBuild(int DaysToBuild) : MediatR.IRequest>; -} +namespace ErsatzTV.Application.Configuration; + +public record UpdatePlayoutDaysToBuild(int DaysToBuild) : MediatR.IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Configuration/Commands/UpdatePlayoutDaysToBuildHandler.cs b/ErsatzTV.Application/Configuration/Commands/UpdatePlayoutDaysToBuildHandler.cs index c8a5b90ad..77f205396 100644 --- a/ErsatzTV.Application/Configuration/Commands/UpdatePlayoutDaysToBuildHandler.cs +++ b/ErsatzTV.Application/Configuration/Commands/UpdatePlayoutDaysToBuildHandler.cs @@ -1,66 +1,59 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Channels; -using System.Threading.Tasks; -using ErsatzTV.Application.Playouts.Commands; +using System.Threading.Channels; +using ErsatzTV.Application.Playouts; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Infrastructure.Data; -using LanguageExt; using Microsoft.EntityFrameworkCore; -using static LanguageExt.Prelude; -namespace ErsatzTV.Application.Configuration.Commands +namespace ErsatzTV.Application.Configuration; + +public class + UpdatePlayoutDaysToBuildHandler : MediatR.IRequestHandler> { - public class - UpdatePlayoutDaysToBuildHandler : MediatR.IRequestHandler> + private readonly IConfigElementRepository _configElementRepository; + private readonly IDbContextFactory _dbContextFactory; + private readonly ChannelWriter _workerChannel; + + public UpdatePlayoutDaysToBuildHandler( + IConfigElementRepository configElementRepository, + IDbContextFactory dbContextFactory, + ChannelWriter workerChannel) { - private readonly IConfigElementRepository _configElementRepository; - private readonly IDbContextFactory _dbContextFactory; - private readonly ChannelWriter _workerChannel; - - public UpdatePlayoutDaysToBuildHandler( - IConfigElementRepository configElementRepository, - IDbContextFactory dbContextFactory, - ChannelWriter workerChannel) - { - _configElementRepository = configElementRepository; - _dbContextFactory = dbContextFactory; - _workerChannel = workerChannel; - } - - public async Task> Handle( - UpdatePlayoutDaysToBuild request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - Validation validation = await Validate(request); - return await validation.Apply(_ => ApplyUpdate(dbContext, request.DaysToBuild)); - } - - private async Task ApplyUpdate(TvContext dbContext, int daysToBuild) - { - await _configElementRepository.Upsert(ConfigElementKey.PlayoutDaysToBuild, daysToBuild); - - // build all playouts to proper number of days - List playouts = await dbContext.Playouts - .Include(p => p.Channel) - .ToListAsync(); - foreach (int playoutId in playouts.OrderBy(p => decimal.Parse(p.Channel.Number)).Map(p => p.Id)) - { - await _workerChannel.WriteAsync(new BuildPlayout(playoutId)); - } - - return Unit.Default; - } - - private static Task> Validate(UpdatePlayoutDaysToBuild request) => - Optional(request.DaysToBuild) - .Where(days => days > 0) - .Map(_ => Unit.Default) - .ToValidation("Days to build must be greater than zero") - .AsTask(); + _configElementRepository = configElementRepository; + _dbContextFactory = dbContextFactory; + _workerChannel = workerChannel; } -} + + public async Task> Handle( + UpdatePlayoutDaysToBuild request, + CancellationToken cancellationToken) + { + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + Validation validation = await Validate(request); + return await validation.Apply(_ => ApplyUpdate(dbContext, request.DaysToBuild)); + } + + private async Task ApplyUpdate(TvContext dbContext, int daysToBuild) + { + await _configElementRepository.Upsert(ConfigElementKey.PlayoutDaysToBuild, daysToBuild); + + // build all playouts to proper number of days + List playouts = await dbContext.Playouts + .Include(p => p.Channel) + .ToListAsync(); + foreach (int playoutId in playouts.OrderBy(p => decimal.Parse(p.Channel.Number)).Map(p => p.Id)) + { + await _workerChannel.WriteAsync(new BuildPlayout(playoutId)); + } + + return Unit.Default; + } + + private static Task> Validate(UpdatePlayoutDaysToBuild request) => + Optional(request.DaysToBuild) + .Where(days => days > 0) + .Map(_ => Unit.Default) + .ToValidation("Days to build must be greater than zero") + .AsTask(); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Configuration/ConfigElementViewModel.cs b/ErsatzTV.Application/Configuration/ConfigElementViewModel.cs index e50b44cdf..6cd251466 100644 --- a/ErsatzTV.Application/Configuration/ConfigElementViewModel.cs +++ b/ErsatzTV.Application/Configuration/ConfigElementViewModel.cs @@ -1,4 +1,3 @@ -namespace ErsatzTV.Application.Configuration -{ - public record ConfigElementViewModel(string Key, string Value); -} +namespace ErsatzTV.Application.Configuration; + +public record ConfigElementViewModel(string Key, string Value); \ No newline at end of file diff --git a/ErsatzTV.Application/Configuration/Mapper.cs b/ErsatzTV.Application/Configuration/Mapper.cs index bffed599d..9731588d8 100644 --- a/ErsatzTV.Application/Configuration/Mapper.cs +++ b/ErsatzTV.Application/Configuration/Mapper.cs @@ -1,10 +1,9 @@ using ErsatzTV.Core.Domain; -namespace ErsatzTV.Application.Configuration +namespace ErsatzTV.Application.Configuration; + +internal static class Mapper { - internal static class Mapper - { - internal static ConfigElementViewModel ProjectToViewModel(ConfigElement element) => - new(element.Key, element.Value); - } -} + internal static ConfigElementViewModel ProjectToViewModel(ConfigElement element) => + new(element.Key, element.Value); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Configuration/Queries/GetConfigElementByKey.cs b/ErsatzTV.Application/Configuration/Queries/GetConfigElementByKey.cs index 96e6368c1..5f62f1922 100644 --- a/ErsatzTV.Application/Configuration/Queries/GetConfigElementByKey.cs +++ b/ErsatzTV.Application/Configuration/Queries/GetConfigElementByKey.cs @@ -1,8 +1,5 @@ using ErsatzTV.Core.Domain; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.Configuration.Queries -{ - public record GetConfigElementByKey(ConfigElementKey Key) : IRequest>; -} +namespace ErsatzTV.Application.Configuration; + +public record GetConfigElementByKey(ConfigElementKey Key) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Configuration/Queries/GetConfigElementByKeyHandler.cs b/ErsatzTV.Application/Configuration/Queries/GetConfigElementByKeyHandler.cs index 76bac5641..c99996f96 100644 --- a/ErsatzTV.Application/Configuration/Queries/GetConfigElementByKeyHandler.cs +++ b/ErsatzTV.Application/Configuration/Queries/GetConfigElementByKeyHandler.cs @@ -1,22 +1,17 @@ -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -using MediatR; +using ErsatzTV.Core.Interfaces.Repositories; using static ErsatzTV.Application.Configuration.Mapper; -namespace ErsatzTV.Application.Configuration.Queries +namespace ErsatzTV.Application.Configuration; + +public class GetConfigElementByKeyHandler : IRequestHandler> { - public class GetConfigElementByKeyHandler : IRequestHandler> - { - private readonly IConfigElementRepository _configElementRepository; + private readonly IConfigElementRepository _configElementRepository; - public GetConfigElementByKeyHandler(IConfigElementRepository configElementRepository) => - _configElementRepository = configElementRepository; + public GetConfigElementByKeyHandler(IConfigElementRepository configElementRepository) => + _configElementRepository = configElementRepository; - public Task> Handle( - GetConfigElementByKey request, - CancellationToken cancellationToken) => - _configElementRepository.Get(request.Key).MapT(ProjectToViewModel); - } -} + public Task> Handle( + GetConfigElementByKey request, + CancellationToken cancellationToken) => + _configElementRepository.Get(request.Key).MapT(ProjectToViewModel); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Configuration/Queries/GetLibraryRefreshInterval.cs b/ErsatzTV.Application/Configuration/Queries/GetLibraryRefreshInterval.cs index 5c36b1ae9..bf727dc55 100644 --- a/ErsatzTV.Application/Configuration/Queries/GetLibraryRefreshInterval.cs +++ b/ErsatzTV.Application/Configuration/Queries/GetLibraryRefreshInterval.cs @@ -1,6 +1,3 @@ -using MediatR; +namespace ErsatzTV.Application.Configuration; -namespace ErsatzTV.Application.Configuration.Queries -{ - public record GetLibraryRefreshInterval : IRequest; -} +public record GetLibraryRefreshInterval : IRequest; \ No newline at end of file diff --git a/ErsatzTV.Application/Configuration/Queries/GetLibraryRefreshIntervalHandler.cs b/ErsatzTV.Application/Configuration/Queries/GetLibraryRefreshIntervalHandler.cs index 1ccdd82e9..ad737c3fc 100644 --- a/ErsatzTV.Application/Configuration/Queries/GetLibraryRefreshIntervalHandler.cs +++ b/ErsatzTV.Application/Configuration/Queries/GetLibraryRefreshIntervalHandler.cs @@ -1,21 +1,16 @@ -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.Configuration.Queries +namespace ErsatzTV.Application.Configuration; + +public class GetLibraryRefreshIntervalHandler : IRequestHandler { - public class GetLibraryRefreshIntervalHandler : IRequestHandler - { - private readonly IConfigElementRepository _configElementRepository; + private readonly IConfigElementRepository _configElementRepository; - public GetLibraryRefreshIntervalHandler(IConfigElementRepository configElementRepository) => - _configElementRepository = configElementRepository; + public GetLibraryRefreshIntervalHandler(IConfigElementRepository configElementRepository) => + _configElementRepository = configElementRepository; - public Task Handle(GetLibraryRefreshInterval request, CancellationToken cancellationToken) => - _configElementRepository.GetValue(ConfigElementKey.LibraryRefreshInterval) - .Map(result => result.IfNone(6)); - } -} + public Task Handle(GetLibraryRefreshInterval request, CancellationToken cancellationToken) => + _configElementRepository.GetValue(ConfigElementKey.LibraryRefreshInterval) + .Map(result => result.IfNone(6)); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Configuration/Queries/GetPlayoutDaysToBuild.cs b/ErsatzTV.Application/Configuration/Queries/GetPlayoutDaysToBuild.cs index b17e1609f..190bb5208 100644 --- a/ErsatzTV.Application/Configuration/Queries/GetPlayoutDaysToBuild.cs +++ b/ErsatzTV.Application/Configuration/Queries/GetPlayoutDaysToBuild.cs @@ -1,6 +1,3 @@ -using MediatR; +namespace ErsatzTV.Application.Configuration; -namespace ErsatzTV.Application.Configuration.Queries -{ - public record GetPlayoutDaysToBuild : IRequest; -} +public record GetPlayoutDaysToBuild : IRequest; \ No newline at end of file diff --git a/ErsatzTV.Application/Configuration/Queries/GetPlayoutDaysToBuildHandler.cs b/ErsatzTV.Application/Configuration/Queries/GetPlayoutDaysToBuildHandler.cs index a328abcf1..036f0ea0f 100644 --- a/ErsatzTV.Application/Configuration/Queries/GetPlayoutDaysToBuildHandler.cs +++ b/ErsatzTV.Application/Configuration/Queries/GetPlayoutDaysToBuildHandler.cs @@ -1,21 +1,16 @@ -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.Configuration.Queries +namespace ErsatzTV.Application.Configuration; + +public class GetPlayoutDaysToBuildHandler : IRequestHandler { - public class GetPlayoutDaysToBuildHandler : IRequestHandler - { - private readonly IConfigElementRepository _configElementRepository; + private readonly IConfigElementRepository _configElementRepository; - public GetPlayoutDaysToBuildHandler(IConfigElementRepository configElementRepository) => - _configElementRepository = configElementRepository; + public GetPlayoutDaysToBuildHandler(IConfigElementRepository configElementRepository) => + _configElementRepository = configElementRepository; - public Task Handle(GetPlayoutDaysToBuild request, CancellationToken cancellationToken) => - _configElementRepository.GetValue(ConfigElementKey.PlayoutDaysToBuild) - .Map(result => result.IfNone(2)); - } -} + public Task Handle(GetPlayoutDaysToBuild request, CancellationToken cancellationToken) => + _configElementRepository.GetValue(ConfigElementKey.PlayoutDaysToBuild) + .Map(result => result.IfNone(2)); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Emby/Commands/DisconnectEmby.cs b/ErsatzTV.Application/Emby/Commands/DisconnectEmby.cs index 29f6474fc..d6a530dac 100644 --- a/ErsatzTV.Application/Emby/Commands/DisconnectEmby.cs +++ b/ErsatzTV.Application/Emby/Commands/DisconnectEmby.cs @@ -1,7 +1,5 @@ using ErsatzTV.Core; -using LanguageExt; -namespace ErsatzTV.Application.Emby.Commands -{ - public record DisconnectEmby : MediatR.IRequest>; -} +namespace ErsatzTV.Application.Emby; + +public record DisconnectEmby : MediatR.IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Emby/Commands/DisconnectEmbyHandler.cs b/ErsatzTV.Application/Emby/Commands/DisconnectEmbyHandler.cs index 0e950cd85..7d72ea1b6 100644 --- a/ErsatzTV.Application/Emby/Commands/DisconnectEmbyHandler.cs +++ b/ErsatzTV.Application/Emby/Commands/DisconnectEmbyHandler.cs @@ -1,46 +1,41 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Emby; using ErsatzTV.Core.Interfaces.Locking; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Search; -using LanguageExt; -namespace ErsatzTV.Application.Emby.Commands +namespace ErsatzTV.Application.Emby; + +public class DisconnectEmbyHandler : MediatR.IRequestHandler> { - public class DisconnectEmbyHandler : MediatR.IRequestHandler> + private readonly IEmbySecretStore _embySecretStore; + private readonly IEntityLocker _entityLocker; + private readonly IMediaSourceRepository _mediaSourceRepository; + private readonly ISearchIndex _searchIndex; + + public DisconnectEmbyHandler( + IMediaSourceRepository mediaSourceRepository, + IEmbySecretStore embySecretStore, + IEntityLocker entityLocker, + ISearchIndex searchIndex) { - private readonly IEmbySecretStore _embySecretStore; - private readonly IEntityLocker _entityLocker; - private readonly IMediaSourceRepository _mediaSourceRepository; - private readonly ISearchIndex _searchIndex; - - public DisconnectEmbyHandler( - IMediaSourceRepository mediaSourceRepository, - IEmbySecretStore embySecretStore, - IEntityLocker entityLocker, - ISearchIndex searchIndex) - { - _mediaSourceRepository = mediaSourceRepository; - _embySecretStore = embySecretStore; - _entityLocker = entityLocker; - _searchIndex = searchIndex; - } - - public async Task> Handle( - DisconnectEmby request, - CancellationToken cancellationToken) - { - List ids = await _mediaSourceRepository.DeleteAllEmby(); - await _searchIndex.RemoveItems(ids); - _searchIndex.Commit(); - await _embySecretStore.DeleteAll(); - _entityLocker.UnlockRemoteMediaSource(); - - return Unit.Default; - } + _mediaSourceRepository = mediaSourceRepository; + _embySecretStore = embySecretStore; + _entityLocker = entityLocker; + _searchIndex = searchIndex; } -} + + public async Task> Handle( + DisconnectEmby request, + CancellationToken cancellationToken) + { + List ids = await _mediaSourceRepository.DeleteAllEmby(); + await _searchIndex.RemoveItems(ids); + _searchIndex.Commit(); + await _embySecretStore.DeleteAll(); + _entityLocker.UnlockRemoteMediaSource(); + + return Unit.Default; + } +} \ No newline at end of file diff --git a/ErsatzTV.Application/Emby/Commands/SaveEmbySecrets.cs b/ErsatzTV.Application/Emby/Commands/SaveEmbySecrets.cs index 5657d165f..673556409 100644 --- a/ErsatzTV.Application/Emby/Commands/SaveEmbySecrets.cs +++ b/ErsatzTV.Application/Emby/Commands/SaveEmbySecrets.cs @@ -1,8 +1,6 @@ using ErsatzTV.Core; using ErsatzTV.Core.Emby; -using LanguageExt; -namespace ErsatzTV.Application.Emby.Commands -{ - public record SaveEmbySecrets(EmbySecrets Secrets) : MediatR.IRequest>; -} +namespace ErsatzTV.Application.Emby; + +public record SaveEmbySecrets(EmbySecrets Secrets) : MediatR.IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Emby/Commands/SaveEmbySecretsHandler.cs b/ErsatzTV.Application/Emby/Commands/SaveEmbySecretsHandler.cs index 7fd4ecb06..87909125f 100644 --- a/ErsatzTV.Application/Emby/Commands/SaveEmbySecretsHandler.cs +++ b/ErsatzTV.Application/Emby/Commands/SaveEmbySecretsHandler.cs @@ -1,60 +1,56 @@ -using System.Threading; -using System.Threading.Channels; -using System.Threading.Tasks; +using System.Threading.Channels; using ErsatzTV.Core; using ErsatzTV.Core.Emby; using ErsatzTV.Core.Interfaces.Emby; using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -namespace ErsatzTV.Application.Emby.Commands +namespace ErsatzTV.Application.Emby; + +public class SaveEmbySecretsHandler : MediatR.IRequestHandler> { - public class SaveEmbySecretsHandler : MediatR.IRequestHandler> + private readonly ChannelWriter _channel; + private readonly IEmbyApiClient _embyApiClient; + private readonly IEmbySecretStore _embySecretStore; + private readonly IMediaSourceRepository _mediaSourceRepository; + + public SaveEmbySecretsHandler( + IEmbySecretStore embySecretStore, + IEmbyApiClient embyApiClient, + IMediaSourceRepository mediaSourceRepository, + ChannelWriter channel) { - private readonly ChannelWriter _channel; - private readonly IEmbyApiClient _embyApiClient; - private readonly IEmbySecretStore _embySecretStore; - private readonly IMediaSourceRepository _mediaSourceRepository; - - public SaveEmbySecretsHandler( - IEmbySecretStore embySecretStore, - IEmbyApiClient embyApiClient, - IMediaSourceRepository mediaSourceRepository, - ChannelWriter channel) - { - _embySecretStore = embySecretStore; - _embyApiClient = embyApiClient; - _mediaSourceRepository = mediaSourceRepository; - _channel = channel; - } - - public Task> Handle(SaveEmbySecrets request, CancellationToken cancellationToken) => - Validate(request) - .MapT(PerformSave) - .Bind(v => v.ToEitherAsync()); - - private async Task> Validate(SaveEmbySecrets request) - { - Either maybeServerInformation = await _embyApiClient - .GetServerInformation(request.Secrets.Address, request.Secrets.ApiKey); - - return maybeServerInformation.Match( - info => Validation.Success(new Parameters(request.Secrets, info)), - error => error); - } - - private async Task PerformSave(Parameters parameters) - { - await _embySecretStore.SaveSecrets(parameters.Secrets); - await _mediaSourceRepository.UpsertEmby( - parameters.Secrets.Address, - parameters.ServerInformation.ServerName, - parameters.ServerInformation.OperatingSystem); - await _channel.WriteAsync(new SynchronizeEmbyMediaSources()); - - return Unit.Default; - } - - private record Parameters(EmbySecrets Secrets, EmbyServerInformation ServerInformation); + _embySecretStore = embySecretStore; + _embyApiClient = embyApiClient; + _mediaSourceRepository = mediaSourceRepository; + _channel = channel; } -} + + public Task> Handle(SaveEmbySecrets request, CancellationToken cancellationToken) => + Validate(request) + .MapT(PerformSave) + .Bind(v => v.ToEitherAsync()); + + private async Task> Validate(SaveEmbySecrets request) + { + Either maybeServerInformation = await _embyApiClient + .GetServerInformation(request.Secrets.Address, request.Secrets.ApiKey); + + return maybeServerInformation.Match( + info => Validation.Success(new Parameters(request.Secrets, info)), + error => error); + } + + private async Task PerformSave(Parameters parameters) + { + await _embySecretStore.SaveSecrets(parameters.Secrets); + await _mediaSourceRepository.UpsertEmby( + parameters.Secrets.Address, + parameters.ServerInformation.ServerName, + parameters.ServerInformation.OperatingSystem); + await _channel.WriteAsync(new SynchronizeEmbyMediaSources()); + + return Unit.Default; + } + + private record Parameters(EmbySecrets Secrets, EmbyServerInformation ServerInformation); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Emby/Commands/SynchronizeEmbyLibraries.cs b/ErsatzTV.Application/Emby/Commands/SynchronizeEmbyLibraries.cs index 1430d2420..f6eb659c4 100644 --- a/ErsatzTV.Application/Emby/Commands/SynchronizeEmbyLibraries.cs +++ b/ErsatzTV.Application/Emby/Commands/SynchronizeEmbyLibraries.cs @@ -1,8 +1,6 @@ using ErsatzTV.Core; -using LanguageExt; -namespace ErsatzTV.Application.Emby.Commands -{ - public record SynchronizeEmbyLibraries(int EmbyMediaSourceId) : MediatR.IRequest>, - IEmbyBackgroundServiceRequest; -} +namespace ErsatzTV.Application.Emby; + +public record SynchronizeEmbyLibraries(int EmbyMediaSourceId) : MediatR.IRequest>, + IEmbyBackgroundServiceRequest; \ No newline at end of file diff --git a/ErsatzTV.Application/Emby/Commands/SynchronizeEmbyLibrariesHandler.cs b/ErsatzTV.Application/Emby/Commands/SynchronizeEmbyLibrariesHandler.cs index f45065233..4ac5ca56a 100644 --- a/ErsatzTV.Application/Emby/Commands/SynchronizeEmbyLibrariesHandler.cs +++ b/ErsatzTV.Application/Emby/Commands/SynchronizeEmbyLibrariesHandler.cs @@ -1,118 +1,111 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Emby; using ErsatzTV.Core.Interfaces.Emby; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Search; -using LanguageExt; using Microsoft.Extensions.Logging; -using static LanguageExt.Prelude; -namespace ErsatzTV.Application.Emby.Commands +namespace ErsatzTV.Application.Emby; + +public class + SynchronizeEmbyLibrariesHandler : MediatR.IRequestHandler> { - public class - SynchronizeEmbyLibrariesHandler : MediatR.IRequestHandler> + private readonly IEmbyApiClient _embyApiClient; + private readonly IEmbySecretStore _embySecretStore; + private readonly ILogger _logger; + private readonly IMediaSourceRepository _mediaSourceRepository; + private readonly ISearchIndex _searchIndex; + + public SynchronizeEmbyLibrariesHandler( + IMediaSourceRepository mediaSourceRepository, + IEmbySecretStore embySecretStore, + IEmbyApiClient embyApiClient, + ILogger logger, + ISearchIndex searchIndex) { - private readonly IEmbyApiClient _embyApiClient; - private readonly IEmbySecretStore _embySecretStore; - private readonly ILogger _logger; - private readonly IMediaSourceRepository _mediaSourceRepository; - private readonly ISearchIndex _searchIndex; - - public SynchronizeEmbyLibrariesHandler( - IMediaSourceRepository mediaSourceRepository, - IEmbySecretStore embySecretStore, - IEmbyApiClient embyApiClient, - ILogger logger, - ISearchIndex searchIndex) - { - _mediaSourceRepository = mediaSourceRepository; - _embySecretStore = embySecretStore; - _embyApiClient = embyApiClient; - _logger = logger; - _searchIndex = searchIndex; - } - - public Task> Handle( - SynchronizeEmbyLibraries request, - CancellationToken cancellationToken) => - Validate(request) - .MapT(SynchronizeLibraries) - .Bind(v => v.ToEitherAsync()); - - private Task> Validate(SynchronizeEmbyLibraries request) => - MediaSourceMustExist(request) - .BindT(MediaSourceMustHaveActiveConnection) - .BindT(MediaSourceMustHaveApiKey); - - private Task> MediaSourceMustExist( - SynchronizeEmbyLibraries request) => - _mediaSourceRepository.GetEmby(request.EmbyMediaSourceId) - .Map(o => o.ToValidation("Emby media source does not exist.")); - - private Validation MediaSourceMustHaveActiveConnection( - EmbyMediaSource embyMediaSource) - { - Option maybeConnection = embyMediaSource.Connections.HeadOrNone(); - return maybeConnection.Map(connection => new ConnectionParameters(embyMediaSource, connection)) - .ToValidation("Emby media source requires an active connection"); - } - - private async Task> MediaSourceMustHaveApiKey( - ConnectionParameters connectionParameters) - { - EmbySecrets secrets = await _embySecretStore.ReadSecrets(); - return Optional(secrets.Address == connectionParameters.ActiveConnection.Address) - .Where(match => match) - .Map(_ => connectionParameters with { ApiKey = secrets.ApiKey }) - .ToValidation("Emby media source requires an api key"); - } - - private async Task SynchronizeLibraries(ConnectionParameters connectionParameters) - { - Either> maybeLibraries = await _embyApiClient.GetLibraries( - connectionParameters.ActiveConnection.Address, - connectionParameters.ApiKey); - - await maybeLibraries.Match( - async libraries => - { - var existing = connectionParameters.EmbyMediaSource.Libraries.OfType() - .ToList(); - var toAdd = libraries.Filter(library => existing.All(l => l.ItemId != library.ItemId)).ToList(); - var toRemove = existing.Filter(library => libraries.All(l => l.ItemId != library.ItemId)).ToList(); - List ids = await _mediaSourceRepository.UpdateLibraries( - connectionParameters.EmbyMediaSource.Id, - toAdd, - toRemove); - if (ids.Any()) - { - await _searchIndex.RemoveItems(ids); - _searchIndex.Commit(); - } - }, - error => - { - _logger.LogWarning( - "Unable to synchronize libraries from emby server {EmbyServer}: {Error}", - connectionParameters.EmbyMediaSource.ServerName, - error.Value); - - return Task.CompletedTask; - }); - - return Unit.Default; - } - - private record ConnectionParameters( - EmbyMediaSource EmbyMediaSource, - EmbyConnection ActiveConnection) - { - public string ApiKey { get; set; } - } + _mediaSourceRepository = mediaSourceRepository; + _embySecretStore = embySecretStore; + _embyApiClient = embyApiClient; + _logger = logger; + _searchIndex = searchIndex; } -} + + public Task> Handle( + SynchronizeEmbyLibraries request, + CancellationToken cancellationToken) => + Validate(request) + .MapT(SynchronizeLibraries) + .Bind(v => v.ToEitherAsync()); + + private Task> Validate(SynchronizeEmbyLibraries request) => + MediaSourceMustExist(request) + .BindT(MediaSourceMustHaveActiveConnection) + .BindT(MediaSourceMustHaveApiKey); + + private Task> MediaSourceMustExist( + SynchronizeEmbyLibraries request) => + _mediaSourceRepository.GetEmby(request.EmbyMediaSourceId) + .Map(o => o.ToValidation("Emby media source does not exist.")); + + private Validation MediaSourceMustHaveActiveConnection( + EmbyMediaSource embyMediaSource) + { + Option maybeConnection = embyMediaSource.Connections.HeadOrNone(); + return maybeConnection.Map(connection => new ConnectionParameters(embyMediaSource, connection)) + .ToValidation("Emby media source requires an active connection"); + } + + private async Task> MediaSourceMustHaveApiKey( + ConnectionParameters connectionParameters) + { + EmbySecrets secrets = await _embySecretStore.ReadSecrets(); + return Optional(secrets.Address == connectionParameters.ActiveConnection.Address) + .Where(match => match) + .Map(_ => connectionParameters with { ApiKey = secrets.ApiKey }) + .ToValidation("Emby media source requires an api key"); + } + + private async Task SynchronizeLibraries(ConnectionParameters connectionParameters) + { + Either> maybeLibraries = await _embyApiClient.GetLibraries( + connectionParameters.ActiveConnection.Address, + connectionParameters.ApiKey); + + await maybeLibraries.Match( + async libraries => + { + var existing = connectionParameters.EmbyMediaSource.Libraries.OfType() + .ToList(); + var toAdd = libraries.Filter(library => existing.All(l => l.ItemId != library.ItemId)).ToList(); + var toRemove = existing.Filter(library => libraries.All(l => l.ItemId != library.ItemId)).ToList(); + List ids = await _mediaSourceRepository.UpdateLibraries( + connectionParameters.EmbyMediaSource.Id, + toAdd, + toRemove); + if (ids.Any()) + { + await _searchIndex.RemoveItems(ids); + _searchIndex.Commit(); + } + }, + error => + { + _logger.LogWarning( + "Unable to synchronize libraries from emby server {EmbyServer}: {Error}", + connectionParameters.EmbyMediaSource.ServerName, + error.Value); + + return Task.CompletedTask; + }); + + return Unit.Default; + } + + private record ConnectionParameters( + EmbyMediaSource EmbyMediaSource, + EmbyConnection ActiveConnection) + { + public string ApiKey { get; set; } + } +} \ No newline at end of file diff --git a/ErsatzTV.Application/Emby/Commands/SynchronizeEmbyLibraryById.cs b/ErsatzTV.Application/Emby/Commands/SynchronizeEmbyLibraryById.cs index 7cadcc141..1901e0b45 100644 --- a/ErsatzTV.Application/Emby/Commands/SynchronizeEmbyLibraryById.cs +++ b/ErsatzTV.Application/Emby/Commands/SynchronizeEmbyLibraryById.cs @@ -1,23 +1,20 @@ using ErsatzTV.Core; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.Emby.Commands +namespace ErsatzTV.Application.Emby; + +public interface ISynchronizeEmbyLibraryById : IRequest>, + IEmbyBackgroundServiceRequest { - public interface ISynchronizeEmbyLibraryById : IRequest>, - IEmbyBackgroundServiceRequest - { - int EmbyLibraryId { get; } - bool ForceScan { get; } - } - - public record SynchronizeEmbyLibraryByIdIfNeeded(int EmbyLibraryId) : ISynchronizeEmbyLibraryById - { - public bool ForceScan => false; - } - - public record ForceSynchronizeEmbyLibraryById(int EmbyLibraryId) : ISynchronizeEmbyLibraryById - { - public bool ForceScan => true; - } + int EmbyLibraryId { get; } + bool ForceScan { get; } } + +public record SynchronizeEmbyLibraryByIdIfNeeded(int EmbyLibraryId) : ISynchronizeEmbyLibraryById +{ + public bool ForceScan => false; +} + +public record ForceSynchronizeEmbyLibraryById(int EmbyLibraryId) : ISynchronizeEmbyLibraryById +{ + public bool ForceScan => true; +} \ No newline at end of file diff --git a/ErsatzTV.Application/Emby/Commands/SynchronizeEmbyLibraryByIdHandler.cs b/ErsatzTV.Application/Emby/Commands/SynchronizeEmbyLibraryByIdHandler.cs index 36fd9cde7..76770d6a5 100644 --- a/ErsatzTV.Application/Emby/Commands/SynchronizeEmbyLibraryByIdHandler.cs +++ b/ErsatzTV.Application/Emby/Commands/SynchronizeEmbyLibraryByIdHandler.cs @@ -1,181 +1,172 @@ -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Emby; using ErsatzTV.Core.Interfaces.Emby; using ErsatzTV.Core.Interfaces.Locking; using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -using MediatR; using Microsoft.Extensions.Logging; -using static LanguageExt.Prelude; -using Unit = LanguageExt.Unit; -namespace ErsatzTV.Application.Emby.Commands +namespace ErsatzTV.Application.Emby; + +public class SynchronizeEmbyLibraryByIdHandler : + IRequestHandler>, + IRequestHandler> { - public class SynchronizeEmbyLibraryByIdHandler : - IRequestHandler>, - IRequestHandler> + private readonly IConfigElementRepository _configElementRepository; + private readonly IEmbyMovieLibraryScanner _embyMovieLibraryScanner; + + private readonly IEmbySecretStore _embySecretStore; + private readonly IEmbyTelevisionLibraryScanner _embyTelevisionLibraryScanner; + private readonly IEntityLocker _entityLocker; + private readonly ILibraryRepository _libraryRepository; + private readonly ILogger _logger; + + private readonly IMediaSourceRepository _mediaSourceRepository; + + public SynchronizeEmbyLibraryByIdHandler( + IMediaSourceRepository mediaSourceRepository, + IEmbySecretStore embySecretStore, + IEmbyMovieLibraryScanner embyMovieLibraryScanner, + IEmbyTelevisionLibraryScanner embyTelevisionLibraryScanner, + ILibraryRepository libraryRepository, + IEntityLocker entityLocker, + IConfigElementRepository configElementRepository, + ILogger logger) { - private readonly IConfigElementRepository _configElementRepository; - private readonly IEmbyMovieLibraryScanner _embyMovieLibraryScanner; - - private readonly IEmbySecretStore _embySecretStore; - private readonly IEmbyTelevisionLibraryScanner _embyTelevisionLibraryScanner; - private readonly IEntityLocker _entityLocker; - private readonly ILibraryRepository _libraryRepository; - private readonly ILogger _logger; - - private readonly IMediaSourceRepository _mediaSourceRepository; - - public SynchronizeEmbyLibraryByIdHandler( - IMediaSourceRepository mediaSourceRepository, - IEmbySecretStore embySecretStore, - IEmbyMovieLibraryScanner embyMovieLibraryScanner, - IEmbyTelevisionLibraryScanner embyTelevisionLibraryScanner, - ILibraryRepository libraryRepository, - IEntityLocker entityLocker, - IConfigElementRepository configElementRepository, - ILogger logger) - { - _mediaSourceRepository = mediaSourceRepository; - _embySecretStore = embySecretStore; - _embyMovieLibraryScanner = embyMovieLibraryScanner; - _embyTelevisionLibraryScanner = embyTelevisionLibraryScanner; - _libraryRepository = libraryRepository; - _entityLocker = entityLocker; - _configElementRepository = configElementRepository; - _logger = logger; - } - - public Task> Handle( - ForceSynchronizeEmbyLibraryById request, - CancellationToken cancellationToken) => Handle(request); - - public Task> Handle( - SynchronizeEmbyLibraryByIdIfNeeded request, - CancellationToken cancellationToken) => Handle(request); - - private Task> - Handle(ISynchronizeEmbyLibraryById request) => - Validate(request) - .MapT(parameters => Synchronize(parameters).Map(_ => parameters.Library.Name)) - .Bind(v => v.ToEitherAsync()); - - private async Task Synchronize(RequestParameters parameters) - { - var lastScan = new DateTimeOffset(parameters.Library.LastScan ?? SystemTime.MinValueUtc, TimeSpan.Zero); - DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(parameters.LibraryRefreshInterval); - if (parameters.ForceScan || nextScan < DateTimeOffset.Now) - { - switch (parameters.Library.MediaKind) - { - case LibraryMediaKind.Movies: - await _embyMovieLibraryScanner.ScanLibrary( - parameters.ConnectionParameters.ActiveConnection.Address, - parameters.ConnectionParameters.ApiKey, - parameters.Library, - parameters.FFprobePath); - break; - case LibraryMediaKind.Shows: - await _embyTelevisionLibraryScanner.ScanLibrary( - parameters.ConnectionParameters.ActiveConnection.Address, - parameters.ConnectionParameters.ApiKey, - parameters.Library, - parameters.FFprobePath); - break; - } - - parameters.Library.LastScan = DateTime.UtcNow; - await _libraryRepository.UpdateLastScan(parameters.Library); - } - else - { - _logger.LogDebug( - "Skipping unforced scan of emby media library {Name}", - parameters.Library.Name); - } - - _entityLocker.UnlockLibrary(parameters.Library.Id); - return Unit.Default; - } - - private async Task> Validate( - ISynchronizeEmbyLibraryById request) => - (await ValidateConnection(request), await EmbyLibraryMustExist(request), - await ValidateLibraryRefreshInterval(), await ValidateFFprobePath()) - .Apply( - (connectionParameters, embyLibrary, libraryRefreshInterval, ffprobePath) => new RequestParameters( - connectionParameters, - embyLibrary, - request.ForceScan, - libraryRefreshInterval, - ffprobePath - )); - - private Task> ValidateConnection( - ISynchronizeEmbyLibraryById request) => - EmbyMediaSourceMustExist(request) - .BindT(MediaSourceMustHaveActiveConnection) - .BindT(MediaSourceMustHaveApiKey); - - private Task> EmbyMediaSourceMustExist( - ISynchronizeEmbyLibraryById request) => - _mediaSourceRepository.GetEmbyByLibraryId(request.EmbyLibraryId) - .Map( - v => v.ToValidation( - $"Emby media source for library {request.EmbyLibraryId} does not exist.")); - - private Validation MediaSourceMustHaveActiveConnection( - EmbyMediaSource embyMediaSource) - { - Option maybeConnection = embyMediaSource.Connections.HeadOrNone(); - return maybeConnection.Map(connection => new ConnectionParameters(embyMediaSource, connection)) - .ToValidation("Emby media source requires an active connection"); - } - - private async Task> MediaSourceMustHaveApiKey( - ConnectionParameters connectionParameters) - { - EmbySecrets secrets = await _embySecretStore.ReadSecrets(); - return Optional(secrets.Address == connectionParameters.ActiveConnection.Address) - .Where(match => match) - .Map(_ => connectionParameters with { ApiKey = secrets.ApiKey }) - .ToValidation("Emby media source requires an api key"); - } - - private Task> EmbyLibraryMustExist( - ISynchronizeEmbyLibraryById request) => - _mediaSourceRepository.GetEmbyLibrary(request.EmbyLibraryId) - .Map(v => v.ToValidation($"Emby library {request.EmbyLibraryId} does not exist.")); - - private Task> ValidateLibraryRefreshInterval() => - _configElementRepository.GetValue(ConfigElementKey.LibraryRefreshInterval) - .FilterT(lri => lri > 0) - .Map(lri => lri.ToValidation("Library refresh interval is invalid")); - - private Task> ValidateFFprobePath() => - _configElementRepository.GetValue(ConfigElementKey.FFprobePath) - .FilterT(File.Exists) - .Map( - ffprobePath => - ffprobePath.ToValidation("FFprobe path does not exist on the file system")); - - private record RequestParameters( - ConnectionParameters ConnectionParameters, - EmbyLibrary Library, - bool ForceScan, - int LibraryRefreshInterval, - string FFprobePath); - - private record ConnectionParameters( - EmbyMediaSource EmbyMediaSource, - EmbyConnection ActiveConnection) - { - public string ApiKey { get; set; } - } + _mediaSourceRepository = mediaSourceRepository; + _embySecretStore = embySecretStore; + _embyMovieLibraryScanner = embyMovieLibraryScanner; + _embyTelevisionLibraryScanner = embyTelevisionLibraryScanner; + _libraryRepository = libraryRepository; + _entityLocker = entityLocker; + _configElementRepository = configElementRepository; + _logger = logger; } -} + + public Task> Handle( + ForceSynchronizeEmbyLibraryById request, + CancellationToken cancellationToken) => Handle(request); + + public Task> Handle( + SynchronizeEmbyLibraryByIdIfNeeded request, + CancellationToken cancellationToken) => Handle(request); + + private Task> + Handle(ISynchronizeEmbyLibraryById request) => + Validate(request) + .MapT(parameters => Synchronize(parameters).Map(_ => parameters.Library.Name)) + .Bind(v => v.ToEitherAsync()); + + private async Task Synchronize(RequestParameters parameters) + { + var lastScan = new DateTimeOffset(parameters.Library.LastScan ?? SystemTime.MinValueUtc, TimeSpan.Zero); + DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(parameters.LibraryRefreshInterval); + if (parameters.ForceScan || nextScan < DateTimeOffset.Now) + { + switch (parameters.Library.MediaKind) + { + case LibraryMediaKind.Movies: + await _embyMovieLibraryScanner.ScanLibrary( + parameters.ConnectionParameters.ActiveConnection.Address, + parameters.ConnectionParameters.ApiKey, + parameters.Library, + parameters.FFprobePath); + break; + case LibraryMediaKind.Shows: + await _embyTelevisionLibraryScanner.ScanLibrary( + parameters.ConnectionParameters.ActiveConnection.Address, + parameters.ConnectionParameters.ApiKey, + parameters.Library, + parameters.FFprobePath); + break; + } + + parameters.Library.LastScan = DateTime.UtcNow; + await _libraryRepository.UpdateLastScan(parameters.Library); + } + else + { + _logger.LogDebug( + "Skipping unforced scan of emby media library {Name}", + parameters.Library.Name); + } + + _entityLocker.UnlockLibrary(parameters.Library.Id); + return Unit.Default; + } + + private async Task> Validate( + ISynchronizeEmbyLibraryById request) => + (await ValidateConnection(request), await EmbyLibraryMustExist(request), + await ValidateLibraryRefreshInterval(), await ValidateFFprobePath()) + .Apply( + (connectionParameters, embyLibrary, libraryRefreshInterval, ffprobePath) => new RequestParameters( + connectionParameters, + embyLibrary, + request.ForceScan, + libraryRefreshInterval, + ffprobePath + )); + + private Task> ValidateConnection( + ISynchronizeEmbyLibraryById request) => + EmbyMediaSourceMustExist(request) + .BindT(MediaSourceMustHaveActiveConnection) + .BindT(MediaSourceMustHaveApiKey); + + private Task> EmbyMediaSourceMustExist( + ISynchronizeEmbyLibraryById request) => + _mediaSourceRepository.GetEmbyByLibraryId(request.EmbyLibraryId) + .Map( + v => v.ToValidation( + $"Emby media source for library {request.EmbyLibraryId} does not exist.")); + + private Validation MediaSourceMustHaveActiveConnection( + EmbyMediaSource embyMediaSource) + { + Option maybeConnection = embyMediaSource.Connections.HeadOrNone(); + return maybeConnection.Map(connection => new ConnectionParameters(embyMediaSource, connection)) + .ToValidation("Emby media source requires an active connection"); + } + + private async Task> MediaSourceMustHaveApiKey( + ConnectionParameters connectionParameters) + { + EmbySecrets secrets = await _embySecretStore.ReadSecrets(); + return Optional(secrets.Address == connectionParameters.ActiveConnection.Address) + .Where(match => match) + .Map(_ => connectionParameters with { ApiKey = secrets.ApiKey }) + .ToValidation("Emby media source requires an api key"); + } + + private Task> EmbyLibraryMustExist( + ISynchronizeEmbyLibraryById request) => + _mediaSourceRepository.GetEmbyLibrary(request.EmbyLibraryId) + .Map(v => v.ToValidation($"Emby library {request.EmbyLibraryId} does not exist.")); + + private Task> ValidateLibraryRefreshInterval() => + _configElementRepository.GetValue(ConfigElementKey.LibraryRefreshInterval) + .FilterT(lri => lri > 0) + .Map(lri => lri.ToValidation("Library refresh interval is invalid")); + + private Task> ValidateFFprobePath() => + _configElementRepository.GetValue(ConfigElementKey.FFprobePath) + .FilterT(File.Exists) + .Map( + ffprobePath => + ffprobePath.ToValidation("FFprobe path does not exist on the file system")); + + private record RequestParameters( + ConnectionParameters ConnectionParameters, + EmbyLibrary Library, + bool ForceScan, + int LibraryRefreshInterval, + string FFprobePath); + + private record ConnectionParameters( + EmbyMediaSource EmbyMediaSource, + EmbyConnection ActiveConnection) + { + public string ApiKey { get; set; } + } +} \ No newline at end of file diff --git a/ErsatzTV.Application/Emby/Commands/SynchronizeEmbyMediaSources.cs b/ErsatzTV.Application/Emby/Commands/SynchronizeEmbyMediaSources.cs index 2ab43890b..0f162f168 100644 --- a/ErsatzTV.Application/Emby/Commands/SynchronizeEmbyMediaSources.cs +++ b/ErsatzTV.Application/Emby/Commands/SynchronizeEmbyMediaSources.cs @@ -1,11 +1,7 @@ -using System.Collections.Generic; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.Emby.Commands -{ - public record SynchronizeEmbyMediaSources : IRequest>>, - IEmbyBackgroundServiceRequest; -} +namespace ErsatzTV.Application.Emby; + +public record SynchronizeEmbyMediaSources : IRequest>>, + IEmbyBackgroundServiceRequest; \ No newline at end of file diff --git a/ErsatzTV.Application/Emby/Commands/SynchronizeEmbyMediaSourcesHandler.cs b/ErsatzTV.Application/Emby/Commands/SynchronizeEmbyMediaSourcesHandler.cs index 8a0fc9cb9..7750c6e05 100644 --- a/ErsatzTV.Application/Emby/Commands/SynchronizeEmbyMediaSourcesHandler.cs +++ b/ErsatzTV.Application/Emby/Commands/SynchronizeEmbyMediaSourcesHandler.cs @@ -1,41 +1,35 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Channels; -using System.Threading.Tasks; +using System.Threading.Channels; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.Emby.Commands +namespace ErsatzTV.Application.Emby; + +public class SynchronizeEmbyMediaSourcesHandler : IRequestHandler>> { - public class SynchronizeEmbyMediaSourcesHandler : IRequestHandler>> + private readonly ChannelWriter _channel; + private readonly IMediaSourceRepository _mediaSourceRepository; + + public SynchronizeEmbyMediaSourcesHandler( + IMediaSourceRepository mediaSourceRepository, + ChannelWriter channel) { - private readonly ChannelWriter _channel; - private readonly IMediaSourceRepository _mediaSourceRepository; - - public SynchronizeEmbyMediaSourcesHandler( - IMediaSourceRepository mediaSourceRepository, - ChannelWriter channel) - { - _mediaSourceRepository = mediaSourceRepository; - _channel = channel; - } - - public async Task>> Handle( - SynchronizeEmbyMediaSources request, - CancellationToken cancellationToken) - { - List mediaSources = await _mediaSourceRepository.GetAllEmby(); - foreach (EmbyMediaSource mediaSource in mediaSources) - { - // await _channel.WriteAsync(new SynchronizeEmbyAdminUserId(mediaSource.Id), cancellationToken); - await _channel.WriteAsync(new SynchronizeEmbyLibraries(mediaSource.Id), cancellationToken); - } - - return mediaSources; - } + _mediaSourceRepository = mediaSourceRepository; + _channel = channel; } -} + + public async Task>> Handle( + SynchronizeEmbyMediaSources request, + CancellationToken cancellationToken) + { + List mediaSources = await _mediaSourceRepository.GetAllEmby(); + foreach (EmbyMediaSource mediaSource in mediaSources) + { + // await _channel.WriteAsync(new SynchronizeEmbyAdminUserId(mediaSource.Id), cancellationToken); + await _channel.WriteAsync(new SynchronizeEmbyLibraries(mediaSource.Id), cancellationToken); + } + + return mediaSources; + } +} \ No newline at end of file diff --git a/ErsatzTV.Application/Emby/Commands/UpdateEmbyLibraryPreferences.cs b/ErsatzTV.Application/Emby/Commands/UpdateEmbyLibraryPreferences.cs index 0ce00cad2..5b11533da 100644 --- a/ErsatzTV.Application/Emby/Commands/UpdateEmbyLibraryPreferences.cs +++ b/ErsatzTV.Application/Emby/Commands/UpdateEmbyLibraryPreferences.cs @@ -1,11 +1,8 @@ -using System.Collections.Generic; -using ErsatzTV.Core; -using LanguageExt; +using ErsatzTV.Core; -namespace ErsatzTV.Application.Emby.Commands -{ - public record UpdateEmbyLibraryPreferences - (List Preferences) : MediatR.IRequest>; +namespace ErsatzTV.Application.Emby; - public record EmbyLibraryPreference(int Id, bool ShouldSyncItems); -} +public record UpdateEmbyLibraryPreferences + (List Preferences) : MediatR.IRequest>; + +public record EmbyLibraryPreference(int Id, bool ShouldSyncItems); \ No newline at end of file diff --git a/ErsatzTV.Application/Emby/Commands/UpdateEmbyLibraryPreferencesHandler.cs b/ErsatzTV.Application/Emby/Commands/UpdateEmbyLibraryPreferencesHandler.cs index 273f320b2..556f5d3c9 100644 --- a/ErsatzTV.Application/Emby/Commands/UpdateEmbyLibraryPreferencesHandler.cs +++ b/ErsatzTV.Application/Emby/Commands/UpdateEmbyLibraryPreferencesHandler.cs @@ -1,42 +1,36 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Search; -using LanguageExt; -namespace ErsatzTV.Application.Emby.Commands +namespace ErsatzTV.Application.Emby; + +public class + UpdateEmbyLibraryPreferencesHandler : MediatR.IRequestHandler> { - public class - UpdateEmbyLibraryPreferencesHandler : MediatR.IRequestHandler> + private readonly IMediaSourceRepository _mediaSourceRepository; + private readonly ISearchIndex _searchIndex; + + public UpdateEmbyLibraryPreferencesHandler( + IMediaSourceRepository mediaSourceRepository, + ISearchIndex searchIndex) { - private readonly IMediaSourceRepository _mediaSourceRepository; - private readonly ISearchIndex _searchIndex; - - public UpdateEmbyLibraryPreferencesHandler( - IMediaSourceRepository mediaSourceRepository, - ISearchIndex searchIndex) - { - _mediaSourceRepository = mediaSourceRepository; - _searchIndex = searchIndex; - } - - public async Task> Handle( - UpdateEmbyLibraryPreferences request, - CancellationToken cancellationToken) - { - var toDisable = request.Preferences.Filter(p => p.ShouldSyncItems == false).Map(p => p.Id).ToList(); - List ids = await _mediaSourceRepository.DisableEmbyLibrarySync(toDisable); - await _searchIndex.RemoveItems(ids); - _searchIndex.Commit(); - - IEnumerable toEnable = request.Preferences.Filter(p => p.ShouldSyncItems).Map(p => p.Id); - await _mediaSourceRepository.EnableEmbyLibrarySync(toEnable); - - return Unit.Default; - } + _mediaSourceRepository = mediaSourceRepository; + _searchIndex = searchIndex; } -} + + public async Task> Handle( + UpdateEmbyLibraryPreferences request, + CancellationToken cancellationToken) + { + var toDisable = request.Preferences.Filter(p => p.ShouldSyncItems == false).Map(p => p.Id).ToList(); + List ids = await _mediaSourceRepository.DisableEmbyLibrarySync(toDisable); + await _searchIndex.RemoveItems(ids); + _searchIndex.Commit(); + + IEnumerable toEnable = request.Preferences.Filter(p => p.ShouldSyncItems).Map(p => p.Id); + await _mediaSourceRepository.EnableEmbyLibrarySync(toEnable); + + return Unit.Default; + } +} \ No newline at end of file diff --git a/ErsatzTV.Application/Emby/Commands/UpdateEmbyPathReplacements.cs b/ErsatzTV.Application/Emby/Commands/UpdateEmbyPathReplacements.cs index 6626cc5e4..90bdf1ed4 100644 --- a/ErsatzTV.Application/Emby/Commands/UpdateEmbyPathReplacements.cs +++ b/ErsatzTV.Application/Emby/Commands/UpdateEmbyPathReplacements.cs @@ -1,12 +1,9 @@ -using System.Collections.Generic; -using ErsatzTV.Core; -using LanguageExt; +using ErsatzTV.Core; -namespace ErsatzTV.Application.Emby.Commands -{ - public record UpdateEmbyPathReplacements( - int EmbyMediaSourceId, - List PathReplacements) : MediatR.IRequest>; +namespace ErsatzTV.Application.Emby; - public record EmbyPathReplacementItem(int Id, string EmbyPath, string LocalPath); -} +public record UpdateEmbyPathReplacements( + int EmbyMediaSourceId, + List PathReplacements) : MediatR.IRequest>; + +public record EmbyPathReplacementItem(int Id, string EmbyPath, string LocalPath); \ No newline at end of file diff --git a/ErsatzTV.Application/Emby/Commands/UpdateEmbyPathReplacementsHandler.cs b/ErsatzTV.Application/Emby/Commands/UpdateEmbyPathReplacementsHandler.cs index fe0af60d4..212b2ae72 100644 --- a/ErsatzTV.Application/Emby/Commands/UpdateEmbyPathReplacementsHandler.cs +++ b/ErsatzTV.Application/Emby/Commands/UpdateEmbyPathReplacementsHandler.cs @@ -1,55 +1,49 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -namespace ErsatzTV.Application.Emby.Commands +namespace ErsatzTV.Application.Emby; + +public class UpdateEmbyPathReplacementsHandler : MediatR.IRequestHandler> { - public class UpdateEmbyPathReplacementsHandler : MediatR.IRequestHandler> + private readonly IMediaSourceRepository _mediaSourceRepository; + + public UpdateEmbyPathReplacementsHandler(IMediaSourceRepository mediaSourceRepository) => + _mediaSourceRepository = mediaSourceRepository; + + public Task> Handle( + UpdateEmbyPathReplacements request, + CancellationToken cancellationToken) => + Validate(request) + .MapT(pms => MergePathReplacements(request, pms)) + .Bind(v => v.ToEitherAsync()); + + private Task MergePathReplacements( + UpdateEmbyPathReplacements request, + EmbyMediaSource embyMediaSource) { - private readonly IMediaSourceRepository _mediaSourceRepository; + embyMediaSource.PathReplacements ??= new List(); - public UpdateEmbyPathReplacementsHandler(IMediaSourceRepository mediaSourceRepository) => - _mediaSourceRepository = mediaSourceRepository; + var incoming = request.PathReplacements.Map(Project).ToList(); - public Task> Handle( - UpdateEmbyPathReplacements request, - CancellationToken cancellationToken) => - Validate(request) - .MapT(pms => MergePathReplacements(request, pms)) - .Bind(v => v.ToEitherAsync()); + var toAdd = incoming.Filter(r => r.Id < 1).ToList(); + var toRemove = embyMediaSource.PathReplacements.Filter(r => incoming.All(pr => pr.Id != r.Id)).ToList(); + var toUpdate = incoming.Except(toAdd).ToList(); - private Task MergePathReplacements( - UpdateEmbyPathReplacements request, - EmbyMediaSource embyMediaSource) - { - embyMediaSource.PathReplacements ??= new List(); - - var incoming = request.PathReplacements.Map(Project).ToList(); - - var toAdd = incoming.Filter(r => r.Id < 1).ToList(); - var toRemove = embyMediaSource.PathReplacements.Filter(r => incoming.All(pr => pr.Id != r.Id)).ToList(); - var toUpdate = incoming.Except(toAdd).ToList(); - - return _mediaSourceRepository.UpdatePathReplacements(embyMediaSource.Id, toAdd, toUpdate, toRemove); - } - - private static EmbyPathReplacement Project(EmbyPathReplacementItem vm) => - new() { Id = vm.Id, EmbyPath = vm.EmbyPath, LocalPath = vm.LocalPath }; - - private Task> Validate(UpdateEmbyPathReplacements request) => - EmbyMediaSourceMustExist(request); - - private Task> EmbyMediaSourceMustExist( - UpdateEmbyPathReplacements request) => - _mediaSourceRepository.GetEmby(request.EmbyMediaSourceId) - .Map( - v => v.ToValidation( - $"Emby media source {request.EmbyMediaSourceId} does not exist.")); + return _mediaSourceRepository.UpdatePathReplacements(embyMediaSource.Id, toAdd, toUpdate, toRemove); } -} + + private static EmbyPathReplacement Project(EmbyPathReplacementItem vm) => + new() { Id = vm.Id, EmbyPath = vm.EmbyPath, LocalPath = vm.LocalPath }; + + private Task> Validate(UpdateEmbyPathReplacements request) => + EmbyMediaSourceMustExist(request); + + private Task> EmbyMediaSourceMustExist( + UpdateEmbyPathReplacements request) => + _mediaSourceRepository.GetEmby(request.EmbyMediaSourceId) + .Map( + v => v.ToValidation( + $"Emby media source {request.EmbyMediaSourceId} does not exist.")); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Emby/EmbyConnectionParametersViewModel.cs b/ErsatzTV.Application/Emby/EmbyConnectionParametersViewModel.cs index 579662dfd..73649b6d8 100644 --- a/ErsatzTV.Application/Emby/EmbyConnectionParametersViewModel.cs +++ b/ErsatzTV.Application/Emby/EmbyConnectionParametersViewModel.cs @@ -1,4 +1,3 @@ -namespace ErsatzTV.Application.Emby -{ - public record EmbyConnectionParametersViewModel(string Address); -} +namespace ErsatzTV.Application.Emby; + +public record EmbyConnectionParametersViewModel(string Address); \ No newline at end of file diff --git a/ErsatzTV.Application/Emby/EmbyLibraryViewModel.cs b/ErsatzTV.Application/Emby/EmbyLibraryViewModel.cs index afde64b28..af6e703ed 100644 --- a/ErsatzTV.Application/Emby/EmbyLibraryViewModel.cs +++ b/ErsatzTV.Application/Emby/EmbyLibraryViewModel.cs @@ -1,8 +1,7 @@ using ErsatzTV.Application.Libraries; using ErsatzTV.Core.Domain; -namespace ErsatzTV.Application.Emby -{ - public record EmbyLibraryViewModel(int Id, string Name, LibraryMediaKind MediaKind, bool ShouldSyncItems) - : LibraryViewModel("Emby", Id, Name, MediaKind); -} +namespace ErsatzTV.Application.Emby; + +public record EmbyLibraryViewModel(int Id, string Name, LibraryMediaKind MediaKind, bool ShouldSyncItems) + : LibraryViewModel("Emby", Id, Name, MediaKind); \ No newline at end of file diff --git a/ErsatzTV.Application/Emby/EmbyMediaSourceViewModel.cs b/ErsatzTV.Application/Emby/EmbyMediaSourceViewModel.cs index b78a10c42..434530b4d 100644 --- a/ErsatzTV.Application/Emby/EmbyMediaSourceViewModel.cs +++ b/ErsatzTV.Application/Emby/EmbyMediaSourceViewModel.cs @@ -1,9 +1,8 @@ using ErsatzTV.Application.MediaSources; -namespace ErsatzTV.Application.Emby -{ - public record EmbyMediaSourceViewModel(int Id, string Name, string Address) : RemoteMediaSourceViewModel( - Id, - Name, - Address); -} +namespace ErsatzTV.Application.Emby; + +public record EmbyMediaSourceViewModel(int Id, string Name, string Address) : RemoteMediaSourceViewModel( + Id, + Name, + Address); \ No newline at end of file diff --git a/ErsatzTV.Application/Emby/EmbyPathReplacementViewModel.cs b/ErsatzTV.Application/Emby/EmbyPathReplacementViewModel.cs index e6da43b0c..da490cb7a 100644 --- a/ErsatzTV.Application/Emby/EmbyPathReplacementViewModel.cs +++ b/ErsatzTV.Application/Emby/EmbyPathReplacementViewModel.cs @@ -1,4 +1,3 @@ -namespace ErsatzTV.Application.Emby -{ - public record EmbyPathReplacementViewModel(int Id, string EmbyPath, string LocalPath); -} +namespace ErsatzTV.Application.Emby; + +public record EmbyPathReplacementViewModel(int Id, string EmbyPath, string LocalPath); \ No newline at end of file diff --git a/ErsatzTV.Application/Emby/Mapper.cs b/ErsatzTV.Application/Emby/Mapper.cs index 512edbab4..528f7bbb4 100644 --- a/ErsatzTV.Application/Emby/Mapper.cs +++ b/ErsatzTV.Application/Emby/Mapper.cs @@ -1,19 +1,18 @@ using ErsatzTV.Core.Domain; -namespace ErsatzTV.Application.Emby +namespace ErsatzTV.Application.Emby; + +internal static class Mapper { - internal static class Mapper - { - internal static EmbyMediaSourceViewModel ProjectToViewModel(EmbyMediaSource embyMediaSource) => - new( - embyMediaSource.Id, - embyMediaSource.ServerName, - embyMediaSource.Connections.HeadOrNone().Match(c => c.Address, string.Empty)); + internal static EmbyMediaSourceViewModel ProjectToViewModel(EmbyMediaSource embyMediaSource) => + new( + embyMediaSource.Id, + embyMediaSource.ServerName, + embyMediaSource.Connections.HeadOrNone().Match(c => c.Address, string.Empty)); - internal static EmbyLibraryViewModel ProjectToViewModel(EmbyLibrary library) => - new(library.Id, library.Name, library.MediaKind, library.ShouldSyncItems); + internal static EmbyLibraryViewModel ProjectToViewModel(EmbyLibrary library) => + new(library.Id, library.Name, library.MediaKind, library.ShouldSyncItems); - internal static EmbyPathReplacementViewModel ProjectToViewModel(EmbyPathReplacement pathReplacement) => - new(pathReplacement.Id, pathReplacement.EmbyPath, pathReplacement.LocalPath); - } -} + internal static EmbyPathReplacementViewModel ProjectToViewModel(EmbyPathReplacement pathReplacement) => + new(pathReplacement.Id, pathReplacement.EmbyPath, pathReplacement.LocalPath); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Emby/Queries/GetAllEmbyMediaSources.cs b/ErsatzTV.Application/Emby/Queries/GetAllEmbyMediaSources.cs index 9f0e6fc14..1334ac37f 100644 --- a/ErsatzTV.Application/Emby/Queries/GetAllEmbyMediaSources.cs +++ b/ErsatzTV.Application/Emby/Queries/GetAllEmbyMediaSources.cs @@ -1,7 +1,3 @@ -using System.Collections.Generic; -using MediatR; +namespace ErsatzTV.Application.Emby; -namespace ErsatzTV.Application.Emby.Queries -{ - public record GetAllEmbyMediaSources : IRequest>; -} +public record GetAllEmbyMediaSources : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Emby/Queries/GetAllEmbyMediaSourcesHandler.cs b/ErsatzTV.Application/Emby/Queries/GetAllEmbyMediaSourcesHandler.cs index d7615d6e2..d735fff24 100644 --- a/ErsatzTV.Application/Emby/Queries/GetAllEmbyMediaSourcesHandler.cs +++ b/ErsatzTV.Application/Emby/Queries/GetAllEmbyMediaSourcesHandler.cs @@ -1,24 +1,17 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -using MediatR; +using ErsatzTV.Core.Interfaces.Repositories; using static ErsatzTV.Application.Emby.Mapper; -namespace ErsatzTV.Application.Emby.Queries +namespace ErsatzTV.Application.Emby; + +public class GetAllEmbyMediaSourcesHandler : IRequestHandler> { - public class GetAllEmbyMediaSourcesHandler : IRequestHandler> - { - private readonly IMediaSourceRepository _mediaSourceRepository; + private readonly IMediaSourceRepository _mediaSourceRepository; - public GetAllEmbyMediaSourcesHandler(IMediaSourceRepository mediaSourceRepository) => - _mediaSourceRepository = mediaSourceRepository; + public GetAllEmbyMediaSourcesHandler(IMediaSourceRepository mediaSourceRepository) => + _mediaSourceRepository = mediaSourceRepository; - public Task> Handle( - GetAllEmbyMediaSources request, - CancellationToken cancellationToken) => - _mediaSourceRepository.GetAllEmby().Map(list => list.Map(ProjectToViewModel).ToList()); - } -} + public Task> Handle( + GetAllEmbyMediaSources request, + CancellationToken cancellationToken) => + _mediaSourceRepository.GetAllEmby().Map(list => list.Map(ProjectToViewModel).ToList()); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Emby/Queries/GetEmbyConnectionParameters.cs b/ErsatzTV.Application/Emby/Queries/GetEmbyConnectionParameters.cs index 6cc1f22fa..653d9cacc 100644 --- a/ErsatzTV.Application/Emby/Queries/GetEmbyConnectionParameters.cs +++ b/ErsatzTV.Application/Emby/Queries/GetEmbyConnectionParameters.cs @@ -1,8 +1,5 @@ using ErsatzTV.Core; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.Emby.Queries -{ - public record GetEmbyConnectionParameters : IRequest>; -} +namespace ErsatzTV.Application.Emby; + +public record GetEmbyConnectionParameters : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Emby/Queries/GetEmbyConnectionParametersHandler.cs b/ErsatzTV.Application/Emby/Queries/GetEmbyConnectionParametersHandler.cs index c67315287..c85eca763 100644 --- a/ErsatzTV.Application/Emby/Queries/GetEmbyConnectionParametersHandler.cs +++ b/ErsatzTV.Application/Emby/Queries/GetEmbyConnectionParametersHandler.cs @@ -1,73 +1,66 @@ -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -using MediatR; using Microsoft.Extensions.Caching.Memory; -namespace ErsatzTV.Application.Emby.Queries +namespace ErsatzTV.Application.Emby; + +public class GetEmbyConnectionParametersHandler : IRequestHandler> { - public class GetEmbyConnectionParametersHandler : IRequestHandler> + private readonly IMediaSourceRepository _mediaSourceRepository; + private readonly IMemoryCache _memoryCache; + + public GetEmbyConnectionParametersHandler( + IMemoryCache memoryCache, + IMediaSourceRepository mediaSourceRepository) { - private readonly IMediaSourceRepository _mediaSourceRepository; - private readonly IMemoryCache _memoryCache; - - public GetEmbyConnectionParametersHandler( - IMemoryCache memoryCache, - IMediaSourceRepository mediaSourceRepository) - { - _memoryCache = memoryCache; - _mediaSourceRepository = mediaSourceRepository; - } - - public async Task> Handle( - GetEmbyConnectionParameters request, - CancellationToken cancellationToken) - { - if (_memoryCache.TryGetValue(request, out EmbyConnectionParametersViewModel parameters)) - { - return parameters; - } - - Either maybeParameters = - await Validate() - .MapT(cp => new EmbyConnectionParametersViewModel(cp.ActiveConnection.Address)) - .Map(v => v.ToEither()); - - return maybeParameters.Match( - p => - { - _memoryCache.Set(request, p, TimeSpan.FromHours(1)); - return maybeParameters; - }, - error => error); - } - - private Task> Validate() => - EmbyMediaSourceMustExist() - .BindT(MediaSourceMustHaveActiveConnection); - - private Task> EmbyMediaSourceMustExist() => - _mediaSourceRepository.GetAllEmby().Map(list => list.HeadOrNone()) - .Map( - v => v.ToValidation( - "Emby media source does not exist.")); - - private Validation MediaSourceMustHaveActiveConnection( - EmbyMediaSource embyMediaSource) - { - Option maybeConnection = embyMediaSource.Connections.FirstOrDefault(); - return maybeConnection.Map(connection => new ConnectionParameters(embyMediaSource, connection)) - .ToValidation("Emby media source requires an active connection"); - } - - private record ConnectionParameters( - EmbyMediaSource EmbyMediaSource, - EmbyConnection ActiveConnection); + _memoryCache = memoryCache; + _mediaSourceRepository = mediaSourceRepository; } -} + + public async Task> Handle( + GetEmbyConnectionParameters request, + CancellationToken cancellationToken) + { + if (_memoryCache.TryGetValue(request, out EmbyConnectionParametersViewModel parameters)) + { + return parameters; + } + + Either maybeParameters = + await Validate() + .MapT(cp => new EmbyConnectionParametersViewModel(cp.ActiveConnection.Address)) + .Map(v => v.ToEither()); + + return maybeParameters.Match( + p => + { + _memoryCache.Set(request, p, TimeSpan.FromHours(1)); + return maybeParameters; + }, + error => error); + } + + private Task> Validate() => + EmbyMediaSourceMustExist() + .BindT(MediaSourceMustHaveActiveConnection); + + private Task> EmbyMediaSourceMustExist() => + _mediaSourceRepository.GetAllEmby().Map(list => list.HeadOrNone()) + .Map( + v => v.ToValidation( + "Emby media source does not exist.")); + + private Validation MediaSourceMustHaveActiveConnection( + EmbyMediaSource embyMediaSource) + { + Option maybeConnection = embyMediaSource.Connections.FirstOrDefault(); + return maybeConnection.Map(connection => new ConnectionParameters(embyMediaSource, connection)) + .ToValidation("Emby media source requires an active connection"); + } + + private record ConnectionParameters( + EmbyMediaSource EmbyMediaSource, + EmbyConnection ActiveConnection); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Emby/Queries/GetEmbyLibrariesBySourceId.cs b/ErsatzTV.Application/Emby/Queries/GetEmbyLibrariesBySourceId.cs index 3bfad5589..5ac0f4afb 100644 --- a/ErsatzTV.Application/Emby/Queries/GetEmbyLibrariesBySourceId.cs +++ b/ErsatzTV.Application/Emby/Queries/GetEmbyLibrariesBySourceId.cs @@ -1,7 +1,3 @@ -using System.Collections.Generic; -using MediatR; +namespace ErsatzTV.Application.Emby; -namespace ErsatzTV.Application.Emby.Queries -{ - public record GetEmbyLibrariesBySourceId(int EmbyMediaSourceId) : IRequest>; -} +public record GetEmbyLibrariesBySourceId(int EmbyMediaSourceId) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Emby/Queries/GetEmbyLibrariesBySourceIdHandler.cs b/ErsatzTV.Application/Emby/Queries/GetEmbyLibrariesBySourceIdHandler.cs index 758d5de64..dda6a3896 100644 --- a/ErsatzTV.Application/Emby/Queries/GetEmbyLibrariesBySourceIdHandler.cs +++ b/ErsatzTV.Application/Emby/Queries/GetEmbyLibrariesBySourceIdHandler.cs @@ -1,26 +1,19 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -using MediatR; +using ErsatzTV.Core.Interfaces.Repositories; using static ErsatzTV.Application.Emby.Mapper; -namespace ErsatzTV.Application.Emby.Queries +namespace ErsatzTV.Application.Emby; + +public class + GetEmbyLibrariesBySourceIdHandler : IRequestHandler> { - public class - GetEmbyLibrariesBySourceIdHandler : IRequestHandler> - { - private readonly IMediaSourceRepository _mediaSourceRepository; + private readonly IMediaSourceRepository _mediaSourceRepository; - public GetEmbyLibrariesBySourceIdHandler(IMediaSourceRepository mediaSourceRepository) => - _mediaSourceRepository = mediaSourceRepository; + public GetEmbyLibrariesBySourceIdHandler(IMediaSourceRepository mediaSourceRepository) => + _mediaSourceRepository = mediaSourceRepository; - public Task> Handle( - GetEmbyLibrariesBySourceId request, - CancellationToken cancellationToken) => - _mediaSourceRepository.GetEmbyLibraries(request.EmbyMediaSourceId) - .Map(list => list.Map(ProjectToViewModel).ToList()); - } -} + public Task> Handle( + GetEmbyLibrariesBySourceId request, + CancellationToken cancellationToken) => + _mediaSourceRepository.GetEmbyLibraries(request.EmbyMediaSourceId) + .Map(list => list.Map(ProjectToViewModel).ToList()); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Emby/Queries/GetEmbyMediaSourceById.cs b/ErsatzTV.Application/Emby/Queries/GetEmbyMediaSourceById.cs index f79a79935..3410201bc 100644 --- a/ErsatzTV.Application/Emby/Queries/GetEmbyMediaSourceById.cs +++ b/ErsatzTV.Application/Emby/Queries/GetEmbyMediaSourceById.cs @@ -1,7 +1,3 @@ -using LanguageExt; -using MediatR; +namespace ErsatzTV.Application.Emby; -namespace ErsatzTV.Application.Emby.Queries -{ - public record GetEmbyMediaSourceById(int EmbyMediaSourceId) : IRequest>; -} +public record GetEmbyMediaSourceById(int EmbyMediaSourceId) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Emby/Queries/GetEmbyMediaSourceByIdHandler.cs b/ErsatzTV.Application/Emby/Queries/GetEmbyMediaSourceByIdHandler.cs index b5e759646..24e8da6ec 100644 --- a/ErsatzTV.Application/Emby/Queries/GetEmbyMediaSourceByIdHandler.cs +++ b/ErsatzTV.Application/Emby/Queries/GetEmbyMediaSourceByIdHandler.cs @@ -1,23 +1,18 @@ -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -using MediatR; +using ErsatzTV.Core.Interfaces.Repositories; using static ErsatzTV.Application.Emby.Mapper; -namespace ErsatzTV.Application.Emby.Queries +namespace ErsatzTV.Application.Emby; + +public class + GetEmbyMediaSourceByIdHandler : IRequestHandler> { - public class - GetEmbyMediaSourceByIdHandler : IRequestHandler> - { - private readonly IMediaSourceRepository _mediaSourceRepository; + private readonly IMediaSourceRepository _mediaSourceRepository; - public GetEmbyMediaSourceByIdHandler(IMediaSourceRepository mediaSourceRepository) => - _mediaSourceRepository = mediaSourceRepository; + public GetEmbyMediaSourceByIdHandler(IMediaSourceRepository mediaSourceRepository) => + _mediaSourceRepository = mediaSourceRepository; - public Task> Handle( - GetEmbyMediaSourceById request, - CancellationToken cancellationToken) => - _mediaSourceRepository.GetEmby(request.EmbyMediaSourceId).MapT(ProjectToViewModel); - } -} + public Task> Handle( + GetEmbyMediaSourceById request, + CancellationToken cancellationToken) => + _mediaSourceRepository.GetEmby(request.EmbyMediaSourceId).MapT(ProjectToViewModel); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Emby/Queries/GetEmbyPathReplacementsBySourceId.cs b/ErsatzTV.Application/Emby/Queries/GetEmbyPathReplacementsBySourceId.cs index c26f3d585..678d31e91 100644 --- a/ErsatzTV.Application/Emby/Queries/GetEmbyPathReplacementsBySourceId.cs +++ b/ErsatzTV.Application/Emby/Queries/GetEmbyPathReplacementsBySourceId.cs @@ -1,8 +1,4 @@ -using System.Collections.Generic; -using MediatR; +namespace ErsatzTV.Application.Emby; -namespace ErsatzTV.Application.Emby.Queries -{ - public record GetEmbyPathReplacementsBySourceId - (int EmbyMediaSourceId) : IRequest>; -} +public record GetEmbyPathReplacementsBySourceId + (int EmbyMediaSourceId) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Emby/Queries/GetEmbyPathReplacementsBySourceIdHandler.cs b/ErsatzTV.Application/Emby/Queries/GetEmbyPathReplacementsBySourceIdHandler.cs index e5cab338f..db1dcbf96 100644 --- a/ErsatzTV.Application/Emby/Queries/GetEmbyPathReplacementsBySourceIdHandler.cs +++ b/ErsatzTV.Application/Emby/Queries/GetEmbyPathReplacementsBySourceIdHandler.cs @@ -1,26 +1,19 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -using MediatR; +using ErsatzTV.Core.Interfaces.Repositories; using static ErsatzTV.Application.Emby.Mapper; -namespace ErsatzTV.Application.Emby.Queries +namespace ErsatzTV.Application.Emby; + +public class GetEmbyPathReplacementsBySourceIdHandler : IRequestHandler> { - public class GetEmbyPathReplacementsBySourceIdHandler : IRequestHandler> - { - private readonly IMediaSourceRepository _mediaSourceRepository; + private readonly IMediaSourceRepository _mediaSourceRepository; - public GetEmbyPathReplacementsBySourceIdHandler(IMediaSourceRepository mediaSourceRepository) => - _mediaSourceRepository = mediaSourceRepository; + public GetEmbyPathReplacementsBySourceIdHandler(IMediaSourceRepository mediaSourceRepository) => + _mediaSourceRepository = mediaSourceRepository; - public Task> Handle( - GetEmbyPathReplacementsBySourceId request, - CancellationToken cancellationToken) => - _mediaSourceRepository.GetEmbyPathReplacements(request.EmbyMediaSourceId) - .Map(list => list.Map(ProjectToViewModel).ToList()); - } -} + public Task> Handle( + GetEmbyPathReplacementsBySourceId request, + CancellationToken cancellationToken) => + _mediaSourceRepository.GetEmbyPathReplacements(request.EmbyMediaSourceId) + .Map(list => list.Map(ProjectToViewModel).ToList()); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Emby/Queries/GetEmbySecrets.cs b/ErsatzTV.Application/Emby/Queries/GetEmbySecrets.cs index 6123398cc..c15302ea7 100644 --- a/ErsatzTV.Application/Emby/Queries/GetEmbySecrets.cs +++ b/ErsatzTV.Application/Emby/Queries/GetEmbySecrets.cs @@ -1,7 +1,5 @@ using ErsatzTV.Core.Emby; -using MediatR; -namespace ErsatzTV.Application.Emby.Queries -{ - public record GetEmbySecrets : IRequest; -} +namespace ErsatzTV.Application.Emby; + +public record GetEmbySecrets : IRequest; \ No newline at end of file diff --git a/ErsatzTV.Application/Emby/Queries/GetEmbySecretsHandler.cs b/ErsatzTV.Application/Emby/Queries/GetEmbySecretsHandler.cs index 1f075353d..9de15a250 100644 --- a/ErsatzTV.Application/Emby/Queries/GetEmbySecretsHandler.cs +++ b/ErsatzTV.Application/Emby/Queries/GetEmbySecretsHandler.cs @@ -1,19 +1,15 @@ -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core.Emby; +using ErsatzTV.Core.Emby; using ErsatzTV.Core.Interfaces.Emby; -using MediatR; -namespace ErsatzTV.Application.Emby.Queries +namespace ErsatzTV.Application.Emby; + +public class GetEmbySecretsHandler : IRequestHandler { - public class GetEmbySecretsHandler : IRequestHandler - { - private readonly IEmbySecretStore _embySecretStore; + private readonly IEmbySecretStore _embySecretStore; - public GetEmbySecretsHandler(IEmbySecretStore embySecretStore) => - _embySecretStore = embySecretStore; + public GetEmbySecretsHandler(IEmbySecretStore embySecretStore) => + _embySecretStore = embySecretStore; - public Task Handle(GetEmbySecrets request, CancellationToken cancellationToken) => - _embySecretStore.ReadSecrets(); - } -} + public Task Handle(GetEmbySecrets request, CancellationToken cancellationToken) => + _embySecretStore.ReadSecrets(); +} \ No newline at end of file diff --git a/ErsatzTV.Application/EntityIdResult.cs b/ErsatzTV.Application/EntityIdResult.cs index a39fdc7e3..feb3d4fb9 100644 --- a/ErsatzTV.Application/EntityIdResult.cs +++ b/ErsatzTV.Application/EntityIdResult.cs @@ -1,4 +1,3 @@ -namespace ErsatzTV.Application -{ - public record EntityIdResult(int Id); -} +namespace ErsatzTV.Application; + +public record EntityIdResult(int Id); \ No newline at end of file diff --git a/ErsatzTV.Application/ErsatzTV.Application.csproj b/ErsatzTV.Application/ErsatzTV.Application.csproj index a13ada020..ff363d636 100644 --- a/ErsatzTV.Application/ErsatzTV.Application.csproj +++ b/ErsatzTV.Application/ErsatzTV.Application.csproj @@ -3,6 +3,7 @@ net6.0 VSTHRD200 + enable diff --git a/ErsatzTV.Application/ErsatzTV.Application.csproj.DotSettings b/ErsatzTV.Application/ErsatzTV.Application.csproj.DotSettings new file mode 100644 index 000000000..ab5fb558b --- /dev/null +++ b/ErsatzTV.Application/ErsatzTV.Application.csproj.DotSettings @@ -0,0 +1,43 @@ + + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True \ No newline at end of file diff --git a/ErsatzTV.Application/FFmpegProfiles/Commands/CopyFFmpegProfile.cs b/ErsatzTV.Application/FFmpegProfiles/Commands/CopyFFmpegProfile.cs index c22736727..539d9b1b5 100644 --- a/ErsatzTV.Application/FFmpegProfiles/Commands/CopyFFmpegProfile.cs +++ b/ErsatzTV.Application/FFmpegProfiles/Commands/CopyFFmpegProfile.cs @@ -1,9 +1,6 @@ using ErsatzTV.Core; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.FFmpegProfiles.Commands -{ - public record CopyFFmpegProfile - (int FFmpegProfileId, string Name) : IRequest>; -} +namespace ErsatzTV.Application.FFmpegProfiles; + +public record CopyFFmpegProfile + (int FFmpegProfileId, string Name) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/FFmpegProfiles/Commands/CopyFFmpegProfileHandler.cs b/ErsatzTV.Application/FFmpegProfiles/Commands/CopyFFmpegProfileHandler.cs index 9f702d652..1a4a4830e 100644 --- a/ErsatzTV.Application/FFmpegProfiles/Commands/CopyFFmpegProfileHandler.cs +++ b/ErsatzTV.Application/FFmpegProfiles/Commands/CopyFFmpegProfileHandler.cs @@ -1,37 +1,32 @@ -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -using MediatR; using static ErsatzTV.Application.FFmpegProfiles.Mapper; -namespace ErsatzTV.Application.FFmpegProfiles.Commands +namespace ErsatzTV.Application.FFmpegProfiles; + +public class + CopyFFmpegProfileHandler : IRequestHandler> { - public class - CopyFFmpegProfileHandler : IRequestHandler> - { - private readonly IFFmpegProfileRepository _ffmpegProfileRepository; + private readonly IFFmpegProfileRepository _ffmpegProfileRepository; - public CopyFFmpegProfileHandler(IFFmpegProfileRepository ffmpegProfileRepository) => - _ffmpegProfileRepository = ffmpegProfileRepository; + public CopyFFmpegProfileHandler(IFFmpegProfileRepository ffmpegProfileRepository) => + _ffmpegProfileRepository = ffmpegProfileRepository; - public Task> Handle( - CopyFFmpegProfile request, - CancellationToken cancellationToken) => - Validate(request) - .MapT(PerformCopy) - .Bind(v => v.ToEitherAsync()); + public Task> Handle( + CopyFFmpegProfile request, + CancellationToken cancellationToken) => + Validate(request) + .MapT(PerformCopy) + .Bind(v => v.ToEitherAsync()); - private Task PerformCopy(CopyFFmpegProfile request) => - _ffmpegProfileRepository.Copy(request.FFmpegProfileId, request.Name) - .Map(ProjectToViewModel); + private Task PerformCopy(CopyFFmpegProfile request) => + _ffmpegProfileRepository.Copy(request.FFmpegProfileId, request.Name) + .Map(ProjectToViewModel); - private Task> Validate(CopyFFmpegProfile request) => - ValidateName(request).AsTask().MapT(_ => request); + private Task> Validate(CopyFFmpegProfile request) => + ValidateName(request).AsTask().MapT(_ => request); - private Validation ValidateName(CopyFFmpegProfile request) => - request.NotEmpty(x => x.Name) - .Bind(_ => request.NotLongerThan(50)(x => x.Name)); - } -} + private Validation ValidateName(CopyFFmpegProfile request) => + request.NotEmpty(x => x.Name) + .Bind(_ => request.NotLongerThan(50)(x => x.Name)); +} \ No newline at end of file diff --git a/ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfile.cs b/ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfile.cs index 4abfc0171..6f2c61427 100644 --- a/ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfile.cs +++ b/ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfile.cs @@ -1,29 +1,26 @@ using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.FFmpeg; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.FFmpegProfiles.Commands -{ - public record CreateFFmpegProfile( - string Name, - int ThreadCount, - bool Transcode, - HardwareAccelerationKind HardwareAcceleration, - VaapiDriver VaapiDriver, - string VaapiDevice, - int ResolutionId, - bool NormalizeVideo, - string VideoCodec, - int VideoBitrate, - int VideoBufferSize, - string AudioCodec, - int AudioBitrate, - int AudioBufferSize, - bool NormalizeLoudness, - int AudioChannels, - int AudioSampleRate, - bool NormalizeAudio, - bool NormalizeFramerate) : IRequest>; -} +namespace ErsatzTV.Application.FFmpegProfiles; + +public record CreateFFmpegProfile( + string Name, + int ThreadCount, + bool Transcode, + HardwareAccelerationKind HardwareAcceleration, + VaapiDriver VaapiDriver, + string VaapiDevice, + int ResolutionId, + bool NormalizeVideo, + string VideoCodec, + int VideoBitrate, + int VideoBufferSize, + string AudioCodec, + int AudioBitrate, + int AudioBufferSize, + bool NormalizeLoudness, + int AudioChannels, + int AudioSampleRate, + bool NormalizeAudio, + bool NormalizeFramerate) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfileHandler.cs b/ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfileHandler.cs index 173fe9181..673be2742 100644 --- a/ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfileHandler.cs +++ b/ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfileHandler.cs @@ -1,80 +1,75 @@ -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; -using LanguageExt; -using MediatR; using Microsoft.EntityFrameworkCore; -namespace ErsatzTV.Application.FFmpegProfiles.Commands +namespace ErsatzTV.Application.FFmpegProfiles; + +public class CreateFFmpegProfileHandler : + IRequestHandler> { - public class CreateFFmpegProfileHandler : - IRequestHandler> + private readonly IDbContextFactory _dbContextFactory; + + public CreateFFmpegProfileHandler(IDbContextFactory dbContextFactory) => + _dbContextFactory = dbContextFactory; + + public async Task> Handle( + CreateFFmpegProfile request, + CancellationToken cancellationToken) { - private readonly IDbContextFactory _dbContextFactory; - - public CreateFFmpegProfileHandler(IDbContextFactory dbContextFactory) => - _dbContextFactory = dbContextFactory; - - public async Task> Handle( - CreateFFmpegProfile request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - Validation validation = await Validate(dbContext, request); - return await validation.Apply(profile => PersistFFmpegProfile(dbContext, profile)); - } - - private static async Task PersistFFmpegProfile( - TvContext dbContext, - FFmpegProfile ffmpegProfile) - { - await dbContext.FFmpegProfiles.AddAsync(ffmpegProfile); - await dbContext.SaveChangesAsync(); - return new CreateFFmpegProfileResult(ffmpegProfile.Id); - } - - private async Task> Validate(TvContext dbContext, CreateFFmpegProfile request) => - (ValidateName(request), ValidateThreadCount(request), await ResolutionMustExist(dbContext, request)) - .Apply( - (name, threadCount, resolutionId) => new FFmpegProfile - { - Name = name, - ThreadCount = threadCount, - Transcode = request.Transcode, - HardwareAcceleration = request.HardwareAcceleration, - VaapiDriver = request.VaapiDriver, - VaapiDevice = request.VaapiDevice, - ResolutionId = resolutionId, - NormalizeVideo = request.NormalizeVideo, - VideoCodec = request.VideoCodec, - VideoBitrate = request.VideoBitrate, - VideoBufferSize = request.VideoBufferSize, - AudioCodec = request.AudioCodec, - AudioBitrate = request.AudioBitrate, - AudioBufferSize = request.AudioBufferSize, - NormalizeLoudness = request.NormalizeLoudness, - AudioChannels = request.AudioChannels, - AudioSampleRate = request.AudioSampleRate, - NormalizeAudio = request.NormalizeAudio, - NormalizeFramerate = request.NormalizeFramerate - }); - - private static Validation ValidateName(CreateFFmpegProfile createFFmpegProfile) => - createFFmpegProfile.NotEmpty(x => x.Name) - .Bind(_ => createFFmpegProfile.NotLongerThan(50)(x => x.Name)); - - private static Validation ValidateThreadCount(CreateFFmpegProfile createFFmpegProfile) => - createFFmpegProfile.AtLeast(0)(p => p.ThreadCount); - - private static Task> ResolutionMustExist( - TvContext dbContext, - CreateFFmpegProfile createFFmpegProfile) => - dbContext.Resolutions - .SelectOneAsync(r => r.Id, r => r.Id == createFFmpegProfile.ResolutionId) - .MapT(r => r.Id) - .Map(o => o.ToValidation($"[Resolution] {createFFmpegProfile.ResolutionId} does not exist")); + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + Validation validation = await Validate(dbContext, request); + return await LanguageExtensions.Apply(validation, profile => PersistFFmpegProfile(dbContext, profile)); } -} + + private static async Task PersistFFmpegProfile( + TvContext dbContext, + FFmpegProfile ffmpegProfile) + { + await dbContext.FFmpegProfiles.AddAsync(ffmpegProfile); + await dbContext.SaveChangesAsync(); + return new CreateFFmpegProfileResult(ffmpegProfile.Id); + } + + private async Task> Validate(TvContext dbContext, CreateFFmpegProfile request) => + (ValidateName(request), ValidateThreadCount(request), await ResolutionMustExist(dbContext, request)) + .Apply( + (name, threadCount, resolutionId) => new FFmpegProfile + { + Name = name, + ThreadCount = threadCount, + Transcode = request.Transcode, + HardwareAcceleration = request.HardwareAcceleration, + VaapiDriver = request.VaapiDriver, + VaapiDevice = request.VaapiDevice, + ResolutionId = resolutionId, + NormalizeVideo = request.NormalizeVideo, + VideoCodec = request.VideoCodec, + VideoBitrate = request.VideoBitrate, + VideoBufferSize = request.VideoBufferSize, + AudioCodec = request.AudioCodec, + AudioBitrate = request.AudioBitrate, + AudioBufferSize = request.AudioBufferSize, + NormalizeLoudness = request.NormalizeLoudness, + AudioChannels = request.AudioChannels, + AudioSampleRate = request.AudioSampleRate, + NormalizeAudio = request.NormalizeAudio, + NormalizeFramerate = request.NormalizeFramerate + }); + + private static Validation ValidateName(CreateFFmpegProfile createFFmpegProfile) => + createFFmpegProfile.NotEmpty(x => x.Name) + .Bind(_ => createFFmpegProfile.NotLongerThan(50)(x => x.Name)); + + private static Validation ValidateThreadCount(CreateFFmpegProfile createFFmpegProfile) => + createFFmpegProfile.AtLeast(0)(p => p.ThreadCount); + + private static Task> ResolutionMustExist( + TvContext dbContext, + CreateFFmpegProfile createFFmpegProfile) => + dbContext.Resolutions + .SelectOneAsync(r => r.Id, r => r.Id == createFFmpegProfile.ResolutionId) + .MapT(r => r.Id) + .Map(o => o.ToValidation($"[Resolution] {createFFmpegProfile.ResolutionId} does not exist")); +} \ No newline at end of file diff --git a/ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfileResult.cs b/ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfileResult.cs index 25c68e0a3..f2a29e103 100644 --- a/ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfileResult.cs +++ b/ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfileResult.cs @@ -1,4 +1,3 @@ -namespace ErsatzTV.Application.FFmpegProfiles.Commands -{ - public record CreateFFmpegProfileResult(int FFmpegProfileId) : EntityIdResult(FFmpegProfileId); -} +namespace ErsatzTV.Application.FFmpegProfiles; + +public record CreateFFmpegProfileResult(int FFmpegProfileId) : EntityIdResult(FFmpegProfileId); \ No newline at end of file diff --git a/ErsatzTV.Application/FFmpegProfiles/Commands/DeleteFFmpegProfile.cs b/ErsatzTV.Application/FFmpegProfiles/Commands/DeleteFFmpegProfile.cs index fc4c52448..6f630f9c6 100644 --- a/ErsatzTV.Application/FFmpegProfiles/Commands/DeleteFFmpegProfile.cs +++ b/ErsatzTV.Application/FFmpegProfiles/Commands/DeleteFFmpegProfile.cs @@ -1,7 +1,5 @@ using ErsatzTV.Core; -using LanguageExt; -namespace ErsatzTV.Application.FFmpegProfiles.Commands -{ - public record DeleteFFmpegProfile(int FFmpegProfileId) : MediatR.IRequest>; -} +namespace ErsatzTV.Application.FFmpegProfiles; + +public record DeleteFFmpegProfile(int FFmpegProfileId) : MediatR.IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/FFmpegProfiles/Commands/DeleteFFmpegProfileHandler.cs b/ErsatzTV.Application/FFmpegProfiles/Commands/DeleteFFmpegProfileHandler.cs index ae01345d0..b8041e3c6 100644 --- a/ErsatzTV.Application/FFmpegProfiles/Commands/DeleteFFmpegProfileHandler.cs +++ b/ErsatzTV.Application/FFmpegProfiles/Commands/DeleteFFmpegProfileHandler.cs @@ -1,43 +1,38 @@ -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; -using LanguageExt; -using MediatR; using Microsoft.EntityFrameworkCore; -namespace ErsatzTV.Application.FFmpegProfiles.Commands +namespace ErsatzTV.Application.FFmpegProfiles; + +public class DeleteFFmpegProfileHandler : IRequestHandler> { - public class DeleteFFmpegProfileHandler : IRequestHandler> + private readonly IDbContextFactory _dbContextFactory; + + public DeleteFFmpegProfileHandler(IDbContextFactory dbContextFactory) => + _dbContextFactory = dbContextFactory; + + public async Task> Handle( + DeleteFFmpegProfile request, + CancellationToken cancellationToken) { - private readonly IDbContextFactory _dbContextFactory; - - public DeleteFFmpegProfileHandler(IDbContextFactory dbContextFactory) => - _dbContextFactory = dbContextFactory; - - public async Task> Handle( - DeleteFFmpegProfile request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - Validation validation = await FFmpegProfileMustExist(dbContext, request); - return await validation.Apply(p => DoDeletion(dbContext, p)); - } - - private static async Task DoDeletion(TvContext dbContext, FFmpegProfile ffmpegProfile) - { - dbContext.FFmpegProfiles.Remove(ffmpegProfile); - await dbContext.SaveChangesAsync(); - return LanguageExt.Unit.Default; - } - - private static Task> FFmpegProfileMustExist( - TvContext dbContext, - DeleteFFmpegProfile request) => - dbContext.FFmpegProfiles - .SelectOneAsync(p => p.Id, p => p.Id == request.FFmpegProfileId) - .Map(o => o.ToValidation($"FFmpegProfile {request.FFmpegProfileId} does not exist")); + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + Validation validation = await FFmpegProfileMustExist(dbContext, request); + return await LanguageExtensions.Apply(validation, p => DoDeletion(dbContext, p)); } -} + + private static async Task DoDeletion(TvContext dbContext, FFmpegProfile ffmpegProfile) + { + dbContext.FFmpegProfiles.Remove(ffmpegProfile); + await dbContext.SaveChangesAsync(); + return LanguageExt.Unit.Default; + } + + private static Task> FFmpegProfileMustExist( + TvContext dbContext, + DeleteFFmpegProfile request) => + dbContext.FFmpegProfiles + .SelectOneAsync(p => p.Id, p => p.Id == request.FFmpegProfileId) + .Map(o => o.ToValidation($"FFmpegProfile {request.FFmpegProfileId} does not exist")); +} \ No newline at end of file diff --git a/ErsatzTV.Application/FFmpegProfiles/Commands/NewFFmpegProfile.cs b/ErsatzTV.Application/FFmpegProfiles/Commands/NewFFmpegProfile.cs index 1bcd94bee..f14242fdb 100644 --- a/ErsatzTV.Application/FFmpegProfiles/Commands/NewFFmpegProfile.cs +++ b/ErsatzTV.Application/FFmpegProfiles/Commands/NewFFmpegProfile.cs @@ -1,10 +1,7 @@ -using MediatR; +namespace ErsatzTV.Application.FFmpegProfiles; -namespace ErsatzTV.Application.FFmpegProfiles.Commands -{ - /// - /// Requests a new ffmpeg profile (view model) that contains - /// appropriate default values. - /// - public record NewFFmpegProfile : IRequest; -} +/// +/// Requests a new ffmpeg profile (view model) that contains +/// appropriate default values. +/// +public record NewFFmpegProfile : IRequest; \ No newline at end of file diff --git a/ErsatzTV.Application/FFmpegProfiles/Commands/NewFFmpegProfileHandler.cs b/ErsatzTV.Application/FFmpegProfiles/Commands/NewFFmpegProfileHandler.cs index 1150feb29..77fbe4fa4 100644 --- a/ErsatzTV.Application/FFmpegProfiles/Commands/NewFFmpegProfileHandler.cs +++ b/ErsatzTV.Application/FFmpegProfiles/Commands/NewFFmpegProfileHandler.cs @@ -1,39 +1,32 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; 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; -namespace ErsatzTV.Application.FFmpegProfiles.Commands +namespace ErsatzTV.Application.FFmpegProfiles; + +public class NewFFmpegProfileHandler : IRequestHandler { - public class NewFFmpegProfileHandler : IRequestHandler + private readonly IDbContextFactory _dbContextFactory; + + public NewFFmpegProfileHandler(IDbContextFactory dbContextFactory) => + _dbContextFactory = dbContextFactory; + + public async Task Handle(NewFFmpegProfile request, CancellationToken cancellationToken) { - private readonly IDbContextFactory _dbContextFactory; + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - public NewFFmpegProfileHandler(IDbContextFactory dbContextFactory) => - _dbContextFactory = dbContextFactory; + int defaultResolutionId = await dbContext.ConfigElements + .GetValue(ConfigElementKey.FFmpegDefaultResolutionId) + .IfNoneAsync(0); - public async Task Handle(NewFFmpegProfile request, CancellationToken cancellationToken) - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + List allResolutions = await dbContext.Resolutions + .ToListAsync(cancellationToken); - int defaultResolutionId = await dbContext.ConfigElements - .GetValue(ConfigElementKey.FFmpegDefaultResolutionId) - .IfNoneAsync(0); + Option maybeDefaultResolution = allResolutions.Find(r => r.Id == defaultResolutionId); + Resolution defaultResolution = maybeDefaultResolution.Match(identity, () => allResolutions.Head()); - List allResolutions = await dbContext.Resolutions - .ToListAsync(cancellationToken); - - Option maybeDefaultResolution = allResolutions.Find(r => r.Id == defaultResolutionId); - Resolution defaultResolution = maybeDefaultResolution.Match(identity, () => allResolutions.Head()); - - return ProjectToViewModel(FFmpegProfile.New("New Profile", defaultResolution)); - } + return ProjectToViewModel(FFmpegProfile.New("New Profile", defaultResolution)); } -} +} \ No newline at end of file diff --git a/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfile.cs b/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfile.cs index 3c7d57ac3..7eded6ee0 100644 --- a/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfile.cs +++ b/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfile.cs @@ -1,30 +1,27 @@ using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.FFmpeg; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.FFmpegProfiles.Commands -{ - public record UpdateFFmpegProfile( - int FFmpegProfileId, - string Name, - int ThreadCount, - bool Transcode, - HardwareAccelerationKind HardwareAcceleration, - VaapiDriver VaapiDriver, - string VaapiDevice, - int ResolutionId, - bool NormalizeVideo, - string VideoCodec, - int VideoBitrate, - int VideoBufferSize, - string AudioCodec, - int AudioBitrate, - int AudioBufferSize, - bool NormalizeLoudness, - int AudioChannels, - int AudioSampleRate, - bool NormalizeAudio, - bool NormalizeFramerate) : IRequest>; -} +namespace ErsatzTV.Application.FFmpegProfiles; + +public record UpdateFFmpegProfile( + int FFmpegProfileId, + string Name, + int ThreadCount, + bool Transcode, + HardwareAccelerationKind HardwareAcceleration, + VaapiDriver VaapiDriver, + string VaapiDevice, + int ResolutionId, + bool NormalizeVideo, + string VideoCodec, + int VideoBitrate, + int VideoBufferSize, + string AudioCodec, + int AudioBitrate, + int AudioBufferSize, + bool NormalizeLoudness, + int AudioChannels, + int AudioSampleRate, + bool NormalizeAudio, + bool NormalizeFramerate) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfileHandler.cs b/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfileHandler.cs index 7a65065ee..656496312 100644 --- a/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfileHandler.cs +++ b/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfileHandler.cs @@ -1,87 +1,82 @@ -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; -using LanguageExt; -using MediatR; using Microsoft.EntityFrameworkCore; -namespace ErsatzTV.Application.FFmpegProfiles.Commands +namespace ErsatzTV.Application.FFmpegProfiles; + +public class + UpdateFFmpegProfileHandler : IRequestHandler> { - public class - UpdateFFmpegProfileHandler : IRequestHandler> + private readonly IDbContextFactory _dbContextFactory; + + public UpdateFFmpegProfileHandler(IDbContextFactory dbContextFactory) => + _dbContextFactory = dbContextFactory; + + public async Task> Handle( + UpdateFFmpegProfile request, + CancellationToken cancellationToken) { - private readonly IDbContextFactory _dbContextFactory; - - public UpdateFFmpegProfileHandler(IDbContextFactory dbContextFactory) => - _dbContextFactory = dbContextFactory; - - public async Task> Handle( - UpdateFFmpegProfile request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - Validation validation = await Validate(dbContext, request); - return await validation.Apply(p => ApplyUpdateRequest(dbContext, p, request)); - } - - private async Task ApplyUpdateRequest( - TvContext dbContext, - FFmpegProfile p, - UpdateFFmpegProfile update) - { - p.Name = update.Name; - p.ThreadCount = update.ThreadCount; - p.Transcode = update.Transcode; - p.HardwareAcceleration = update.HardwareAcceleration; - p.VaapiDriver = update.VaapiDriver; - p.VaapiDevice = update.VaapiDevice; - p.ResolutionId = update.ResolutionId; - p.NormalizeVideo = update.Transcode && update.NormalizeVideo; - p.VideoCodec = update.VideoCodec; - p.VideoBitrate = update.VideoBitrate; - p.VideoBufferSize = update.VideoBufferSize; - p.AudioCodec = update.AudioCodec; - p.AudioBitrate = update.AudioBitrate; - p.AudioBufferSize = update.AudioBufferSize; - p.NormalizeLoudness = update.Transcode && update.NormalizeLoudness; - p.AudioChannels = update.AudioChannels; - p.AudioSampleRate = update.AudioSampleRate; - p.NormalizeAudio = update.Transcode && update.NormalizeAudio; - p.NormalizeFramerate = update.Transcode && update.NormalizeFramerate; - await dbContext.SaveChangesAsync(); - return new UpdateFFmpegProfileResult(p.Id); - } - - private static async Task> Validate( - TvContext dbContext, - UpdateFFmpegProfile request) => - (await FFmpegProfileMustExist(dbContext, request), ValidateName(request), ValidateThreadCount(request), - await ResolutionMustExist(dbContext, request)) - .Apply((ffmpegProfileToUpdate, _, _, _) => ffmpegProfileToUpdate); - - private static Task> FFmpegProfileMustExist( - TvContext dbContext, - UpdateFFmpegProfile updateFFmpegProfile) => - dbContext.FFmpegProfiles - .SelectOneAsync(p => p.Id, p => p.Id == updateFFmpegProfile.FFmpegProfileId) - .Map(o => o.ToValidation("FFmpegProfile does not exist.")); - - private static Validation ValidateName(UpdateFFmpegProfile updateFFmpegProfile) => - updateFFmpegProfile.NotEmpty(x => x.Name) - .Bind(_ => updateFFmpegProfile.NotLongerThan(50)(x => x.Name)); - - private static Validation ValidateThreadCount(UpdateFFmpegProfile updateFFmpegProfile) => - updateFFmpegProfile.AtLeast(0)(p => p.ThreadCount); - - private static Task> ResolutionMustExist( - TvContext dbContext, - UpdateFFmpegProfile updateFFmpegProfile) => - dbContext.Resolutions - .SelectOneAsync(r => r.Id, r => r.Id == updateFFmpegProfile.ResolutionId) - .MapT(r => r.Id) - .Map(o => o.ToValidation($"[Resolution] {updateFFmpegProfile.ResolutionId} does not exist")); + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + Validation validation = await Validate(dbContext, request); + return await LanguageExtensions.Apply(validation, p => ApplyUpdateRequest(dbContext, p, request)); } -} + + private async Task ApplyUpdateRequest( + TvContext dbContext, + FFmpegProfile p, + UpdateFFmpegProfile update) + { + p.Name = update.Name; + p.ThreadCount = update.ThreadCount; + p.Transcode = update.Transcode; + p.HardwareAcceleration = update.HardwareAcceleration; + p.VaapiDriver = update.VaapiDriver; + p.VaapiDevice = update.VaapiDevice; + p.ResolutionId = update.ResolutionId; + p.NormalizeVideo = update.Transcode && update.NormalizeVideo; + p.VideoCodec = update.VideoCodec; + p.VideoBitrate = update.VideoBitrate; + p.VideoBufferSize = update.VideoBufferSize; + p.AudioCodec = update.AudioCodec; + p.AudioBitrate = update.AudioBitrate; + p.AudioBufferSize = update.AudioBufferSize; + p.NormalizeLoudness = update.Transcode && update.NormalizeLoudness; + p.AudioChannels = update.AudioChannels; + p.AudioSampleRate = update.AudioSampleRate; + p.NormalizeAudio = update.Transcode && update.NormalizeAudio; + p.NormalizeFramerate = update.Transcode && update.NormalizeFramerate; + await dbContext.SaveChangesAsync(); + return new UpdateFFmpegProfileResult(p.Id); + } + + private static async Task> Validate( + TvContext dbContext, + UpdateFFmpegProfile request) => + (await FFmpegProfileMustExist(dbContext, request), ValidateName(request), ValidateThreadCount(request), + await ResolutionMustExist(dbContext, request)) + .Apply((ffmpegProfileToUpdate, _, _, _) => ffmpegProfileToUpdate); + + private static Task> FFmpegProfileMustExist( + TvContext dbContext, + UpdateFFmpegProfile updateFFmpegProfile) => + dbContext.FFmpegProfiles + .SelectOneAsync(p => p.Id, p => p.Id == updateFFmpegProfile.FFmpegProfileId) + .Map(o => o.ToValidation("FFmpegProfile does not exist.")); + + private static Validation ValidateName(UpdateFFmpegProfile updateFFmpegProfile) => + updateFFmpegProfile.NotEmpty(x => x.Name) + .Bind(_ => updateFFmpegProfile.NotLongerThan(50)(x => x.Name)); + + private static Validation ValidateThreadCount(UpdateFFmpegProfile updateFFmpegProfile) => + updateFFmpegProfile.AtLeast(0)(p => p.ThreadCount); + + private static Task> ResolutionMustExist( + TvContext dbContext, + UpdateFFmpegProfile updateFFmpegProfile) => + dbContext.Resolutions + .SelectOneAsync(r => r.Id, r => r.Id == updateFFmpegProfile.ResolutionId) + .MapT(r => r.Id) + .Map(o => o.ToValidation($"[Resolution] {updateFFmpegProfile.ResolutionId} does not exist")); +} \ No newline at end of file diff --git a/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfileResult.cs b/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfileResult.cs index c574c6ed0..9fa704174 100644 --- a/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfileResult.cs +++ b/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfileResult.cs @@ -1,4 +1,3 @@ -namespace ErsatzTV.Application.FFmpegProfiles.Commands -{ - public record UpdateFFmpegProfileResult(int FFmpegProfileId) : EntityIdResult(FFmpegProfileId); -} +namespace ErsatzTV.Application.FFmpegProfiles; + +public record UpdateFFmpegProfileResult(int FFmpegProfileId) : EntityIdResult(FFmpegProfileId); \ No newline at end of file diff --git a/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegSettings.cs b/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegSettings.cs index e4b3b392d..383e0739b 100644 --- a/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegSettings.cs +++ b/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegSettings.cs @@ -1,7 +1,5 @@ using ErsatzTV.Core; -using LanguageExt; -namespace ErsatzTV.Application.FFmpegProfiles.Commands -{ - public record UpdateFFmpegSettings(FFmpegSettingsViewModel Settings) : MediatR.IRequest>; -} +namespace ErsatzTV.Application.FFmpegProfiles; + +public record UpdateFFmpegSettings(FFmpegSettingsViewModel Settings) : MediatR.IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegSettingsHandler.cs b/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegSettingsHandler.cs index bd0bdc3b2..21494dae1 100644 --- a/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegSettingsHandler.cs +++ b/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegSettingsHandler.cs @@ -1,133 +1,128 @@ using System.Diagnostics; -using System.IO; -using System.Threading; -using System.Threading.Tasks; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -namespace ErsatzTV.Application.FFmpegProfiles.Commands +namespace ErsatzTV.Application.FFmpegProfiles; + +public class UpdateFFmpegSettingsHandler : MediatR.IRequestHandler> { - public class UpdateFFmpegSettingsHandler : MediatR.IRequestHandler> + private readonly IConfigElementRepository _configElementRepository; + private readonly ILocalFileSystem _localFileSystem; + + public UpdateFFmpegSettingsHandler( + IConfigElementRepository configElementRepository, + ILocalFileSystem localFileSystem) { - private readonly IConfigElementRepository _configElementRepository; - private readonly ILocalFileSystem _localFileSystem; - - public UpdateFFmpegSettingsHandler( - IConfigElementRepository configElementRepository, - ILocalFileSystem localFileSystem) - { - _configElementRepository = configElementRepository; - _localFileSystem = localFileSystem; - } - - public Task> Handle( - UpdateFFmpegSettings request, - CancellationToken cancellationToken) => - Validate(request) - .MapT(_ => ApplyUpdate(request)) - .Bind(v => v.ToEitherAsync()); - - private async Task> Validate(UpdateFFmpegSettings request) => - (await FFmpegMustExist(request), await FFprobeMustExist(request)) - .Apply((_, _) => Unit.Default); - - private Task> FFmpegMustExist(UpdateFFmpegSettings request) => - ValidateToolPath(request.Settings.FFmpegPath, "ffmpeg"); - - private Task> FFprobeMustExist(UpdateFFmpegSettings request) => - ValidateToolPath(request.Settings.FFprobePath, "ffprobe"); - - private async Task> ValidateToolPath(string path, string name) - { - if (!_localFileSystem.FileExists(path)) - { - return BaseError.New($"{name} path does not exist"); - } - - var startInfo = new ProcessStartInfo - { - FileName = path, - Arguments = "-version", - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false - }; - - var test = new Process - { - StartInfo = startInfo - }; - - test.Start(); - string output = await test.StandardOutput.ReadToEndAsync(); - await test.WaitForExitAsync(); - return test.ExitCode == 0 && output.Contains($"{name} version") - ? Unit.Default - : BaseError.New($"Unable to verify {name} version"); - } - - private async Task ApplyUpdate(UpdateFFmpegSettings request) - { - 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 _configElementRepository.Upsert( - ConfigElementKey.FFmpegUseLegacyTranscoder, - request.Settings.UseLegacyTranscoder.ToString()); - - await _configElementRepository.Upsert( - ConfigElementKey.FFmpegPreferredLanguageCode, - request.Settings.PreferredLanguageCode); - - if (request.Settings.GlobalWatermarkId is not null) - { - await _configElementRepository.Upsert( - ConfigElementKey.FFmpegGlobalWatermarkId, - request.Settings.GlobalWatermarkId.Value); - } - else - { - await _configElementRepository.Delete(ConfigElementKey.FFmpegGlobalWatermarkId); - } - - if (request.Settings.GlobalFallbackFillerId is not null) - { - await _configElementRepository.Upsert( - ConfigElementKey.FFmpegGlobalFallbackFillerId, - request.Settings.GlobalFallbackFillerId.Value); - } - else - { - await _configElementRepository.Delete(ConfigElementKey.FFmpegGlobalFallbackFillerId); - } - - await _configElementRepository.Upsert( - ConfigElementKey.FFmpegSegmenterTimeout, - request.Settings.HlsSegmenterIdleTimeout); - - await _configElementRepository.Upsert( - ConfigElementKey.FFmpegWorkAheadSegmenters, - request.Settings.WorkAheadSegmenterLimit); - - await _configElementRepository.Upsert( - ConfigElementKey.FFmpegInitialSegmentCount, - request.Settings.InitialSegmentCount); - - return Unit.Default; - } + _configElementRepository = configElementRepository; + _localFileSystem = localFileSystem; } -} + + public Task> Handle( + UpdateFFmpegSettings request, + CancellationToken cancellationToken) => + Validate(request) + .MapT(_ => ApplyUpdate(request)) + .Bind(v => v.ToEitherAsync()); + + private async Task> Validate(UpdateFFmpegSettings request) => + (await FFmpegMustExist(request), await FFprobeMustExist(request)) + .Apply((_, _) => Unit.Default); + + private Task> FFmpegMustExist(UpdateFFmpegSettings request) => + ValidateToolPath(request.Settings.FFmpegPath, "ffmpeg"); + + private Task> FFprobeMustExist(UpdateFFmpegSettings request) => + ValidateToolPath(request.Settings.FFprobePath, "ffprobe"); + + private async Task> ValidateToolPath(string path, string name) + { + if (!_localFileSystem.FileExists(path)) + { + return BaseError.New($"{name} path does not exist"); + } + + var startInfo = new ProcessStartInfo + { + FileName = path, + Arguments = "-version", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false + }; + + var test = new Process + { + StartInfo = startInfo + }; + + test.Start(); + string output = await test.StandardOutput.ReadToEndAsync(); + await test.WaitForExitAsync(); + return test.ExitCode == 0 && output.Contains($"{name} version") + ? Unit.Default + : BaseError.New($"Unable to verify {name} version"); + } + + private async Task ApplyUpdate(UpdateFFmpegSettings request) + { + 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 _configElementRepository.Upsert( + ConfigElementKey.FFmpegUseLegacyTranscoder, + request.Settings.UseLegacyTranscoder.ToString()); + + await _configElementRepository.Upsert( + ConfigElementKey.FFmpegPreferredLanguageCode, + request.Settings.PreferredLanguageCode); + + if (request.Settings.GlobalWatermarkId is not null) + { + await _configElementRepository.Upsert( + ConfigElementKey.FFmpegGlobalWatermarkId, + request.Settings.GlobalWatermarkId.Value); + } + else + { + await _configElementRepository.Delete(ConfigElementKey.FFmpegGlobalWatermarkId); + } + + if (request.Settings.GlobalFallbackFillerId is not null) + { + await _configElementRepository.Upsert( + ConfigElementKey.FFmpegGlobalFallbackFillerId, + request.Settings.GlobalFallbackFillerId.Value); + } + else + { + await _configElementRepository.Delete(ConfigElementKey.FFmpegGlobalFallbackFillerId); + } + + await _configElementRepository.Upsert( + ConfigElementKey.FFmpegSegmenterTimeout, + request.Settings.HlsSegmenterIdleTimeout); + + await _configElementRepository.Upsert( + ConfigElementKey.FFmpegWorkAheadSegmenters, + request.Settings.WorkAheadSegmenterLimit); + + await _configElementRepository.Upsert( + ConfigElementKey.FFmpegInitialSegmentCount, + request.Settings.InitialSegmentCount); + + return Unit.Default; + } +} \ No newline at end of file diff --git a/ErsatzTV.Application/FFmpegProfiles/FFmpegProfileViewModel.cs b/ErsatzTV.Application/FFmpegProfiles/FFmpegProfileViewModel.cs index c30ce3357..4bc68012c 100644 --- a/ErsatzTV.Application/FFmpegProfiles/FFmpegProfileViewModel.cs +++ b/ErsatzTV.Application/FFmpegProfiles/FFmpegProfileViewModel.cs @@ -2,27 +2,26 @@ using ErsatzTV.Core.Domain; using ErsatzTV.Core.FFmpeg; -namespace ErsatzTV.Application.FFmpegProfiles -{ - public record FFmpegProfileViewModel( - int Id, - string Name, - int ThreadCount, - bool Transcode, - HardwareAccelerationKind HardwareAcceleration, - VaapiDriver VaapiDriver, - string VaapiDevice, - ResolutionViewModel Resolution, - bool NormalizeVideo, - string VideoCodec, - int VideoBitrate, - int VideoBufferSize, - string AudioCodec, - int AudioBitrate, - int AudioBufferSize, - bool NormalizeLoudness, - int AudioChannels, - int AudioSampleRate, - bool NormalizeAudio, - bool NormalizeFramerate); -} +namespace ErsatzTV.Application.FFmpegProfiles; + +public record FFmpegProfileViewModel( + int Id, + string Name, + int ThreadCount, + bool Transcode, + HardwareAccelerationKind HardwareAcceleration, + VaapiDriver VaapiDriver, + string VaapiDevice, + ResolutionViewModel Resolution, + bool NormalizeVideo, + string VideoCodec, + int VideoBitrate, + int VideoBufferSize, + string AudioCodec, + int AudioBitrate, + int AudioBufferSize, + bool NormalizeLoudness, + int AudioChannels, + int AudioSampleRate, + bool NormalizeAudio, + bool NormalizeFramerate); \ No newline at end of file diff --git a/ErsatzTV.Application/FFmpegProfiles/FFmpegSettingsViewModel.cs b/ErsatzTV.Application/FFmpegProfiles/FFmpegSettingsViewModel.cs index b06ef54a6..da1a40f84 100644 --- a/ErsatzTV.Application/FFmpegProfiles/FFmpegSettingsViewModel.cs +++ b/ErsatzTV.Application/FFmpegProfiles/FFmpegSettingsViewModel.cs @@ -1,17 +1,16 @@ -namespace ErsatzTV.Application.FFmpegProfiles +namespace ErsatzTV.Application.FFmpegProfiles; + +public class FFmpegSettingsViewModel { - public class FFmpegSettingsViewModel - { - public string FFmpegPath { get; set; } - public string FFprobePath { get; set; } - public int DefaultFFmpegProfileId { get; set; } - public string PreferredLanguageCode { get; set; } - public bool SaveReports { get; set; } - public int? GlobalWatermarkId { get; set; } - public int? GlobalFallbackFillerId { get; set; } - public int HlsSegmenterIdleTimeout { get; set; } - public int WorkAheadSegmenterLimit { get; set; } - public int InitialSegmentCount { get; set; } - public bool UseLegacyTranscoder { get; set; } - } -} + public string FFmpegPath { get; set; } + public string FFprobePath { get; set; } + public int DefaultFFmpegProfileId { get; set; } + public string PreferredLanguageCode { get; set; } + public bool SaveReports { get; set; } + public int? GlobalWatermarkId { get; set; } + public int? GlobalFallbackFillerId { get; set; } + public int HlsSegmenterIdleTimeout { get; set; } + public int WorkAheadSegmenterLimit { get; set; } + public int InitialSegmentCount { get; set; } + public bool UseLegacyTranscoder { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Application/FFmpegProfiles/Mapper.cs b/ErsatzTV.Application/FFmpegProfiles/Mapper.cs index 807cf21cc..a25e07827 100644 --- a/ErsatzTV.Application/FFmpegProfiles/Mapper.cs +++ b/ErsatzTV.Application/FFmpegProfiles/Mapper.cs @@ -1,34 +1,33 @@ using ErsatzTV.Application.Resolutions; using ErsatzTV.Core.Domain; -namespace ErsatzTV.Application.FFmpegProfiles -{ - internal static class Mapper - { - internal static FFmpegProfileViewModel ProjectToViewModel(FFmpegProfile profile) => - new( - profile.Id, - profile.Name, - profile.ThreadCount, - profile.Transcode, - profile.HardwareAcceleration, - profile.VaapiDriver, - profile.VaapiDevice, - Project(profile.Resolution), - profile.NormalizeVideo, - profile.VideoCodec, - profile.VideoBitrate, - profile.VideoBufferSize, - profile.AudioCodec, - profile.AudioBitrate, - profile.AudioBufferSize, - profile.NormalizeLoudness, - profile.AudioChannels, - profile.AudioSampleRate, - profile.NormalizeAudio, - profile.NormalizeVideo && profile.NormalizeFramerate); +namespace ErsatzTV.Application.FFmpegProfiles; - private static ResolutionViewModel Project(Resolution resolution) => - new(resolution.Id, resolution.Name, resolution.Width, resolution.Height); - } -} +internal static class Mapper +{ + internal static FFmpegProfileViewModel ProjectToViewModel(FFmpegProfile profile) => + new( + profile.Id, + profile.Name, + profile.ThreadCount, + profile.Transcode, + profile.HardwareAcceleration, + profile.VaapiDriver, + profile.VaapiDevice, + Project(profile.Resolution), + profile.NormalizeVideo, + profile.VideoCodec, + profile.VideoBitrate, + profile.VideoBufferSize, + profile.AudioCodec, + profile.AudioBitrate, + profile.AudioBufferSize, + profile.NormalizeLoudness, + profile.AudioChannels, + profile.AudioSampleRate, + profile.NormalizeAudio, + profile.NormalizeVideo && profile.NormalizeFramerate); + + private static ResolutionViewModel Project(Resolution resolution) => + new(resolution.Id, resolution.Name, resolution.Width, resolution.Height); +} \ No newline at end of file diff --git a/ErsatzTV.Application/FFmpegProfiles/Queries/GetAllFFmpegProfiles.cs b/ErsatzTV.Application/FFmpegProfiles/Queries/GetAllFFmpegProfiles.cs index 90a3fdba0..7c6e39c3e 100644 --- a/ErsatzTV.Application/FFmpegProfiles/Queries/GetAllFFmpegProfiles.cs +++ b/ErsatzTV.Application/FFmpegProfiles/Queries/GetAllFFmpegProfiles.cs @@ -1,7 +1,3 @@ -using System.Collections.Generic; -using MediatR; +namespace ErsatzTV.Application.FFmpegProfiles; -namespace ErsatzTV.Application.FFmpegProfiles.Queries -{ - public record GetAllFFmpegProfiles : IRequest>; -} +public record GetAllFFmpegProfiles : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/FFmpegProfiles/Queries/GetAllFFmpegProfilesHandler.cs b/ErsatzTV.Application/FFmpegProfiles/Queries/GetAllFFmpegProfilesHandler.cs index 7a1a6f36f..c0ad5478f 100644 --- a/ErsatzTV.Application/FFmpegProfiles/Queries/GetAllFFmpegProfilesHandler.cs +++ b/ErsatzTV.Application/FFmpegProfiles/Queries/GetAllFFmpegProfilesHandler.cs @@ -1,31 +1,24 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Infrastructure.Data; -using LanguageExt; -using MediatR; +using ErsatzTV.Infrastructure.Data; using Microsoft.EntityFrameworkCore; using static ErsatzTV.Application.FFmpegProfiles.Mapper; -namespace ErsatzTV.Application.FFmpegProfiles.Queries +namespace ErsatzTV.Application.FFmpegProfiles; + +public class GetAllFFmpegProfilesHandler : IRequestHandler> { - public class GetAllFFmpegProfilesHandler : IRequestHandler> + private readonly IDbContextFactory _dbContextFactory; + + public GetAllFFmpegProfilesHandler(IDbContextFactory dbContextFactory) => + _dbContextFactory = dbContextFactory; + + public async Task> Handle( + GetAllFFmpegProfiles request, + CancellationToken cancellationToken) { - private readonly IDbContextFactory _dbContextFactory; - - public GetAllFFmpegProfilesHandler(IDbContextFactory dbContextFactory) => - _dbContextFactory = dbContextFactory; - - public async Task> Handle( - GetAllFFmpegProfiles request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - return await dbContext.FFmpegProfiles - .Include(p => p.Resolution) - .ToListAsync(cancellationToken) - .Map(list => list.Map(ProjectToViewModel).ToList()); - } + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + return await dbContext.FFmpegProfiles + .Include(p => p.Resolution) + .ToListAsync(cancellationToken) + .Map(list => list.Map(ProjectToViewModel).ToList()); } -} +} \ No newline at end of file diff --git a/ErsatzTV.Application/FFmpegProfiles/Queries/GetFFmpegProfileById.cs b/ErsatzTV.Application/FFmpegProfiles/Queries/GetFFmpegProfileById.cs index 206a19422..6a5404a73 100644 --- a/ErsatzTV.Application/FFmpegProfiles/Queries/GetFFmpegProfileById.cs +++ b/ErsatzTV.Application/FFmpegProfiles/Queries/GetFFmpegProfileById.cs @@ -1,7 +1,3 @@ -using LanguageExt; -using MediatR; +namespace ErsatzTV.Application.FFmpegProfiles; -namespace ErsatzTV.Application.FFmpegProfiles.Queries -{ - public record GetFFmpegProfileById(int Id) : IRequest>; -} +public record GetFFmpegProfileById(int Id) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/FFmpegProfiles/Queries/GetFFmpegProfileByIdHandler.cs b/ErsatzTV.Application/FFmpegProfiles/Queries/GetFFmpegProfileByIdHandler.cs index 239412ac8..f17fb3439 100644 --- a/ErsatzTV.Application/FFmpegProfiles/Queries/GetFFmpegProfileByIdHandler.cs +++ b/ErsatzTV.Application/FFmpegProfiles/Queries/GetFFmpegProfileByIdHandler.cs @@ -1,30 +1,25 @@ -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Infrastructure.Data; +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 +namespace ErsatzTV.Application.FFmpegProfiles; + +public class GetFFmpegProfileByIdHandler : IRequestHandler> { - public class GetFFmpegProfileByIdHandler : IRequestHandler> + private readonly IDbContextFactory _dbContextFactory; + + public GetFFmpegProfileByIdHandler(IDbContextFactory dbContextFactory) => + _dbContextFactory = dbContextFactory; + + public async Task> Handle( + GetFFmpegProfileById request, + CancellationToken cancellationToken) { - private readonly IDbContextFactory _dbContextFactory; - - public GetFFmpegProfileByIdHandler(IDbContextFactory dbContextFactory) => - _dbContextFactory = dbContextFactory; - - public async Task> Handle( - GetFFmpegProfileById request, - 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); - } + 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); } -} +} \ No newline at end of file diff --git a/ErsatzTV.Application/FFmpegProfiles/Queries/GetFFmpegSettings.cs b/ErsatzTV.Application/FFmpegProfiles/Queries/GetFFmpegSettings.cs index 167b7bf63..db95f2a8e 100644 --- a/ErsatzTV.Application/FFmpegProfiles/Queries/GetFFmpegSettings.cs +++ b/ErsatzTV.Application/FFmpegProfiles/Queries/GetFFmpegSettings.cs @@ -1,6 +1,3 @@ -using MediatR; +namespace ErsatzTV.Application.FFmpegProfiles; -namespace ErsatzTV.Application.FFmpegProfiles.Queries -{ - public record GetFFmpegSettings : IRequest; -} +public record GetFFmpegSettings : IRequest; \ No newline at end of file diff --git a/ErsatzTV.Application/FFmpegProfiles/Queries/GetFFmpegSettingsHandler.cs b/ErsatzTV.Application/FFmpegProfiles/Queries/GetFFmpegSettingsHandler.cs index 73694e2fe..df11fb109 100644 --- a/ErsatzTV.Application/FFmpegProfiles/Queries/GetFFmpegSettingsHandler.cs +++ b/ErsatzTV.Application/FFmpegProfiles/Queries/GetFFmpegSettingsHandler.cs @@ -1,68 +1,63 @@ -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.FFmpegProfiles.Queries +namespace ErsatzTV.Application.FFmpegProfiles; + +public class GetFFmpegSettingsHandler : IRequestHandler { - public class GetFFmpegSettingsHandler : IRequestHandler + private readonly IConfigElementRepository _configElementRepository; + + public GetFFmpegSettingsHandler(IConfigElementRepository configElementRepository) => + _configElementRepository = configElementRepository; + + public async Task Handle( + GetFFmpegSettings request, + CancellationToken cancellationToken) { - private readonly IConfigElementRepository _configElementRepository; + Option ffmpegPath = await _configElementRepository.GetValue(ConfigElementKey.FFmpegPath); + Option ffprobePath = await _configElementRepository.GetValue(ConfigElementKey.FFprobePath); + Option defaultFFmpegProfileId = + await _configElementRepository.GetValue(ConfigElementKey.FFmpegDefaultProfileId); + Option saveReports = + await _configElementRepository.GetValue(ConfigElementKey.FFmpegSaveReports); + Option preferredLanguageCode = + await _configElementRepository.GetValue(ConfigElementKey.FFmpegPreferredLanguageCode); + Option watermark = + await _configElementRepository.GetValue(ConfigElementKey.FFmpegGlobalWatermarkId); + Option fallbackFiller = + await _configElementRepository.GetValue(ConfigElementKey.FFmpegGlobalFallbackFillerId); + Option hlsSegmenterIdleTimeout = + await _configElementRepository.GetValue(ConfigElementKey.FFmpegSegmenterTimeout); + Option workAheadSegmenterLimit = + await _configElementRepository.GetValue(ConfigElementKey.FFmpegWorkAheadSegmenters); + Option initialSegmentCount = + await _configElementRepository.GetValue(ConfigElementKey.FFmpegInitialSegmentCount); + Option useLegacyTranscoder = + await _configElementRepository.GetValue(ConfigElementKey.FFmpegUseLegacyTranscoder); - public GetFFmpegSettingsHandler(IConfigElementRepository configElementRepository) => - _configElementRepository = configElementRepository; - - public async Task Handle( - GetFFmpegSettings request, - CancellationToken cancellationToken) + var result = new FFmpegSettingsViewModel { - Option ffmpegPath = await _configElementRepository.GetValue(ConfigElementKey.FFmpegPath); - Option ffprobePath = await _configElementRepository.GetValue(ConfigElementKey.FFprobePath); - Option defaultFFmpegProfileId = - await _configElementRepository.GetValue(ConfigElementKey.FFmpegDefaultProfileId); - Option saveReports = - await _configElementRepository.GetValue(ConfigElementKey.FFmpegSaveReports); - Option preferredLanguageCode = - await _configElementRepository.GetValue(ConfigElementKey.FFmpegPreferredLanguageCode); - Option watermark = - await _configElementRepository.GetValue(ConfigElementKey.FFmpegGlobalWatermarkId); - Option fallbackFiller = - await _configElementRepository.GetValue(ConfigElementKey.FFmpegGlobalFallbackFillerId); - Option hlsSegmenterIdleTimeout = - await _configElementRepository.GetValue(ConfigElementKey.FFmpegSegmenterTimeout); - Option workAheadSegmenterLimit = - await _configElementRepository.GetValue(ConfigElementKey.FFmpegWorkAheadSegmenters); - Option initialSegmentCount = - await _configElementRepository.GetValue(ConfigElementKey.FFmpegInitialSegmentCount); - Option useLegacyTranscoder = - await _configElementRepository.GetValue(ConfigElementKey.FFmpegUseLegacyTranscoder); + FFmpegPath = await ffmpegPath.IfNoneAsync(string.Empty), + FFprobePath = await ffprobePath.IfNoneAsync(string.Empty), + DefaultFFmpegProfileId = await defaultFFmpegProfileId.IfNoneAsync(0), + SaveReports = await saveReports.IfNoneAsync(false), + PreferredLanguageCode = await preferredLanguageCode.IfNoneAsync("eng"), + HlsSegmenterIdleTimeout = await hlsSegmenterIdleTimeout.IfNoneAsync(60), + WorkAheadSegmenterLimit = await workAheadSegmenterLimit.IfNoneAsync(1), + InitialSegmentCount = await initialSegmentCount.IfNoneAsync(1), + UseLegacyTranscoder = await useLegacyTranscoder.IfNoneAsync(false) + }; - var result = new FFmpegSettingsViewModel - { - FFmpegPath = await ffmpegPath.IfNoneAsync(string.Empty), - FFprobePath = await ffprobePath.IfNoneAsync(string.Empty), - DefaultFFmpegProfileId = await defaultFFmpegProfileId.IfNoneAsync(0), - SaveReports = await saveReports.IfNoneAsync(false), - PreferredLanguageCode = await preferredLanguageCode.IfNoneAsync("eng"), - HlsSegmenterIdleTimeout = await hlsSegmenterIdleTimeout.IfNoneAsync(60), - WorkAheadSegmenterLimit = await workAheadSegmenterLimit.IfNoneAsync(1), - InitialSegmentCount = await initialSegmentCount.IfNoneAsync(1), - UseLegacyTranscoder = await useLegacyTranscoder.IfNoneAsync(false) - }; - - foreach (int watermarkId in watermark) - { - result.GlobalWatermarkId = watermarkId; - } - - foreach (int fallbackFillerId in fallbackFiller) - { - result.GlobalFallbackFillerId = fallbackFillerId; - } - - return result; + foreach (int watermarkId in watermark) + { + result.GlobalWatermarkId = watermarkId; } + + foreach (int fallbackFillerId in fallbackFiller) + { + result.GlobalFallbackFillerId = fallbackFillerId; + } + + return result; } -} +} \ No newline at end of file diff --git a/ErsatzTV.Application/Filler/Commands/CreateFillerPreset.cs b/ErsatzTV.Application/Filler/Commands/CreateFillerPreset.cs index c5cf1d70a..aae977fb4 100644 --- a/ErsatzTV.Application/Filler/Commands/CreateFillerPreset.cs +++ b/ErsatzTV.Application/Filler/Commands/CreateFillerPreset.cs @@ -1,24 +1,19 @@ -using System; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Filler; -using LanguageExt; -using MediatR; -using Unit = LanguageExt.Unit; -namespace ErsatzTV.Application.Filler.Commands -{ - public record CreateFillerPreset( - string Name, - FillerKind FillerKind, - FillerMode FillerMode, - TimeSpan? Duration, - int? Count, - int? PadToNearestMinute, - ProgramScheduleItemCollectionType CollectionType, - int? CollectionId, - int? MediaItemId, - int? MultiCollectionId, - int? SmartCollectionId - ) : IRequest>; -} +namespace ErsatzTV.Application.Filler; + +public record CreateFillerPreset( + string Name, + FillerKind FillerKind, + FillerMode FillerMode, + TimeSpan? Duration, + int? Count, + int? PadToNearestMinute, + ProgramScheduleItemCollectionType CollectionType, + int? CollectionId, + int? MediaItemId, + int? MultiCollectionId, + int? SmartCollectionId +) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Filler/Commands/CreateFillerPresetHandler.cs b/ErsatzTV.Application/Filler/Commands/CreateFillerPresetHandler.cs index 2ee14c3f9..86f3540d7 100644 --- a/ErsatzTV.Application/Filler/Commands/CreateFillerPresetHandler.cs +++ b/ErsatzTV.Application/Filler/Commands/CreateFillerPresetHandler.cs @@ -1,55 +1,48 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain.Filler; using ErsatzTV.Infrastructure.Data; -using LanguageExt; -using MediatR; using Microsoft.EntityFrameworkCore; -using Unit = LanguageExt.Unit; -namespace ErsatzTV.Application.Filler.Commands +namespace ErsatzTV.Application.Filler; + +public class CreateFillerPresetHandler : IRequestHandler> { - public class CreateFillerPresetHandler : IRequestHandler> + private readonly IDbContextFactory _dbContextFactory; + + public CreateFillerPresetHandler(IDbContextFactory dbContextFactory) { - private readonly IDbContextFactory _dbContextFactory; + _dbContextFactory = dbContextFactory; + } - public CreateFillerPresetHandler(IDbContextFactory dbContextFactory) + public async Task> Handle(CreateFillerPreset request, CancellationToken cancellationToken) + { + try { - _dbContextFactory = dbContextFactory; + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + + var fillerPreset = new FillerPreset + { + Name = request.Name, + FillerKind = request.FillerKind, + FillerMode = request.FillerMode, + Duration = request.Duration, + Count = request.Count, + PadToNearestMinute = request.PadToNearestMinute, + CollectionType = request.CollectionType, + CollectionId = request.CollectionId, + MediaItemId = request.MediaItemId, + MultiCollectionId = request.MultiCollectionId, + SmartCollectionId = request.SmartCollectionId + }; + + await dbContext.FillerPresets.AddAsync(fillerPreset, cancellationToken); + await dbContext.SaveChangesAsync(cancellationToken); + + return Unit.Default; } - - public async Task> Handle(CreateFillerPreset request, CancellationToken cancellationToken) + catch (Exception ex) { - try - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - - var fillerPreset = new FillerPreset - { - Name = request.Name, - FillerKind = request.FillerKind, - FillerMode = request.FillerMode, - Duration = request.Duration, - Count = request.Count, - PadToNearestMinute = request.PadToNearestMinute, - CollectionType = request.CollectionType, - CollectionId = request.CollectionId, - MediaItemId = request.MediaItemId, - MultiCollectionId = request.MultiCollectionId, - SmartCollectionId = request.SmartCollectionId - }; - - await dbContext.FillerPresets.AddAsync(fillerPreset, cancellationToken); - await dbContext.SaveChangesAsync(cancellationToken); - - return Unit.Default; - } - catch (Exception ex) - { - return BaseError.New(ex.Message); - } + return BaseError.New(ex.Message); } } -} +} \ No newline at end of file diff --git a/ErsatzTV.Application/Filler/Commands/DeleteFillerPreset.cs b/ErsatzTV.Application/Filler/Commands/DeleteFillerPreset.cs index 69b66bd6b..cd5e64c77 100644 --- a/ErsatzTV.Application/Filler/Commands/DeleteFillerPreset.cs +++ b/ErsatzTV.Application/Filler/Commands/DeleteFillerPreset.cs @@ -1,8 +1,5 @@ using ErsatzTV.Core; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.Filler.Commands -{ - public record DeleteFillerPreset(int FillerPresetId) : IRequest>; -} +namespace ErsatzTV.Application.Filler; + +public record DeleteFillerPreset(int FillerPresetId) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Filler/Commands/DeleteFillerPresetHandler.cs b/ErsatzTV.Application/Filler/Commands/DeleteFillerPresetHandler.cs index 688002f26..0f030bc86 100644 --- a/ErsatzTV.Application/Filler/Commands/DeleteFillerPresetHandler.cs +++ b/ErsatzTV.Application/Filler/Commands/DeleteFillerPresetHandler.cs @@ -1,43 +1,37 @@ -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain.Filler; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; -using LanguageExt; -using MediatR; using Microsoft.EntityFrameworkCore; -using Unit = LanguageExt.Unit; -namespace ErsatzTV.Application.Filler.Commands +namespace ErsatzTV.Application.Filler; + +public class DeleteFillerPresetHandler : IRequestHandler> { - public class DeleteFillerPresetHandler : IRequestHandler> + private readonly IDbContextFactory _dbContextFactory; + + public DeleteFillerPresetHandler(IDbContextFactory dbContextFactory) => + _dbContextFactory = dbContextFactory; + + public async Task> Handle( + DeleteFillerPreset request, + CancellationToken cancellationToken) { - private readonly IDbContextFactory _dbContextFactory; - - public DeleteFillerPresetHandler(IDbContextFactory dbContextFactory) => - _dbContextFactory = dbContextFactory; - - public async Task> Handle( - DeleteFillerPreset request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - Validation validation = await FillerPresetMustExist(dbContext, request); - return await validation.Apply(ps => DoDeletion(dbContext, ps)); - } - - private static Task DoDeletion(TvContext dbContext, FillerPreset fillerPreset) - { - dbContext.FillerPresets.Remove(fillerPreset); - return dbContext.SaveChangesAsync().ToUnit(); - } - - private Task> FillerPresetMustExist( - TvContext dbContext, - DeleteFillerPreset request) => - dbContext.FillerPresets - .SelectOneAsync(fp => fp.Id, ps => ps.Id == request.FillerPresetId) - .Map(o => o.ToValidation($"FillerPreset {request.FillerPresetId} does not exist.")); + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + Validation validation = await FillerPresetMustExist(dbContext, request); + return await LanguageExtensions.Apply(validation, ps => DoDeletion(dbContext, ps)); } -} + + private static Task DoDeletion(TvContext dbContext, FillerPreset fillerPreset) + { + dbContext.FillerPresets.Remove(fillerPreset); + return dbContext.SaveChangesAsync().ToUnit(); + } + + private Task> FillerPresetMustExist( + TvContext dbContext, + DeleteFillerPreset request) => + dbContext.FillerPresets + .SelectOneAsync(fp => fp.Id, ps => ps.Id == request.FillerPresetId) + .Map(o => o.ToValidation($"FillerPreset {request.FillerPresetId} does not exist.")); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Filler/Commands/UpdateFillerPreset.cs b/ErsatzTV.Application/Filler/Commands/UpdateFillerPreset.cs index c6c5d8993..01169c5fd 100644 --- a/ErsatzTV.Application/Filler/Commands/UpdateFillerPreset.cs +++ b/ErsatzTV.Application/Filler/Commands/UpdateFillerPreset.cs @@ -1,25 +1,20 @@ -using System; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Filler; -using LanguageExt; -using MediatR; -using Unit = LanguageExt.Unit; -namespace ErsatzTV.Application.Filler.Commands -{ - public record UpdateFillerPreset( - int Id, - string Name, - FillerKind FillerKind, - FillerMode FillerMode, - TimeSpan? Duration, - int? Count, - int? PadToNearestMinute, - ProgramScheduleItemCollectionType CollectionType, - int? CollectionId, - int? MediaItemId, - int? MultiCollectionId, - int? SmartCollectionId - ) : IRequest>; -} +namespace ErsatzTV.Application.Filler; + +public record UpdateFillerPreset( + int Id, + string Name, + FillerKind FillerKind, + FillerMode FillerMode, + TimeSpan? Duration, + int? Count, + int? PadToNearestMinute, + ProgramScheduleItemCollectionType CollectionType, + int? CollectionId, + int? MediaItemId, + int? MultiCollectionId, + int? SmartCollectionId +) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Filler/Commands/UpdateFillerPresetHandler.cs b/ErsatzTV.Application/Filler/Commands/UpdateFillerPresetHandler.cs index 44a931c7f..30b73f828 100644 --- a/ErsatzTV.Application/Filler/Commands/UpdateFillerPresetHandler.cs +++ b/ErsatzTV.Application/Filler/Commands/UpdateFillerPresetHandler.cs @@ -1,60 +1,54 @@ -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain.Filler; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; -using LanguageExt; -using MediatR; using Microsoft.EntityFrameworkCore; -using Unit = LanguageExt.Unit; -namespace ErsatzTV.Application.Filler.Commands +namespace ErsatzTV.Application.Filler; + +public class UpdateFillerPresetHandler : IRequestHandler> { - public class UpdateFillerPresetHandler : IRequestHandler> + private readonly IDbContextFactory _dbContextFactory; + + public UpdateFillerPresetHandler(IDbContextFactory dbContextFactory) { - private readonly IDbContextFactory _dbContextFactory; - - public UpdateFillerPresetHandler(IDbContextFactory dbContextFactory) - { - _dbContextFactory = dbContextFactory; - } - - public async Task> Handle(UpdateFillerPreset request, CancellationToken cancellationToken) - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - - Validation validation = await FillerPresetMustExist(dbContext, request); - return await validation.Apply(ps => ApplyUpdateRequest(dbContext, ps, request)); - } - - private async Task ApplyUpdateRequest( - TvContext dbContext, - FillerPreset existing, - UpdateFillerPreset request) - { - existing.Name = request.Name; - existing.FillerKind = request.FillerKind; - existing.FillerMode = request.FillerMode; - existing.Duration = request.Duration; - existing.Count = request.Count; - existing.PadToNearestMinute = request.PadToNearestMinute; - existing.CollectionType = request.CollectionType; - existing.CollectionId = request.CollectionId; - existing.MediaItemId = request.MediaItemId; - existing.MultiCollectionId = request.MultiCollectionId; - existing.SmartCollectionId = request.SmartCollectionId; - - await dbContext.SaveChangesAsync(); - - return Unit.Default; - } - - private static Task> FillerPresetMustExist( - TvContext dbContext, - UpdateFillerPreset request) => - dbContext.FillerPresets - .SelectOneAsync(ps => ps.Id, ps => ps.Id == request.Id) - .Map(o => o.ToValidation("FillerPreset does not exist")); + _dbContextFactory = dbContextFactory; } -} + + public async Task> Handle(UpdateFillerPreset request, CancellationToken cancellationToken) + { + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + + Validation validation = await FillerPresetMustExist(dbContext, request); + return await LanguageExtensions.Apply(validation, ps => ApplyUpdateRequest(dbContext, ps, request)); + } + + private async Task ApplyUpdateRequest( + TvContext dbContext, + FillerPreset existing, + UpdateFillerPreset request) + { + existing.Name = request.Name; + existing.FillerKind = request.FillerKind; + existing.FillerMode = request.FillerMode; + existing.Duration = request.Duration; + existing.Count = request.Count; + existing.PadToNearestMinute = request.PadToNearestMinute; + existing.CollectionType = request.CollectionType; + existing.CollectionId = request.CollectionId; + existing.MediaItemId = request.MediaItemId; + existing.MultiCollectionId = request.MultiCollectionId; + existing.SmartCollectionId = request.SmartCollectionId; + + await dbContext.SaveChangesAsync(); + + return Unit.Default; + } + + private static Task> FillerPresetMustExist( + TvContext dbContext, + UpdateFillerPreset request) => + dbContext.FillerPresets + .SelectOneAsync(ps => ps.Id, ps => ps.Id == request.Id) + .Map(o => o.ToValidation("FillerPreset does not exist")); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Filler/FillerPresetViewModel.cs b/ErsatzTV.Application/Filler/FillerPresetViewModel.cs index 34af550c1..383fcade3 100644 --- a/ErsatzTV.Application/Filler/FillerPresetViewModel.cs +++ b/ErsatzTV.Application/Filler/FillerPresetViewModel.cs @@ -1,20 +1,18 @@ -using System; -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Filler; -namespace ErsatzTV.Application.Filler -{ - public record FillerPresetViewModel( - int Id, - string Name, - FillerKind FillerKind, - FillerMode FillerMode, - TimeSpan? Duration, - int? Count, - int? PadToNearestMinute, - ProgramScheduleItemCollectionType CollectionType, - int? CollectionId, - int? MediaItemId, - int? MultiCollectionId, - int? SmartCollectionId); -} +namespace ErsatzTV.Application.Filler; + +public record FillerPresetViewModel( + int Id, + string Name, + FillerKind FillerKind, + FillerMode FillerMode, + TimeSpan? Duration, + int? Count, + int? PadToNearestMinute, + ProgramScheduleItemCollectionType CollectionType, + int? CollectionId, + int? MediaItemId, + int? MultiCollectionId, + int? SmartCollectionId); \ No newline at end of file diff --git a/ErsatzTV.Application/Filler/Mapper.cs b/ErsatzTV.Application/Filler/Mapper.cs index 972e57442..f7ddb8fcf 100644 --- a/ErsatzTV.Application/Filler/Mapper.cs +++ b/ErsatzTV.Application/Filler/Mapper.cs @@ -1,22 +1,21 @@ using ErsatzTV.Core.Domain.Filler; -namespace ErsatzTV.Application.Filler +namespace ErsatzTV.Application.Filler; + +internal static class Mapper { - internal static class Mapper - { - internal static FillerPresetViewModel ProjectToViewModel(FillerPreset fillerPreset) => - new( - fillerPreset.Id, - fillerPreset.Name, - fillerPreset.FillerKind, - fillerPreset.FillerMode, - fillerPreset.Duration, - fillerPreset.Count, - fillerPreset.PadToNearestMinute, - fillerPreset.CollectionType, - fillerPreset.CollectionId, - fillerPreset.MediaItemId, - fillerPreset.MultiCollectionId, - fillerPreset.SmartCollectionId); - } -} + internal static FillerPresetViewModel ProjectToViewModel(FillerPreset fillerPreset) => + new( + fillerPreset.Id, + fillerPreset.Name, + fillerPreset.FillerKind, + fillerPreset.FillerMode, + fillerPreset.Duration, + fillerPreset.Count, + fillerPreset.PadToNearestMinute, + fillerPreset.CollectionType, + fillerPreset.CollectionId, + fillerPreset.MediaItemId, + fillerPreset.MultiCollectionId, + fillerPreset.SmartCollectionId); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Filler/PagedFillerPresetsViewModel.cs b/ErsatzTV.Application/Filler/PagedFillerPresetsViewModel.cs index ba4efb8f7..fcec726e3 100644 --- a/ErsatzTV.Application/Filler/PagedFillerPresetsViewModel.cs +++ b/ErsatzTV.Application/Filler/PagedFillerPresetsViewModel.cs @@ -1,6 +1,3 @@ -using System.Collections.Generic; +namespace ErsatzTV.Application.Filler; -namespace ErsatzTV.Application.Filler -{ - public record PagedFillerPresetsViewModel(int TotalCount, List Page); -} +public record PagedFillerPresetsViewModel(int TotalCount, List Page); \ No newline at end of file diff --git a/ErsatzTV.Application/Filler/Queries/GetAllFillerPresets.cs b/ErsatzTV.Application/Filler/Queries/GetAllFillerPresets.cs index ef5fff339..2b252f984 100644 --- a/ErsatzTV.Application/Filler/Queries/GetAllFillerPresets.cs +++ b/ErsatzTV.Application/Filler/Queries/GetAllFillerPresets.cs @@ -1,7 +1,3 @@ -using System.Collections.Generic; -using MediatR; +namespace ErsatzTV.Application.Filler; -namespace ErsatzTV.Application.Filler.Queries -{ - public record GetAllFillerPresets : IRequest>; -} +public record GetAllFillerPresets : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Filler/Queries/GetAllFillerPresetsHandler.cs b/ErsatzTV.Application/Filler/Queries/GetAllFillerPresetsHandler.cs index 5bea7fa1f..de9252b85 100644 --- a/ErsatzTV.Application/Filler/Queries/GetAllFillerPresetsHandler.cs +++ b/ErsatzTV.Application/Filler/Queries/GetAllFillerPresetsHandler.cs @@ -1,29 +1,22 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Infrastructure.Data; -using MediatR; +using ErsatzTV.Infrastructure.Data; using Microsoft.EntityFrameworkCore; -using LanguageExt; using static ErsatzTV.Application.Filler.Mapper; -namespace ErsatzTV.Application.Filler.Queries +namespace ErsatzTV.Application.Filler; + +public class GetAllFillerPresetsHandler : IRequestHandler> { - public class GetAllFillerPresetsHandler : IRequestHandler> + private readonly IDbContextFactory _dbContextFactory; + + public GetAllFillerPresetsHandler(IDbContextFactory dbContextFactory) => + _dbContextFactory = dbContextFactory; + + public async Task> Handle( + GetAllFillerPresets request, + CancellationToken cancellationToken) { - private readonly IDbContextFactory _dbContextFactory; - - public GetAllFillerPresetsHandler(IDbContextFactory dbContextFactory) => - _dbContextFactory = dbContextFactory; - - public async Task> Handle( - GetAllFillerPresets request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - return await dbContext.FillerPresets.ToListAsync(cancellationToken) - .Map(presets => presets.Map(ProjectToViewModel).ToList()); - } + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + return await dbContext.FillerPresets.ToListAsync(cancellationToken) + .Map(presets => presets.Map(ProjectToViewModel).ToList()); } -} +} \ No newline at end of file diff --git a/ErsatzTV.Application/Filler/Queries/GetFillerPresetById.cs b/ErsatzTV.Application/Filler/Queries/GetFillerPresetById.cs index 7ab7e8143..2ff167f60 100644 --- a/ErsatzTV.Application/Filler/Queries/GetFillerPresetById.cs +++ b/ErsatzTV.Application/Filler/Queries/GetFillerPresetById.cs @@ -1,7 +1,3 @@ -using LanguageExt; -using MediatR; +namespace ErsatzTV.Application.Filler; -namespace ErsatzTV.Application.Filler.Queries -{ - public record GetFillerPresetById(int Id) : IRequest>; -} +public record GetFillerPresetById(int Id) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Filler/Queries/GetFillerPresetByIdHandler.cs b/ErsatzTV.Application/Filler/Queries/GetFillerPresetByIdHandler.cs index 1d6500773..769779e03 100644 --- a/ErsatzTV.Application/Filler/Queries/GetFillerPresetByIdHandler.cs +++ b/ErsatzTV.Application/Filler/Queries/GetFillerPresetByIdHandler.cs @@ -1,29 +1,24 @@ -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Infrastructure.Data; +using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; -using LanguageExt; -using MediatR; using Microsoft.EntityFrameworkCore; using static ErsatzTV.Application.Filler.Mapper; -namespace ErsatzTV.Application.Filler.Queries +namespace ErsatzTV.Application.Filler; + +public class GetFillerPresetByIdHandler : IRequestHandler> { - public class GetFillerPresetByIdHandler : IRequestHandler> + private readonly IDbContextFactory _dbContextFactory; + + public GetFillerPresetByIdHandler(IDbContextFactory dbContextFactory) => + _dbContextFactory = dbContextFactory; + + public async Task> Handle( + GetFillerPresetById request, + CancellationToken cancellationToken) { - private readonly IDbContextFactory _dbContextFactory; - - public GetFillerPresetByIdHandler(IDbContextFactory dbContextFactory) => - _dbContextFactory = dbContextFactory; - - public async Task> Handle( - GetFillerPresetById request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - return await dbContext.FillerPresets - .SelectOneAsync(c => c.Id, c => c.Id == request.Id) - .MapT(ProjectToViewModel); - } + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + return await dbContext.FillerPresets + .SelectOneAsync(c => c.Id, c => c.Id == request.Id) + .MapT(ProjectToViewModel); } -} +} \ No newline at end of file diff --git a/ErsatzTV.Application/Filler/Queries/GetPagedFillerPresets.cs b/ErsatzTV.Application/Filler/Queries/GetPagedFillerPresets.cs index ed068508c..c3938857a 100644 --- a/ErsatzTV.Application/Filler/Queries/GetPagedFillerPresets.cs +++ b/ErsatzTV.Application/Filler/Queries/GetPagedFillerPresets.cs @@ -1,6 +1,3 @@ -using MediatR; +namespace ErsatzTV.Application.Filler; -namespace ErsatzTV.Application.Filler.Queries -{ - public record GetPagedFillerPresets(int PageNum, int PageSize) : IRequest; -} +public record GetPagedFillerPresets(int PageNum, int PageSize) : IRequest; \ No newline at end of file diff --git a/ErsatzTV.Application/Filler/Queries/GetPagedFillerPresetsHandler.cs b/ErsatzTV.Application/Filler/Queries/GetPagedFillerPresetsHandler.cs index 8bb57db20..279d081de 100644 --- a/ErsatzTV.Application/Filler/Queries/GetPagedFillerPresetsHandler.cs +++ b/ErsatzTV.Application/Filler/Queries/GetPagedFillerPresetsHandler.cs @@ -1,46 +1,39 @@ -using System.Collections.Generic; -using System.Data; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; +using System.Data; using Dapper; using ErsatzTV.Infrastructure.Data; -using MediatR; using Microsoft.EntityFrameworkCore; -using LanguageExt; using static ErsatzTV.Application.Filler.Mapper; -namespace ErsatzTV.Application.Filler.Queries +namespace ErsatzTV.Application.Filler; + +public class GetPagedFillerPresetsHandler : IRequestHandler { - public class GetPagedFillerPresetsHandler : IRequestHandler + private readonly IDbConnection _dbConnection; + private readonly IDbContextFactory _dbContextFactory; + + public GetPagedFillerPresetsHandler(IDbContextFactory dbContextFactory, IDbConnection dbConnection) { - private readonly IDbConnection _dbConnection; - private readonly IDbContextFactory _dbContextFactory; + _dbContextFactory = dbContextFactory; + _dbConnection = dbConnection; + } - public GetPagedFillerPresetsHandler(IDbContextFactory dbContextFactory, IDbConnection dbConnection) - { - _dbContextFactory = dbContextFactory; - _dbConnection = dbConnection; - } + public async Task Handle( + GetPagedFillerPresets request, + CancellationToken cancellationToken) + { + int count = await _dbConnection.QuerySingleAsync(@"SELECT COUNT (*) FROM FillerPreset"); - public async Task Handle( - GetPagedFillerPresets request, - CancellationToken cancellationToken) - { - int count = await _dbConnection.QuerySingleAsync(@"SELECT COUNT (*) FROM FillerPreset"); - - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - List page = await dbContext.FillerPresets.FromSqlRaw( - @"SELECT * FROM FillerPreset + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + List page = await dbContext.FillerPresets.FromSqlRaw( + @"SELECT * FROM FillerPreset ORDER BY Name COLLATE NOCASE LIMIT {0} OFFSET {1}", - request.PageSize, - request.PageNum * request.PageSize) - .ToListAsync(cancellationToken) - .Map(list => list.Map(ProjectToViewModel).ToList()); + request.PageSize, + request.PageNum * request.PageSize) + .ToListAsync(cancellationToken) + .Map(list => list.Map(ProjectToViewModel).ToList()); - return new PagedFillerPresetsViewModel(count, page); - } + return new PagedFillerPresetsViewModel(count, page); } -} +} \ No newline at end of file diff --git a/ErsatzTV.Application/GlobalUsings.cs b/ErsatzTV.Application/GlobalUsings.cs new file mode 100644 index 000000000..72fed1436 --- /dev/null +++ b/ErsatzTV.Application/GlobalUsings.cs @@ -0,0 +1,5 @@ +global using LanguageExt; +global using MediatR; +global using static LanguageExt.Prelude; +global using Unit = LanguageExt.Unit; +global using Array = System.Array; \ No newline at end of file diff --git a/ErsatzTV.Application/HDHR/Commands/UpdateHDHRTunerCount.cs b/ErsatzTV.Application/HDHR/Commands/UpdateHDHRTunerCount.cs index 68379df93..b19735ce8 100644 --- a/ErsatzTV.Application/HDHR/Commands/UpdateHDHRTunerCount.cs +++ b/ErsatzTV.Application/HDHR/Commands/UpdateHDHRTunerCount.cs @@ -1,7 +1,5 @@ using ErsatzTV.Core; -using LanguageExt; -namespace ErsatzTV.Application.HDHR.Commands -{ - public record UpdateHDHRTunerCount(int TunerCount) : MediatR.IRequest>; -} +namespace ErsatzTV.Application.HDHR; + +public record UpdateHDHRTunerCount(int TunerCount) : MediatR.IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/HDHR/Commands/UpdateHDHRTunerCountHandler.cs b/ErsatzTV.Application/HDHR/Commands/UpdateHDHRTunerCountHandler.cs index 6c31ae15e..1b62bfb9a 100644 --- a/ErsatzTV.Application/HDHR/Commands/UpdateHDHRTunerCountHandler.cs +++ b/ErsatzTV.Application/HDHR/Commands/UpdateHDHRTunerCountHandler.cs @@ -1,32 +1,27 @@ -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -using static LanguageExt.Prelude; -namespace ErsatzTV.Application.HDHR.Commands +namespace ErsatzTV.Application.HDHR; + +public class UpdateHDHRTunerCountHandler : MediatR.IRequestHandler> { - public class UpdateHDHRTunerCountHandler : MediatR.IRequestHandler> - { - private readonly IConfigElementRepository _configElementRepository; + private readonly IConfigElementRepository _configElementRepository; - public UpdateHDHRTunerCountHandler(IConfigElementRepository configElementRepository) => - _configElementRepository = configElementRepository; + public UpdateHDHRTunerCountHandler(IConfigElementRepository configElementRepository) => + _configElementRepository = configElementRepository; - public Task> Handle( - UpdateHDHRTunerCount request, - CancellationToken cancellationToken) => - Validate(request) - .MapT(_ => _configElementRepository.Upsert(ConfigElementKey.HDHRTunerCount, request.TunerCount.ToString())) - .Bind(v => v.ToEitherAsync()); + public Task> Handle( + UpdateHDHRTunerCount request, + CancellationToken cancellationToken) => + Validate(request) + .MapT(_ => _configElementRepository.Upsert(ConfigElementKey.HDHRTunerCount, request.TunerCount.ToString())) + .Bind(v => v.ToEitherAsync()); - private static Task> Validate(UpdateHDHRTunerCount request) => - Optional(request.TunerCount) - .Where(tc => tc > 0) - .Map(_ => Unit.Default) - .ToValidation("Tuner count must be greater than zero") - .AsTask(); - } -} + private static Task> Validate(UpdateHDHRTunerCount request) => + Optional(request.TunerCount) + .Where(tc => tc > 0) + .Map(_ => Unit.Default) + .ToValidation("Tuner count must be greater than zero") + .AsTask(); +} \ No newline at end of file diff --git a/ErsatzTV.Application/HDHR/Queries/GetHDHRTunerCount.cs b/ErsatzTV.Application/HDHR/Queries/GetHDHRTunerCount.cs index 97f242748..79f03b216 100644 --- a/ErsatzTV.Application/HDHR/Queries/GetHDHRTunerCount.cs +++ b/ErsatzTV.Application/HDHR/Queries/GetHDHRTunerCount.cs @@ -1,6 +1,3 @@ -using MediatR; +namespace ErsatzTV.Application.HDHR; -namespace ErsatzTV.Application.HDHR.Queries -{ - public record GetHDHRTunerCount : IRequest; -} +public record GetHDHRTunerCount : IRequest; \ No newline at end of file diff --git a/ErsatzTV.Application/HDHR/Queries/GetHDHRTunerCountHandler.cs b/ErsatzTV.Application/HDHR/Queries/GetHDHRTunerCountHandler.cs index e5c3528eb..ef5f4f236 100644 --- a/ErsatzTV.Application/HDHR/Queries/GetHDHRTunerCountHandler.cs +++ b/ErsatzTV.Application/HDHR/Queries/GetHDHRTunerCountHandler.cs @@ -1,21 +1,16 @@ -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.HDHR.Queries +namespace ErsatzTV.Application.HDHR; + +public class GetHDHRTunerCountHandler : IRequestHandler { - public class GetHDHRTunerCountHandler : IRequestHandler - { - private readonly IConfigElementRepository _configElementRepository; + private readonly IConfigElementRepository _configElementRepository; - public GetHDHRTunerCountHandler(IConfigElementRepository configElementRepository) => - _configElementRepository = configElementRepository; + public GetHDHRTunerCountHandler(IConfigElementRepository configElementRepository) => + _configElementRepository = configElementRepository; - public Task Handle(GetHDHRTunerCount request, CancellationToken cancellationToken) => - _configElementRepository.GetValue(ConfigElementKey.HDHRTunerCount) - .Map(result => result.IfNone(2)); - } -} + public Task Handle(GetHDHRTunerCount request, CancellationToken cancellationToken) => + _configElementRepository.GetValue(ConfigElementKey.HDHRTunerCount) + .Map(result => result.IfNone(2)); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Health/Queries/GetAllHealthCheckResults.cs b/ErsatzTV.Application/Health/Queries/GetAllHealthCheckResults.cs index e8fccba0f..269f46489 100644 --- a/ErsatzTV.Application/Health/Queries/GetAllHealthCheckResults.cs +++ b/ErsatzTV.Application/Health/Queries/GetAllHealthCheckResults.cs @@ -1,8 +1,5 @@ -using System.Collections.Generic; -using ErsatzTV.Core.Health; -using MediatR; +using ErsatzTV.Core.Health; -namespace ErsatzTV.Application.Health.Queries -{ - public record GetAllHealthCheckResults : IRequest>; -} +namespace ErsatzTV.Application.Health; + +public record GetAllHealthCheckResults : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Health/Queries/GetAllHealthCheckResultsHandler.cs b/ErsatzTV.Application/Health/Queries/GetAllHealthCheckResultsHandler.cs index 4aeec64d8..c7ca03423 100644 --- a/ErsatzTV.Application/Health/Queries/GetAllHealthCheckResultsHandler.cs +++ b/ErsatzTV.Application/Health/Queries/GetAllHealthCheckResultsHandler.cs @@ -1,25 +1,19 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core.Health; -using MediatR; +using ErsatzTV.Core.Health; -namespace ErsatzTV.Application.Health.Queries +namespace ErsatzTV.Application.Health; + +public class GetAllHealthCheckResultsHandler : IRequestHandler> { - public class GetAllHealthCheckResultsHandler : IRequestHandler> + private readonly IHealthCheckService _healthCheckService; + + public GetAllHealthCheckResultsHandler(IHealthCheckService healthCheckService) => + _healthCheckService = healthCheckService; + + public async Task> Handle( + GetAllHealthCheckResults request, + CancellationToken cancellationToken) { - private readonly IHealthCheckService _healthCheckService; - - public GetAllHealthCheckResultsHandler(IHealthCheckService healthCheckService) => - _healthCheckService = healthCheckService; - - public async Task> Handle( - GetAllHealthCheckResults request, - CancellationToken cancellationToken) - { - List results = await _healthCheckService.PerformHealthChecks(); - return results.Filter(r => r.Status != HealthCheckStatus.NotApplicable).ToList(); - } + List results = await _healthCheckService.PerformHealthChecks(); + return results.Filter(r => r.Status != HealthCheckStatus.NotApplicable).ToList(); } -} +} \ No newline at end of file diff --git a/ErsatzTV.Application/IBackgroundServiceRequest.cs b/ErsatzTV.Application/IBackgroundServiceRequest.cs index 7854670db..d86df59a6 100644 --- a/ErsatzTV.Application/IBackgroundServiceRequest.cs +++ b/ErsatzTV.Application/IBackgroundServiceRequest.cs @@ -1,6 +1,5 @@ -namespace ErsatzTV.Application +namespace ErsatzTV.Application; + +public interface IBackgroundServiceRequest { - public interface IBackgroundServiceRequest - { - } -} +} \ No newline at end of file diff --git a/ErsatzTV.Application/IEmbyBackgroundServiceRequest.cs b/ErsatzTV.Application/IEmbyBackgroundServiceRequest.cs index b35664529..766521731 100644 --- a/ErsatzTV.Application/IEmbyBackgroundServiceRequest.cs +++ b/ErsatzTV.Application/IEmbyBackgroundServiceRequest.cs @@ -1,6 +1,5 @@ -namespace ErsatzTV.Application +namespace ErsatzTV.Application; + +public interface IEmbyBackgroundServiceRequest { - public interface IEmbyBackgroundServiceRequest - { - } -} +} \ No newline at end of file diff --git a/ErsatzTV.Application/IFFmpegWorkerRequest.cs b/ErsatzTV.Application/IFFmpegWorkerRequest.cs index 3cc6edf06..a95ac18d8 100644 --- a/ErsatzTV.Application/IFFmpegWorkerRequest.cs +++ b/ErsatzTV.Application/IFFmpegWorkerRequest.cs @@ -1,6 +1,5 @@ -namespace ErsatzTV.Application +namespace ErsatzTV.Application; + +public interface IFFmpegWorkerRequest { - public interface IFFmpegWorkerRequest - { - } -} +} \ No newline at end of file diff --git a/ErsatzTV.Application/IJellyfinBackgroundServiceRequest.cs b/ErsatzTV.Application/IJellyfinBackgroundServiceRequest.cs index d976852ef..b6e844585 100644 --- a/ErsatzTV.Application/IJellyfinBackgroundServiceRequest.cs +++ b/ErsatzTV.Application/IJellyfinBackgroundServiceRequest.cs @@ -1,6 +1,5 @@ -namespace ErsatzTV.Application +namespace ErsatzTV.Application; + +public interface IJellyfinBackgroundServiceRequest { - public interface IJellyfinBackgroundServiceRequest - { - } -} +} \ No newline at end of file diff --git a/ErsatzTV.Application/IPlexBackgroundServiceRequest.cs b/ErsatzTV.Application/IPlexBackgroundServiceRequest.cs index b80ccea84..467bf9bfc 100644 --- a/ErsatzTV.Application/IPlexBackgroundServiceRequest.cs +++ b/ErsatzTV.Application/IPlexBackgroundServiceRequest.cs @@ -1,6 +1,5 @@ -namespace ErsatzTV.Application +namespace ErsatzTV.Application; + +public interface IPlexBackgroundServiceRequest { - public interface IPlexBackgroundServiceRequest - { - } -} +} \ No newline at end of file diff --git a/ErsatzTV.Application/Images/CachedImagePathViewModel.cs b/ErsatzTV.Application/Images/CachedImagePathViewModel.cs index bc276043a..0a1f9a1e4 100644 --- a/ErsatzTV.Application/Images/CachedImagePathViewModel.cs +++ b/ErsatzTV.Application/Images/CachedImagePathViewModel.cs @@ -1,4 +1,3 @@ -namespace ErsatzTV.Application.Images -{ - public record CachedImagePathViewModel(string FileName, string MimeType); -} +namespace ErsatzTV.Application.Images; + +public record CachedImagePathViewModel(string FileName, string MimeType); \ No newline at end of file diff --git a/ErsatzTV.Application/Images/Commands/SaveArtworkToDisk.cs b/ErsatzTV.Application/Images/Commands/SaveArtworkToDisk.cs index c431b7541..3a4b34b92 100644 --- a/ErsatzTV.Application/Images/Commands/SaveArtworkToDisk.cs +++ b/ErsatzTV.Application/Images/Commands/SaveArtworkToDisk.cs @@ -1,11 +1,7 @@ -using System.IO; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.Images.Commands -{ - // ReSharper disable once SuggestBaseTypeForParameter - public record SaveArtworkToDisk(Stream Stream, ArtworkKind ArtworkKind) : IRequest>; -} +namespace ErsatzTV.Application.Images; + +// ReSharper disable once SuggestBaseTypeForParameter +public record SaveArtworkToDisk(Stream Stream, ArtworkKind ArtworkKind) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Images/Commands/SaveArtworkToDiskHandler.cs b/ErsatzTV.Application/Images/Commands/SaveArtworkToDiskHandler.cs index dc7caa57d..d4d458957 100644 --- a/ErsatzTV.Application/Images/Commands/SaveArtworkToDiskHandler.cs +++ b/ErsatzTV.Application/Images/Commands/SaveArtworkToDiskHandler.cs @@ -1,19 +1,14 @@ -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Interfaces.Images; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.Images.Commands +namespace ErsatzTV.Application.Images; + +public class SaveArtworkToDiskHandler : IRequestHandler> { - public class SaveArtworkToDiskHandler : IRequestHandler> - { - private readonly IImageCache _imageCache; + private readonly IImageCache _imageCache; - public SaveArtworkToDiskHandler(IImageCache imageCache) => _imageCache = imageCache; + public SaveArtworkToDiskHandler(IImageCache imageCache) => _imageCache = imageCache; - public Task> Handle(SaveArtworkToDisk request, CancellationToken cancellationToken) => - _imageCache.SaveArtworkToCache(request.Stream, request.ArtworkKind); - } -} + public Task> Handle(SaveArtworkToDisk request, CancellationToken cancellationToken) => + _imageCache.SaveArtworkToCache(request.Stream, request.ArtworkKind); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Images/Queries/GetCachedImagePath.cs b/ErsatzTV.Application/Images/Queries/GetCachedImagePath.cs index f8996757f..ea7d98e20 100644 --- a/ErsatzTV.Application/Images/Queries/GetCachedImagePath.cs +++ b/ErsatzTV.Application/Images/Queries/GetCachedImagePath.cs @@ -1,11 +1,8 @@ using ErsatzTV.Core; using ErsatzTV.Core.Domain; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.Images.Queries -{ - public record GetCachedImagePath - (string FileName, ArtworkKind ArtworkKind, int? MaxHeight = null) : IRequest< - Either>; -} +namespace ErsatzTV.Application.Images; + +public record GetCachedImagePath + (string FileName, ArtworkKind ArtworkKind, int? MaxHeight = null) : IRequest< + Either>; \ No newline at end of file diff --git a/ErsatzTV.Application/Images/Queries/GetCachedImagePathHandler.cs b/ErsatzTV.Application/Images/Queries/GetCachedImagePathHandler.cs index a8c6585da..7e845828d 100644 --- a/ErsatzTV.Application/Images/Queries/GetCachedImagePathHandler.cs +++ b/ErsatzTV.Application/Images/Queries/GetCachedImagePathHandler.cs @@ -1,72 +1,64 @@ -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Interfaces.Images; -using LanguageExt; -using MediatR; using Winista.Mime; -using static LanguageExt.Prelude; -namespace ErsatzTV.Application.Images.Queries +namespace ErsatzTV.Application.Images; + +public class + GetCachedImagePathHandler : IRequestHandler> { - public class - GetCachedImagePathHandler : IRequestHandler> + private static readonly MimeTypes MimeTypes = new(); + private readonly IImageCache _imageCache; + + public GetCachedImagePathHandler(IImageCache imageCache) => _imageCache = imageCache; + + public async Task> Handle( + GetCachedImagePath request, + CancellationToken cancellationToken) { - private static readonly MimeTypes MimeTypes = new(); - private readonly IImageCache _imageCache; - - public GetCachedImagePathHandler(IImageCache imageCache) => _imageCache = imageCache; - - public async Task> Handle( - GetCachedImagePath request, - CancellationToken cancellationToken) + try { - try + MimeType mimeType; + + string cachePath = _imageCache.GetPathForImage( + request.FileName, + request.ArtworkKind, + Optional(request.MaxHeight)); + if (!File.Exists(cachePath)) { - MimeType mimeType; - - string cachePath = _imageCache.GetPathForImage( - request.FileName, - request.ArtworkKind, - Optional(request.MaxHeight)); - if (!File.Exists(cachePath)) + if (request.MaxHeight.HasValue) { - if (request.MaxHeight.HasValue) + string originalPath = _imageCache.GetPathForImage(request.FileName, request.ArtworkKind, None); + byte[] contents = await File.ReadAllBytesAsync(originalPath, cancellationToken); + Either resizeResult = + await _imageCache.ResizeImage(contents, request.MaxHeight.Value); + resizeResult.IfRight(result => contents = result); + + string baseFolder = Path.GetDirectoryName(cachePath); + if (baseFolder != null && !Directory.Exists(baseFolder)) { - string originalPath = _imageCache.GetPathForImage(request.FileName, request.ArtworkKind, None); - byte[] contents = await File.ReadAllBytesAsync(originalPath, cancellationToken); - Either resizeResult = - await _imageCache.ResizeImage(contents, request.MaxHeight.Value); - resizeResult.IfRight(result => contents = result); - - string baseFolder = Path.GetDirectoryName(cachePath); - if (baseFolder != null && !Directory.Exists(baseFolder)) - { - Directory.CreateDirectory(baseFolder); - } - - await File.WriteAllBytesAsync(cachePath, contents, cancellationToken); - - mimeType = new MimeType("image/jpeg"); - } - else - { - return BaseError.New($"Artwork does not exist on disk at {cachePath}"); + Directory.CreateDirectory(baseFolder); } + + await File.WriteAllBytesAsync(cachePath, contents, cancellationToken); + + mimeType = new MimeType("image/jpeg"); } else { - mimeType = MimeTypes.GetMimeTypeFromFile(cachePath); + return BaseError.New($"Artwork does not exist on disk at {cachePath}"); } - - return new CachedImagePathViewModel(cachePath, mimeType.Name); } - catch (Exception ex) + else { - return BaseError.New(ex.Message); + mimeType = MimeTypes.GetMimeTypeFromFile(cachePath); } + + return new CachedImagePathViewModel(cachePath, mimeType.Name); + } + catch (Exception ex) + { + return BaseError.New(ex.Message); } } -} +} \ No newline at end of file diff --git a/ErsatzTV.Application/Jellyfin/Commands/DisconnectJellyfin.cs b/ErsatzTV.Application/Jellyfin/Commands/DisconnectJellyfin.cs index df438ec71..dd651ff51 100644 --- a/ErsatzTV.Application/Jellyfin/Commands/DisconnectJellyfin.cs +++ b/ErsatzTV.Application/Jellyfin/Commands/DisconnectJellyfin.cs @@ -1,7 +1,5 @@ using ErsatzTV.Core; -using LanguageExt; -namespace ErsatzTV.Application.Jellyfin.Commands -{ - public record DisconnectJellyfin : MediatR.IRequest>; -} +namespace ErsatzTV.Application.Jellyfin; + +public record DisconnectJellyfin : MediatR.IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Jellyfin/Commands/DisconnectJellyfinHandler.cs b/ErsatzTV.Application/Jellyfin/Commands/DisconnectJellyfinHandler.cs index 26452d3e4..bb286ac83 100644 --- a/ErsatzTV.Application/Jellyfin/Commands/DisconnectJellyfinHandler.cs +++ b/ErsatzTV.Application/Jellyfin/Commands/DisconnectJellyfinHandler.cs @@ -1,46 +1,41 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Jellyfin; using ErsatzTV.Core.Interfaces.Locking; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Search; -using LanguageExt; -namespace ErsatzTV.Application.Jellyfin.Commands +namespace ErsatzTV.Application.Jellyfin; + +public class DisconnectJellyfinHandler : MediatR.IRequestHandler> { - public class DisconnectJellyfinHandler : MediatR.IRequestHandler> + private readonly IEntityLocker _entityLocker; + private readonly IJellyfinSecretStore _jellyfinSecretStore; + private readonly IMediaSourceRepository _mediaSourceRepository; + private readonly ISearchIndex _searchIndex; + + public DisconnectJellyfinHandler( + IMediaSourceRepository mediaSourceRepository, + IJellyfinSecretStore jellyfinSecretStore, + IEntityLocker entityLocker, + ISearchIndex searchIndex) { - private readonly IEntityLocker _entityLocker; - private readonly IJellyfinSecretStore _jellyfinSecretStore; - private readonly IMediaSourceRepository _mediaSourceRepository; - private readonly ISearchIndex _searchIndex; - - public DisconnectJellyfinHandler( - IMediaSourceRepository mediaSourceRepository, - IJellyfinSecretStore jellyfinSecretStore, - IEntityLocker entityLocker, - ISearchIndex searchIndex) - { - _mediaSourceRepository = mediaSourceRepository; - _jellyfinSecretStore = jellyfinSecretStore; - _entityLocker = entityLocker; - _searchIndex = searchIndex; - } - - public async Task> Handle( - DisconnectJellyfin request, - CancellationToken cancellationToken) - { - List ids = await _mediaSourceRepository.DeleteAllJellyfin(); - await _searchIndex.RemoveItems(ids); - _searchIndex.Commit(); - await _jellyfinSecretStore.DeleteAll(); - _entityLocker.UnlockRemoteMediaSource(); - - return Unit.Default; - } + _mediaSourceRepository = mediaSourceRepository; + _jellyfinSecretStore = jellyfinSecretStore; + _entityLocker = entityLocker; + _searchIndex = searchIndex; } -} + + public async Task> Handle( + DisconnectJellyfin request, + CancellationToken cancellationToken) + { + List ids = await _mediaSourceRepository.DeleteAllJellyfin(); + await _searchIndex.RemoveItems(ids); + _searchIndex.Commit(); + await _jellyfinSecretStore.DeleteAll(); + _entityLocker.UnlockRemoteMediaSource(); + + return Unit.Default; + } +} \ No newline at end of file diff --git a/ErsatzTV.Application/Jellyfin/Commands/SaveJellyfinSecrets.cs b/ErsatzTV.Application/Jellyfin/Commands/SaveJellyfinSecrets.cs index 024500ac5..bc154cecb 100644 --- a/ErsatzTV.Application/Jellyfin/Commands/SaveJellyfinSecrets.cs +++ b/ErsatzTV.Application/Jellyfin/Commands/SaveJellyfinSecrets.cs @@ -1,8 +1,6 @@ using ErsatzTV.Core; using ErsatzTV.Core.Jellyfin; -using LanguageExt; -namespace ErsatzTV.Application.Jellyfin.Commands -{ - public record SaveJellyfinSecrets(JellyfinSecrets Secrets) : MediatR.IRequest>; -} +namespace ErsatzTV.Application.Jellyfin; + +public record SaveJellyfinSecrets(JellyfinSecrets Secrets) : MediatR.IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Jellyfin/Commands/SaveJellyfinSecretsHandler.cs b/ErsatzTV.Application/Jellyfin/Commands/SaveJellyfinSecretsHandler.cs index a00b83f5a..3d6f0226b 100644 --- a/ErsatzTV.Application/Jellyfin/Commands/SaveJellyfinSecretsHandler.cs +++ b/ErsatzTV.Application/Jellyfin/Commands/SaveJellyfinSecretsHandler.cs @@ -1,60 +1,56 @@ -using System.Threading; -using System.Threading.Channels; -using System.Threading.Tasks; +using System.Threading.Channels; using ErsatzTV.Core; using ErsatzTV.Core.Interfaces.Jellyfin; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Jellyfin; -using LanguageExt; -namespace ErsatzTV.Application.Jellyfin.Commands +namespace ErsatzTV.Application.Jellyfin; + +public class SaveJellyfinSecretsHandler : MediatR.IRequestHandler> { - public class SaveJellyfinSecretsHandler : MediatR.IRequestHandler> + private readonly ChannelWriter _channel; + private readonly IJellyfinApiClient _jellyfinApiClient; + private readonly IJellyfinSecretStore _jellyfinSecretStore; + private readonly IMediaSourceRepository _mediaSourceRepository; + + public SaveJellyfinSecretsHandler( + IJellyfinSecretStore jellyfinSecretStore, + IJellyfinApiClient jellyfinApiClient, + IMediaSourceRepository mediaSourceRepository, + ChannelWriter channel) { - private readonly ChannelWriter _channel; - private readonly IJellyfinApiClient _jellyfinApiClient; - private readonly IJellyfinSecretStore _jellyfinSecretStore; - private readonly IMediaSourceRepository _mediaSourceRepository; - - public SaveJellyfinSecretsHandler( - IJellyfinSecretStore jellyfinSecretStore, - IJellyfinApiClient jellyfinApiClient, - IMediaSourceRepository mediaSourceRepository, - ChannelWriter channel) - { - _jellyfinSecretStore = jellyfinSecretStore; - _jellyfinApiClient = jellyfinApiClient; - _mediaSourceRepository = mediaSourceRepository; - _channel = channel; - } - - public Task> Handle(SaveJellyfinSecrets request, CancellationToken cancellationToken) => - Validate(request) - .MapT(PerformSave) - .Bind(v => v.ToEitherAsync()); - - private async Task> Validate(SaveJellyfinSecrets request) - { - Either maybeServerInformation = await _jellyfinApiClient - .GetServerInformation(request.Secrets.Address, request.Secrets.ApiKey); - - return maybeServerInformation.Match( - info => Validation.Success(new Parameters(request.Secrets, info)), - error => error); - } - - private async Task PerformSave(Parameters parameters) - { - await _jellyfinSecretStore.SaveSecrets(parameters.Secrets); - await _mediaSourceRepository.UpsertJellyfin( - parameters.Secrets.Address, - parameters.ServerInformation.ServerName, - parameters.ServerInformation.OperatingSystem); - await _channel.WriteAsync(new SynchronizeJellyfinMediaSources()); - - return Unit.Default; - } - - private record Parameters(JellyfinSecrets Secrets, JellyfinServerInformation ServerInformation); + _jellyfinSecretStore = jellyfinSecretStore; + _jellyfinApiClient = jellyfinApiClient; + _mediaSourceRepository = mediaSourceRepository; + _channel = channel; } -} + + public Task> Handle(SaveJellyfinSecrets request, CancellationToken cancellationToken) => + Validate(request) + .MapT(PerformSave) + .Bind(v => v.ToEitherAsync()); + + private async Task> Validate(SaveJellyfinSecrets request) + { + Either maybeServerInformation = await _jellyfinApiClient + .GetServerInformation(request.Secrets.Address, request.Secrets.ApiKey); + + return maybeServerInformation.Match( + info => Validation.Success(new Parameters(request.Secrets, info)), + error => error); + } + + private async Task PerformSave(Parameters parameters) + { + await _jellyfinSecretStore.SaveSecrets(parameters.Secrets); + await _mediaSourceRepository.UpsertJellyfin( + parameters.Secrets.Address, + parameters.ServerInformation.ServerName, + parameters.ServerInformation.OperatingSystem); + await _channel.WriteAsync(new SynchronizeJellyfinMediaSources()); + + return Unit.Default; + } + + private record Parameters(JellyfinSecrets Secrets, JellyfinServerInformation ServerInformation); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Jellyfin/Commands/SynchronizeJellyfinAdminUserId.cs b/ErsatzTV.Application/Jellyfin/Commands/SynchronizeJellyfinAdminUserId.cs index ad4563def..f77142258 100644 --- a/ErsatzTV.Application/Jellyfin/Commands/SynchronizeJellyfinAdminUserId.cs +++ b/ErsatzTV.Application/Jellyfin/Commands/SynchronizeJellyfinAdminUserId.cs @@ -1,8 +1,6 @@ using ErsatzTV.Core; -using LanguageExt; -namespace ErsatzTV.Application.Jellyfin.Commands -{ - public record SynchronizeJellyfinAdminUserId(int JellyfinMediaSourceId) : MediatR.IRequest>, - IJellyfinBackgroundServiceRequest; -} +namespace ErsatzTV.Application.Jellyfin; + +public record SynchronizeJellyfinAdminUserId(int JellyfinMediaSourceId) : MediatR.IRequest>, + IJellyfinBackgroundServiceRequest; \ No newline at end of file diff --git a/ErsatzTV.Application/Jellyfin/Commands/SynchronizeJellyfinAdminUserIdHandler.cs b/ErsatzTV.Application/Jellyfin/Commands/SynchronizeJellyfinAdminUserIdHandler.cs index fc30c131f..c5d542647 100644 --- a/ErsatzTV.Application/Jellyfin/Commands/SynchronizeJellyfinAdminUserIdHandler.cs +++ b/ErsatzTV.Application/Jellyfin/Commands/SynchronizeJellyfinAdminUserIdHandler.cs @@ -1,112 +1,107 @@ -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Jellyfin; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Jellyfin; -using LanguageExt; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; -using static LanguageExt.Prelude; -namespace ErsatzTV.Application.Jellyfin.Commands +namespace ErsatzTV.Application.Jellyfin; + +public class + SynchronizeJellyfinAdminUserIdHandler : MediatR.IRequestHandler> { - public class - SynchronizeJellyfinAdminUserIdHandler : MediatR.IRequestHandler> + private readonly IJellyfinApiClient _jellyfinApiClient; + private readonly IJellyfinSecretStore _jellyfinSecretStore; + private readonly ILogger _logger; + private readonly IMediaSourceRepository _mediaSourceRepository; + private readonly IMemoryCache _memoryCache; + + public SynchronizeJellyfinAdminUserIdHandler( + IMemoryCache memoryCache, + IMediaSourceRepository mediaSourceRepository, + IJellyfinSecretStore jellyfinSecretStore, + IJellyfinApiClient jellyfinApiClient, + ILogger logger) { - private readonly IJellyfinApiClient _jellyfinApiClient; - private readonly IJellyfinSecretStore _jellyfinSecretStore; - private readonly ILogger _logger; - private readonly IMediaSourceRepository _mediaSourceRepository; - private readonly IMemoryCache _memoryCache; - - public SynchronizeJellyfinAdminUserIdHandler( - IMemoryCache memoryCache, - IMediaSourceRepository mediaSourceRepository, - IJellyfinSecretStore jellyfinSecretStore, - IJellyfinApiClient jellyfinApiClient, - ILogger logger) - { - _memoryCache = memoryCache; - _mediaSourceRepository = mediaSourceRepository; - _jellyfinSecretStore = jellyfinSecretStore; - _jellyfinApiClient = jellyfinApiClient; - _logger = logger; - } - - public Task> Handle( - SynchronizeJellyfinAdminUserId request, - CancellationToken cancellationToken) => - Validate(request) - .Map(v => v.ToEither()) - .BindT(PerformSync); - - private async Task> PerformSync(ConnectionParameters parameters) - { - if (_memoryCache.TryGetValue($"jellyfin_admin_user_id.{parameters.JellyfinMediaSource.Id}", out string _)) - { - return Unit.Default; - } - - Either maybeUserId = await _jellyfinApiClient.GetAdminUserId( - parameters.ActiveConnection.Address, - parameters.ApiKey); - - return await maybeUserId.Match( - userId => - { - // _logger.LogDebug("Jellyfin admin user id is {UserId}", userId); - _memoryCache.Set($"jellyfin_admin_user_id.{parameters.JellyfinMediaSource.Id}", userId); - return Task.FromResult>(Unit.Default); - }, - async error => - { - // clear api key if unable to sync with jellyfin - if (error.Value.Contains("Unauthorized")) - { - await _jellyfinSecretStore.SaveSecrets( - new JellyfinSecrets { Address = parameters.ActiveConnection.Address, ApiKey = null }); - } - - return Left(error); - }); - } - - private Task> Validate(SynchronizeJellyfinAdminUserId request) => - MediaSourceMustExist(request) - .BindT(MediaSourceMustHaveActiveConnection) - .BindT(MediaSourceMustHaveApiKey); - - private Task> MediaSourceMustExist( - SynchronizeJellyfinAdminUserId request) => - _mediaSourceRepository.GetJellyfin(request.JellyfinMediaSourceId) - .Map(o => o.ToValidation("Jellyfin media source does not exist.")); - - private Validation MediaSourceMustHaveActiveConnection( - JellyfinMediaSource jellyfinMediaSource) - { - Option maybeConnection = jellyfinMediaSource.Connections.HeadOrNone(); - return maybeConnection.Map(connection => new ConnectionParameters(jellyfinMediaSource, connection)) - .ToValidation("Jellyfin media source requires an active connection"); - } - - private async Task> MediaSourceMustHaveApiKey( - ConnectionParameters connectionParameters) - { - JellyfinSecrets secrets = await _jellyfinSecretStore.ReadSecrets(); - return Optional(secrets.Address == connectionParameters.ActiveConnection.Address) - .Where(match => match) - .Map(_ => connectionParameters with { ApiKey = secrets.ApiKey }) - .ToValidation("Jellyfin media source requires an api key"); - } - - private record ConnectionParameters( - JellyfinMediaSource JellyfinMediaSource, - JellyfinConnection ActiveConnection) - { - public string ApiKey { get; set; } - } + _memoryCache = memoryCache; + _mediaSourceRepository = mediaSourceRepository; + _jellyfinSecretStore = jellyfinSecretStore; + _jellyfinApiClient = jellyfinApiClient; + _logger = logger; } -} + + public Task> Handle( + SynchronizeJellyfinAdminUserId request, + CancellationToken cancellationToken) => + Validate(request) + .Map(v => v.ToEither()) + .BindT(PerformSync); + + private async Task> PerformSync(ConnectionParameters parameters) + { + if (_memoryCache.TryGetValue($"jellyfin_admin_user_id.{parameters.JellyfinMediaSource.Id}", out string _)) + { + return Unit.Default; + } + + Either maybeUserId = await _jellyfinApiClient.GetAdminUserId( + parameters.ActiveConnection.Address, + parameters.ApiKey); + + return await maybeUserId.Match( + userId => + { + // _logger.LogDebug("Jellyfin admin user id is {UserId}", userId); + _memoryCache.Set($"jellyfin_admin_user_id.{parameters.JellyfinMediaSource.Id}", userId); + return Task.FromResult>(Unit.Default); + }, + async error => + { + // clear api key if unable to sync with jellyfin + if (error.Value.Contains("Unauthorized")) + { + await _jellyfinSecretStore.SaveSecrets( + new JellyfinSecrets { Address = parameters.ActiveConnection.Address, ApiKey = null }); + } + + return Left(error); + }); + } + + private Task> Validate(SynchronizeJellyfinAdminUserId request) => + MediaSourceMustExist(request) + .BindT(MediaSourceMustHaveActiveConnection) + .BindT(MediaSourceMustHaveApiKey); + + private Task> MediaSourceMustExist( + SynchronizeJellyfinAdminUserId request) => + _mediaSourceRepository.GetJellyfin(request.JellyfinMediaSourceId) + .Map(o => o.ToValidation("Jellyfin media source does not exist.")); + + private Validation MediaSourceMustHaveActiveConnection( + JellyfinMediaSource jellyfinMediaSource) + { + Option maybeConnection = jellyfinMediaSource.Connections.HeadOrNone(); + return maybeConnection.Map(connection => new ConnectionParameters(jellyfinMediaSource, connection)) + .ToValidation("Jellyfin media source requires an active connection"); + } + + private async Task> MediaSourceMustHaveApiKey( + ConnectionParameters connectionParameters) + { + JellyfinSecrets secrets = await _jellyfinSecretStore.ReadSecrets(); + return Optional(secrets.Address == connectionParameters.ActiveConnection.Address) + .Where(match => match) + .Map(_ => connectionParameters with { ApiKey = secrets.ApiKey }) + .ToValidation("Jellyfin media source requires an api key"); + } + + private record ConnectionParameters( + JellyfinMediaSource JellyfinMediaSource, + JellyfinConnection ActiveConnection) + { + public string ApiKey { get; set; } + } +} \ No newline at end of file diff --git a/ErsatzTV.Application/Jellyfin/Commands/SynchronizeJellyfinLibraries.cs b/ErsatzTV.Application/Jellyfin/Commands/SynchronizeJellyfinLibraries.cs index 422114169..de711cd93 100644 --- a/ErsatzTV.Application/Jellyfin/Commands/SynchronizeJellyfinLibraries.cs +++ b/ErsatzTV.Application/Jellyfin/Commands/SynchronizeJellyfinLibraries.cs @@ -1,8 +1,6 @@ using ErsatzTV.Core; -using LanguageExt; -namespace ErsatzTV.Application.Jellyfin.Commands -{ - public record SynchronizeJellyfinLibraries(int JellyfinMediaSourceId) : MediatR.IRequest>, - IJellyfinBackgroundServiceRequest; -} +namespace ErsatzTV.Application.Jellyfin; + +public record SynchronizeJellyfinLibraries(int JellyfinMediaSourceId) : MediatR.IRequest>, + IJellyfinBackgroundServiceRequest; \ No newline at end of file diff --git a/ErsatzTV.Application/Jellyfin/Commands/SynchronizeJellyfinLibrariesHandler.cs b/ErsatzTV.Application/Jellyfin/Commands/SynchronizeJellyfinLibrariesHandler.cs index 439250a4f..cf8488879 100644 --- a/ErsatzTV.Application/Jellyfin/Commands/SynchronizeJellyfinLibrariesHandler.cs +++ b/ErsatzTV.Application/Jellyfin/Commands/SynchronizeJellyfinLibrariesHandler.cs @@ -1,120 +1,113 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Jellyfin; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Jellyfin; -using LanguageExt; using Microsoft.Extensions.Logging; -using static LanguageExt.Prelude; -namespace ErsatzTV.Application.Jellyfin.Commands +namespace ErsatzTV.Application.Jellyfin; + +public class + SynchronizeJellyfinLibrariesHandler : MediatR.IRequestHandler> + { - public class - SynchronizeJellyfinLibrariesHandler : MediatR.IRequestHandler> + private readonly IJellyfinApiClient _jellyfinApiClient; + private readonly IJellyfinSecretStore _jellyfinSecretStore; + private readonly ILogger _logger; + private readonly IMediaSourceRepository _mediaSourceRepository; + private readonly ISearchIndex _searchIndex; + public SynchronizeJellyfinLibrariesHandler( + IMediaSourceRepository mediaSourceRepository, + IJellyfinSecretStore jellyfinSecretStore, + IJellyfinApiClient jellyfinApiClient, + ILogger logger, + ISearchIndex searchIndex) { - private readonly IJellyfinApiClient _jellyfinApiClient; - private readonly IJellyfinSecretStore _jellyfinSecretStore; - private readonly ILogger _logger; - private readonly IMediaSourceRepository _mediaSourceRepository; - private readonly ISearchIndex _searchIndex; - - public SynchronizeJellyfinLibrariesHandler( - IMediaSourceRepository mediaSourceRepository, - IJellyfinSecretStore jellyfinSecretStore, - IJellyfinApiClient jellyfinApiClient, - ILogger logger, - ISearchIndex searchIndex) - { - _mediaSourceRepository = mediaSourceRepository; - _jellyfinSecretStore = jellyfinSecretStore; - _jellyfinApiClient = jellyfinApiClient; - _logger = logger; - _searchIndex = searchIndex; - } - - public Task> Handle( - SynchronizeJellyfinLibraries request, - CancellationToken cancellationToken) => - Validate(request) - .MapT(SynchronizeLibraries) - .Bind(v => v.ToEitherAsync()); - - private Task> Validate(SynchronizeJellyfinLibraries request) => - MediaSourceMustExist(request) - .BindT(MediaSourceMustHaveActiveConnection) - .BindT(MediaSourceMustHaveApiKey); - - private Task> MediaSourceMustExist( - SynchronizeJellyfinLibraries request) => - _mediaSourceRepository.GetJellyfin(request.JellyfinMediaSourceId) - .Map(o => o.ToValidation("Jellyfin media source does not exist.")); - - private Validation MediaSourceMustHaveActiveConnection( - JellyfinMediaSource jellyfinMediaSource) - { - Option maybeConnection = jellyfinMediaSource.Connections.HeadOrNone(); - return maybeConnection.Map(connection => new ConnectionParameters(jellyfinMediaSource, connection)) - .ToValidation("Jellyfin media source requires an active connection"); - } - - private async Task> MediaSourceMustHaveApiKey( - ConnectionParameters connectionParameters) - { - JellyfinSecrets secrets = await _jellyfinSecretStore.ReadSecrets(); - return Optional(secrets.Address == connectionParameters.ActiveConnection.Address) - .Where(match => match) - .Map(_ => connectionParameters with { ApiKey = secrets.ApiKey }) - .ToValidation("Jellyfin media source requires an api key"); - } - - private async Task SynchronizeLibraries(ConnectionParameters connectionParameters) - { - Either> maybeLibraries = await _jellyfinApiClient.GetLibraries( - connectionParameters.ActiveConnection.Address, - connectionParameters.ApiKey); - - await maybeLibraries.Match( - async libraries => - { - var existing = connectionParameters.JellyfinMediaSource.Libraries.OfType() - .ToList(); - var toAdd = libraries.Filter(library => existing.All(l => l.ItemId != library.ItemId)).ToList(); - var toRemove = existing.Filter(library => libraries.All(l => l.ItemId != library.ItemId)).ToList(); - List ids = await _mediaSourceRepository.UpdateLibraries( - connectionParameters.JellyfinMediaSource.Id, - toAdd, - toRemove); - if (ids.Any()) - { - await _searchIndex.RemoveItems(ids); - _searchIndex.Commit(); - } - }, - error => - { - _logger.LogWarning( - "Unable to synchronize libraries from jellyfin server {JellyfinServer}: {Error}", - connectionParameters.JellyfinMediaSource.ServerName, - error.Value); - - return Task.CompletedTask; - }); - - return Unit.Default; - } - - private record ConnectionParameters( - JellyfinMediaSource JellyfinMediaSource, - JellyfinConnection ActiveConnection) - { - public string ApiKey { get; set; } - } + _mediaSourceRepository = mediaSourceRepository; + _jellyfinSecretStore = jellyfinSecretStore; + _jellyfinApiClient = jellyfinApiClient; + _logger = logger; + _searchIndex = searchIndex; } -} + + public Task> Handle( + SynchronizeJellyfinLibraries request, + CancellationToken cancellationToken) => + Validate(request) + .MapT(SynchronizeLibraries) + .Bind(v => v.ToEitherAsync()); + + private Task> Validate(SynchronizeJellyfinLibraries request) => + MediaSourceMustExist(request) + .BindT(MediaSourceMustHaveActiveConnection) + .BindT(MediaSourceMustHaveApiKey); + + private Task> MediaSourceMustExist( + SynchronizeJellyfinLibraries request) => + _mediaSourceRepository.GetJellyfin(request.JellyfinMediaSourceId) + .Map(o => o.ToValidation("Jellyfin media source does not exist.")); + + private Validation MediaSourceMustHaveActiveConnection( + JellyfinMediaSource jellyfinMediaSource) + { + Option maybeConnection = jellyfinMediaSource.Connections.HeadOrNone(); + return maybeConnection.Map(connection => new ConnectionParameters(jellyfinMediaSource, connection)) + .ToValidation("Jellyfin media source requires an active connection"); + } + + private async Task> MediaSourceMustHaveApiKey( + ConnectionParameters connectionParameters) + { + JellyfinSecrets secrets = await _jellyfinSecretStore.ReadSecrets(); + return Optional(secrets.Address == connectionParameters.ActiveConnection.Address) + .Where(match => match) + .Map(_ => connectionParameters with { ApiKey = secrets.ApiKey }) + .ToValidation("Jellyfin media source requires an api key"); + } + + private async Task SynchronizeLibraries(ConnectionParameters connectionParameters) + { + Either> maybeLibraries = await _jellyfinApiClient.GetLibraries( + connectionParameters.ActiveConnection.Address, + connectionParameters.ApiKey); + + await maybeLibraries.Match( + async libraries => + { + var existing = connectionParameters.JellyfinMediaSource.Libraries.OfType() + .ToList(); + var toAdd = libraries.Filter(library => existing.All(l => l.ItemId != library.ItemId)).ToList(); + var toRemove = existing.Filter(library => libraries.All(l => l.ItemId != library.ItemId)).ToList(); + List ids = await _mediaSourceRepository.UpdateLibraries( + connectionParameters.JellyfinMediaSource.Id, + toAdd, + toRemove); + if (ids.Any()) + { + await _searchIndex.RemoveItems(ids); + _searchIndex.Commit(); + } + }, + error => + { + _logger.LogWarning( + "Unable to synchronize libraries from jellyfin server {JellyfinServer}: {Error}", + connectionParameters.JellyfinMediaSource.ServerName, + error.Value); + + return Task.CompletedTask; + }); + + return Unit.Default; + } + + private record ConnectionParameters( + JellyfinMediaSource JellyfinMediaSource, + JellyfinConnection ActiveConnection) + { + public string ApiKey { get; set; } + } +} \ No newline at end of file diff --git a/ErsatzTV.Application/Jellyfin/Commands/SynchronizeJellyfinLibraryById.cs b/ErsatzTV.Application/Jellyfin/Commands/SynchronizeJellyfinLibraryById.cs index 8dbf89f15..926d25733 100644 --- a/ErsatzTV.Application/Jellyfin/Commands/SynchronizeJellyfinLibraryById.cs +++ b/ErsatzTV.Application/Jellyfin/Commands/SynchronizeJellyfinLibraryById.cs @@ -1,23 +1,20 @@ using ErsatzTV.Core; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.Jellyfin.Commands +namespace ErsatzTV.Application.Jellyfin; + +public interface ISynchronizeJellyfinLibraryById : IRequest>, + IJellyfinBackgroundServiceRequest { - public interface ISynchronizeJellyfinLibraryById : IRequest>, - IJellyfinBackgroundServiceRequest - { - int JellyfinLibraryId { get; } - bool ForceScan { get; } - } - - public record SynchronizeJellyfinLibraryByIdIfNeeded(int JellyfinLibraryId) : ISynchronizeJellyfinLibraryById - { - public bool ForceScan => false; - } - - public record ForceSynchronizeJellyfinLibraryById(int JellyfinLibraryId) : ISynchronizeJellyfinLibraryById - { - public bool ForceScan => true; - } + int JellyfinLibraryId { get; } + bool ForceScan { get; } } + +public record SynchronizeJellyfinLibraryByIdIfNeeded(int JellyfinLibraryId) : ISynchronizeJellyfinLibraryById +{ + public bool ForceScan => false; +} + +public record ForceSynchronizeJellyfinLibraryById(int JellyfinLibraryId) : ISynchronizeJellyfinLibraryById +{ + public bool ForceScan => true; +} \ No newline at end of file diff --git a/ErsatzTV.Application/Jellyfin/Commands/SynchronizeJellyfinLibraryByIdHandler.cs b/ErsatzTV.Application/Jellyfin/Commands/SynchronizeJellyfinLibraryByIdHandler.cs index 25d8abc18..3348dfd5b 100644 --- a/ErsatzTV.Application/Jellyfin/Commands/SynchronizeJellyfinLibraryByIdHandler.cs +++ b/ErsatzTV.Application/Jellyfin/Commands/SynchronizeJellyfinLibraryByIdHandler.cs @@ -1,181 +1,172 @@ -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Jellyfin; using ErsatzTV.Core.Interfaces.Locking; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Jellyfin; -using LanguageExt; -using MediatR; using Microsoft.Extensions.Logging; -using static LanguageExt.Prelude; -using Unit = LanguageExt.Unit; -namespace ErsatzTV.Application.Jellyfin.Commands +namespace ErsatzTV.Application.Jellyfin; + +public class SynchronizeJellyfinLibraryByIdHandler : + IRequestHandler>, + IRequestHandler> { - public class SynchronizeJellyfinLibraryByIdHandler : - IRequestHandler>, - IRequestHandler> + private readonly IConfigElementRepository _configElementRepository; + private readonly IEntityLocker _entityLocker; + private readonly IJellyfinMovieLibraryScanner _jellyfinMovieLibraryScanner; + + private readonly IJellyfinSecretStore _jellyfinSecretStore; + private readonly IJellyfinTelevisionLibraryScanner _jellyfinTelevisionLibraryScanner; + private readonly ILibraryRepository _libraryRepository; + private readonly ILogger _logger; + + private readonly IMediaSourceRepository _mediaSourceRepository; + + public SynchronizeJellyfinLibraryByIdHandler( + IMediaSourceRepository mediaSourceRepository, + IJellyfinSecretStore jellyfinSecretStore, + IJellyfinMovieLibraryScanner jellyfinMovieLibraryScanner, + IJellyfinTelevisionLibraryScanner jellyfinTelevisionLibraryScanner, + ILibraryRepository libraryRepository, + IEntityLocker entityLocker, + IConfigElementRepository configElementRepository, + ILogger logger) { - private readonly IConfigElementRepository _configElementRepository; - private readonly IEntityLocker _entityLocker; - private readonly IJellyfinMovieLibraryScanner _jellyfinMovieLibraryScanner; - - private readonly IJellyfinSecretStore _jellyfinSecretStore; - private readonly IJellyfinTelevisionLibraryScanner _jellyfinTelevisionLibraryScanner; - private readonly ILibraryRepository _libraryRepository; - private readonly ILogger _logger; - - private readonly IMediaSourceRepository _mediaSourceRepository; - - public SynchronizeJellyfinLibraryByIdHandler( - IMediaSourceRepository mediaSourceRepository, - IJellyfinSecretStore jellyfinSecretStore, - IJellyfinMovieLibraryScanner jellyfinMovieLibraryScanner, - IJellyfinTelevisionLibraryScanner jellyfinTelevisionLibraryScanner, - ILibraryRepository libraryRepository, - IEntityLocker entityLocker, - IConfigElementRepository configElementRepository, - ILogger logger) - { - _mediaSourceRepository = mediaSourceRepository; - _jellyfinSecretStore = jellyfinSecretStore; - _jellyfinMovieLibraryScanner = jellyfinMovieLibraryScanner; - _jellyfinTelevisionLibraryScanner = jellyfinTelevisionLibraryScanner; - _libraryRepository = libraryRepository; - _entityLocker = entityLocker; - _configElementRepository = configElementRepository; - _logger = logger; - } - - public Task> Handle( - ForceSynchronizeJellyfinLibraryById request, - CancellationToken cancellationToken) => Handle(request); - - public Task> Handle( - SynchronizeJellyfinLibraryByIdIfNeeded request, - CancellationToken cancellationToken) => Handle(request); - - private Task> - Handle(ISynchronizeJellyfinLibraryById request) => - Validate(request) - .MapT(parameters => Synchronize(parameters).Map(_ => parameters.Library.Name)) - .Bind(v => v.ToEitherAsync()); - - private async Task Synchronize(RequestParameters parameters) - { - var lastScan = new DateTimeOffset(parameters.Library.LastScan ?? SystemTime.MinValueUtc, TimeSpan.Zero); - DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(parameters.LibraryRefreshInterval); - if (parameters.ForceScan || nextScan < DateTimeOffset.Now) - { - switch (parameters.Library.MediaKind) - { - case LibraryMediaKind.Movies: - await _jellyfinMovieLibraryScanner.ScanLibrary( - parameters.ConnectionParameters.ActiveConnection.Address, - parameters.ConnectionParameters.ApiKey, - parameters.Library, - parameters.FFprobePath); - break; - case LibraryMediaKind.Shows: - await _jellyfinTelevisionLibraryScanner.ScanLibrary( - parameters.ConnectionParameters.ActiveConnection.Address, - parameters.ConnectionParameters.ApiKey, - parameters.Library, - parameters.FFprobePath); - break; - } - - parameters.Library.LastScan = DateTime.UtcNow; - await _libraryRepository.UpdateLastScan(parameters.Library); - } - else - { - _logger.LogDebug( - "Skipping unforced scan of jellyfin media library {Name}", - parameters.Library.Name); - } - - _entityLocker.UnlockLibrary(parameters.Library.Id); - return Unit.Default; - } - - private async Task> Validate( - ISynchronizeJellyfinLibraryById request) => - (await ValidateConnection(request), await JellyfinLibraryMustExist(request), - await ValidateLibraryRefreshInterval(), await ValidateFFprobePath()) - .Apply( - (connectionParameters, jellyfinLibrary, libraryRefreshInterval, ffprobePath) => new RequestParameters( - connectionParameters, - jellyfinLibrary, - request.ForceScan, - libraryRefreshInterval, - ffprobePath - )); - - private Task> ValidateConnection( - ISynchronizeJellyfinLibraryById request) => - JellyfinMediaSourceMustExist(request) - .BindT(MediaSourceMustHaveActiveConnection) - .BindT(MediaSourceMustHaveApiKey); - - private Task> JellyfinMediaSourceMustExist( - ISynchronizeJellyfinLibraryById request) => - _mediaSourceRepository.GetJellyfinByLibraryId(request.JellyfinLibraryId) - .Map( - v => v.ToValidation( - $"Jellyfin media source for library {request.JellyfinLibraryId} does not exist.")); - - private Validation MediaSourceMustHaveActiveConnection( - JellyfinMediaSource jellyfinMediaSource) - { - Option maybeConnection = jellyfinMediaSource.Connections.HeadOrNone(); - return maybeConnection.Map(connection => new ConnectionParameters(jellyfinMediaSource, connection)) - .ToValidation("Jellyfin media source requires an active connection"); - } - - private async Task> MediaSourceMustHaveApiKey( - ConnectionParameters connectionParameters) - { - JellyfinSecrets secrets = await _jellyfinSecretStore.ReadSecrets(); - return Optional(secrets.Address == connectionParameters.ActiveConnection.Address) - .Where(match => match) - .Map(_ => connectionParameters with { ApiKey = secrets.ApiKey }) - .ToValidation("Jellyfin media source requires an api key"); - } - - private Task> JellyfinLibraryMustExist( - ISynchronizeJellyfinLibraryById request) => - _mediaSourceRepository.GetJellyfinLibrary(request.JellyfinLibraryId) - .Map(v => v.ToValidation($"Jellyfin library {request.JellyfinLibraryId} does not exist.")); - - private Task> ValidateLibraryRefreshInterval() => - _configElementRepository.GetValue(ConfigElementKey.LibraryRefreshInterval) - .FilterT(lri => lri > 0) - .Map(lri => lri.ToValidation("Library refresh interval is invalid")); - - private Task> ValidateFFprobePath() => - _configElementRepository.GetValue(ConfigElementKey.FFprobePath) - .FilterT(File.Exists) - .Map( - ffprobePath => - ffprobePath.ToValidation("FFprobe path does not exist on the file system")); - - private record RequestParameters( - ConnectionParameters ConnectionParameters, - JellyfinLibrary Library, - bool ForceScan, - int LibraryRefreshInterval, - string FFprobePath); - - private record ConnectionParameters( - JellyfinMediaSource JellyfinMediaSource, - JellyfinConnection ActiveConnection) - { - public string ApiKey { get; set; } - } + _mediaSourceRepository = mediaSourceRepository; + _jellyfinSecretStore = jellyfinSecretStore; + _jellyfinMovieLibraryScanner = jellyfinMovieLibraryScanner; + _jellyfinTelevisionLibraryScanner = jellyfinTelevisionLibraryScanner; + _libraryRepository = libraryRepository; + _entityLocker = entityLocker; + _configElementRepository = configElementRepository; + _logger = logger; } -} + + public Task> Handle( + ForceSynchronizeJellyfinLibraryById request, + CancellationToken cancellationToken) => Handle(request); + + public Task> Handle( + SynchronizeJellyfinLibraryByIdIfNeeded request, + CancellationToken cancellationToken) => Handle(request); + + private Task> + Handle(ISynchronizeJellyfinLibraryById request) => + Validate(request) + .MapT(parameters => Synchronize(parameters).Map(_ => parameters.Library.Name)) + .Bind(v => v.ToEitherAsync()); + + private async Task Synchronize(RequestParameters parameters) + { + var lastScan = new DateTimeOffset(parameters.Library.LastScan ?? SystemTime.MinValueUtc, TimeSpan.Zero); + DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(parameters.LibraryRefreshInterval); + if (parameters.ForceScan || nextScan < DateTimeOffset.Now) + { + switch (parameters.Library.MediaKind) + { + case LibraryMediaKind.Movies: + await _jellyfinMovieLibraryScanner.ScanLibrary( + parameters.ConnectionParameters.ActiveConnection.Address, + parameters.ConnectionParameters.ApiKey, + parameters.Library, + parameters.FFprobePath); + break; + case LibraryMediaKind.Shows: + await _jellyfinTelevisionLibraryScanner.ScanLibrary( + parameters.ConnectionParameters.ActiveConnection.Address, + parameters.ConnectionParameters.ApiKey, + parameters.Library, + parameters.FFprobePath); + break; + } + + parameters.Library.LastScan = DateTime.UtcNow; + await _libraryRepository.UpdateLastScan(parameters.Library); + } + else + { + _logger.LogDebug( + "Skipping unforced scan of jellyfin media library {Name}", + parameters.Library.Name); + } + + _entityLocker.UnlockLibrary(parameters.Library.Id); + return Unit.Default; + } + + private async Task> Validate( + ISynchronizeJellyfinLibraryById request) => + (await ValidateConnection(request), await JellyfinLibraryMustExist(request), + await ValidateLibraryRefreshInterval(), await ValidateFFprobePath()) + .Apply( + (connectionParameters, jellyfinLibrary, libraryRefreshInterval, ffprobePath) => new RequestParameters( + connectionParameters, + jellyfinLibrary, + request.ForceScan, + libraryRefreshInterval, + ffprobePath + )); + + private Task> ValidateConnection( + ISynchronizeJellyfinLibraryById request) => + JellyfinMediaSourceMustExist(request) + .BindT(MediaSourceMustHaveActiveConnection) + .BindT(MediaSourceMustHaveApiKey); + + private Task> JellyfinMediaSourceMustExist( + ISynchronizeJellyfinLibraryById request) => + _mediaSourceRepository.GetJellyfinByLibraryId(request.JellyfinLibraryId) + .Map( + v => v.ToValidation( + $"Jellyfin media source for library {request.JellyfinLibraryId} does not exist.")); + + private Validation MediaSourceMustHaveActiveConnection( + JellyfinMediaSource jellyfinMediaSource) + { + Option maybeConnection = jellyfinMediaSource.Connections.HeadOrNone(); + return maybeConnection.Map(connection => new ConnectionParameters(jellyfinMediaSource, connection)) + .ToValidation("Jellyfin media source requires an active connection"); + } + + private async Task> MediaSourceMustHaveApiKey( + ConnectionParameters connectionParameters) + { + JellyfinSecrets secrets = await _jellyfinSecretStore.ReadSecrets(); + return Optional(secrets.Address == connectionParameters.ActiveConnection.Address) + .Where(match => match) + .Map(_ => connectionParameters with { ApiKey = secrets.ApiKey }) + .ToValidation("Jellyfin media source requires an api key"); + } + + private Task> JellyfinLibraryMustExist( + ISynchronizeJellyfinLibraryById request) => + _mediaSourceRepository.GetJellyfinLibrary(request.JellyfinLibraryId) + .Map(v => v.ToValidation($"Jellyfin library {request.JellyfinLibraryId} does not exist.")); + + private Task> ValidateLibraryRefreshInterval() => + _configElementRepository.GetValue(ConfigElementKey.LibraryRefreshInterval) + .FilterT(lri => lri > 0) + .Map(lri => lri.ToValidation("Library refresh interval is invalid")); + + private Task> ValidateFFprobePath() => + _configElementRepository.GetValue(ConfigElementKey.FFprobePath) + .FilterT(File.Exists) + .Map( + ffprobePath => + ffprobePath.ToValidation("FFprobe path does not exist on the file system")); + + private record RequestParameters( + ConnectionParameters ConnectionParameters, + JellyfinLibrary Library, + bool ForceScan, + int LibraryRefreshInterval, + string FFprobePath); + + private record ConnectionParameters( + JellyfinMediaSource JellyfinMediaSource, + JellyfinConnection ActiveConnection) + { + public string ApiKey { get; set; } + } +} \ No newline at end of file diff --git a/ErsatzTV.Application/Jellyfin/Commands/SynchronizeJellyfinMediaSources.cs b/ErsatzTV.Application/Jellyfin/Commands/SynchronizeJellyfinMediaSources.cs index ccd67e35b..9ae409b00 100644 --- a/ErsatzTV.Application/Jellyfin/Commands/SynchronizeJellyfinMediaSources.cs +++ b/ErsatzTV.Application/Jellyfin/Commands/SynchronizeJellyfinMediaSources.cs @@ -1,11 +1,7 @@ -using System.Collections.Generic; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.Jellyfin.Commands -{ - public record SynchronizeJellyfinMediaSources : IRequest>>, - IJellyfinBackgroundServiceRequest; -} +namespace ErsatzTV.Application.Jellyfin; + +public record SynchronizeJellyfinMediaSources : IRequest>>, + IJellyfinBackgroundServiceRequest; \ No newline at end of file diff --git a/ErsatzTV.Application/Jellyfin/Commands/SynchronizeJellyfinMediaSourcesHandler.cs b/ErsatzTV.Application/Jellyfin/Commands/SynchronizeJellyfinMediaSourcesHandler.cs index 0677c86e6..4171335ed 100644 --- a/ErsatzTV.Application/Jellyfin/Commands/SynchronizeJellyfinMediaSourcesHandler.cs +++ b/ErsatzTV.Application/Jellyfin/Commands/SynchronizeJellyfinMediaSourcesHandler.cs @@ -1,41 +1,35 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Channels; -using System.Threading.Tasks; +using System.Threading.Channels; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.Jellyfin.Commands +namespace ErsatzTV.Application.Jellyfin; + +public class SynchronizeJellyfinMediaSourcesHandler : IRequestHandler>> { - public class SynchronizeJellyfinMediaSourcesHandler : IRequestHandler>> + private readonly ChannelWriter _channel; + private readonly IMediaSourceRepository _mediaSourceRepository; + + public SynchronizeJellyfinMediaSourcesHandler( + IMediaSourceRepository mediaSourceRepository, + ChannelWriter channel) { - private readonly ChannelWriter _channel; - private readonly IMediaSourceRepository _mediaSourceRepository; - - public SynchronizeJellyfinMediaSourcesHandler( - IMediaSourceRepository mediaSourceRepository, - ChannelWriter channel) - { - _mediaSourceRepository = mediaSourceRepository; - _channel = channel; - } - - public async Task>> Handle( - SynchronizeJellyfinMediaSources request, - CancellationToken cancellationToken) - { - List mediaSources = await _mediaSourceRepository.GetAllJellyfin(); - foreach (JellyfinMediaSource mediaSource in mediaSources) - { - await _channel.WriteAsync(new SynchronizeJellyfinAdminUserId(mediaSource.Id), cancellationToken); - await _channel.WriteAsync(new SynchronizeJellyfinLibraries(mediaSource.Id), cancellationToken); - } - - return mediaSources; - } + _mediaSourceRepository = mediaSourceRepository; + _channel = channel; } -} + + public async Task>> Handle( + SynchronizeJellyfinMediaSources request, + CancellationToken cancellationToken) + { + List mediaSources = await _mediaSourceRepository.GetAllJellyfin(); + foreach (JellyfinMediaSource mediaSource in mediaSources) + { + await _channel.WriteAsync(new SynchronizeJellyfinAdminUserId(mediaSource.Id), cancellationToken); + await _channel.WriteAsync(new SynchronizeJellyfinLibraries(mediaSource.Id), cancellationToken); + } + + return mediaSources; + } +} \ No newline at end of file diff --git a/ErsatzTV.Application/Jellyfin/Commands/UpdateJellyfinLibraryPreferences.cs b/ErsatzTV.Application/Jellyfin/Commands/UpdateJellyfinLibraryPreferences.cs index 64fe8ad27..655dee6df 100644 --- a/ErsatzTV.Application/Jellyfin/Commands/UpdateJellyfinLibraryPreferences.cs +++ b/ErsatzTV.Application/Jellyfin/Commands/UpdateJellyfinLibraryPreferences.cs @@ -1,11 +1,8 @@ -using System.Collections.Generic; -using ErsatzTV.Core; -using LanguageExt; +using ErsatzTV.Core; -namespace ErsatzTV.Application.Jellyfin.Commands -{ - public record UpdateJellyfinLibraryPreferences - (List Preferences) : MediatR.IRequest>; +namespace ErsatzTV.Application.Jellyfin; - public record JellyfinLibraryPreference(int Id, bool ShouldSyncItems); -} +public record UpdateJellyfinLibraryPreferences + (List Preferences) : MediatR.IRequest>; + +public record JellyfinLibraryPreference(int Id, bool ShouldSyncItems); \ No newline at end of file diff --git a/ErsatzTV.Application/Jellyfin/Commands/UpdateJellyfinLibraryPreferencesHandler.cs b/ErsatzTV.Application/Jellyfin/Commands/UpdateJellyfinLibraryPreferencesHandler.cs index 4060af789..0748af034 100644 --- a/ErsatzTV.Application/Jellyfin/Commands/UpdateJellyfinLibraryPreferencesHandler.cs +++ b/ErsatzTV.Application/Jellyfin/Commands/UpdateJellyfinLibraryPreferencesHandler.cs @@ -1,42 +1,36 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Search; -using LanguageExt; -namespace ErsatzTV.Application.Jellyfin.Commands +namespace ErsatzTV.Application.Jellyfin; + +public class + UpdateJellyfinLibraryPreferencesHandler : MediatR.IRequestHandler> { - public class - UpdateJellyfinLibraryPreferencesHandler : MediatR.IRequestHandler> + private readonly IMediaSourceRepository _mediaSourceRepository; + private readonly ISearchIndex _searchIndex; + + public UpdateJellyfinLibraryPreferencesHandler( + IMediaSourceRepository mediaSourceRepository, + ISearchIndex searchIndex) { - private readonly IMediaSourceRepository _mediaSourceRepository; - private readonly ISearchIndex _searchIndex; - - public UpdateJellyfinLibraryPreferencesHandler( - IMediaSourceRepository mediaSourceRepository, - ISearchIndex searchIndex) - { - _mediaSourceRepository = mediaSourceRepository; - _searchIndex = searchIndex; - } - - public async Task> Handle( - UpdateJellyfinLibraryPreferences request, - CancellationToken cancellationToken) - { - var toDisable = request.Preferences.Filter(p => p.ShouldSyncItems == false).Map(p => p.Id).ToList(); - List ids = await _mediaSourceRepository.DisableJellyfinLibrarySync(toDisable); - await _searchIndex.RemoveItems(ids); - _searchIndex.Commit(); - - IEnumerable toEnable = request.Preferences.Filter(p => p.ShouldSyncItems).Map(p => p.Id); - await _mediaSourceRepository.EnableJellyfinLibrarySync(toEnable); - - return Unit.Default; - } + _mediaSourceRepository = mediaSourceRepository; + _searchIndex = searchIndex; } -} + + public async Task> Handle( + UpdateJellyfinLibraryPreferences request, + CancellationToken cancellationToken) + { + var toDisable = request.Preferences.Filter(p => p.ShouldSyncItems == false).Map(p => p.Id).ToList(); + List ids = await _mediaSourceRepository.DisableJellyfinLibrarySync(toDisable); + await _searchIndex.RemoveItems(ids); + _searchIndex.Commit(); + + IEnumerable toEnable = request.Preferences.Filter(p => p.ShouldSyncItems).Map(p => p.Id); + await _mediaSourceRepository.EnableJellyfinLibrarySync(toEnable); + + return Unit.Default; + } +} \ No newline at end of file diff --git a/ErsatzTV.Application/Jellyfin/Commands/UpdateJellyfinPathReplacements.cs b/ErsatzTV.Application/Jellyfin/Commands/UpdateJellyfinPathReplacements.cs index 0fb94474e..b17b3aa2a 100644 --- a/ErsatzTV.Application/Jellyfin/Commands/UpdateJellyfinPathReplacements.cs +++ b/ErsatzTV.Application/Jellyfin/Commands/UpdateJellyfinPathReplacements.cs @@ -1,12 +1,9 @@ -using System.Collections.Generic; -using ErsatzTV.Core; -using LanguageExt; +using ErsatzTV.Core; -namespace ErsatzTV.Application.Jellyfin.Commands -{ - public record UpdateJellyfinPathReplacements( - int JellyfinMediaSourceId, - List PathReplacements) : MediatR.IRequest>; +namespace ErsatzTV.Application.Jellyfin; - public record JellyfinPathReplacementItem(int Id, string JellyfinPath, string LocalPath); -} +public record UpdateJellyfinPathReplacements( + int JellyfinMediaSourceId, + List PathReplacements) : MediatR.IRequest>; + +public record JellyfinPathReplacementItem(int Id, string JellyfinPath, string LocalPath); \ No newline at end of file diff --git a/ErsatzTV.Application/Jellyfin/Commands/UpdateJellyfinPathReplacementsHandler.cs b/ErsatzTV.Application/Jellyfin/Commands/UpdateJellyfinPathReplacementsHandler.cs index 3d4877a0c..dbf45f68b 100644 --- a/ErsatzTV.Application/Jellyfin/Commands/UpdateJellyfinPathReplacementsHandler.cs +++ b/ErsatzTV.Application/Jellyfin/Commands/UpdateJellyfinPathReplacementsHandler.cs @@ -1,55 +1,49 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -namespace ErsatzTV.Application.Jellyfin.Commands +namespace ErsatzTV.Application.Jellyfin; + +public class UpdateJellyfinPathReplacementsHandler : MediatR.IRequestHandler> { - public class UpdateJellyfinPathReplacementsHandler : MediatR.IRequestHandler> + private readonly IMediaSourceRepository _mediaSourceRepository; + + public UpdateJellyfinPathReplacementsHandler(IMediaSourceRepository mediaSourceRepository) => + _mediaSourceRepository = mediaSourceRepository; + + public Task> Handle( + UpdateJellyfinPathReplacements request, + CancellationToken cancellationToken) => + Validate(request) + .MapT(pms => MergePathReplacements(request, pms)) + .Bind(v => v.ToEitherAsync()); + + private Task MergePathReplacements( + UpdateJellyfinPathReplacements request, + JellyfinMediaSource jellyfinMediaSource) { - private readonly IMediaSourceRepository _mediaSourceRepository; + jellyfinMediaSource.PathReplacements ??= new List(); - public UpdateJellyfinPathReplacementsHandler(IMediaSourceRepository mediaSourceRepository) => - _mediaSourceRepository = mediaSourceRepository; + var incoming = request.PathReplacements.Map(Project).ToList(); - public Task> Handle( - UpdateJellyfinPathReplacements request, - CancellationToken cancellationToken) => - Validate(request) - .MapT(pms => MergePathReplacements(request, pms)) - .Bind(v => v.ToEitherAsync()); + var toAdd = incoming.Filter(r => r.Id < 1).ToList(); + var toRemove = jellyfinMediaSource.PathReplacements.Filter(r => incoming.All(pr => pr.Id != r.Id)).ToList(); + var toUpdate = incoming.Except(toAdd).ToList(); - private Task MergePathReplacements( - UpdateJellyfinPathReplacements request, - JellyfinMediaSource jellyfinMediaSource) - { - jellyfinMediaSource.PathReplacements ??= new List(); - - var incoming = request.PathReplacements.Map(Project).ToList(); - - var toAdd = incoming.Filter(r => r.Id < 1).ToList(); - var toRemove = jellyfinMediaSource.PathReplacements.Filter(r => incoming.All(pr => pr.Id != r.Id)).ToList(); - var toUpdate = incoming.Except(toAdd).ToList(); - - return _mediaSourceRepository.UpdatePathReplacements(jellyfinMediaSource.Id, toAdd, toUpdate, toRemove); - } - - private static JellyfinPathReplacement Project(JellyfinPathReplacementItem vm) => - new() { Id = vm.Id, JellyfinPath = vm.JellyfinPath, LocalPath = vm.LocalPath }; - - private Task> Validate(UpdateJellyfinPathReplacements request) => - JellyfinMediaSourceMustExist(request); - - private Task> JellyfinMediaSourceMustExist( - UpdateJellyfinPathReplacements request) => - _mediaSourceRepository.GetJellyfin(request.JellyfinMediaSourceId) - .Map( - v => v.ToValidation( - $"Jellyfin media source {request.JellyfinMediaSourceId} does not exist.")); + return _mediaSourceRepository.UpdatePathReplacements(jellyfinMediaSource.Id, toAdd, toUpdate, toRemove); } -} + + private static JellyfinPathReplacement Project(JellyfinPathReplacementItem vm) => + new() { Id = vm.Id, JellyfinPath = vm.JellyfinPath, LocalPath = vm.LocalPath }; + + private Task> Validate(UpdateJellyfinPathReplacements request) => + JellyfinMediaSourceMustExist(request); + + private Task> JellyfinMediaSourceMustExist( + UpdateJellyfinPathReplacements request) => + _mediaSourceRepository.GetJellyfin(request.JellyfinMediaSourceId) + .Map( + v => v.ToValidation( + $"Jellyfin media source {request.JellyfinMediaSourceId} does not exist.")); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Jellyfin/JellyfinConnectionParametersViewModel.cs b/ErsatzTV.Application/Jellyfin/JellyfinConnectionParametersViewModel.cs index 2ad489515..50592c61a 100644 --- a/ErsatzTV.Application/Jellyfin/JellyfinConnectionParametersViewModel.cs +++ b/ErsatzTV.Application/Jellyfin/JellyfinConnectionParametersViewModel.cs @@ -1,4 +1,3 @@ -namespace ErsatzTV.Application.Jellyfin -{ - public record JellyfinConnectionParametersViewModel(string Address); -} +namespace ErsatzTV.Application.Jellyfin; + +public record JellyfinConnectionParametersViewModel(string Address); \ No newline at end of file diff --git a/ErsatzTV.Application/Jellyfin/JellyfinLibraryViewModel.cs b/ErsatzTV.Application/Jellyfin/JellyfinLibraryViewModel.cs index 2cc95f476..24e661db4 100644 --- a/ErsatzTV.Application/Jellyfin/JellyfinLibraryViewModel.cs +++ b/ErsatzTV.Application/Jellyfin/JellyfinLibraryViewModel.cs @@ -1,8 +1,7 @@ using ErsatzTV.Application.Libraries; using ErsatzTV.Core.Domain; -namespace ErsatzTV.Application.Jellyfin -{ - public record JellyfinLibraryViewModel(int Id, string Name, LibraryMediaKind MediaKind, bool ShouldSyncItems) - : LibraryViewModel("Jellyfin", Id, Name, MediaKind); -} +namespace ErsatzTV.Application.Jellyfin; + +public record JellyfinLibraryViewModel(int Id, string Name, LibraryMediaKind MediaKind, bool ShouldSyncItems) + : LibraryViewModel("Jellyfin", Id, Name, MediaKind); \ No newline at end of file diff --git a/ErsatzTV.Application/Jellyfin/JellyfinMediaSourceViewModel.cs b/ErsatzTV.Application/Jellyfin/JellyfinMediaSourceViewModel.cs index 361a6b948..b5db08944 100644 --- a/ErsatzTV.Application/Jellyfin/JellyfinMediaSourceViewModel.cs +++ b/ErsatzTV.Application/Jellyfin/JellyfinMediaSourceViewModel.cs @@ -1,9 +1,8 @@ using ErsatzTV.Application.MediaSources; -namespace ErsatzTV.Application.Jellyfin -{ - public record JellyfinMediaSourceViewModel(int Id, string Name, string Address) : RemoteMediaSourceViewModel( - Id, - Name, - Address); -} +namespace ErsatzTV.Application.Jellyfin; + +public record JellyfinMediaSourceViewModel(int Id, string Name, string Address) : RemoteMediaSourceViewModel( + Id, + Name, + Address); \ No newline at end of file diff --git a/ErsatzTV.Application/Jellyfin/JellyfinPathReplacementViewModel.cs b/ErsatzTV.Application/Jellyfin/JellyfinPathReplacementViewModel.cs index 624e9ae13..fa80a7297 100644 --- a/ErsatzTV.Application/Jellyfin/JellyfinPathReplacementViewModel.cs +++ b/ErsatzTV.Application/Jellyfin/JellyfinPathReplacementViewModel.cs @@ -1,4 +1,3 @@ -namespace ErsatzTV.Application.Jellyfin -{ - public record JellyfinPathReplacementViewModel(int Id, string JellyfinPath, string LocalPath); -} +namespace ErsatzTV.Application.Jellyfin; + +public record JellyfinPathReplacementViewModel(int Id, string JellyfinPath, string LocalPath); \ No newline at end of file diff --git a/ErsatzTV.Application/Jellyfin/Mapper.cs b/ErsatzTV.Application/Jellyfin/Mapper.cs index eab0ebbb8..f696ff259 100644 --- a/ErsatzTV.Application/Jellyfin/Mapper.cs +++ b/ErsatzTV.Application/Jellyfin/Mapper.cs @@ -1,19 +1,18 @@ using ErsatzTV.Core.Domain; -namespace ErsatzTV.Application.Jellyfin +namespace ErsatzTV.Application.Jellyfin; + +internal static class Mapper { - internal static class Mapper - { - internal static JellyfinMediaSourceViewModel ProjectToViewModel(JellyfinMediaSource jellyfinMediaSource) => - new( - jellyfinMediaSource.Id, - jellyfinMediaSource.ServerName, - jellyfinMediaSource.Connections.HeadOrNone().Match(c => c.Address, string.Empty)); + internal static JellyfinMediaSourceViewModel ProjectToViewModel(JellyfinMediaSource jellyfinMediaSource) => + new( + jellyfinMediaSource.Id, + jellyfinMediaSource.ServerName, + jellyfinMediaSource.Connections.HeadOrNone().Match(c => c.Address, string.Empty)); - internal static JellyfinLibraryViewModel ProjectToViewModel(JellyfinLibrary library) => - new(library.Id, library.Name, library.MediaKind, library.ShouldSyncItems); + internal static JellyfinLibraryViewModel ProjectToViewModel(JellyfinLibrary library) => + new(library.Id, library.Name, library.MediaKind, library.ShouldSyncItems); - internal static JellyfinPathReplacementViewModel ProjectToViewModel(JellyfinPathReplacement pathReplacement) => - new(pathReplacement.Id, pathReplacement.JellyfinPath, pathReplacement.LocalPath); - } -} + internal static JellyfinPathReplacementViewModel ProjectToViewModel(JellyfinPathReplacement pathReplacement) => + new(pathReplacement.Id, pathReplacement.JellyfinPath, pathReplacement.LocalPath); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Jellyfin/Queries/GetAllJellyfinMediaSources.cs b/ErsatzTV.Application/Jellyfin/Queries/GetAllJellyfinMediaSources.cs index c02e83261..032f4c828 100644 --- a/ErsatzTV.Application/Jellyfin/Queries/GetAllJellyfinMediaSources.cs +++ b/ErsatzTV.Application/Jellyfin/Queries/GetAllJellyfinMediaSources.cs @@ -1,7 +1,3 @@ -using System.Collections.Generic; -using MediatR; +namespace ErsatzTV.Application.Jellyfin; -namespace ErsatzTV.Application.Jellyfin.Queries -{ - public record GetAllJellyfinMediaSources : IRequest>; -} +public record GetAllJellyfinMediaSources : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Jellyfin/Queries/GetAllJellyfinMediaSourcesHandler.cs b/ErsatzTV.Application/Jellyfin/Queries/GetAllJellyfinMediaSourcesHandler.cs index 948f66a6f..330797299 100644 --- a/ErsatzTV.Application/Jellyfin/Queries/GetAllJellyfinMediaSourcesHandler.cs +++ b/ErsatzTV.Application/Jellyfin/Queries/GetAllJellyfinMediaSourcesHandler.cs @@ -1,26 +1,19 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -using MediatR; +using ErsatzTV.Core.Interfaces.Repositories; using static ErsatzTV.Application.Jellyfin.Mapper; -namespace ErsatzTV.Application.Jellyfin.Queries +namespace ErsatzTV.Application.Jellyfin; + +public class + GetAllJellyfinMediaSourcesHandler : IRequestHandler> { - public class - GetAllJellyfinMediaSourcesHandler : IRequestHandler> - { - private readonly IMediaSourceRepository _mediaSourceRepository; + private readonly IMediaSourceRepository _mediaSourceRepository; - public GetAllJellyfinMediaSourcesHandler(IMediaSourceRepository mediaSourceRepository) => - _mediaSourceRepository = mediaSourceRepository; + public GetAllJellyfinMediaSourcesHandler(IMediaSourceRepository mediaSourceRepository) => + _mediaSourceRepository = mediaSourceRepository; - public Task> Handle( - GetAllJellyfinMediaSources request, - CancellationToken cancellationToken) => - _mediaSourceRepository.GetAllJellyfin().Map(list => list.Map(ProjectToViewModel).ToList()); - } -} + public Task> Handle( + GetAllJellyfinMediaSources request, + CancellationToken cancellationToken) => + _mediaSourceRepository.GetAllJellyfin().Map(list => list.Map(ProjectToViewModel).ToList()); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Jellyfin/Queries/GetJellyfinConnectionParameters.cs b/ErsatzTV.Application/Jellyfin/Queries/GetJellyfinConnectionParameters.cs index b3666e6dc..f007ee670 100644 --- a/ErsatzTV.Application/Jellyfin/Queries/GetJellyfinConnectionParameters.cs +++ b/ErsatzTV.Application/Jellyfin/Queries/GetJellyfinConnectionParameters.cs @@ -1,8 +1,5 @@ using ErsatzTV.Core; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.Jellyfin.Queries -{ - public record GetJellyfinConnectionParameters : IRequest>; -} +namespace ErsatzTV.Application.Jellyfin; + +public record GetJellyfinConnectionParameters : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Jellyfin/Queries/GetJellyfinConnectionParametersHandler.cs b/ErsatzTV.Application/Jellyfin/Queries/GetJellyfinConnectionParametersHandler.cs index 23f359575..07fdfc781 100644 --- a/ErsatzTV.Application/Jellyfin/Queries/GetJellyfinConnectionParametersHandler.cs +++ b/ErsatzTV.Application/Jellyfin/Queries/GetJellyfinConnectionParametersHandler.cs @@ -1,73 +1,66 @@ -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -using MediatR; using Microsoft.Extensions.Caching.Memory; -namespace ErsatzTV.Application.Jellyfin.Queries +namespace ErsatzTV.Application.Jellyfin; + +public class GetJellyfinConnectionParametersHandler : IRequestHandler> { - public class GetJellyfinConnectionParametersHandler : IRequestHandler> + private readonly IMediaSourceRepository _mediaSourceRepository; + private readonly IMemoryCache _memoryCache; + + public GetJellyfinConnectionParametersHandler( + IMemoryCache memoryCache, + IMediaSourceRepository mediaSourceRepository) { - private readonly IMediaSourceRepository _mediaSourceRepository; - private readonly IMemoryCache _memoryCache; - - public GetJellyfinConnectionParametersHandler( - IMemoryCache memoryCache, - IMediaSourceRepository mediaSourceRepository) - { - _memoryCache = memoryCache; - _mediaSourceRepository = mediaSourceRepository; - } - - public async Task> Handle( - GetJellyfinConnectionParameters request, - CancellationToken cancellationToken) - { - if (_memoryCache.TryGetValue(request, out JellyfinConnectionParametersViewModel parameters)) - { - return parameters; - } - - Either maybeParameters = - await Validate() - .MapT(cp => new JellyfinConnectionParametersViewModel(cp.ActiveConnection.Address)) - .Map(v => v.ToEither()); - - return maybeParameters.Match( - p => - { - _memoryCache.Set(request, p, TimeSpan.FromHours(1)); - return maybeParameters; - }, - error => error); - } - - private Task> Validate() => - JellyfinMediaSourceMustExist() - .BindT(MediaSourceMustHaveActiveConnection); - - private Task> JellyfinMediaSourceMustExist() => - _mediaSourceRepository.GetAllJellyfin().Map(list => list.HeadOrNone()) - .Map( - v => v.ToValidation( - "Jellyfin media source does not exist.")); - - private Validation MediaSourceMustHaveActiveConnection( - JellyfinMediaSource jellyfinMediaSource) - { - Option maybeConnection = jellyfinMediaSource.Connections.FirstOrDefault(); - return maybeConnection.Map(connection => new ConnectionParameters(jellyfinMediaSource, connection)) - .ToValidation("Jellyfin media source requires an active connection"); - } - - private record ConnectionParameters( - JellyfinMediaSource JellyfinMediaSource, - JellyfinConnection ActiveConnection); + _memoryCache = memoryCache; + _mediaSourceRepository = mediaSourceRepository; } -} + + public async Task> Handle( + GetJellyfinConnectionParameters request, + CancellationToken cancellationToken) + { + if (_memoryCache.TryGetValue(request, out JellyfinConnectionParametersViewModel parameters)) + { + return parameters; + } + + Either maybeParameters = + await Validate() + .MapT(cp => new JellyfinConnectionParametersViewModel(cp.ActiveConnection.Address)) + .Map(v => v.ToEither()); + + return maybeParameters.Match( + p => + { + _memoryCache.Set(request, p, TimeSpan.FromHours(1)); + return maybeParameters; + }, + error => error); + } + + private Task> Validate() => + JellyfinMediaSourceMustExist() + .BindT(MediaSourceMustHaveActiveConnection); + + private Task> JellyfinMediaSourceMustExist() => + _mediaSourceRepository.GetAllJellyfin().Map(list => list.HeadOrNone()) + .Map( + v => v.ToValidation( + "Jellyfin media source does not exist.")); + + private Validation MediaSourceMustHaveActiveConnection( + JellyfinMediaSource jellyfinMediaSource) + { + Option maybeConnection = jellyfinMediaSource.Connections.FirstOrDefault(); + return maybeConnection.Map(connection => new ConnectionParameters(jellyfinMediaSource, connection)) + .ToValidation("Jellyfin media source requires an active connection"); + } + + private record ConnectionParameters( + JellyfinMediaSource JellyfinMediaSource, + JellyfinConnection ActiveConnection); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Jellyfin/Queries/GetJellyfinLibrariesBySourceId.cs b/ErsatzTV.Application/Jellyfin/Queries/GetJellyfinLibrariesBySourceId.cs index 2d5e106ec..7e9783c49 100644 --- a/ErsatzTV.Application/Jellyfin/Queries/GetJellyfinLibrariesBySourceId.cs +++ b/ErsatzTV.Application/Jellyfin/Queries/GetJellyfinLibrariesBySourceId.cs @@ -1,7 +1,3 @@ -using System.Collections.Generic; -using MediatR; +namespace ErsatzTV.Application.Jellyfin; -namespace ErsatzTV.Application.Jellyfin.Queries -{ - public record GetJellyfinLibrariesBySourceId(int JellyfinMediaSourceId) : IRequest>; -} +public record GetJellyfinLibrariesBySourceId(int JellyfinMediaSourceId) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Jellyfin/Queries/GetJellyfinLibrariesBySourceIdHandler.cs b/ErsatzTV.Application/Jellyfin/Queries/GetJellyfinLibrariesBySourceIdHandler.cs index a82c31e99..b1232a4bd 100644 --- a/ErsatzTV.Application/Jellyfin/Queries/GetJellyfinLibrariesBySourceIdHandler.cs +++ b/ErsatzTV.Application/Jellyfin/Queries/GetJellyfinLibrariesBySourceIdHandler.cs @@ -1,27 +1,20 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -using MediatR; +using ErsatzTV.Core.Interfaces.Repositories; using static ErsatzTV.Application.Jellyfin.Mapper; -namespace ErsatzTV.Application.Jellyfin.Queries +namespace ErsatzTV.Application.Jellyfin; + +public class + GetJellyfinLibrariesBySourceIdHandler : IRequestHandler> { - public class - GetJellyfinLibrariesBySourceIdHandler : IRequestHandler> - { - private readonly IMediaSourceRepository _mediaSourceRepository; + private readonly IMediaSourceRepository _mediaSourceRepository; - public GetJellyfinLibrariesBySourceIdHandler(IMediaSourceRepository mediaSourceRepository) => - _mediaSourceRepository = mediaSourceRepository; + public GetJellyfinLibrariesBySourceIdHandler(IMediaSourceRepository mediaSourceRepository) => + _mediaSourceRepository = mediaSourceRepository; - public Task> Handle( - GetJellyfinLibrariesBySourceId request, - CancellationToken cancellationToken) => - _mediaSourceRepository.GetJellyfinLibraries(request.JellyfinMediaSourceId) - .Map(list => list.Map(ProjectToViewModel).ToList()); - } -} + public Task> Handle( + GetJellyfinLibrariesBySourceId request, + CancellationToken cancellationToken) => + _mediaSourceRepository.GetJellyfinLibraries(request.JellyfinMediaSourceId) + .Map(list => list.Map(ProjectToViewModel).ToList()); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Jellyfin/Queries/GetJellyfinMediaSourceById.cs b/ErsatzTV.Application/Jellyfin/Queries/GetJellyfinMediaSourceById.cs index 1ba7b8a09..b6808d9ae 100644 --- a/ErsatzTV.Application/Jellyfin/Queries/GetJellyfinMediaSourceById.cs +++ b/ErsatzTV.Application/Jellyfin/Queries/GetJellyfinMediaSourceById.cs @@ -1,8 +1,4 @@ -using LanguageExt; -using MediatR; +namespace ErsatzTV.Application.Jellyfin; -namespace ErsatzTV.Application.Jellyfin.Queries -{ - public record GetJellyfinMediaSourceById - (int JellyfinMediaSourceId) : IRequest>; -} +public record GetJellyfinMediaSourceById + (int JellyfinMediaSourceId) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Jellyfin/Queries/GetJellyfinMediaSourceByIdHandler.cs b/ErsatzTV.Application/Jellyfin/Queries/GetJellyfinMediaSourceByIdHandler.cs index a9cc36fab..9c9f7e454 100644 --- a/ErsatzTV.Application/Jellyfin/Queries/GetJellyfinMediaSourceByIdHandler.cs +++ b/ErsatzTV.Application/Jellyfin/Queries/GetJellyfinMediaSourceByIdHandler.cs @@ -1,24 +1,19 @@ -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -using MediatR; +using ErsatzTV.Core.Interfaces.Repositories; using static ErsatzTV.Application.Jellyfin.Mapper; -namespace ErsatzTV.Application.Jellyfin.Queries +namespace ErsatzTV.Application.Jellyfin; + +public class + GetJellyfinMediaSourceByIdHandler : IRequestHandler> { - public class - GetJellyfinMediaSourceByIdHandler : IRequestHandler> - { - private readonly IMediaSourceRepository _mediaSourceRepository; + private readonly IMediaSourceRepository _mediaSourceRepository; - public GetJellyfinMediaSourceByIdHandler(IMediaSourceRepository mediaSourceRepository) => - _mediaSourceRepository = mediaSourceRepository; + public GetJellyfinMediaSourceByIdHandler(IMediaSourceRepository mediaSourceRepository) => + _mediaSourceRepository = mediaSourceRepository; - public Task> Handle( - GetJellyfinMediaSourceById request, - CancellationToken cancellationToken) => - _mediaSourceRepository.GetJellyfin(request.JellyfinMediaSourceId).MapT(ProjectToViewModel); - } -} + public Task> Handle( + GetJellyfinMediaSourceById request, + CancellationToken cancellationToken) => + _mediaSourceRepository.GetJellyfin(request.JellyfinMediaSourceId).MapT(ProjectToViewModel); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Jellyfin/Queries/GetJellyfinPathReplacementsBySourceId.cs b/ErsatzTV.Application/Jellyfin/Queries/GetJellyfinPathReplacementsBySourceId.cs index 73aa27862..83eadc9e6 100644 --- a/ErsatzTV.Application/Jellyfin/Queries/GetJellyfinPathReplacementsBySourceId.cs +++ b/ErsatzTV.Application/Jellyfin/Queries/GetJellyfinPathReplacementsBySourceId.cs @@ -1,8 +1,4 @@ -using System.Collections.Generic; -using MediatR; +namespace ErsatzTV.Application.Jellyfin; -namespace ErsatzTV.Application.Jellyfin.Queries -{ - public record GetJellyfinPathReplacementsBySourceId - (int JellyfinMediaSourceId) : IRequest>; -} +public record GetJellyfinPathReplacementsBySourceId + (int JellyfinMediaSourceId) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Jellyfin/Queries/GetJellyfinPathReplacementsBySourceIdHandler.cs b/ErsatzTV.Application/Jellyfin/Queries/GetJellyfinPathReplacementsBySourceIdHandler.cs index 4db4d0226..b84654063 100644 --- a/ErsatzTV.Application/Jellyfin/Queries/GetJellyfinPathReplacementsBySourceIdHandler.cs +++ b/ErsatzTV.Application/Jellyfin/Queries/GetJellyfinPathReplacementsBySourceIdHandler.cs @@ -1,26 +1,19 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -using MediatR; +using ErsatzTV.Core.Interfaces.Repositories; using static ErsatzTV.Application.Jellyfin.Mapper; -namespace ErsatzTV.Application.Jellyfin.Queries +namespace ErsatzTV.Application.Jellyfin; + +public class GetJellyfinPathReplacementsBySourceIdHandler : IRequestHandler> { - public class GetJellyfinPathReplacementsBySourceIdHandler : IRequestHandler> - { - private readonly IMediaSourceRepository _mediaSourceRepository; + private readonly IMediaSourceRepository _mediaSourceRepository; - public GetJellyfinPathReplacementsBySourceIdHandler(IMediaSourceRepository mediaSourceRepository) => - _mediaSourceRepository = mediaSourceRepository; + public GetJellyfinPathReplacementsBySourceIdHandler(IMediaSourceRepository mediaSourceRepository) => + _mediaSourceRepository = mediaSourceRepository; - public Task> Handle( - GetJellyfinPathReplacementsBySourceId request, - CancellationToken cancellationToken) => - _mediaSourceRepository.GetJellyfinPathReplacements(request.JellyfinMediaSourceId) - .Map(list => list.Map(ProjectToViewModel).ToList()); - } -} + public Task> Handle( + GetJellyfinPathReplacementsBySourceId request, + CancellationToken cancellationToken) => + _mediaSourceRepository.GetJellyfinPathReplacements(request.JellyfinMediaSourceId) + .Map(list => list.Map(ProjectToViewModel).ToList()); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Jellyfin/Queries/GetJellyfinSecrets.cs b/ErsatzTV.Application/Jellyfin/Queries/GetJellyfinSecrets.cs index f0a8ef8cc..b67748306 100644 --- a/ErsatzTV.Application/Jellyfin/Queries/GetJellyfinSecrets.cs +++ b/ErsatzTV.Application/Jellyfin/Queries/GetJellyfinSecrets.cs @@ -1,7 +1,5 @@ using ErsatzTV.Core.Jellyfin; -using MediatR; -namespace ErsatzTV.Application.Jellyfin.Queries -{ - public record GetJellyfinSecrets : IRequest; -} +namespace ErsatzTV.Application.Jellyfin; + +public record GetJellyfinSecrets : IRequest; \ No newline at end of file diff --git a/ErsatzTV.Application/Jellyfin/Queries/GetJellyfinSecretsHandler.cs b/ErsatzTV.Application/Jellyfin/Queries/GetJellyfinSecretsHandler.cs index 135822f8f..3f93735b7 100644 --- a/ErsatzTV.Application/Jellyfin/Queries/GetJellyfinSecretsHandler.cs +++ b/ErsatzTV.Application/Jellyfin/Queries/GetJellyfinSecretsHandler.cs @@ -1,19 +1,15 @@ -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core.Interfaces.Jellyfin; +using ErsatzTV.Core.Interfaces.Jellyfin; using ErsatzTV.Core.Jellyfin; -using MediatR; -namespace ErsatzTV.Application.Jellyfin.Queries +namespace ErsatzTV.Application.Jellyfin; + +public class GetJellyfinSecretsHandler : IRequestHandler { - public class GetJellyfinSecretsHandler : IRequestHandler - { - private readonly IJellyfinSecretStore _jellyfinSecretStore; + private readonly IJellyfinSecretStore _jellyfinSecretStore; - public GetJellyfinSecretsHandler(IJellyfinSecretStore jellyfinSecretStore) => - _jellyfinSecretStore = jellyfinSecretStore; + public GetJellyfinSecretsHandler(IJellyfinSecretStore jellyfinSecretStore) => + _jellyfinSecretStore = jellyfinSecretStore; - public Task Handle(GetJellyfinSecrets request, CancellationToken cancellationToken) => - _jellyfinSecretStore.ReadSecrets(); - } -} + public Task Handle(GetJellyfinSecrets request, CancellationToken cancellationToken) => + _jellyfinSecretStore.ReadSecrets(); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Libraries/Commands/CreateLocalLibrary.cs b/ErsatzTV.Application/Libraries/Commands/CreateLocalLibrary.cs index 01f856685..b31e02118 100644 --- a/ErsatzTV.Application/Libraries/Commands/CreateLocalLibrary.cs +++ b/ErsatzTV.Application/Libraries/Commands/CreateLocalLibrary.cs @@ -1,11 +1,7 @@ -using System.Collections.Generic; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.Libraries.Commands -{ - public record CreateLocalLibrary(string Name, LibraryMediaKind MediaKind, List Paths) - : ILocalLibraryRequest, IRequest>; -} +namespace ErsatzTV.Application.Libraries; + +public record CreateLocalLibrary(string Name, LibraryMediaKind MediaKind, List Paths) + : ILocalLibraryRequest, IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Libraries/Commands/CreateLocalLibraryHandler.cs b/ErsatzTV.Application/Libraries/Commands/CreateLocalLibraryHandler.cs index dc5a7e8bf..83ed4eac8 100644 --- a/ErsatzTV.Application/Libraries/Commands/CreateLocalLibraryHandler.cs +++ b/ErsatzTV.Application/Libraries/Commands/CreateLocalLibraryHandler.cs @@ -1,83 +1,76 @@ -using System.Linq; -using System.Threading; -using System.Threading.Channels; -using System.Threading.Tasks; -using ErsatzTV.Application.MediaSources.Commands; +using System.Threading.Channels; +using ErsatzTV.Application.MediaSources; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Locking; using ErsatzTV.Infrastructure.Data; -using LanguageExt; -using MediatR; using Microsoft.EntityFrameworkCore; using static ErsatzTV.Application.Libraries.Mapper; -using static LanguageExt.Prelude; -namespace ErsatzTV.Application.Libraries.Commands +namespace ErsatzTV.Application.Libraries; + +public class CreateLocalLibraryHandler : LocalLibraryHandlerBase, + IRequestHandler> { - public class CreateLocalLibraryHandler : LocalLibraryHandlerBase, - IRequestHandler> + private readonly ChannelWriter _workerChannel; + private readonly IEntityLocker _entityLocker; + private readonly IDbContextFactory _dbContextFactory; + + public CreateLocalLibraryHandler( + ChannelWriter workerChannel, + IEntityLocker entityLocker, + IDbContextFactory dbContextFactory) { - private readonly ChannelWriter _workerChannel; - private readonly IEntityLocker _entityLocker; - private readonly IDbContextFactory _dbContextFactory; - - public CreateLocalLibraryHandler( - ChannelWriter workerChannel, - IEntityLocker entityLocker, - IDbContextFactory dbContextFactory) - { - _workerChannel = workerChannel; - _entityLocker = entityLocker; - _dbContextFactory = dbContextFactory; - } - - public async Task> Handle( - CreateLocalLibrary request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - Validation validation = await Validate(dbContext, request); - return await validation.Apply(localLibrary => PersistLocalLibrary(dbContext, localLibrary)); - } - - private async Task PersistLocalLibrary( - TvContext dbContext, - LocalLibrary localLibrary) - { - await dbContext.LocalLibraries.AddAsync(localLibrary); - await dbContext.SaveChangesAsync(); - - if (_entityLocker.LockLibrary(localLibrary.Id)) - { - await _workerChannel.WriteAsync(new ForceScanLocalLibrary(localLibrary.Id)); - } - - return ProjectToViewModel(localLibrary); - } - - private static Task> Validate( - TvContext dbContext, - CreateLocalLibrary request) => - MediaSourceMustExist(dbContext, request) - .BindT(localLibrary => NameMustBeValid(request, localLibrary)) - .BindT(localLibrary => PathsMustBeValid(dbContext, localLibrary)); - - private static Task> MediaSourceMustExist( - TvContext dbContext, - CreateLocalLibrary request) => - dbContext.LocalMediaSources - .OrderBy(lms => lms.Id) - .FirstOrDefaultAsync() - .Map(Optional) - .MapT( - lms => new LocalLibrary - { - Name = request.Name, - Paths = request.Paths.Map(p => new LibraryPath { Path = p }).ToList(), - MediaKind = request.MediaKind, - MediaSourceId = lms.Id - }) - .Map(o => o.ToValidation("LocalMediaSource does not exist.")); + _workerChannel = workerChannel; + _entityLocker = entityLocker; + _dbContextFactory = dbContextFactory; } -} + + public async Task> Handle( + CreateLocalLibrary request, + CancellationToken cancellationToken) + { + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + Validation validation = await Validate(dbContext, request); + return await LanguageExtensions.Apply(validation, localLibrary => PersistLocalLibrary(dbContext, localLibrary)); + } + + private async Task PersistLocalLibrary( + TvContext dbContext, + LocalLibrary localLibrary) + { + await dbContext.LocalLibraries.AddAsync(localLibrary); + await dbContext.SaveChangesAsync(); + + if (_entityLocker.LockLibrary(localLibrary.Id)) + { + await _workerChannel.WriteAsync(new ForceScanLocalLibrary(localLibrary.Id)); + } + + return ProjectToViewModel(localLibrary); + } + + private static Task> Validate( + TvContext dbContext, + CreateLocalLibrary request) => + MediaSourceMustExist(dbContext, request) + .BindT(localLibrary => NameMustBeValid(request, localLibrary)) + .BindT(localLibrary => PathsMustBeValid(dbContext, localLibrary)); + + private static Task> MediaSourceMustExist( + TvContext dbContext, + CreateLocalLibrary request) => + dbContext.LocalMediaSources + .OrderBy(lms => lms.Id) + .FirstOrDefaultAsync() + .Map(Optional) + .MapT( + lms => new LocalLibrary + { + Name = request.Name, + Paths = request.Paths.Map(p => new LibraryPath { Path = p }).ToList(), + MediaKind = request.MediaKind, + MediaSourceId = lms.Id + }) + .Map(o => o.ToValidation("LocalMediaSource does not exist.")); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Libraries/Commands/CreateLocalLibraryPath.cs b/ErsatzTV.Application/Libraries/Commands/CreateLocalLibraryPath.cs index 8970f67b9..d53b74971 100644 --- a/ErsatzTV.Application/Libraries/Commands/CreateLocalLibraryPath.cs +++ b/ErsatzTV.Application/Libraries/Commands/CreateLocalLibraryPath.cs @@ -1,9 +1,6 @@ using ErsatzTV.Core; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.Libraries.Commands -{ - public record CreateLocalLibraryPath - (int LibraryId, string Path) : IRequest>; -} +namespace ErsatzTV.Application.Libraries; + +public record CreateLocalLibraryPath + (int LibraryId, string Path) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Libraries/Commands/CreateLocalLibraryPathHandler.cs b/ErsatzTV.Application/Libraries/Commands/CreateLocalLibraryPathHandler.cs index 2cd3903dd..72470d58e 100644 --- a/ErsatzTV.Application/Libraries/Commands/CreateLocalLibraryPathHandler.cs +++ b/ErsatzTV.Application/Libraries/Commands/CreateLocalLibraryPathHandler.cs @@ -1,61 +1,51 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -using MediatR; -using static LanguageExt.Prelude; using static ErsatzTV.Application.Libraries.Mapper; -namespace ErsatzTV.Application.Libraries.Commands +namespace ErsatzTV.Application.Libraries; + +public class CreateLocalLibraryPathHandler : IRequestHandler> { - public class CreateLocalLibraryPathHandler : IRequestHandler> + private readonly ILibraryRepository _libraryRepository; + + public CreateLocalLibraryPathHandler(ILibraryRepository libraryRepository) => + _libraryRepository = libraryRepository; + + public Task> Handle( + CreateLocalLibraryPath request, + CancellationToken cancellationToken) => + Validate(request).MapT(PersistLocalLibraryPath).Bind(v => v.ToEitherAsync()); + + private Task PersistLocalLibraryPath(LibraryPath p) => + _libraryRepository.Add(p).Map(ProjectToViewModel); + + private Task> Validate(CreateLocalLibraryPath request) => + ValidateFolder(request) + .MapT( + folder => + new LibraryPath + { + LibraryId = request.LibraryId, + Path = folder + }); + + private async Task> ValidateFolder(CreateLocalLibraryPath request) { - private readonly ILibraryRepository _libraryRepository; + List allPaths = await _libraryRepository.GetLocalPaths(request.LibraryId) + .Map(list => list.Map(c => c.Path).ToList()); - public CreateLocalLibraryPathHandler(ILibraryRepository libraryRepository) => - _libraryRepository = libraryRepository; - - public Task> Handle( - CreateLocalLibraryPath request, - CancellationToken cancellationToken) => - Validate(request).MapT(PersistLocalLibraryPath).Bind(v => v.ToEitherAsync()); - - private Task PersistLocalLibraryPath(LibraryPath p) => - _libraryRepository.Add(p).Map(ProjectToViewModel); - - private Task> Validate(CreateLocalLibraryPath request) => - ValidateFolder(request) - .MapT( - folder => - new LibraryPath - { - LibraryId = request.LibraryId, - Path = folder - }); - - private async Task> ValidateFolder(CreateLocalLibraryPath request) - { - List allPaths = await _libraryRepository.GetLocalPaths(request.LibraryId) - .Map(list => list.Map(c => c.Path).ToList()); - - return Optional(request.Path) - .Where(folder => allPaths.ForAll(f => !AreSubPaths(f, folder))) - .ToValidation("Path must not belong to another library path"); - } - - private static bool AreSubPaths(string path1, string path2) - { - string one = path1 + Path.DirectorySeparatorChar; - string two = path2 + Path.DirectorySeparatorChar; - return one == two || one.StartsWith(two, StringComparison.OrdinalIgnoreCase) || - two.StartsWith(one, StringComparison.OrdinalIgnoreCase); - } + return Optional(request.Path) + .Where(folder => allPaths.ForAll(f => !AreSubPaths(f, folder))) + .ToValidation("Path must not belong to another library path"); } -} + + private static bool AreSubPaths(string path1, string path2) + { + string one = path1 + Path.DirectorySeparatorChar; + string two = path2 + Path.DirectorySeparatorChar; + return one == two || one.StartsWith(two, StringComparison.OrdinalIgnoreCase) || + two.StartsWith(one, StringComparison.OrdinalIgnoreCase); + } +} \ No newline at end of file diff --git a/ErsatzTV.Application/Libraries/Commands/DeleteLocalLibrary.cs b/ErsatzTV.Application/Libraries/Commands/DeleteLocalLibrary.cs index 6a425d305..e563241c2 100644 --- a/ErsatzTV.Application/Libraries/Commands/DeleteLocalLibrary.cs +++ b/ErsatzTV.Application/Libraries/Commands/DeleteLocalLibrary.cs @@ -1,9 +1,5 @@ using ErsatzTV.Core; -using LanguageExt; -using MediatR; -using Unit = LanguageExt.Unit; -namespace ErsatzTV.Application.Libraries.Commands -{ - public record DeleteLocalLibrary(int LocalLibraryId) : IRequest>; -} +namespace ErsatzTV.Application.Libraries; + +public record DeleteLocalLibrary(int LocalLibraryId) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Libraries/Commands/DeleteLocalLibraryHandler.cs b/ErsatzTV.Application/Libraries/Commands/DeleteLocalLibraryHandler.cs index 69fe3c933..d9ff2ca5e 100644 --- a/ErsatzTV.Application/Libraries/Commands/DeleteLocalLibraryHandler.cs +++ b/ErsatzTV.Application/Libraries/Commands/DeleteLocalLibraryHandler.cs @@ -1,70 +1,62 @@ -using System.Collections.Generic; -using System.Data; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; +using System.Data; using Dapper; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; -using LanguageExt; -using MediatR; using Microsoft.EntityFrameworkCore; -using Unit = LanguageExt.Unit; -namespace ErsatzTV.Application.Libraries.Commands +namespace ErsatzTV.Application.Libraries; + +public class DeleteLocalLibraryHandler : LocalLibraryHandlerBase, + IRequestHandler> { - public class DeleteLocalLibraryHandler : LocalLibraryHandlerBase, - IRequestHandler> + private readonly IDbContextFactory _dbContextFactory; + private readonly IDbConnection _dbConnection; + private readonly ISearchIndex _searchIndex; + + public DeleteLocalLibraryHandler( + IDbContextFactory dbContextFactory, + IDbConnection dbConnection, + ISearchIndex searchIndex) { - private readonly IDbContextFactory _dbContextFactory; - private readonly IDbConnection _dbConnection; - private readonly ISearchIndex _searchIndex; + _dbContextFactory = dbContextFactory; + _dbConnection = dbConnection; + _searchIndex = searchIndex; + } - public DeleteLocalLibraryHandler( - IDbContextFactory dbContextFactory, - IDbConnection dbConnection, - ISearchIndex searchIndex) - { - _dbContextFactory = dbContextFactory; - _dbConnection = dbConnection; - _searchIndex = searchIndex; - } + public async Task> Handle( + DeleteLocalLibrary request, + CancellationToken cancellationToken) + { + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + Validation validation = await LocalLibraryMustExist(dbContext, request); + return await LanguageExtensions.Apply(validation, localLibrary => DoDeletion(dbContext, localLibrary)); + } - public async Task> Handle( - DeleteLocalLibrary request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - Validation validation = await LocalLibraryMustExist(dbContext, request); - return await validation.Apply(localLibrary => DoDeletion(dbContext, localLibrary)); - } - - private async Task DoDeletion(TvContext dbContext, LocalLibrary localLibrary) - { - List ids = await _dbConnection.QueryAsync( - @"SELECT MediaItem.Id FROM MediaItem + private async Task DoDeletion(TvContext dbContext, LocalLibrary localLibrary) + { + List ids = await _dbConnection.QueryAsync( + @"SELECT MediaItem.Id FROM MediaItem INNER JOIN LibraryPath LP on MediaItem.LibraryPathId = LP.Id WHERE LP.LibraryId = @LibraryId", - new { LibraryId = localLibrary.Id }) - .Map(result => result.ToList()); + new { LibraryId = localLibrary.Id }) + .Map(result => result.ToList()); - await _searchIndex.RemoveItems(ids); - _searchIndex.Commit(); + await _searchIndex.RemoveItems(ids); + _searchIndex.Commit(); - dbContext.LocalLibraries.Remove(localLibrary); - await dbContext.SaveChangesAsync(); + dbContext.LocalLibraries.Remove(localLibrary); + await dbContext.SaveChangesAsync(); - return Unit.Default; - } - - private static Task> LocalLibraryMustExist( - TvContext dbContext, - DeleteLocalLibrary request) => - dbContext.LocalLibraries - .SelectOneAsync(ll => ll.Id, ll => ll.Id == request.LocalLibraryId) - .Map(o => o.ToValidation($"Local library {request.LocalLibraryId} does not exist.")); + return Unit.Default; } -} + + private static Task> LocalLibraryMustExist( + TvContext dbContext, + DeleteLocalLibrary request) => + dbContext.LocalLibraries + .SelectOneAsync(ll => ll.Id, ll => ll.Id == request.LocalLibraryId) + .Map(o => o.ToValidation($"Local library {request.LocalLibraryId} does not exist.")); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Libraries/Commands/ILocalLibraryRequest.cs b/ErsatzTV.Application/Libraries/Commands/ILocalLibraryRequest.cs index 59682c5c0..0eba13106 100644 --- a/ErsatzTV.Application/Libraries/Commands/ILocalLibraryRequest.cs +++ b/ErsatzTV.Application/Libraries/Commands/ILocalLibraryRequest.cs @@ -1,7 +1,6 @@ -namespace ErsatzTV.Application.Libraries.Commands +namespace ErsatzTV.Application.Libraries; + +public interface ILocalLibraryRequest { - public interface ILocalLibraryRequest - { - public string Name { get; } - } -} + public string Name { get; } +} \ No newline at end of file diff --git a/ErsatzTV.Application/Libraries/Commands/LocalLibraryHandlerBase.cs b/ErsatzTV.Application/Libraries/Commands/LocalLibraryHandlerBase.cs index 82d22acec..17688ce65 100644 --- a/ErsatzTV.Application/Libraries/Commands/LocalLibraryHandlerBase.cs +++ b/ErsatzTV.Application/Libraries/Commands/LocalLibraryHandlerBase.cs @@ -1,49 +1,41 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Infrastructure.Data; -using LanguageExt; using Microsoft.EntityFrameworkCore; -using static LanguageExt.Prelude; -namespace ErsatzTV.Application.Libraries.Commands +namespace ErsatzTV.Application.Libraries; + +public abstract class LocalLibraryHandlerBase { - public abstract class LocalLibraryHandlerBase + protected static Task> NameMustBeValid( + ILocalLibraryRequest request, + LocalLibrary localLibrary) => + request.NotEmpty(c => c.Name) + .Bind(_ => request.NotLongerThan(50)(c => c.Name)) + .Map(_ => localLibrary).AsTask(); + + protected static async Task> PathsMustBeValid( + TvContext dbContext, + LocalLibrary localLibrary, + int? existingLibraryId = null) { - protected static Task> NameMustBeValid( - ILocalLibraryRequest request, - LocalLibrary localLibrary) => - request.NotEmpty(c => c.Name) - .Bind(_ => request.NotLongerThan(50)(c => c.Name)) - .Map(_ => localLibrary).AsTask(); + List allPaths = await dbContext.LocalLibraries + .Include(ll => ll.Paths) + .Filter(ll => existingLibraryId == null || ll.Id != existingLibraryId) + .ToListAsync() + .Map(list => list.SelectMany(ll => ll.Paths).Map(lp => lp.Path).ToList()); - protected static async Task> PathsMustBeValid( - TvContext dbContext, - LocalLibrary localLibrary, - int? existingLibraryId = null) - { - List allPaths = await dbContext.LocalLibraries - .Include(ll => ll.Paths) - .Filter(ll => existingLibraryId == null || ll.Id != existingLibraryId) - .ToListAsync() - .Map(list => list.SelectMany(ll => ll.Paths).Map(lp => lp.Path).ToList()); - - return Optional(localLibrary.Paths.Count(folder => allPaths.Any(f => AreSubPaths(f, folder.Path)))) - .Where(length => length == 0) - .Map(_ => localLibrary) - .ToValidation("Path must not belong to another library path"); - } - - private static bool AreSubPaths(string path1, string path2) - { - string one = path1 + Path.DirectorySeparatorChar; - string two = path2 + Path.DirectorySeparatorChar; - return one == two || one.StartsWith(two, StringComparison.OrdinalIgnoreCase) || - two.StartsWith(one, StringComparison.OrdinalIgnoreCase); - } + return Optional(localLibrary.Paths.Count(folder => allPaths.Any(f => AreSubPaths(f, folder.Path)))) + .Where(length => length == 0) + .Map(_ => localLibrary) + .ToValidation("Path must not belong to another library path"); } -} + + private static bool AreSubPaths(string path1, string path2) + { + string one = path1 + Path.DirectorySeparatorChar; + string two = path2 + Path.DirectorySeparatorChar; + return one == two || one.StartsWith(two, StringComparison.OrdinalIgnoreCase) || + two.StartsWith(one, StringComparison.OrdinalIgnoreCase); + } +} \ No newline at end of file diff --git a/ErsatzTV.Application/Libraries/Commands/MoveLocalLibraryPath.cs b/ErsatzTV.Application/Libraries/Commands/MoveLocalLibraryPath.cs index 588008308..ecf9d52b9 100644 --- a/ErsatzTV.Application/Libraries/Commands/MoveLocalLibraryPath.cs +++ b/ErsatzTV.Application/Libraries/Commands/MoveLocalLibraryPath.cs @@ -1,9 +1,5 @@ using ErsatzTV.Core; -using LanguageExt; -using MediatR; -using Unit = LanguageExt.Unit; -namespace ErsatzTV.Application.Libraries.Commands -{ - public record MoveLocalLibraryPath(int LibraryPathId, int TargetLibraryId) : IRequest>; -} +namespace ErsatzTV.Application.Libraries; + +public record MoveLocalLibraryPath(int LibraryPathId, int TargetLibraryId) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Libraries/Commands/MoveLocalLibraryPathHandler.cs b/ErsatzTV.Application/Libraries/Commands/MoveLocalLibraryPathHandler.cs index ab98e318c..91e8980e4 100644 --- a/ErsatzTV.Application/Libraries/Commands/MoveLocalLibraryPathHandler.cs +++ b/ErsatzTV.Application/Libraries/Commands/MoveLocalLibraryPathHandler.cs @@ -1,8 +1,4 @@ -using System.Collections.Generic; -using System.Data; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; +using System.Data; using Dapper; using ErsatzTV.Core; using ErsatzTV.Core.Domain; @@ -10,112 +6,108 @@ using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; -using LanguageExt; -using MediatR; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using Unit = LanguageExt.Unit; -namespace ErsatzTV.Application.Libraries.Commands +namespace ErsatzTV.Application.Libraries; + +public class MoveLocalLibraryPathHandler : IRequestHandler> { - public class MoveLocalLibraryPathHandler : IRequestHandler> + private readonly ISearchIndex _searchIndex; + private readonly ISearchRepository _searchRepository; + private readonly IDbContextFactory _dbContextFactory; + private readonly IDbConnection _dbConnection; + private readonly ILogger _logger; + + public MoveLocalLibraryPathHandler( + ISearchIndex searchIndex, + ISearchRepository searchRepository, + IDbContextFactory dbContextFactory, + IDbConnection dbConnection, + ILogger logger) { - private readonly ISearchIndex _searchIndex; - private readonly ISearchRepository _searchRepository; - private readonly IDbContextFactory _dbContextFactory; - private readonly IDbConnection _dbConnection; - private readonly ILogger _logger; + _searchIndex = searchIndex; + _searchRepository = searchRepository; + _dbContextFactory = dbContextFactory; + _dbConnection = dbConnection; + _logger = logger; + } - public MoveLocalLibraryPathHandler( - ISearchIndex searchIndex, - ISearchRepository searchRepository, - IDbContextFactory dbContextFactory, - IDbConnection dbConnection, - ILogger logger) + public async Task> Handle( + MoveLocalLibraryPath request, + CancellationToken cancellationToken) + { + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + Validation validation = await Validate(dbContext, request); + return await LanguageExtensions.Apply(validation, parameters => MovePath(dbContext, parameters)); + } + + private async Task MovePath(TvContext dbContext, Parameters parameters) + { + LibraryPath path = parameters.LibraryPath; + LocalLibrary newLibrary = parameters.Library; + + path.LibraryId = newLibrary.Id; + if (await dbContext.SaveChangesAsync() > 0) { - _searchIndex = searchIndex; - _searchRepository = searchRepository; - _dbContextFactory = dbContextFactory; - _dbConnection = dbConnection; - _logger = logger; - } + List ids = await _dbConnection.QueryAsync( + @"SELECT MediaItem.Id FROM MediaItem WHERE LibraryPathId = @LibraryPathId", + new { LibraryPathId = path.Id }) + .Map(result => result.ToList()); - public async Task> Handle( - MoveLocalLibraryPath request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - Validation validation = await Validate(dbContext, request); - return await validation.Apply(parameters => MovePath(dbContext, parameters)); - } - - private async Task MovePath(TvContext dbContext, Parameters parameters) - { - LibraryPath path = parameters.LibraryPath; - LocalLibrary newLibrary = parameters.Library; - - path.LibraryId = newLibrary.Id; - if (await dbContext.SaveChangesAsync() > 0) + foreach (int id in ids) { - List ids = await _dbConnection.QueryAsync( - @"SELECT MediaItem.Id FROM MediaItem WHERE LibraryPathId = @LibraryPathId", - new { LibraryPathId = path.Id }) - .Map(result => result.ToList()); - - foreach (int id in ids) + Option maybeMediaItem = await _searchRepository.GetItemToIndex(id); + foreach (MediaItem mediaItem in maybeMediaItem) { - Option maybeMediaItem = await _searchRepository.GetItemToIndex(id); - foreach (MediaItem mediaItem in maybeMediaItem) - { - _logger.LogInformation("Moving item at {Path}", await GetPath(mediaItem)); - await _searchIndex.UpdateItems(_searchRepository, new List { mediaItem }); - } + _logger.LogInformation("Moving item at {Path}", await GetPath(mediaItem)); + await _searchIndex.UpdateItems(_searchRepository, new List { mediaItem }); } } - - return Unit.Default; } - private static async Task> Validate( - TvContext dbContext, - MoveLocalLibraryPath request) => - (await LibraryPathMustExist(dbContext, request), await LocalLibraryMustExist(dbContext, request)) - .Apply((libraryPath, localLibrary) => new Parameters(libraryPath, localLibrary)); + return Unit.Default; + } - private static Task> LibraryPathMustExist( - TvContext dbContext, - MoveLocalLibraryPath request) => - dbContext.LibraryPaths - .Include(lp => lp.Library) - .SelectOneAsync(c => c.Id, c => c.Id == request.LibraryPathId) - .Map(o => o.ToValidation("LibraryPath does not exist.")); + private static async Task> Validate( + TvContext dbContext, + MoveLocalLibraryPath request) => + (await LibraryPathMustExist(dbContext, request), await LocalLibraryMustExist(dbContext, request)) + .Apply((libraryPath, localLibrary) => new Parameters(libraryPath, localLibrary)); - private static Task> LocalLibraryMustExist( - TvContext dbContext, - MoveLocalLibraryPath request) => - dbContext.LocalLibraries - .Include(ll => ll.Paths) - .SelectOneAsync(a => a.Id, a => a.Id == request.TargetLibraryId) - .Map(o => o.ToValidation("LocalLibrary does not exist")); + private static Task> LibraryPathMustExist( + TvContext dbContext, + MoveLocalLibraryPath request) => + dbContext.LibraryPaths + .Include(lp => lp.Library) + .SelectOneAsync(c => c.Id, c => c.Id == request.LibraryPathId) + .Map(o => o.ToValidation("LibraryPath does not exist.")); - private async Task GetPath(MediaItem mediaItem) => - mediaItem switch - { - Movie => await _dbConnection.QuerySingleAsync( - @"SELECT Path FROM MediaFile + private static Task> LocalLibraryMustExist( + TvContext dbContext, + MoveLocalLibraryPath request) => + dbContext.LocalLibraries + .Include(ll => ll.Paths) + .SelectOneAsync(a => a.Id, a => a.Id == request.TargetLibraryId) + .Map(o => o.ToValidation("LocalLibrary does not exist")); + + private async Task GetPath(MediaItem mediaItem) => + mediaItem switch + { + Movie => await _dbConnection.QuerySingleAsync( + @"SELECT Path FROM MediaFile INNER JOIN MediaVersion MV on MediaFile.MediaVersionId = MV.Id WHERE MV.MovieId = @Id", new { mediaItem.Id }), - Episode => await _dbConnection.QuerySingleAsync( - @"SELECT Path FROM MediaFile + Episode => await _dbConnection.QuerySingleAsync( + @"SELECT Path FROM MediaFile INNER JOIN MediaVersion MV on MediaFile.MediaVersionId = MV.Id WHERE MV.EpisodeId = @Id", new { mediaItem.Id }), - MusicVideo => await _dbConnection.QuerySingleAsync( - @"SELECT Path FROM MediaFile + MusicVideo => await _dbConnection.QuerySingleAsync( + @"SELECT Path FROM MediaFile INNER JOIN MediaVersion MV on MediaFile.MediaVersionId = MV.Id WHERE MV.MusicVideoId = @Id", new { mediaItem.Id }), - _ => null - }; + _ => null + }; - private record Parameters(LibraryPath LibraryPath, LocalLibrary Library); - } -} + private record Parameters(LibraryPath LibraryPath, LocalLibrary Library); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Libraries/Commands/UpdateLocalLibrary.cs b/ErsatzTV.Application/Libraries/Commands/UpdateLocalLibrary.cs index efc73b4d0..1116d7047 100644 --- a/ErsatzTV.Application/Libraries/Commands/UpdateLocalLibrary.cs +++ b/ErsatzTV.Application/Libraries/Commands/UpdateLocalLibrary.cs @@ -1,12 +1,8 @@ -using System.Collections.Generic; -using ErsatzTV.Core; -using LanguageExt; -using MediatR; +using ErsatzTV.Core; -namespace ErsatzTV.Application.Libraries.Commands -{ - public record UpdateLocalLibraryPath(int Id, string Path); +namespace ErsatzTV.Application.Libraries; - public record UpdateLocalLibrary(int Id, string Name, List Paths) : ILocalLibraryRequest, - IRequest>; -} +public record UpdateLocalLibraryPath(int Id, string Path); + +public record UpdateLocalLibrary(int Id, string Name, List Paths) : ILocalLibraryRequest, + IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Libraries/Commands/UpdateLocalLibraryHandler.cs b/ErsatzTV.Application/Libraries/Commands/UpdateLocalLibraryHandler.cs index c4f4576f9..3aa29fbf1 100644 --- a/ErsatzTV.Application/Libraries/Commands/UpdateLocalLibraryHandler.cs +++ b/ErsatzTV.Application/Libraries/Commands/UpdateLocalLibraryHandler.cs @@ -1,125 +1,116 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Channels; -using System.Threading.Tasks; -using ErsatzTV.Application.MediaSources.Commands; +using System.Threading.Channels; +using ErsatzTV.Application.MediaSources; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Locking; using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; -using LanguageExt; -using MediatR; using Microsoft.EntityFrameworkCore; using static ErsatzTV.Application.Libraries.Mapper; -namespace ErsatzTV.Application.Libraries.Commands +namespace ErsatzTV.Application.Libraries; + +public class UpdateLocalLibraryHandler : LocalLibraryHandlerBase, + IRequestHandler> { - public class UpdateLocalLibraryHandler : LocalLibraryHandlerBase, - IRequestHandler> + private readonly ChannelWriter _workerChannel; + private readonly IEntityLocker _entityLocker; + private readonly ISearchIndex _searchIndex; + private readonly IDbContextFactory _dbContextFactory; + + public UpdateLocalLibraryHandler( + ChannelWriter workerChannel, + IEntityLocker entityLocker, + ISearchIndex searchIndex, + IDbContextFactory dbContextFactory) { - private readonly ChannelWriter _workerChannel; - private readonly IEntityLocker _entityLocker; - private readonly ISearchIndex _searchIndex; - private readonly IDbContextFactory _dbContextFactory; - - public UpdateLocalLibraryHandler( - ChannelWriter workerChannel, - IEntityLocker entityLocker, - ISearchIndex searchIndex, - IDbContextFactory dbContextFactory) - { - _workerChannel = workerChannel; - _entityLocker = entityLocker; - _searchIndex = searchIndex; - _dbContextFactory = dbContextFactory; - } - - public async Task> Handle( - UpdateLocalLibrary request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - Validation validation = await Validate(dbContext, request); - return await validation.Apply(parameters => UpdateLocalLibrary(dbContext, parameters)); - } - - private async Task UpdateLocalLibrary(TvContext dbContext, Parameters parameters) - { - (LocalLibrary existing, LocalLibrary incoming) = parameters; - existing.Name = incoming.Name; - - var toAdd = incoming.Paths - .Filter(p => existing.Paths.All(ep => NormalizePath(ep.Path) != NormalizePath(p.Path))) - .ToList(); - var toRemove = existing.Paths - .Filter(ep => incoming.Paths.All(p => NormalizePath(p.Path) != NormalizePath(ep.Path))) - .ToList(); - - var toRemoveIds = toRemove.Map(lp => lp.Id).ToList(); - - List itemsToRemove = await dbContext.MediaItems - .Filter(mi => toRemoveIds.Contains(mi.LibraryPathId)) - .Map(mi => mi.Id) - .ToListAsync(); - - existing.Paths.RemoveAll(toRemove.Contains); - existing.Paths.AddRange(toAdd); - - if (await dbContext.SaveChangesAsync() > 0) - { - await _searchIndex.RemoveItems(itemsToRemove); - _searchIndex.Commit(); - } - - if ((toAdd.Count > 0 || toRemove.Count > 0) && _entityLocker.LockLibrary(existing.Id)) - { - await _workerChannel.WriteAsync(new ForceScanLocalLibrary(existing.Id)); - } - - return ProjectToViewModel(existing); - } - - private static Task> Validate( - TvContext dbContext, - UpdateLocalLibrary request) => - LocalLibraryMustExist(dbContext, request) - .BindT(parameters => NameMustBeValid(request, parameters.Incoming).MapT(_ => parameters)) - .BindT( - parameters => PathsMustBeValid(dbContext, parameters.Incoming, parameters.Existing.Id) - .MapT(_ => parameters)); - - private static Task> LocalLibraryMustExist( - TvContext dbContext, - UpdateLocalLibrary request) => - dbContext.LocalLibraries - .Include(ll => ll.Paths) - .SelectOneAsync(ll => ll.Id, ll => ll.Id == request.Id) - .MapT( - existing => - { - var incoming = new LocalLibrary - { - Name = request.Name, - Paths = request.Paths.Map(p => new LibraryPath { Id = p.Id, Path = p.Path }).ToList(), - MediaSourceId = existing.Id - }; - - return new Parameters(existing, incoming); - }) - .Map(o => o.ToValidation("LocalLibrary does not exist.")); - - private record Parameters(LocalLibrary Existing, LocalLibrary Incoming); - - private static string NormalizePath(string path) - { - return Path.GetFullPath(new Uri(path).LocalPath) - .TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) - .ToUpperInvariant(); - } + _workerChannel = workerChannel; + _entityLocker = entityLocker; + _searchIndex = searchIndex; + _dbContextFactory = dbContextFactory; } -} + + public async Task> Handle( + UpdateLocalLibrary request, + CancellationToken cancellationToken) + { + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + Validation validation = await Validate(dbContext, request); + return await LanguageExtensions.Apply(validation, parameters => UpdateLocalLibrary(dbContext, parameters)); + } + + private async Task UpdateLocalLibrary(TvContext dbContext, Parameters parameters) + { + (LocalLibrary existing, LocalLibrary incoming) = parameters; + existing.Name = incoming.Name; + + var toAdd = incoming.Paths + .Filter(p => existing.Paths.All(ep => NormalizePath(ep.Path) != NormalizePath(p.Path))) + .ToList(); + var toRemove = existing.Paths + .Filter(ep => incoming.Paths.All(p => NormalizePath(p.Path) != NormalizePath(ep.Path))) + .ToList(); + + var toRemoveIds = toRemove.Map(lp => lp.Id).ToList(); + + List itemsToRemove = await dbContext.MediaItems + .Filter(mi => toRemoveIds.Contains(mi.LibraryPathId)) + .Map(mi => mi.Id) + .ToListAsync(); + + existing.Paths.RemoveAll(toRemove.Contains); + existing.Paths.AddRange(toAdd); + + if (await dbContext.SaveChangesAsync() > 0) + { + await _searchIndex.RemoveItems(itemsToRemove); + _searchIndex.Commit(); + } + + if ((toAdd.Count > 0 || toRemove.Count > 0) && _entityLocker.LockLibrary(existing.Id)) + { + await _workerChannel.WriteAsync(new ForceScanLocalLibrary(existing.Id)); + } + + return ProjectToViewModel(existing); + } + + private static Task> Validate( + TvContext dbContext, + UpdateLocalLibrary request) => + LocalLibraryMustExist(dbContext, request) + .BindT(parameters => NameMustBeValid(request, parameters.Incoming).MapT(_ => parameters)) + .BindT( + parameters => PathsMustBeValid(dbContext, parameters.Incoming, parameters.Existing.Id) + .MapT(_ => parameters)); + + private static Task> LocalLibraryMustExist( + TvContext dbContext, + UpdateLocalLibrary request) => + dbContext.LocalLibraries + .Include(ll => ll.Paths) + .SelectOneAsync(ll => ll.Id, ll => ll.Id == request.Id) + .MapT( + existing => + { + var incoming = new LocalLibrary + { + Name = request.Name, + Paths = request.Paths.Map(p => new LibraryPath { Id = p.Id, Path = p.Path }).ToList(), + MediaSourceId = existing.Id + }; + + return new Parameters(existing, incoming); + }) + .Map(o => o.ToValidation("LocalLibrary does not exist.")); + + private record Parameters(LocalLibrary Existing, LocalLibrary Incoming); + + private static string NormalizePath(string path) + { + return Path.GetFullPath(new Uri(path).LocalPath) + .TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + .ToUpperInvariant(); + } +} \ No newline at end of file diff --git a/ErsatzTV.Application/Libraries/LibraryViewModel.cs b/ErsatzTV.Application/Libraries/LibraryViewModel.cs index 8fbe74c0f..cb3b52149 100644 --- a/ErsatzTV.Application/Libraries/LibraryViewModel.cs +++ b/ErsatzTV.Application/Libraries/LibraryViewModel.cs @@ -1,6 +1,5 @@ using ErsatzTV.Core.Domain; -namespace ErsatzTV.Application.Libraries -{ - public record LibraryViewModel(string LibraryKind, int Id, string Name, LibraryMediaKind MediaKind); -} +namespace ErsatzTV.Application.Libraries; + +public record LibraryViewModel(string LibraryKind, int Id, string Name, LibraryMediaKind MediaKind); \ No newline at end of file diff --git a/ErsatzTV.Application/Libraries/LocalLibraryPathViewModel.cs b/ErsatzTV.Application/Libraries/LocalLibraryPathViewModel.cs index 9c8356bf8..54778c8f0 100644 --- a/ErsatzTV.Application/Libraries/LocalLibraryPathViewModel.cs +++ b/ErsatzTV.Application/Libraries/LocalLibraryPathViewModel.cs @@ -1,4 +1,3 @@ -namespace ErsatzTV.Application.Libraries -{ - public record LocalLibraryPathViewModel(int Id, int LibraryId, string Path); -} +namespace ErsatzTV.Application.Libraries; + +public record LocalLibraryPathViewModel(int Id, int LibraryId, string Path); \ No newline at end of file diff --git a/ErsatzTV.Application/Libraries/LocalLibraryViewModel.cs b/ErsatzTV.Application/Libraries/LocalLibraryViewModel.cs index 5c51fc60a..64acf4140 100644 --- a/ErsatzTV.Application/Libraries/LocalLibraryViewModel.cs +++ b/ErsatzTV.Application/Libraries/LocalLibraryViewModel.cs @@ -1,7 +1,6 @@ using ErsatzTV.Core.Domain; -namespace ErsatzTV.Application.Libraries -{ - public record LocalLibraryViewModel(int Id, string Name, LibraryMediaKind MediaKind) - : LibraryViewModel("Local", Id, Name, MediaKind); -} +namespace ErsatzTV.Application.Libraries; + +public record LocalLibraryViewModel(int Id, string Name, LibraryMediaKind MediaKind) + : LibraryViewModel("Local", Id, Name, MediaKind); \ No newline at end of file diff --git a/ErsatzTV.Application/Libraries/Mapper.cs b/ErsatzTV.Application/Libraries/Mapper.cs index 42ff4ddc1..76e56856f 100644 --- a/ErsatzTV.Application/Libraries/Mapper.cs +++ b/ErsatzTV.Application/Libraries/Mapper.cs @@ -1,26 +1,24 @@ -using System; -using ErsatzTV.Application.Emby; +using ErsatzTV.Application.Emby; using ErsatzTV.Application.Jellyfin; using ErsatzTV.Core.Domain; -namespace ErsatzTV.Application.Libraries +namespace ErsatzTV.Application.Libraries; + +internal static class Mapper { - internal static class Mapper - { - public static LibraryViewModel ProjectToViewModel(Library library) => - library switch - { - LocalLibrary l => ProjectToViewModel(l), - PlexLibrary p => new PlexLibraryViewModel(p.Id, p.Name, p.MediaKind), - JellyfinLibrary j => new JellyfinLibraryViewModel(j.Id, j.Name, j.MediaKind, j.ShouldSyncItems), - EmbyLibrary e => new EmbyLibraryViewModel(e.Id, e.Name, e.MediaKind, e.ShouldSyncItems), - _ => throw new ArgumentOutOfRangeException(nameof(library)) - }; + public static LibraryViewModel ProjectToViewModel(Library library) => + library switch + { + LocalLibrary l => ProjectToViewModel(l), + PlexLibrary p => new PlexLibraryViewModel(p.Id, p.Name, p.MediaKind), + JellyfinLibrary j => new JellyfinLibraryViewModel(j.Id, j.Name, j.MediaKind, j.ShouldSyncItems), + EmbyLibrary e => new EmbyLibraryViewModel(e.Id, e.Name, e.MediaKind, e.ShouldSyncItems), + _ => throw new ArgumentOutOfRangeException(nameof(library)) + }; - public static LocalLibraryViewModel ProjectToViewModel(LocalLibrary library) => - new(library.Id, library.Name, library.MediaKind); + public static LocalLibraryViewModel ProjectToViewModel(LocalLibrary library) => + new(library.Id, library.Name, library.MediaKind); - public static LocalLibraryPathViewModel ProjectToViewModel(LibraryPath libraryPath) => - new(libraryPath.Id, libraryPath.LibraryId, libraryPath.Path); - } -} + public static LocalLibraryPathViewModel ProjectToViewModel(LibraryPath libraryPath) => + new(libraryPath.Id, libraryPath.LibraryId, libraryPath.Path); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Libraries/PlexLibraryViewModel.cs b/ErsatzTV.Application/Libraries/PlexLibraryViewModel.cs index 05d4be639..eabc26bb3 100644 --- a/ErsatzTV.Application/Libraries/PlexLibraryViewModel.cs +++ b/ErsatzTV.Application/Libraries/PlexLibraryViewModel.cs @@ -1,7 +1,6 @@ using ErsatzTV.Core.Domain; -namespace ErsatzTV.Application.Libraries -{ - public record PlexLibraryViewModel(int Id, string Name, LibraryMediaKind MediaKind) - : LibraryViewModel("Plex", Id, Name, MediaKind); -} +namespace ErsatzTV.Application.Libraries; + +public record PlexLibraryViewModel(int Id, string Name, LibraryMediaKind MediaKind) + : LibraryViewModel("Plex", Id, Name, MediaKind); \ No newline at end of file diff --git a/ErsatzTV.Application/Libraries/Queries/CountMediaItemsByLibrary.cs b/ErsatzTV.Application/Libraries/Queries/CountMediaItemsByLibrary.cs index 51d0befa8..f7353132e 100644 --- a/ErsatzTV.Application/Libraries/Queries/CountMediaItemsByLibrary.cs +++ b/ErsatzTV.Application/Libraries/Queries/CountMediaItemsByLibrary.cs @@ -1,6 +1,3 @@ -using MediatR; +namespace ErsatzTV.Application.Libraries; -namespace ErsatzTV.Application.Libraries.Queries -{ - public record CountMediaItemsByLibrary(int LibraryId) : IRequest; -} +public record CountMediaItemsByLibrary(int LibraryId) : IRequest; \ No newline at end of file diff --git a/ErsatzTV.Application/Libraries/Queries/CountMediaItemsByLibraryHandler.cs b/ErsatzTV.Application/Libraries/Queries/CountMediaItemsByLibraryHandler.cs index 8247a2139..44ee657ef 100644 --- a/ErsatzTV.Application/Libraries/Queries/CountMediaItemsByLibraryHandler.cs +++ b/ErsatzTV.Application/Libraries/Queries/CountMediaItemsByLibraryHandler.cs @@ -1,25 +1,21 @@ using System.Data; -using System.Threading; -using System.Threading.Tasks; using Dapper; -using MediatR; -namespace ErsatzTV.Application.Libraries.Queries +namespace ErsatzTV.Application.Libraries; + +public class CountMediaItemsByLibraryHandler : IRequestHandler { - public class CountMediaItemsByLibraryHandler : IRequestHandler + private readonly IDbConnection _dbConnection; + + public CountMediaItemsByLibraryHandler(IDbConnection dbConnection) { - private readonly IDbConnection _dbConnection; + _dbConnection = dbConnection; + } - public CountMediaItemsByLibraryHandler(IDbConnection dbConnection) - { - _dbConnection = dbConnection; - } - - public Task Handle(CountMediaItemsByLibrary request, CancellationToken cancellationToken) => - _dbConnection.QuerySingleAsync( - @"SELECT COUNT(*) FROM MediaItem + public Task Handle(CountMediaItemsByLibrary request, CancellationToken cancellationToken) => + _dbConnection.QuerySingleAsync( + @"SELECT COUNT(*) FROM MediaItem INNER JOIN LibraryPath LP on MediaItem.LibraryPathId = LP.Id WHERE LP.LibraryId = @LibraryId", - new { request.LibraryId }); - } -} + new { request.LibraryId }); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Libraries/Queries/CountMediaItemsByLibraryPath.cs b/ErsatzTV.Application/Libraries/Queries/CountMediaItemsByLibraryPath.cs index 0b3d3b7d7..6f42896ca 100644 --- a/ErsatzTV.Application/Libraries/Queries/CountMediaItemsByLibraryPath.cs +++ b/ErsatzTV.Application/Libraries/Queries/CountMediaItemsByLibraryPath.cs @@ -1,6 +1,3 @@ -using MediatR; +namespace ErsatzTV.Application.Libraries; -namespace ErsatzTV.Application.Libraries.Queries -{ - public record CountMediaItemsByLibraryPath(int LibraryPathId) : IRequest; -} +public record CountMediaItemsByLibraryPath(int LibraryPathId) : IRequest; \ No newline at end of file diff --git a/ErsatzTV.Application/Libraries/Queries/CountMediaItemsByLibraryPathHandler.cs b/ErsatzTV.Application/Libraries/Queries/CountMediaItemsByLibraryPathHandler.cs index 4b862af7c..398bc79bc 100644 --- a/ErsatzTV.Application/Libraries/Queries/CountMediaItemsByLibraryPathHandler.cs +++ b/ErsatzTV.Application/Libraries/Queries/CountMediaItemsByLibraryPathHandler.cs @@ -1,18 +1,14 @@ -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core.Interfaces.Repositories; -using MediatR; +using ErsatzTV.Core.Interfaces.Repositories; -namespace ErsatzTV.Application.Libraries.Queries +namespace ErsatzTV.Application.Libraries; + +public class CountMediaItemsByLibraryPathHandler : IRequestHandler { - public class CountMediaItemsByLibraryPathHandler : IRequestHandler - { - private readonly ILibraryRepository _libraryRepository; + private readonly ILibraryRepository _libraryRepository; - public CountMediaItemsByLibraryPathHandler(ILibraryRepository libraryRepository) => - _libraryRepository = libraryRepository; + public CountMediaItemsByLibraryPathHandler(ILibraryRepository libraryRepository) => + _libraryRepository = libraryRepository; - public Task Handle(CountMediaItemsByLibraryPath request, CancellationToken cancellationToken) => - _libraryRepository.CountMediaItemsByPath(request.LibraryPathId); - } -} + public Task Handle(CountMediaItemsByLibraryPath request, CancellationToken cancellationToken) => + _libraryRepository.CountMediaItemsByPath(request.LibraryPathId); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Libraries/Queries/GetAllLocalLibraries.cs b/ErsatzTV.Application/Libraries/Queries/GetAllLocalLibraries.cs index 379b9f67d..28d3ff1ed 100644 --- a/ErsatzTV.Application/Libraries/Queries/GetAllLocalLibraries.cs +++ b/ErsatzTV.Application/Libraries/Queries/GetAllLocalLibraries.cs @@ -1,7 +1,3 @@ -using System.Collections.Generic; -using MediatR; +namespace ErsatzTV.Application.Libraries; -namespace ErsatzTV.Application.Libraries.Queries -{ - public record GetAllLocalLibraries : IRequest>; -} +public record GetAllLocalLibraries : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Libraries/Queries/GetAllLocalLibrariesHandler.cs b/ErsatzTV.Application/Libraries/Queries/GetAllLocalLibrariesHandler.cs index b30eef61a..b0b18efec 100644 --- a/ErsatzTV.Application/Libraries/Queries/GetAllLocalLibrariesHandler.cs +++ b/ErsatzTV.Application/Libraries/Queries/GetAllLocalLibrariesHandler.cs @@ -1,30 +1,23 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -using MediatR; using static ErsatzTV.Application.Libraries.Mapper; -namespace ErsatzTV.Application.Libraries.Queries +namespace ErsatzTV.Application.Libraries; + +public class GetAllLocalLibrariesHandler : IRequestHandler> { - public class GetAllLocalLibrariesHandler : IRequestHandler> - { - private readonly ILibraryRepository _libraryRepository; + private readonly ILibraryRepository _libraryRepository; - public GetAllLocalLibrariesHandler(ILibraryRepository libraryRepository) => _libraryRepository = libraryRepository; + public GetAllLocalLibrariesHandler(ILibraryRepository libraryRepository) => _libraryRepository = libraryRepository; - public Task> Handle( - GetAllLocalLibraries request, - CancellationToken cancellationToken) => - _libraryRepository.GetAll() - .Map( - list => list - .OfType() - .OrderBy(l => l.MediaKind) - .Map(ProjectToViewModel) - .ToList()); - } -} + public Task> Handle( + GetAllLocalLibraries request, + CancellationToken cancellationToken) => + _libraryRepository.GetAll() + .Map( + list => list + .OfType() + .OrderBy(l => l.MediaKind) + .Map(ProjectToViewModel) + .ToList()); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Libraries/Queries/GetConfiguredLibraries.cs b/ErsatzTV.Application/Libraries/Queries/GetConfiguredLibraries.cs index eb12b8403..42e2779b5 100644 --- a/ErsatzTV.Application/Libraries/Queries/GetConfiguredLibraries.cs +++ b/ErsatzTV.Application/Libraries/Queries/GetConfiguredLibraries.cs @@ -1,7 +1,3 @@ -using System.Collections.Generic; -using MediatR; +namespace ErsatzTV.Application.Libraries; -namespace ErsatzTV.Application.Libraries.Queries -{ - public record GetConfiguredLibraries : IRequest>; -} +public record GetConfiguredLibraries : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Libraries/Queries/GetConfiguredLibrariesHandler.cs b/ErsatzTV.Application/Libraries/Queries/GetConfiguredLibrariesHandler.cs index f6999fa65..2187e1945 100644 --- a/ErsatzTV.Application/Libraries/Queries/GetConfiguredLibrariesHandler.cs +++ b/ErsatzTV.Application/Libraries/Queries/GetConfiguredLibrariesHandler.cs @@ -1,41 +1,34 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -using MediatR; using static ErsatzTV.Application.Libraries.Mapper; -namespace ErsatzTV.Application.Libraries.Queries +namespace ErsatzTV.Application.Libraries; + +public class GetConfiguredLibrariesHandler : IRequestHandler> { - public class GetConfiguredLibrariesHandler : IRequestHandler> - { - private readonly ILibraryRepository _libraryRepository; + private readonly ILibraryRepository _libraryRepository; - public GetConfiguredLibrariesHandler(ILibraryRepository libraryRepository) => - _libraryRepository = libraryRepository; + public GetConfiguredLibrariesHandler(ILibraryRepository libraryRepository) => + _libraryRepository = libraryRepository; - public Task> Handle( - GetConfiguredLibraries request, - CancellationToken cancellationToken) => - _libraryRepository.GetAll() - .Map( - list => list.Filter(ShouldIncludeLibrary) - .OrderBy(l => l.MediaSource is LocalMediaSource ? 0 : 1) - .ThenBy(l => l.GetType().Name) - .ThenBy(l => l.MediaKind) - .Map(ProjectToViewModel).ToList()); + public Task> Handle( + GetConfiguredLibraries request, + CancellationToken cancellationToken) => + _libraryRepository.GetAll() + .Map( + list => list.Filter(ShouldIncludeLibrary) + .OrderBy(l => l.MediaSource is LocalMediaSource ? 0 : 1) + .ThenBy(l => l.GetType().Name) + .ThenBy(l => l.MediaKind) + .Map(ProjectToViewModel).ToList()); - private static bool ShouldIncludeLibrary(Library library) => - library switch - { - LocalLibrary => library.Paths.Count > 0, - PlexLibrary plex => plex.ShouldSyncItems, - JellyfinLibrary jellyfin => jellyfin.ShouldSyncItems, - EmbyLibrary emby => emby.ShouldSyncItems, - _ => false - }; - } -} + private static bool ShouldIncludeLibrary(Library library) => + library switch + { + LocalLibrary => library.Paths.Count > 0, + PlexLibrary plex => plex.ShouldSyncItems, + JellyfinLibrary jellyfin => jellyfin.ShouldSyncItems, + EmbyLibrary emby => emby.ShouldSyncItems, + _ => false + }; +} \ No newline at end of file diff --git a/ErsatzTV.Application/Libraries/Queries/GetLocalLibraryById.cs b/ErsatzTV.Application/Libraries/Queries/GetLocalLibraryById.cs index becb8e12c..229c46999 100644 --- a/ErsatzTV.Application/Libraries/Queries/GetLocalLibraryById.cs +++ b/ErsatzTV.Application/Libraries/Queries/GetLocalLibraryById.cs @@ -1,7 +1,3 @@ -using LanguageExt; -using MediatR; +namespace ErsatzTV.Application.Libraries; -namespace ErsatzTV.Application.Libraries.Queries -{ - public record GetLocalLibraryById(int LibraryId) : IRequest>; -} +public record GetLocalLibraryById(int LibraryId) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Libraries/Queries/GetLocalLibraryByIdHandler.cs b/ErsatzTV.Application/Libraries/Queries/GetLocalLibraryByIdHandler.cs index 6e9f111af..de85e1b7b 100644 --- a/ErsatzTV.Application/Libraries/Queries/GetLocalLibraryByIdHandler.cs +++ b/ErsatzTV.Application/Libraries/Queries/GetLocalLibraryByIdHandler.cs @@ -1,22 +1,17 @@ -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -using MediatR; +using ErsatzTV.Core.Interfaces.Repositories; using static ErsatzTV.Application.Libraries.Mapper; -namespace ErsatzTV.Application.Libraries.Queries +namespace ErsatzTV.Application.Libraries; + +public class GetLocalLibraryByIdHandler : IRequestHandler> { - public class GetLocalLibraryByIdHandler : IRequestHandler> - { - private readonly ILibraryRepository _libraryRepository; + private readonly ILibraryRepository _libraryRepository; - public GetLocalLibraryByIdHandler(ILibraryRepository libraryRepository) => - _libraryRepository = libraryRepository; + public GetLocalLibraryByIdHandler(ILibraryRepository libraryRepository) => + _libraryRepository = libraryRepository; - public Task> Handle( - GetLocalLibraryById request, - CancellationToken cancellationToken) => - _libraryRepository.GetLocal(request.LibraryId).MapT(ProjectToViewModel); - } -} + public Task> Handle( + GetLocalLibraryById request, + CancellationToken cancellationToken) => + _libraryRepository.GetLocal(request.LibraryId).MapT(ProjectToViewModel); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Libraries/Queries/GetLocalLibraryPaths.cs b/ErsatzTV.Application/Libraries/Queries/GetLocalLibraryPaths.cs index 1bf34612b..46077f1f1 100644 --- a/ErsatzTV.Application/Libraries/Queries/GetLocalLibraryPaths.cs +++ b/ErsatzTV.Application/Libraries/Queries/GetLocalLibraryPaths.cs @@ -1,7 +1,3 @@ -using System.Collections.Generic; -using MediatR; +namespace ErsatzTV.Application.Libraries; -namespace ErsatzTV.Application.Libraries.Queries -{ - public record GetLocalLibraryPaths(int LocalLibraryId) : IRequest>; -} +public record GetLocalLibraryPaths(int LocalLibraryId) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Libraries/Queries/GetLocalLibraryPathsHandler.cs b/ErsatzTV.Application/Libraries/Queries/GetLocalLibraryPathsHandler.cs index 517985a7f..c0f596ffb 100644 --- a/ErsatzTV.Application/Libraries/Queries/GetLocalLibraryPathsHandler.cs +++ b/ErsatzTV.Application/Libraries/Queries/GetLocalLibraryPathsHandler.cs @@ -1,25 +1,18 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -using MediatR; +using ErsatzTV.Core.Interfaces.Repositories; using static ErsatzTV.Application.Libraries.Mapper; -namespace ErsatzTV.Application.Libraries.Queries +namespace ErsatzTV.Application.Libraries; + +public class GetLocalLibraryPathsHandler : IRequestHandler> { - public class GetLocalLibraryPathsHandler : IRequestHandler> - { - private readonly ILibraryRepository _libraryRepository; + private readonly ILibraryRepository _libraryRepository; - public GetLocalLibraryPathsHandler(ILibraryRepository libraryRepository) => - _libraryRepository = libraryRepository; + public GetLocalLibraryPathsHandler(ILibraryRepository libraryRepository) => + _libraryRepository = libraryRepository; - public Task> Handle( - GetLocalLibraryPaths request, - CancellationToken cancellationToken) => - _libraryRepository.GetLocalPaths(request.LocalLibraryId) - .Map(list => list.Map(ProjectToViewModel).ToList()); - } -} + public Task> Handle( + GetLocalLibraryPaths request, + CancellationToken cancellationToken) => + _libraryRepository.GetLocalPaths(request.LocalLibraryId) + .Map(list => list.Map(ProjectToViewModel).ToList()); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Logs/LogEntryViewModel.cs b/ErsatzTV.Application/Logs/LogEntryViewModel.cs index f94332a62..140d5a51d 100644 --- a/ErsatzTV.Application/Logs/LogEntryViewModel.cs +++ b/ErsatzTV.Application/Logs/LogEntryViewModel.cs @@ -1,12 +1,10 @@ -using System; -using Serilog.Events; +using Serilog.Events; -namespace ErsatzTV.Application.Logs -{ - public record LogEntryViewModel( - int Id, - DateTime Timestamp, - LogEventLevel Level, - string Exception, - string Message); -} +namespace ErsatzTV.Application.Logs; + +public record LogEntryViewModel( + int Id, + DateTime Timestamp, + LogEventLevel Level, + string Exception, + string Message); \ No newline at end of file diff --git a/ErsatzTV.Application/Logs/Mapper.cs b/ErsatzTV.Application/Logs/Mapper.cs index 24b254a7b..3d55a3820 100644 --- a/ErsatzTV.Application/Logs/Mapper.cs +++ b/ErsatzTV.Application/Logs/Mapper.cs @@ -1,45 +1,42 @@ -using System; -using System.Collections.Generic; -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; using Newtonsoft.Json.Linq; using Serilog.Events; -namespace ErsatzTV.Application.Logs -{ - internal static class Mapper - { - internal static LogEntryViewModel ProjectToViewModel(LogEntry logEntry) - { - string message = logEntry.RenderedMessage; - if (!string.IsNullOrWhiteSpace(logEntry.Properties)) - { - foreach (KeyValuePair property in JObject.Parse(logEntry.Properties)) - { - var token = $"{{{property.Key}}}"; - if (message.Contains(token)) - { - message = message.Replace(token, property.Value.ToString()); - } +namespace ErsatzTV.Application.Logs; - var destructureToken = $"{{@{property.Key}}}"; - if (message.Contains(destructureToken)) - { - message = message.Replace(destructureToken, property.Value.ToString()); - } +internal static class Mapper +{ + internal static LogEntryViewModel ProjectToViewModel(LogEntry logEntry) + { + string message = logEntry.RenderedMessage; + if (!string.IsNullOrWhiteSpace(logEntry.Properties)) + { + foreach (KeyValuePair property in JObject.Parse(logEntry.Properties)) + { + var token = $"{{{property.Key}}}"; + if (message.Contains(token)) + { + message = message.Replace(token, property.Value.ToString()); + } + + var destructureToken = $"{{@{property.Key}}}"; + if (message.Contains(destructureToken)) + { + message = message.Replace(destructureToken, property.Value.ToString()); } } - - if (!Enum.TryParse(logEntry.Level, out LogEventLevel level)) - { - level = LogEventLevel.Debug; - } - - return new LogEntryViewModel( - logEntry.Id, - logEntry.Timestamp, - level, - logEntry.Exception, - message); } + + if (!Enum.TryParse(logEntry.Level, out LogEventLevel level)) + { + level = LogEventLevel.Debug; + } + + return new LogEntryViewModel( + logEntry.Id, + logEntry.Timestamp, + level, + logEntry.Exception, + message); } -} +} \ No newline at end of file diff --git a/ErsatzTV.Application/Logs/PagedLogEntriesViewModel.cs b/ErsatzTV.Application/Logs/PagedLogEntriesViewModel.cs index 0bf8cb95d..6a343d644 100644 --- a/ErsatzTV.Application/Logs/PagedLogEntriesViewModel.cs +++ b/ErsatzTV.Application/Logs/PagedLogEntriesViewModel.cs @@ -1,6 +1,3 @@ -using System.Collections.Generic; +namespace ErsatzTV.Application.Logs; -namespace ErsatzTV.Application.Logs -{ - public record PagedLogEntriesViewModel(int TotalCount, List Page); -} +public record PagedLogEntriesViewModel(int TotalCount, List Page); \ No newline at end of file diff --git a/ErsatzTV.Application/Logs/Queries/GetRecentLogEntries.cs b/ErsatzTV.Application/Logs/Queries/GetRecentLogEntries.cs index efd936b59..5344085df 100644 --- a/ErsatzTV.Application/Logs/Queries/GetRecentLogEntries.cs +++ b/ErsatzTV.Application/Logs/Queries/GetRecentLogEntries.cs @@ -1,14 +1,10 @@ -using System; -using System.Linq.Expressions; +using System.Linq.Expressions; using ErsatzTV.Core.Domain; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.Logs.Queries +namespace ErsatzTV.Application.Logs; + +public record GetRecentLogEntries(int PageNum, int PageSize) : IRequest { - public record GetRecentLogEntries(int PageNum, int PageSize) : IRequest - { - public Expression> SortExpression { get; set; } - public Option SortDescending { get; set; } - } -} + public Expression> SortExpression { get; set; } + public Option SortDescending { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Application/Logs/Queries/GetRecentLogEntriesHandler.cs b/ErsatzTV.Application/Logs/Queries/GetRecentLogEntriesHandler.cs index 0df9f80da..bf7881f10 100644 --- a/ErsatzTV.Application/Logs/Queries/GetRecentLogEntriesHandler.cs +++ b/ErsatzTV.Application/Logs/Queries/GetRecentLogEntriesHandler.cs @@ -1,47 +1,40 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; using ErsatzTV.Infrastructure.Data; -using LanguageExt; -using MediatR; using Microsoft.EntityFrameworkCore; using static ErsatzTV.Application.Logs.Mapper; -namespace ErsatzTV.Application.Logs.Queries +namespace ErsatzTV.Application.Logs; + +public class GetRecentLogEntriesHandler : IRequestHandler { - public class GetRecentLogEntriesHandler : IRequestHandler + private readonly IDbContextFactory _dbContextFactory; + + public GetRecentLogEntriesHandler(IDbContextFactory dbContextFactory) => + _dbContextFactory = dbContextFactory; + + public async Task Handle( + GetRecentLogEntries request, + CancellationToken cancellationToken) { - private readonly IDbContextFactory _dbContextFactory; + await using LogContext logContext = _dbContextFactory.CreateDbContext(); + int count = await logContext.LogEntries.CountAsync(cancellationToken); - public GetRecentLogEntriesHandler(IDbContextFactory dbContextFactory) => - _dbContextFactory = dbContextFactory; + IOrderedQueryable ordered = logContext.LogEntries + .OrderByDescending(le => le.Id); - public async Task Handle( - GetRecentLogEntries request, - CancellationToken cancellationToken) + foreach (bool descending in request.SortDescending) { - await using LogContext logContext = _dbContextFactory.CreateDbContext(); - int count = await logContext.LogEntries.CountAsync(cancellationToken); - - IOrderedQueryable ordered = logContext.LogEntries - .OrderByDescending(le => le.Id); - - foreach (bool descending in request.SortDescending) - { - ordered = descending - ? logContext.LogEntries.OrderByDescending(request.SortExpression).ThenByDescending(le => le.Id) - : logContext.LogEntries.OrderBy(request.SortExpression).ThenByDescending(le => le.Id); - } - - List page = await ordered - .Skip(request.PageNum * request.PageSize) - .Take(request.PageSize) - .ToListAsync(cancellationToken) - .Map(list => list.Map(ProjectToViewModel).ToList()); - - return new PagedLogEntriesViewModel(count, page); + ordered = descending + ? logContext.LogEntries.OrderByDescending(request.SortExpression).ThenByDescending(le => le.Id) + : logContext.LogEntries.OrderBy(request.SortExpression).ThenByDescending(le => le.Id); } + + List page = await ordered + .Skip(request.PageNum * request.PageSize) + .Take(request.PageSize) + .ToListAsync(cancellationToken) + .Map(list => list.Map(ProjectToViewModel).ToList()); + + return new PagedLogEntriesViewModel(count, page); } -} +} \ No newline at end of file diff --git a/ErsatzTV.Application/Maintenance/Commands/DeleteItemsFromDatabase.cs b/ErsatzTV.Application/Maintenance/Commands/DeleteItemsFromDatabase.cs index 953c612f1..855dbb763 100644 --- a/ErsatzTV.Application/Maintenance/Commands/DeleteItemsFromDatabase.cs +++ b/ErsatzTV.Application/Maintenance/Commands/DeleteItemsFromDatabase.cs @@ -1,9 +1,5 @@ -using System.Collections.Generic; -using ErsatzTV.Core; -using LanguageExt; -using MediatR; -using Unit = LanguageExt.Unit; +using ErsatzTV.Core; -namespace ErsatzTV.Application.Maintenance.Commands; +namespace ErsatzTV.Application.Maintenance; public record DeleteItemsFromDatabase(List MediaItemIds) : IRequest>; diff --git a/ErsatzTV.Application/Maintenance/Commands/DeleteItemsFromDatabaseHandler.cs b/ErsatzTV.Application/Maintenance/Commands/DeleteItemsFromDatabaseHandler.cs index 47d2dbc83..dc2bec2b3 100644 --- a/ErsatzTV.Application/Maintenance/Commands/DeleteItemsFromDatabaseHandler.cs +++ b/ErsatzTV.Application/Maintenance/Commands/DeleteItemsFromDatabaseHandler.cs @@ -1,38 +1,34 @@ -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Search; -using LanguageExt; -namespace ErsatzTV.Application.Maintenance.Commands +namespace ErsatzTV.Application.Maintenance; + +public class + DeleteItemsFromDatabaseHandler : MediatR.IRequestHandler> { - public class - DeleteItemsFromDatabaseHandler : MediatR.IRequestHandler> + private readonly IMediaItemRepository _mediaItemRepository; + private readonly ISearchIndex _searchIndex; + + public DeleteItemsFromDatabaseHandler( + IMediaItemRepository mediaItemRepository, + ISearchIndex searchIndex) { - private readonly IMediaItemRepository _mediaItemRepository; - private readonly ISearchIndex _searchIndex; - - public DeleteItemsFromDatabaseHandler( - IMediaItemRepository mediaItemRepository, - ISearchIndex searchIndex) - { - _mediaItemRepository = mediaItemRepository; - _searchIndex = searchIndex; - } - - public async Task> Handle( - DeleteItemsFromDatabase request, - CancellationToken cancellationToken) - { - Either deleteResult = await _mediaItemRepository.DeleteItems(request.MediaItemIds); - if (deleteResult.IsRight) - { - await _searchIndex.RemoveItems(request.MediaItemIds); - _searchIndex.Commit(); - } - - return deleteResult; - } + _mediaItemRepository = mediaItemRepository; + _searchIndex = searchIndex; } -} + + public async Task> Handle( + DeleteItemsFromDatabase request, + CancellationToken cancellationToken) + { + Either deleteResult = await _mediaItemRepository.DeleteItems(request.MediaItemIds); + if (deleteResult.IsRight) + { + await _searchIndex.RemoveItems(request.MediaItemIds); + _searchIndex.Commit(); + } + + return deleteResult; + } +} \ No newline at end of file diff --git a/ErsatzTV.Application/Maintenance/Commands/DeleteOrphanedArtwork.cs b/ErsatzTV.Application/Maintenance/Commands/DeleteOrphanedArtwork.cs index 8455fb8aa..9ab46be81 100644 --- a/ErsatzTV.Application/Maintenance/Commands/DeleteOrphanedArtwork.cs +++ b/ErsatzTV.Application/Maintenance/Commands/DeleteOrphanedArtwork.cs @@ -1,7 +1,5 @@ using ErsatzTV.Core; -using LanguageExt; -namespace ErsatzTV.Application.Maintenance.Commands -{ - public record DeleteOrphanedArtwork : MediatR.IRequest>, IBackgroundServiceRequest; -} +namespace ErsatzTV.Application.Maintenance; + +public record DeleteOrphanedArtwork : MediatR.IRequest>, IBackgroundServiceRequest; \ No newline at end of file diff --git a/ErsatzTV.Application/Maintenance/Commands/DeleteOrphanedArtworkHandler.cs b/ErsatzTV.Application/Maintenance/Commands/DeleteOrphanedArtworkHandler.cs index 5cf2e26c2..41222782e 100644 --- a/ErsatzTV.Application/Maintenance/Commands/DeleteOrphanedArtworkHandler.cs +++ b/ErsatzTV.Application/Maintenance/Commands/DeleteOrphanedArtworkHandler.cs @@ -1,23 +1,18 @@ -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -using static LanguageExt.Prelude; -namespace ErsatzTV.Application.Maintenance.Commands +namespace ErsatzTV.Application.Maintenance; + +public class DeleteOrphanedArtworkHandler : MediatR.IRequestHandler> { - public class DeleteOrphanedArtworkHandler : MediatR.IRequestHandler> - { - private readonly IArtworkRepository _artworkRepository; + private readonly IArtworkRepository _artworkRepository; - public DeleteOrphanedArtworkHandler(IArtworkRepository artworkRepository) => - _artworkRepository = artworkRepository; + public DeleteOrphanedArtworkHandler(IArtworkRepository artworkRepository) => + _artworkRepository = artworkRepository; - public Task> - Handle(DeleteOrphanedArtwork request, CancellationToken cancellationToken) => - _artworkRepository.GetOrphanedArtwork() - .Bind(_artworkRepository.Delete) - .Map(_ => Right(Unit.Default)); - } -} + public Task> + Handle(DeleteOrphanedArtwork request, CancellationToken cancellationToken) => + _artworkRepository.GetOrphanedArtwork() + .Bind(_artworkRepository.Delete) + .Map(_ => Right(Unit.Default)); +} \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCards/ActorCardViewModel.cs b/ErsatzTV.Application/MediaCards/ActorCardViewModel.cs index cb55ba658..f6cce92ca 100644 --- a/ErsatzTV.Application/MediaCards/ActorCardViewModel.cs +++ b/ErsatzTV.Application/MediaCards/ActorCardViewModel.cs @@ -1,7 +1,6 @@ using ErsatzTV.Core.Domain; -namespace ErsatzTV.Application.MediaCards -{ - public record ActorCardViewModel(int Id, string Name, string Role, string Thumb, MediaItemState State) : - MediaCardViewModel(Id, Name, Role, Name, Thumb, State); -} +namespace ErsatzTV.Application.MediaCards; + +public record ActorCardViewModel(int Id, string Name, string Role, string Thumb, MediaItemState State) : + MediaCardViewModel(Id, Name, Role, Name, Thumb, State); \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCards/ArtistCardResultsViewModel.cs b/ErsatzTV.Application/MediaCards/ArtistCardResultsViewModel.cs index cc88a7bec..dc2522cb2 100644 --- a/ErsatzTV.Application/MediaCards/ArtistCardResultsViewModel.cs +++ b/ErsatzTV.Application/MediaCards/ArtistCardResultsViewModel.cs @@ -1,11 +1,8 @@ -using System.Collections.Generic; -using ErsatzTV.Core.Search; -using LanguageExt; +using ErsatzTV.Core.Search; -namespace ErsatzTV.Application.MediaCards -{ - public record ArtistCardResultsViewModel( - int Count, - List Cards, - Option PageMap); -} +namespace ErsatzTV.Application.MediaCards; + +public record ArtistCardResultsViewModel( + int Count, + List Cards, + Option PageMap); \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCards/ArtistCardViewModel.cs b/ErsatzTV.Application/MediaCards/ArtistCardViewModel.cs index 2dbef7bb8..74cc8b99f 100644 --- a/ErsatzTV.Application/MediaCards/ArtistCardViewModel.cs +++ b/ErsatzTV.Application/MediaCards/ArtistCardViewModel.cs @@ -1,19 +1,18 @@ using ErsatzTV.Core.Domain; -namespace ErsatzTV.Application.MediaCards -{ - public record ArtistCardViewModel - ( - int ArtistId, - string Title, - string Subtitle, - string SortTitle, - string Poster, - MediaItemState State) : MediaCardViewModel( - ArtistId, - Title, - Subtitle, - SortTitle, - Poster, - State); -} +namespace ErsatzTV.Application.MediaCards; + +public record ArtistCardViewModel +( + int ArtistId, + string Title, + string Subtitle, + string SortTitle, + string Poster, + MediaItemState State) : MediaCardViewModel( + ArtistId, + Title, + Subtitle, + SortTitle, + Poster, + State); \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCards/CollectionCardResultsViewModel.cs b/ErsatzTV.Application/MediaCards/CollectionCardResultsViewModel.cs index 820d167bc..ac9737603 100644 --- a/ErsatzTV.Application/MediaCards/CollectionCardResultsViewModel.cs +++ b/ErsatzTV.Application/MediaCards/CollectionCardResultsViewModel.cs @@ -1,18 +1,15 @@ -using System.Collections.Generic; +namespace ErsatzTV.Application.MediaCards; -namespace ErsatzTV.Application.MediaCards +public record CollectionCardResultsViewModel( + string Name, + List MovieCards, + List ShowCards, + List SeasonCards, + List EpisodeCards, + List ArtistCards, + List MusicVideoCards, + List OtherVideoCards, + List SongCards) { - public record CollectionCardResultsViewModel( - string Name, - List MovieCards, - List ShowCards, - List SeasonCards, - List EpisodeCards, - List ArtistCards, - List MusicVideoCards, - List OtherVideoCards, - List SongCards) - { - public bool UseCustomPlaybackOrder { get; set; } - } -} + public bool UseCustomPlaybackOrder { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCards/Mapper.cs b/ErsatzTV.Application/MediaCards/Mapper.cs index 305bf155b..1f4f262c4 100644 --- a/ErsatzTV.Application/MediaCards/Mapper.cs +++ b/ErsatzTV.Application/MediaCards/Mapper.cs @@ -1,263 +1,259 @@ -using System.Linq; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Emby; using ErsatzTV.Core.Extensions; using ErsatzTV.Core.Jellyfin; -using LanguageExt; -using static LanguageExt.Prelude; -namespace ErsatzTV.Application.MediaCards +namespace ErsatzTV.Application.MediaCards; + +internal static class Mapper { - internal static class Mapper - { - internal static TelevisionShowCardViewModel ProjectToViewModel( - ShowMetadata showMetadata, - Option maybeJellyfin, - Option maybeEmby) => - new( - showMetadata.ShowId, - showMetadata.Title, - showMetadata.Year?.ToString(), - showMetadata.SortTitle, - GetPoster(showMetadata, maybeJellyfin, maybeEmby), - showMetadata.Show.State); + internal static TelevisionShowCardViewModel ProjectToViewModel( + ShowMetadata showMetadata, + Option maybeJellyfin, + Option maybeEmby) => + new( + showMetadata.ShowId, + showMetadata.Title, + showMetadata.Year?.ToString(), + showMetadata.SortTitle, + GetPoster(showMetadata, maybeJellyfin, maybeEmby), + showMetadata.Show.State); - internal static TelevisionSeasonCardViewModel ProjectToViewModel( - Season season, - Option maybeJellyfin, - Option maybeEmby) => - new( - season.Show.ShowMetadata.HeadOrNone().Match(m => m.Title ?? string.Empty, () => string.Empty), - season.Id, - season.SeasonNumber, - GetSeasonName(season.SeasonNumber), - string.Empty, - GetSeasonName(season.SeasonNumber), - season.SeasonMetadata.HeadOrNone().Map(sm => GetPoster(sm, maybeJellyfin, maybeEmby)) - .IfNone(string.Empty), - season.SeasonNumber == 0 ? "S" : season.SeasonNumber.ToString(), - season.State); + internal static TelevisionSeasonCardViewModel ProjectToViewModel( + Season season, + Option maybeJellyfin, + Option maybeEmby) => + new( + season.Show.ShowMetadata.HeadOrNone().Match(m => m.Title ?? string.Empty, () => string.Empty), + season.Id, + season.SeasonNumber, + GetSeasonName(season.SeasonNumber), + string.Empty, + GetSeasonName(season.SeasonNumber), + season.SeasonMetadata.HeadOrNone().Map(sm => GetPoster(sm, maybeJellyfin, maybeEmby)) + .IfNone(string.Empty), + season.SeasonNumber == 0 ? "S" : season.SeasonNumber.ToString(), + season.State); - internal static TelevisionSeasonCardViewModel ProjectToViewModel( - SeasonMetadata seasonMetadata, - Option maybeJellyfin, - Option maybeEmby) - { - string showTitle = seasonMetadata.Season.Show.ShowMetadata.HeadOrNone().Match( - m => m.Title ?? string.Empty, - () => string.Empty); + internal static TelevisionSeasonCardViewModel ProjectToViewModel( + SeasonMetadata seasonMetadata, + Option maybeJellyfin, + Option maybeEmby) + { + string showTitle = seasonMetadata.Season.Show.ShowMetadata.HeadOrNone().Match( + m => m.Title ?? string.Empty, + () => string.Empty); - return new TelevisionSeasonCardViewModel( - showTitle, - seasonMetadata.SeasonId, - seasonMetadata.Season.SeasonNumber, - showTitle, - GetSeasonName(seasonMetadata.Season.SeasonNumber), - $"{showTitle}_{seasonMetadata.Season.SeasonNumber:0000}", - GetPoster(seasonMetadata, maybeJellyfin, maybeEmby), - seasonMetadata.Season.SeasonNumber == 0 ? "S" : seasonMetadata.Season.SeasonNumber.ToString(), - seasonMetadata.Season.State); - } - - internal static TelevisionEpisodeCardViewModel ProjectToViewModel( - EpisodeMetadata episodeMetadata, - Option maybeJellyfin, - Option maybeEmby, - bool isSearchResult) => - new( - episodeMetadata.EpisodeId, - episodeMetadata.ReleaseDate ?? SystemTime.MinValueUtc, - episodeMetadata.Episode.Season.Show.ShowMetadata.HeadOrNone().Match( - m => m.Title ?? string.Empty, - () => string.Empty), - episodeMetadata.Episode.Season.ShowId, - episodeMetadata.Episode.SeasonId, - episodeMetadata.Episode.Season.SeasonNumber, - episodeMetadata.Episode.EpisodeMetadata.HeadOrNone().Match(em => em.EpisodeNumber, () => 0), - episodeMetadata.Title, - episodeMetadata.SortTitle, - episodeMetadata.Episode.EpisodeMetadata.HeadOrNone().Match( - em => em.Plot ?? string.Empty, - () => string.Empty), - isSearchResult - ? GetEpisodePoster(episodeMetadata, maybeJellyfin, maybeEmby) - : GetThumbnail(episodeMetadata, maybeJellyfin, maybeEmby), - episodeMetadata.Directors.Map(d => d.Name).ToList(), - episodeMetadata.Writers.Map(w => w.Name).ToList(), - episodeMetadata.Episode.State, - episodeMetadata.Episode.GetHeadVersion().MediaFiles.Head().Path); - - internal static MovieCardViewModel ProjectToViewModel( - MovieMetadata movieMetadata, - Option maybeJellyfin, - Option maybeEmby) => - new( - movieMetadata.MovieId, - movieMetadata.Title, - movieMetadata.Year?.ToString(), - movieMetadata.SortTitle, - GetPoster(movieMetadata, maybeJellyfin, maybeEmby), - movieMetadata.Movie.State); - - internal static MusicVideoCardViewModel ProjectToViewModel(MusicVideoMetadata musicVideoMetadata) => - new( - musicVideoMetadata.MusicVideoId, - musicVideoMetadata.Title, - musicVideoMetadata.MusicVideo.Artist.ArtistMetadata.Head().Title, - musicVideoMetadata.SortTitle, - musicVideoMetadata.Plot, - musicVideoMetadata.Album, - GetThumbnail(musicVideoMetadata, None, None), - musicVideoMetadata.MusicVideo.State, - musicVideoMetadata.MusicVideo.GetHeadVersion().MediaFiles.Head().Path); - - internal static OtherVideoCardViewModel ProjectToViewModel(OtherVideoMetadata otherVideoMetadata) => - new( - otherVideoMetadata.OtherVideoId, - otherVideoMetadata.Title, - otherVideoMetadata.OriginalTitle, - otherVideoMetadata.SortTitle, - otherVideoMetadata.OtherVideo.State); - - internal static SongCardViewModel ProjectToViewModel(SongMetadata songMetadata) - { - string album = string.IsNullOrWhiteSpace(songMetadata.Album) ? "" : $" - {songMetadata.Album}"; - return new SongCardViewModel( - songMetadata.SongId, - songMetadata.Title, - songMetadata.Artist + album, - songMetadata.SortTitle, - GetThumbnail(songMetadata, None, None), - songMetadata.Song.State); - } - - internal static ArtistCardViewModel ProjectToViewModel(ArtistMetadata artistMetadata) => - new( - artistMetadata.ArtistId, - artistMetadata.Title, - artistMetadata.Disambiguation, - artistMetadata.SortTitle, - GetThumbnail(artistMetadata, None, None), - artistMetadata.Artist.State); - - internal static CollectionCardResultsViewModel - ProjectToViewModel( - Collection collection, - Option maybeJellyfin, - Option maybeEmby) => - new( - collection.Name, - collection.MediaItems.OfType().Map( - m => ProjectToViewModel(m.MovieMetadata.Head(), maybeJellyfin, maybeEmby) with - { - CustomIndex = GetCustomIndex(collection, m.Id) - }).ToList(), - collection.MediaItems.OfType() - .Map(s => ProjectToViewModel(s.ShowMetadata.Head(), maybeJellyfin, maybeEmby)) - .ToList(), - collection.MediaItems.OfType().Map(s => ProjectToViewModel(s, maybeJellyfin, maybeEmby)) - .ToList(), - collection.MediaItems.OfType() - .Map(e => ProjectToViewModel(e.EpisodeMetadata.Head(), maybeJellyfin, maybeEmby, false)) - .ToList(), - collection.MediaItems.OfType().Map(a => ProjectToViewModel(a.ArtistMetadata.Head())).ToList(), - collection.MediaItems.OfType().Map(mv => ProjectToViewModel(mv.MusicVideoMetadata.Head())) - .ToList(), - collection.MediaItems.OfType().Map(ov => ProjectToViewModel(ov.OtherVideoMetadata.Head())) - .ToList(), - collection.MediaItems.OfType().Map(s => ProjectToViewModel(s.SongMetadata.Head())) - .ToList()) { UseCustomPlaybackOrder = collection.UseCustomPlaybackOrder }; - - internal static ActorCardViewModel ProjectToViewModel( - Actor actor, - Option maybeJellyfin, - Option maybeEmby) - { - string artwork = actor.Artwork?.Path ?? string.Empty; - - if (maybeJellyfin.IsSome && artwork.StartsWith("jellyfin://")) - { - artwork = JellyfinUrl.RelativeProxyForArtwork(artwork) - .SetQueryParam("fillHeight", 440); - } - else if (maybeEmby.IsSome && artwork.StartsWith("emby://")) - { - artwork = EmbyUrl.RelativeProxyForArtwork(artwork) - .SetQueryParam("maxHeight", 440); - } - - return new ActorCardViewModel(actor.Id, actor.Name, actor.Role, artwork, MediaItemState.Normal); - } - - private static int GetCustomIndex(Collection collection, int mediaItemId) => - Optional(collection.CollectionItems.Find(ci => ci.MediaItemId == mediaItemId)) - .Map(ci => ci.CustomIndex ?? 0) - .IfNone(0); - - private static string GetSeasonName(int number) => - number == 0 ? "Specials" : $"Season {number}"; - - private static string GetEpisodePoster( - EpisodeMetadata episodeMetadata, - Option maybeJellyfin, - Option maybeEmby) - { - Option maybeSeasonMetadata = episodeMetadata.Episode.Season.SeasonMetadata.HeadOrNone(); - return maybeSeasonMetadata.Match( - seasonMetadata => GetPoster(seasonMetadata, maybeJellyfin, maybeEmby), - () => - { - Option maybeShowMetadata = - episodeMetadata.Episode.Season.Show.ShowMetadata.HeadOrNone(); - return maybeShowMetadata.Match( - showMetadata => GetPoster(showMetadata, maybeJellyfin, maybeEmby), - () => string.Empty); - }); - } - - private static string GetPoster( - Metadata metadata, - Option maybeJellyfin, - Option maybeEmby) - { - string poster = Optional(metadata.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Poster)) - .Match(a => a.Path, string.Empty); - - if (maybeJellyfin.IsSome && poster.StartsWith("jellyfin://")) - { - poster = JellyfinUrl.RelativeProxyForArtwork(poster) - .SetQueryParam("fillHeight", 440); - } - else if (maybeEmby.IsSome && poster.StartsWith("emby://")) - { - poster = EmbyUrl.RelativeProxyForArtwork(poster) - .SetQueryParam("maxHeight", 440); - } - - return poster; - } - - private static string GetThumbnail( - Metadata metadata, - Option maybeJellyfin, - Option maybeEmby) - { - string thumb = Optional(metadata.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Thumbnail)) - .Match(a => a.Path, string.Empty); - - if (maybeJellyfin.IsSome && thumb.StartsWith("jellyfin://")) - { - thumb = JellyfinUrl.RelativeProxyForArtwork(thumb) - .SetQueryParam("fillHeight", 220); - } - else if (maybeEmby.IsSome && thumb.StartsWith("emby://")) - { - thumb = EmbyUrl.RelativeProxyForArtwork(thumb) - .SetQueryParam("maxHeight", 220); - } - - return thumb; - } + return new TelevisionSeasonCardViewModel( + showTitle, + seasonMetadata.SeasonId, + seasonMetadata.Season.SeasonNumber, + showTitle, + GetSeasonName(seasonMetadata.Season.SeasonNumber), + $"{showTitle}_{seasonMetadata.Season.SeasonNumber:0000}", + GetPoster(seasonMetadata, maybeJellyfin, maybeEmby), + seasonMetadata.Season.SeasonNumber == 0 ? "S" : seasonMetadata.Season.SeasonNumber.ToString(), + seasonMetadata.Season.State); } -} + + internal static TelevisionEpisodeCardViewModel ProjectToViewModel( + EpisodeMetadata episodeMetadata, + Option maybeJellyfin, + Option maybeEmby, + bool isSearchResult) => + new( + episodeMetadata.EpisodeId, + episodeMetadata.ReleaseDate ?? SystemTime.MinValueUtc, + episodeMetadata.Episode.Season.Show.ShowMetadata.HeadOrNone().Match( + m => m.Title ?? string.Empty, + () => string.Empty), + episodeMetadata.Episode.Season.ShowId, + episodeMetadata.Episode.SeasonId, + episodeMetadata.Episode.Season.SeasonNumber, + episodeMetadata.Episode.EpisodeMetadata.HeadOrNone().Match(em => em.EpisodeNumber, () => 0), + episodeMetadata.Title, + episodeMetadata.SortTitle, + episodeMetadata.Episode.EpisodeMetadata.HeadOrNone().Match( + em => em.Plot ?? string.Empty, + () => string.Empty), + isSearchResult + ? GetEpisodePoster(episodeMetadata, maybeJellyfin, maybeEmby) + : GetThumbnail(episodeMetadata, maybeJellyfin, maybeEmby), + episodeMetadata.Directors.Map(d => d.Name).ToList(), + episodeMetadata.Writers.Map(w => w.Name).ToList(), + episodeMetadata.Episode.State, + episodeMetadata.Episode.GetHeadVersion().MediaFiles.Head().Path); + + internal static MovieCardViewModel ProjectToViewModel( + MovieMetadata movieMetadata, + Option maybeJellyfin, + Option maybeEmby) => + new( + movieMetadata.MovieId, + movieMetadata.Title, + movieMetadata.Year?.ToString(), + movieMetadata.SortTitle, + GetPoster(movieMetadata, maybeJellyfin, maybeEmby), + movieMetadata.Movie.State); + + internal static MusicVideoCardViewModel ProjectToViewModel(MusicVideoMetadata musicVideoMetadata) => + new( + musicVideoMetadata.MusicVideoId, + musicVideoMetadata.Title, + musicVideoMetadata.MusicVideo.Artist.ArtistMetadata.Head().Title, + musicVideoMetadata.SortTitle, + musicVideoMetadata.Plot, + musicVideoMetadata.Album, + GetThumbnail(musicVideoMetadata, None, None), + musicVideoMetadata.MusicVideo.State, + musicVideoMetadata.MusicVideo.GetHeadVersion().MediaFiles.Head().Path); + + internal static OtherVideoCardViewModel ProjectToViewModel(OtherVideoMetadata otherVideoMetadata) => + new( + otherVideoMetadata.OtherVideoId, + otherVideoMetadata.Title, + otherVideoMetadata.OriginalTitle, + otherVideoMetadata.SortTitle, + otherVideoMetadata.OtherVideo.State); + + internal static SongCardViewModel ProjectToViewModel(SongMetadata songMetadata) + { + string album = string.IsNullOrWhiteSpace(songMetadata.Album) ? "" : $" - {songMetadata.Album}"; + return new SongCardViewModel( + songMetadata.SongId, + songMetadata.Title, + songMetadata.Artist + album, + songMetadata.SortTitle, + GetThumbnail(songMetadata, None, None), + songMetadata.Song.State); + } + + internal static ArtistCardViewModel ProjectToViewModel(ArtistMetadata artistMetadata) => + new( + artistMetadata.ArtistId, + artistMetadata.Title, + artistMetadata.Disambiguation, + artistMetadata.SortTitle, + GetThumbnail(artistMetadata, None, None), + artistMetadata.Artist.State); + + internal static CollectionCardResultsViewModel + ProjectToViewModel( + Collection collection, + Option maybeJellyfin, + Option maybeEmby) => + new( + collection.Name, + collection.MediaItems.OfType().Map( + m => ProjectToViewModel(m.MovieMetadata.Head(), maybeJellyfin, maybeEmby) with + { + CustomIndex = GetCustomIndex(collection, m.Id) + }).ToList(), + collection.MediaItems.OfType() + .Map(s => ProjectToViewModel(s.ShowMetadata.Head(), maybeJellyfin, maybeEmby)) + .ToList(), + collection.MediaItems.OfType().Map(s => ProjectToViewModel(s, maybeJellyfin, maybeEmby)) + .ToList(), + collection.MediaItems.OfType() + .Map(e => ProjectToViewModel(e.EpisodeMetadata.Head(), maybeJellyfin, maybeEmby, false)) + .ToList(), + collection.MediaItems.OfType().Map(a => ProjectToViewModel(a.ArtistMetadata.Head())).ToList(), + collection.MediaItems.OfType().Map(mv => ProjectToViewModel(mv.MusicVideoMetadata.Head())) + .ToList(), + collection.MediaItems.OfType().Map(ov => ProjectToViewModel(ov.OtherVideoMetadata.Head())) + .ToList(), + collection.MediaItems.OfType().Map(s => ProjectToViewModel(s.SongMetadata.Head())) + .ToList()) { UseCustomPlaybackOrder = collection.UseCustomPlaybackOrder }; + + internal static ActorCardViewModel ProjectToViewModel( + Actor actor, + Option maybeJellyfin, + Option maybeEmby) + { + string artwork = actor.Artwork?.Path ?? string.Empty; + + if (maybeJellyfin.IsSome && artwork.StartsWith("jellyfin://")) + { + artwork = JellyfinUrl.RelativeProxyForArtwork(artwork) + .SetQueryParam("fillHeight", 440); + } + else if (maybeEmby.IsSome && artwork.StartsWith("emby://")) + { + artwork = EmbyUrl.RelativeProxyForArtwork(artwork) + .SetQueryParam("maxHeight", 440); + } + + return new ActorCardViewModel(actor.Id, actor.Name, actor.Role, artwork, MediaItemState.Normal); + } + + private static int GetCustomIndex(Collection collection, int mediaItemId) => + Optional(collection.CollectionItems.Find(ci => ci.MediaItemId == mediaItemId)) + .Map(ci => ci.CustomIndex ?? 0) + .IfNone(0); + + private static string GetSeasonName(int number) => + number == 0 ? "Specials" : $"Season {number}"; + + private static string GetEpisodePoster( + EpisodeMetadata episodeMetadata, + Option maybeJellyfin, + Option maybeEmby) + { + Option maybeSeasonMetadata = episodeMetadata.Episode.Season.SeasonMetadata.HeadOrNone(); + return maybeSeasonMetadata.Match( + seasonMetadata => GetPoster(seasonMetadata, maybeJellyfin, maybeEmby), + () => + { + Option maybeShowMetadata = + episodeMetadata.Episode.Season.Show.ShowMetadata.HeadOrNone(); + return maybeShowMetadata.Match( + showMetadata => GetPoster(showMetadata, maybeJellyfin, maybeEmby), + () => string.Empty); + }); + } + + private static string GetPoster( + Metadata metadata, + Option maybeJellyfin, + Option maybeEmby) + { + string poster = Optional(metadata.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Poster)) + .Match(a => a.Path, string.Empty); + + if (maybeJellyfin.IsSome && poster.StartsWith("jellyfin://")) + { + poster = JellyfinUrl.RelativeProxyForArtwork(poster) + .SetQueryParam("fillHeight", 440); + } + else if (maybeEmby.IsSome && poster.StartsWith("emby://")) + { + poster = EmbyUrl.RelativeProxyForArtwork(poster) + .SetQueryParam("maxHeight", 440); + } + + return poster; + } + + private static string GetThumbnail( + Metadata metadata, + Option maybeJellyfin, + Option maybeEmby) + { + string thumb = Optional(metadata.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Thumbnail)) + .Match(a => a.Path, string.Empty); + + if (maybeJellyfin.IsSome && thumb.StartsWith("jellyfin://")) + { + thumb = JellyfinUrl.RelativeProxyForArtwork(thumb) + .SetQueryParam("fillHeight", 220); + } + else if (maybeEmby.IsSome && thumb.StartsWith("emby://")) + { + thumb = EmbyUrl.RelativeProxyForArtwork(thumb) + .SetQueryParam("maxHeight", 220); + } + + return thumb; + } +} \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCards/MediaCardViewModel.cs b/ErsatzTV.Application/MediaCards/MediaCardViewModel.cs index 1f9112318..db68dd45b 100644 --- a/ErsatzTV.Application/MediaCards/MediaCardViewModel.cs +++ b/ErsatzTV.Application/MediaCards/MediaCardViewModel.cs @@ -1,12 +1,11 @@ using ErsatzTV.Core.Domain; -namespace ErsatzTV.Application.MediaCards -{ - public record MediaCardViewModel( - int MediaItemId, - string Title, - string Subtitle, - string SortTitle, - string Poster, - MediaItemState State); -} +namespace ErsatzTV.Application.MediaCards; + +public record MediaCardViewModel( + int MediaItemId, + string Title, + string Subtitle, + string SortTitle, + string Poster, + MediaItemState State); \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCards/MovieCardResultsViewModel.cs b/ErsatzTV.Application/MediaCards/MovieCardResultsViewModel.cs index 6f07386f2..e2906452c 100644 --- a/ErsatzTV.Application/MediaCards/MovieCardResultsViewModel.cs +++ b/ErsatzTV.Application/MediaCards/MovieCardResultsViewModel.cs @@ -1,8 +1,5 @@ -using System.Collections.Generic; -using ErsatzTV.Core.Search; -using LanguageExt; +using ErsatzTV.Core.Search; -namespace ErsatzTV.Application.MediaCards -{ - public record MovieCardResultsViewModel(int Count, List Cards, Option PageMap); -} +namespace ErsatzTV.Application.MediaCards; + +public record MovieCardResultsViewModel(int Count, List Cards, Option PageMap); \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCards/MovieCardViewModel.cs b/ErsatzTV.Application/MediaCards/MovieCardViewModel.cs index 47641b8c8..71e4e9690 100644 --- a/ErsatzTV.Application/MediaCards/MovieCardViewModel.cs +++ b/ErsatzTV.Application/MediaCards/MovieCardViewModel.cs @@ -1,22 +1,21 @@ using ErsatzTV.Core.Domain; -namespace ErsatzTV.Application.MediaCards +namespace ErsatzTV.Application.MediaCards; + +public record MovieCardViewModel +( + int MovieId, + string Title, + string Subtitle, + string SortTitle, + string Poster, + MediaItemState State) : MediaCardViewModel( + MovieId, + Title, + Subtitle, + SortTitle, + Poster, + State) { - public record MovieCardViewModel - ( - int MovieId, - string Title, - string Subtitle, - string SortTitle, - string Poster, - MediaItemState State) : MediaCardViewModel( - MovieId, - Title, - Subtitle, - SortTitle, - Poster, - State) - { - public int CustomIndex { get; set; } - } -} + public int CustomIndex { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCards/MusicVideoCardResultsViewModel.cs b/ErsatzTV.Application/MediaCards/MusicVideoCardResultsViewModel.cs index ff0816da9..4d11cfac8 100644 --- a/ErsatzTV.Application/MediaCards/MusicVideoCardResultsViewModel.cs +++ b/ErsatzTV.Application/MediaCards/MusicVideoCardResultsViewModel.cs @@ -1,11 +1,8 @@ -using System.Collections.Generic; -using ErsatzTV.Core.Search; -using LanguageExt; +using ErsatzTV.Core.Search; -namespace ErsatzTV.Application.MediaCards -{ - public record MusicVideoCardResultsViewModel( - int Count, - List Cards, - Option PageMap); -} +namespace ErsatzTV.Application.MediaCards; + +public record MusicVideoCardResultsViewModel( + int Count, + List Cards, + Option PageMap); \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCards/MusicVideoCardViewModel.cs b/ErsatzTV.Application/MediaCards/MusicVideoCardViewModel.cs index c7aa360eb..ebe4824fa 100644 --- a/ErsatzTV.Application/MediaCards/MusicVideoCardViewModel.cs +++ b/ErsatzTV.Application/MediaCards/MusicVideoCardViewModel.cs @@ -1,25 +1,24 @@ using ErsatzTV.Core.Domain; -namespace ErsatzTV.Application.MediaCards +namespace ErsatzTV.Application.MediaCards; + +public record MusicVideoCardViewModel +( + int MusicVideoId, + string Title, + string Subtitle, + string SortTitle, + string Plot, + string Album, + string Poster, + MediaItemState State, + string Path) : MediaCardViewModel( + MusicVideoId, + Title, + Subtitle, + SortTitle, + Poster, + State) { - public record MusicVideoCardViewModel - ( - int MusicVideoId, - string Title, - string Subtitle, - string SortTitle, - string Plot, - string Album, - string Poster, - MediaItemState State, - string Path) : MediaCardViewModel( - MusicVideoId, - Title, - Subtitle, - SortTitle, - Poster, - State) - { - public int CustomIndex { get; set; } - } -} + public int CustomIndex { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCards/OtherVideoCardResultsViewModel.cs b/ErsatzTV.Application/MediaCards/OtherVideoCardResultsViewModel.cs index b48437df2..45b9585ef 100644 --- a/ErsatzTV.Application/MediaCards/OtherVideoCardResultsViewModel.cs +++ b/ErsatzTV.Application/MediaCards/OtherVideoCardResultsViewModel.cs @@ -1,11 +1,8 @@ -using System.Collections.Generic; -using ErsatzTV.Core.Search; -using LanguageExt; +using ErsatzTV.Core.Search; -namespace ErsatzTV.Application.MediaCards -{ - public record OtherVideoCardResultsViewModel( - int Count, - List Cards, - Option PageMap); -} +namespace ErsatzTV.Application.MediaCards; + +public record OtherVideoCardResultsViewModel( + int Count, + List Cards, + Option PageMap); \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCards/OtherVideoCardViewModel.cs b/ErsatzTV.Application/MediaCards/OtherVideoCardViewModel.cs index d75cc863b..a4092658e 100644 --- a/ErsatzTV.Application/MediaCards/OtherVideoCardViewModel.cs +++ b/ErsatzTV.Application/MediaCards/OtherVideoCardViewModel.cs @@ -1,21 +1,20 @@ using ErsatzTV.Core.Domain; -namespace ErsatzTV.Application.MediaCards +namespace ErsatzTV.Application.MediaCards; + +public record OtherVideoCardViewModel +( + int OtherVideoId, + string Title, + string Subtitle, + string SortTitle, + MediaItemState State) : MediaCardViewModel( + OtherVideoId, + Title, + Subtitle, + SortTitle, + null, + State) { - public record OtherVideoCardViewModel - ( - int OtherVideoId, - string Title, - string Subtitle, - string SortTitle, - MediaItemState State) : MediaCardViewModel( - OtherVideoId, - Title, - Subtitle, - SortTitle, - null, - State) - { - public int CustomIndex { get; set; } - } -} + public int CustomIndex { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCards/Queries/GetCollectionCards.cs b/ErsatzTV.Application/MediaCards/Queries/GetCollectionCards.cs index 210276295..bf3b80d58 100644 --- a/ErsatzTV.Application/MediaCards/Queries/GetCollectionCards.cs +++ b/ErsatzTV.Application/MediaCards/Queries/GetCollectionCards.cs @@ -1,8 +1,5 @@ using ErsatzTV.Core; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.MediaCards.Queries -{ - public record GetCollectionCards(int Id) : IRequest>; -} +namespace ErsatzTV.Application.MediaCards; + +public record GetCollectionCards(int Id) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCards/Queries/GetCollectionCardsHandler.cs b/ErsatzTV.Application/MediaCards/Queries/GetCollectionCardsHandler.cs index 8a118cc30..215212a4a 100644 --- a/ErsatzTV.Application/MediaCards/Queries/GetCollectionCardsHandler.cs +++ b/ErsatzTV.Application/MediaCards/Queries/GetCollectionCardsHandler.cs @@ -1,109 +1,104 @@ -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +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 +namespace ErsatzTV.Application.MediaCards; + +public class GetCollectionCardsHandler : + IRequestHandler> { - public class GetCollectionCardsHandler : - IRequestHandler> + private readonly IDbContextFactory _dbContextFactory; + private readonly IMediaSourceRepository _mediaSourceRepository; + + public GetCollectionCardsHandler( + IDbContextFactory dbContextFactory, + IMediaSourceRepository mediaSourceRepository) { - private readonly IDbContextFactory _dbContextFactory; - private readonly IMediaSourceRepository _mediaSourceRepository; - - public GetCollectionCardsHandler( - IDbContextFactory dbContextFactory, - IMediaSourceRepository mediaSourceRepository) - { - _dbContextFactory = dbContextFactory; - _mediaSourceRepository = mediaSourceRepository; - } - - public async Task> Handle( - GetCollectionCards request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - - Option maybeJellyfin = await _mediaSourceRepository.GetAllJellyfin() - .Map(list => list.HeadOrNone()); - - Option maybeEmby = await _mediaSourceRepository.GetAllEmby() - .Map(list => list.HeadOrNone()); - - 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 Movie).MediaVersions) - .ThenInclude(mv => mv.MediaFiles) - .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 MusicVideo).MediaVersions) - .ThenInclude(mv => mv.MediaFiles) - .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) - .Include(c => c.MediaItems) - .ThenInclude(i => (i as Episode).MediaVersions) - .ThenInclude(mv => mv.MediaFiles) - .Include(c => c.MediaItems) - .ThenInclude(i => (i as OtherVideo).OtherVideoMetadata) - .ThenInclude(ovm => ovm.Artwork) - .Include(c => c.MediaItems) - .ThenInclude(i => (i as OtherVideo).MediaVersions) - .ThenInclude(mv => mv.MediaFiles) - .Include(c => c.MediaItems) - .ThenInclude(i => (i as Song).SongMetadata) - .ThenInclude(ovm => ovm.Artwork) - .Include(c => c.MediaItems) - .ThenInclude(i => (i as Song).MediaVersions) - .ThenInclude(mv => mv.MediaFiles) - .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)); - } + _dbContextFactory = dbContextFactory; + _mediaSourceRepository = mediaSourceRepository; } -} + + public async Task> Handle( + GetCollectionCards request, + CancellationToken cancellationToken) + { + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + + Option maybeJellyfin = await _mediaSourceRepository.GetAllJellyfin() + .Map(list => list.HeadOrNone()); + + Option maybeEmby = await _mediaSourceRepository.GetAllEmby() + .Map(list => list.HeadOrNone()); + + 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 Movie).MediaVersions) + .ThenInclude(mv => mv.MediaFiles) + .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 MusicVideo).MediaVersions) + .ThenInclude(mv => mv.MediaFiles) + .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) + .Include(c => c.MediaItems) + .ThenInclude(i => (i as Episode).MediaVersions) + .ThenInclude(mv => mv.MediaFiles) + .Include(c => c.MediaItems) + .ThenInclude(i => (i as OtherVideo).OtherVideoMetadata) + .ThenInclude(ovm => ovm.Artwork) + .Include(c => c.MediaItems) + .ThenInclude(i => (i as OtherVideo).MediaVersions) + .ThenInclude(mv => mv.MediaFiles) + .Include(c => c.MediaItems) + .ThenInclude(i => (i as Song).SongMetadata) + .ThenInclude(ovm => ovm.Artwork) + .Include(c => c.MediaItems) + .ThenInclude(i => (i as Song).MediaVersions) + .ThenInclude(mv => mv.MediaFiles) + .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)); + } +} \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCards/Queries/GetMusicVideoCards.cs b/ErsatzTV.Application/MediaCards/Queries/GetMusicVideoCards.cs index 71564a568..2399f57a9 100644 --- a/ErsatzTV.Application/MediaCards/Queries/GetMusicVideoCards.cs +++ b/ErsatzTV.Application/MediaCards/Queries/GetMusicVideoCards.cs @@ -1,7 +1,4 @@ -using MediatR; +namespace ErsatzTV.Application.MediaCards; -namespace ErsatzTV.Application.MediaCards.Queries -{ - public record GetMusicVideoCards - (int ArtistId, int PageNumber, int PageSize) : IRequest; -} +public record GetMusicVideoCards + (int ArtistId, int PageNumber, int PageSize) : IRequest; \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCards/Queries/GetMusicVideoCardsHandler.cs b/ErsatzTV.Application/MediaCards/Queries/GetMusicVideoCardsHandler.cs index b47c62879..6ff72922b 100644 --- a/ErsatzTV.Application/MediaCards/Queries/GetMusicVideoCardsHandler.cs +++ b/ErsatzTV.Application/MediaCards/Queries/GetMusicVideoCardsHandler.cs @@ -1,33 +1,25 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -using MediatR; +using ErsatzTV.Core.Interfaces.Repositories; using static ErsatzTV.Application.MediaCards.Mapper; -using static LanguageExt.Prelude; -namespace ErsatzTV.Application.MediaCards.Queries +namespace ErsatzTV.Application.MediaCards; + +public class GetMusicVideoCardsHandler : IRequestHandler { - public class GetMusicVideoCardsHandler : IRequestHandler + private readonly IMusicVideoRepository _musicVideoRepository; + + public GetMusicVideoCardsHandler(IMusicVideoRepository musicVideoRepository) => + _musicVideoRepository = musicVideoRepository; + + public async Task Handle( + GetMusicVideoCards request, + CancellationToken cancellationToken) { - private readonly IMusicVideoRepository _musicVideoRepository; + int count = await _musicVideoRepository.GetMusicVideoCount(request.ArtistId); - public GetMusicVideoCardsHandler(IMusicVideoRepository musicVideoRepository) => - _musicVideoRepository = musicVideoRepository; + List results = await _musicVideoRepository + .GetPagedMusicVideos(request.ArtistId, request.PageNumber, request.PageSize) + .Map(list => list.Map(ProjectToViewModel).ToList()); - public async Task Handle( - GetMusicVideoCards request, - CancellationToken cancellationToken) - { - int count = await _musicVideoRepository.GetMusicVideoCount(request.ArtistId); - - List results = await _musicVideoRepository - .GetPagedMusicVideos(request.ArtistId, request.PageNumber, request.PageSize) - .Map(list => list.Map(ProjectToViewModel).ToList()); - - return new MusicVideoCardResultsViewModel(count, results, None); - } + return new MusicVideoCardResultsViewModel(count, results, None); } -} +} \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCards/Queries/GetTelevisionEpisodeCards.cs b/ErsatzTV.Application/MediaCards/Queries/GetTelevisionEpisodeCards.cs index b6aa3cc51..3a18d5b69 100644 --- a/ErsatzTV.Application/MediaCards/Queries/GetTelevisionEpisodeCards.cs +++ b/ErsatzTV.Application/MediaCards/Queries/GetTelevisionEpisodeCards.cs @@ -1,7 +1,4 @@ -using MediatR; +namespace ErsatzTV.Application.MediaCards; -namespace ErsatzTV.Application.MediaCards.Queries -{ - public record GetTelevisionEpisodeCards - (int TelevisionSeasonId, int PageNumber, int PageSize) : IRequest; -} +public record GetTelevisionEpisodeCards + (int TelevisionSeasonId, int PageNumber, int PageSize) : IRequest; \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCards/Queries/GetTelevisionEpisodeCardsHandler.cs b/ErsatzTV.Application/MediaCards/Queries/GetTelevisionEpisodeCardsHandler.cs index 97607c779..3863ca021 100644 --- a/ErsatzTV.Application/MediaCards/Queries/GetTelevisionEpisodeCardsHandler.cs +++ b/ErsatzTV.Application/MediaCards/Queries/GetTelevisionEpisodeCardsHandler.cs @@ -1,48 +1,41 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Search; -using LanguageExt; -using MediatR; using static ErsatzTV.Application.MediaCards.Mapper; -namespace ErsatzTV.Application.MediaCards.Queries +namespace ErsatzTV.Application.MediaCards; + +public class + GetTelevisionEpisodeCardsHandler : IRequestHandler { - public class - GetTelevisionEpisodeCardsHandler : IRequestHandler + private readonly IMediaSourceRepository _mediaSourceRepository; + private readonly ITelevisionRepository _televisionRepository; + + public GetTelevisionEpisodeCardsHandler( + ITelevisionRepository televisionRepository, + IMediaSourceRepository mediaSourceRepository) { - private readonly IMediaSourceRepository _mediaSourceRepository; - private readonly ITelevisionRepository _televisionRepository; - - public GetTelevisionEpisodeCardsHandler( - ITelevisionRepository televisionRepository, - IMediaSourceRepository mediaSourceRepository) - { - _televisionRepository = televisionRepository; - _mediaSourceRepository = mediaSourceRepository; - } - - public async Task Handle( - GetTelevisionEpisodeCards request, - CancellationToken cancellationToken) - { - int count = await _televisionRepository.GetEpisodeCount(request.TelevisionSeasonId); - - Option maybeJellyfin = await _mediaSourceRepository.GetAllJellyfin() - .Map(list => list.HeadOrNone()); - - Option maybeEmby = await _mediaSourceRepository.GetAllEmby() - .Map(list => list.HeadOrNone()); - - List results = await _televisionRepository - .GetPagedEpisodes(request.TelevisionSeasonId, request.PageNumber, request.PageSize) - .Map(list => list.Map(e => ProjectToViewModel(e, maybeJellyfin, maybeEmby, false)).ToList()); - - return new TelevisionEpisodeCardResultsViewModel(count, results, Option.None); - } + _televisionRepository = televisionRepository; + _mediaSourceRepository = mediaSourceRepository; } -} + + public async Task Handle( + GetTelevisionEpisodeCards request, + CancellationToken cancellationToken) + { + int count = await _televisionRepository.GetEpisodeCount(request.TelevisionSeasonId); + + Option maybeJellyfin = await _mediaSourceRepository.GetAllJellyfin() + .Map(list => list.HeadOrNone()); + + Option maybeEmby = await _mediaSourceRepository.GetAllEmby() + .Map(list => list.HeadOrNone()); + + List results = await _televisionRepository + .GetPagedEpisodes(request.TelevisionSeasonId, request.PageNumber, request.PageSize) + .Map(list => list.Map(e => ProjectToViewModel(e, maybeJellyfin, maybeEmby, false)).ToList()); + + return new TelevisionEpisodeCardResultsViewModel(count, results, Option.None); + } +} \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCards/Queries/GetTelevisionSeasonCards.cs b/ErsatzTV.Application/MediaCards/Queries/GetTelevisionSeasonCards.cs index 85f7546fb..20cfeabd2 100644 --- a/ErsatzTV.Application/MediaCards/Queries/GetTelevisionSeasonCards.cs +++ b/ErsatzTV.Application/MediaCards/Queries/GetTelevisionSeasonCards.cs @@ -1,7 +1,4 @@ -using MediatR; +namespace ErsatzTV.Application.MediaCards; -namespace ErsatzTV.Application.MediaCards.Queries -{ - public record GetTelevisionSeasonCards - (int TelevisionShowId, int PageNumber, int PageSize) : IRequest; -} +public record GetTelevisionSeasonCards + (int TelevisionShowId, int PageNumber, int PageSize) : IRequest; \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCards/Queries/GetTelevisionSeasonCardsHandler.cs b/ErsatzTV.Application/MediaCards/Queries/GetTelevisionSeasonCardsHandler.cs index 2eb386a2c..61d3b5a54 100644 --- a/ErsatzTV.Application/MediaCards/Queries/GetTelevisionSeasonCardsHandler.cs +++ b/ErsatzTV.Application/MediaCards/Queries/GetTelevisionSeasonCardsHandler.cs @@ -1,48 +1,40 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -using MediatR; using static ErsatzTV.Application.MediaCards.Mapper; -using static LanguageExt.Prelude; -namespace ErsatzTV.Application.MediaCards.Queries +namespace ErsatzTV.Application.MediaCards; + +public class + GetTelevisionSeasonCardsHandler : IRequestHandler { - public class - GetTelevisionSeasonCardsHandler : IRequestHandler + private readonly IMediaSourceRepository _mediaSourceRepository; + private readonly ITelevisionRepository _televisionRepository; + + public GetTelevisionSeasonCardsHandler( + ITelevisionRepository televisionRepository, + IMediaSourceRepository mediaSourceRepository) { - private readonly IMediaSourceRepository _mediaSourceRepository; - private readonly ITelevisionRepository _televisionRepository; - - public GetTelevisionSeasonCardsHandler( - ITelevisionRepository televisionRepository, - IMediaSourceRepository mediaSourceRepository) - { - _televisionRepository = televisionRepository; - _mediaSourceRepository = mediaSourceRepository; - } - - public async Task Handle( - GetTelevisionSeasonCards request, - CancellationToken cancellationToken) - { - int count = await _televisionRepository.GetSeasonCount(request.TelevisionShowId); - - Option maybeJellyfin = await _mediaSourceRepository.GetAllJellyfin() - .Map(list => list.HeadOrNone()); - - Option maybeEmby = await _mediaSourceRepository.GetAllEmby() - .Map(list => list.HeadOrNone()); - - List results = await _televisionRepository - .GetPagedSeasons(request.TelevisionShowId, request.PageNumber, request.PageSize) - .Map(list => list.Map(s => ProjectToViewModel(s, maybeJellyfin, maybeEmby)).ToList()); - - return new TelevisionSeasonCardResultsViewModel(count, results, None); - } + _televisionRepository = televisionRepository; + _mediaSourceRepository = mediaSourceRepository; } -} + + public async Task Handle( + GetTelevisionSeasonCards request, + CancellationToken cancellationToken) + { + int count = await _televisionRepository.GetSeasonCount(request.TelevisionShowId); + + Option maybeJellyfin = await _mediaSourceRepository.GetAllJellyfin() + .Map(list => list.HeadOrNone()); + + Option maybeEmby = await _mediaSourceRepository.GetAllEmby() + .Map(list => list.HeadOrNone()); + + List results = await _televisionRepository + .GetPagedSeasons(request.TelevisionShowId, request.PageNumber, request.PageSize) + .Map(list => list.Map(s => ProjectToViewModel(s, maybeJellyfin, maybeEmby)).ToList()); + + return new TelevisionSeasonCardResultsViewModel(count, results, None); + } +} \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCards/SearchCardResultsViewModel.cs b/ErsatzTV.Application/MediaCards/SearchCardResultsViewModel.cs index e84f3c413..88be2d8a6 100644 --- a/ErsatzTV.Application/MediaCards/SearchCardResultsViewModel.cs +++ b/ErsatzTV.Application/MediaCards/SearchCardResultsViewModel.cs @@ -1,8 +1,5 @@ -using System.Collections.Generic; +namespace ErsatzTV.Application.MediaCards; -namespace ErsatzTV.Application.MediaCards -{ - public record SearchCardResultsViewModel( - List MovieCards, - List ShowCards); -} +public record SearchCardResultsViewModel( + List MovieCards, + List ShowCards); \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCards/SongCardResultsViewModel.cs b/ErsatzTV.Application/MediaCards/SongCardResultsViewModel.cs index e2b8f71b4..eca3dc2cb 100644 --- a/ErsatzTV.Application/MediaCards/SongCardResultsViewModel.cs +++ b/ErsatzTV.Application/MediaCards/SongCardResultsViewModel.cs @@ -1,11 +1,8 @@ -using System.Collections.Generic; -using ErsatzTV.Core.Search; -using LanguageExt; +using ErsatzTV.Core.Search; -namespace ErsatzTV.Application.MediaCards -{ - public record SongCardResultsViewModel( - int Count, - List Cards, - Option PageMap); -} +namespace ErsatzTV.Application.MediaCards; + +public record SongCardResultsViewModel( + int Count, + List Cards, + Option PageMap); \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCards/SongCardViewModel.cs b/ErsatzTV.Application/MediaCards/SongCardViewModel.cs index f755b8c6d..b1a02141a 100644 --- a/ErsatzTV.Application/MediaCards/SongCardViewModel.cs +++ b/ErsatzTV.Application/MediaCards/SongCardViewModel.cs @@ -1,22 +1,21 @@ using ErsatzTV.Core.Domain; -namespace ErsatzTV.Application.MediaCards +namespace ErsatzTV.Application.MediaCards; + +public record SongCardViewModel +( + int SongId, + string Title, + string Subtitle, + string SortTitle, + string Poster, + MediaItemState State) : MediaCardViewModel( + SongId, + Title, + Subtitle, + SortTitle, + Poster, + State) { - public record SongCardViewModel - ( - int SongId, - string Title, - string Subtitle, - string SortTitle, - string Poster, - MediaItemState State) : MediaCardViewModel( - SongId, - Title, - Subtitle, - SortTitle, - Poster, - State) - { - public int CustomIndex { get; set; } - } -} + public int CustomIndex { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCards/TelevisionEpisodeCardResultsViewModel.cs b/ErsatzTV.Application/MediaCards/TelevisionEpisodeCardResultsViewModel.cs index bec3054a2..af3c11ccd 100644 --- a/ErsatzTV.Application/MediaCards/TelevisionEpisodeCardResultsViewModel.cs +++ b/ErsatzTV.Application/MediaCards/TelevisionEpisodeCardResultsViewModel.cs @@ -1,11 +1,8 @@ -using System.Collections.Generic; -using ErsatzTV.Core.Search; -using LanguageExt; +using ErsatzTV.Core.Search; -namespace ErsatzTV.Application.MediaCards -{ - public record TelevisionEpisodeCardResultsViewModel( - int Count, - List Cards, - Option PageMap); -} +namespace ErsatzTV.Application.MediaCards; + +public record TelevisionEpisodeCardResultsViewModel( + int Count, + List Cards, + Option PageMap); \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCards/TelevisionEpisodeCardViewModel.cs b/ErsatzTV.Application/MediaCards/TelevisionEpisodeCardViewModel.cs index 8f0f7b614..5d47f6c2e 100644 --- a/ErsatzTV.Application/MediaCards/TelevisionEpisodeCardViewModel.cs +++ b/ErsatzTV.Application/MediaCards/TelevisionEpisodeCardViewModel.cs @@ -1,30 +1,27 @@ -using System; -using System.Collections.Generic; -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; -namespace ErsatzTV.Application.MediaCards -{ - public record TelevisionEpisodeCardViewModel - ( - int EpisodeId, - DateTime Aired, - string ShowTitle, - int ShowId, - int SeasonId, - int Season, - int Episode, - string Title, - string SortTitle, - string Plot, - string Poster, - List Directors, - List Writers, - MediaItemState State, - string Path) : MediaCardViewModel( - EpisodeId, - Title, - $"Episode {Episode}", - SortTitle, - Poster, - State); -} +namespace ErsatzTV.Application.MediaCards; + +public record TelevisionEpisodeCardViewModel +( + int EpisodeId, + DateTime Aired, + string ShowTitle, + int ShowId, + int SeasonId, + int Season, + int Episode, + string Title, + string SortTitle, + string Plot, + string Poster, + List Directors, + List Writers, + MediaItemState State, + string Path) : MediaCardViewModel( + EpisodeId, + Title, + $"Episode {Episode}", + SortTitle, + Poster, + State); \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCards/TelevisionSeasonCardResultsViewModel.cs b/ErsatzTV.Application/MediaCards/TelevisionSeasonCardResultsViewModel.cs index 028935af2..c3a2d2057 100644 --- a/ErsatzTV.Application/MediaCards/TelevisionSeasonCardResultsViewModel.cs +++ b/ErsatzTV.Application/MediaCards/TelevisionSeasonCardResultsViewModel.cs @@ -1,11 +1,8 @@ -using System.Collections.Generic; -using ErsatzTV.Core.Search; -using LanguageExt; +using ErsatzTV.Core.Search; -namespace ErsatzTV.Application.MediaCards -{ - public record TelevisionSeasonCardResultsViewModel( - int Count, - List Cards, - Option PageMap); -} +namespace ErsatzTV.Application.MediaCards; + +public record TelevisionSeasonCardResultsViewModel( + int Count, + List Cards, + Option PageMap); \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCards/TelevisionSeasonCardViewModel.cs b/ErsatzTV.Application/MediaCards/TelevisionSeasonCardViewModel.cs index 65dc96e47..d2f42db14 100644 --- a/ErsatzTV.Application/MediaCards/TelevisionSeasonCardViewModel.cs +++ b/ErsatzTV.Application/MediaCards/TelevisionSeasonCardViewModel.cs @@ -1,22 +1,21 @@ using ErsatzTV.Core.Domain; -namespace ErsatzTV.Application.MediaCards -{ - public record TelevisionSeasonCardViewModel - ( - string ShowTitle, - int TelevisionSeasonId, - int TelevisionSeasonNumber, - string Title, - string Subtitle, - string SortTitle, - string Poster, - string Placeholder, - MediaItemState State) : MediaCardViewModel( - TelevisionSeasonId, - Title, - Subtitle, - SortTitle, - Poster, - State); -} +namespace ErsatzTV.Application.MediaCards; + +public record TelevisionSeasonCardViewModel +( + string ShowTitle, + int TelevisionSeasonId, + int TelevisionSeasonNumber, + string Title, + string Subtitle, + string SortTitle, + string Poster, + string Placeholder, + MediaItemState State) : MediaCardViewModel( + TelevisionSeasonId, + Title, + Subtitle, + SortTitle, + Poster, + State); \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCards/TelevisionShowCardResultsViewModel.cs b/ErsatzTV.Application/MediaCards/TelevisionShowCardResultsViewModel.cs index 1d8cee3c4..3e1264369 100644 --- a/ErsatzTV.Application/MediaCards/TelevisionShowCardResultsViewModel.cs +++ b/ErsatzTV.Application/MediaCards/TelevisionShowCardResultsViewModel.cs @@ -1,11 +1,8 @@ -using System.Collections.Generic; -using ErsatzTV.Core.Search; -using LanguageExt; +using ErsatzTV.Core.Search; -namespace ErsatzTV.Application.MediaCards -{ - public record TelevisionShowCardResultsViewModel( - int Count, - List Cards, - Option PageMap); -} +namespace ErsatzTV.Application.MediaCards; + +public record TelevisionShowCardResultsViewModel( + int Count, + List Cards, + Option PageMap); \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCards/TelevisionShowCardViewModel.cs b/ErsatzTV.Application/MediaCards/TelevisionShowCardViewModel.cs index b7e86eac3..3aee93167 100644 --- a/ErsatzTV.Application/MediaCards/TelevisionShowCardViewModel.cs +++ b/ErsatzTV.Application/MediaCards/TelevisionShowCardViewModel.cs @@ -1,19 +1,18 @@ using ErsatzTV.Core.Domain; -namespace ErsatzTV.Application.MediaCards -{ - public record TelevisionShowCardViewModel - ( - int TelevisionShowId, - string Title, - string Subtitle, - string SortTitle, - string Poster, - MediaItemState State) : MediaCardViewModel( - TelevisionShowId, - Title, - Subtitle, - SortTitle, - Poster, - State); -} +namespace ErsatzTV.Application.MediaCards; + +public record TelevisionShowCardViewModel +( + int TelevisionShowId, + string Title, + string Subtitle, + string SortTitle, + string Poster, + MediaItemState State) : MediaCardViewModel( + TelevisionShowId, + Title, + Subtitle, + SortTitle, + Poster, + State); \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddArtistToCollection.cs b/ErsatzTV.Application/MediaCollections/Commands/AddArtistToCollection.cs index 328a8af4a..c2711031b 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/AddArtistToCollection.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/AddArtistToCollection.cs @@ -1,8 +1,6 @@ using ErsatzTV.Core; -using LanguageExt; -namespace ErsatzTV.Application.MediaCollections.Commands -{ - public record AddArtistToCollection - (int CollectionId, int ArtistId) : MediatR.IRequest>; -} +namespace ErsatzTV.Application.MediaCollections; + +public record AddArtistToCollection + (int CollectionId, int ArtistId) : MediatR.IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddArtistToCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/AddArtistToCollectionHandler.cs index 99470520a..e27fe6931 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/AddArtistToCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/AddArtistToCollectionHandler.cs @@ -1,80 +1,76 @@ -using System.Threading; -using System.Threading.Channels; -using System.Threading.Tasks; -using ErsatzTV.Application.Playouts.Commands; +using System.Threading.Channels; +using ErsatzTV.Application.Playouts; 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 +namespace ErsatzTV.Application.MediaCollections; + +public class AddArtistToCollectionHandler : + MediatR.IRequestHandler> { - public class AddArtistToCollectionHandler : - MediatR.IRequestHandler> + private readonly ChannelWriter _channel; + private readonly IDbContextFactory _dbContextFactory; + private readonly IMediaCollectionRepository _mediaCollectionRepository; + + public AddArtistToCollectionHandler( + IDbContextFactory dbContextFactory, + IMediaCollectionRepository mediaCollectionRepository, + ChannelWriter channel) { - private readonly ChannelWriter _channel; - private readonly IDbContextFactory _dbContextFactory; - private readonly IMediaCollectionRepository _mediaCollectionRepository; - - public AddArtistToCollectionHandler( - IDbContextFactory dbContextFactory, - IMediaCollectionRepository mediaCollectionRepository, - ChannelWriter channel) - { - _dbContextFactory = dbContextFactory; - _mediaCollectionRepository = mediaCollectionRepository; - _channel = channel; - } - - public async Task> Handle( - AddArtistToCollection request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - Validation validation = await Validate(dbContext, request); - return await validation.Apply(parameters => ApplyAddArtistRequest(dbContext, parameters)); - } - - private async Task ApplyAddArtistRequest(TvContext dbContext, Parameters parameters) - { - 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(parameters.Collection.Id)) - { - await _channel.WriteAsync(new BuildPlayout(playoutId, true)); - } - } - - return Unit.Default; - } - - private static async Task> Validate( - TvContext dbContext, - AddArtistToCollection request) => - (await CollectionMustExist(dbContext, request), await ValidateArtist(dbContext, request)) - .Apply((collection, artist) => new Parameters(collection, artist)); - - private static Task> CollectionMustExist( - TvContext dbContext, - AddArtistToCollection request) => - dbContext.Collections - .Include(c => c.MediaItems) - .SelectOneAsync(c => c.Id, c => c.Id == request.CollectionId) - .Map(o => o.ToValidation("Collection does not exist.")); - - private static Task> ValidateArtist( - TvContext dbContext, - AddArtistToCollection request) => - dbContext.Artists - .SelectOneAsync(a => a.Id, a => a.Id == request.ArtistId) - .Map(o => o.ToValidation("Artist does not exist")); - - private record Parameters(Collection Collection, Artist Artist); + _dbContextFactory = dbContextFactory; + _mediaCollectionRepository = mediaCollectionRepository; + _channel = channel; } -} + + public async Task> Handle( + AddArtistToCollection request, + CancellationToken cancellationToken) + { + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + Validation validation = await Validate(dbContext, request); + return await LanguageExtensions.Apply(validation, parameters => ApplyAddArtistRequest(dbContext, parameters)); + } + + private async Task ApplyAddArtistRequest(TvContext dbContext, Parameters parameters) + { + 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(parameters.Collection.Id)) + { + await _channel.WriteAsync(new BuildPlayout(playoutId, true)); + } + } + + return Unit.Default; + } + + private static async Task> Validate( + TvContext dbContext, + AddArtistToCollection request) => + (await CollectionMustExist(dbContext, request), await ValidateArtist(dbContext, request)) + .Apply((collection, artist) => new Parameters(collection, artist)); + + private static Task> CollectionMustExist( + TvContext dbContext, + AddArtistToCollection request) => + dbContext.Collections + .Include(c => c.MediaItems) + .SelectOneAsync(c => c.Id, c => c.Id == request.CollectionId) + .Map(o => o.ToValidation("Collection does not exist.")); + + private static Task> ValidateArtist( + TvContext dbContext, + AddArtistToCollection request) => + dbContext.Artists + .SelectOneAsync(a => a.Id, a => a.Id == request.ArtistId) + .Map(o => o.ToValidation("Artist does not exist")); + + private record Parameters(Collection Collection, Artist Artist); +} \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddEpisodeToCollection.cs b/ErsatzTV.Application/MediaCollections/Commands/AddEpisodeToCollection.cs index ae69c119c..79ce6509f 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/AddEpisodeToCollection.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/AddEpisodeToCollection.cs @@ -1,7 +1,5 @@ using ErsatzTV.Core; -using LanguageExt; -namespace ErsatzTV.Application.MediaCollections.Commands -{ - public record AddEpisodeToCollection(int CollectionId, int EpisodeId) : MediatR.IRequest>; -} +namespace ErsatzTV.Application.MediaCollections; + +public record AddEpisodeToCollection(int CollectionId, int EpisodeId) : MediatR.IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddEpisodeToCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/AddEpisodeToCollectionHandler.cs index 625d013e2..1db8dd485 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/AddEpisodeToCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/AddEpisodeToCollectionHandler.cs @@ -1,80 +1,76 @@ -using System.Threading; -using System.Threading.Channels; -using System.Threading.Tasks; -using ErsatzTV.Application.Playouts.Commands; +using System.Threading.Channels; +using ErsatzTV.Application.Playouts; 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 +namespace ErsatzTV.Application.MediaCollections; + +public class AddEpisodeToCollectionHandler : + MediatR.IRequestHandler> { - public class AddEpisodeToCollectionHandler : - MediatR.IRequestHandler> + private readonly ChannelWriter _channel; + private readonly IDbContextFactory _dbContextFactory; + private readonly IMediaCollectionRepository _mediaCollectionRepository; + + public AddEpisodeToCollectionHandler( + IDbContextFactory dbContextFactory, + IMediaCollectionRepository mediaCollectionRepository, + ChannelWriter channel) { - private readonly ChannelWriter _channel; - private readonly IDbContextFactory _dbContextFactory; - private readonly IMediaCollectionRepository _mediaCollectionRepository; - - public AddEpisodeToCollectionHandler( - IDbContextFactory dbContextFactory, - IMediaCollectionRepository mediaCollectionRepository, - ChannelWriter channel) - { - _dbContextFactory = dbContextFactory; - _mediaCollectionRepository = mediaCollectionRepository; - _channel = channel; - } - - public async Task> Handle( - AddEpisodeToCollection request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - Validation validation = await Validate(dbContext, request); - return await validation.Apply(parameters => ApplyAddTelevisionEpisodeRequest(dbContext, parameters)); - } - - private async Task ApplyAddTelevisionEpisodeRequest(TvContext dbContext, Parameters parameters) - { - 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(parameters.Collection.Id)) - { - await _channel.WriteAsync(new BuildPlayout(playoutId, true)); - } - } - - return Unit.Default; - } - - private static async Task> Validate( - TvContext dbContext, - AddEpisodeToCollection request) => - (await CollectionMustExist(dbContext, request), await ValidateEpisode(dbContext, request)) - .Apply((collection, episode) => new Parameters(collection, episode)); - - private static Task> CollectionMustExist( - TvContext dbContext, - AddEpisodeToCollection request) => - dbContext.Collections - .Include(c => c.MediaItems) - .SelectOneAsync(c => c.Id, c => c.Id == request.CollectionId) - .Map(o => o.ToValidation("Collection does not exist.")); - - private static Task> ValidateEpisode( - TvContext dbContext, - AddEpisodeToCollection request) => - dbContext.Episodes - .SelectOneAsync(e => e.Id, e => e.Id == request.EpisodeId) - .Map(o => o.ToValidation("Episode does not exist")); - - private record Parameters(Collection Collection, Episode Episode); + _dbContextFactory = dbContextFactory; + _mediaCollectionRepository = mediaCollectionRepository; + _channel = channel; } -} + + public async Task> Handle( + AddEpisodeToCollection request, + CancellationToken cancellationToken) + { + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + Validation validation = await Validate(dbContext, request); + return await LanguageExtensions.Apply(validation, parameters => ApplyAddTelevisionEpisodeRequest(dbContext, parameters)); + } + + private async Task ApplyAddTelevisionEpisodeRequest(TvContext dbContext, Parameters parameters) + { + 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(parameters.Collection.Id)) + { + await _channel.WriteAsync(new BuildPlayout(playoutId, true)); + } + } + + return Unit.Default; + } + + private static async Task> Validate( + TvContext dbContext, + AddEpisodeToCollection request) => + (await CollectionMustExist(dbContext, request), await ValidateEpisode(dbContext, request)) + .Apply((collection, episode) => new Parameters(collection, episode)); + + private static Task> CollectionMustExist( + TvContext dbContext, + AddEpisodeToCollection request) => + dbContext.Collections + .Include(c => c.MediaItems) + .SelectOneAsync(c => c.Id, c => c.Id == request.CollectionId) + .Map(o => o.ToValidation("Collection does not exist.")); + + private static Task> ValidateEpisode( + TvContext dbContext, + AddEpisodeToCollection request) => + dbContext.Episodes + .SelectOneAsync(e => e.Id, e => e.Id == request.EpisodeId) + .Map(o => o.ToValidation("Episode does not exist")); + + private record Parameters(Collection Collection, Episode Episode); +} \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollection.cs b/ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollection.cs index 1c5302f17..aada16a44 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollection.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollection.cs @@ -1,18 +1,15 @@ -using System.Collections.Generic; -using ErsatzTV.Core; -using LanguageExt; +using ErsatzTV.Core; -namespace ErsatzTV.Application.MediaCollections.Commands -{ - public record AddItemsToCollection - ( - int CollectionId, - List MovieIds, - List ShowIds, - List SeasonIds, - List EpisodeIds, - List ArtistIds, - List MusicVideoIds, - List OtherVideoIds, - List SongIds) : MediatR.IRequest>; -} +namespace ErsatzTV.Application.MediaCollections; + +public record AddItemsToCollection +( + int CollectionId, + List MovieIds, + List ShowIds, + List SeasonIds, + List EpisodeIds, + List ArtistIds, + List MusicVideoIds, + List OtherVideoIds, + List SongIds) : MediatR.IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollectionHandler.cs index 59fcf4280..a2fbdd222 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollectionHandler.cs @@ -1,129 +1,122 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Channels; -using System.Threading.Tasks; -using ErsatzTV.Application.Playouts.Commands; +using System.Threading.Channels; +using ErsatzTV.Application.Playouts; 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 +namespace ErsatzTV.Application.MediaCollections; + +public class AddItemsToCollectionHandler : + MediatR.IRequestHandler> { - public class AddItemsToCollectionHandler : - MediatR.IRequestHandler> + private readonly ChannelWriter _channel; + private readonly IDbContextFactory _dbContextFactory; + private readonly IMediaCollectionRepository _mediaCollectionRepository; + private readonly IMovieRepository _movieRepository; + private readonly ITelevisionRepository _televisionRepository; + + public AddItemsToCollectionHandler( + IDbContextFactory dbContextFactory, + IMediaCollectionRepository mediaCollectionRepository, + IMovieRepository movieRepository, + ITelevisionRepository televisionRepository, + ChannelWriter channel) { - private readonly ChannelWriter _channel; - private readonly IDbContextFactory _dbContextFactory; - private readonly IMediaCollectionRepository _mediaCollectionRepository; - private readonly IMovieRepository _movieRepository; - private readonly ITelevisionRepository _televisionRepository; - - public AddItemsToCollectionHandler( - IDbContextFactory dbContextFactory, - IMediaCollectionRepository mediaCollectionRepository, - IMovieRepository movieRepository, - ITelevisionRepository televisionRepository, - ChannelWriter channel) - { - _dbContextFactory = dbContextFactory; - _mediaCollectionRepository = mediaCollectionRepository; - _movieRepository = movieRepository; - _televisionRepository = televisionRepository; - _channel = channel; - } - - public async Task> Handle( - AddItemsToCollection request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - - Validation validation = await Validate(dbContext, request); - return await validation.Apply(c => ApplyAddItemsRequest(dbContext, c, request)); - } - - private async Task ApplyAddItemsRequest(TvContext dbContext, Collection collection, AddItemsToCollection request) - { - var allItems = request.MovieIds - .Append(request.ShowIds) - .Append(request.SeasonIds) - .Append(request.EpisodeIds) - .Append(request.ArtistIds) - .Append(request.MusicVideoIds) - .Append(request.OtherVideoIds) - .Append(request.SongIds) - .ToList(); - - var toAddIds = allItems.Where(item => collection.MediaItems.All(mi => mi.Id != item)).ToList(); - List 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 - .PlayoutIdsUsingCollection(request.CollectionId)) - { - await _channel.WriteAsync(new BuildPlayout(playoutId, true)); - } - } - - return Unit.Default; - } - - private async Task> Validate( - TvContext dbContext, - AddItemsToCollection request) => - (await CollectionMustExist(dbContext, request), - await ValidateMovies(request), - await ValidateShows(request), - await ValidateSeasons(request), - await ValidateEpisodes(request)) - .Apply((collection, _, _, _, _) => collection); - - private static Task> CollectionMustExist( - TvContext dbContext, - AddItemsToCollection request) => - dbContext.Collections - .Include(c => c.MediaItems) - .SelectOneAsync(c => c.Id, c => c.Id == request.CollectionId) - .Map(o => o.ToValidation("Collection does not exist.")); - - private Task> ValidateMovies(AddItemsToCollection request) => - _movieRepository.AllMoviesExist(request.MovieIds) - .Map(Optional) - .Filter(v => v == true) - .MapT(_ => Unit.Default) - .Map(v => v.ToValidation("Movie does not exist")); - - private Task> ValidateShows(AddItemsToCollection request) => - _televisionRepository.AllShowsExist(request.ShowIds) - .Map(Optional) - .Filter(v => v == true) - .MapT(_ => Unit.Default) - .Map(v => v.ToValidation("Show does not exist")); - - private Task> ValidateSeasons(AddItemsToCollection request) => - _televisionRepository.AllSeasonsExist(request.SeasonIds) - .Map(Optional) - .Filter(v => v == true) - .MapT(_ => Unit.Default) - .Map(v => v.ToValidation("Season does not exist")); - - private Task> ValidateEpisodes(AddItemsToCollection request) => - _televisionRepository.AllEpisodesExist(request.EpisodeIds) - .Map(Optional) - .Filter(v => v == true) - .MapT(_ => Unit.Default) - .Map(v => v.ToValidation("Episode does not exist")); + _dbContextFactory = dbContextFactory; + _mediaCollectionRepository = mediaCollectionRepository; + _movieRepository = movieRepository; + _televisionRepository = televisionRepository; + _channel = channel; } -} + + public async Task> Handle( + AddItemsToCollection request, + CancellationToken cancellationToken) + { + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + + Validation validation = await Validate(dbContext, request); + return await LanguageExtensions.Apply(validation, c => ApplyAddItemsRequest(dbContext, c, request)); + } + + private async Task ApplyAddItemsRequest(TvContext dbContext, Collection collection, AddItemsToCollection request) + { + var allItems = request.MovieIds + .Append(request.ShowIds) + .Append(request.SeasonIds) + .Append(request.EpisodeIds) + .Append(request.ArtistIds) + .Append(request.MusicVideoIds) + .Append(request.OtherVideoIds) + .Append(request.SongIds) + .ToList(); + + var toAddIds = allItems.Where(item => collection.MediaItems.All(mi => mi.Id != item)).ToList(); + List 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 + .PlayoutIdsUsingCollection(request.CollectionId)) + { + await _channel.WriteAsync(new BuildPlayout(playoutId, true)); + } + } + + return Unit.Default; + } + + private async Task> Validate( + TvContext dbContext, + AddItemsToCollection request) => + (await CollectionMustExist(dbContext, request), + await ValidateMovies(request), + await ValidateShows(request), + await ValidateSeasons(request), + await ValidateEpisodes(request)) + .Apply((collection, _, _, _, _) => collection); + + private static Task> CollectionMustExist( + TvContext dbContext, + AddItemsToCollection request) => + dbContext.Collections + .Include(c => c.MediaItems) + .SelectOneAsync(c => c.Id, c => c.Id == request.CollectionId) + .Map(o => o.ToValidation("Collection does not exist.")); + + private Task> ValidateMovies(AddItemsToCollection request) => + _movieRepository.AllMoviesExist(request.MovieIds) + .Map(Optional) + .Filter(v => v == true) + .MapT(_ => Unit.Default) + .Map(v => v.ToValidation("Movie does not exist")); + + private Task> ValidateShows(AddItemsToCollection request) => + _televisionRepository.AllShowsExist(request.ShowIds) + .Map(Optional) + .Filter(v => v == true) + .MapT(_ => Unit.Default) + .Map(v => v.ToValidation("Show does not exist")); + + private Task> ValidateSeasons(AddItemsToCollection request) => + _televisionRepository.AllSeasonsExist(request.SeasonIds) + .Map(Optional) + .Filter(v => v == true) + .MapT(_ => Unit.Default) + .Map(v => v.ToValidation("Season does not exist")); + + private Task> ValidateEpisodes(AddItemsToCollection request) => + _televisionRepository.AllEpisodesExist(request.EpisodeIds) + .Map(Optional) + .Filter(v => v == true) + .MapT(_ => Unit.Default) + .Map(v => v.ToValidation("Episode does not exist")); +} \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddMovieCollection.cs b/ErsatzTV.Application/MediaCollections/Commands/AddMovieCollection.cs index e9bc90deb..2ee69d58d 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/AddMovieCollection.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/AddMovieCollection.cs @@ -1,7 +1,5 @@ using ErsatzTV.Core; -using LanguageExt; -namespace ErsatzTV.Application.MediaCollections.Commands -{ - public record AddMovieToCollection(int CollectionId, int MovieId) : MediatR.IRequest>; -} +namespace ErsatzTV.Application.MediaCollections; + +public record AddMovieToCollection(int CollectionId, int MovieId) : MediatR.IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddMovieToCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/AddMovieToCollectionHandler.cs index 7c92ec84b..11b8616e5 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/AddMovieToCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/AddMovieToCollectionHandler.cs @@ -1,80 +1,76 @@ -using System.Threading; -using System.Threading.Channels; -using System.Threading.Tasks; -using ErsatzTV.Application.Playouts.Commands; +using System.Threading.Channels; +using ErsatzTV.Application.Playouts; 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 +namespace ErsatzTV.Application.MediaCollections; + +public class AddMovieToCollectionHandler : + MediatR.IRequestHandler> { - public class AddMovieToCollectionHandler : - MediatR.IRequestHandler> + private readonly ChannelWriter _channel; + private readonly IDbContextFactory _dbContextFactory; + private readonly IMediaCollectionRepository _mediaCollectionRepository; + + public AddMovieToCollectionHandler( + IDbContextFactory dbContextFactory, + IMediaCollectionRepository mediaCollectionRepository, + ChannelWriter channel) { - private readonly ChannelWriter _channel; - private readonly IDbContextFactory _dbContextFactory; - private readonly IMediaCollectionRepository _mediaCollectionRepository; - - public AddMovieToCollectionHandler( - IDbContextFactory dbContextFactory, - IMediaCollectionRepository mediaCollectionRepository, - ChannelWriter channel) - { - _dbContextFactory = dbContextFactory; - _mediaCollectionRepository = mediaCollectionRepository; - _channel = channel; - } - - public async Task> Handle( - AddMovieToCollection request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - Validation validation = await Validate(dbContext, request); - return await validation.Apply(parameters => ApplyAddMovieRequest(dbContext, parameters)); - } - - private async Task ApplyAddMovieRequest(TvContext dbContext, Parameters parameters) - { - 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(parameters.Collection.Id)) - { - await _channel.WriteAsync(new BuildPlayout(playoutId, true)); - } - } - - return Unit.Default; - } - - private static async Task> Validate( - TvContext dbContext, - AddMovieToCollection request) => - (await CollectionMustExist(dbContext, request), await ValidateMovie(dbContext, request)) - .Apply((collection, episode) => new Parameters(collection, episode)); - - private static Task> CollectionMustExist( - TvContext dbContext, - AddMovieToCollection request) => - dbContext.Collections - .Include(c => c.MediaItems) - .SelectOneAsync(c => c.Id, c => c.Id == request.CollectionId) - .Map(o => o.ToValidation("Collection does not exist.")); - - private static Task> ValidateMovie( - TvContext dbContext, - AddMovieToCollection request) => - dbContext.Movies - .SelectOneAsync(m => m.Id, e => e.Id == request.MovieId) - .Map(o => o.ToValidation("Movie does not exist")); - - private record Parameters(Collection Collection, Movie Movie); + _dbContextFactory = dbContextFactory; + _mediaCollectionRepository = mediaCollectionRepository; + _channel = channel; } -} + + public async Task> Handle( + AddMovieToCollection request, + CancellationToken cancellationToken) + { + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + Validation validation = await Validate(dbContext, request); + return await LanguageExtensions.Apply(validation, parameters => ApplyAddMovieRequest(dbContext, parameters)); + } + + private async Task ApplyAddMovieRequest(TvContext dbContext, Parameters parameters) + { + 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(parameters.Collection.Id)) + { + await _channel.WriteAsync(new BuildPlayout(playoutId, true)); + } + } + + return Unit.Default; + } + + private static async Task> Validate( + TvContext dbContext, + AddMovieToCollection request) => + (await CollectionMustExist(dbContext, request), await ValidateMovie(dbContext, request)) + .Apply((collection, episode) => new Parameters(collection, episode)); + + private static Task> CollectionMustExist( + TvContext dbContext, + AddMovieToCollection request) => + dbContext.Collections + .Include(c => c.MediaItems) + .SelectOneAsync(c => c.Id, c => c.Id == request.CollectionId) + .Map(o => o.ToValidation("Collection does not exist.")); + + private static Task> ValidateMovie( + TvContext dbContext, + AddMovieToCollection request) => + dbContext.Movies + .SelectOneAsync(m => m.Id, e => e.Id == request.MovieId) + .Map(o => o.ToValidation("Movie does not exist")); + + private record Parameters(Collection Collection, Movie Movie); +} \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddMusicVideoToCollection.cs b/ErsatzTV.Application/MediaCollections/Commands/AddMusicVideoToCollection.cs index b89e902ba..279206f58 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/AddMusicVideoToCollection.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/AddMusicVideoToCollection.cs @@ -1,8 +1,6 @@ using ErsatzTV.Core; -using LanguageExt; -namespace ErsatzTV.Application.MediaCollections.Commands -{ - public record AddMusicVideoToCollection - (int CollectionId, int MusicVideoId) : MediatR.IRequest>; -} +namespace ErsatzTV.Application.MediaCollections; + +public record AddMusicVideoToCollection + (int CollectionId, int MusicVideoId) : MediatR.IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddMusicVideoToCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/AddMusicVideoToCollectionHandler.cs index b21b3408f..d4a359136 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/AddMusicVideoToCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/AddMusicVideoToCollectionHandler.cs @@ -1,80 +1,76 @@ -using System.Threading; -using System.Threading.Channels; -using System.Threading.Tasks; -using ErsatzTV.Application.Playouts.Commands; +using System.Threading.Channels; +using ErsatzTV.Application.Playouts; 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 +namespace ErsatzTV.Application.MediaCollections; + +public class AddMusicVideoToCollectionHandler : + MediatR.IRequestHandler> { - public class AddMusicVideoToCollectionHandler : - MediatR.IRequestHandler> + private readonly ChannelWriter _channel; + private readonly IDbContextFactory _dbContextFactory; + private readonly IMediaCollectionRepository _mediaCollectionRepository; + + public AddMusicVideoToCollectionHandler( + IDbContextFactory dbContextFactory, + IMediaCollectionRepository mediaCollectionRepository, + ChannelWriter channel) { - private readonly ChannelWriter _channel; - private readonly IDbContextFactory _dbContextFactory; - private readonly IMediaCollectionRepository _mediaCollectionRepository; - - public AddMusicVideoToCollectionHandler( - IDbContextFactory dbContextFactory, - IMediaCollectionRepository mediaCollectionRepository, - ChannelWriter channel) - { - _dbContextFactory = dbContextFactory; - _mediaCollectionRepository = mediaCollectionRepository; - _channel = channel; - } - - public async Task> Handle( - AddMusicVideoToCollection request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - Validation validation = await Validate(dbContext, request); - return await validation.Apply(parameters => ApplyAddMusicVideoRequest(dbContext, parameters)); - } - - private async Task ApplyAddMusicVideoRequest(TvContext dbContext, Parameters parameters) - { - 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(parameters.Collection.Id)) - { - await _channel.WriteAsync(new BuildPlayout(playoutId, true)); - } - } - - return Unit.Default; - } - - private static async Task> Validate( - TvContext dbContext, - AddMusicVideoToCollection request) => - (await CollectionMustExist(dbContext, request), await ValidateMusicVideo(dbContext, request)) - .Apply((collection, episode) => new Parameters(collection, episode)); - - private static Task> CollectionMustExist( - TvContext dbContext, - AddMusicVideoToCollection request) => - dbContext.Collections - .Include(c => c.MediaItems) - .SelectOneAsync(c => c.Id, c => c.Id == request.CollectionId) - .Map(o => o.ToValidation("Collection does not exist.")); - - private static Task> ValidateMusicVideo( - TvContext dbContext, - AddMusicVideoToCollection request) => - dbContext.MusicVideos - .SelectOneAsync(m => m.Id, e => e.Id == request.MusicVideoId) - .Map(o => o.ToValidation("MusicVideo does not exist")); - - private record Parameters(Collection Collection, MusicVideo MusicVideo); + _dbContextFactory = dbContextFactory; + _mediaCollectionRepository = mediaCollectionRepository; + _channel = channel; } -} + + public async Task> Handle( + AddMusicVideoToCollection request, + CancellationToken cancellationToken) + { + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + Validation validation = await Validate(dbContext, request); + return await LanguageExtensions.Apply(validation, parameters => ApplyAddMusicVideoRequest(dbContext, parameters)); + } + + private async Task ApplyAddMusicVideoRequest(TvContext dbContext, Parameters parameters) + { + 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(parameters.Collection.Id)) + { + await _channel.WriteAsync(new BuildPlayout(playoutId, true)); + } + } + + return Unit.Default; + } + + private static async Task> Validate( + TvContext dbContext, + AddMusicVideoToCollection request) => + (await CollectionMustExist(dbContext, request), await ValidateMusicVideo(dbContext, request)) + .Apply((collection, episode) => new Parameters(collection, episode)); + + private static Task> CollectionMustExist( + TvContext dbContext, + AddMusicVideoToCollection request) => + dbContext.Collections + .Include(c => c.MediaItems) + .SelectOneAsync(c => c.Id, c => c.Id == request.CollectionId) + .Map(o => o.ToValidation("Collection does not exist.")); + + private static Task> ValidateMusicVideo( + TvContext dbContext, + AddMusicVideoToCollection request) => + dbContext.MusicVideos + .SelectOneAsync(m => m.Id, e => e.Id == request.MusicVideoId) + .Map(o => o.ToValidation("MusicVideo does not exist")); + + private record Parameters(Collection Collection, MusicVideo MusicVideo); +} \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddOtherVideoToCollection.cs b/ErsatzTV.Application/MediaCollections/Commands/AddOtherVideoToCollection.cs index cc017345f..c44310190 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/AddOtherVideoToCollection.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/AddOtherVideoToCollection.cs @@ -1,8 +1,6 @@ using ErsatzTV.Core; -using LanguageExt; -namespace ErsatzTV.Application.MediaCollections.Commands -{ - public record AddOtherVideoToCollection - (int CollectionId, int OtherVideoId) : MediatR.IRequest>; -} +namespace ErsatzTV.Application.MediaCollections; + +public record AddOtherVideoToCollection + (int CollectionId, int OtherVideoId) : MediatR.IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddOtherVideoToCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/AddOtherVideoToCollectionHandler.cs index ffa8d1059..24313e6bd 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/AddOtherVideoToCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/AddOtherVideoToCollectionHandler.cs @@ -1,80 +1,76 @@ -using System.Threading; -using System.Threading.Channels; -using System.Threading.Tasks; -using ErsatzTV.Application.Playouts.Commands; +using System.Threading.Channels; +using ErsatzTV.Application.Playouts; 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 +namespace ErsatzTV.Application.MediaCollections; + +public class AddOtherVideoToCollectionHandler : + MediatR.IRequestHandler> { - public class AddOtherVideoToCollectionHandler : - MediatR.IRequestHandler> + private readonly ChannelWriter _channel; + private readonly IDbContextFactory _dbContextFactory; + private readonly IMediaCollectionRepository _mediaCollectionRepository; + + public AddOtherVideoToCollectionHandler( + IDbContextFactory dbContextFactory, + IMediaCollectionRepository mediaCollectionRepository, + ChannelWriter channel) { - private readonly ChannelWriter _channel; - private readonly IDbContextFactory _dbContextFactory; - private readonly IMediaCollectionRepository _mediaCollectionRepository; - - public AddOtherVideoToCollectionHandler( - IDbContextFactory dbContextFactory, - IMediaCollectionRepository mediaCollectionRepository, - ChannelWriter channel) - { - _dbContextFactory = dbContextFactory; - _mediaCollectionRepository = mediaCollectionRepository; - _channel = channel; - } - - public async Task> Handle( - AddOtherVideoToCollection request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - Validation validation = await Validate(dbContext, request); - return await validation.Apply(parameters => ApplyAddOtherVideoRequest(dbContext, parameters)); - } - - private async Task ApplyAddOtherVideoRequest(TvContext dbContext, Parameters parameters) - { - parameters.Collection.MediaItems.Add(parameters.OtherVideo); - if (await dbContext.SaveChangesAsync() > 0) - { - // rebuild all playouts that use this collection - foreach (int playoutId in await _mediaCollectionRepository - .PlayoutIdsUsingCollection(parameters.Collection.Id)) - { - await _channel.WriteAsync(new BuildPlayout(playoutId, true)); - } - } - - return Unit.Default; - } - - private static async Task> Validate( - TvContext dbContext, - AddOtherVideoToCollection request) => - (await CollectionMustExist(dbContext, request), await ValidateOtherVideo(dbContext, request)) - .Apply((collection, episode) => new Parameters(collection, episode)); - - private static Task> CollectionMustExist( - TvContext dbContext, - AddOtherVideoToCollection request) => - dbContext.Collections - .Include(c => c.MediaItems) - .SelectOneAsync(c => c.Id, c => c.Id == request.CollectionId) - .Map(o => o.ToValidation("Collection does not exist.")); - - private static Task> ValidateOtherVideo( - TvContext dbContext, - AddOtherVideoToCollection request) => - dbContext.OtherVideos - .SelectOneAsync(m => m.Id, e => e.Id == request.OtherVideoId) - .Map(o => o.ToValidation("OtherVideo does not exist")); - - private record Parameters(Collection Collection, OtherVideo OtherVideo); + _dbContextFactory = dbContextFactory; + _mediaCollectionRepository = mediaCollectionRepository; + _channel = channel; } -} + + public async Task> Handle( + AddOtherVideoToCollection request, + CancellationToken cancellationToken) + { + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + Validation validation = await Validate(dbContext, request); + return await LanguageExtensions.Apply(validation, parameters => ApplyAddOtherVideoRequest(dbContext, parameters)); + } + + private async Task ApplyAddOtherVideoRequest(TvContext dbContext, Parameters parameters) + { + parameters.Collection.MediaItems.Add(parameters.OtherVideo); + if (await dbContext.SaveChangesAsync() > 0) + { + // rebuild all playouts that use this collection + foreach (int playoutId in await _mediaCollectionRepository + .PlayoutIdsUsingCollection(parameters.Collection.Id)) + { + await _channel.WriteAsync(new BuildPlayout(playoutId, true)); + } + } + + return Unit.Default; + } + + private static async Task> Validate( + TvContext dbContext, + AddOtherVideoToCollection request) => + (await CollectionMustExist(dbContext, request), await ValidateOtherVideo(dbContext, request)) + .Apply((collection, episode) => new Parameters(collection, episode)); + + private static Task> CollectionMustExist( + TvContext dbContext, + AddOtherVideoToCollection request) => + dbContext.Collections + .Include(c => c.MediaItems) + .SelectOneAsync(c => c.Id, c => c.Id == request.CollectionId) + .Map(o => o.ToValidation("Collection does not exist.")); + + private static Task> ValidateOtherVideo( + TvContext dbContext, + AddOtherVideoToCollection request) => + dbContext.OtherVideos + .SelectOneAsync(m => m.Id, e => e.Id == request.OtherVideoId) + .Map(o => o.ToValidation("OtherVideo does not exist")); + + private record Parameters(Collection Collection, OtherVideo OtherVideo); +} \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddSeasonToCollection.cs b/ErsatzTV.Application/MediaCollections/Commands/AddSeasonToCollection.cs index e3f795f82..57d43c27f 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/AddSeasonToCollection.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/AddSeasonToCollection.cs @@ -1,7 +1,5 @@ using ErsatzTV.Core; -using LanguageExt; -namespace ErsatzTV.Application.MediaCollections.Commands -{ - public record AddSeasonToCollection(int CollectionId, int SeasonId) : MediatR.IRequest>; -} +namespace ErsatzTV.Application.MediaCollections; + +public record AddSeasonToCollection(int CollectionId, int SeasonId) : MediatR.IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddSeasonToCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/AddSeasonToCollectionHandler.cs index f5d3e3952..893b463d1 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/AddSeasonToCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/AddSeasonToCollectionHandler.cs @@ -1,80 +1,76 @@ -using System.Threading; -using System.Threading.Channels; -using System.Threading.Tasks; -using ErsatzTV.Application.Playouts.Commands; +using System.Threading.Channels; +using ErsatzTV.Application.Playouts; 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 +namespace ErsatzTV.Application.MediaCollections; + +public class AddSeasonToCollectionHandler : + MediatR.IRequestHandler> { - public class AddSeasonToCollectionHandler : - MediatR.IRequestHandler> + private readonly ChannelWriter _channel; + private readonly IDbContextFactory _dbContextFactory; + private readonly IMediaCollectionRepository _mediaCollectionRepository; + + public AddSeasonToCollectionHandler( + IDbContextFactory dbContextFactory, + IMediaCollectionRepository mediaCollectionRepository, + ChannelWriter channel) { - private readonly ChannelWriter _channel; - private readonly IDbContextFactory _dbContextFactory; - private readonly IMediaCollectionRepository _mediaCollectionRepository; - - public AddSeasonToCollectionHandler( - IDbContextFactory dbContextFactory, - IMediaCollectionRepository mediaCollectionRepository, - ChannelWriter channel) - { - _dbContextFactory = dbContextFactory; - _mediaCollectionRepository = mediaCollectionRepository; - _channel = channel; - } - - public async Task> Handle( - AddSeasonToCollection request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - Validation validation = await Validate(dbContext, request); - return await validation.Apply(parameters => ApplyAddSeasonRequest(dbContext, parameters)); - } - - private async Task ApplyAddSeasonRequest(TvContext dbContext, Parameters parameters) - { - 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(parameters.Collection.Id)) - { - await _channel.WriteAsync(new BuildPlayout(playoutId, true)); - } - } - - return Unit.Default; - } - - private static async Task> Validate( - TvContext dbContext, - AddSeasonToCollection request) => - (await CollectionMustExist(dbContext, request), await ValidateSeason(dbContext, request)) - .Apply((collection, episode) => new Parameters(collection, episode)); - - private static Task> CollectionMustExist( - TvContext dbContext, - AddSeasonToCollection request) => - dbContext.Collections - .Include(c => c.MediaItems) - .SelectOneAsync(c => c.Id, c => c.Id == request.CollectionId) - .Map(o => o.ToValidation("Collection does not exist.")); - - private static Task> ValidateSeason( - TvContext dbContext, - AddSeasonToCollection request) => - dbContext.Seasons - .SelectOneAsync(m => m.Id, e => e.Id == request.SeasonId) - .Map(o => o.ToValidation("Season does not exist")); - - private record Parameters(Collection Collection, Season Season); + _dbContextFactory = dbContextFactory; + _mediaCollectionRepository = mediaCollectionRepository; + _channel = channel; } -} + + public async Task> Handle( + AddSeasonToCollection request, + CancellationToken cancellationToken) + { + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + Validation validation = await Validate(dbContext, request); + return await LanguageExtensions.Apply(validation, parameters => ApplyAddSeasonRequest(dbContext, parameters)); + } + + private async Task ApplyAddSeasonRequest(TvContext dbContext, Parameters parameters) + { + 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(parameters.Collection.Id)) + { + await _channel.WriteAsync(new BuildPlayout(playoutId, true)); + } + } + + return Unit.Default; + } + + private static async Task> Validate( + TvContext dbContext, + AddSeasonToCollection request) => + (await CollectionMustExist(dbContext, request), await ValidateSeason(dbContext, request)) + .Apply((collection, episode) => new Parameters(collection, episode)); + + private static Task> CollectionMustExist( + TvContext dbContext, + AddSeasonToCollection request) => + dbContext.Collections + .Include(c => c.MediaItems) + .SelectOneAsync(c => c.Id, c => c.Id == request.CollectionId) + .Map(o => o.ToValidation("Collection does not exist.")); + + private static Task> ValidateSeason( + TvContext dbContext, + AddSeasonToCollection request) => + dbContext.Seasons + .SelectOneAsync(m => m.Id, e => e.Id == request.SeasonId) + .Map(o => o.ToValidation("Season does not exist")); + + private record Parameters(Collection Collection, Season Season); +} \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddShowToCollection.cs b/ErsatzTV.Application/MediaCollections/Commands/AddShowToCollection.cs index 48233e6d4..476e29123 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/AddShowToCollection.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/AddShowToCollection.cs @@ -1,7 +1,5 @@ using ErsatzTV.Core; -using LanguageExt; -namespace ErsatzTV.Application.MediaCollections.Commands -{ - public record AddShowToCollection(int CollectionId, int ShowId) : MediatR.IRequest>; -} +namespace ErsatzTV.Application.MediaCollections; + +public record AddShowToCollection(int CollectionId, int ShowId) : MediatR.IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddShowToCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/AddShowToCollectionHandler.cs index adee60732..a6ea3bfd2 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/AddShowToCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/AddShowToCollectionHandler.cs @@ -1,80 +1,76 @@ -using System.Threading; -using System.Threading.Channels; -using System.Threading.Tasks; -using ErsatzTV.Application.Playouts.Commands; +using System.Threading.Channels; +using ErsatzTV.Application.Playouts; 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 +namespace ErsatzTV.Application.MediaCollections; + +public class AddShowToCollectionHandler : + MediatR.IRequestHandler> { - public class AddShowToCollectionHandler : - MediatR.IRequestHandler> + private readonly ChannelWriter _channel; + private readonly IDbContextFactory _dbContextFactory; + private readonly IMediaCollectionRepository _mediaCollectionRepository; + + public AddShowToCollectionHandler( + IDbContextFactory dbContextFactory, + IMediaCollectionRepository mediaCollectionRepository, + ChannelWriter channel) { - private readonly ChannelWriter _channel; - private readonly IDbContextFactory _dbContextFactory; - private readonly IMediaCollectionRepository _mediaCollectionRepository; - - public AddShowToCollectionHandler( - IDbContextFactory dbContextFactory, - IMediaCollectionRepository mediaCollectionRepository, - ChannelWriter channel) - { - _dbContextFactory = dbContextFactory; - _mediaCollectionRepository = mediaCollectionRepository; - _channel = channel; - } - - public async Task> Handle( - AddShowToCollection request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - Validation validation = await Validate(dbContext, request); - return await validation.Apply(parameters => ApplyAddShowRequest(dbContext, parameters)); - } - - private async Task 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(parameters.Collection.Id)) - { - await _channel.WriteAsync(new BuildPlayout(playoutId, true)); - } - } - - return Unit.Default; - } - - private static async Task> Validate( - TvContext dbContext, - AddShowToCollection request) => - (await CollectionMustExist(dbContext, request), await ValidateShow(dbContext, request)) - .Apply((collection, episode) => new Parameters(collection, episode)); - - private static Task> CollectionMustExist( - TvContext dbContext, - AddShowToCollection request) => - dbContext.Collections - .Include(c => c.MediaItems) - .SelectOneAsync(c => c.Id, c => c.Id == request.CollectionId) - .Map(o => o.ToValidation("Collection does not exist.")); - - private static Task> ValidateShow( - TvContext dbContext, - AddShowToCollection request) => - dbContext.Shows - .SelectOneAsync(m => m.Id, e => e.Id == request.ShowId) - .Map(o => o.ToValidation("Show does not exist")); - - private record Parameters(Collection Collection, Show Show); + _dbContextFactory = dbContextFactory; + _mediaCollectionRepository = mediaCollectionRepository; + _channel = channel; } -} + + public async Task> Handle( + AddShowToCollection request, + CancellationToken cancellationToken) + { + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + Validation validation = await Validate(dbContext, request); + return await LanguageExtensions.Apply(validation, parameters => ApplyAddShowRequest(dbContext, parameters)); + } + + private async Task 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(parameters.Collection.Id)) + { + await _channel.WriteAsync(new BuildPlayout(playoutId, true)); + } + } + + return Unit.Default; + } + + private static async Task> Validate( + TvContext dbContext, + AddShowToCollection request) => + (await CollectionMustExist(dbContext, request), await ValidateShow(dbContext, request)) + .Apply((collection, episode) => new Parameters(collection, episode)); + + private static Task> CollectionMustExist( + TvContext dbContext, + AddShowToCollection request) => + dbContext.Collections + .Include(c => c.MediaItems) + .SelectOneAsync(c => c.Id, c => c.Id == request.CollectionId) + .Map(o => o.ToValidation("Collection does not exist.")); + + private static Task> ValidateShow( + TvContext dbContext, + AddShowToCollection request) => + dbContext.Shows + .SelectOneAsync(m => m.Id, e => e.Id == request.ShowId) + .Map(o => o.ToValidation("Show does not exist")); + + private record Parameters(Collection Collection, Show Show); +} \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddSongToCollection.cs b/ErsatzTV.Application/MediaCollections/Commands/AddSongToCollection.cs index f99ed854b..5bd9c0edf 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/AddSongToCollection.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/AddSongToCollection.cs @@ -1,8 +1,6 @@ using ErsatzTV.Core; -using LanguageExt; -namespace ErsatzTV.Application.MediaCollections.Commands -{ - public record AddSongToCollection - (int CollectionId, int SongId) : MediatR.IRequest>; -} +namespace ErsatzTV.Application.MediaCollections; + +public record AddSongToCollection + (int CollectionId, int SongId) : MediatR.IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddSongToCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/AddSongToCollectionHandler.cs index c1ce55245..69a277e6d 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/AddSongToCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/AddSongToCollectionHandler.cs @@ -1,80 +1,76 @@ -using System.Threading; -using System.Threading.Channels; -using System.Threading.Tasks; -using ErsatzTV.Application.Playouts.Commands; +using System.Threading.Channels; +using ErsatzTV.Application.Playouts; 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 +namespace ErsatzTV.Application.MediaCollections; + +public class AddSongToCollectionHandler : + MediatR.IRequestHandler> { - public class AddSongToCollectionHandler : - MediatR.IRequestHandler> + private readonly ChannelWriter _channel; + private readonly IDbContextFactory _dbContextFactory; + private readonly IMediaCollectionRepository _mediaCollectionRepository; + + public AddSongToCollectionHandler( + IDbContextFactory dbContextFactory, + IMediaCollectionRepository mediaCollectionRepository, + ChannelWriter channel) { - private readonly ChannelWriter _channel; - private readonly IDbContextFactory _dbContextFactory; - private readonly IMediaCollectionRepository _mediaCollectionRepository; - - public AddSongToCollectionHandler( - IDbContextFactory dbContextFactory, - IMediaCollectionRepository mediaCollectionRepository, - ChannelWriter channel) - { - _dbContextFactory = dbContextFactory; - _mediaCollectionRepository = mediaCollectionRepository; - _channel = channel; - } - - public async Task> Handle( - AddSongToCollection request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - Validation validation = await Validate(dbContext, request); - return await validation.Apply(parameters => ApplyAddSongRequest(dbContext, parameters)); - } - - private async Task ApplyAddSongRequest(TvContext dbContext, Parameters parameters) - { - parameters.Collection.MediaItems.Add(parameters.Song); - if (await dbContext.SaveChangesAsync() > 0) - { - // rebuild all playouts that use this collection - foreach (int playoutId in await _mediaCollectionRepository - .PlayoutIdsUsingCollection(parameters.Collection.Id)) - { - await _channel.WriteAsync(new BuildPlayout(playoutId, true)); - } - } - - return Unit.Default; - } - - private static async Task> Validate( - TvContext dbContext, - AddSongToCollection request) => - (await CollectionMustExist(dbContext, request), await ValidateSong(dbContext, request)) - .Apply((collection, episode) => new Parameters(collection, episode)); - - private static Task> CollectionMustExist( - TvContext dbContext, - AddSongToCollection request) => - dbContext.Collections - .Include(c => c.MediaItems) - .SelectOneAsync(c => c.Id, c => c.Id == request.CollectionId) - .Map(o => o.ToValidation("Collection does not exist.")); - - private static Task> ValidateSong( - TvContext dbContext, - AddSongToCollection request) => - dbContext.Songs - .SelectOneAsync(m => m.Id, e => e.Id == request.SongId) - .Map(o => o.ToValidation("Song does not exist")); - - private record Parameters(Collection Collection, Song Song); + _dbContextFactory = dbContextFactory; + _mediaCollectionRepository = mediaCollectionRepository; + _channel = channel; } -} + + public async Task> Handle( + AddSongToCollection request, + CancellationToken cancellationToken) + { + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + Validation validation = await Validate(dbContext, request); + return await LanguageExtensions.Apply(validation, parameters => ApplyAddSongRequest(dbContext, parameters)); + } + + private async Task ApplyAddSongRequest(TvContext dbContext, Parameters parameters) + { + parameters.Collection.MediaItems.Add(parameters.Song); + if (await dbContext.SaveChangesAsync() > 0) + { + // rebuild all playouts that use this collection + foreach (int playoutId in await _mediaCollectionRepository + .PlayoutIdsUsingCollection(parameters.Collection.Id)) + { + await _channel.WriteAsync(new BuildPlayout(playoutId, true)); + } + } + + return Unit.Default; + } + + private static async Task> Validate( + TvContext dbContext, + AddSongToCollection request) => + (await CollectionMustExist(dbContext, request), await ValidateSong(dbContext, request)) + .Apply((collection, episode) => new Parameters(collection, episode)); + + private static Task> CollectionMustExist( + TvContext dbContext, + AddSongToCollection request) => + dbContext.Collections + .Include(c => c.MediaItems) + .SelectOneAsync(c => c.Id, c => c.Id == request.CollectionId) + .Map(o => o.ToValidation("Collection does not exist.")); + + private static Task> ValidateSong( + TvContext dbContext, + AddSongToCollection request) => + dbContext.Songs + .SelectOneAsync(m => m.Id, e => e.Id == request.SongId) + .Map(o => o.ToValidation("Song does not exist")); + + private record Parameters(Collection Collection, Song Song); +} \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddTraktList.cs b/ErsatzTV.Application/MediaCollections/Commands/AddTraktList.cs index 75657ea3d..9568409fe 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/AddTraktList.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/AddTraktList.cs @@ -1,9 +1,5 @@ using ErsatzTV.Core; -using LanguageExt; -using MediatR; -using Unit = LanguageExt.Unit; -namespace ErsatzTV.Application.MediaCollections.Commands -{ - public record AddTraktList(string TraktListUrl) : IRequest>, IBackgroundServiceRequest; -} +namespace ErsatzTV.Application.MediaCollections; + +public record AddTraktList(string TraktListUrl) : IRequest>, IBackgroundServiceRequest; \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddTraktListHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/AddTraktListHandler.cs index b89e801cf..dfdbef63b 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/AddTraktListHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/AddTraktListHandler.cs @@ -1,80 +1,74 @@ using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; using ErsatzTV.Core; using ErsatzTV.Core.Interfaces.Locking; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Interfaces.Trakt; using ErsatzTV.Infrastructure.Data; -using LanguageExt; -using MediatR; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using Unit = LanguageExt.Unit; -namespace ErsatzTV.Application.MediaCollections.Commands +namespace ErsatzTV.Application.MediaCollections; + +public class AddTraktListHandler : TraktCommandBase, IRequestHandler> { - public class AddTraktListHandler : TraktCommandBase, IRequestHandler> + private readonly IDbContextFactory _dbContextFactory; + private readonly IEntityLocker _entityLocker; + + public AddTraktListHandler( + ITraktApiClient traktApiClient, + ISearchRepository searchRepository, + ISearchIndex searchIndex, + IDbContextFactory dbContextFactory, + ILogger logger, + IEntityLocker entityLocker) + : base(traktApiClient, searchRepository, searchIndex, logger) { - private readonly IDbContextFactory _dbContextFactory; - private readonly IEntityLocker _entityLocker; - - public AddTraktListHandler( - ITraktApiClient traktApiClient, - ISearchRepository searchRepository, - ISearchIndex searchIndex, - IDbContextFactory dbContextFactory, - ILogger logger, - IEntityLocker entityLocker) - : base(traktApiClient, searchRepository, searchIndex, logger) - { - _dbContextFactory = dbContextFactory; - _entityLocker = entityLocker; - } - - public async Task> Handle(AddTraktList request, CancellationToken cancellationToken) - { - try - { - Validation validation = ValidateUrl(request); - return await validation.Match( - DoAdd, - error => Task.FromResult>(error.Join())); - } - finally - { - _entityLocker.UnlockTrakt(); - } - } - - private static Validation ValidateUrl(AddTraktList request) - { - const string PATTERN = @"(?:https:\/\/trakt\.tv\/users\/)?([\w\-_]+)\/(?:lists\/)?([\w\-_]+)"; - Match match = Regex.Match(request.TraktListUrl, PATTERN); - if (match.Success) - { - string user = match.Groups[1].Value; - string list = match.Groups[2].Value; - return new Parameters(user, list); - } - - return BaseError.New("Invalid Trakt list url"); - } - - private async Task> DoAdd(Parameters parameters) - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - - return await TraktApiClient.GetUserList(parameters.User, parameters.List) - .BindT(list => SaveList(dbContext, list)) - .BindT(list => SaveListItems(dbContext, list)) - .BindT(list => MatchListItems(dbContext, list)) - .MapT(_ => Unit.Default); - - // match list items (and update in search index) - } - - private record Parameters(string User, string List); + _dbContextFactory = dbContextFactory; + _entityLocker = entityLocker; } -} + + public async Task> Handle(AddTraktList request, CancellationToken cancellationToken) + { + try + { + Validation validation = ValidateUrl(request); + return await validation.Match( + DoAdd, + error => Task.FromResult>(error.Join())); + } + finally + { + _entityLocker.UnlockTrakt(); + } + } + + private static Validation ValidateUrl(AddTraktList request) + { + const string PATTERN = @"(?:https:\/\/trakt\.tv\/users\/)?([\w\-_]+)\/(?:lists\/)?([\w\-_]+)"; + Match match = Regex.Match(request.TraktListUrl, PATTERN); + if (match.Success) + { + string user = match.Groups[1].Value; + string list = match.Groups[2].Value; + return new Parameters(user, list); + } + + return BaseError.New("Invalid Trakt list url"); + } + + private async Task> DoAdd(Parameters parameters) + { + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + + return await TraktApiClient.GetUserList(parameters.User, parameters.List) + .BindT(list => SaveList(dbContext, list)) + .BindT(list => SaveListItems(dbContext, list)) + .BindT(list => MatchListItems(dbContext, list)) + .MapT(_ => Unit.Default); + + // match list items (and update in search index) + } + + private record Parameters(string User, string List); +} \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Commands/CreateCollection.cs b/ErsatzTV.Application/MediaCollections/Commands/CreateCollection.cs index b4450b085..2fdbd8807 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/CreateCollection.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/CreateCollection.cs @@ -1,8 +1,5 @@ using ErsatzTV.Core; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.MediaCollections.Commands -{ - public record CreateCollection(string Name) : IRequest>; -} +namespace ErsatzTV.Application.MediaCollections; + +public record CreateCollection(string Name) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Commands/CreateCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/CreateCollectionHandler.cs index e64068714..6ec7d3070 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/CreateCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/CreateCollectionHandler.cs @@ -1,69 +1,62 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; 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 +namespace ErsatzTV.Application.MediaCollections; + +public class CreateCollectionHandler : + IRequestHandler> { - public class CreateCollectionHandler : - IRequestHandler> + private readonly IDbContextFactory _dbContextFactory; + + public CreateCollectionHandler(IDbContextFactory dbContextFactory) => + _dbContextFactory = dbContextFactory; + + public async Task> Handle( + CreateCollection request, + CancellationToken cancellationToken) { - private readonly IDbContextFactory _dbContextFactory; - - public CreateCollectionHandler(IDbContextFactory dbContextFactory) => - _dbContextFactory = dbContextFactory; - - public async Task> Handle( - CreateCollection request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - Validation validation = await Validate(dbContext, request); - return await validation.Apply(c => PersistCollection(dbContext, c)); - } - - private static async Task PersistCollection( - TvContext dbContext, - Collection collection) - { - await dbContext.Collections.AddAsync(collection); - await dbContext.SaveChangesAsync(); - return ProjectToViewModel(collection); - } - - private static Task> Validate( - TvContext dbContext, - CreateCollection request) => - ValidateName(dbContext, request).MapT( - name => new Collection - { - Name = name, - MediaItems = new List() - }); - - private static async Task> ValidateName( - TvContext dbContext, - CreateCollection createCollection) - { - List allNames = await dbContext.Collections - .Map(c => c.Name) - .ToListAsync(); - - Validation result1 = createCollection.NotEmpty(c => c.Name) - .Bind(_ => createCollection.NotLongerThan(50)(c => c.Name)); - - var result2 = Optional(createCollection.Name) - .Where(name => !allNames.Contains(name)) - .ToValidation("Collection name must be unique"); - - return (result1, result2).Apply((_, _) => createCollection.Name); - } + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + Validation validation = await Validate(dbContext, request); + return await LanguageExtensions.Apply(validation, c => PersistCollection(dbContext, c)); } -} + + private static async Task PersistCollection( + TvContext dbContext, + Collection collection) + { + await dbContext.Collections.AddAsync(collection); + await dbContext.SaveChangesAsync(); + return ProjectToViewModel(collection); + } + + private static Task> Validate( + TvContext dbContext, + CreateCollection request) => + ValidateName(dbContext, request).MapT( + name => new Collection + { + Name = name, + MediaItems = new List() + }); + + private static async Task> ValidateName( + TvContext dbContext, + CreateCollection createCollection) + { + List allNames = await dbContext.Collections + .Map(c => c.Name) + .ToListAsync(); + + Validation result1 = createCollection.NotEmpty(c => c.Name) + .Bind(_ => createCollection.NotLongerThan(50)(c => c.Name)); + + var result2 = Optional(createCollection.Name) + .Where(name => !allNames.Contains(name)) + .ToValidation("Collection name must be unique"); + + return (result1, result2).Apply((_, _) => createCollection.Name); + } +} \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Commands/CreateMultiCollection.cs b/ErsatzTV.Application/MediaCollections/Commands/CreateMultiCollection.cs index b1cba7947..aa094dcff 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/CreateMultiCollection.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/CreateMultiCollection.cs @@ -1,13 +1,9 @@ -using System.Collections.Generic; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.MediaCollections.Commands -{ - public record CreateMultiCollectionItem(int? CollectionId, int? SmartCollectionId, bool ScheduleAsGroup, PlaybackOrder PlaybackOrder); +namespace ErsatzTV.Application.MediaCollections; - public record CreateMultiCollection - (string Name, List Items) : IRequest>; -} +public record CreateMultiCollectionItem(int? CollectionId, int? SmartCollectionId, bool ScheduleAsGroup, PlaybackOrder PlaybackOrder); + +public record CreateMultiCollection + (string Name, List Items) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Commands/CreateMultiCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/CreateMultiCollectionHandler.cs index 4b8a8c81f..054c381b3 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/CreateMultiCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/CreateMultiCollectionHandler.cs @@ -1,113 +1,105 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; 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 +namespace ErsatzTV.Application.MediaCollections; + +public class CreateMultiCollectionHandler : + IRequestHandler> { - public class CreateMultiCollectionHandler : - IRequestHandler> + private readonly IDbContextFactory _dbContextFactory; + + public CreateMultiCollectionHandler(IDbContextFactory dbContextFactory) => + _dbContextFactory = dbContextFactory; + + public async Task> Handle( + CreateMultiCollection request, + CancellationToken cancellationToken) { - private readonly IDbContextFactory _dbContextFactory; - - public CreateMultiCollectionHandler(IDbContextFactory dbContextFactory) => - _dbContextFactory = dbContextFactory; - - public async Task> Handle( - CreateMultiCollection request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - Validation validation = await Validate(dbContext, request); - return await validation.Apply(c => PersistCollection(dbContext, c)); - } - - private static async Task PersistCollection( - TvContext dbContext, - MultiCollection multiCollection) - { - await dbContext.MultiCollections.AddAsync(multiCollection); - await dbContext.SaveChangesAsync(); - await dbContext.Entry(multiCollection) - .Collection(c => c.MultiCollectionItems) - .Query() - .Include(i => i.Collection) - .LoadAsync(); - await dbContext.Entry(multiCollection) - .Collection(c => c.MultiCollectionSmartItems) - .Query() - .Include(i => i.SmartCollection) - .LoadAsync(); - return ProjectToViewModel(multiCollection); - } - - private static Task> Validate( - TvContext dbContext, - CreateMultiCollection request) => - ValidateName(dbContext, request).MapT( - name => new MultiCollection - { - Name = name, - MultiCollectionItems = request.Items.Bind( - i => - { - if (i.CollectionId.HasValue) - { - return Some( - new MultiCollectionItem - { - CollectionId = i.CollectionId.Value, - ScheduleAsGroup = i.ScheduleAsGroup, - PlaybackOrder = i.PlaybackOrder - }); - } - - return Option.None; - }) - .ToList(), - MultiCollectionSmartItems = request.Items.Bind( - i => - { - if (i.SmartCollectionId.HasValue) - { - return Some( - new MultiCollectionSmartItem - { - SmartCollectionId = i.SmartCollectionId.Value, - ScheduleAsGroup = i.ScheduleAsGroup, - PlaybackOrder = i.PlaybackOrder - }); - } - - return Option.None; - }) - .ToList() - }); - - private static async Task> ValidateName( - TvContext dbContext, - CreateMultiCollection createMultiCollection) - { - List allNames = await dbContext.MultiCollections - .Map(c => c.Name) - .ToListAsync(); - - Validation result1 = createMultiCollection.NotEmpty(c => c.Name) - .Bind(_ => createMultiCollection.NotLongerThan(50)(c => c.Name)); - - var result2 = Optional(createMultiCollection.Name) - .Where(name => !allNames.Contains(name)) - .ToValidation("MultiCollection name must be unique"); - - return (result1, result2).Apply((_, _) => createMultiCollection.Name); - } + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + Validation validation = await Validate(dbContext, request); + return await LanguageExtensions.Apply(validation, c => PersistCollection(dbContext, c)); } -} + + private static async Task PersistCollection( + TvContext dbContext, + MultiCollection multiCollection) + { + await dbContext.MultiCollections.AddAsync(multiCollection); + await dbContext.SaveChangesAsync(); + await dbContext.Entry(multiCollection) + .Collection(c => c.MultiCollectionItems) + .Query() + .Include(i => i.Collection) + .LoadAsync(); + await dbContext.Entry(multiCollection) + .Collection(c => c.MultiCollectionSmartItems) + .Query() + .Include(i => i.SmartCollection) + .LoadAsync(); + return ProjectToViewModel(multiCollection); + } + + private static Task> Validate( + TvContext dbContext, + CreateMultiCollection request) => + ValidateName(dbContext, request).MapT( + name => new MultiCollection + { + Name = name, + MultiCollectionItems = request.Items.Bind( + i => + { + if (i.CollectionId.HasValue) + { + return Some( + new MultiCollectionItem + { + CollectionId = i.CollectionId.Value, + ScheduleAsGroup = i.ScheduleAsGroup, + PlaybackOrder = i.PlaybackOrder + }); + } + + return Option.None; + }) + .ToList(), + MultiCollectionSmartItems = request.Items.Bind( + i => + { + if (i.SmartCollectionId.HasValue) + { + return Some( + new MultiCollectionSmartItem + { + SmartCollectionId = i.SmartCollectionId.Value, + ScheduleAsGroup = i.ScheduleAsGroup, + PlaybackOrder = i.PlaybackOrder + }); + } + + return Option.None; + }) + .ToList() + }); + + private static async Task> ValidateName( + TvContext dbContext, + CreateMultiCollection createMultiCollection) + { + List allNames = await dbContext.MultiCollections + .Map(c => c.Name) + .ToListAsync(); + + Validation result1 = createMultiCollection.NotEmpty(c => c.Name) + .Bind(_ => createMultiCollection.NotLongerThan(50)(c => c.Name)); + + var result2 = Optional(createMultiCollection.Name) + .Where(name => !allNames.Contains(name)) + .ToValidation("MultiCollection name must be unique"); + + return (result1, result2).Apply((_, _) => createMultiCollection.Name); + } +} \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Commands/CreateSmartCollection.cs b/ErsatzTV.Application/MediaCollections/Commands/CreateSmartCollection.cs index 9ef5d956b..2a4977bcf 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/CreateSmartCollection.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/CreateSmartCollection.cs @@ -1,9 +1,6 @@ using ErsatzTV.Core; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.MediaCollections.Commands -{ - public record CreateSmartCollection - (string Query, string Name) : IRequest>; -} +namespace ErsatzTV.Application.MediaCollections; + +public record CreateSmartCollection + (string Query, string Name) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Commands/CreateSmartCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/CreateSmartCollectionHandler.cs index 5177d6e69..255bc5e65 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/CreateSmartCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/CreateSmartCollectionHandler.cs @@ -1,70 +1,62 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; 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 +namespace ErsatzTV.Application.MediaCollections; + +public class CreateSmartCollectionHandler : + IRequestHandler> { - public class CreateSmartCollectionHandler : - IRequestHandler> + private readonly IDbContextFactory _dbContextFactory; + + public CreateSmartCollectionHandler(IDbContextFactory dbContextFactory) => + _dbContextFactory = dbContextFactory; + + public async Task> Handle( + CreateSmartCollection request, + CancellationToken cancellationToken) { - private readonly IDbContextFactory _dbContextFactory; - - public CreateSmartCollectionHandler(IDbContextFactory dbContextFactory) => - _dbContextFactory = dbContextFactory; - - public async Task> Handle( - CreateSmartCollection request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - Validation validation = await Validate(dbContext, request); - return await validation.Apply(c => PersistCollection(dbContext, c)); - } - - private static async Task PersistCollection( - TvContext dbContext, - SmartCollection smartCollection) - { - await dbContext.SmartCollections.AddAsync(smartCollection); - await dbContext.SaveChangesAsync(); - return ProjectToViewModel(smartCollection); - } - - private static Task> Validate( - TvContext dbContext, - CreateSmartCollection request) => - ValidateName(dbContext, request).MapT( - name => new SmartCollection - { - Name = name, - Query = request.Query - }); - - private static async Task> ValidateName( - TvContext dbContext, - CreateSmartCollection createSmartCollection) - { - List allNames = await dbContext.SmartCollections - .Map(c => c.Name) - .ToListAsync(); - - Validation result1 = createSmartCollection.NotEmpty(c => c.Name) - .Bind(_ => createSmartCollection.NotLongerThan(50)(c => c.Name)); - - var result2 = Optional(createSmartCollection.Name) - .Where(name => !allNames.Contains(name)) - .ToValidation("SmartCollection name must be unique"); - - return (result1, result2).Apply((_, _) => createSmartCollection.Name); - } + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + Validation validation = await Validate(dbContext, request); + return await LanguageExtensions.Apply(validation, c => PersistCollection(dbContext, c)); } -} + + private static async Task PersistCollection( + TvContext dbContext, + SmartCollection smartCollection) + { + await dbContext.SmartCollections.AddAsync(smartCollection); + await dbContext.SaveChangesAsync(); + return ProjectToViewModel(smartCollection); + } + + private static Task> Validate( + TvContext dbContext, + CreateSmartCollection request) => + ValidateName(dbContext, request).MapT( + name => new SmartCollection + { + Name = name, + Query = request.Query + }); + + private static async Task> ValidateName( + TvContext dbContext, + CreateSmartCollection createSmartCollection) + { + List allNames = await dbContext.SmartCollections + .Map(c => c.Name) + .ToListAsync(); + + Validation result1 = createSmartCollection.NotEmpty(c => c.Name) + .Bind(_ => createSmartCollection.NotLongerThan(50)(c => c.Name)); + + var result2 = Optional(createSmartCollection.Name) + .Where(name => !allNames.Contains(name)) + .ToValidation("SmartCollection name must be unique"); + + return (result1, result2).Apply((_, _) => createSmartCollection.Name); + } +} \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Commands/DeleteCollection.cs b/ErsatzTV.Application/MediaCollections/Commands/DeleteCollection.cs index fb0b8f3c1..55ddc1ab4 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/DeleteCollection.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/DeleteCollection.cs @@ -1,8 +1,5 @@ using ErsatzTV.Core; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.MediaCollections.Commands -{ - public record DeleteCollection(int CollectionId) : IRequest>; -} +namespace ErsatzTV.Application.MediaCollections; + +public record DeleteCollection(int CollectionId) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Commands/DeleteCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/DeleteCollectionHandler.cs index 00f53d3aa..530fd0746 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/DeleteCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/DeleteCollectionHandler.cs @@ -1,42 +1,38 @@ -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; -using LanguageExt; using Microsoft.EntityFrameworkCore; -namespace ErsatzTV.Application.MediaCollections.Commands +namespace ErsatzTV.Application.MediaCollections; + +public class DeleteCollectionHandler : MediatR.IRequestHandler> { - public class DeleteCollectionHandler : MediatR.IRequestHandler> + private readonly IDbContextFactory _dbContextFactory; + + public DeleteCollectionHandler(IDbContextFactory dbContextFactory) => + _dbContextFactory = dbContextFactory; + + public async Task> Handle( + DeleteCollection request, + CancellationToken cancellationToken) { - private readonly IDbContextFactory _dbContextFactory; + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - public DeleteCollectionHandler(IDbContextFactory dbContextFactory) => - _dbContextFactory = dbContextFactory; - - public async Task> Handle( - DeleteCollection request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - - Validation validation = await CollectionMustExist(dbContext, request); - return await validation.Apply(c => DoDeletion(dbContext, c)); - } - - private static Task DoDeletion(TvContext dbContext, Collection collection) - { - dbContext.Collections.Remove(collection); - return dbContext.SaveChangesAsync().ToUnit(); - } - - private static Task> CollectionMustExist( - TvContext dbContext, - DeleteCollection request) => - dbContext.Collections - .SelectOneAsync(c => c.Id, c => c.Id == request.CollectionId) - .Map(o => o.ToValidation($"Collection {request.CollectionId} does not exist.")); + Validation validation = await CollectionMustExist(dbContext, request); + return await LanguageExtensions.Apply(validation, c => DoDeletion(dbContext, c)); } -} + + private static Task DoDeletion(TvContext dbContext, Collection collection) + { + dbContext.Collections.Remove(collection); + return dbContext.SaveChangesAsync().ToUnit(); + } + + private static Task> CollectionMustExist( + TvContext dbContext, + DeleteCollection request) => + dbContext.Collections + .SelectOneAsync(c => c.Id, c => c.Id == request.CollectionId) + .Map(o => o.ToValidation($"Collection {request.CollectionId} does not exist.")); +} \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Commands/DeleteMultiCollection.cs b/ErsatzTV.Application/MediaCollections/Commands/DeleteMultiCollection.cs index 52e4f098d..886da8b45 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/DeleteMultiCollection.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/DeleteMultiCollection.cs @@ -1,7 +1,5 @@ using ErsatzTV.Core; -using LanguageExt; -namespace ErsatzTV.Application.MediaCollections.Commands -{ - public record DeleteMultiCollection(int MultiCollectionId) : MediatR.IRequest>; -} +namespace ErsatzTV.Application.MediaCollections; + +public record DeleteMultiCollection(int MultiCollectionId) : MediatR.IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Commands/DeleteMultiCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/DeleteMultiCollectionHandler.cs index 14b1d2fe7..5f37670a7 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/DeleteMultiCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/DeleteMultiCollectionHandler.cs @@ -1,42 +1,38 @@ -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; -using LanguageExt; using Microsoft.EntityFrameworkCore; -namespace ErsatzTV.Application.MediaCollections.Commands +namespace ErsatzTV.Application.MediaCollections; + +public class DeleteMultiCollectionHandler : MediatR.IRequestHandler> { - public class DeleteMultiCollectionHandler : MediatR.IRequestHandler> + private readonly IDbContextFactory _dbContextFactory; + + public DeleteMultiCollectionHandler(IDbContextFactory dbContextFactory) => + _dbContextFactory = dbContextFactory; + + public async Task> Handle( + DeleteMultiCollection request, + CancellationToken cancellationToken) { - private readonly IDbContextFactory _dbContextFactory; + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - public DeleteMultiCollectionHandler(IDbContextFactory dbContextFactory) => - _dbContextFactory = dbContextFactory; - - public async Task> Handle( - DeleteMultiCollection request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - - Validation validation = await MultiCollectionMustExist(dbContext, request); - return await validation.Apply(c => DoDeletion(dbContext, c)); - } - - private static Task DoDeletion(TvContext dbContext, MultiCollection multiCollection) - { - dbContext.MultiCollections.Remove(multiCollection); - return dbContext.SaveChangesAsync().ToUnit(); - } - - private static Task> MultiCollectionMustExist( - TvContext dbContext, - DeleteMultiCollection request) => - dbContext.MultiCollections - .SelectOneAsync(c => c.Id, c => c.Id == request.MultiCollectionId) - .Map(o => o.ToValidation($"MultiCollection {request.MultiCollectionId} does not exist.")); + Validation validation = await MultiCollectionMustExist(dbContext, request); + return await LanguageExtensions.Apply(validation, c => DoDeletion(dbContext, c)); } -} + + private static Task DoDeletion(TvContext dbContext, MultiCollection multiCollection) + { + dbContext.MultiCollections.Remove(multiCollection); + return dbContext.SaveChangesAsync().ToUnit(); + } + + private static Task> MultiCollectionMustExist( + TvContext dbContext, + DeleteMultiCollection request) => + dbContext.MultiCollections + .SelectOneAsync(c => c.Id, c => c.Id == request.MultiCollectionId) + .Map(o => o.ToValidation($"MultiCollection {request.MultiCollectionId} does not exist.")); +} \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Commands/DeleteSmartCollection.cs b/ErsatzTV.Application/MediaCollections/Commands/DeleteSmartCollection.cs index 7fe1c6ffb..02d9e3277 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/DeleteSmartCollection.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/DeleteSmartCollection.cs @@ -1,7 +1,5 @@ using ErsatzTV.Core; -using LanguageExt; -namespace ErsatzTV.Application.MediaCollections.Commands -{ - public record DeleteSmartCollection(int SmartCollectionId) : MediatR.IRequest>; -} +namespace ErsatzTV.Application.MediaCollections; + +public record DeleteSmartCollection(int SmartCollectionId) : MediatR.IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Commands/DeleteSmartCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/DeleteSmartCollectionHandler.cs index 7036237ff..216517728 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/DeleteSmartCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/DeleteSmartCollectionHandler.cs @@ -1,42 +1,38 @@ -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; -using LanguageExt; using Microsoft.EntityFrameworkCore; -namespace ErsatzTV.Application.MediaCollections.Commands +namespace ErsatzTV.Application.MediaCollections; + +public class DeleteSmartCollectionHandler : MediatR.IRequestHandler> { - public class DeleteSmartCollectionHandler : MediatR.IRequestHandler> + private readonly IDbContextFactory _dbContextFactory; + + public DeleteSmartCollectionHandler(IDbContextFactory dbContextFactory) => + _dbContextFactory = dbContextFactory; + + public async Task> Handle( + DeleteSmartCollection request, + CancellationToken cancellationToken) { - private readonly IDbContextFactory _dbContextFactory; + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - public DeleteSmartCollectionHandler(IDbContextFactory dbContextFactory) => - _dbContextFactory = dbContextFactory; - - public async Task> Handle( - DeleteSmartCollection request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - - Validation validation = await SmartCollectionMustExist(dbContext, request); - return await validation.Apply(c => DoDeletion(dbContext, c)); - } - - private static Task DoDeletion(TvContext dbContext, SmartCollection smartCollection) - { - dbContext.SmartCollections.Remove(smartCollection); - return dbContext.SaveChangesAsync().ToUnit(); - } - - private static Task> SmartCollectionMustExist( - TvContext dbContext, - DeleteSmartCollection request) => - dbContext.SmartCollections - .SelectOneAsync(c => c.Id, c => c.Id == request.SmartCollectionId) - .Map(o => o.ToValidation($"SmartCollection {request.SmartCollectionId} does not exist.")); + Validation validation = await SmartCollectionMustExist(dbContext, request); + return await LanguageExtensions.Apply(validation, c => DoDeletion(dbContext, c)); } -} + + private static Task DoDeletion(TvContext dbContext, SmartCollection smartCollection) + { + dbContext.SmartCollections.Remove(smartCollection); + return dbContext.SaveChangesAsync().ToUnit(); + } + + private static Task> SmartCollectionMustExist( + TvContext dbContext, + DeleteSmartCollection request) => + dbContext.SmartCollections + .SelectOneAsync(c => c.Id, c => c.Id == request.SmartCollectionId) + .Map(o => o.ToValidation($"SmartCollection {request.SmartCollectionId} does not exist.")); +} \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Commands/DeleteTraktList.cs b/ErsatzTV.Application/MediaCollections/Commands/DeleteTraktList.cs index 94724e92c..47f38ec81 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/DeleteTraktList.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/DeleteTraktList.cs @@ -1,9 +1,6 @@ using ErsatzTV.Core; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.MediaCollections.Commands -{ - public record DeleteTraktList(int TraktListId) : IRequest>, - IBackgroundServiceRequest; -} +namespace ErsatzTV.Application.MediaCollections; + +public record DeleteTraktList(int TraktListId) : IRequest>, + IBackgroundServiceRequest; \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Commands/DeleteTraktListHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/DeleteTraktListHandler.cs index 5fa7b99db..4f66001e2 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/DeleteTraktListHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/DeleteTraktListHandler.cs @@ -1,79 +1,72 @@ -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Locking; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Interfaces.Trakt; using ErsatzTV.Infrastructure.Data; -using ErsatzTV.Infrastructure.Extensions; -using LanguageExt; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using static LanguageExt.Prelude; -namespace ErsatzTV.Application.MediaCollections.Commands +namespace ErsatzTV.Application.MediaCollections; + +public class DeleteTraktListHandler : TraktCommandBase, MediatR.IRequestHandler> { - public class DeleteTraktListHandler : TraktCommandBase, MediatR.IRequestHandler> + private readonly ISearchRepository _searchRepository; + private readonly ISearchIndex _searchIndex; + private readonly IDbContextFactory _dbContextFactory; + private readonly IEntityLocker _entityLocker; + + public DeleteTraktListHandler( + ITraktApiClient traktApiClient, + ISearchRepository searchRepository, + ISearchIndex searchIndex, + IDbContextFactory dbContextFactory, + ILogger logger, + IEntityLocker entityLocker) + : base(traktApiClient, searchRepository, searchIndex, logger) { - private readonly ISearchRepository _searchRepository; - private readonly ISearchIndex _searchIndex; - private readonly IDbContextFactory _dbContextFactory; - private readonly IEntityLocker _entityLocker; + _searchRepository = searchRepository; + _searchIndex = searchIndex; + _dbContextFactory = dbContextFactory; + _entityLocker = entityLocker; + } - public DeleteTraktListHandler( - ITraktApiClient traktApiClient, - ISearchRepository searchRepository, - ISearchIndex searchIndex, - IDbContextFactory dbContextFactory, - ILogger logger, - IEntityLocker entityLocker) - : base(traktApiClient, searchRepository, searchIndex, logger) + public async Task> Handle( + DeleteTraktList request, + CancellationToken cancellationToken) + { + try { - _searchRepository = searchRepository; - _searchIndex = searchIndex; - _dbContextFactory = dbContextFactory; - _entityLocker = entityLocker; + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + + Validation validation = await TraktListMustExist(dbContext, request.TraktListId); + return await LanguageExtensions.Apply(validation, c => DoDeletion(dbContext, c)); } - - public async Task> Handle( - DeleteTraktList request, - CancellationToken cancellationToken) + finally { - try - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - - Validation validation = await TraktListMustExist(dbContext, request.TraktListId); - return await validation.Apply(c => DoDeletion(dbContext, c)); - } - finally - { - _entityLocker.UnlockTrakt(); - } - } - - private async Task DoDeletion(TvContext dbContext, TraktList traktList) - { - var mediaItemIds = traktList.Items.Bind(i => Optional(i.MediaItemId)).ToList(); - - dbContext.TraktLists.Remove(traktList); - if (await dbContext.SaveChangesAsync() > 0) - { - foreach (int mediaItemId in mediaItemIds) - { - foreach (MediaItem mediaItem in await _searchRepository.GetItemToIndex(mediaItemId)) - { - await _searchIndex.UpdateItems(_searchRepository, new[] { mediaItem }.ToList()); - } - } - } - - _searchIndex.Commit(); - - return Unit.Default; + _entityLocker.UnlockTrakt(); } } -} + + private async Task DoDeletion(TvContext dbContext, TraktList traktList) + { + var mediaItemIds = traktList.Items.Bind(i => Optional(i.MediaItemId)).ToList(); + + dbContext.TraktLists.Remove(traktList); + if (await dbContext.SaveChangesAsync() > 0) + { + foreach (int mediaItemId in mediaItemIds) + { + foreach (MediaItem mediaItem in await _searchRepository.GetItemToIndex(mediaItemId)) + { + await _searchIndex.UpdateItems(_searchRepository, new[] { mediaItem }.ToList()); + } + } + } + + _searchIndex.Commit(); + + return Unit.Default; + } +} \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Commands/MatchTraktListItems.cs b/ErsatzTV.Application/MediaCollections/Commands/MatchTraktListItems.cs index 34c06c913..e651129b4 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/MatchTraktListItems.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/MatchTraktListItems.cs @@ -1,10 +1,6 @@ using ErsatzTV.Core; -using LanguageExt; -using MediatR; -using Unit = LanguageExt.Unit; -namespace ErsatzTV.Application.MediaCollections.Commands -{ - public record MatchTraktListItems(int TraktListId, bool Unlock = true) : IRequest>, - IBackgroundServiceRequest; -} +namespace ErsatzTV.Application.MediaCollections; + +public record MatchTraktListItems(int TraktListId, bool Unlock = true) : IRequest>, + IBackgroundServiceRequest; \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Commands/MatchTraktListItemsHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/MatchTraktListItemsHandler.cs index d9251e202..cb0fb2d32 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/MatchTraktListItemsHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/MatchTraktListItemsHandler.cs @@ -1,58 +1,52 @@ -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Locking; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Interfaces.Trakt; using ErsatzTV.Infrastructure.Data; -using LanguageExt; -using MediatR; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using Unit = LanguageExt.Unit; -namespace ErsatzTV.Application.MediaCollections.Commands +namespace ErsatzTV.Application.MediaCollections; + +public class MatchTraktListItemsHandler : TraktCommandBase, + IRequestHandler> { - public class MatchTraktListItemsHandler : TraktCommandBase, - IRequestHandler> + private readonly IDbContextFactory _dbContextFactory; + private readonly IEntityLocker _entityLocker; + + public MatchTraktListItemsHandler( + ITraktApiClient traktApiClient, + ISearchRepository searchRepository, + ISearchIndex searchIndex, + IDbContextFactory dbContextFactory, + ILogger logger, + IEntityLocker entityLocker) : base(traktApiClient, searchRepository, searchIndex, logger) { - private readonly IDbContextFactory _dbContextFactory; - private readonly IEntityLocker _entityLocker; + _dbContextFactory = dbContextFactory; + _entityLocker = entityLocker; + } - public MatchTraktListItemsHandler( - ITraktApiClient traktApiClient, - ISearchRepository searchRepository, - ISearchIndex searchIndex, - IDbContextFactory dbContextFactory, - ILogger logger, - IEntityLocker entityLocker) : base(traktApiClient, searchRepository, searchIndex, logger) + public async Task> Handle( + MatchTraktListItems request, + CancellationToken cancellationToken) + { + try { - _dbContextFactory = dbContextFactory; - _entityLocker = entityLocker; + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + + Validation validation = await TraktListMustExist(dbContext, request.TraktListId); + return await validation.Match( + async l => await MatchListItems(dbContext, l).MapT(_ => Unit.Default), + error => Task.FromResult>(error.Join())); } - - public async Task> Handle( - MatchTraktListItems request, - CancellationToken cancellationToken) + finally { - try + if (request.Unlock) { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - - Validation validation = await TraktListMustExist(dbContext, request.TraktListId); - return await validation.Match( - async l => await MatchListItems(dbContext, l).MapT(_ => Unit.Default), - error => Task.FromResult>(error.Join())); - } - finally - { - if (request.Unlock) - { - _entityLocker.UnlockTrakt(); - } + _entityLocker.UnlockTrakt(); } } } -} +} \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Commands/RemoveItemsFromCollection.cs b/ErsatzTV.Application/MediaCollections/Commands/RemoveItemsFromCollection.cs index 29aaa6703..1f949b695 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/RemoveItemsFromCollection.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/RemoveItemsFromCollection.cs @@ -1,11 +1,8 @@ -using System.Collections.Generic; -using ErsatzTV.Core; -using LanguageExt; +using ErsatzTV.Core; -namespace ErsatzTV.Application.MediaCollections.Commands +namespace ErsatzTV.Application.MediaCollections; + +public record RemoveItemsFromCollection(int MediaCollectionId) : MediatR.IRequest> { - public record RemoveItemsFromCollection(int MediaCollectionId) : MediatR.IRequest> - { - public List MediaItemIds { get; set; } = new(); - } -} + public List MediaItemIds { get; set; } = new(); +} \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Commands/RemoveItemsFromCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/RemoveItemsFromCollectionHandler.cs index 4e932a0f6..2c4537755 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/RemoveItemsFromCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/RemoveItemsFromCollectionHandler.cs @@ -1,78 +1,73 @@ -using System.Linq; -using System.Threading; -using System.Threading.Channels; -using System.Threading.Tasks; -using ErsatzTV.Application.Playouts.Commands; +using System.Threading.Channels; +using ErsatzTV.Application.Playouts; 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 +namespace ErsatzTV.Application.MediaCollections; + +public class RemoveItemsFromCollectionHandler : + MediatR.IRequestHandler> { - public class RemoveItemsFromCollectionHandler : - MediatR.IRequestHandler> + private readonly ChannelWriter _channel; + private readonly IDbContextFactory _dbContextFactory; + private readonly IMediaCollectionRepository _mediaCollectionRepository; + + public RemoveItemsFromCollectionHandler( + IDbContextFactory dbContextFactory, + IMediaCollectionRepository mediaCollectionRepository, + ChannelWriter channel) { - private readonly ChannelWriter _channel; - private readonly IDbContextFactory _dbContextFactory; - private readonly IMediaCollectionRepository _mediaCollectionRepository; - - public RemoveItemsFromCollectionHandler( - IDbContextFactory dbContextFactory, - IMediaCollectionRepository mediaCollectionRepository, - ChannelWriter channel) - { - _dbContextFactory = dbContextFactory; - _mediaCollectionRepository = mediaCollectionRepository; - _channel = channel; - } - - public async Task> Handle( - RemoveItemsFromCollection request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - Validation validation = await Validate(dbContext, request); - return await validation.Apply(c => ApplyRemoveItemsRequest(dbContext, request, c)); - } - - private async Task ApplyRemoveItemsRequest( - TvContext dbContext, - RemoveItemsFromCollection request, - Collection collection) - { - var itemsToRemove = collection.MediaItems - .Filter(m => request.MediaItemIds.Contains(m.Id)) - .ToList(); - - itemsToRemove.ForEach(m => collection.MediaItems.Remove(m)); - - if (itemsToRemove.Any() && await dbContext.SaveChangesAsync() > 0) - { - // rebuild all playouts that use this collection - foreach (int playoutId in await _mediaCollectionRepository.PlayoutIdsUsingCollection(collection.Id)) - { - await _channel.WriteAsync(new BuildPlayout(playoutId, true)); - } - } - - return Unit.Default; - } - - private static Task> Validate( - TvContext dbContext, - RemoveItemsFromCollection request) => - CollectionMustExist(dbContext, request); - - private static Task> CollectionMustExist( - TvContext dbContext, - RemoveItemsFromCollection request) => - dbContext.Collections - .Include(c => c.MediaItems) - .SelectOneAsync(c => c.Id, c => c.Id == request.MediaCollectionId) - .Map(o => o.ToValidation("Collection does not exist.")); + _dbContextFactory = dbContextFactory; + _mediaCollectionRepository = mediaCollectionRepository; + _channel = channel; } -} + + public async Task> Handle( + RemoveItemsFromCollection request, + CancellationToken cancellationToken) + { + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + Validation validation = await Validate(dbContext, request); + return await LanguageExtensions.Apply(validation, c => ApplyRemoveItemsRequest(dbContext, request, c)); + } + + private async Task ApplyRemoveItemsRequest( + TvContext dbContext, + RemoveItemsFromCollection request, + Collection collection) + { + var itemsToRemove = collection.MediaItems + .Filter(m => request.MediaItemIds.Contains(m.Id)) + .ToList(); + + itemsToRemove.ForEach(m => collection.MediaItems.Remove(m)); + + if (itemsToRemove.Any() && await dbContext.SaveChangesAsync() > 0) + { + // rebuild all playouts that use this collection + foreach (int playoutId in await _mediaCollectionRepository.PlayoutIdsUsingCollection(collection.Id)) + { + await _channel.WriteAsync(new BuildPlayout(playoutId, true)); + } + } + + return Unit.Default; + } + + private static Task> Validate( + TvContext dbContext, + RemoveItemsFromCollection request) => + CollectionMustExist(dbContext, request); + + private static Task> CollectionMustExist( + TvContext dbContext, + RemoveItemsFromCollection request) => + dbContext.Collections + .Include(c => c.MediaItems) + .SelectOneAsync(c => c.Id, c => c.Id == request.MediaCollectionId) + .Map(o => o.ToValidation("Collection does not exist.")); +} \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Commands/TraktCommandBase.cs b/ErsatzTV.Application/MediaCollections/Commands/TraktCommandBase.cs index 85f263a51..e535851e3 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/TraktCommandBase.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/TraktCommandBase.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Search; @@ -10,340 +6,337 @@ using ErsatzTV.Core.Interfaces.Trakt; using ErsatzTV.Core.Trakt; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; -using LanguageExt; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using static LanguageExt.Prelude; -namespace ErsatzTV.Application.MediaCollections.Commands +namespace ErsatzTV.Application.MediaCollections; + +public abstract class TraktCommandBase { - public abstract class TraktCommandBase + private readonly ISearchRepository _searchRepository; + private readonly ISearchIndex _searchIndex; + private readonly ILogger _logger; + + protected TraktCommandBase( + ITraktApiClient traktApiClient, + ISearchRepository searchRepository, + ISearchIndex searchIndex, + ILogger logger) { - private readonly ISearchRepository _searchRepository; - private readonly ISearchIndex _searchIndex; - private readonly ILogger _logger; + _searchRepository = searchRepository; + _searchIndex = searchIndex; + _logger = logger; + TraktApiClient = traktApiClient; + } - protected TraktCommandBase( - ITraktApiClient traktApiClient, - ISearchRepository searchRepository, - ISearchIndex searchIndex, - ILogger logger) - { - _searchRepository = searchRepository; - _searchIndex = searchIndex; - _logger = logger; - TraktApiClient = traktApiClient; - } + protected ITraktApiClient TraktApiClient { get; } - protected ITraktApiClient TraktApiClient { get; } + protected static Task> + TraktListMustExist(TvContext dbContext, int traktListId) => + dbContext.TraktLists + .Include(l => l.Items) + .ThenInclude(i => i.Guids) + .SelectOneAsync(c => c.Id, c => c.Id == traktListId) + .Map(o => o.ToValidation($"TraktList {traktListId} does not exist.")); - protected static Task> - TraktListMustExist(TvContext dbContext, int traktListId) => - dbContext.TraktLists - .Include(l => l.Items) - .ThenInclude(i => i.Guids) - .SelectOneAsync(c => c.Id, c => c.Id == traktListId) - .Map(o => o.ToValidation($"TraktList {traktListId} does not exist.")); + protected async Task> SaveList(TvContext dbContext, TraktList list) + { + Option maybeExisting = await dbContext.TraktLists + .Include(l => l.Items) + .ThenInclude(i => i.Guids) + .SelectOneAsync(tl => tl.Id, tl => tl.User == list.User && tl.List == list.List); - protected async Task> SaveList(TvContext dbContext, TraktList list) - { - Option maybeExisting = await dbContext.TraktLists - .Include(l => l.Items) - .ThenInclude(i => i.Guids) - .SelectOneAsync(tl => tl.Id, tl => tl.User == list.User && tl.List == list.List); - - return await maybeExisting.Match( - async existing => - { - existing.Name = list.Name; - existing.Description = list.Description; - existing.ItemCount = list.ItemCount; - - await dbContext.SaveChangesAsync(); - - return existing; - }, - async () => - { - await dbContext.TraktLists.AddAsync(list); - await dbContext.SaveChangesAsync(); - - return list; - }); - } - - protected async Task> SaveListItems(TvContext dbContext, TraktList list) - { - Either> maybeItems = - await TraktApiClient.GetUserListItems(list.User, list.List); - - return await maybeItems.Match>>( - async items => - { - var toAdd = items.Filter(i => list.Items.All(i2 => i2.TraktId != i.TraktId)).ToList(); - var toRemove = list.Items.Filter(i => items.All(i2 => i2.TraktId != i.TraktId)).ToList(); - var toUpdate = list.Items.Filter(i => !toRemove.Contains(i)).ToList(); - - list.Items.RemoveAll(toRemove.Contains); - list.Items.AddRange(toAdd.Map(a => ProjectItem(list, a))); - - foreach (TraktListItem existing in toUpdate) - { - Option maybeIncoming = list.Items.Find(i => i.TraktId == existing.TraktId); - foreach (TraktListItem incoming in maybeIncoming) - { - existing.Kind = incoming.Kind; - existing.Rank = incoming.Rank; - existing.Title = incoming.Title; - existing.Year = incoming.Year; - existing.Season = incoming.Season; - existing.Episode = incoming.Episode; - existing.Guids.Clear(); - existing.Guids.AddRange(incoming.Guids); - existing.MediaItemId = null; - existing.MediaItem = null; - } - } - - await dbContext.SaveChangesAsync(); - - return list; - }, - error => Task.FromResult(Left(error))); - } - - protected async Task> MatchListItems(TvContext dbContext, TraktList list) - { - try + return await maybeExisting.Match( + async existing => { - var ids = new System.Collections.Generic.HashSet(); + existing.Name = list.Name; + existing.Description = list.Description; + existing.ItemCount = list.ItemCount; - foreach (TraktListItem item in list.Items - .OrderBy(i => i.Title).ThenBy(i => i.Year).ThenBy(i => i.Season).ThenBy(i => i.Episode)) + await dbContext.SaveChangesAsync(); + + return existing; + }, + async () => + { + await dbContext.TraktLists.AddAsync(list); + await dbContext.SaveChangesAsync(); + + return list; + }); + } + + protected async Task> SaveListItems(TvContext dbContext, TraktList list) + { + Either> maybeItems = + await TraktApiClient.GetUserListItems(list.User, list.List); + + return await maybeItems.Match>>( + async items => + { + var toAdd = items.Filter(i => list.Items.All(i2 => i2.TraktId != i.TraktId)).ToList(); + var toRemove = list.Items.Filter(i => items.All(i2 => i2.TraktId != i.TraktId)).ToList(); + var toUpdate = list.Items.Filter(i => !toRemove.Contains(i)).ToList(); + + list.Items.RemoveAll(toRemove.Contains); + list.Items.AddRange(toAdd.Map(a => ProjectItem(list, a))); + + foreach (TraktListItem existing in toUpdate) { - switch (item.Kind) + Option maybeIncoming = list.Items.Find(i => i.TraktId == existing.TraktId); + foreach (TraktListItem incoming in maybeIncoming) { - case TraktListItemKind.Movie: - Option maybeMovieId = await IdentifyMovie(dbContext, item); - foreach (int movieId in maybeMovieId) - { - ids.Add(movieId); - item.MediaItemId = movieId; - } - - break; - case TraktListItemKind.Show: - Option maybeShowId = await IdentifyShow(dbContext, item); - foreach (int showId in maybeShowId) - { - ids.Add(showId); - item.MediaItemId = showId; - } - - break; - case TraktListItemKind.Season: - Option maybeSeasonId = await IdentifySeason(dbContext, item); - foreach (int seasonId in maybeSeasonId) - { - ids.Add(seasonId); - item.MediaItemId = seasonId; - } - - break; - default: - Option maybeEpisodeId = await IdentifyEpisode(dbContext, item); - foreach (int episodeId in maybeEpisodeId) - { - ids.Add(episodeId); - item.MediaItemId = episodeId; - } - - break; + existing.Kind = incoming.Kind; + existing.Rank = incoming.Rank; + existing.Title = incoming.Title; + existing.Year = incoming.Year; + existing.Season = incoming.Season; + existing.Episode = incoming.Episode; + existing.Guids.Clear(); + existing.Guids.AddRange(incoming.Guids); + existing.MediaItemId = null; + existing.MediaItem = null; } } await dbContext.SaveChangesAsync(); - foreach (int mediaItemId in ids) - { - Option maybeItem = await _searchRepository.GetItemToIndex(mediaItemId); - foreach (MediaItem item in maybeItem) - { - await _searchIndex.UpdateItems(_searchRepository, new[] { item }.ToList()); - } - } - - _searchIndex.Commit(); - return list; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error matching trakt list items"); - return BaseError.New(ex.Message); - } - } + }, + error => Task.FromResult(Left(error))); + } - private static TraktListItem ProjectItem(TraktList list, TraktListItemWithGuids item) + protected async Task> MatchListItems(TvContext dbContext, TraktList list) + { + try { - var result = new TraktListItem + var ids = new System.Collections.Generic.HashSet(); + + foreach (TraktListItem item in list.Items + .OrderBy(i => i.Title).ThenBy(i => i.Year).ThenBy(i => i.Season).ThenBy(i => i.Episode)) { - TraktList = list, - Kind = item.Kind, - TraktId = item.TraktId, - Rank = item.Rank, - Title = item.Title, - Year = item.Year, - Season = item.Season, - Episode = item.Episode, - }; + switch (item.Kind) + { + case TraktListItemKind.Movie: + Option maybeMovieId = await IdentifyMovie(dbContext, item); + foreach (int movieId in maybeMovieId) + { + ids.Add(movieId); + item.MediaItemId = movieId; + } - result.Guids = item.Guids.Map(g => new TraktListItemGuid { Guid = g, TraktListItem = result }).ToList(); + break; + case TraktListItemKind.Show: + Option maybeShowId = await IdentifyShow(dbContext, item); + foreach (int showId in maybeShowId) + { + ids.Add(showId); + item.MediaItemId = showId; + } - return result; + break; + case TraktListItemKind.Season: + Option maybeSeasonId = await IdentifySeason(dbContext, item); + foreach (int seasonId in maybeSeasonId) + { + ids.Add(seasonId); + item.MediaItemId = seasonId; + } + + break; + default: + Option maybeEpisodeId = await IdentifyEpisode(dbContext, item); + foreach (int episodeId in maybeEpisodeId) + { + ids.Add(episodeId); + item.MediaItemId = episodeId; + } + + break; + } + } + + await dbContext.SaveChangesAsync(); + + foreach (int mediaItemId in ids) + { + Option maybeItem = await _searchRepository.GetItemToIndex(mediaItemId); + foreach (MediaItem item in maybeItem) + { + await _searchIndex.UpdateItems(_searchRepository, new[] { item }.ToList()); + } + } + + _searchIndex.Commit(); + + return list; } - - private async Task> IdentifyMovie(TvContext dbContext, TraktListItem item) + catch (Exception ex) { - var guids = item.Guids.Map(g => g.Guid).ToList(); - - Option maybeMovieByGuid = await dbContext.MovieMetadata - .AsNoTracking() - .Filter(mm => mm.Guids.Any(g => guids.Contains(g.Guid))) - .FirstOrDefaultAsync() - .Map(Optional) - .MapT(mm => mm.MovieId); - - foreach (int movieId in maybeMovieByGuid) - { - _logger.LogDebug("Located trakt movie {Title} by id", item.DisplayTitle); - return movieId; - } - - Option maybeMovieByTitleYear = await dbContext.MovieMetadata - .AsNoTracking() - .Filter(mm => mm.Title == item.Title && mm.Year == item.Year) - .FirstOrDefaultAsync() - .Map(Optional) - .MapT(mm => mm.MovieId); - - foreach (int movieId in maybeMovieByTitleYear) - { - _logger.LogDebug("Located trakt movie {Title} by title/year", item.DisplayTitle); - return movieId; - } - - _logger.LogDebug("Unable to locate trakt movie {Title}", item.DisplayTitle); - - return None; - } - - private async Task> IdentifyShow(TvContext dbContext, TraktListItem item) - { - var guids = item.Guids.Map(g => g.Guid).ToList(); - - Option maybeShowByGuid = await dbContext.ShowMetadata - .AsNoTracking() - .Filter(sm => sm.Guids.Any(g => guids.Contains(g.Guid))) - .FirstOrDefaultAsync() - .Map(Optional) - .MapT(sm => sm.ShowId); - - foreach (int showId in maybeShowByGuid) - { - _logger.LogDebug("Located trakt show {Title} by id", item.DisplayTitle); - return showId; - } - - Option maybeShowByTitleYear = await dbContext.ShowMetadata - .AsNoTracking() - .Filter(sm => sm.Title == item.Title && sm.Year == item.Year) - .FirstOrDefaultAsync() - .Map(Optional) - .MapT(sm => sm.ShowId); - - foreach (int showId in maybeShowByTitleYear) - { - _logger.LogDebug("Located trakt show {Title} by title/year", item.Title); - return showId; - } - - _logger.LogDebug("Unable to locate trakt show {Title}", item.DisplayTitle); - - return None; - } - - private async Task> IdentifySeason(TvContext dbContext, TraktListItem item) - { - var guids = item.Guids.Map(g => g.Guid).ToList(); - - Option maybeSeasonByGuid = await dbContext.SeasonMetadata - .AsNoTracking() - .Filter(sm => sm.Guids.Any(g => guids.Contains(g.Guid))) - .FirstOrDefaultAsync() - .Map(Optional) - .MapT(sm => sm.SeasonId); - - foreach (int seasonId in maybeSeasonByGuid) - { - _logger.LogDebug("Located trakt season {Title} by id", item.DisplayTitle); - return seasonId; - } - - Option maybeSeasonByTitleYear = await dbContext.SeasonMetadata - .AsNoTracking() - .Filter(sm => sm.Season.Show.ShowMetadata.Any(s => s.Title == item.Title && s.Year == item.Year)) - .Filter(sm => sm.Season.SeasonNumber == item.Season) - .FirstOrDefaultAsync() - .Map(Optional) - .MapT(sm => sm.SeasonId); - - foreach (int seasonId in maybeSeasonByTitleYear) - { - _logger.LogDebug("Located trakt season {Title} by title/year/season", item.DisplayTitle); - return seasonId; - } - - _logger.LogDebug("Unable to locate trakt season {Title}", item.DisplayTitle); - - return None; - } - - private async Task> IdentifyEpisode(TvContext dbContext, TraktListItem item) - { - var guids = item.Guids.Map(g => g.Guid).ToList(); - - Option maybeEpisodeByGuid = await dbContext.EpisodeMetadata - .AsNoTracking() - .Filter(em => em.Guids.Any(g => guids.Contains(g.Guid))) - .FirstOrDefaultAsync() - .Map(Optional) - .MapT(sm => sm.EpisodeId); - - foreach (int episodeId in maybeEpisodeByGuid) - { - _logger.LogDebug("Located trakt episode {Title} by id", item.DisplayTitle); - return episodeId; - } - - Option maybeEpisodeByTitleYear = await dbContext.EpisodeMetadata - .AsNoTracking() - .Filter(sm => sm.Episode.Season.Show.ShowMetadata.Any(s => s.Title == item.Title && s.Year == item.Year)) - .Filter(em => em.Episode.Season.SeasonNumber == item.Season) - .Filter(sm => sm.Episode.EpisodeMetadata.Any(e => e.EpisodeNumber == item.Episode)) - .FirstOrDefaultAsync() - .Map(Optional) - .MapT(sm => sm.EpisodeId); - - foreach (int episodeId in maybeEpisodeByTitleYear) - { - _logger.LogDebug("Located trakt episode {Title} by title/year/season/episode", item.DisplayTitle); - return episodeId; - } - - _logger.LogDebug("Unable to locate trakt episode {Title}", item.DisplayTitle); - - return None; + _logger.LogError(ex, "Error matching trakt list items"); + return BaseError.New(ex.Message); } } -} + + private static TraktListItem ProjectItem(TraktList list, TraktListItemWithGuids item) + { + var result = new TraktListItem + { + TraktList = list, + Kind = item.Kind, + TraktId = item.TraktId, + Rank = item.Rank, + Title = item.Title, + Year = item.Year, + Season = item.Season, + Episode = item.Episode, + }; + + result.Guids = item.Guids.Map(g => new TraktListItemGuid { Guid = g, TraktListItem = result }).ToList(); + + return result; + } + + private async Task> IdentifyMovie(TvContext dbContext, TraktListItem item) + { + var guids = item.Guids.Map(g => g.Guid).ToList(); + + Option maybeMovieByGuid = await dbContext.MovieMetadata + .AsNoTracking() + .Filter(mm => mm.Guids.Any(g => guids.Contains(g.Guid))) + .FirstOrDefaultAsync() + .Map(Optional) + .MapT(mm => mm.MovieId); + + foreach (int movieId in maybeMovieByGuid) + { + _logger.LogDebug("Located trakt movie {Title} by id", item.DisplayTitle); + return movieId; + } + + Option maybeMovieByTitleYear = await dbContext.MovieMetadata + .AsNoTracking() + .Filter(mm => mm.Title == item.Title && mm.Year == item.Year) + .FirstOrDefaultAsync() + .Map(Optional) + .MapT(mm => mm.MovieId); + + foreach (int movieId in maybeMovieByTitleYear) + { + _logger.LogDebug("Located trakt movie {Title} by title/year", item.DisplayTitle); + return movieId; + } + + _logger.LogDebug("Unable to locate trakt movie {Title}", item.DisplayTitle); + + return None; + } + + private async Task> IdentifyShow(TvContext dbContext, TraktListItem item) + { + var guids = item.Guids.Map(g => g.Guid).ToList(); + + Option maybeShowByGuid = await dbContext.ShowMetadata + .AsNoTracking() + .Filter(sm => sm.Guids.Any(g => guids.Contains(g.Guid))) + .FirstOrDefaultAsync() + .Map(Optional) + .MapT(sm => sm.ShowId); + + foreach (int showId in maybeShowByGuid) + { + _logger.LogDebug("Located trakt show {Title} by id", item.DisplayTitle); + return showId; + } + + Option maybeShowByTitleYear = await dbContext.ShowMetadata + .AsNoTracking() + .Filter(sm => sm.Title == item.Title && sm.Year == item.Year) + .FirstOrDefaultAsync() + .Map(Optional) + .MapT(sm => sm.ShowId); + + foreach (int showId in maybeShowByTitleYear) + { + _logger.LogDebug("Located trakt show {Title} by title/year", item.Title); + return showId; + } + + _logger.LogDebug("Unable to locate trakt show {Title}", item.DisplayTitle); + + return None; + } + + private async Task> IdentifySeason(TvContext dbContext, TraktListItem item) + { + var guids = item.Guids.Map(g => g.Guid).ToList(); + + Option maybeSeasonByGuid = await dbContext.SeasonMetadata + .AsNoTracking() + .Filter(sm => sm.Guids.Any(g => guids.Contains(g.Guid))) + .FirstOrDefaultAsync() + .Map(Optional) + .MapT(sm => sm.SeasonId); + + foreach (int seasonId in maybeSeasonByGuid) + { + _logger.LogDebug("Located trakt season {Title} by id", item.DisplayTitle); + return seasonId; + } + + Option maybeSeasonByTitleYear = await dbContext.SeasonMetadata + .AsNoTracking() + .Filter(sm => sm.Season.Show.ShowMetadata.Any(s => s.Title == item.Title && s.Year == item.Year)) + .Filter(sm => sm.Season.SeasonNumber == item.Season) + .FirstOrDefaultAsync() + .Map(Optional) + .MapT(sm => sm.SeasonId); + + foreach (int seasonId in maybeSeasonByTitleYear) + { + _logger.LogDebug("Located trakt season {Title} by title/year/season", item.DisplayTitle); + return seasonId; + } + + _logger.LogDebug("Unable to locate trakt season {Title}", item.DisplayTitle); + + return None; + } + + private async Task> IdentifyEpisode(TvContext dbContext, TraktListItem item) + { + var guids = item.Guids.Map(g => g.Guid).ToList(); + + Option maybeEpisodeByGuid = await dbContext.EpisodeMetadata + .AsNoTracking() + .Filter(em => em.Guids.Any(g => guids.Contains(g.Guid))) + .FirstOrDefaultAsync() + .Map(Optional) + .MapT(sm => sm.EpisodeId); + + foreach (int episodeId in maybeEpisodeByGuid) + { + _logger.LogDebug("Located trakt episode {Title} by id", item.DisplayTitle); + return episodeId; + } + + Option maybeEpisodeByTitleYear = await dbContext.EpisodeMetadata + .AsNoTracking() + .Filter(sm => sm.Episode.Season.Show.ShowMetadata.Any(s => s.Title == item.Title && s.Year == item.Year)) + .Filter(em => em.Episode.Season.SeasonNumber == item.Season) + .Filter(sm => sm.Episode.EpisodeMetadata.Any(e => e.EpisodeNumber == item.Episode)) + .FirstOrDefaultAsync() + .Map(Optional) + .MapT(sm => sm.EpisodeId); + + foreach (int episodeId in maybeEpisodeByTitleYear) + { + _logger.LogDebug("Located trakt episode {Title} by title/year/season/episode", item.DisplayTitle); + return episodeId; + } + + _logger.LogDebug("Unable to locate trakt episode {Title}", item.DisplayTitle); + + return None; + } +} \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Commands/UpdateCollection.cs b/ErsatzTV.Application/MediaCollections/Commands/UpdateCollection.cs index 1661fd2ce..887211a79 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/UpdateCollection.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/UpdateCollection.cs @@ -1,12 +1,9 @@ using ErsatzTV.Core; -using LanguageExt; -using static LanguageExt.Prelude; -namespace ErsatzTV.Application.MediaCollections.Commands +namespace ErsatzTV.Application.MediaCollections; + +public record UpdateCollection + (int CollectionId, string Name) : MediatR.IRequest> { - public record UpdateCollection - (int CollectionId, string Name) : MediatR.IRequest> - { - public Option UseCustomPlaybackOrder { get; set; } = None; - } -} + public Option UseCustomPlaybackOrder { get; set; } = None; +} \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Commands/UpdateCollectionCustomOrder.cs b/ErsatzTV.Application/MediaCollections/Commands/UpdateCollectionCustomOrder.cs index 6a8f2f024..b8b565112 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/UpdateCollectionCustomOrder.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/UpdateCollectionCustomOrder.cs @@ -1,13 +1,10 @@ -using System.Collections.Generic; -using ErsatzTV.Core; -using LanguageExt; +using ErsatzTV.Core; -namespace ErsatzTV.Application.MediaCollections.Commands -{ - public record UpdateCollectionCustomOrder - ( - int CollectionId, - List MediaItemCustomOrders) : MediatR.IRequest>; +namespace ErsatzTV.Application.MediaCollections; - public record MediaItemCustomOrder(int MediaItemId, int CustomIndex); -} +public record UpdateCollectionCustomOrder +( + int CollectionId, + List MediaItemCustomOrders) : MediatR.IRequest>; + +public record MediaItemCustomOrder(int MediaItemId, int CustomIndex); \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Commands/UpdateCollectionCustomOrderHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/UpdateCollectionCustomOrderHandler.cs index a40add059..903983a95 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/UpdateCollectionCustomOrderHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/UpdateCollectionCustomOrderHandler.cs @@ -1,84 +1,79 @@ -using System.Linq; -using System.Threading; -using System.Threading.Channels; -using System.Threading.Tasks; -using ErsatzTV.Application.Playouts.Commands; +using System.Threading.Channels; +using ErsatzTV.Application.Playouts; 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 +namespace ErsatzTV.Application.MediaCollections; + +public class UpdateCollectionCustomOrderHandler : + MediatR.IRequestHandler> { - public class UpdateCollectionCustomOrderHandler : - MediatR.IRequestHandler> + private readonly ChannelWriter _channel; + private readonly IDbContextFactory _dbContextFactory; + private readonly IMediaCollectionRepository _mediaCollectionRepository; + + public UpdateCollectionCustomOrderHandler( + IDbContextFactory dbContextFactory, + IMediaCollectionRepository mediaCollectionRepository, + ChannelWriter channel) { - private readonly ChannelWriter _channel; - private readonly IDbContextFactory _dbContextFactory; - private readonly IMediaCollectionRepository _mediaCollectionRepository; - - public UpdateCollectionCustomOrderHandler( - IDbContextFactory dbContextFactory, - IMediaCollectionRepository mediaCollectionRepository, - ChannelWriter channel) - { - _dbContextFactory = dbContextFactory; - _mediaCollectionRepository = mediaCollectionRepository; - _channel = channel; - } - - public async Task> Handle( - UpdateCollectionCustomOrder request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - Validation validation = await Validate(dbContext, request); - return await validation.Apply(c => ApplyUpdateRequest(dbContext, c, request)); - } - - private async Task ApplyUpdateRequest( - TvContext dbContext, - Collection c, - UpdateCollectionCustomOrder request) - { - foreach (MediaItemCustomOrder updateItem in request.MediaItemCustomOrders) - { - Option maybeCollectionItem = c.CollectionItems - .FirstOrDefault(ci => ci.MediaItemId == updateItem.MediaItemId); - - foreach (CollectionItem collectionItem in maybeCollectionItem) - { - collectionItem.CustomIndex = updateItem.CustomIndex; - } - } - - if (await dbContext.SaveChangesAsync() > 0) - { - // rebuild all playouts that use this collection - foreach (int playoutId in await _mediaCollectionRepository - .PlayoutIdsUsingCollection(request.CollectionId)) - { - await _channel.WriteAsync(new BuildPlayout(playoutId, true)); - } - } - - return Unit.Default; - } - - private static Task> Validate( - TvContext dbContext, - UpdateCollectionCustomOrder request) => - CollectionMustExist(dbContext, request); - - private static Task> CollectionMustExist( - TvContext dbContext, - UpdateCollectionCustomOrder request) => - dbContext.Collections - .Include(c => c.CollectionItems) - .SelectOneAsync(c => c.Id, c => c.Id == request.CollectionId) - .Map(o => o.ToValidation("Collection does not exist.")); + _dbContextFactory = dbContextFactory; + _mediaCollectionRepository = mediaCollectionRepository; + _channel = channel; } -} + + public async Task> Handle( + UpdateCollectionCustomOrder request, + CancellationToken cancellationToken) + { + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + Validation validation = await Validate(dbContext, request); + return await LanguageExtensions.Apply(validation, c => ApplyUpdateRequest(dbContext, c, request)); + } + + private async Task ApplyUpdateRequest( + TvContext dbContext, + Collection c, + UpdateCollectionCustomOrder request) + { + foreach (MediaItemCustomOrder updateItem in request.MediaItemCustomOrders) + { + Option maybeCollectionItem = c.CollectionItems + .FirstOrDefault(ci => ci.MediaItemId == updateItem.MediaItemId); + + foreach (CollectionItem collectionItem in maybeCollectionItem) + { + collectionItem.CustomIndex = updateItem.CustomIndex; + } + } + + if (await dbContext.SaveChangesAsync() > 0) + { + // rebuild all playouts that use this collection + foreach (int playoutId in await _mediaCollectionRepository + .PlayoutIdsUsingCollection(request.CollectionId)) + { + await _channel.WriteAsync(new BuildPlayout(playoutId, true)); + } + } + + return Unit.Default; + } + + private static Task> Validate( + TvContext dbContext, + UpdateCollectionCustomOrder request) => + CollectionMustExist(dbContext, request); + + private static Task> CollectionMustExist( + TvContext dbContext, + UpdateCollectionCustomOrder request) => + dbContext.Collections + .Include(c => c.CollectionItems) + .SelectOneAsync(c => c.Id, c => c.Id == request.CollectionId) + .Map(o => o.ToValidation("Collection does not exist.")); +} \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Commands/UpdateCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/UpdateCollectionHandler.cs index 2d7847d05..067fa4e0e 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/UpdateCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/UpdateCollectionHandler.cs @@ -1,78 +1,74 @@ -using System.Threading; -using System.Threading.Channels; -using System.Threading.Tasks; -using ErsatzTV.Application.Playouts.Commands; +using System.Threading.Channels; +using ErsatzTV.Application.Playouts; 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 +namespace ErsatzTV.Application.MediaCollections; + +public class UpdateCollectionHandler : MediatR.IRequestHandler> { - public class UpdateCollectionHandler : MediatR.IRequestHandler> + private readonly ChannelWriter _channel; + private readonly IDbContextFactory _dbContextFactory; + private readonly IMediaCollectionRepository _mediaCollectionRepository; + + public UpdateCollectionHandler( + IDbContextFactory dbContextFactory, + IMediaCollectionRepository mediaCollectionRepository, + ChannelWriter channel) { - private readonly ChannelWriter _channel; - private readonly IDbContextFactory _dbContextFactory; - private readonly IMediaCollectionRepository _mediaCollectionRepository; - - public UpdateCollectionHandler( - IDbContextFactory dbContextFactory, - IMediaCollectionRepository mediaCollectionRepository, - ChannelWriter channel) - { - _dbContextFactory = dbContextFactory; - _mediaCollectionRepository = mediaCollectionRepository; - _channel = channel; - } - - public async Task> Handle( - UpdateCollection request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - Validation validation = await Validate(dbContext, request); - return await validation.Apply(c => ApplyUpdateRequest(dbContext, c, request)); - } - - private async Task ApplyUpdateRequest(TvContext dbContext, Collection c, UpdateCollection request) - { - c.Name = request.Name; - 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( - request.CollectionId)) - { - await _channel.WriteAsync(new BuildPlayout(playoutId, true)); - } - } - - return Unit.Default; - } - - private static async Task> Validate( - TvContext dbContext, - UpdateCollection request) => - (await CollectionMustExist(dbContext, request), ValidateName(request)) - .Apply((collectionToUpdate, _) => collectionToUpdate); - - private static Task> CollectionMustExist( - TvContext dbContext, - UpdateCollection updateCollection) => - dbContext.Collections - .SelectOneAsync(c => c.Id, c => c.Id == updateCollection.CollectionId) - .Map(o => o.ToValidation("Collection does not exist.")); - - private static Validation ValidateName(UpdateCollection updateSimpleMediaCollection) => - updateSimpleMediaCollection.NotEmpty(c => c.Name) - .Bind(_ => updateSimpleMediaCollection.NotLongerThan(50)(c => c.Name)); + _dbContextFactory = dbContextFactory; + _mediaCollectionRepository = mediaCollectionRepository; + _channel = channel; } -} + + public async Task> Handle( + UpdateCollection request, + CancellationToken cancellationToken) + { + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + Validation validation = await Validate(dbContext, request); + return await LanguageExtensions.Apply(validation, c => ApplyUpdateRequest(dbContext, c, request)); + } + + private async Task ApplyUpdateRequest(TvContext dbContext, Collection c, UpdateCollection request) + { + c.Name = request.Name; + 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( + request.CollectionId)) + { + await _channel.WriteAsync(new BuildPlayout(playoutId, true)); + } + } + + return Unit.Default; + } + + private static async Task> Validate( + TvContext dbContext, + UpdateCollection request) => + (await CollectionMustExist(dbContext, request), ValidateName(request)) + .Apply((collectionToUpdate, _) => collectionToUpdate); + + private static Task> CollectionMustExist( + TvContext dbContext, + UpdateCollection updateCollection) => + dbContext.Collections + .SelectOneAsync(c => c.Id, c => c.Id == updateCollection.CollectionId) + .Map(o => o.ToValidation("Collection does not exist.")); + + private static Validation ValidateName(UpdateCollection updateSimpleMediaCollection) => + updateSimpleMediaCollection.NotEmpty(c => c.Name) + .Bind(_ => updateSimpleMediaCollection.NotLongerThan(50)(c => c.Name)); +} \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Commands/UpdateMultiCollection.cs b/ErsatzTV.Application/MediaCollections/Commands/UpdateMultiCollection.cs index ad8bf890a..017afe549 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/UpdateMultiCollection.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/UpdateMultiCollection.cs @@ -1,15 +1,12 @@ -using System.Collections.Generic; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; -using LanguageExt; -namespace ErsatzTV.Application.MediaCollections.Commands -{ - public record UpdateMultiCollectionItem(int? CollectionId, int? SmartCollectionId, bool ScheduleAsGroup, PlaybackOrder PlaybackOrder); +namespace ErsatzTV.Application.MediaCollections; - public record UpdateMultiCollection - ( - int MultiCollectionId, - string Name, - List Items) : MediatR.IRequest>; -} +public record UpdateMultiCollectionItem(int? CollectionId, int? SmartCollectionId, bool ScheduleAsGroup, PlaybackOrder PlaybackOrder); + +public record UpdateMultiCollection +( + int MultiCollectionId, + string Name, + List Items) : MediatR.IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Commands/UpdateMultiCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/UpdateMultiCollectionHandler.cs index 36c6cdb0a..f56093f89 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/UpdateMultiCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/UpdateMultiCollectionHandler.cs @@ -1,165 +1,157 @@ -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Threading; -using System.Threading.Channels; -using System.Threading.Tasks; -using ErsatzTV.Application.Playouts.Commands; +using System.Threading.Channels; +using ErsatzTV.Application.Playouts; 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 +namespace ErsatzTV.Application.MediaCollections; + +public class UpdateMultiCollectionHandler : MediatR.IRequestHandler> { - public class UpdateMultiCollectionHandler : MediatR.IRequestHandler> + private readonly ChannelWriter _channel; + private readonly IDbContextFactory _dbContextFactory; + private readonly IMediaCollectionRepository _mediaCollectionRepository; + + public UpdateMultiCollectionHandler( + IDbContextFactory dbContextFactory, + IMediaCollectionRepository mediaCollectionRepository, + ChannelWriter channel) { - private readonly ChannelWriter _channel; - private readonly IDbContextFactory _dbContextFactory; - private readonly IMediaCollectionRepository _mediaCollectionRepository; - - public UpdateMultiCollectionHandler( - IDbContextFactory dbContextFactory, - IMediaCollectionRepository mediaCollectionRepository, - ChannelWriter channel) - { - _dbContextFactory = dbContextFactory; - _mediaCollectionRepository = mediaCollectionRepository; - _channel = channel; - } - - public async Task> Handle( - UpdateMultiCollection request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - Validation validation = await Validate(dbContext, request); - return await validation.Apply(c => ApplyUpdateRequest(dbContext, c, request)); - } - - private async Task ApplyUpdateRequest(TvContext dbContext, MultiCollection c, UpdateMultiCollection request) - { - c.Name = request.Name; - - // save name first so playouts don't get rebuilt for a name change - await dbContext.SaveChangesAsync(); - - var toAdd = request.Items - .Filter(i => i.CollectionId.HasValue) - // ReSharper disable once PossibleInvalidOperationException - .Filter(i => c.MultiCollectionItems.All(i2 => i2.CollectionId != i.CollectionId.Value)) - .Map(i => new MultiCollectionItem - { - // ReSharper disable once PossibleInvalidOperationException - CollectionId = i.CollectionId.Value, - MultiCollectionId = c.Id, - ScheduleAsGroup = i.ScheduleAsGroup, - PlaybackOrder = i.PlaybackOrder - }) - .ToList(); - var toRemove = c.MultiCollectionItems - .Filter(i => request.Items.All(i2 => i2.CollectionId != i.CollectionId)) - .ToList(); - - // remove items that are no longer present - c.MultiCollectionItems.RemoveAll(toRemove.Contains); - - // update existing items - foreach (MultiCollectionItem item in c.MultiCollectionItems) - { - foreach (UpdateMultiCollectionItem incoming in request.Items.Filter( - i => i.CollectionId == item.CollectionId)) - { - item.ScheduleAsGroup = incoming.ScheduleAsGroup; - item.PlaybackOrder = incoming.PlaybackOrder; - } - } - - // add new items - c.MultiCollectionItems.AddRange(toAdd); - - var toAddSmart = request.Items - .Filter(i => i.SmartCollectionId.HasValue) - // ReSharper disable once PossibleInvalidOperationException - .Filter(i => c.MultiCollectionSmartItems.All(i2 => i2.SmartCollectionId != i.SmartCollectionId.Value)) - .Map(i => new MultiCollectionSmartItem - { - // ReSharper disable once PossibleInvalidOperationException - SmartCollectionId = i.SmartCollectionId.Value, - MultiCollectionId = c.Id, - ScheduleAsGroup = i.ScheduleAsGroup, - PlaybackOrder = i.PlaybackOrder - }) - .ToList(); - var toRemoveSmart = c.MultiCollectionSmartItems - .Filter(i => request.Items.All(i2 => i2.SmartCollectionId != i.SmartCollectionId)) - .ToList(); - - // remove items that are no longer present - c.MultiCollectionSmartItems.RemoveAll(toRemoveSmart.Contains); - - // update existing items - foreach (MultiCollectionSmartItem item in c.MultiCollectionSmartItems) - { - foreach (UpdateMultiCollectionItem incoming in request.Items.Filter( - i => i.SmartCollectionId == item.SmartCollectionId)) - { - item.ScheduleAsGroup = incoming.ScheduleAsGroup; - item.PlaybackOrder = incoming.PlaybackOrder; - } - } - - // add new items - c.MultiCollectionSmartItems.AddRange(toAddSmart); - - // rebuild playouts - if (await dbContext.SaveChangesAsync() > 0) - { - // rebuild all playouts that use this collection - foreach (int playoutId in await _mediaCollectionRepository.PlayoutIdsUsingMultiCollection( - request.MultiCollectionId)) - { - await _channel.WriteAsync(new BuildPlayout(playoutId, true)); - } - } - - return Unit.Default; - } - - private static async Task> Validate( - TvContext dbContext, - UpdateMultiCollection request) => - (await MultiCollectionMustExist(dbContext, request), await ValidateName(dbContext, request)) - .Apply((collectionToUpdate, _) => collectionToUpdate); - - private static Task> MultiCollectionMustExist( - TvContext dbContext, - UpdateMultiCollection updateCollection) => - dbContext.MultiCollections - .Include(mc => mc.MultiCollectionItems) - .Include(mc => mc.MultiCollectionSmartItems) - .SelectOneAsync(c => c.Id, c => c.Id == updateCollection.MultiCollectionId) - .Map(o => o.ToValidation("MultiCollection does not exist.")); - - private static async Task> ValidateName(TvContext dbContext, UpdateMultiCollection updateMultiCollection) - { - List allNames = await dbContext.MultiCollections - .Filter(mc => mc.Id != updateMultiCollection.MultiCollectionId) - .Map(c => c.Name) - .ToListAsync(); - - Validation result1 = updateMultiCollection.NotEmpty(c => c.Name) - .Bind(_ => updateMultiCollection.NotLongerThan(50)(c => c.Name)); - - var result2 = Optional(updateMultiCollection.Name) - .Where(name => !allNames.Contains(name)) - .ToValidation("MultiCollection name must be unique"); - - return (result1, result2).Apply((_, _) => updateMultiCollection.Name); - } + _dbContextFactory = dbContextFactory; + _mediaCollectionRepository = mediaCollectionRepository; + _channel = channel; } -} + + public async Task> Handle( + UpdateMultiCollection request, + CancellationToken cancellationToken) + { + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + Validation validation = await Validate(dbContext, request); + return await LanguageExtensions.Apply(validation, c => ApplyUpdateRequest(dbContext, c, request)); + } + + private async Task ApplyUpdateRequest(TvContext dbContext, MultiCollection c, UpdateMultiCollection request) + { + c.Name = request.Name; + + // save name first so playouts don't get rebuilt for a name change + await dbContext.SaveChangesAsync(); + + var toAdd = request.Items + .Filter(i => i.CollectionId.HasValue) + // ReSharper disable once PossibleInvalidOperationException + .Filter(i => c.MultiCollectionItems.All(i2 => i2.CollectionId != i.CollectionId.Value)) + .Map(i => new MultiCollectionItem + { + // ReSharper disable once PossibleInvalidOperationException + CollectionId = i.CollectionId.Value, + MultiCollectionId = c.Id, + ScheduleAsGroup = i.ScheduleAsGroup, + PlaybackOrder = i.PlaybackOrder + }) + .ToList(); + var toRemove = c.MultiCollectionItems + .Filter(i => request.Items.All(i2 => i2.CollectionId != i.CollectionId)) + .ToList(); + + // remove items that are no longer present + c.MultiCollectionItems.RemoveAll(toRemove.Contains); + + // update existing items + foreach (MultiCollectionItem item in c.MultiCollectionItems) + { + foreach (UpdateMultiCollectionItem incoming in request.Items.Filter( + i => i.CollectionId == item.CollectionId)) + { + item.ScheduleAsGroup = incoming.ScheduleAsGroup; + item.PlaybackOrder = incoming.PlaybackOrder; + } + } + + // add new items + c.MultiCollectionItems.AddRange(toAdd); + + var toAddSmart = request.Items + .Filter(i => i.SmartCollectionId.HasValue) + // ReSharper disable once PossibleInvalidOperationException + .Filter(i => c.MultiCollectionSmartItems.All(i2 => i2.SmartCollectionId != i.SmartCollectionId.Value)) + .Map(i => new MultiCollectionSmartItem + { + // ReSharper disable once PossibleInvalidOperationException + SmartCollectionId = i.SmartCollectionId.Value, + MultiCollectionId = c.Id, + ScheduleAsGroup = i.ScheduleAsGroup, + PlaybackOrder = i.PlaybackOrder + }) + .ToList(); + var toRemoveSmart = c.MultiCollectionSmartItems + .Filter(i => request.Items.All(i2 => i2.SmartCollectionId != i.SmartCollectionId)) + .ToList(); + + // remove items that are no longer present + c.MultiCollectionSmartItems.RemoveAll(toRemoveSmart.Contains); + + // update existing items + foreach (MultiCollectionSmartItem item in c.MultiCollectionSmartItems) + { + foreach (UpdateMultiCollectionItem incoming in request.Items.Filter( + i => i.SmartCollectionId == item.SmartCollectionId)) + { + item.ScheduleAsGroup = incoming.ScheduleAsGroup; + item.PlaybackOrder = incoming.PlaybackOrder; + } + } + + // add new items + c.MultiCollectionSmartItems.AddRange(toAddSmart); + + // rebuild playouts + if (await dbContext.SaveChangesAsync() > 0) + { + // rebuild all playouts that use this collection + foreach (int playoutId in await _mediaCollectionRepository.PlayoutIdsUsingMultiCollection( + request.MultiCollectionId)) + { + await _channel.WriteAsync(new BuildPlayout(playoutId, true)); + } + } + + return Unit.Default; + } + + private static async Task> Validate( + TvContext dbContext, + UpdateMultiCollection request) => + (await MultiCollectionMustExist(dbContext, request), await ValidateName(dbContext, request)) + .Apply((collectionToUpdate, _) => collectionToUpdate); + + private static Task> MultiCollectionMustExist( + TvContext dbContext, + UpdateMultiCollection updateCollection) => + dbContext.MultiCollections + .Include(mc => mc.MultiCollectionItems) + .Include(mc => mc.MultiCollectionSmartItems) + .SelectOneAsync(c => c.Id, c => c.Id == updateCollection.MultiCollectionId) + .Map(o => o.ToValidation("MultiCollection does not exist.")); + + private static async Task> ValidateName(TvContext dbContext, UpdateMultiCollection updateMultiCollection) + { + List allNames = await dbContext.MultiCollections + .Filter(mc => mc.Id != updateMultiCollection.MultiCollectionId) + .Map(c => c.Name) + .ToListAsync(); + + Validation result1 = updateMultiCollection.NotEmpty(c => c.Name) + .Bind(_ => updateMultiCollection.NotLongerThan(50)(c => c.Name)); + + var result2 = Optional(updateMultiCollection.Name) + .Where(name => !allNames.Contains(name)) + .ToValidation("MultiCollection name must be unique"); + + return (result1, result2).Apply((_, _) => updateMultiCollection.Name); + } +} \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Commands/UpdateSmartCollection.cs b/ErsatzTV.Application/MediaCollections/Commands/UpdateSmartCollection.cs index 6ee1df3ed..bbb1d115a 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/UpdateSmartCollection.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/UpdateSmartCollection.cs @@ -1,9 +1,5 @@ using ErsatzTV.Core; -using LanguageExt; -using MediatR; -using Unit = LanguageExt.Unit; -namespace ErsatzTV.Application.MediaCollections.Commands -{ - public record UpdateSmartCollection(int Id, string Query) : IRequest>; -} +namespace ErsatzTV.Application.MediaCollections; + +public record UpdateSmartCollection(int Id, string Query) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Commands/UpdateSmartCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/UpdateSmartCollectionHandler.cs index 3fab12b94..e48a89563 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/UpdateSmartCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/UpdateSmartCollectionHandler.cs @@ -1,71 +1,64 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Channels; -using System.Threading.Tasks; -using ErsatzTV.Application.Playouts.Commands; +using System.Threading.Channels; +using ErsatzTV.Application.Playouts; 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 +namespace ErsatzTV.Application.MediaCollections; + +public class UpdateSmartCollectionHandler : MediatR.IRequestHandler> { - public class UpdateSmartCollectionHandler : MediatR.IRequestHandler> + private readonly ChannelWriter _channel; + private readonly IDbContextFactory _dbContextFactory; + private readonly IMediaCollectionRepository _mediaCollectionRepository; + + public UpdateSmartCollectionHandler( + IDbContextFactory dbContextFactory, + IMediaCollectionRepository mediaCollectionRepository, + ChannelWriter channel) { - private readonly ChannelWriter _channel; - private readonly IDbContextFactory _dbContextFactory; - private readonly IMediaCollectionRepository _mediaCollectionRepository; - - public UpdateSmartCollectionHandler( - IDbContextFactory dbContextFactory, - IMediaCollectionRepository mediaCollectionRepository, - ChannelWriter channel) - { - _dbContextFactory = dbContextFactory; - _mediaCollectionRepository = mediaCollectionRepository; - _channel = channel; - } - - public async Task> Handle( - UpdateSmartCollection request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - Validation validation = await Validate(dbContext, request); - return await validation.Apply(c => ApplyUpdateRequest(dbContext, c, request)); - } - - private async Task ApplyUpdateRequest(TvContext dbContext, SmartCollection c, UpdateSmartCollection request) - { - c.Query = request.Query; - - // rebuild playouts - if (await dbContext.SaveChangesAsync() > 0) - { - // rebuild all playouts that use this smart collection - foreach (int playoutId in await _mediaCollectionRepository.PlayoutIdsUsingSmartCollection(request.Id)) - { - await _channel.WriteAsync(new BuildPlayout(playoutId, true)); - } - } - - return Unit.Default; - } - - private static Task> Validate( - TvContext dbContext, - UpdateSmartCollection request) => SmartCollectionMustExist(dbContext, request); - - private static Task> SmartCollectionMustExist( - TvContext dbContext, - UpdateSmartCollection updateCollection) => - dbContext.SmartCollections - .SelectOneAsync(c => c.Id, c => c.Id == updateCollection.Id) - .Map(o => o.ToValidation("SmartCollection does not exist.")); + _dbContextFactory = dbContextFactory; + _mediaCollectionRepository = mediaCollectionRepository; + _channel = channel; } -} + + public async Task> Handle( + UpdateSmartCollection request, + CancellationToken cancellationToken) + { + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + Validation validation = await Validate(dbContext, request); + return await LanguageExtensions.Apply(validation, c => ApplyUpdateRequest(dbContext, c, request)); + } + + private async Task ApplyUpdateRequest(TvContext dbContext, SmartCollection c, UpdateSmartCollection request) + { + c.Query = request.Query; + + // rebuild playouts + if (await dbContext.SaveChangesAsync() > 0) + { + // rebuild all playouts that use this smart collection + foreach (int playoutId in await _mediaCollectionRepository.PlayoutIdsUsingSmartCollection(request.Id)) + { + await _channel.WriteAsync(new BuildPlayout(playoutId, true)); + } + } + + return Unit.Default; + } + + private static Task> Validate( + TvContext dbContext, + UpdateSmartCollection request) => SmartCollectionMustExist(dbContext, request); + + private static Task> SmartCollectionMustExist( + TvContext dbContext, + UpdateSmartCollection updateCollection) => + dbContext.SmartCollections + .SelectOneAsync(c => c.Id, c => c.Id == updateCollection.Id) + .Map(o => o.ToValidation("SmartCollection does not exist.")); +} \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Mapper.cs b/ErsatzTV.Application/MediaCollections/Mapper.cs index 4fffe975c..688a4b144 100644 --- a/ErsatzTV.Application/MediaCollections/Mapper.cs +++ b/ErsatzTV.Application/MediaCollections/Mapper.cs @@ -1,45 +1,42 @@ -using System.Linq; -using ErsatzTV.Core.Domain; -using static LanguageExt.Prelude; +using ErsatzTV.Core.Domain; -namespace ErsatzTV.Application.MediaCollections +namespace ErsatzTV.Application.MediaCollections; + +internal static class Mapper { - internal static class Mapper - { - internal static MediaCollectionViewModel ProjectToViewModel(Collection collection) => - new(collection.Id, collection.Name, collection.UseCustomPlaybackOrder, MediaItemState.Normal); + internal static MediaCollectionViewModel ProjectToViewModel(Collection collection) => + new(collection.Id, collection.Name, collection.UseCustomPlaybackOrder, MediaItemState.Normal); - internal static MultiCollectionViewModel ProjectToViewModel(MultiCollection multiCollection) => - new( - multiCollection.Id, - multiCollection.Name, - Optional(multiCollection.MultiCollectionItems).Flatten().Map(ProjectToViewModel).ToList(), - Optional(multiCollection.MultiCollectionSmartItems).Flatten().Map(ProjectToViewModel).ToList()); + internal static MultiCollectionViewModel ProjectToViewModel(MultiCollection multiCollection) => + new( + multiCollection.Id, + multiCollection.Name, + Optional(multiCollection.MultiCollectionItems).Flatten().Map(ProjectToViewModel).ToList(), + Optional(multiCollection.MultiCollectionSmartItems).Flatten().Map(ProjectToViewModel).ToList()); - internal static SmartCollectionViewModel ProjectToViewModel(SmartCollection collection) => - new(collection.Id, collection.Name, collection.Query); + internal static SmartCollectionViewModel ProjectToViewModel(SmartCollection collection) => + new(collection.Id, collection.Name, collection.Query); - internal static TraktListViewModel ProjectToViewModel(TraktList traktList) => - new( - traktList.Id, - traktList.TraktId, - $"{traktList.User}/{traktList.List}", - traktList.Name, - traktList.ItemCount, - traktList.Items.Count(i => i.MediaItemId.HasValue)); + internal static TraktListViewModel ProjectToViewModel(TraktList traktList) => + new( + traktList.Id, + traktList.TraktId, + $"{traktList.User}/{traktList.List}", + traktList.Name, + traktList.ItemCount, + traktList.Items.Count(i => i.MediaItemId.HasValue)); - private static MultiCollectionItemViewModel ProjectToViewModel(MultiCollectionItem multiCollectionItem) => - new( - multiCollectionItem.MultiCollectionId, - ProjectToViewModel(multiCollectionItem.Collection), - multiCollectionItem.ScheduleAsGroup, - multiCollectionItem.PlaybackOrder); + private static MultiCollectionItemViewModel ProjectToViewModel(MultiCollectionItem multiCollectionItem) => + new( + multiCollectionItem.MultiCollectionId, + ProjectToViewModel(multiCollectionItem.Collection), + multiCollectionItem.ScheduleAsGroup, + multiCollectionItem.PlaybackOrder); - private static MultiCollectionSmartItemViewModel ProjectToViewModel(MultiCollectionSmartItem multiCollectionSmartItem) => - new( - multiCollectionSmartItem.MultiCollectionId, - ProjectToViewModel(multiCollectionSmartItem.SmartCollection), - multiCollectionSmartItem.ScheduleAsGroup, - multiCollectionSmartItem.PlaybackOrder); - } -} + private static MultiCollectionSmartItemViewModel ProjectToViewModel(MultiCollectionSmartItem multiCollectionSmartItem) => + new( + multiCollectionSmartItem.MultiCollectionId, + ProjectToViewModel(multiCollectionSmartItem.SmartCollection), + multiCollectionSmartItem.ScheduleAsGroup, + multiCollectionSmartItem.PlaybackOrder); +} \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/MediaCollectionSummaryViewModel.cs b/ErsatzTV.Application/MediaCollections/MediaCollectionSummaryViewModel.cs index 1a9d552f3..3aacaa275 100644 --- a/ErsatzTV.Application/MediaCollections/MediaCollectionSummaryViewModel.cs +++ b/ErsatzTV.Application/MediaCollections/MediaCollectionSummaryViewModel.cs @@ -1,4 +1,3 @@ -namespace ErsatzTV.Application.MediaCollections -{ - public record MediaCollectionSummaryViewModel(int Id, string Name, int ItemCount, bool IsSimple); -} +namespace ErsatzTV.Application.MediaCollections; + +public record MediaCollectionSummaryViewModel(int Id, string Name, int ItemCount, bool IsSimple); \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/MediaCollectionViewModel.cs b/ErsatzTV.Application/MediaCollections/MediaCollectionViewModel.cs index e0cefedb8..81c768352 100644 --- a/ErsatzTV.Application/MediaCollections/MediaCollectionViewModel.cs +++ b/ErsatzTV.Application/MediaCollections/MediaCollectionViewModel.cs @@ -1,17 +1,16 @@ using ErsatzTV.Application.MediaCards; using ErsatzTV.Core.Domain; -namespace ErsatzTV.Application.MediaCollections -{ - public record MediaCollectionViewModel( - int Id, - string Name, - bool UseCustomPlaybackOrder, - MediaItemState State) : MediaCardViewModel( - Id, - Name, - string.Empty, - Name, - string.Empty, - State); -} +namespace ErsatzTV.Application.MediaCollections; + +public record MediaCollectionViewModel( + int Id, + string Name, + bool UseCustomPlaybackOrder, + MediaItemState State) : MediaCardViewModel( + Id, + Name, + string.Empty, + Name, + string.Empty, + State); \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/MultiCollectionItemViewModel.cs b/ErsatzTV.Application/MediaCollections/MultiCollectionItemViewModel.cs index eb0757c93..38d71eb4a 100644 --- a/ErsatzTV.Application/MediaCollections/MultiCollectionItemViewModel.cs +++ b/ErsatzTV.Application/MediaCollections/MultiCollectionItemViewModel.cs @@ -1,10 +1,9 @@ using ErsatzTV.Core.Domain; -namespace ErsatzTV.Application.MediaCollections -{ - public record MultiCollectionItemViewModel( - int MultiCollectionId, - MediaCollectionViewModel Collection, - bool ScheduleAsGroup, - PlaybackOrder PlaybackOrder); -} +namespace ErsatzTV.Application.MediaCollections; + +public record MultiCollectionItemViewModel( + int MultiCollectionId, + MediaCollectionViewModel Collection, + bool ScheduleAsGroup, + PlaybackOrder PlaybackOrder); \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/MultiCollectionSmartItemViewModel.cs b/ErsatzTV.Application/MediaCollections/MultiCollectionSmartItemViewModel.cs index a4202a2f4..46adfa4a8 100644 --- a/ErsatzTV.Application/MediaCollections/MultiCollectionSmartItemViewModel.cs +++ b/ErsatzTV.Application/MediaCollections/MultiCollectionSmartItemViewModel.cs @@ -1,10 +1,9 @@ using ErsatzTV.Core.Domain; -namespace ErsatzTV.Application.MediaCollections -{ - public record MultiCollectionSmartItemViewModel( - int MultiCollectionId, - SmartCollectionViewModel SmartCollection, - bool ScheduleAsGroup, - PlaybackOrder PlaybackOrder); -} +namespace ErsatzTV.Application.MediaCollections; + +public record MultiCollectionSmartItemViewModel( + int MultiCollectionId, + SmartCollectionViewModel SmartCollection, + bool ScheduleAsGroup, + PlaybackOrder PlaybackOrder); \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/MultiCollectionViewModel.cs b/ErsatzTV.Application/MediaCollections/MultiCollectionViewModel.cs index 0e581a115..5cc6bfdd8 100644 --- a/ErsatzTV.Application/MediaCollections/MultiCollectionViewModel.cs +++ b/ErsatzTV.Application/MediaCollections/MultiCollectionViewModel.cs @@ -1,10 +1,7 @@ -using System.Collections.Generic; +namespace ErsatzTV.Application.MediaCollections; -namespace ErsatzTV.Application.MediaCollections -{ - public record MultiCollectionViewModel( - int Id, - string Name, - List Items, - List SmartItems); -} +public record MultiCollectionViewModel( + int Id, + string Name, + List Items, + List SmartItems); \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/PagedMediaCollectionsViewModel.cs b/ErsatzTV.Application/MediaCollections/PagedMediaCollectionsViewModel.cs index bde112dc0..dfef499c3 100644 --- a/ErsatzTV.Application/MediaCollections/PagedMediaCollectionsViewModel.cs +++ b/ErsatzTV.Application/MediaCollections/PagedMediaCollectionsViewModel.cs @@ -1,6 +1,3 @@ -using System.Collections.Generic; +namespace ErsatzTV.Application.MediaCollections; -namespace ErsatzTV.Application.MediaCollections -{ - public record PagedMediaCollectionsViewModel(int TotalCount, List Page); -} +public record PagedMediaCollectionsViewModel(int TotalCount, List Page); \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/PagedMultiCollectionsViewModel.cs b/ErsatzTV.Application/MediaCollections/PagedMultiCollectionsViewModel.cs index f0318701a..e03dbe56d 100644 --- a/ErsatzTV.Application/MediaCollections/PagedMultiCollectionsViewModel.cs +++ b/ErsatzTV.Application/MediaCollections/PagedMultiCollectionsViewModel.cs @@ -1,6 +1,3 @@ -using System.Collections.Generic; +namespace ErsatzTV.Application.MediaCollections; -namespace ErsatzTV.Application.MediaCollections -{ - public record PagedMultiCollectionsViewModel(int TotalCount, List Page); -} +public record PagedMultiCollectionsViewModel(int TotalCount, List Page); \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/PagedSmartCollectionsViewModel.cs b/ErsatzTV.Application/MediaCollections/PagedSmartCollectionsViewModel.cs index 4b761022d..5bf17cb0a 100644 --- a/ErsatzTV.Application/MediaCollections/PagedSmartCollectionsViewModel.cs +++ b/ErsatzTV.Application/MediaCollections/PagedSmartCollectionsViewModel.cs @@ -1,6 +1,3 @@ -using System.Collections.Generic; +namespace ErsatzTV.Application.MediaCollections; -namespace ErsatzTV.Application.MediaCollections -{ - public record PagedSmartCollectionsViewModel(int TotalCount, List Page); -} +public record PagedSmartCollectionsViewModel(int TotalCount, List Page); \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/PagedTraktListsViewModel.cs b/ErsatzTV.Application/MediaCollections/PagedTraktListsViewModel.cs index a0a3f0eb2..1ec2c7466 100644 --- a/ErsatzTV.Application/MediaCollections/PagedTraktListsViewModel.cs +++ b/ErsatzTV.Application/MediaCollections/PagedTraktListsViewModel.cs @@ -1,6 +1,3 @@ -using System.Collections.Generic; +namespace ErsatzTV.Application.MediaCollections; -namespace ErsatzTV.Application.MediaCollections -{ - public record PagedTraktListsViewModel(int TotalCount, List Page); -} +public record PagedTraktListsViewModel(int TotalCount, List Page); \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Queries/GetAllCollections.cs b/ErsatzTV.Application/MediaCollections/Queries/GetAllCollections.cs index 92cbd8427..a3e0c132f 100644 --- a/ErsatzTV.Application/MediaCollections/Queries/GetAllCollections.cs +++ b/ErsatzTV.Application/MediaCollections/Queries/GetAllCollections.cs @@ -1,7 +1,3 @@ -using System.Collections.Generic; -using MediatR; +namespace ErsatzTV.Application.MediaCollections; -namespace ErsatzTV.Application.MediaCollections.Queries -{ - public record GetAllCollections : IRequest>; -} +public record GetAllCollections : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Queries/GetAllCollectionsHandler.cs b/ErsatzTV.Application/MediaCollections/Queries/GetAllCollectionsHandler.cs index 44b4b4f2d..31f586fe4 100644 --- a/ErsatzTV.Application/MediaCollections/Queries/GetAllCollectionsHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Queries/GetAllCollectionsHandler.cs @@ -1,30 +1,23 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Infrastructure.Data; -using LanguageExt; -using MediatR; +using ErsatzTV.Infrastructure.Data; using Microsoft.EntityFrameworkCore; using static ErsatzTV.Application.MediaCollections.Mapper; -namespace ErsatzTV.Application.MediaCollections.Queries +namespace ErsatzTV.Application.MediaCollections; + +public class GetAllCollectionsHandler : IRequestHandler> { - public class GetAllCollectionsHandler : IRequestHandler> + private readonly IDbContextFactory _dbContextFactory; + + public GetAllCollectionsHandler(IDbContextFactory dbContextFactory) => + _dbContextFactory = dbContextFactory; + + public async Task> Handle( + GetAllCollections request, + CancellationToken cancellationToken) { - private readonly IDbContextFactory _dbContextFactory; - - public GetAllCollectionsHandler(IDbContextFactory dbContextFactory) => - _dbContextFactory = dbContextFactory; - - public async Task> Handle( - GetAllCollections request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - return await dbContext.Collections - .ToListAsync(cancellationToken) - .Map(list => list.Map(ProjectToViewModel).ToList()); - } + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + return await dbContext.Collections + .ToListAsync(cancellationToken) + .Map(list => list.Map(ProjectToViewModel).ToList()); } -} +} \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Queries/GetAllMultiCollections.cs b/ErsatzTV.Application/MediaCollections/Queries/GetAllMultiCollections.cs index 2492e9b53..ef22f393f 100644 --- a/ErsatzTV.Application/MediaCollections/Queries/GetAllMultiCollections.cs +++ b/ErsatzTV.Application/MediaCollections/Queries/GetAllMultiCollections.cs @@ -1,7 +1,3 @@ -using System.Collections.Generic; -using MediatR; +namespace ErsatzTV.Application.MediaCollections; -namespace ErsatzTV.Application.MediaCollections.Queries -{ - public record GetAllMultiCollections : IRequest>; -} +public record GetAllMultiCollections : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Queries/GetAllMultiCollectionsHandler.cs b/ErsatzTV.Application/MediaCollections/Queries/GetAllMultiCollectionsHandler.cs index 752712cf0..cf1982f66 100644 --- a/ErsatzTV.Application/MediaCollections/Queries/GetAllMultiCollectionsHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Queries/GetAllMultiCollectionsHandler.cs @@ -1,30 +1,23 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Infrastructure.Data; -using LanguageExt; -using MediatR; +using ErsatzTV.Infrastructure.Data; using Microsoft.EntityFrameworkCore; using static ErsatzTV.Application.MediaCollections.Mapper; -namespace ErsatzTV.Application.MediaCollections.Queries +namespace ErsatzTV.Application.MediaCollections; + +public class GetAllMultiCollectionsHandler : IRequestHandler> { - public class GetAllMultiCollectionsHandler : IRequestHandler> + private readonly IDbContextFactory _dbContextFactory; + + public GetAllMultiCollectionsHandler(IDbContextFactory dbContextFactory) => + _dbContextFactory = dbContextFactory; + + public async Task> Handle( + GetAllMultiCollections request, + CancellationToken cancellationToken) { - private readonly IDbContextFactory _dbContextFactory; - - public GetAllMultiCollectionsHandler(IDbContextFactory dbContextFactory) => - _dbContextFactory = dbContextFactory; - - public async Task> Handle( - GetAllMultiCollections request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - return await dbContext.MultiCollections - .ToListAsync(cancellationToken) - .Map(list => list.Map(ProjectToViewModel).ToList()); - } + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + return await dbContext.MultiCollections + .ToListAsync(cancellationToken) + .Map(list => list.Map(ProjectToViewModel).ToList()); } -} +} \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Queries/GetAllSmartCollections.cs b/ErsatzTV.Application/MediaCollections/Queries/GetAllSmartCollections.cs index 53b1ca9ac..147327ee3 100644 --- a/ErsatzTV.Application/MediaCollections/Queries/GetAllSmartCollections.cs +++ b/ErsatzTV.Application/MediaCollections/Queries/GetAllSmartCollections.cs @@ -1,7 +1,3 @@ -using System.Collections.Generic; -using MediatR; +namespace ErsatzTV.Application.MediaCollections; -namespace ErsatzTV.Application.MediaCollections.Queries -{ - public record GetAllSmartCollections : IRequest>; -} +public record GetAllSmartCollections : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Queries/GetAllSmartCollectionsHandler.cs b/ErsatzTV.Application/MediaCollections/Queries/GetAllSmartCollectionsHandler.cs index f6e3d11b0..2f3648b47 100644 --- a/ErsatzTV.Application/MediaCollections/Queries/GetAllSmartCollectionsHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Queries/GetAllSmartCollectionsHandler.cs @@ -1,30 +1,23 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Infrastructure.Data; -using LanguageExt; -using MediatR; +using ErsatzTV.Infrastructure.Data; using Microsoft.EntityFrameworkCore; using static ErsatzTV.Application.MediaCollections.Mapper; -namespace ErsatzTV.Application.MediaCollections.Queries +namespace ErsatzTV.Application.MediaCollections; + +public class GetAllSmartCollectionsHandler : IRequestHandler> { - public class GetAllSmartCollectionsHandler : IRequestHandler> + private readonly IDbContextFactory _dbContextFactory; + + public GetAllSmartCollectionsHandler(IDbContextFactory dbContextFactory) => + _dbContextFactory = dbContextFactory; + + public async Task> Handle( + GetAllSmartCollections request, + CancellationToken cancellationToken) { - private readonly IDbContextFactory _dbContextFactory; - - public GetAllSmartCollectionsHandler(IDbContextFactory dbContextFactory) => - _dbContextFactory = dbContextFactory; - - public async Task> Handle( - GetAllSmartCollections request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - return await dbContext.SmartCollections - .ToListAsync(cancellationToken) - .Map(list => list.Map(ProjectToViewModel).ToList()); - } + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + return await dbContext.SmartCollections + .ToListAsync(cancellationToken) + .Map(list => list.Map(ProjectToViewModel).ToList()); } -} +} \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Queries/GetCollectionById.cs b/ErsatzTV.Application/MediaCollections/Queries/GetCollectionById.cs index 8d988eecd..71370776b 100644 --- a/ErsatzTV.Application/MediaCollections/Queries/GetCollectionById.cs +++ b/ErsatzTV.Application/MediaCollections/Queries/GetCollectionById.cs @@ -1,7 +1,3 @@ -using LanguageExt; -using MediatR; +namespace ErsatzTV.Application.MediaCollections; -namespace ErsatzTV.Application.MediaCollections.Queries -{ - public record GetCollectionById(int Id) : IRequest>; -} +public record GetCollectionById(int Id) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Queries/GetCollectionByIdHandler.cs b/ErsatzTV.Application/MediaCollections/Queries/GetCollectionByIdHandler.cs index 4d7684d31..567841b94 100644 --- a/ErsatzTV.Application/MediaCollections/Queries/GetCollectionByIdHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Queries/GetCollectionByIdHandler.cs @@ -1,30 +1,25 @@ -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Infrastructure.Data; +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 +namespace ErsatzTV.Application.MediaCollections; + +public class GetCollectionByIdHandler : + IRequestHandler> { - public class GetCollectionByIdHandler : - IRequestHandler> + private readonly IDbContextFactory _dbContextFactory; + + public GetCollectionByIdHandler(IDbContextFactory dbContextFactory) => + _dbContextFactory = dbContextFactory; + + public async Task> Handle( + GetCollectionById request, + CancellationToken cancellationToken) { - private readonly IDbContextFactory _dbContextFactory; - - public GetCollectionByIdHandler(IDbContextFactory dbContextFactory) => - _dbContextFactory = dbContextFactory; - - public async Task> Handle( - GetCollectionById request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - return await dbContext.Collections - .SelectOneAsync(c => c.Id, c => c.Id == request.Id) - .MapT(ProjectToViewModel); - } + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + return await dbContext.Collections + .SelectOneAsync(c => c.Id, c => c.Id == request.Id) + .MapT(ProjectToViewModel); } -} +} \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Queries/GetMultiCollectionById.cs b/ErsatzTV.Application/MediaCollections/Queries/GetMultiCollectionById.cs index d60dabcec..a98200ae5 100644 --- a/ErsatzTV.Application/MediaCollections/Queries/GetMultiCollectionById.cs +++ b/ErsatzTV.Application/MediaCollections/Queries/GetMultiCollectionById.cs @@ -1,7 +1,3 @@ -using LanguageExt; -using MediatR; +namespace ErsatzTV.Application.MediaCollections; -namespace ErsatzTV.Application.MediaCollections.Queries -{ - public record GetMultiCollectionById(int Id) : IRequest>; -} +public record GetMultiCollectionById(int Id) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Queries/GetMultiCollectionByIdHandler.cs b/ErsatzTV.Application/MediaCollections/Queries/GetMultiCollectionByIdHandler.cs index b56285db7..6aed14b61 100644 --- a/ErsatzTV.Application/MediaCollections/Queries/GetMultiCollectionByIdHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Queries/GetMultiCollectionByIdHandler.cs @@ -1,33 +1,28 @@ -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Infrastructure.Data; +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 +namespace ErsatzTV.Application.MediaCollections; + +public class GetMultiCollectionByIdHandler : IRequestHandler> { - public class GetMultiCollectionByIdHandler : IRequestHandler> + private readonly IDbContextFactory _dbContextFactory; + + public GetMultiCollectionByIdHandler(IDbContextFactory dbContextFactory) => + _dbContextFactory = dbContextFactory; + + public async Task> Handle( + GetMultiCollectionById request, + CancellationToken cancellationToken) { - private readonly IDbContextFactory _dbContextFactory; - - public GetMultiCollectionByIdHandler(IDbContextFactory dbContextFactory) => - _dbContextFactory = dbContextFactory; - - public async Task> Handle( - GetMultiCollectionById request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - return await dbContext.MultiCollections - .Include(mc => mc.MultiCollectionItems) - .ThenInclude(mc => mc.Collection) - .Include(mc => mc.MultiCollectionSmartItems) - .ThenInclude(mc => mc.SmartCollection) - .SelectOneAsync(c => c.Id, c => c.Id == request.Id) - .MapT(ProjectToViewModel); - } + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + return await dbContext.MultiCollections + .Include(mc => mc.MultiCollectionItems) + .ThenInclude(mc => mc.Collection) + .Include(mc => mc.MultiCollectionSmartItems) + .ThenInclude(mc => mc.SmartCollection) + .SelectOneAsync(c => c.Id, c => c.Id == request.Id) + .MapT(ProjectToViewModel); } -} +} \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Queries/GetPagedCollections.cs b/ErsatzTV.Application/MediaCollections/Queries/GetPagedCollections.cs index 45dded975..f90fdf714 100644 --- a/ErsatzTV.Application/MediaCollections/Queries/GetPagedCollections.cs +++ b/ErsatzTV.Application/MediaCollections/Queries/GetPagedCollections.cs @@ -1,6 +1,3 @@ -using MediatR; +namespace ErsatzTV.Application.MediaCollections; -namespace ErsatzTV.Application.MediaCollections.Queries -{ - public record GetPagedCollections(int PageNum, int PageSize) : IRequest; -} +public record GetPagedCollections(int PageNum, int PageSize) : IRequest; \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Queries/GetPagedCollectionsHandler.cs b/ErsatzTV.Application/MediaCollections/Queries/GetPagedCollectionsHandler.cs index b002284b9..1d361a0aa 100644 --- a/ErsatzTV.Application/MediaCollections/Queries/GetPagedCollectionsHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Queries/GetPagedCollectionsHandler.cs @@ -1,46 +1,39 @@ -using System.Collections.Generic; -using System.Data; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; +using System.Data; using Dapper; using ErsatzTV.Infrastructure.Data; -using LanguageExt; -using MediatR; using Microsoft.EntityFrameworkCore; using static ErsatzTV.Application.MediaCollections.Mapper; -namespace ErsatzTV.Application.MediaCollections.Queries +namespace ErsatzTV.Application.MediaCollections; + +public class GetPagedCollectionsHandler : IRequestHandler { - public class GetPagedCollectionsHandler : IRequestHandler + private readonly IDbConnection _dbConnection; + private readonly IDbContextFactory _dbContextFactory; + + public GetPagedCollectionsHandler(IDbContextFactory dbContextFactory, IDbConnection dbConnection) { - private readonly IDbConnection _dbConnection; - private readonly IDbContextFactory _dbContextFactory; + _dbContextFactory = dbContextFactory; + _dbConnection = dbConnection; + } - public GetPagedCollectionsHandler(IDbContextFactory dbContextFactory, IDbConnection dbConnection) - { - _dbContextFactory = dbContextFactory; - _dbConnection = dbConnection; - } + public async Task Handle( + GetPagedCollections request, + CancellationToken cancellationToken) + { + int count = await _dbConnection.QuerySingleAsync(@"SELECT COUNT (*) FROM Collection"); - public async Task Handle( - GetPagedCollections request, - CancellationToken cancellationToken) - { - int count = await _dbConnection.QuerySingleAsync(@"SELECT COUNT (*) FROM Collection"); - - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - List page = await dbContext.Collections.FromSqlRaw( - @"SELECT * FROM Collection + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + List page = await dbContext.Collections.FromSqlRaw( + @"SELECT * FROM Collection ORDER BY Name COLLATE NOCASE LIMIT {0} OFFSET {1}", - request.PageSize, - request.PageNum * request.PageSize) - .ToListAsync(cancellationToken) - .Map(list => list.Map(ProjectToViewModel).ToList()); + request.PageSize, + request.PageNum * request.PageSize) + .ToListAsync(cancellationToken) + .Map(list => list.Map(ProjectToViewModel).ToList()); - return new PagedMediaCollectionsViewModel(count, page); - } + return new PagedMediaCollectionsViewModel(count, page); } -} +} \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Queries/GetPagedMultiCollections.cs b/ErsatzTV.Application/MediaCollections/Queries/GetPagedMultiCollections.cs index f000d06b5..cb91f8489 100644 --- a/ErsatzTV.Application/MediaCollections/Queries/GetPagedMultiCollections.cs +++ b/ErsatzTV.Application/MediaCollections/Queries/GetPagedMultiCollections.cs @@ -1,6 +1,3 @@ -using MediatR; +namespace ErsatzTV.Application.MediaCollections; -namespace ErsatzTV.Application.MediaCollections.Queries -{ - public record GetPagedMultiCollections(int PageNum, int PageSize) : IRequest; -} +public record GetPagedMultiCollections(int PageNum, int PageSize) : IRequest; \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Queries/GetPagedMultiCollectionsHandler.cs b/ErsatzTV.Application/MediaCollections/Queries/GetPagedMultiCollectionsHandler.cs index 51dde5d88..b1534c999 100644 --- a/ErsatzTV.Application/MediaCollections/Queries/GetPagedMultiCollectionsHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Queries/GetPagedMultiCollectionsHandler.cs @@ -1,48 +1,41 @@ -using System.Collections.Generic; -using System.Data; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; +using System.Data; using Dapper; using ErsatzTV.Infrastructure.Data; -using LanguageExt; -using MediatR; using Microsoft.EntityFrameworkCore; using static ErsatzTV.Application.MediaCollections.Mapper; -namespace ErsatzTV.Application.MediaCollections.Queries +namespace ErsatzTV.Application.MediaCollections; + +public class GetPagedMultiCollectionsHandler : IRequestHandler { - public class GetPagedMultiCollectionsHandler : IRequestHandler + private readonly IDbConnection _dbConnection; + private readonly IDbContextFactory _dbContextFactory; + + public GetPagedMultiCollectionsHandler(IDbContextFactory dbContextFactory, IDbConnection dbConnection) { - private readonly IDbConnection _dbConnection; - private readonly IDbContextFactory _dbContextFactory; + _dbContextFactory = dbContextFactory; + _dbConnection = dbConnection; + } - public GetPagedMultiCollectionsHandler(IDbContextFactory dbContextFactory, IDbConnection dbConnection) - { - _dbContextFactory = dbContextFactory; - _dbConnection = dbConnection; - } + public async Task Handle( + GetPagedMultiCollections request, + CancellationToken cancellationToken) + { + int count = await _dbConnection.QuerySingleAsync(@"SELECT COUNT (*) FROM MultiCollection"); - public async Task Handle( - GetPagedMultiCollections request, - CancellationToken cancellationToken) - { - int count = await _dbConnection.QuerySingleAsync(@"SELECT COUNT (*) FROM MultiCollection"); - - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - List page = await dbContext.MultiCollections.FromSqlRaw( - @"SELECT * FROM MultiCollection + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + List page = await dbContext.MultiCollections.FromSqlRaw( + @"SELECT * FROM MultiCollection ORDER BY Name COLLATE NOCASE LIMIT {0} OFFSET {1}", - request.PageSize, - request.PageNum * request.PageSize) - .Include(mc => mc.MultiCollectionItems) - .ThenInclude(i => i.Collection) - .ToListAsync(cancellationToken) - .Map(list => list.Map(ProjectToViewModel).ToList()); + request.PageSize, + request.PageNum * request.PageSize) + .Include(mc => mc.MultiCollectionItems) + .ThenInclude(i => i.Collection) + .ToListAsync(cancellationToken) + .Map(list => list.Map(ProjectToViewModel).ToList()); - return new PagedMultiCollectionsViewModel(count, page); - } + return new PagedMultiCollectionsViewModel(count, page); } -} +} \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Queries/GetPagedSmartCollections.cs b/ErsatzTV.Application/MediaCollections/Queries/GetPagedSmartCollections.cs index 7fa74cfcd..c0a1849b9 100644 --- a/ErsatzTV.Application/MediaCollections/Queries/GetPagedSmartCollections.cs +++ b/ErsatzTV.Application/MediaCollections/Queries/GetPagedSmartCollections.cs @@ -1,6 +1,3 @@ -using MediatR; +namespace ErsatzTV.Application.MediaCollections; -namespace ErsatzTV.Application.MediaCollections.Queries -{ - public record GetPagedSmartCollections(int PageNum, int PageSize) : IRequest; -} +public record GetPagedSmartCollections(int PageNum, int PageSize) : IRequest; \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Queries/GetPagedSmartCollectionsHandler.cs b/ErsatzTV.Application/MediaCollections/Queries/GetPagedSmartCollectionsHandler.cs index fc42a61c9..a4915be00 100644 --- a/ErsatzTV.Application/MediaCollections/Queries/GetPagedSmartCollectionsHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Queries/GetPagedSmartCollectionsHandler.cs @@ -1,46 +1,39 @@ -using System.Collections.Generic; -using System.Data; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; +using System.Data; using Dapper; using ErsatzTV.Infrastructure.Data; -using LanguageExt; -using MediatR; using Microsoft.EntityFrameworkCore; using static ErsatzTV.Application.MediaCollections.Mapper; -namespace ErsatzTV.Application.MediaCollections.Queries +namespace ErsatzTV.Application.MediaCollections; + +public class GetPagedSmartCollectionsHandler : IRequestHandler { - public class GetPagedSmartCollectionsHandler : IRequestHandler + private readonly IDbConnection _dbConnection; + private readonly IDbContextFactory _dbContextFactory; + + public GetPagedSmartCollectionsHandler(IDbContextFactory dbContextFactory, IDbConnection dbConnection) { - private readonly IDbConnection _dbConnection; - private readonly IDbContextFactory _dbContextFactory; + _dbContextFactory = dbContextFactory; + _dbConnection = dbConnection; + } - public GetPagedSmartCollectionsHandler(IDbContextFactory dbContextFactory, IDbConnection dbConnection) - { - _dbContextFactory = dbContextFactory; - _dbConnection = dbConnection; - } + public async Task Handle( + GetPagedSmartCollections request, + CancellationToken cancellationToken) + { + int count = await _dbConnection.QuerySingleAsync(@"SELECT COUNT (*) FROM SmartCollection"); - public async Task Handle( - GetPagedSmartCollections request, - CancellationToken cancellationToken) - { - int count = await _dbConnection.QuerySingleAsync(@"SELECT COUNT (*) FROM SmartCollection"); - - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - List page = await dbContext.SmartCollections.FromSqlRaw( - @"SELECT * FROM SmartCollection + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + List page = await dbContext.SmartCollections.FromSqlRaw( + @"SELECT * FROM SmartCollection ORDER BY Name COLLATE NOCASE LIMIT {0} OFFSET {1}", - request.PageSize, - request.PageNum * request.PageSize) - .ToListAsync(cancellationToken) - .Map(list => list.Map(ProjectToViewModel).ToList()); + request.PageSize, + request.PageNum * request.PageSize) + .ToListAsync(cancellationToken) + .Map(list => list.Map(ProjectToViewModel).ToList()); - return new PagedSmartCollectionsViewModel(count, page); - } + return new PagedSmartCollectionsViewModel(count, page); } -} +} \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Queries/GetPagedTraktLists.cs b/ErsatzTV.Application/MediaCollections/Queries/GetPagedTraktLists.cs index 5f28b1efc..e6771f0c4 100644 --- a/ErsatzTV.Application/MediaCollections/Queries/GetPagedTraktLists.cs +++ b/ErsatzTV.Application/MediaCollections/Queries/GetPagedTraktLists.cs @@ -1,6 +1,3 @@ -using MediatR; +namespace ErsatzTV.Application.MediaCollections; -namespace ErsatzTV.Application.MediaCollections.Queries -{ - public record GetPagedTraktLists(int PageNum, int PageSize) : IRequest; -} +public record GetPagedTraktLists(int PageNum, int PageSize) : IRequest; \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/Queries/GetPagedTraktListsHandler.cs b/ErsatzTV.Application/MediaCollections/Queries/GetPagedTraktListsHandler.cs index 88624dff7..826e13665 100644 --- a/ErsatzTV.Application/MediaCollections/Queries/GetPagedTraktListsHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Queries/GetPagedTraktListsHandler.cs @@ -1,47 +1,40 @@ -using System.Collections.Generic; -using System.Data; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; +using System.Data; using Dapper; using ErsatzTV.Infrastructure.Data; -using LanguageExt; -using MediatR; using Microsoft.EntityFrameworkCore; using static ErsatzTV.Application.MediaCollections.Mapper; -namespace ErsatzTV.Application.MediaCollections.Queries +namespace ErsatzTV.Application.MediaCollections; + +public class GetPagedTraktListsHandler : IRequestHandler { - public class GetPagedTraktListsHandler : IRequestHandler + private readonly IDbConnection _dbConnection; + private readonly IDbContextFactory _dbContextFactory; + + public GetPagedTraktListsHandler(IDbContextFactory dbContextFactory, IDbConnection dbConnection) { - private readonly IDbConnection _dbConnection; - private readonly IDbContextFactory _dbContextFactory; + _dbContextFactory = dbContextFactory; + _dbConnection = dbConnection; + } - public GetPagedTraktListsHandler(IDbContextFactory dbContextFactory, IDbConnection dbConnection) - { - _dbContextFactory = dbContextFactory; - _dbConnection = dbConnection; - } + public async Task Handle( + GetPagedTraktLists request, + CancellationToken cancellationToken) + { + int count = await _dbConnection.QuerySingleAsync(@"SELECT COUNT (*) FROM TraktList"); - public async Task Handle( - GetPagedTraktLists request, - CancellationToken cancellationToken) - { - int count = await _dbConnection.QuerySingleAsync(@"SELECT COUNT (*) FROM TraktList"); - - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - List page = await dbContext.TraktLists.FromSqlRaw( - @"SELECT * FROM TraktList + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + List page = await dbContext.TraktLists.FromSqlRaw( + @"SELECT * FROM TraktList ORDER BY Name COLLATE NOCASE LIMIT {0} OFFSET {1}", - request.PageSize, - request.PageNum * request.PageSize) - .Include(l => l.Items) - .ToListAsync(cancellationToken) - .Map(list => list.Map(ProjectToViewModel).ToList()); + request.PageSize, + request.PageNum * request.PageSize) + .Include(l => l.Items) + .ToListAsync(cancellationToken) + .Map(list => list.Map(ProjectToViewModel).ToList()); - return new PagedTraktListsViewModel(count, page); - } + return new PagedTraktListsViewModel(count, page); } -} +} \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/SmartCollectionViewModel.cs b/ErsatzTV.Application/MediaCollections/SmartCollectionViewModel.cs index 07d032251..487ca1be5 100644 --- a/ErsatzTV.Application/MediaCollections/SmartCollectionViewModel.cs +++ b/ErsatzTV.Application/MediaCollections/SmartCollectionViewModel.cs @@ -1,4 +1,3 @@ -namespace ErsatzTV.Application.MediaCollections -{ - public record SmartCollectionViewModel(int Id, string Name, string Query); -} +namespace ErsatzTV.Application.MediaCollections; + +public record SmartCollectionViewModel(int Id, string Name, string Query); \ No newline at end of file diff --git a/ErsatzTV.Application/MediaCollections/TraktListViewModel.cs b/ErsatzTV.Application/MediaCollections/TraktListViewModel.cs index 9c96c1009..2c5be7ab6 100644 --- a/ErsatzTV.Application/MediaCollections/TraktListViewModel.cs +++ b/ErsatzTV.Application/MediaCollections/TraktListViewModel.cs @@ -1,4 +1,3 @@ -namespace ErsatzTV.Application.MediaCollections -{ - public record TraktListViewModel(int Id, int TraktId, string Slug, string Name, int ItemCount, int MatchCount); -} +namespace ErsatzTV.Application.MediaCollections; + +public record TraktListViewModel(int Id, int TraktId, string Slug, string Name, int ItemCount, int MatchCount); \ No newline at end of file diff --git a/ErsatzTV.Application/MediaItems/Mapper.cs b/ErsatzTV.Application/MediaItems/Mapper.cs index 364631feb..5bd82a910 100644 --- a/ErsatzTV.Application/MediaItems/Mapper.cs +++ b/ErsatzTV.Application/MediaItems/Mapper.cs @@ -1,22 +1,21 @@ using ErsatzTV.Core.Domain; -namespace ErsatzTV.Application.MediaItems +namespace ErsatzTV.Application.MediaItems; + +internal static class Mapper { - internal static class Mapper - { - internal static NamedMediaItemViewModel ProjectToViewModel(Show show) => - new(show.Id, show.ShowMetadata.HeadOrNone().Map(sm => $"{sm?.Title} ({sm?.Year})").IfNone("???")); + internal static NamedMediaItemViewModel ProjectToViewModel(Show show) => + new(show.Id, show.ShowMetadata.HeadOrNone().Map(sm => $"{sm?.Title} ({sm?.Year})").IfNone("???")); - internal static NamedMediaItemViewModel ProjectToViewModel(Season season) => - new(season.Id, $"{ShowTitle(season)} ({SeasonDescription(season)})"); + internal static NamedMediaItemViewModel ProjectToViewModel(Season season) => + new(season.Id, $"{ShowTitle(season)} ({SeasonDescription(season)})"); - internal static NamedMediaItemViewModel ProjectToViewModel(Artist artist) => - new(artist.Id, artist.ArtistMetadata.HeadOrNone().Match(am => am.Title, () => "???")); + internal static NamedMediaItemViewModel ProjectToViewModel(Artist artist) => + new(artist.Id, artist.ArtistMetadata.HeadOrNone().Match(am => am.Title, () => "???")); - private static string ShowTitle(Season season) => - season.Show.ShowMetadata.HeadOrNone().Map(sm => sm.Title).IfNone("???"); + private static string ShowTitle(Season season) => + season.Show.ShowMetadata.HeadOrNone().Map(sm => sm.Title).IfNone("???"); - private static string SeasonDescription(Season season) => - season.SeasonNumber == 0 ? "Specials" : $"Season {season.SeasonNumber}"; - } -} + private static string SeasonDescription(Season season) => + season.SeasonNumber == 0 ? "Specials" : $"Season {season.SeasonNumber}"; +} \ No newline at end of file diff --git a/ErsatzTV.Application/MediaItems/NamedMediaItemViewModel.cs b/ErsatzTV.Application/MediaItems/NamedMediaItemViewModel.cs index 8af3f337a..af889ae8a 100644 --- a/ErsatzTV.Application/MediaItems/NamedMediaItemViewModel.cs +++ b/ErsatzTV.Application/MediaItems/NamedMediaItemViewModel.cs @@ -1,4 +1,3 @@ -namespace ErsatzTV.Application.MediaItems -{ - public record NamedMediaItemViewModel(int MediaItemId, string Name); -} +namespace ErsatzTV.Application.MediaItems; + +public record NamedMediaItemViewModel(int MediaItemId, string Name); \ No newline at end of file diff --git a/ErsatzTV.Application/MediaItems/Queries/GetAllLanguageCodes.cs b/ErsatzTV.Application/MediaItems/Queries/GetAllLanguageCodes.cs index c9145ea10..f0edf0e64 100644 --- a/ErsatzTV.Application/MediaItems/Queries/GetAllLanguageCodes.cs +++ b/ErsatzTV.Application/MediaItems/Queries/GetAllLanguageCodes.cs @@ -1,8 +1,5 @@ -using System.Collections.Generic; -using System.Globalization; -using MediatR; +using System.Globalization; -namespace ErsatzTV.Application.MediaItems.Queries -{ - public record GetAllLanguageCodes : IRequest>; -} +namespace ErsatzTV.Application.MediaItems; + +public record GetAllLanguageCodes : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/MediaItems/Queries/GetAllLanguageCodesHandler.cs b/ErsatzTV.Application/MediaItems/Queries/GetAllLanguageCodesHandler.cs index 275f53ff3..ea37fdd1e 100644 --- a/ErsatzTV.Application/MediaItems/Queries/GetAllLanguageCodesHandler.cs +++ b/ErsatzTV.Application/MediaItems/Queries/GetAllLanguageCodesHandler.cs @@ -1,53 +1,45 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; +using System.Globalization; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; -using LanguageExt; -using MediatR; using Microsoft.EntityFrameworkCore; -namespace ErsatzTV.Application.MediaItems.Queries +namespace ErsatzTV.Application.MediaItems; + +public class GetAllLanguageCodesHandler : IRequestHandler> { - public class GetAllLanguageCodesHandler : IRequestHandler> + private readonly IDbContextFactory _dbContextFactory; + private readonly IMediaItemRepository _mediaItemRepository; + + public GetAllLanguageCodesHandler( + IDbContextFactory dbContextFactory, + IMediaItemRepository mediaItemRepository) { - private readonly IDbContextFactory _dbContextFactory; - private readonly IMediaItemRepository _mediaItemRepository; + _dbContextFactory = dbContextFactory; + _mediaItemRepository = mediaItemRepository; + } - public GetAllLanguageCodesHandler( - IDbContextFactory dbContextFactory, - IMediaItemRepository mediaItemRepository) - { - _dbContextFactory = dbContextFactory; - _mediaItemRepository = mediaItemRepository; - } - - public async Task> Handle(GetAllLanguageCodes request, CancellationToken cancellationToken) - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + public async Task> Handle(GetAllLanguageCodes request, CancellationToken cancellationToken) + { + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - var result = new System.Collections.Generic.HashSet(); + var result = new System.Collections.Generic.HashSet(); - CultureInfo[] allCultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures); - List mediaCodes = await _mediaItemRepository.GetAllLanguageCodes(); - foreach (string mediaCode in mediaCodes) + CultureInfo[] allCultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures); + List mediaCodes = await _mediaItemRepository.GetAllLanguageCodes(); + foreach (string mediaCode in mediaCodes) + { + foreach (string code in await dbContext.LanguageCodes.GetAllLanguageCodes(mediaCode)) { - foreach (string code in await dbContext.LanguageCodes.GetAllLanguageCodes(mediaCode)) + Option maybeCulture = allCultures.Find( + c => string.Equals(code, c.ThreeLetterISOLanguageName, StringComparison.OrdinalIgnoreCase)); + foreach (CultureInfo culture in maybeCulture) { - Option maybeCulture = allCultures.Find( - c => string.Equals(code, c.ThreeLetterISOLanguageName, StringComparison.OrdinalIgnoreCase)); - foreach (CultureInfo culture in maybeCulture) - { - result.Add(culture); - } + result.Add(culture); } } - - return result.ToList(); } + + return result.ToList(); } -} +} \ No newline at end of file diff --git a/ErsatzTV.Application/MediaSources/Commands/ScanLocalLibrary.cs b/ErsatzTV.Application/MediaSources/Commands/ScanLocalLibrary.cs index da4999e12..74faa99ae 100644 --- a/ErsatzTV.Application/MediaSources/Commands/ScanLocalLibrary.cs +++ b/ErsatzTV.Application/MediaSources/Commands/ScanLocalLibrary.cs @@ -1,22 +1,19 @@ using ErsatzTV.Core; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.MediaSources.Commands +namespace ErsatzTV.Application.MediaSources; + +public interface IScanLocalLibrary : IRequest>, IBackgroundServiceRequest { - public interface IScanLocalLibrary : IRequest>, IBackgroundServiceRequest - { - int LibraryId { get; } - bool ForceScan { get; } - } - - public record ScanLocalLibraryIfNeeded(int LibraryId) : IScanLocalLibrary - { - public bool ForceScan => false; - } - - public record ForceScanLocalLibrary(int LibraryId) : IScanLocalLibrary - { - public bool ForceScan => true; - } + int LibraryId { get; } + bool ForceScan { get; } } + +public record ScanLocalLibraryIfNeeded(int LibraryId) : IScanLocalLibrary +{ + public bool ForceScan => false; +} + +public record ForceScanLocalLibrary(int LibraryId) : IScanLocalLibrary +{ + public bool ForceScan => true; +} \ No newline at end of file diff --git a/ErsatzTV.Application/MediaSources/Commands/ScanLocalLibraryHandler.cs b/ErsatzTV.Application/MediaSources/Commands/ScanLocalLibraryHandler.cs index 57dd9b6f1..64b21dcb0 100644 --- a/ErsatzTV.Application/MediaSources/Commands/ScanLocalLibraryHandler.cs +++ b/ErsatzTV.Application/MediaSources/Commands/ScanLocalLibraryHandler.cs @@ -1,226 +1,218 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.Threading; -using System.Threading.Tasks; +using System.Diagnostics; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Locking; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Metadata; -using LanguageExt; -using MediatR; using Microsoft.Extensions.Logging; -using Unit = LanguageExt.Unit; -namespace ErsatzTV.Application.MediaSources.Commands +namespace ErsatzTV.Application.MediaSources; + +public class ScanLocalLibraryHandler : IRequestHandler>, + IRequestHandler> { - public class ScanLocalLibraryHandler : IRequestHandler>, - IRequestHandler> + private readonly IConfigElementRepository _configElementRepository; + private readonly IEntityLocker _entityLocker; + private readonly ILibraryRepository _libraryRepository; + private readonly ILogger _logger; + private readonly IMediator _mediator; + private readonly IMovieFolderScanner _movieFolderScanner; + private readonly IMusicVideoFolderScanner _musicVideoFolderScanner; + private readonly IOtherVideoFolderScanner _otherVideoFolderScanner; + private readonly ISongFolderScanner _songFolderScanner; + private readonly ITelevisionFolderScanner _televisionFolderScanner; + + public ScanLocalLibraryHandler( + ILibraryRepository libraryRepository, + IConfigElementRepository configElementRepository, + IMovieFolderScanner movieFolderScanner, + ITelevisionFolderScanner televisionFolderScanner, + IMusicVideoFolderScanner musicVideoFolderScanner, + IOtherVideoFolderScanner otherVideoFolderScanner, + ISongFolderScanner songFolderScanner, + IEntityLocker entityLocker, + IMediator mediator, + ILogger logger) { - private readonly IConfigElementRepository _configElementRepository; - private readonly IEntityLocker _entityLocker; - private readonly ILibraryRepository _libraryRepository; - private readonly ILogger _logger; - private readonly IMediator _mediator; - private readonly IMovieFolderScanner _movieFolderScanner; - private readonly IMusicVideoFolderScanner _musicVideoFolderScanner; - private readonly IOtherVideoFolderScanner _otherVideoFolderScanner; - private readonly ISongFolderScanner _songFolderScanner; - private readonly ITelevisionFolderScanner _televisionFolderScanner; + _libraryRepository = libraryRepository; + _configElementRepository = configElementRepository; + _movieFolderScanner = movieFolderScanner; + _televisionFolderScanner = televisionFolderScanner; + _musicVideoFolderScanner = musicVideoFolderScanner; + _otherVideoFolderScanner = otherVideoFolderScanner; + _songFolderScanner = songFolderScanner; + _entityLocker = entityLocker; + _mediator = mediator; + _logger = logger; + } - public ScanLocalLibraryHandler( - ILibraryRepository libraryRepository, - IConfigElementRepository configElementRepository, - IMovieFolderScanner movieFolderScanner, - ITelevisionFolderScanner televisionFolderScanner, - IMusicVideoFolderScanner musicVideoFolderScanner, - IOtherVideoFolderScanner otherVideoFolderScanner, - ISongFolderScanner songFolderScanner, - IEntityLocker entityLocker, - IMediator mediator, - ILogger logger) + public Task> Handle( + ForceScanLocalLibrary request, + CancellationToken cancellationToken) => Handle(request); + + public Task> Handle( + ScanLocalLibraryIfNeeded request, + CancellationToken cancellationToken) => Handle(request); + + private Task> + Handle(IScanLocalLibrary request) => + Validate(request) + .MapT(parameters => PerformScan(parameters).Map(_ => parameters.LocalLibrary.Name)) + .Bind(v => v.ToEitherAsync()); + + private async Task PerformScan(RequestParameters parameters) + { + (LocalLibrary localLibrary, string ffprobePath, string ffmpegPath, bool forceScan, + int libraryRefreshInterval) = parameters; + + var sw = new Stopwatch(); + sw.Start(); + + var scanned = false; + + for (var i = 0; i < localLibrary.Paths.Count; i++) { - _libraryRepository = libraryRepository; - _configElementRepository = configElementRepository; - _movieFolderScanner = movieFolderScanner; - _televisionFolderScanner = televisionFolderScanner; - _musicVideoFolderScanner = musicVideoFolderScanner; - _otherVideoFolderScanner = otherVideoFolderScanner; - _songFolderScanner = songFolderScanner; - _entityLocker = entityLocker; - _mediator = mediator; - _logger = logger; - } + LibraryPath libraryPath = localLibrary.Paths[i]; - public Task> Handle( - ForceScanLocalLibrary request, - CancellationToken cancellationToken) => Handle(request); + decimal progressMin = (decimal) i / localLibrary.Paths.Count; + decimal progressMax = (decimal) (i + 1) / localLibrary.Paths.Count; - public Task> Handle( - ScanLocalLibraryIfNeeded request, - CancellationToken cancellationToken) => Handle(request); - - private Task> - Handle(IScanLocalLibrary request) => - Validate(request) - .MapT(parameters => PerformScan(parameters).Map(_ => parameters.LocalLibrary.Name)) - .Bind(v => v.ToEitherAsync()); - - private async Task PerformScan(RequestParameters parameters) - { - (LocalLibrary localLibrary, string ffprobePath, string ffmpegPath, bool forceScan, - int libraryRefreshInterval) = parameters; - - var sw = new Stopwatch(); - sw.Start(); - - var scanned = false; - - for (var i = 0; i < localLibrary.Paths.Count; i++) + var lastScan = new DateTimeOffset(libraryPath.LastScan ?? SystemTime.MinValueUtc, TimeSpan.Zero); + DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(libraryRefreshInterval); + if (forceScan || nextScan < DateTimeOffset.Now) { - LibraryPath libraryPath = localLibrary.Paths[i]; + scanned = true; - decimal progressMin = (decimal) i / localLibrary.Paths.Count; - decimal progressMax = (decimal) (i + 1) / localLibrary.Paths.Count; - - var lastScan = new DateTimeOffset(libraryPath.LastScan ?? SystemTime.MinValueUtc, TimeSpan.Zero); - DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(libraryRefreshInterval); - if (forceScan || nextScan < DateTimeOffset.Now) + switch (localLibrary.MediaKind) { - scanned = true; - - switch (localLibrary.MediaKind) - { - case LibraryMediaKind.Movies: - await _movieFolderScanner.ScanFolder( - libraryPath, - ffprobePath, - progressMin, - progressMax); - break; - case LibraryMediaKind.Shows: - await _televisionFolderScanner.ScanFolder( - libraryPath, - ffprobePath, - progressMin, - progressMax); - break; - case LibraryMediaKind.MusicVideos: - await _musicVideoFolderScanner.ScanFolder( - libraryPath, - ffprobePath, - progressMin, - progressMax); - break; - case LibraryMediaKind.OtherVideos: - await _otherVideoFolderScanner.ScanFolder( - libraryPath, - ffprobePath, - progressMin, - progressMax); - break; - case LibraryMediaKind.Songs: - await _songFolderScanner.ScanFolder( - libraryPath, - ffprobePath, - ffmpegPath, - progressMin, - progressMax); - break; - } - - libraryPath.LastScan = DateTime.UtcNow; - await _libraryRepository.UpdateLastScan(libraryPath); - } - - await _mediator.Publish(new LibraryScanProgress(libraryPath.LibraryId, progressMax)); - } - - sw.Stop(); - - if (scanned) - { - _logger.LogDebug( - "Scan of library {Name} completed in {Duration}", - localLibrary.Name, - TimeSpan.FromMilliseconds(sw.ElapsedMilliseconds)); - } - else - { - _logger.LogDebug( - "Skipping unforced scan of local media library {Name}", - localLibrary.Name); - } - - await _mediator.Publish(new LibraryScanProgress(localLibrary.Id, 0)); - - _entityLocker.UnlockLibrary(localLibrary.Id); - return Unit.Default; - } - - private async Task> Validate(IScanLocalLibrary request) - { - Validation libraryResult = await LocalLibraryMustExist(request); - Validation ffprobePathResult = await ValidateFFprobePath(); - Validation ffmpegPathResult = await ValidateFFmpegPath(); - Validation refreshIntervalResult = await ValidateLibraryRefreshInterval(); - - try - { - return (libraryResult, ffprobePathResult, ffmpegPathResult, refreshIntervalResult) - .Apply( - (library, ffprobePath, ffmpegPath, libraryRefreshInterval) => new RequestParameters( - library, + case LibraryMediaKind.Movies: + await _movieFolderScanner.ScanFolder( + libraryPath, + ffprobePath, + progressMin, + progressMax); + break; + case LibraryMediaKind.Shows: + await _televisionFolderScanner.ScanFolder( + libraryPath, + ffprobePath, + progressMin, + progressMax); + break; + case LibraryMediaKind.MusicVideos: + await _musicVideoFolderScanner.ScanFolder( + libraryPath, + ffprobePath, + progressMin, + progressMax); + break; + case LibraryMediaKind.OtherVideos: + await _otherVideoFolderScanner.ScanFolder( + libraryPath, + ffprobePath, + progressMin, + progressMax); + break; + case LibraryMediaKind.Songs: + await _songFolderScanner.ScanFolder( + libraryPath, ffprobePath, ffmpegPath, - request.ForceScan, - libraryRefreshInterval)); + progressMin, + progressMax); + break; + } + + libraryPath.LastScan = DateTime.UtcNow; + await _libraryRepository.UpdateLastScan(libraryPath); } - finally + + await _mediator.Publish(new LibraryScanProgress(libraryPath.LibraryId, progressMax)); + } + + sw.Stop(); + + if (scanned) + { + _logger.LogDebug( + "Scan of library {Name} completed in {Duration}", + localLibrary.Name, + TimeSpan.FromMilliseconds(sw.ElapsedMilliseconds)); + } + else + { + _logger.LogDebug( + "Skipping unforced scan of local media library {Name}", + localLibrary.Name); + } + + await _mediator.Publish(new LibraryScanProgress(localLibrary.Id, 0)); + + _entityLocker.UnlockLibrary(localLibrary.Id); + return Unit.Default; + } + + private async Task> Validate(IScanLocalLibrary request) + { + Validation libraryResult = await LocalLibraryMustExist(request); + Validation ffprobePathResult = await ValidateFFprobePath(); + Validation ffmpegPathResult = await ValidateFFmpegPath(); + Validation refreshIntervalResult = await ValidateLibraryRefreshInterval(); + + try + { + return (libraryResult, ffprobePathResult, ffmpegPathResult, refreshIntervalResult) + .Apply( + (library, ffprobePath, ffmpegPath, libraryRefreshInterval) => new RequestParameters( + library, + ffprobePath, + ffmpegPath, + request.ForceScan, + libraryRefreshInterval)); + } + finally + { + // ensure we unlock the library if any validation is unsuccessful + foreach (LocalLibrary library in libraryResult.SuccessToSeq()) { - // ensure we unlock the library if any validation is unsuccessful - foreach (LocalLibrary library in libraryResult.SuccessToSeq()) + if (ffprobePathResult.IsFail || ffmpegPathResult.IsFail || refreshIntervalResult.IsFail) { - if (ffprobePathResult.IsFail || ffmpegPathResult.IsFail || refreshIntervalResult.IsFail) - { - _entityLocker.UnlockLibrary(library.Id); - } + _entityLocker.UnlockLibrary(library.Id); } } } - - private Task> LocalLibraryMustExist( - IScanLocalLibrary request) => - _libraryRepository.Get(request.LibraryId) - .Map(maybeLibrary => maybeLibrary.Map(ms => ms as LocalLibrary)) - .Map(v => v.ToValidation($"Local library {request.LibraryId} does not exist.")); - - private Task> ValidateFFprobePath() => - _configElementRepository.GetValue(ConfigElementKey.FFprobePath) - .FilterT(File.Exists) - .Map( - ffprobePath => - ffprobePath.ToValidation("FFprobe path does not exist on the file system")); - - private Task> ValidateFFmpegPath() => - _configElementRepository.GetValue(ConfigElementKey.FFmpegPath) - .FilterT(File.Exists) - .Map( - ffmpegPath => - ffmpegPath.ToValidation("FFmpeg path does not exist on the file system")); - - private Task> ValidateLibraryRefreshInterval() => - _configElementRepository.GetValue(ConfigElementKey.LibraryRefreshInterval) - .FilterT(lri => lri > 0) - .Map(lri => lri.ToValidation("Library refresh interval is invalid")); - - private record RequestParameters( - LocalLibrary LocalLibrary, - string FFprobePath, - string FFmpegPath, - bool ForceScan, - int LibraryRefreshInterval); } -} + + private Task> LocalLibraryMustExist( + IScanLocalLibrary request) => + _libraryRepository.Get(request.LibraryId) + .Map(maybeLibrary => maybeLibrary.Map(ms => ms as LocalLibrary)) + .Map(v => v.ToValidation($"Local library {request.LibraryId} does not exist.")); + + private Task> ValidateFFprobePath() => + _configElementRepository.GetValue(ConfigElementKey.FFprobePath) + .FilterT(File.Exists) + .Map( + ffprobePath => + ffprobePath.ToValidation("FFprobe path does not exist on the file system")); + + private Task> ValidateFFmpegPath() => + _configElementRepository.GetValue(ConfigElementKey.FFmpegPath) + .FilterT(File.Exists) + .Map( + ffmpegPath => + ffmpegPath.ToValidation("FFmpeg path does not exist on the file system")); + + private Task> ValidateLibraryRefreshInterval() => + _configElementRepository.GetValue(ConfigElementKey.LibraryRefreshInterval) + .FilterT(lri => lri > 0) + .Map(lri => lri.ToValidation("Library refresh interval is invalid")); + + private record RequestParameters( + LocalLibrary LocalLibrary, + string FFprobePath, + string FFmpegPath, + bool ForceScan, + int LibraryRefreshInterval); +} \ No newline at end of file diff --git a/ErsatzTV.Application/MediaSources/LocalMediaSourceViewModel.cs b/ErsatzTV.Application/MediaSources/LocalMediaSourceViewModel.cs index 96111acf5..90d012b27 100644 --- a/ErsatzTV.Application/MediaSources/LocalMediaSourceViewModel.cs +++ b/ErsatzTV.Application/MediaSources/LocalMediaSourceViewModel.cs @@ -1,4 +1,3 @@ -namespace ErsatzTV.Application.MediaSources -{ - public record LocalMediaSourceViewModel(int Id) : MediaSourceViewModel(Id, "Local"); -} +namespace ErsatzTV.Application.MediaSources; + +public record LocalMediaSourceViewModel(int Id) : MediaSourceViewModel(Id, "Local"); \ No newline at end of file diff --git a/ErsatzTV.Application/MediaSources/MediaSourceViewModel.cs b/ErsatzTV.Application/MediaSources/MediaSourceViewModel.cs index 036a23d78..bb829f5a0 100644 --- a/ErsatzTV.Application/MediaSources/MediaSourceViewModel.cs +++ b/ErsatzTV.Application/MediaSources/MediaSourceViewModel.cs @@ -1,4 +1,3 @@ -namespace ErsatzTV.Application.MediaSources -{ - public record MediaSourceViewModel(int Id, string Name); -} +namespace ErsatzTV.Application.MediaSources; + +public record MediaSourceViewModel(int Id, string Name); \ No newline at end of file diff --git a/ErsatzTV.Application/MediaSources/RemoteMediaSourceViewModel.cs b/ErsatzTV.Application/MediaSources/RemoteMediaSourceViewModel.cs index 7ae53a49f..0b00f2aee 100644 --- a/ErsatzTV.Application/MediaSources/RemoteMediaSourceViewModel.cs +++ b/ErsatzTV.Application/MediaSources/RemoteMediaSourceViewModel.cs @@ -1,4 +1,3 @@ -namespace ErsatzTV.Application.MediaSources -{ - public record RemoteMediaSourceViewModel(int Id, string Name, string Address) : MediaSourceViewModel(Id, Name); -} +namespace ErsatzTV.Application.MediaSources; + +public record RemoteMediaSourceViewModel(int Id, string Name, string Address) : MediaSourceViewModel(Id, Name); \ No newline at end of file diff --git a/ErsatzTV.Application/Movies/Mapper.cs b/ErsatzTV.Application/Movies/Mapper.cs index 2f954ad33..f958ae562 100644 --- a/ErsatzTV.Application/Movies/Mapper.cs +++ b/ErsatzTV.Application/Movies/Mapper.cs @@ -1,94 +1,88 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; +using System.Globalization; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Emby; using ErsatzTV.Core.Extensions; using ErsatzTV.Core.Jellyfin; using Flurl; -using LanguageExt; -using static LanguageExt.Prelude; -namespace ErsatzTV.Application.Movies +namespace ErsatzTV.Application.Movies; + +internal static class Mapper { - internal static class Mapper + internal static MovieViewModel ProjectToViewModel( + Movie movie, + List languageCodes, + Option maybeJellyfin, + Option maybeEmby) { - internal static MovieViewModel ProjectToViewModel( - Movie movie, - List languageCodes, - Option maybeJellyfin, - Option maybeEmby) + MovieMetadata metadata = Optional(movie.MovieMetadata).Flatten().Head(); + return new MovieViewModel( + metadata.Title, + metadata.Year?.ToString(), + metadata.Plot, + metadata.Genres.Map(g => g.Name).ToList(), + metadata.Tags.Map(t => t.Name).ToList(), + metadata.Studios.Map(s => s.Name).ToList(), + (metadata.ContentRating ?? string.Empty).Split("/").Map(s => s.Trim()) + .Where(x => !string.IsNullOrWhiteSpace(x)).ToList(), + LanguagesForMovie(languageCodes), + metadata.Actors.OrderBy(a => a.Order).ThenBy(a => a.Id) + .Map(a => MediaCards.Mapper.ProjectToViewModel(a, maybeJellyfin, maybeEmby)) + .ToList(), + metadata.Directors.Map(d => d.Name).ToList(), + metadata.Writers.Map(w => w.Name).ToList(), + movie.GetHeadVersion().MediaFiles.Head().Path, + movie.State) { - MovieMetadata metadata = Optional(movie.MovieMetadata).Flatten().Head(); - return new MovieViewModel( - metadata.Title, - metadata.Year?.ToString(), - metadata.Plot, - metadata.Genres.Map(g => g.Name).ToList(), - metadata.Tags.Map(t => t.Name).ToList(), - metadata.Studios.Map(s => s.Name).ToList(), - (metadata.ContentRating ?? string.Empty).Split("/").Map(s => s.Trim()) - .Where(x => !string.IsNullOrWhiteSpace(x)).ToList(), - LanguagesForMovie(languageCodes), - metadata.Actors.OrderBy(a => a.Order).ThenBy(a => a.Id) - .Map(a => MediaCards.Mapper.ProjectToViewModel(a, maybeJellyfin, maybeEmby)) - .ToList(), - metadata.Directors.Map(d => d.Name).ToList(), - metadata.Writers.Map(w => w.Name).ToList(), - movie.GetHeadVersion().MediaFiles.Head().Path, - movie.State) - { - Poster = Artwork(metadata, ArtworkKind.Poster, maybeJellyfin, maybeEmby), - FanArt = Artwork(metadata, ArtworkKind.FanArt, maybeJellyfin, maybeEmby) - }; - } - - private static List LanguagesForMovie(List languageCodes) - { - CultureInfo[] allCultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures); - - return languageCodes - .Distinct() - .Map( - lang => allCultures.Filter( - ci => string.Equals(ci.ThreeLetterISOLanguageName, lang, StringComparison.OrdinalIgnoreCase))) - .Sequence() - .Flatten() - .ToList(); - } - - private static string Artwork( - Metadata metadata, - ArtworkKind artworkKind, - Option maybeJellyfin, - Option maybeEmby) - { - string artwork = Optional(metadata.Artwork.FirstOrDefault(a => a.ArtworkKind == artworkKind)) - .Match(a => a.Path, string.Empty); - - if (maybeJellyfin.IsSome && artwork.StartsWith("jellyfin://")) - { - Url url = JellyfinUrl.RelativeProxyForArtwork(artwork); - if (artworkKind is ArtworkKind.Poster or ArtworkKind.Thumbnail) - { - url.SetQueryParam("fillHeight", 440); - } - - artwork = url; - } - else if (maybeEmby.IsSome && artwork.StartsWith("emby://")) - { - Url url = EmbyUrl.RelativeProxyForArtwork(artwork); - if (artworkKind is ArtworkKind.Poster or ArtworkKind.Thumbnail) - { - url.SetQueryParam("maxHeight", 440); - } - - artwork = url; - } - - return artwork; - } + Poster = Artwork(metadata, ArtworkKind.Poster, maybeJellyfin, maybeEmby), + FanArt = Artwork(metadata, ArtworkKind.FanArt, maybeJellyfin, maybeEmby) + }; } -} + + private static List LanguagesForMovie(List languageCodes) + { + CultureInfo[] allCultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures); + + return languageCodes + .Distinct() + .Map( + lang => allCultures.Filter( + ci => string.Equals(ci.ThreeLetterISOLanguageName, lang, StringComparison.OrdinalIgnoreCase))) + .Sequence() + .Flatten() + .ToList(); + } + + private static string Artwork( + Metadata metadata, + ArtworkKind artworkKind, + Option maybeJellyfin, + Option maybeEmby) + { + string artwork = Optional(metadata.Artwork.FirstOrDefault(a => a.ArtworkKind == artworkKind)) + .Match(a => a.Path, string.Empty); + + if (maybeJellyfin.IsSome && artwork.StartsWith("jellyfin://")) + { + Url url = JellyfinUrl.RelativeProxyForArtwork(artwork); + if (artworkKind is ArtworkKind.Poster or ArtworkKind.Thumbnail) + { + url.SetQueryParam("fillHeight", 440); + } + + artwork = url; + } + else if (maybeEmby.IsSome && artwork.StartsWith("emby://")) + { + Url url = EmbyUrl.RelativeProxyForArtwork(artwork); + if (artworkKind is ArtworkKind.Poster or ArtworkKind.Thumbnail) + { + url.SetQueryParam("maxHeight", 440); + } + + artwork = url; + } + + return artwork; + } +} \ No newline at end of file diff --git a/ErsatzTV.Application/Movies/MovieViewModel.cs b/ErsatzTV.Application/Movies/MovieViewModel.cs index 65750eccc..6c63f61d7 100644 --- a/ErsatzTV.Application/Movies/MovieViewModel.cs +++ b/ErsatzTV.Application/Movies/MovieViewModel.cs @@ -1,26 +1,24 @@ -using System.Collections.Generic; -using System.Globalization; +using System.Globalization; using ErsatzTV.Application.MediaCards; using ErsatzTV.Core.Domain; -namespace ErsatzTV.Application.Movies +namespace ErsatzTV.Application.Movies; + +public record MovieViewModel( + string Title, + string Year, + string Plot, + List Genres, + List Tags, + List Studios, + List ContentRatings, + List Languages, + List Actors, + List Directors, + List Writers, + string Path, + MediaItemState MediaItemState) { - public record MovieViewModel( - string Title, - string Year, - string Plot, - List Genres, - List Tags, - List Studios, - List ContentRatings, - List Languages, - List Actors, - List Directors, - List Writers, - string Path, - MediaItemState MediaItemState) - { - public string Poster { get; set; } - public string FanArt { get; set; } - } -} + public string Poster { get; set; } + public string FanArt { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Application/Movies/Queries/GetMovieById.cs b/ErsatzTV.Application/Movies/Queries/GetMovieById.cs index 7a264190d..10e9073c5 100644 --- a/ErsatzTV.Application/Movies/Queries/GetMovieById.cs +++ b/ErsatzTV.Application/Movies/Queries/GetMovieById.cs @@ -1,7 +1,3 @@ -using LanguageExt; -using MediatR; +namespace ErsatzTV.Application.Movies; -namespace ErsatzTV.Application.Movies.Queries -{ - public record GetMovieById(int Id) : IRequest>; -} +public record GetMovieById(int Id) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Movies/Queries/GetMovieByIdHandler.cs b/ErsatzTV.Application/Movies/Queries/GetMovieByIdHandler.cs index d82415673..07589f58b 100644 --- a/ErsatzTV.Application/Movies/Queries/GetMovieByIdHandler.cs +++ b/ErsatzTV.Application/Movies/Queries/GetMovieByIdHandler.cs @@ -1,61 +1,54 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; -using LanguageExt; -using MediatR; using Microsoft.EntityFrameworkCore; using static ErsatzTV.Application.Movies.Mapper; -namespace ErsatzTV.Application.Movies.Queries +namespace ErsatzTV.Application.Movies; + +public class GetMovieByIdHandler : IRequestHandler> { - public class GetMovieByIdHandler : IRequestHandler> + private readonly IMediaSourceRepository _mediaSourceRepository; + private readonly IDbContextFactory _dbContextFactory; + private readonly IMovieRepository _movieRepository; + + public GetMovieByIdHandler( + IDbContextFactory dbContextFactory, + IMovieRepository movieRepository, + IMediaSourceRepository mediaSourceRepository) { - private readonly IMediaSourceRepository _mediaSourceRepository; - private readonly IDbContextFactory _dbContextFactory; - private readonly IMovieRepository _movieRepository; - - public GetMovieByIdHandler( - IDbContextFactory dbContextFactory, - IMovieRepository movieRepository, - IMediaSourceRepository mediaSourceRepository) - { - _dbContextFactory = dbContextFactory; - _movieRepository = movieRepository; - _mediaSourceRepository = mediaSourceRepository; - } - - public async Task> Handle( - GetMovieById request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - - Option maybeJellyfin = await _mediaSourceRepository.GetAllJellyfin() - .Map(list => list.HeadOrNone()); - - Option maybeEmby = await _mediaSourceRepository.GetAllEmby() - .Map(list => list.HeadOrNone()); - - Option movie = await _movieRepository.GetMovie(request.Id); - - Option maybeVersion = movie.Map(m => m.MediaVersions.HeadOrNone()).Flatten(); - var languageCodes = new List(); - foreach (MediaVersion version in maybeVersion) - { - var mediaCodes = version.Streams - .Filter(ms => ms.MediaStreamKind == MediaStreamKind.Audio) - .Map(ms => ms.Language) - .ToList(); - - languageCodes.AddRange(await dbContext.LanguageCodes.GetAllLanguageCodes(mediaCodes)); - } - - return movie.Map(m => ProjectToViewModel(m, languageCodes, maybeJellyfin, maybeEmby)); - } + _dbContextFactory = dbContextFactory; + _movieRepository = movieRepository; + _mediaSourceRepository = mediaSourceRepository; } -} + + public async Task> Handle( + GetMovieById request, + CancellationToken cancellationToken) + { + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + + Option maybeJellyfin = await _mediaSourceRepository.GetAllJellyfin() + .Map(list => list.HeadOrNone()); + + Option maybeEmby = await _mediaSourceRepository.GetAllEmby() + .Map(list => list.HeadOrNone()); + + Option movie = await _movieRepository.GetMovie(request.Id); + + Option maybeVersion = movie.Map(m => m.MediaVersions.HeadOrNone()).Flatten(); + var languageCodes = new List(); + foreach (MediaVersion version in maybeVersion) + { + var mediaCodes = version.Streams + .Filter(ms => ms.MediaStreamKind == MediaStreamKind.Audio) + .Map(ms => ms.Language) + .ToList(); + + languageCodes.AddRange(await dbContext.LanguageCodes.GetAllLanguageCodes(mediaCodes)); + } + + return movie.Map(m => ProjectToViewModel(m, languageCodes, maybeJellyfin, maybeEmby)); + } +} \ No newline at end of file diff --git a/ErsatzTV.Application/Playouts/Commands/BuildPlayout.cs b/ErsatzTV.Application/Playouts/Commands/BuildPlayout.cs index 014271147..d3134c9ff 100644 --- a/ErsatzTV.Application/Playouts/Commands/BuildPlayout.cs +++ b/ErsatzTV.Application/Playouts/Commands/BuildPlayout.cs @@ -1,8 +1,6 @@ using ErsatzTV.Core; -using LanguageExt; -namespace ErsatzTV.Application.Playouts.Commands -{ - public record BuildPlayout(int PlayoutId, bool Rebuild = false) : MediatR.IRequest>, - IBackgroundServiceRequest; -} +namespace ErsatzTV.Application.Playouts; + +public record BuildPlayout(int PlayoutId, bool Rebuild = false) : MediatR.IRequest>, + IBackgroundServiceRequest; \ No newline at end of file diff --git a/ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs b/ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs index 0f9eedfb8..26d267f1c 100644 --- a/ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs +++ b/ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs @@ -1,73 +1,69 @@ -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Scheduling; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; -using LanguageExt; using Microsoft.EntityFrameworkCore; -namespace ErsatzTV.Application.Playouts.Commands +namespace ErsatzTV.Application.Playouts; + +public class BuildPlayoutHandler : MediatR.IRequestHandler> { - public class BuildPlayoutHandler : MediatR.IRequestHandler> + private readonly IDbContextFactory _dbContextFactory; + private readonly IPlayoutBuilder _playoutBuilder; + + public BuildPlayoutHandler(IDbContextFactory dbContextFactory, IPlayoutBuilder playoutBuilder) { - private readonly IDbContextFactory _dbContextFactory; - private readonly IPlayoutBuilder _playoutBuilder; - - public BuildPlayoutHandler(IDbContextFactory dbContextFactory, IPlayoutBuilder playoutBuilder) - { - _dbContextFactory = dbContextFactory; - _playoutBuilder = playoutBuilder; - } - - public async Task> Handle(BuildPlayout request, CancellationToken cancellationToken) - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - Validation validation = await Validate(dbContext, request); - return await validation.Apply(playout => ApplyUpdateRequest(dbContext, request, playout)); - } - - private async Task ApplyUpdateRequest(TvContext dbContext, BuildPlayout request, Playout playout) - { - await _playoutBuilder.BuildPlayoutItems(playout, request.Rebuild); - await dbContext.SaveChangesAsync(); - return Unit.Default; - } - - private static Task> Validate(TvContext dbContext, BuildPlayout request) => - PlayoutMustExist(dbContext, request); - - private static Task> 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) - .Include(p => p.ProgramSchedule) - .ThenInclude(ps => ps.Items) - .ThenInclude(psi => psi.PreRollFiller) - .Include(p => p.ProgramSchedule) - .ThenInclude(ps => ps.Items) - .ThenInclude(psi => psi.MidRollFiller) - .Include(p => p.ProgramSchedule) - .ThenInclude(ps => ps.Items) - .ThenInclude(psi => psi.PostRollFiller) - .Include(p => p.ProgramSchedule) - .ThenInclude(ps => ps.Items) - .ThenInclude(psi => psi.TailFiller) - .Include(p => p.ProgramSchedule) - .ThenInclude(ps => ps.Items) - .ThenInclude(psi => psi.FallbackFiller) - .SelectOneAsync(p => p.Id, p => p.Id == buildPlayout.PlayoutId) - .Map(o => o.ToValidation("Playout does not exist.")); + _dbContextFactory = dbContextFactory; + _playoutBuilder = playoutBuilder; } -} + + public async Task> Handle(BuildPlayout request, CancellationToken cancellationToken) + { + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + Validation validation = await Validate(dbContext, request); + return await LanguageExtensions.Apply(validation, playout => ApplyUpdateRequest(dbContext, request, playout)); + } + + private async Task ApplyUpdateRequest(TvContext dbContext, BuildPlayout request, Playout playout) + { + await _playoutBuilder.BuildPlayoutItems(playout, request.Rebuild); + await dbContext.SaveChangesAsync(); + return Unit.Default; + } + + private static Task> Validate(TvContext dbContext, BuildPlayout request) => + PlayoutMustExist(dbContext, request); + + private static Task> 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) + .Include(p => p.ProgramSchedule) + .ThenInclude(ps => ps.Items) + .ThenInclude(psi => psi.PreRollFiller) + .Include(p => p.ProgramSchedule) + .ThenInclude(ps => ps.Items) + .ThenInclude(psi => psi.MidRollFiller) + .Include(p => p.ProgramSchedule) + .ThenInclude(ps => ps.Items) + .ThenInclude(psi => psi.PostRollFiller) + .Include(p => p.ProgramSchedule) + .ThenInclude(ps => ps.Items) + .ThenInclude(psi => psi.TailFiller) + .Include(p => p.ProgramSchedule) + .ThenInclude(ps => ps.Items) + .ThenInclude(psi => psi.FallbackFiller) + .SelectOneAsync(p => p.Id, p => p.Id == buildPlayout.PlayoutId) + .Map(o => o.ToValidation("Playout does not exist.")); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Playouts/Commands/CreatePlayout.cs b/ErsatzTV.Application/Playouts/Commands/CreatePlayout.cs index 68a47d502..29ec84f26 100644 --- a/ErsatzTV.Application/Playouts/Commands/CreatePlayout.cs +++ b/ErsatzTV.Application/Playouts/Commands/CreatePlayout.cs @@ -1,12 +1,9 @@ using ErsatzTV.Core; using ErsatzTV.Core.Domain; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.Playouts.Commands -{ - public record CreatePlayout( - int ChannelId, - int ProgramScheduleId, - ProgramSchedulePlayoutType ProgramSchedulePlayoutType) : IRequest>; -} +namespace ErsatzTV.Application.Playouts; + +public record CreatePlayout( + int ChannelId, + int ProgramScheduleId, + ProgramSchedulePlayoutType ProgramSchedulePlayoutType) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Playouts/Commands/CreatePlayoutHandler.cs b/ErsatzTV.Application/Playouts/Commands/CreatePlayoutHandler.cs index 48c6a3e5b..1b346d1d5 100644 --- a/ErsatzTV.Application/Playouts/Commands/CreatePlayoutHandler.cs +++ b/ErsatzTV.Application/Playouts/Commands/CreatePlayoutHandler.cs @@ -1,95 +1,88 @@ -using System.Linq; -using System.Threading; -using System.Threading.Channels; -using System.Threading.Tasks; +using System.Threading.Channels; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; -using LanguageExt; -using MediatR; using Microsoft.EntityFrameworkCore; -using static LanguageExt.Prelude; using Channel = ErsatzTV.Core.Domain.Channel; -namespace ErsatzTV.Application.Playouts.Commands +namespace ErsatzTV.Application.Playouts; + +public class CreatePlayoutHandler : IRequestHandler> { - public class CreatePlayoutHandler : IRequestHandler> + private readonly ChannelWriter _channel; + private readonly IDbContextFactory _dbContextFactory; + + public CreatePlayoutHandler( + ChannelWriter channel, + IDbContextFactory dbContextFactory) { - private readonly ChannelWriter _channel; - private readonly IDbContextFactory _dbContextFactory; - - public CreatePlayoutHandler( - ChannelWriter channel, - IDbContextFactory dbContextFactory) - { - _channel = channel; - _dbContextFactory = dbContextFactory; - } - - public async Task> Handle( - CreatePlayout request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - - Validation validation = await Validate(dbContext, request); - return await validation.Apply(playout => PersistPlayout(dbContext, playout)); - } - - private async Task PersistPlayout(TvContext dbContext, Playout playout) - { - await dbContext.Playouts.AddAsync(playout); - await dbContext.SaveChangesAsync(); - await _channel.WriteAsync(new BuildPlayout(playout.Id)); - return new CreatePlayoutResponse(playout.Id); - } - - private async Task> Validate(TvContext dbContext, CreatePlayout request) => - (await ValidateChannel(dbContext, request), await ValidateProgramSchedule(dbContext, request), - ValidatePlayoutType(request)) - .Apply( - (channel, programSchedule, playoutType) => new Playout - { - ChannelId = channel.Id, - ProgramScheduleId = programSchedule.Id, - ProgramSchedulePlayoutType = playoutType - }); - - private static Task> ValidateChannel( - TvContext dbContext, - CreatePlayout createPlayout) => - dbContext.Channels - .Include(c => c.Playouts) - .SelectOneAsync(c => c.Id, c => c.Id == createPlayout.ChannelId) - .Map(o => o.ToValidation("Channel does not exist")) - .BindT(ChannelMustNotHavePlayouts); - - private static Validation ChannelMustNotHavePlayouts(Channel channel) => - Optional(channel.Playouts.Count) - .Filter(count => count == 0) - .Map(_ => channel) - .ToValidation("Channel already has one playout"); - - private static Task> ValidateProgramSchedule( - TvContext dbContext, - CreatePlayout createPlayout) => - dbContext.ProgramSchedules - .Include(ps => ps.Items) - .SelectOneAsync(ps => ps.Id, ps => ps.Id == createPlayout.ProgramScheduleId) - .Map(o => o.ToValidation("Program schedule does not exist")) - .BindT(ProgramScheduleMustHaveItems); - - private static Validation ProgramScheduleMustHaveItems( - ProgramSchedule programSchedule) => - Optional(programSchedule) - .Filter(ps => ps.Items.Any()) - .ToValidation("Program schedule must have items"); - - private static Validation ValidatePlayoutType( - CreatePlayout createPlayout) => - Optional(createPlayout.ProgramSchedulePlayoutType) - .Filter(playoutType => playoutType != ProgramSchedulePlayoutType.None) - .ToValidation("[ProgramSchedulePlayoutType] must not be None"); + _channel = channel; + _dbContextFactory = dbContextFactory; } -} + + public async Task> Handle( + CreatePlayout request, + CancellationToken cancellationToken) + { + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + + Validation validation = await Validate(dbContext, request); + return await LanguageExtensions.Apply(validation, playout => PersistPlayout(dbContext, playout)); + } + + private async Task PersistPlayout(TvContext dbContext, Playout playout) + { + await dbContext.Playouts.AddAsync(playout); + await dbContext.SaveChangesAsync(); + await _channel.WriteAsync(new BuildPlayout(playout.Id)); + return new CreatePlayoutResponse(playout.Id); + } + + private async Task> Validate(TvContext dbContext, CreatePlayout request) => + (await ValidateChannel(dbContext, request), await ValidateProgramSchedule(dbContext, request), + ValidatePlayoutType(request)) + .Apply( + (channel, programSchedule, playoutType) => new Playout + { + ChannelId = channel.Id, + ProgramScheduleId = programSchedule.Id, + ProgramSchedulePlayoutType = playoutType + }); + + private static Task> ValidateChannel( + TvContext dbContext, + CreatePlayout createPlayout) => + dbContext.Channels + .Include(c => c.Playouts) + .SelectOneAsync(c => c.Id, c => c.Id == createPlayout.ChannelId) + .Map(o => o.ToValidation("Channel does not exist")) + .BindT(ChannelMustNotHavePlayouts); + + private static Validation ChannelMustNotHavePlayouts(Channel channel) => + Optional(channel.Playouts.Count) + .Filter(count => count == 0) + .Map(_ => channel) + .ToValidation("Channel already has one playout"); + + private static Task> ValidateProgramSchedule( + TvContext dbContext, + CreatePlayout createPlayout) => + dbContext.ProgramSchedules + .Include(ps => ps.Items) + .SelectOneAsync(ps => ps.Id, ps => ps.Id == createPlayout.ProgramScheduleId) + .Map(o => o.ToValidation("Program schedule does not exist")) + .BindT(ProgramScheduleMustHaveItems); + + private static Validation ProgramScheduleMustHaveItems( + ProgramSchedule programSchedule) => + Optional(programSchedule) + .Filter(ps => ps.Items.Any()) + .ToValidation("Program schedule must have items"); + + private static Validation ValidatePlayoutType( + CreatePlayout createPlayout) => + Optional(createPlayout.ProgramSchedulePlayoutType) + .Filter(playoutType => playoutType != ProgramSchedulePlayoutType.None) + .ToValidation("[ProgramSchedulePlayoutType] must not be None"); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Playouts/Commands/CreatePlayoutResponse.cs b/ErsatzTV.Application/Playouts/Commands/CreatePlayoutResponse.cs index c5d266f89..fb4d596b6 100644 --- a/ErsatzTV.Application/Playouts/Commands/CreatePlayoutResponse.cs +++ b/ErsatzTV.Application/Playouts/Commands/CreatePlayoutResponse.cs @@ -1,4 +1,3 @@ -namespace ErsatzTV.Application.Playouts.Commands -{ - public record CreatePlayoutResponse(int PlayoutId); -} +namespace ErsatzTV.Application.Playouts; + +public record CreatePlayoutResponse(int PlayoutId); \ No newline at end of file diff --git a/ErsatzTV.Application/Playouts/Commands/DeletePlayout.cs b/ErsatzTV.Application/Playouts/Commands/DeletePlayout.cs index ddbb53384..6ccc3dd74 100644 --- a/ErsatzTV.Application/Playouts/Commands/DeletePlayout.cs +++ b/ErsatzTV.Application/Playouts/Commands/DeletePlayout.cs @@ -1,9 +1,5 @@ using ErsatzTV.Core; -using LanguageExt; -using MediatR; -using Unit = LanguageExt.Unit; -namespace ErsatzTV.Application.Playouts.Commands -{ - public record DeletePlayout(int PlayoutId) : IRequest>; -} +namespace ErsatzTV.Application.Playouts; + +public record DeletePlayout(int PlayoutId) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Playouts/Commands/DeletePlayoutHandler.cs b/ErsatzTV.Application/Playouts/Commands/DeletePlayoutHandler.cs index 9240ab05e..6e26187e0 100644 --- a/ErsatzTV.Application/Playouts/Commands/DeletePlayoutHandler.cs +++ b/ErsatzTV.Application/Playouts/Commands/DeletePlayoutHandler.cs @@ -1,42 +1,35 @@ -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Infrastructure.Data; -using LanguageExt; -using MediatR; using Microsoft.EntityFrameworkCore; -using Unit = LanguageExt.Unit; -namespace ErsatzTV.Application.Playouts.Commands +namespace ErsatzTV.Application.Playouts; + +public class DeletePlayoutHandler : IRequestHandler> { - public class DeletePlayoutHandler : IRequestHandler> + private readonly IDbContextFactory _dbContextFactory; + + public DeletePlayoutHandler(IDbContextFactory dbContextFactory) => + _dbContextFactory = dbContextFactory; + + public async Task> Handle( + DeletePlayout request, + CancellationToken cancellationToken) { - private readonly IDbContextFactory _dbContextFactory; + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - public DeletePlayoutHandler(IDbContextFactory dbContextFactory) => - _dbContextFactory = dbContextFactory; + Option maybePlayout = await dbContext.Playouts + .OrderBy(p => p.Id) + .FirstOrDefaultAsync(p => p.Id == request.PlayoutId, cancellationToken); - public async Task> Handle( - DeletePlayout request, - CancellationToken cancellationToken) + foreach (Playout playout in maybePlayout) { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - - Option maybePlayout = await dbContext.Playouts - .OrderBy(p => p.Id) - .FirstOrDefaultAsync(p => p.Id == request.PlayoutId, cancellationToken); - - 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.")); + dbContext.Playouts.Remove(playout); + await dbContext.SaveChangesAsync(cancellationToken); } + + return maybePlayout + .Map(_ => Unit.Default) + .ToEither(BaseError.New($"Playout {request.PlayoutId} does not exist.")); } -} +} \ No newline at end of file diff --git a/ErsatzTV.Application/Playouts/Commands/UpdatePlayout.cs b/ErsatzTV.Application/Playouts/Commands/UpdatePlayout.cs index fc13cc2fb..1668286bb 100644 --- a/ErsatzTV.Application/Playouts/Commands/UpdatePlayout.cs +++ b/ErsatzTV.Application/Playouts/Commands/UpdatePlayout.cs @@ -1,10 +1,6 @@ -using System; -using ErsatzTV.Core; -using LanguageExt; -using MediatR; +using ErsatzTV.Core; -namespace ErsatzTV.Application.Playouts.Commands -{ - public record UpdatePlayout - (int PlayoutId, Option DailyRebuildTime) : IRequest>; -} +namespace ErsatzTV.Application.Playouts; + +public record UpdatePlayout + (int PlayoutId, Option DailyRebuildTime) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Playouts/Commands/UpdatePlayoutHandler.cs b/ErsatzTV.Application/Playouts/Commands/UpdatePlayoutHandler.cs index a95766ba9..9bf853ea6 100644 --- a/ErsatzTV.Application/Playouts/Commands/UpdatePlayoutHandler.cs +++ b/ErsatzTV.Application/Playouts/Commands/UpdatePlayoutHandler.cs @@ -1,65 +1,58 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; -using LanguageExt; -using MediatR; using Microsoft.EntityFrameworkCore; -using static LanguageExt.Prelude; -namespace ErsatzTV.Application.Playouts.Commands +namespace ErsatzTV.Application.Playouts; + +public class UpdatePlayoutHandler : IRequestHandler> { - public class UpdatePlayoutHandler : IRequestHandler> + private readonly IDbContextFactory _dbContextFactory; + + public UpdatePlayoutHandler(IDbContextFactory dbContextFactory) => + _dbContextFactory = dbContextFactory; + + public async Task> Handle( + UpdatePlayout request, + CancellationToken cancellationToken) { - private readonly IDbContextFactory _dbContextFactory; - - public UpdatePlayoutHandler(IDbContextFactory dbContextFactory) => - _dbContextFactory = dbContextFactory; - - public async Task> Handle( - UpdatePlayout request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - Validation validation = await Validate(dbContext, request); - return await validation.Apply(playout => ApplyUpdateRequest(dbContext, request, playout)); - } - - private static async Task ApplyUpdateRequest( - TvContext dbContext, - UpdatePlayout request, - Playout playout) - { - playout.DailyRebuildTime = null; - - foreach (TimeSpan dailyRebuildTime in request.DailyRebuildTime) - { - playout.DailyRebuildTime = dailyRebuildTime; - } - - await dbContext.SaveChangesAsync(); - - return new PlayoutNameViewModel( - playout.Id, - playout.Channel.Name, - playout.Channel.Number, - playout.ProgramSchedule.Name, - Optional(playout.DailyRebuildTime)); - } - - private static Task> Validate(TvContext dbContext, UpdatePlayout request) => - PlayoutMustExist(dbContext, request); - - private static Task> PlayoutMustExist( - TvContext dbContext, - UpdatePlayout updatePlayout) => - dbContext.Playouts - .Include(p => p.Channel) - .Include(p => p.ProgramSchedule) - .SelectOneAsync(p => p.Id, p => p.Id == updatePlayout.PlayoutId) - .Map(o => o.ToValidation("Playout does not exist.")); + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + Validation validation = await Validate(dbContext, request); + return await LanguageExtensions.Apply(validation, playout => ApplyUpdateRequest(dbContext, request, playout)); } -} + + private static async Task ApplyUpdateRequest( + TvContext dbContext, + UpdatePlayout request, + Playout playout) + { + playout.DailyRebuildTime = null; + + foreach (TimeSpan dailyRebuildTime in request.DailyRebuildTime) + { + playout.DailyRebuildTime = dailyRebuildTime; + } + + await dbContext.SaveChangesAsync(); + + return new PlayoutNameViewModel( + playout.Id, + playout.Channel.Name, + playout.Channel.Number, + playout.ProgramSchedule.Name, + Optional(playout.DailyRebuildTime)); + } + + private static Task> Validate(TvContext dbContext, UpdatePlayout request) => + PlayoutMustExist(dbContext, request); + + private static Task> PlayoutMustExist( + TvContext dbContext, + UpdatePlayout updatePlayout) => + dbContext.Playouts + .Include(p => p.Channel) + .Include(p => p.ProgramSchedule) + .SelectOneAsync(p => p.Id, p => p.Id == updatePlayout.PlayoutId) + .Map(o => o.ToValidation("Playout does not exist.")); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Playouts/Mapper.cs b/ErsatzTV.Application/Playouts/Mapper.cs index 1dc658877..b4e1ed56d 100644 --- a/ErsatzTV.Application/Playouts/Mapper.cs +++ b/ErsatzTV.Application/Playouts/Mapper.cs @@ -1,69 +1,66 @@ -using System; -using System.Linq; -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; -namespace ErsatzTV.Application.Playouts +namespace ErsatzTV.Application.Playouts; + +internal static class Mapper { - internal static class Mapper + internal static PlayoutItemViewModel ProjectToViewModel(PlayoutItem playoutItem) => + new( + GetDisplayTitle(playoutItem), + playoutItem.StartOffset, + GetDisplayDuration(playoutItem.FinishOffset - playoutItem.StartOffset)); + + private static string GetDisplayTitle(PlayoutItem playoutItem) { - internal static PlayoutItemViewModel ProjectToViewModel(PlayoutItem playoutItem) => - new( - GetDisplayTitle(playoutItem), - playoutItem.StartOffset, - GetDisplayDuration(playoutItem.FinishOffset - playoutItem.StartOffset)); - - private static string GetDisplayTitle(PlayoutItem playoutItem) + switch (playoutItem.MediaItem) { - switch (playoutItem.MediaItem) - { - case Episode e: - string showTitle = e.Season.Show.ShowMetadata.HeadOrNone() - .Map(sm => $"{sm.Title} - ").IfNone(string.Empty); - var episodeNumbers = e.EpisodeMetadata.Map(em => em.EpisodeNumber).ToList(); - var episodeTitles = e.EpisodeMetadata.Map(em => em.Title).ToList(); - if (episodeNumbers.Count == 0 || episodeTitles.Count == 0) - { - return "[unknown episode]"; - } + case Episode e: + string showTitle = e.Season.Show.ShowMetadata.HeadOrNone() + .Map(sm => $"{sm.Title} - ").IfNone(string.Empty); + var episodeNumbers = e.EpisodeMetadata.Map(em => em.EpisodeNumber).ToList(); + var episodeTitles = e.EpisodeMetadata.Map(em => em.Title).ToList(); + if (episodeNumbers.Count == 0 || episodeTitles.Count == 0) + { + return "[unknown episode]"; + } - var numbersString = $"e{string.Join('e', episodeNumbers.Map(n => $"{n:00}"))}"; - var titlesString = $"{string.Join('/', episodeTitles)}"; - if (!string.IsNullOrWhiteSpace(playoutItem.ChapterTitle)) - { - titlesString += $" ({playoutItem.ChapterTitle})"; - } + var numbersString = $"e{string.Join('e', episodeNumbers.Map(n => $"{n:00}"))}"; + var titlesString = $"{string.Join('/', episodeTitles)}"; + if (!string.IsNullOrWhiteSpace(playoutItem.ChapterTitle)) + { + titlesString += $" ({playoutItem.ChapterTitle})"; + } - return $"{showTitle}s{e.Season.SeasonNumber:00}{numbersString} - {titlesString}"; - case Movie m: - return m.MovieMetadata.HeadOrNone().Map(mm => mm.Title).IfNone("[unknown movie]"); - case MusicVideo mv: - string artistName = mv.Artist.ArtistMetadata.HeadOrNone() - .Map(am => $"{am.Title} - ").IfNone(string.Empty); - return mv.MusicVideoMetadata.HeadOrNone() - .Map(mvm => $"{artistName}{mvm.Title}") - .Map(s => string.IsNullOrWhiteSpace(playoutItem.ChapterTitle) ? s : $"{s} ({playoutItem.ChapterTitle})") - .IfNone("[unknown music video]"); - case OtherVideo ov: - return ov.OtherVideoMetadata.HeadOrNone() - .Map(ovm => ovm.Title ?? string.Empty) - .Map(s => string.IsNullOrWhiteSpace(playoutItem.ChapterTitle) ? s : $"{s} ({playoutItem.ChapterTitle})") - .IfNone("[unknown video]"); - case Song s: - string songArtist = s.SongMetadata.HeadOrNone() - .Map(sm => string.IsNullOrWhiteSpace(sm.Artist) ? string.Empty : $"{sm.Artist} - ") - .IfNone(string.Empty); - return s.SongMetadata.HeadOrNone() - .Map(sm => $"{songArtist}{sm.Title ?? string.Empty}") - .Map(t => string.IsNullOrWhiteSpace(playoutItem.ChapterTitle) ? t : $"{s} ({playoutItem.ChapterTitle})") - .IfNone("[unknown song]"); - default: - return string.Empty; - } + return $"{showTitle}s{e.Season.SeasonNumber:00}{numbersString} - {titlesString}"; + case Movie m: + return m.MovieMetadata.HeadOrNone().Map(mm => mm.Title).IfNone("[unknown movie]"); + case MusicVideo mv: + string artistName = mv.Artist.ArtistMetadata.HeadOrNone() + .Map(am => $"{am.Title} - ").IfNone(string.Empty); + return mv.MusicVideoMetadata.HeadOrNone() + .Map(mvm => $"{artistName}{mvm.Title}") + .Map(s => string.IsNullOrWhiteSpace(playoutItem.ChapterTitle) ? s : $"{s} ({playoutItem.ChapterTitle})") + .IfNone("[unknown music video]"); + case OtherVideo ov: + return ov.OtherVideoMetadata.HeadOrNone() + .Map(ovm => ovm.Title ?? string.Empty) + .Map(s => string.IsNullOrWhiteSpace(playoutItem.ChapterTitle) ? s : $"{s} ({playoutItem.ChapterTitle})") + .IfNone("[unknown video]"); + case Song s: + string songArtist = s.SongMetadata.HeadOrNone() + .Map(sm => string.IsNullOrWhiteSpace(sm.Artist) ? string.Empty : $"{sm.Artist} - ") + .IfNone(string.Empty); + return s.SongMetadata.HeadOrNone() + .Map(sm => $"{songArtist}{sm.Title ?? string.Empty}") + .Map(t => string.IsNullOrWhiteSpace(playoutItem.ChapterTitle) ? t : $"{s} ({playoutItem.ChapterTitle})") + .IfNone("[unknown song]"); + default: + return string.Empty; } - - private static string GetDisplayDuration(TimeSpan duration) => - string.Format( - duration.TotalHours >= 1 ? @"{0:h\:mm\:ss}" : @"{0:mm\:ss}", - duration); } -} + + private static string GetDisplayDuration(TimeSpan duration) => + string.Format( + duration.TotalHours >= 1 ? @"{0:h\:mm\:ss}" : @"{0:mm\:ss}", + duration); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Playouts/PagedPlayoutItemsViewModel.cs b/ErsatzTV.Application/Playouts/PagedPlayoutItemsViewModel.cs index 11260308f..0e6568dc5 100644 --- a/ErsatzTV.Application/Playouts/PagedPlayoutItemsViewModel.cs +++ b/ErsatzTV.Application/Playouts/PagedPlayoutItemsViewModel.cs @@ -1,6 +1,3 @@ -using System.Collections.Generic; +namespace ErsatzTV.Application.Playouts; -namespace ErsatzTV.Application.Playouts -{ - public record PagedPlayoutItemsViewModel(int TotalCount, List Page); -} +public record PagedPlayoutItemsViewModel(int TotalCount, List Page); \ No newline at end of file diff --git a/ErsatzTV.Application/Playouts/PlayoutChannelViewModel.cs b/ErsatzTV.Application/Playouts/PlayoutChannelViewModel.cs index d21d6e00c..3968e90a2 100644 --- a/ErsatzTV.Application/Playouts/PlayoutChannelViewModel.cs +++ b/ErsatzTV.Application/Playouts/PlayoutChannelViewModel.cs @@ -1,4 +1,3 @@ -namespace ErsatzTV.Application.Playouts -{ - public record PlayoutChannelViewModel(int Id, string Number, string Name); -} +namespace ErsatzTV.Application.Playouts; + +public record PlayoutChannelViewModel(int Id, string Number, string Name); \ No newline at end of file diff --git a/ErsatzTV.Application/Playouts/PlayoutItemViewModel.cs b/ErsatzTV.Application/Playouts/PlayoutItemViewModel.cs index 5e064c037..118ba6536 100644 --- a/ErsatzTV.Application/Playouts/PlayoutItemViewModel.cs +++ b/ErsatzTV.Application/Playouts/PlayoutItemViewModel.cs @@ -1,6 +1,3 @@ -using System; +namespace ErsatzTV.Application.Playouts; -namespace ErsatzTV.Application.Playouts -{ - public record PlayoutItemViewModel(string Title, DateTimeOffset Start, string Duration); -} +public record PlayoutItemViewModel(string Title, DateTimeOffset Start, string Duration); \ No newline at end of file diff --git a/ErsatzTV.Application/Playouts/PlayoutNameViewModel.cs b/ErsatzTV.Application/Playouts/PlayoutNameViewModel.cs index d727f112c..9aeaeb386 100644 --- a/ErsatzTV.Application/Playouts/PlayoutNameViewModel.cs +++ b/ErsatzTV.Application/Playouts/PlayoutNameViewModel.cs @@ -1,12 +1,8 @@ -using System; -using LanguageExt; +namespace ErsatzTV.Application.Playouts; -namespace ErsatzTV.Application.Playouts -{ - public record PlayoutNameViewModel( - int PlayoutId, - string ChannelName, - string ChannelNumber, - string ScheduleName, - Option DailyRebuildTime); -} +public record PlayoutNameViewModel( + int PlayoutId, + string ChannelName, + string ChannelNumber, + string ScheduleName, + Option DailyRebuildTime); \ No newline at end of file diff --git a/ErsatzTV.Application/Playouts/PlayoutProgramScheduleViewModel.cs b/ErsatzTV.Application/Playouts/PlayoutProgramScheduleViewModel.cs index 1d1ca0049..e4e3e9824 100644 --- a/ErsatzTV.Application/Playouts/PlayoutProgramScheduleViewModel.cs +++ b/ErsatzTV.Application/Playouts/PlayoutProgramScheduleViewModel.cs @@ -1,4 +1,3 @@ -namespace ErsatzTV.Application.Playouts -{ - public record PlayoutProgramScheduleViewModel(int Id, string Name); -} +namespace ErsatzTV.Application.Playouts; + +public record PlayoutProgramScheduleViewModel(int Id, string Name); \ No newline at end of file diff --git a/ErsatzTV.Application/Playouts/PlayoutViewModel.cs b/ErsatzTV.Application/Playouts/PlayoutViewModel.cs index 978a35046..55bf72ea7 100644 --- a/ErsatzTV.Application/Playouts/PlayoutViewModel.cs +++ b/ErsatzTV.Application/Playouts/PlayoutViewModel.cs @@ -1,10 +1,9 @@ using ErsatzTV.Core.Domain; -namespace ErsatzTV.Application.Playouts -{ - public record PlayoutViewModel( - int Id, - PlayoutChannelViewModel Channel, - PlayoutProgramScheduleViewModel ProgramSchedule, - ProgramSchedulePlayoutType ProgramSchedulePlayoutType); -} +namespace ErsatzTV.Application.Playouts; + +public record PlayoutViewModel( + int Id, + PlayoutChannelViewModel Channel, + PlayoutProgramScheduleViewModel ProgramSchedule, + ProgramSchedulePlayoutType ProgramSchedulePlayoutType); \ No newline at end of file diff --git a/ErsatzTV.Application/Playouts/Queries/GetAllPlayouts.cs b/ErsatzTV.Application/Playouts/Queries/GetAllPlayouts.cs index ae258c66a..429eb6f43 100644 --- a/ErsatzTV.Application/Playouts/Queries/GetAllPlayouts.cs +++ b/ErsatzTV.Application/Playouts/Queries/GetAllPlayouts.cs @@ -1,7 +1,3 @@ -using System.Collections.Generic; -using MediatR; +namespace ErsatzTV.Application.Playouts; -namespace ErsatzTV.Application.Playouts.Queries -{ - public record GetAllPlayouts : IRequest>; -} +public record GetAllPlayouts : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Playouts/Queries/GetAllPlayoutsHandler.cs b/ErsatzTV.Application/Playouts/Queries/GetAllPlayoutsHandler.cs index d2e85a5b7..f26922620 100644 --- a/ErsatzTV.Application/Playouts/Queries/GetAllPlayoutsHandler.cs +++ b/ErsatzTV.Application/Playouts/Queries/GetAllPlayoutsHandler.cs @@ -1,35 +1,29 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Infrastructure.Data; -using MediatR; +using ErsatzTV.Infrastructure.Data; using Microsoft.EntityFrameworkCore; -using static LanguageExt.Prelude; -namespace ErsatzTV.Application.Playouts.Queries +namespace ErsatzTV.Application.Playouts; + +public class GetAllPlayoutsHandler : IRequestHandler> { - public class GetAllPlayoutsHandler : IRequestHandler> + private readonly IDbContextFactory _dbContextFactory; + + public GetAllPlayoutsHandler(IDbContextFactory dbContextFactory) => + _dbContextFactory = dbContextFactory; + + public async Task> Handle( + GetAllPlayouts request, + CancellationToken cancellationToken) { - private readonly IDbContextFactory _dbContextFactory; - - public GetAllPlayoutsHandler(IDbContextFactory dbContextFactory) => - _dbContextFactory = dbContextFactory; - - public async Task> Handle( - GetAllPlayouts request, - 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, - Optional(p.DailyRebuildTime))) - .ToListAsync(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, + Optional(p.DailyRebuildTime))) + .ToListAsync(cancellationToken); } -} +} \ No newline at end of file diff --git a/ErsatzTV.Application/Playouts/Queries/GetFuturePlayoutItemsById.cs b/ErsatzTV.Application/Playouts/Queries/GetFuturePlayoutItemsById.cs index b831c8726..a134ffb1d 100644 --- a/ErsatzTV.Application/Playouts/Queries/GetFuturePlayoutItemsById.cs +++ b/ErsatzTV.Application/Playouts/Queries/GetFuturePlayoutItemsById.cs @@ -1,6 +1,3 @@ -using MediatR; +namespace ErsatzTV.Application.Playouts; -namespace ErsatzTV.Application.Playouts.Queries -{ - public record GetFuturePlayoutItemsById(int PlayoutId, bool ShowFiller, int PageNum, int PageSize) : IRequest; -} +public record GetFuturePlayoutItemsById(int PlayoutId, bool ShowFiller, int PageNum, int PageSize) : IRequest; \ No newline at end of file diff --git a/ErsatzTV.Application/Playouts/Queries/GetFuturePlayoutItemsByIdHandler.cs b/ErsatzTV.Application/Playouts/Queries/GetFuturePlayoutItemsByIdHandler.cs index a22182887..1e87870c0 100644 --- a/ErsatzTV.Application/Playouts/Queries/GetFuturePlayoutItemsByIdHandler.cs +++ b/ErsatzTV.Application/Playouts/Queries/GetFuturePlayoutItemsByIdHandler.cs @@ -1,76 +1,68 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Filler; using ErsatzTV.Infrastructure.Data; -using LanguageExt; -using MediatR; using Microsoft.EntityFrameworkCore; using static ErsatzTV.Application.Playouts.Mapper; -namespace ErsatzTV.Application.Playouts.Queries +namespace ErsatzTV.Application.Playouts; + +public class GetFuturePlayoutItemsByIdHandler : IRequestHandler { - public class GetFuturePlayoutItemsByIdHandler : IRequestHandler + private readonly IDbContextFactory _dbContextFactory; + + public GetFuturePlayoutItemsByIdHandler(IDbContextFactory dbContextFactory) => + _dbContextFactory = dbContextFactory; + + public async Task Handle( + GetFuturePlayoutItemsById request, + CancellationToken cancellationToken) { - private readonly IDbContextFactory _dbContextFactory; + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - public GetFuturePlayoutItemsByIdHandler(IDbContextFactory dbContextFactory) => - _dbContextFactory = dbContextFactory; + DateTime now = DateTimeOffset.Now.UtcDateTime; - public async Task Handle( - GetFuturePlayoutItemsById request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - - DateTime now = DateTimeOffset.Now.UtcDateTime; - - int totalCount = await dbContext.PlayoutItems - .CountAsync(i => i.Finish >= now && i.PlayoutId == request.PlayoutId && (request.ShowFiller || i.FillerKind == FillerKind.None), cancellationToken); + int totalCount = await dbContext.PlayoutItems + .CountAsync(i => i.Finish >= now && i.PlayoutId == request.PlayoutId && (request.ShowFiller || i.FillerKind == FillerKind.None), cancellationToken); - List page = await dbContext.PlayoutItems - .Include(i => i.MediaItem) - .ThenInclude(mi => (mi as Movie).MovieMetadata) - .Include(i => i.MediaItem) - .ThenInclude(mi => (mi as Movie).MediaVersions) - .Include(i => i.MediaItem) - .ThenInclude(mi => (mi as MusicVideo).MusicVideoMetadata) - .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) - .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) - .Include(i => i.MediaItem) - .ThenInclude(mi => (mi as OtherVideo).OtherVideoMetadata) - .Include(i => i.MediaItem) - .ThenInclude(mi => (mi as OtherVideo).MediaVersions) - .Include(i => i.MediaItem) - .ThenInclude(mi => (mi as Song).SongMetadata) - .Include(i => i.MediaItem) - .ThenInclude(mi => (mi as Song).MediaVersions) - .Filter(i => i.PlayoutId == request.PlayoutId) - .Filter(i => i.Finish >= now) - .Filter(i => request.ShowFiller || i.FillerKind == FillerKind.None) - .OrderBy(i => i.Start) - .Skip(request.PageNum * request.PageSize) - .Take(request.PageSize) - .ToListAsync(cancellationToken) - .Map(list => list.Map(ProjectToViewModel).ToList()); + List page = await dbContext.PlayoutItems + .Include(i => i.MediaItem) + .ThenInclude(mi => (mi as Movie).MovieMetadata) + .Include(i => i.MediaItem) + .ThenInclude(mi => (mi as Movie).MediaVersions) + .Include(i => i.MediaItem) + .ThenInclude(mi => (mi as MusicVideo).MusicVideoMetadata) + .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) + .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) + .Include(i => i.MediaItem) + .ThenInclude(mi => (mi as OtherVideo).OtherVideoMetadata) + .Include(i => i.MediaItem) + .ThenInclude(mi => (mi as OtherVideo).MediaVersions) + .Include(i => i.MediaItem) + .ThenInclude(mi => (mi as Song).SongMetadata) + .Include(i => i.MediaItem) + .ThenInclude(mi => (mi as Song).MediaVersions) + .Filter(i => i.PlayoutId == request.PlayoutId) + .Filter(i => i.Finish >= now) + .Filter(i => request.ShowFiller || i.FillerKind == FillerKind.None) + .OrderBy(i => i.Start) + .Skip(request.PageNum * request.PageSize) + .Take(request.PageSize) + .ToListAsync(cancellationToken) + .Map(list => list.Map(ProjectToViewModel).ToList()); - return new PagedPlayoutItemsViewModel(totalCount, page); - } + return new PagedPlayoutItemsViewModel(totalCount, page); } -} +} \ No newline at end of file diff --git a/ErsatzTV.Application/Plex/Commands/SignOutOfPlex.cs b/ErsatzTV.Application/Plex/Commands/SignOutOfPlex.cs index 926f318ed..5a8e96665 100644 --- a/ErsatzTV.Application/Plex/Commands/SignOutOfPlex.cs +++ b/ErsatzTV.Application/Plex/Commands/SignOutOfPlex.cs @@ -1,7 +1,5 @@ using ErsatzTV.Core; -using LanguageExt; -namespace ErsatzTV.Application.Plex.Commands -{ - public record SignOutOfPlex : MediatR.IRequest>; -} +namespace ErsatzTV.Application.Plex; + +public record SignOutOfPlex : MediatR.IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Plex/Commands/SignOutOfPlexHandler.cs b/ErsatzTV.Application/Plex/Commands/SignOutOfPlexHandler.cs index 3d4227230..076d3dfb4 100644 --- a/ErsatzTV.Application/Plex/Commands/SignOutOfPlexHandler.cs +++ b/ErsatzTV.Application/Plex/Commands/SignOutOfPlexHandler.cs @@ -1,43 +1,38 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Interfaces.Locking; using ErsatzTV.Core.Interfaces.Plex; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Search; -using LanguageExt; -namespace ErsatzTV.Application.Plex.Commands +namespace ErsatzTV.Application.Plex; + +public class SignOutOfPlexHandler : MediatR.IRequestHandler> { - public class SignOutOfPlexHandler : MediatR.IRequestHandler> + private readonly IEntityLocker _entityLocker; + private readonly IMediaSourceRepository _mediaSourceRepository; + private readonly IPlexSecretStore _plexSecretStore; + private readonly ISearchIndex _searchIndex; + + public SignOutOfPlexHandler( + IMediaSourceRepository mediaSourceRepository, + IPlexSecretStore plexSecretStore, + IEntityLocker entityLocker, + ISearchIndex searchIndex) { - private readonly IEntityLocker _entityLocker; - private readonly IMediaSourceRepository _mediaSourceRepository; - private readonly IPlexSecretStore _plexSecretStore; - private readonly ISearchIndex _searchIndex; - - public SignOutOfPlexHandler( - IMediaSourceRepository mediaSourceRepository, - IPlexSecretStore plexSecretStore, - IEntityLocker entityLocker, - ISearchIndex searchIndex) - { - _mediaSourceRepository = mediaSourceRepository; - _plexSecretStore = plexSecretStore; - _entityLocker = entityLocker; - _searchIndex = searchIndex; - } - - public async Task> Handle(SignOutOfPlex request, CancellationToken cancellationToken) - { - List ids = await _mediaSourceRepository.DeleteAllPlex(); - await _searchIndex.RemoveItems(ids); - _searchIndex.Commit(); - await _plexSecretStore.DeleteAll(); - _entityLocker.UnlockPlex(); - - return Unit.Default; - } + _mediaSourceRepository = mediaSourceRepository; + _plexSecretStore = plexSecretStore; + _entityLocker = entityLocker; + _searchIndex = searchIndex; } -} + + public async Task> Handle(SignOutOfPlex request, CancellationToken cancellationToken) + { + List ids = await _mediaSourceRepository.DeleteAllPlex(); + await _searchIndex.RemoveItems(ids); + _searchIndex.Commit(); + await _plexSecretStore.DeleteAll(); + _entityLocker.UnlockPlex(); + + return Unit.Default; + } +} \ No newline at end of file diff --git a/ErsatzTV.Application/Plex/Commands/StartPlexPinFlow.cs b/ErsatzTV.Application/Plex/Commands/StartPlexPinFlow.cs index 5c5eebbc2..62e6c62ec 100644 --- a/ErsatzTV.Application/Plex/Commands/StartPlexPinFlow.cs +++ b/ErsatzTV.Application/Plex/Commands/StartPlexPinFlow.cs @@ -1,8 +1,5 @@ using ErsatzTV.Core; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.Plex.Commands -{ - public record StartPlexPinFlow : IRequest>; -} +namespace ErsatzTV.Application.Plex; + +public record StartPlexPinFlow : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Plex/Commands/StartPlexPinFlowHandler.cs b/ErsatzTV.Application/Plex/Commands/StartPlexPinFlowHandler.cs index 869897018..4c417b83b 100644 --- a/ErsatzTV.Application/Plex/Commands/StartPlexPinFlowHandler.cs +++ b/ErsatzTV.Application/Plex/Commands/StartPlexPinFlowHandler.cs @@ -1,38 +1,32 @@ -using System.Threading; -using System.Threading.Channels; -using System.Threading.Tasks; +using System.Threading.Channels; using ErsatzTV.Core; using ErsatzTV.Core.Interfaces.Plex; -using LanguageExt; -using MediatR; -using static LanguageExt.Prelude; -namespace ErsatzTV.Application.Plex.Commands +namespace ErsatzTV.Application.Plex; + +public class StartPlexPinFlowHandler : IRequestHandler> { - public class StartPlexPinFlowHandler : IRequestHandler> + private readonly ChannelWriter _channel; + private readonly IPlexTvApiClient _plexTvApiClient; + + public StartPlexPinFlowHandler( + IPlexTvApiClient plexTvApiClient, + ChannelWriter channel) { - private readonly ChannelWriter _channel; - private readonly IPlexTvApiClient _plexTvApiClient; - - public StartPlexPinFlowHandler( - IPlexTvApiClient plexTvApiClient, - ChannelWriter channel) - { - _plexTvApiClient = plexTvApiClient; - _channel = channel; - } - - public Task> Handle( - StartPlexPinFlow request, - CancellationToken cancellationToken) => - _plexTvApiClient.StartPinFlow().Bind( - result => result.Match( - Left: error => Task.FromResult(Left(error)), - Right: async pin => - { - await _channel.WriteAsync(new TryCompletePlexPinFlow(pin), cancellationToken); - return Right(pin.Url); - }) - ); + _plexTvApiClient = plexTvApiClient; + _channel = channel; } -} + + public Task> Handle( + StartPlexPinFlow request, + CancellationToken cancellationToken) => + _plexTvApiClient.StartPinFlow().Bind( + result => result.Match( + Left: error => Task.FromResult(Left(error)), + Right: async pin => + { + await _channel.WriteAsync(new TryCompletePlexPinFlow(pin), cancellationToken); + return Right(pin.Url); + }) + ); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Plex/Commands/SynchronizePlexLibraries.cs b/ErsatzTV.Application/Plex/Commands/SynchronizePlexLibraries.cs index 9640fc735..4f23bcab6 100644 --- a/ErsatzTV.Application/Plex/Commands/SynchronizePlexLibraries.cs +++ b/ErsatzTV.Application/Plex/Commands/SynchronizePlexLibraries.cs @@ -1,8 +1,6 @@ using ErsatzTV.Core; -using LanguageExt; -namespace ErsatzTV.Application.Plex.Commands -{ - public record SynchronizePlexLibraries(int PlexMediaSourceId) : MediatR.IRequest>, - IPlexBackgroundServiceRequest; -} +namespace ErsatzTV.Application.Plex; + +public record SynchronizePlexLibraries(int PlexMediaSourceId) : MediatR.IRequest>, + IPlexBackgroundServiceRequest; \ No newline at end of file diff --git a/ErsatzTV.Application/Plex/Commands/SynchronizePlexLibrariesHandler.cs b/ErsatzTV.Application/Plex/Commands/SynchronizePlexLibrariesHandler.cs index c970ec314..76ca7f64c 100644 --- a/ErsatzTV.Application/Plex/Commands/SynchronizePlexLibrariesHandler.cs +++ b/ErsatzTV.Application/Plex/Commands/SynchronizePlexLibrariesHandler.cs @@ -1,115 +1,109 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Plex; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Plex; -using LanguageExt; using Microsoft.Extensions.Logging; -namespace ErsatzTV.Application.Plex.Commands +namespace ErsatzTV.Application.Plex; + +public class + SynchronizePlexLibrariesHandler : MediatR.IRequestHandler> { - public class - SynchronizePlexLibrariesHandler : MediatR.IRequestHandler> + private readonly ILogger _logger; + private readonly IMediaSourceRepository _mediaSourceRepository; + private readonly IPlexSecretStore _plexSecretStore; + private readonly IPlexServerApiClient _plexServerApiClient; + private readonly ISearchIndex _searchIndex; + + public SynchronizePlexLibrariesHandler( + IMediaSourceRepository mediaSourceRepository, + IPlexSecretStore plexSecretStore, + IPlexServerApiClient plexServerApiClient, + ILogger logger, + ISearchIndex searchIndex) { - private readonly ILogger _logger; - private readonly IMediaSourceRepository _mediaSourceRepository; - private readonly IPlexSecretStore _plexSecretStore; - private readonly IPlexServerApiClient _plexServerApiClient; - private readonly ISearchIndex _searchIndex; - - public SynchronizePlexLibrariesHandler( - IMediaSourceRepository mediaSourceRepository, - IPlexSecretStore plexSecretStore, - IPlexServerApiClient plexServerApiClient, - ILogger logger, - ISearchIndex searchIndex) - { - _mediaSourceRepository = mediaSourceRepository; - _plexSecretStore = plexSecretStore; - _plexServerApiClient = plexServerApiClient; - _logger = logger; - _searchIndex = searchIndex; - } - - public Task> Handle( - SynchronizePlexLibraries request, - CancellationToken cancellationToken) => - Validate(request) - .MapT(SynchronizeLibraries) - .Bind(v => v.ToEitherAsync()); - - private Task> Validate(SynchronizePlexLibraries request) => - MediaSourceMustExist(request) - .BindT(MediaSourceMustHaveActiveConnection) - .BindT(MediaSourceMustHaveToken); - - private Task> MediaSourceMustExist(SynchronizePlexLibraries request) => - _mediaSourceRepository.GetPlex(request.PlexMediaSourceId) - .Map(o => o.ToValidation("Plex media source does not exist.")); - - private Validation MediaSourceMustHaveActiveConnection( - PlexMediaSource plexMediaSource) - { - Option maybeConnection = - plexMediaSource.Connections.SingleOrDefault(c => c.IsActive); - return maybeConnection.Map(connection => new ConnectionParameters(plexMediaSource, connection)) - .ToValidation("Plex media source requires an active connection"); - } - - private async Task> MediaSourceMustHaveToken( - ConnectionParameters connectionParameters) - { - Option maybeToken = await - _plexSecretStore.GetServerAuthToken(connectionParameters.PlexMediaSource.ClientIdentifier); - return maybeToken.Map(token => connectionParameters with { PlexServerAuthToken = token }) - .ToValidation("Plex media source requires a token"); - } - - private async Task SynchronizeLibraries(ConnectionParameters connectionParameters) - { - Either> maybeLibraries = await _plexServerApiClient.GetLibraries( - connectionParameters.ActiveConnection, - connectionParameters.PlexServerAuthToken); - - await maybeLibraries.Match( - async libraries => - { - var existing = connectionParameters.PlexMediaSource.Libraries.OfType().ToList(); - var toAdd = libraries.Filter(library => existing.All(l => l.Key != library.Key)).ToList(); - var toRemove = existing.Filter(library => libraries.All(l => l.Key != library.Key)).ToList(); - List ids = await _mediaSourceRepository.UpdateLibraries( - connectionParameters.PlexMediaSource.Id, - toAdd, - toRemove); - if (ids.Any()) - { - await _searchIndex.RemoveItems(ids); - _searchIndex.Commit(); - } - }, - error => - { - _logger.LogWarning( - "Unable to synchronize libraries from plex server {PlexServer}: {Error}", - connectionParameters.PlexMediaSource.ServerName, - error.Value); - - return Task.CompletedTask; - }); - - return Unit.Default; - } - - private record ConnectionParameters( - PlexMediaSource PlexMediaSource, - PlexConnection ActiveConnection) - { - public PlexServerAuthToken PlexServerAuthToken { get; set; } - } + _mediaSourceRepository = mediaSourceRepository; + _plexSecretStore = plexSecretStore; + _plexServerApiClient = plexServerApiClient; + _logger = logger; + _searchIndex = searchIndex; } -} + + public Task> Handle( + SynchronizePlexLibraries request, + CancellationToken cancellationToken) => + Validate(request) + .MapT(SynchronizeLibraries) + .Bind(v => v.ToEitherAsync()); + + private Task> Validate(SynchronizePlexLibraries request) => + MediaSourceMustExist(request) + .BindT(MediaSourceMustHaveActiveConnection) + .BindT(MediaSourceMustHaveToken); + + private Task> MediaSourceMustExist(SynchronizePlexLibraries request) => + _mediaSourceRepository.GetPlex(request.PlexMediaSourceId) + .Map(o => o.ToValidation("Plex media source does not exist.")); + + private Validation MediaSourceMustHaveActiveConnection( + PlexMediaSource plexMediaSource) + { + Option maybeConnection = + plexMediaSource.Connections.SingleOrDefault(c => c.IsActive); + return maybeConnection.Map(connection => new ConnectionParameters(plexMediaSource, connection)) + .ToValidation("Plex media source requires an active connection"); + } + + private async Task> MediaSourceMustHaveToken( + ConnectionParameters connectionParameters) + { + Option maybeToken = await + _plexSecretStore.GetServerAuthToken(connectionParameters.PlexMediaSource.ClientIdentifier); + return maybeToken.Map(token => connectionParameters with { PlexServerAuthToken = token }) + .ToValidation("Plex media source requires a token"); + } + + private async Task SynchronizeLibraries(ConnectionParameters connectionParameters) + { + Either> maybeLibraries = await _plexServerApiClient.GetLibraries( + connectionParameters.ActiveConnection, + connectionParameters.PlexServerAuthToken); + + await maybeLibraries.Match( + async libraries => + { + var existing = connectionParameters.PlexMediaSource.Libraries.OfType().ToList(); + var toAdd = libraries.Filter(library => existing.All(l => l.Key != library.Key)).ToList(); + var toRemove = existing.Filter(library => libraries.All(l => l.Key != library.Key)).ToList(); + List ids = await _mediaSourceRepository.UpdateLibraries( + connectionParameters.PlexMediaSource.Id, + toAdd, + toRemove); + if (ids.Any()) + { + await _searchIndex.RemoveItems(ids); + _searchIndex.Commit(); + } + }, + error => + { + _logger.LogWarning( + "Unable to synchronize libraries from plex server {PlexServer}: {Error}", + connectionParameters.PlexMediaSource.ServerName, + error.Value); + + return Task.CompletedTask; + }); + + return Unit.Default; + } + + private record ConnectionParameters( + PlexMediaSource PlexMediaSource, + PlexConnection ActiveConnection) + { + public PlexServerAuthToken PlexServerAuthToken { get; set; } + } +} \ No newline at end of file diff --git a/ErsatzTV.Application/Plex/Commands/SynchronizePlexLibraryById.cs b/ErsatzTV.Application/Plex/Commands/SynchronizePlexLibraryById.cs index 4a979d047..1192f8d50 100644 --- a/ErsatzTV.Application/Plex/Commands/SynchronizePlexLibraryById.cs +++ b/ErsatzTV.Application/Plex/Commands/SynchronizePlexLibraryById.cs @@ -1,24 +1,21 @@ using ErsatzTV.Core; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.Plex.Commands +namespace ErsatzTV.Application.Plex; + +public interface ISynchronizePlexLibraryById : IRequest>, IPlexBackgroundServiceRequest { - public interface ISynchronizePlexLibraryById : IRequest>, IPlexBackgroundServiceRequest - { - int PlexLibraryId { get; } - bool ForceScan { get; } - } - - public record SynchronizePlexLibraryByIdIfNeeded - (int PlexLibraryId) : ISynchronizePlexLibraryById - { - public bool ForceScan => false; - } - - public record ForceSynchronizePlexLibraryById - (int PlexLibraryId) : ISynchronizePlexLibraryById - { - public bool ForceScan => true; - } + int PlexLibraryId { get; } + bool ForceScan { get; } } + +public record SynchronizePlexLibraryByIdIfNeeded + (int PlexLibraryId) : ISynchronizePlexLibraryById +{ + public bool ForceScan => false; +} + +public record ForceSynchronizePlexLibraryById + (int PlexLibraryId) : ISynchronizePlexLibraryById +{ + public bool ForceScan => true; +} \ No newline at end of file diff --git a/ErsatzTV.Application/Plex/Commands/SynchronizePlexLibraryByIdHandler.cs b/ErsatzTV.Application/Plex/Commands/SynchronizePlexLibraryByIdHandler.cs index 49b1a91ae..5cd0d2a04 100644 --- a/ErsatzTV.Application/Plex/Commands/SynchronizePlexLibraryByIdHandler.cs +++ b/ErsatzTV.Application/Plex/Commands/SynchronizePlexLibraryByIdHandler.cs @@ -1,176 +1,167 @@ -using System; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Locking; using ErsatzTV.Core.Interfaces.Plex; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Plex; -using LanguageExt; -using MediatR; using Microsoft.Extensions.Logging; -using Unit = LanguageExt.Unit; -namespace ErsatzTV.Application.Plex.Commands +namespace ErsatzTV.Application.Plex; + +public class + SynchronizePlexLibraryByIdHandler : IRequestHandler>, + IRequestHandler> { - public class - SynchronizePlexLibraryByIdHandler : IRequestHandler>, - IRequestHandler> + private readonly IConfigElementRepository _configElementRepository; + private readonly IEntityLocker _entityLocker; + private readonly ILibraryRepository _libraryRepository; + private readonly ILogger _logger; + private readonly IMediaSourceRepository _mediaSourceRepository; + private readonly IPlexMovieLibraryScanner _plexMovieLibraryScanner; + private readonly IPlexSecretStore _plexSecretStore; + private readonly IPlexTelevisionLibraryScanner _plexTelevisionLibraryScanner; + + public SynchronizePlexLibraryByIdHandler( + IMediaSourceRepository mediaSourceRepository, + IConfigElementRepository configElementRepository, + IPlexSecretStore plexSecretStore, + IPlexMovieLibraryScanner plexMovieLibraryScanner, + IPlexTelevisionLibraryScanner plexTelevisionLibraryScanner, + ILibraryRepository libraryRepository, + IEntityLocker entityLocker, + ILogger logger) { - private readonly IConfigElementRepository _configElementRepository; - private readonly IEntityLocker _entityLocker; - private readonly ILibraryRepository _libraryRepository; - private readonly ILogger _logger; - private readonly IMediaSourceRepository _mediaSourceRepository; - private readonly IPlexMovieLibraryScanner _plexMovieLibraryScanner; - private readonly IPlexSecretStore _plexSecretStore; - private readonly IPlexTelevisionLibraryScanner _plexTelevisionLibraryScanner; - - public SynchronizePlexLibraryByIdHandler( - IMediaSourceRepository mediaSourceRepository, - IConfigElementRepository configElementRepository, - IPlexSecretStore plexSecretStore, - IPlexMovieLibraryScanner plexMovieLibraryScanner, - IPlexTelevisionLibraryScanner plexTelevisionLibraryScanner, - ILibraryRepository libraryRepository, - IEntityLocker entityLocker, - ILogger logger) - { - _mediaSourceRepository = mediaSourceRepository; - _configElementRepository = configElementRepository; - _plexSecretStore = plexSecretStore; - _plexMovieLibraryScanner = plexMovieLibraryScanner; - _plexTelevisionLibraryScanner = plexTelevisionLibraryScanner; - _libraryRepository = libraryRepository; - _entityLocker = entityLocker; - _logger = logger; - } - - public Task> Handle( - ForceSynchronizePlexLibraryById request, - CancellationToken cancellationToken) => Handle(request); - - public Task> Handle( - SynchronizePlexLibraryByIdIfNeeded request, - CancellationToken cancellationToken) => Handle(request); - - private Task> - Handle(ISynchronizePlexLibraryById request) => - Validate(request) - .MapT(parameters => Synchronize(parameters).Map(_ => parameters.Library.Name)) - .Bind(v => v.ToEitherAsync()); - - private async Task Synchronize(RequestParameters parameters) - { - var lastScan = new DateTimeOffset(parameters.Library.LastScan ?? SystemTime.MinValueUtc, TimeSpan.Zero); - DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(parameters.LibraryRefreshInterval); - if (parameters.ForceScan || nextScan < DateTimeOffset.Now) - { - switch (parameters.Library.MediaKind) - { - case LibraryMediaKind.Movies: - await _plexMovieLibraryScanner.ScanLibrary( - parameters.ConnectionParameters.ActiveConnection, - parameters.ConnectionParameters.PlexServerAuthToken, - parameters.Library, - parameters.FFprobePath); - break; - case LibraryMediaKind.Shows: - await _plexTelevisionLibraryScanner.ScanLibrary( - parameters.ConnectionParameters.ActiveConnection, - parameters.ConnectionParameters.PlexServerAuthToken, - parameters.Library, - parameters.FFprobePath); - break; - } - - parameters.Library.LastScan = DateTime.UtcNow; - await _libraryRepository.UpdateLastScan(parameters.Library); - } - else - { - _logger.LogDebug( - "Skipping unforced scan of plex media library {Name}", - parameters.Library.Name); - } - - _entityLocker.UnlockLibrary(parameters.Library.Id); - return Unit.Default; - } - - private async Task> Validate(ISynchronizePlexLibraryById request) => - (await ValidateConnection(request), await PlexLibraryMustExist(request), - await ValidateLibraryRefreshInterval(), await ValidateFFprobePath()) - .Apply( - (connectionParameters, plexLibrary, libraryRefreshInterval, ffprobePath) => new RequestParameters( - connectionParameters, - plexLibrary, - request.ForceScan, - libraryRefreshInterval, - ffprobePath - )); - - private Task> ValidateConnection( - ISynchronizePlexLibraryById request) => - PlexMediaSourceMustExist(request) - .BindT(MediaSourceMustHaveActiveConnection) - .BindT(MediaSourceMustHaveToken); - - private Task> PlexMediaSourceMustExist( - ISynchronizePlexLibraryById request) => - _mediaSourceRepository.GetPlexByLibraryId(request.PlexLibraryId) - .Map( - v => v.ToValidation( - $"Plex media source for library {request.PlexLibraryId} does not exist.")); - - private Validation MediaSourceMustHaveActiveConnection( - PlexMediaSource plexMediaSource) - { - Option maybeConnection = - plexMediaSource.Connections.SingleOrDefault(c => c.IsActive); - return maybeConnection.Map(connection => new ConnectionParameters(plexMediaSource, connection)) - .ToValidation("Plex media source requires an active connection"); - } - - private async Task> MediaSourceMustHaveToken( - ConnectionParameters connectionParameters) - { - Option maybeToken = await - _plexSecretStore.GetServerAuthToken(connectionParameters.PlexMediaSource.ClientIdentifier); - return maybeToken.Map(token => connectionParameters with { PlexServerAuthToken = token }) - .ToValidation("Plex media source requires a token"); - } - - private Task> PlexLibraryMustExist( - ISynchronizePlexLibraryById request) => - _mediaSourceRepository.GetPlexLibrary(request.PlexLibraryId) - .Map(v => v.ToValidation($"Plex library {request.PlexLibraryId} does not exist.")); - - private Task> ValidateLibraryRefreshInterval() => - _configElementRepository.GetValue(ConfigElementKey.LibraryRefreshInterval) - .FilterT(lri => lri > 0) - .Map(lri => lri.ToValidation("Library refresh interval is invalid")); - - private Task> ValidateFFprobePath() => - _configElementRepository.GetValue(ConfigElementKey.FFprobePath) - .FilterT(File.Exists) - .Map( - ffprobePath => - ffprobePath.ToValidation("FFprobe path does not exist on the file system")); - - private record RequestParameters( - ConnectionParameters ConnectionParameters, - PlexLibrary Library, - bool ForceScan, - int LibraryRefreshInterval, - string FFprobePath); - - private record ConnectionParameters(PlexMediaSource PlexMediaSource, PlexConnection ActiveConnection) - { - public PlexServerAuthToken PlexServerAuthToken { get; set; } - } + _mediaSourceRepository = mediaSourceRepository; + _configElementRepository = configElementRepository; + _plexSecretStore = plexSecretStore; + _plexMovieLibraryScanner = plexMovieLibraryScanner; + _plexTelevisionLibraryScanner = plexTelevisionLibraryScanner; + _libraryRepository = libraryRepository; + _entityLocker = entityLocker; + _logger = logger; } -} + + public Task> Handle( + ForceSynchronizePlexLibraryById request, + CancellationToken cancellationToken) => Handle(request); + + public Task> Handle( + SynchronizePlexLibraryByIdIfNeeded request, + CancellationToken cancellationToken) => Handle(request); + + private Task> + Handle(ISynchronizePlexLibraryById request) => + Validate(request) + .MapT(parameters => Synchronize(parameters).Map(_ => parameters.Library.Name)) + .Bind(v => v.ToEitherAsync()); + + private async Task Synchronize(RequestParameters parameters) + { + var lastScan = new DateTimeOffset(parameters.Library.LastScan ?? SystemTime.MinValueUtc, TimeSpan.Zero); + DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(parameters.LibraryRefreshInterval); + if (parameters.ForceScan || nextScan < DateTimeOffset.Now) + { + switch (parameters.Library.MediaKind) + { + case LibraryMediaKind.Movies: + await _plexMovieLibraryScanner.ScanLibrary( + parameters.ConnectionParameters.ActiveConnection, + parameters.ConnectionParameters.PlexServerAuthToken, + parameters.Library, + parameters.FFprobePath); + break; + case LibraryMediaKind.Shows: + await _plexTelevisionLibraryScanner.ScanLibrary( + parameters.ConnectionParameters.ActiveConnection, + parameters.ConnectionParameters.PlexServerAuthToken, + parameters.Library, + parameters.FFprobePath); + break; + } + + parameters.Library.LastScan = DateTime.UtcNow; + await _libraryRepository.UpdateLastScan(parameters.Library); + } + else + { + _logger.LogDebug( + "Skipping unforced scan of plex media library {Name}", + parameters.Library.Name); + } + + _entityLocker.UnlockLibrary(parameters.Library.Id); + return Unit.Default; + } + + private async Task> Validate(ISynchronizePlexLibraryById request) => + (await ValidateConnection(request), await PlexLibraryMustExist(request), + await ValidateLibraryRefreshInterval(), await ValidateFFprobePath()) + .Apply( + (connectionParameters, plexLibrary, libraryRefreshInterval, ffprobePath) => new RequestParameters( + connectionParameters, + plexLibrary, + request.ForceScan, + libraryRefreshInterval, + ffprobePath + )); + + private Task> ValidateConnection( + ISynchronizePlexLibraryById request) => + PlexMediaSourceMustExist(request) + .BindT(MediaSourceMustHaveActiveConnection) + .BindT(MediaSourceMustHaveToken); + + private Task> PlexMediaSourceMustExist( + ISynchronizePlexLibraryById request) => + _mediaSourceRepository.GetPlexByLibraryId(request.PlexLibraryId) + .Map( + v => v.ToValidation( + $"Plex media source for library {request.PlexLibraryId} does not exist.")); + + private Validation MediaSourceMustHaveActiveConnection( + PlexMediaSource plexMediaSource) + { + Option maybeConnection = + plexMediaSource.Connections.SingleOrDefault(c => c.IsActive); + return maybeConnection.Map(connection => new ConnectionParameters(plexMediaSource, connection)) + .ToValidation("Plex media source requires an active connection"); + } + + private async Task> MediaSourceMustHaveToken( + ConnectionParameters connectionParameters) + { + Option maybeToken = await + _plexSecretStore.GetServerAuthToken(connectionParameters.PlexMediaSource.ClientIdentifier); + return maybeToken.Map(token => connectionParameters with { PlexServerAuthToken = token }) + .ToValidation("Plex media source requires a token"); + } + + private Task> PlexLibraryMustExist( + ISynchronizePlexLibraryById request) => + _mediaSourceRepository.GetPlexLibrary(request.PlexLibraryId) + .Map(v => v.ToValidation($"Plex library {request.PlexLibraryId} does not exist.")); + + private Task> ValidateLibraryRefreshInterval() => + _configElementRepository.GetValue(ConfigElementKey.LibraryRefreshInterval) + .FilterT(lri => lri > 0) + .Map(lri => lri.ToValidation("Library refresh interval is invalid")); + + private Task> ValidateFFprobePath() => + _configElementRepository.GetValue(ConfigElementKey.FFprobePath) + .FilterT(File.Exists) + .Map( + ffprobePath => + ffprobePath.ToValidation("FFprobe path does not exist on the file system")); + + private record RequestParameters( + ConnectionParameters ConnectionParameters, + PlexLibrary Library, + bool ForceScan, + int LibraryRefreshInterval, + string FFprobePath); + + private record ConnectionParameters(PlexMediaSource PlexMediaSource, PlexConnection ActiveConnection) + { + public PlexServerAuthToken PlexServerAuthToken { get; set; } + } +} \ No newline at end of file diff --git a/ErsatzTV.Application/Plex/Commands/SynchronizePlexMediaSources.cs b/ErsatzTV.Application/Plex/Commands/SynchronizePlexMediaSources.cs index c1c7680a5..83da2a37a 100644 --- a/ErsatzTV.Application/Plex/Commands/SynchronizePlexMediaSources.cs +++ b/ErsatzTV.Application/Plex/Commands/SynchronizePlexMediaSources.cs @@ -1,11 +1,7 @@ -using System.Collections.Generic; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.Plex.Commands -{ - public record - SynchronizePlexMediaSources : IRequest>>, IPlexBackgroundServiceRequest; -} +namespace ErsatzTV.Application.Plex; + +public record + SynchronizePlexMediaSources : IRequest>>, IPlexBackgroundServiceRequest; \ No newline at end of file diff --git a/ErsatzTV.Application/Plex/Commands/SynchronizePlexMediaSourcesHandler.cs b/ErsatzTV.Application/Plex/Commands/SynchronizePlexMediaSourcesHandler.cs index b524991a1..4552769ed 100644 --- a/ErsatzTV.Application/Plex/Commands/SynchronizePlexMediaSourcesHandler.cs +++ b/ErsatzTV.Application/Plex/Commands/SynchronizePlexMediaSourcesHandler.cs @@ -1,151 +1,144 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Channels; -using System.Threading.Tasks; +using System.Threading.Channels; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Locking; using ErsatzTV.Core.Interfaces.Plex; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Plex; -using LanguageExt; -using MediatR; using Microsoft.Extensions.Logging; -namespace ErsatzTV.Application.Plex.Commands +namespace ErsatzTV.Application.Plex; + +public class + SynchronizePlexMediaSourcesHandler : IRequestHandler>> { - public class - SynchronizePlexMediaSourcesHandler : IRequestHandler>> + private readonly ChannelWriter _channel; + private readonly IEntityLocker _entityLocker; + private readonly ILogger _logger; + private readonly IMediaSourceRepository _mediaSourceRepository; + private readonly IPlexTvApiClient _plexTvApiClient; + private readonly IPlexServerApiClient _plexServerApiClient; + private readonly IPlexSecretStore _plexSecretStore; + + public SynchronizePlexMediaSourcesHandler( + IMediaSourceRepository mediaSourceRepository, + IPlexTvApiClient plexTvApiClient, + IPlexServerApiClient plexServerApiClient, + IPlexSecretStore plexSecretStore, + ChannelWriter channel, + IEntityLocker entityLocker, + ILogger logger) { - private readonly ChannelWriter _channel; - private readonly IEntityLocker _entityLocker; - private readonly ILogger _logger; - private readonly IMediaSourceRepository _mediaSourceRepository; - private readonly IPlexTvApiClient _plexTvApiClient; - private readonly IPlexServerApiClient _plexServerApiClient; - private readonly IPlexSecretStore _plexSecretStore; + _mediaSourceRepository = mediaSourceRepository; + _plexTvApiClient = plexTvApiClient; + _plexServerApiClient = plexServerApiClient; + _plexSecretStore = plexSecretStore; + _channel = channel; + _entityLocker = entityLocker; + _logger = logger; + } - public SynchronizePlexMediaSourcesHandler( - IMediaSourceRepository mediaSourceRepository, - IPlexTvApiClient plexTvApiClient, - IPlexServerApiClient plexServerApiClient, - IPlexSecretStore plexSecretStore, - ChannelWriter channel, - IEntityLocker entityLocker, - ILogger logger) + public Task>> Handle( + SynchronizePlexMediaSources request, + CancellationToken cancellationToken) => _plexTvApiClient.GetServers().BindAsync(SynchronizeAllServers); + + private async Task>> SynchronizeAllServers( + List servers) + { + List allExisting = await _mediaSourceRepository.GetAllPlex(); + foreach (PlexMediaSource server in servers) { - _mediaSourceRepository = mediaSourceRepository; - _plexTvApiClient = plexTvApiClient; - _plexServerApiClient = plexServerApiClient; - _plexSecretStore = plexSecretStore; - _channel = channel; - _entityLocker = entityLocker; - _logger = logger; + await SynchronizeServer(allExisting, server); } - public Task>> Handle( - SynchronizePlexMediaSources request, - CancellationToken cancellationToken) => _plexTvApiClient.GetServers().BindAsync(SynchronizeAllServers); - - private async Task>> SynchronizeAllServers( - List servers) + // delete removed servers + foreach (PlexMediaSource removed in allExisting.Filter( + s => servers.All(pms => pms.ClientIdentifier != s.ClientIdentifier))) { - List allExisting = await _mediaSourceRepository.GetAllPlex(); - foreach (PlexMediaSource server in servers) - { - await SynchronizeServer(allExisting, server); - } - - // delete removed servers - foreach (PlexMediaSource removed in allExisting.Filter( - s => servers.All(pms => pms.ClientIdentifier != s.ClientIdentifier))) - { - _logger.LogWarning("Deleting removed Plex server {ServerName}!", removed.Id.ToString()); - await _mediaSourceRepository.DeletePlex(removed); - } - - foreach (PlexMediaSource mediaSource in await _mediaSourceRepository.GetAllPlex()) - { - await _channel.WriteAsync(new SynchronizePlexLibraries(mediaSource.Id)); - } - - _entityLocker.UnlockPlex(); - - return allExisting; + _logger.LogWarning("Deleting removed Plex server {ServerName}!", removed.Id.ToString()); + await _mediaSourceRepository.DeletePlex(removed); } - private async Task SynchronizeServer(List allExisting, PlexMediaSource server) + foreach (PlexMediaSource mediaSource in await _mediaSourceRepository.GetAllPlex()) { - Option maybeExisting = - allExisting.Find(s => s.ClientIdentifier == server.ClientIdentifier); - - foreach (PlexMediaSource existing in maybeExisting) - { - existing.Platform = server.Platform; - existing.PlatformVersion = server.PlatformVersion; - existing.ProductVersion = server.ProductVersion; - existing.ServerName = server.ServerName; - var toAdd = server.Connections - .Filter(connection => existing.Connections.All(c => c.Uri != connection.Uri)).ToList(); - var toRemove = existing.Connections - .Filter(connection => server.Connections.All(c => c.Uri != connection.Uri)).ToList(); - await _mediaSourceRepository.Update(existing, toAdd, toRemove); - await FindConnectionToActivate(existing); - } - - if (maybeExisting.IsNone) - { - await _mediaSourceRepository.Add(server); - await FindConnectionToActivate(server); - } + await _channel.WriteAsync(new SynchronizePlexLibraries(mediaSource.Id)); } - private async Task FindConnectionToActivate(PlexMediaSource server) + _entityLocker.UnlockPlex(); + + return allExisting; + } + + private async Task SynchronizeServer(List allExisting, PlexMediaSource server) + { + Option maybeExisting = + allExisting.Find(s => s.ClientIdentifier == server.ClientIdentifier); + + foreach (PlexMediaSource existing in maybeExisting) { - var prioritized = server.Connections.OrderBy(pc => pc.IsActive ? 0 : 1).ToList(); - foreach (PlexConnection connection in server.Connections) - { - connection.IsActive = false; - } + existing.Platform = server.Platform; + existing.PlatformVersion = server.PlatformVersion; + existing.ProductVersion = server.ProductVersion; + existing.ServerName = server.ServerName; + var toAdd = server.Connections + .Filter(connection => existing.Connections.All(c => c.Uri != connection.Uri)).ToList(); + var toRemove = existing.Connections + .Filter(connection => server.Connections.All(c => c.Uri != connection.Uri)).ToList(); + await _mediaSourceRepository.Update(existing, toAdd, toRemove); + await FindConnectionToActivate(existing); + } - Option maybeToken = await _plexSecretStore.GetServerAuthToken(server.ClientIdentifier); - foreach (PlexServerAuthToken token in maybeToken) - { - foreach (PlexConnection connection in prioritized) - { - try - { - _logger.LogDebug("Attempting to locate to Plex at {Uri}", connection.Uri); - if (await _plexServerApiClient.Ping(connection, token)) - { - _logger.LogInformation("Located Plex at {Uri}", connection.Uri); - connection.IsActive = true; - break; - } - } - catch - { - // do nothing - } - } - } - - if (maybeToken.IsNone) - { - _logger.LogError( - "Unable to activate Plex connection for server {Server} without auth token", - server.ServerName); - } - - if (server.Connections.All(c => !c.IsActive)) - { - _logger.LogError("Unable to locate Plex"); - server.Connections.Head().IsActive = true; - } - - await _mediaSourceRepository.Update(server, new List(), new List()); + if (maybeExisting.IsNone) + { + await _mediaSourceRepository.Add(server); + await FindConnectionToActivate(server); } } -} + + private async Task FindConnectionToActivate(PlexMediaSource server) + { + var prioritized = server.Connections.OrderBy(pc => pc.IsActive ? 0 : 1).ToList(); + foreach (PlexConnection connection in server.Connections) + { + connection.IsActive = false; + } + + Option maybeToken = await _plexSecretStore.GetServerAuthToken(server.ClientIdentifier); + foreach (PlexServerAuthToken token in maybeToken) + { + foreach (PlexConnection connection in prioritized) + { + try + { + _logger.LogDebug("Attempting to locate to Plex at {Uri}", connection.Uri); + if (await _plexServerApiClient.Ping(connection, token)) + { + _logger.LogInformation("Located Plex at {Uri}", connection.Uri); + connection.IsActive = true; + break; + } + } + catch + { + // do nothing + } + } + } + + if (maybeToken.IsNone) + { + _logger.LogError( + "Unable to activate Plex connection for server {Server} without auth token", + server.ServerName); + } + + if (server.Connections.All(c => !c.IsActive)) + { + _logger.LogError("Unable to locate Plex"); + server.Connections.Head().IsActive = true; + } + + await _mediaSourceRepository.Update(server, new List(), new List()); + } +} \ No newline at end of file diff --git a/ErsatzTV.Application/Plex/Commands/TryCompletePlexPinFlow.cs b/ErsatzTV.Application/Plex/Commands/TryCompletePlexPinFlow.cs index 97dc66389..00d3fa4d0 100644 --- a/ErsatzTV.Application/Plex/Commands/TryCompletePlexPinFlow.cs +++ b/ErsatzTV.Application/Plex/Commands/TryCompletePlexPinFlow.cs @@ -1,10 +1,7 @@ using ErsatzTV.Core; using ErsatzTV.Core.Plex; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.Plex.Commands -{ - public record TryCompletePlexPinFlow(PlexAuthPin AuthPin) : IRequest>, - IPlexBackgroundServiceRequest; -} +namespace ErsatzTV.Application.Plex; + +public record TryCompletePlexPinFlow(PlexAuthPin AuthPin) : IRequest>, + IPlexBackgroundServiceRequest; \ No newline at end of file diff --git a/ErsatzTV.Application/Plex/Commands/TryCompletePlexPinFlowHandler.cs b/ErsatzTV.Application/Plex/Commands/TryCompletePlexPinFlowHandler.cs index c8f535b29..b283695e1 100644 --- a/ErsatzTV.Application/Plex/Commands/TryCompletePlexPinFlowHandler.cs +++ b/ErsatzTV.Application/Plex/Commands/TryCompletePlexPinFlowHandler.cs @@ -1,45 +1,39 @@ -using System; -using System.Threading; -using System.Threading.Channels; -using System.Threading.Tasks; +using System.Threading.Channels; using ErsatzTV.Core; using ErsatzTV.Core.Interfaces.Plex; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.Plex.Commands +namespace ErsatzTV.Application.Plex; + +public class TryCompletePlexPinFlowHandler : IRequestHandler> { - public class TryCompletePlexPinFlowHandler : IRequestHandler> + private readonly ChannelWriter _channel; + private readonly IPlexTvApiClient _plexTvApiClient; + + public TryCompletePlexPinFlowHandler( + IPlexTvApiClient plexTvApiClient, + ChannelWriter channel) { - private readonly ChannelWriter _channel; - private readonly IPlexTvApiClient _plexTvApiClient; + _plexTvApiClient = plexTvApiClient; + _channel = channel; + } - public TryCompletePlexPinFlowHandler( - IPlexTvApiClient plexTvApiClient, - ChannelWriter channel) + public async Task> + Handle(TryCompletePlexPinFlow request, CancellationToken cancellationToken) + { + var cts = new CancellationTokenSource(TimeSpan.FromMinutes(2)); + CancellationToken token = cts.Token; + while (!token.IsCancellationRequested) { - _plexTvApiClient = plexTvApiClient; - _channel = channel; - } - - public async Task> - Handle(TryCompletePlexPinFlow request, CancellationToken cancellationToken) - { - var cts = new CancellationTokenSource(TimeSpan.FromMinutes(2)); - CancellationToken token = cts.Token; - while (!token.IsCancellationRequested) + bool result = await _plexTvApiClient.TryCompletePinFlow(request.AuthPin); + if (result) { - bool result = await _plexTvApiClient.TryCompletePinFlow(request.AuthPin); - if (result) - { - await _channel.WriteAsync(new SynchronizePlexMediaSources(), cancellationToken); - return true; - } - - await Task.Delay(TimeSpan.FromSeconds(1), token); + await _channel.WriteAsync(new SynchronizePlexMediaSources(), cancellationToken); + return true; } - return false; + await Task.Delay(TimeSpan.FromSeconds(1), token); } + + return false; } -} +} \ No newline at end of file diff --git a/ErsatzTV.Application/Plex/Commands/UpdatePlexLibraryPreferences.cs b/ErsatzTV.Application/Plex/Commands/UpdatePlexLibraryPreferences.cs index 244c15f8d..57dcb40f1 100644 --- a/ErsatzTV.Application/Plex/Commands/UpdatePlexLibraryPreferences.cs +++ b/ErsatzTV.Application/Plex/Commands/UpdatePlexLibraryPreferences.cs @@ -1,11 +1,8 @@ -using System.Collections.Generic; -using ErsatzTV.Core; -using LanguageExt; +using ErsatzTV.Core; -namespace ErsatzTV.Application.Plex.Commands -{ - public record UpdatePlexLibraryPreferences - (List Preferences) : MediatR.IRequest>; +namespace ErsatzTV.Application.Plex; - public record PlexLibraryPreference(int Id, bool ShouldSyncItems); -} +public record UpdatePlexLibraryPreferences + (List Preferences) : MediatR.IRequest>; + +public record PlexLibraryPreference(int Id, bool ShouldSyncItems); \ No newline at end of file diff --git a/ErsatzTV.Application/Plex/Commands/UpdatePlexLibraryPreferencesHandler.cs b/ErsatzTV.Application/Plex/Commands/UpdatePlexLibraryPreferencesHandler.cs index fdd3355d1..ddc24f837 100644 --- a/ErsatzTV.Application/Plex/Commands/UpdatePlexLibraryPreferencesHandler.cs +++ b/ErsatzTV.Application/Plex/Commands/UpdatePlexLibraryPreferencesHandler.cs @@ -1,42 +1,36 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Search; -using LanguageExt; -namespace ErsatzTV.Application.Plex.Commands +namespace ErsatzTV.Application.Plex; + +public class + UpdatePlexLibraryPreferencesHandler : MediatR.IRequestHandler> { - public class - UpdatePlexLibraryPreferencesHandler : MediatR.IRequestHandler> + private readonly IMediaSourceRepository _mediaSourceRepository; + private readonly ISearchIndex _searchIndex; + + public UpdatePlexLibraryPreferencesHandler( + IMediaSourceRepository mediaSourceRepository, + ISearchIndex searchIndex) { - private readonly IMediaSourceRepository _mediaSourceRepository; - private readonly ISearchIndex _searchIndex; - - public UpdatePlexLibraryPreferencesHandler( - IMediaSourceRepository mediaSourceRepository, - ISearchIndex searchIndex) - { - _mediaSourceRepository = mediaSourceRepository; - _searchIndex = searchIndex; - } - - public async Task> Handle( - UpdatePlexLibraryPreferences request, - CancellationToken cancellationToken) - { - var toDisable = request.Preferences.Filter(p => p.ShouldSyncItems == false).Map(p => p.Id).ToList(); - List ids = await _mediaSourceRepository.DisablePlexLibrarySync(toDisable); - await _searchIndex.RemoveItems(ids); - _searchIndex.Commit(); - - IEnumerable toEnable = request.Preferences.Filter(p => p.ShouldSyncItems).Map(p => p.Id); - await _mediaSourceRepository.EnablePlexLibrarySync(toEnable); - - return Unit.Default; - } + _mediaSourceRepository = mediaSourceRepository; + _searchIndex = searchIndex; } -} + + public async Task> Handle( + UpdatePlexLibraryPreferences request, + CancellationToken cancellationToken) + { + var toDisable = request.Preferences.Filter(p => p.ShouldSyncItems == false).Map(p => p.Id).ToList(); + List ids = await _mediaSourceRepository.DisablePlexLibrarySync(toDisable); + await _searchIndex.RemoveItems(ids); + _searchIndex.Commit(); + + IEnumerable toEnable = request.Preferences.Filter(p => p.ShouldSyncItems).Map(p => p.Id); + await _mediaSourceRepository.EnablePlexLibrarySync(toEnable); + + return Unit.Default; + } +} \ No newline at end of file diff --git a/ErsatzTV.Application/Plex/Commands/UpdatePlexPathReplacements.cs b/ErsatzTV.Application/Plex/Commands/UpdatePlexPathReplacements.cs index cb4319e02..54e5306f7 100644 --- a/ErsatzTV.Application/Plex/Commands/UpdatePlexPathReplacements.cs +++ b/ErsatzTV.Application/Plex/Commands/UpdatePlexPathReplacements.cs @@ -1,13 +1,10 @@ -using System.Collections.Generic; -using ErsatzTV.Core; -using LanguageExt; +using ErsatzTV.Core; -namespace ErsatzTV.Application.Plex.Commands -{ - public record UpdatePlexPathReplacements - ( - int PlexMediaSourceId, - List PathReplacements) : MediatR.IRequest>; +namespace ErsatzTV.Application.Plex; - public record PlexPathReplacementItem(int Id, string PlexPath, string LocalPath); -} +public record UpdatePlexPathReplacements +( + int PlexMediaSourceId, + List PathReplacements) : MediatR.IRequest>; + +public record PlexPathReplacementItem(int Id, string PlexPath, string LocalPath); \ No newline at end of file diff --git a/ErsatzTV.Application/Plex/Commands/UpdatePlexPathReplacementsHandler.cs b/ErsatzTV.Application/Plex/Commands/UpdatePlexPathReplacementsHandler.cs index 67f469523..3c7036384 100644 --- a/ErsatzTV.Application/Plex/Commands/UpdatePlexPathReplacementsHandler.cs +++ b/ErsatzTV.Application/Plex/Commands/UpdatePlexPathReplacementsHandler.cs @@ -1,51 +1,45 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -namespace ErsatzTV.Application.Plex.Commands +namespace ErsatzTV.Application.Plex; + +public class + UpdatePlexPathReplacementsHandler : MediatR.IRequestHandler> { - public class - UpdatePlexPathReplacementsHandler : MediatR.IRequestHandler> + private readonly IMediaSourceRepository _mediaSourceRepository; + + public UpdatePlexPathReplacementsHandler(IMediaSourceRepository mediaSourceRepository) => + _mediaSourceRepository = mediaSourceRepository; + + public Task> Handle( + UpdatePlexPathReplacements request, + CancellationToken cancellationToken) => + Validate(request) + .MapT(pms => MergePathReplacements(request, pms)) + .Bind(v => v.ToEitherAsync()); + + private Task MergePathReplacements(UpdatePlexPathReplacements request, PlexMediaSource plexMediaSource) { - private readonly IMediaSourceRepository _mediaSourceRepository; + plexMediaSource.PathReplacements ??= new List(); - public UpdatePlexPathReplacementsHandler(IMediaSourceRepository mediaSourceRepository) => - _mediaSourceRepository = mediaSourceRepository; + var incoming = request.PathReplacements.Map(Project).ToList(); - public Task> Handle( - UpdatePlexPathReplacements request, - CancellationToken cancellationToken) => - Validate(request) - .MapT(pms => MergePathReplacements(request, pms)) - .Bind(v => v.ToEitherAsync()); + var toAdd = incoming.Filter(r => r.Id < 1).ToList(); + var toRemove = plexMediaSource.PathReplacements.Filter(r => incoming.All(pr => pr.Id != r.Id)).ToList(); + var toUpdate = incoming.Except(toAdd).ToList(); - private Task MergePathReplacements(UpdatePlexPathReplacements request, PlexMediaSource plexMediaSource) - { - plexMediaSource.PathReplacements ??= new List(); - - var incoming = request.PathReplacements.Map(Project).ToList(); - - var toAdd = incoming.Filter(r => r.Id < 1).ToList(); - var toRemove = plexMediaSource.PathReplacements.Filter(r => incoming.All(pr => pr.Id != r.Id)).ToList(); - var toUpdate = incoming.Except(toAdd).ToList(); - - return _mediaSourceRepository.UpdatePathReplacements(plexMediaSource.Id, toAdd, toUpdate, toRemove); - } - - private static PlexPathReplacement Project(PlexPathReplacementItem vm) => - new() { Id = vm.Id, PlexPath = vm.PlexPath, LocalPath = vm.LocalPath }; - - private Task> Validate(UpdatePlexPathReplacements request) => - PlexMediaSourceMustExist(request); - - private Task> PlexMediaSourceMustExist( - UpdatePlexPathReplacements request) => - _mediaSourceRepository.GetPlex(request.PlexMediaSourceId) - .Map(v => v.ToValidation($"Plex media source {request.PlexMediaSourceId} does not exist.")); + return _mediaSourceRepository.UpdatePathReplacements(plexMediaSource.Id, toAdd, toUpdate, toRemove); } -} + + private static PlexPathReplacement Project(PlexPathReplacementItem vm) => + new() { Id = vm.Id, PlexPath = vm.PlexPath, LocalPath = vm.LocalPath }; + + private Task> Validate(UpdatePlexPathReplacements request) => + PlexMediaSourceMustExist(request); + + private Task> PlexMediaSourceMustExist( + UpdatePlexPathReplacements request) => + _mediaSourceRepository.GetPlex(request.PlexMediaSourceId) + .Map(v => v.ToValidation($"Plex media source {request.PlexMediaSourceId} does not exist.")); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Plex/Mapper.cs b/ErsatzTV.Application/Plex/Mapper.cs index c8b2a1d37..55f23ed17 100644 --- a/ErsatzTV.Application/Plex/Mapper.cs +++ b/ErsatzTV.Application/Plex/Mapper.cs @@ -1,21 +1,18 @@ -using System.Linq; -using ErsatzTV.Core.Domain; -using static LanguageExt.Prelude; +using ErsatzTV.Core.Domain; -namespace ErsatzTV.Application.Plex +namespace ErsatzTV.Application.Plex; + +internal static class Mapper { - internal static class Mapper - { - internal static PlexMediaSourceViewModel ProjectToViewModel(PlexMediaSource plexMediaSource) => - new( - plexMediaSource.Id, - plexMediaSource.ServerName, - Optional(plexMediaSource.Connections.SingleOrDefault(c => c.IsActive)).Match(c => c.Uri, string.Empty)); + internal static PlexMediaSourceViewModel ProjectToViewModel(PlexMediaSource plexMediaSource) => + new( + plexMediaSource.Id, + plexMediaSource.ServerName, + Optional(plexMediaSource.Connections.SingleOrDefault(c => c.IsActive)).Match(c => c.Uri, string.Empty)); - internal static PlexLibraryViewModel ProjectToViewModel(PlexLibrary library) => - new(library.Id, library.Name, library.MediaKind, library.ShouldSyncItems); + internal static PlexLibraryViewModel ProjectToViewModel(PlexLibrary library) => + new(library.Id, library.Name, library.MediaKind, library.ShouldSyncItems); - internal static PlexPathReplacementViewModel ProjectToViewModel(PlexPathReplacement pathReplacement) => - new(pathReplacement.Id, pathReplacement.PlexPath, pathReplacement.LocalPath); - } -} + internal static PlexPathReplacementViewModel ProjectToViewModel(PlexPathReplacement pathReplacement) => + new(pathReplacement.Id, pathReplacement.PlexPath, pathReplacement.LocalPath); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Plex/PlexConnectionParametersViewModel.cs b/ErsatzTV.Application/Plex/PlexConnectionParametersViewModel.cs index d01b17e50..eefa84c87 100644 --- a/ErsatzTV.Application/Plex/PlexConnectionParametersViewModel.cs +++ b/ErsatzTV.Application/Plex/PlexConnectionParametersViewModel.cs @@ -1,6 +1,3 @@ -using System; +namespace ErsatzTV.Application.Plex; -namespace ErsatzTV.Application.Plex -{ - public record PlexConnectionParametersViewModel(Uri Uri, string AuthToken); -} +public record PlexConnectionParametersViewModel(Uri Uri, string AuthToken); \ No newline at end of file diff --git a/ErsatzTV.Application/Plex/PlexLibraryViewModel.cs b/ErsatzTV.Application/Plex/PlexLibraryViewModel.cs index d0dae98ee..f09ea9bb6 100644 --- a/ErsatzTV.Application/Plex/PlexLibraryViewModel.cs +++ b/ErsatzTV.Application/Plex/PlexLibraryViewModel.cs @@ -1,6 +1,5 @@ using ErsatzTV.Core.Domain; -namespace ErsatzTV.Application.Plex -{ - public record PlexLibraryViewModel(int Id, string Name, LibraryMediaKind MediaKind, bool ShouldSyncItems); -} +namespace ErsatzTV.Application.Plex; + +public record PlexLibraryViewModel(int Id, string Name, LibraryMediaKind MediaKind, bool ShouldSyncItems); \ No newline at end of file diff --git a/ErsatzTV.Application/Plex/PlexMediaSourceViewModel.cs b/ErsatzTV.Application/Plex/PlexMediaSourceViewModel.cs index a5567d43b..df86da4dd 100644 --- a/ErsatzTV.Application/Plex/PlexMediaSourceViewModel.cs +++ b/ErsatzTV.Application/Plex/PlexMediaSourceViewModel.cs @@ -1,6 +1,5 @@ using ErsatzTV.Application.MediaSources; -namespace ErsatzTV.Application.Plex -{ - public record PlexMediaSourceViewModel(int Id, string Name, string Address) : MediaSourceViewModel(Id, Name); -} +namespace ErsatzTV.Application.Plex; + +public record PlexMediaSourceViewModel(int Id, string Name, string Address) : MediaSourceViewModel(Id, Name); \ No newline at end of file diff --git a/ErsatzTV.Application/Plex/PlexPathReplacementViewModel.cs b/ErsatzTV.Application/Plex/PlexPathReplacementViewModel.cs index a8809250c..266d60c2a 100644 --- a/ErsatzTV.Application/Plex/PlexPathReplacementViewModel.cs +++ b/ErsatzTV.Application/Plex/PlexPathReplacementViewModel.cs @@ -1,4 +1,3 @@ -namespace ErsatzTV.Application.Plex -{ - public record PlexPathReplacementViewModel(int Id, string PlexPath, string LocalPath); -} +namespace ErsatzTV.Application.Plex; + +public record PlexPathReplacementViewModel(int Id, string PlexPath, string LocalPath); \ No newline at end of file diff --git a/ErsatzTV.Application/Plex/Queries/GetAllPlexMediaSources.cs b/ErsatzTV.Application/Plex/Queries/GetAllPlexMediaSources.cs index c99fa95fc..81d0bd7c7 100644 --- a/ErsatzTV.Application/Plex/Queries/GetAllPlexMediaSources.cs +++ b/ErsatzTV.Application/Plex/Queries/GetAllPlexMediaSources.cs @@ -1,7 +1,3 @@ -using System.Collections.Generic; -using MediatR; +namespace ErsatzTV.Application.Plex; -namespace ErsatzTV.Application.Plex.Queries -{ - public record GetAllPlexMediaSources : IRequest>; -} +public record GetAllPlexMediaSources : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Plex/Queries/GetAllPlexMediaSourcesHandler.cs b/ErsatzTV.Application/Plex/Queries/GetAllPlexMediaSourcesHandler.cs index 22761238f..7bc40d26b 100644 --- a/ErsatzTV.Application/Plex/Queries/GetAllPlexMediaSourcesHandler.cs +++ b/ErsatzTV.Application/Plex/Queries/GetAllPlexMediaSourcesHandler.cs @@ -1,24 +1,17 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -using MediatR; +using ErsatzTV.Core.Interfaces.Repositories; using static ErsatzTV.Application.Plex.Mapper; -namespace ErsatzTV.Application.Plex.Queries +namespace ErsatzTV.Application.Plex; + +public class GetAllPlexMediaSourcesHandler : IRequestHandler> { - public class GetAllPlexMediaSourcesHandler : IRequestHandler> - { - private readonly IMediaSourceRepository _mediaSourceRepository; + private readonly IMediaSourceRepository _mediaSourceRepository; - public GetAllPlexMediaSourcesHandler(IMediaSourceRepository mediaSourceRepository) => - _mediaSourceRepository = mediaSourceRepository; + public GetAllPlexMediaSourcesHandler(IMediaSourceRepository mediaSourceRepository) => + _mediaSourceRepository = mediaSourceRepository; - public Task> Handle( - GetAllPlexMediaSources request, - CancellationToken cancellationToken) => - _mediaSourceRepository.GetAllPlex().Map(list => list.Map(ProjectToViewModel).ToList()); - } -} + public Task> Handle( + GetAllPlexMediaSources request, + CancellationToken cancellationToken) => + _mediaSourceRepository.GetAllPlex().Map(list => list.Map(ProjectToViewModel).ToList()); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Plex/Queries/GetPlexConnectionParameters.cs b/ErsatzTV.Application/Plex/Queries/GetPlexConnectionParameters.cs index 3607ee8db..23cb87aa4 100644 --- a/ErsatzTV.Application/Plex/Queries/GetPlexConnectionParameters.cs +++ b/ErsatzTV.Application/Plex/Queries/GetPlexConnectionParameters.cs @@ -1,9 +1,6 @@ using ErsatzTV.Core; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.Plex.Queries -{ - public record GetPlexConnectionParameters - (int PlexMediaSourceId) : IRequest>; -} +namespace ErsatzTV.Application.Plex; + +public record GetPlexConnectionParameters + (int PlexMediaSourceId) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Plex/Queries/GetPlexConnectionParametersHandler.cs b/ErsatzTV.Application/Plex/Queries/GetPlexConnectionParametersHandler.cs index 0ea0e867a..53f26f948 100644 --- a/ErsatzTV.Application/Plex/Queries/GetPlexConnectionParametersHandler.cs +++ b/ErsatzTV.Application/Plex/Queries/GetPlexConnectionParametersHandler.cs @@ -1,94 +1,87 @@ -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Plex; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Plex; -using LanguageExt; -using MediatR; using Microsoft.Extensions.Caching.Memory; -namespace ErsatzTV.Application.Plex.Queries +namespace ErsatzTV.Application.Plex; + +public class GetPlexConnectionParametersHandler : IRequestHandler> { - public class GetPlexConnectionParametersHandler : IRequestHandler> + private readonly IMediaSourceRepository _mediaSourceRepository; + private readonly IMemoryCache _memoryCache; + private readonly IPlexSecretStore _plexSecretStore; + + public GetPlexConnectionParametersHandler( + IMemoryCache memoryCache, + IMediaSourceRepository mediaSourceRepository, + IPlexSecretStore plexSecretStore) { - private readonly IMediaSourceRepository _mediaSourceRepository; - private readonly IMemoryCache _memoryCache; - private readonly IPlexSecretStore _plexSecretStore; - - public GetPlexConnectionParametersHandler( - IMemoryCache memoryCache, - IMediaSourceRepository mediaSourceRepository, - IPlexSecretStore plexSecretStore) - { - _memoryCache = memoryCache; - _mediaSourceRepository = mediaSourceRepository; - _plexSecretStore = plexSecretStore; - } - - public async Task> Handle( - GetPlexConnectionParameters request, - CancellationToken cancellationToken) - { - if (_memoryCache.TryGetValue(request, out PlexConnectionParametersViewModel parameters)) - { - return parameters; - } - - Either maybeParameters = - await Validate(request) - .MapT( - cp => new PlexConnectionParametersViewModel( - new Uri(cp.ActiveConnection.Uri), - cp.PlexServerAuthToken.AuthToken)) - .Map(v => v.ToEither()); - - return maybeParameters.Match( - p => - { - _memoryCache.Set(request, p, TimeSpan.FromHours(1)); - return maybeParameters; - }, - error => error); - } - - private Task> Validate(GetPlexConnectionParameters request) => - PlexMediaSourceMustExist(request) - .BindT(MediaSourceMustHaveActiveConnection) - .BindT(MediaSourceMustHaveToken); - - private Task> PlexMediaSourceMustExist( - GetPlexConnectionParameters request) => - _mediaSourceRepository.GetPlex(request.PlexMediaSourceId) - .Map( - v => v.ToValidation( - $"Plex media source {request.PlexMediaSourceId} does not exist.")); - - private Validation MediaSourceMustHaveActiveConnection( - PlexMediaSource plexMediaSource) - { - Option maybeConnection = - plexMediaSource.Connections.SingleOrDefault(c => c.IsActive); - return maybeConnection.Map(connection => new ConnectionParameters(plexMediaSource, connection)) - .ToValidation("Plex media source requires an active connection"); - } - - private async Task> MediaSourceMustHaveToken( - ConnectionParameters connectionParameters) - { - Option maybeToken = await - _plexSecretStore.GetServerAuthToken(connectionParameters.PlexMediaSource.ClientIdentifier); - return maybeToken.Map(token => connectionParameters with { PlexServerAuthToken = token }) - .ToValidation("Plex media source requires a token"); - } - - private record ConnectionParameters(PlexMediaSource PlexMediaSource, PlexConnection ActiveConnection) - { - public PlexServerAuthToken PlexServerAuthToken { get; set; } - } + _memoryCache = memoryCache; + _mediaSourceRepository = mediaSourceRepository; + _plexSecretStore = plexSecretStore; } -} + + public async Task> Handle( + GetPlexConnectionParameters request, + CancellationToken cancellationToken) + { + if (_memoryCache.TryGetValue(request, out PlexConnectionParametersViewModel parameters)) + { + return parameters; + } + + Either maybeParameters = + await Validate(request) + .MapT( + cp => new PlexConnectionParametersViewModel( + new Uri(cp.ActiveConnection.Uri), + cp.PlexServerAuthToken.AuthToken)) + .Map(v => v.ToEither()); + + return maybeParameters.Match( + p => + { + _memoryCache.Set(request, p, TimeSpan.FromHours(1)); + return maybeParameters; + }, + error => error); + } + + private Task> Validate(GetPlexConnectionParameters request) => + PlexMediaSourceMustExist(request) + .BindT(MediaSourceMustHaveActiveConnection) + .BindT(MediaSourceMustHaveToken); + + private Task> PlexMediaSourceMustExist( + GetPlexConnectionParameters request) => + _mediaSourceRepository.GetPlex(request.PlexMediaSourceId) + .Map( + v => v.ToValidation( + $"Plex media source {request.PlexMediaSourceId} does not exist.")); + + private Validation MediaSourceMustHaveActiveConnection( + PlexMediaSource plexMediaSource) + { + Option maybeConnection = + plexMediaSource.Connections.SingleOrDefault(c => c.IsActive); + return maybeConnection.Map(connection => new ConnectionParameters(plexMediaSource, connection)) + .ToValidation("Plex media source requires an active connection"); + } + + private async Task> MediaSourceMustHaveToken( + ConnectionParameters connectionParameters) + { + Option maybeToken = await + _plexSecretStore.GetServerAuthToken(connectionParameters.PlexMediaSource.ClientIdentifier); + return maybeToken.Map(token => connectionParameters with { PlexServerAuthToken = token }) + .ToValidation("Plex media source requires a token"); + } + + private record ConnectionParameters(PlexMediaSource PlexMediaSource, PlexConnection ActiveConnection) + { + public PlexServerAuthToken PlexServerAuthToken { get; set; } + } +} \ No newline at end of file diff --git a/ErsatzTV.Application/Plex/Queries/GetPlexLibrariesBySourceId.cs b/ErsatzTV.Application/Plex/Queries/GetPlexLibrariesBySourceId.cs index 11da4bc88..150cdc294 100644 --- a/ErsatzTV.Application/Plex/Queries/GetPlexLibrariesBySourceId.cs +++ b/ErsatzTV.Application/Plex/Queries/GetPlexLibrariesBySourceId.cs @@ -1,7 +1,3 @@ -using System.Collections.Generic; -using MediatR; +namespace ErsatzTV.Application.Plex; -namespace ErsatzTV.Application.Plex.Queries -{ - public record GetPlexLibrariesBySourceId(int PlexMediaSourceId) : IRequest>; -} +public record GetPlexLibrariesBySourceId(int PlexMediaSourceId) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Plex/Queries/GetPlexLibrariesBySourceIdHandler.cs b/ErsatzTV.Application/Plex/Queries/GetPlexLibrariesBySourceIdHandler.cs index e73dd612f..9bfc86ea7 100644 --- a/ErsatzTV.Application/Plex/Queries/GetPlexLibrariesBySourceIdHandler.cs +++ b/ErsatzTV.Application/Plex/Queries/GetPlexLibrariesBySourceIdHandler.cs @@ -1,27 +1,20 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -using MediatR; +using ErsatzTV.Core.Interfaces.Repositories; using static ErsatzTV.Application.Plex.Mapper; -namespace ErsatzTV.Application.Plex.Queries +namespace ErsatzTV.Application.Plex; + +public class + GetPlexLibrariesBySourceIdHandler : IRequestHandler> { - public class - GetPlexLibrariesBySourceIdHandler : IRequestHandler> - { - private readonly IMediaSourceRepository _mediaSourceRepository; + private readonly IMediaSourceRepository _mediaSourceRepository; - public GetPlexLibrariesBySourceIdHandler(IMediaSourceRepository mediaSourceRepository) => - _mediaSourceRepository = mediaSourceRepository; + public GetPlexLibrariesBySourceIdHandler(IMediaSourceRepository mediaSourceRepository) => + _mediaSourceRepository = mediaSourceRepository; - public Task> Handle( - GetPlexLibrariesBySourceId request, - CancellationToken cancellationToken) => - _mediaSourceRepository.GetPlexLibraries(request.PlexMediaSourceId) - .Map(list => list.Map(ProjectToViewModel).ToList()); - } -} + public Task> Handle( + GetPlexLibrariesBySourceId request, + CancellationToken cancellationToken) => + _mediaSourceRepository.GetPlexLibraries(request.PlexMediaSourceId) + .Map(list => list.Map(ProjectToViewModel).ToList()); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Plex/Queries/GetPlexMediaSourceById.cs b/ErsatzTV.Application/Plex/Queries/GetPlexMediaSourceById.cs index 5fdc05044..8e89fc273 100644 --- a/ErsatzTV.Application/Plex/Queries/GetPlexMediaSourceById.cs +++ b/ErsatzTV.Application/Plex/Queries/GetPlexMediaSourceById.cs @@ -1,7 +1,3 @@ -using LanguageExt; -using MediatR; +namespace ErsatzTV.Application.Plex; -namespace ErsatzTV.Application.Plex.Queries -{ - public record GetPlexMediaSourceById(int PlexMediaSourceId) : IRequest>; -} +public record GetPlexMediaSourceById(int PlexMediaSourceId) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Plex/Queries/GetPlexMediaSourceByIdHandler.cs b/ErsatzTV.Application/Plex/Queries/GetPlexMediaSourceByIdHandler.cs index 8bd9f9f19..46337c903 100644 --- a/ErsatzTV.Application/Plex/Queries/GetPlexMediaSourceByIdHandler.cs +++ b/ErsatzTV.Application/Plex/Queries/GetPlexMediaSourceByIdHandler.cs @@ -1,23 +1,18 @@ -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -using MediatR; +using ErsatzTV.Core.Interfaces.Repositories; using static ErsatzTV.Application.Plex.Mapper; -namespace ErsatzTV.Application.Plex.Queries +namespace ErsatzTV.Application.Plex; + +public class + GetPlexMediaSourceByIdHandler : IRequestHandler> { - public class - GetPlexMediaSourceByIdHandler : IRequestHandler> - { - private readonly IMediaSourceRepository _mediaSourceRepository; + private readonly IMediaSourceRepository _mediaSourceRepository; - public GetPlexMediaSourceByIdHandler(IMediaSourceRepository mediaSourceRepository) => - _mediaSourceRepository = mediaSourceRepository; + public GetPlexMediaSourceByIdHandler(IMediaSourceRepository mediaSourceRepository) => + _mediaSourceRepository = mediaSourceRepository; - public Task> Handle( - GetPlexMediaSourceById request, - CancellationToken cancellationToken) => - _mediaSourceRepository.GetPlex(request.PlexMediaSourceId).MapT(ProjectToViewModel); - } -} + public Task> Handle( + GetPlexMediaSourceById request, + CancellationToken cancellationToken) => + _mediaSourceRepository.GetPlex(request.PlexMediaSourceId).MapT(ProjectToViewModel); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Plex/Queries/GetPlexPathReplacementsBySourceId.cs b/ErsatzTV.Application/Plex/Queries/GetPlexPathReplacementsBySourceId.cs index 5246d3d08..b153ef3e7 100644 --- a/ErsatzTV.Application/Plex/Queries/GetPlexPathReplacementsBySourceId.cs +++ b/ErsatzTV.Application/Plex/Queries/GetPlexPathReplacementsBySourceId.cs @@ -1,8 +1,4 @@ -using System.Collections.Generic; -using MediatR; +namespace ErsatzTV.Application.Plex; -namespace ErsatzTV.Application.Plex.Queries -{ - public record GetPlexPathReplacementsBySourceId - (int PlexMediaSourceId) : IRequest>; -} +public record GetPlexPathReplacementsBySourceId + (int PlexMediaSourceId) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Plex/Queries/GetPlexPathReplacementsBySourceIdHandler.cs b/ErsatzTV.Application/Plex/Queries/GetPlexPathReplacementsBySourceIdHandler.cs index d5aa31cd3..45913a458 100644 --- a/ErsatzTV.Application/Plex/Queries/GetPlexPathReplacementsBySourceIdHandler.cs +++ b/ErsatzTV.Application/Plex/Queries/GetPlexPathReplacementsBySourceIdHandler.cs @@ -1,26 +1,19 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -using MediatR; +using ErsatzTV.Core.Interfaces.Repositories; using static ErsatzTV.Application.Plex.Mapper; -namespace ErsatzTV.Application.Plex.Queries +namespace ErsatzTV.Application.Plex; + +public class GetPlexPathReplacementsBySourceIdHandler : IRequestHandler> { - public class GetPlexPathReplacementsBySourceIdHandler : IRequestHandler> - { - private readonly IMediaSourceRepository _mediaSourceRepository; + private readonly IMediaSourceRepository _mediaSourceRepository; - public GetPlexPathReplacementsBySourceIdHandler(IMediaSourceRepository mediaSourceRepository) => - _mediaSourceRepository = mediaSourceRepository; + public GetPlexPathReplacementsBySourceIdHandler(IMediaSourceRepository mediaSourceRepository) => + _mediaSourceRepository = mediaSourceRepository; - public Task> Handle( - GetPlexPathReplacementsBySourceId request, - CancellationToken cancellationToken) => - _mediaSourceRepository.GetPlexPathReplacements(request.PlexMediaSourceId) - .Map(list => list.Map(ProjectToViewModel).ToList()); - } -} + public Task> Handle( + GetPlexPathReplacementsBySourceId request, + CancellationToken cancellationToken) => + _mediaSourceRepository.GetPlexPathReplacements(request.PlexMediaSourceId) + .Map(list => list.Map(ProjectToViewModel).ToList()); +} \ No newline at end of file diff --git a/ErsatzTV.Application/ProgramSchedules/Commands/AddProgramScheduleItem.cs b/ErsatzTV.Application/ProgramSchedules/Commands/AddProgramScheduleItem.cs index cfbb0cc67..ecd869fb4 100644 --- a/ErsatzTV.Application/ProgramSchedules/Commands/AddProgramScheduleItem.cs +++ b/ErsatzTV.Application/ProgramSchedules/Commands/AddProgramScheduleItem.cs @@ -1,30 +1,26 @@ -using System; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.ProgramSchedules.Commands -{ - public record AddProgramScheduleItem( - int ProgramScheduleId, - StartType StartType, - TimeSpan? StartTime, - PlayoutMode PlayoutMode, - ProgramScheduleItemCollectionType CollectionType, - int? CollectionId, - int? MultiCollectionId, - int? SmartCollectionId, - int? MediaItemId, - PlaybackOrder PlaybackOrder, - int? MultipleCount, - TimeSpan? PlayoutDuration, - TailMode TailMode, - string CustomTitle, - GuideMode GuideMode, - int? PreRollFillerId, - int? MidRollFillerId, - int? PostRollFillerId, - int? TailFillerId, - int? FallbackFillerId) : IRequest>, IProgramScheduleItemRequest; -} +namespace ErsatzTV.Application.ProgramSchedules; + +public record AddProgramScheduleItem( + int ProgramScheduleId, + StartType StartType, + TimeSpan? StartTime, + PlayoutMode PlayoutMode, + ProgramScheduleItemCollectionType CollectionType, + int? CollectionId, + int? MultiCollectionId, + int? SmartCollectionId, + int? MediaItemId, + PlaybackOrder PlaybackOrder, + int? MultipleCount, + TimeSpan? PlayoutDuration, + TailMode TailMode, + string CustomTitle, + GuideMode GuideMode, + int? PreRollFillerId, + int? MidRollFillerId, + int? PostRollFillerId, + int? TailFillerId, + int? FallbackFillerId) : IRequest>, IProgramScheduleItemRequest; \ No newline at end of file diff --git a/ErsatzTV.Application/ProgramSchedules/Commands/AddProgramScheduleItemHandler.cs b/ErsatzTV.Application/ProgramSchedules/Commands/AddProgramScheduleItemHandler.cs index 7a9866089..d495c23a1 100644 --- a/ErsatzTV.Application/ProgramSchedules/Commands/AddProgramScheduleItemHandler.cs +++ b/ErsatzTV.Application/ProgramSchedules/Commands/AddProgramScheduleItemHandler.cs @@ -1,66 +1,60 @@ -using System.Linq; -using System.Threading; -using System.Threading.Channels; -using System.Threading.Tasks; -using ErsatzTV.Application.Playouts.Commands; +using System.Threading.Channels; +using ErsatzTV.Application.Playouts; using ErsatzTV.Core; 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.Commands +namespace ErsatzTV.Application.ProgramSchedules; + +public class AddProgramScheduleItemHandler : ProgramScheduleItemCommandBase, + IRequestHandler> { - public class AddProgramScheduleItemHandler : ProgramScheduleItemCommandBase, - IRequestHandler> + private readonly ChannelWriter _channel; + private readonly IDbContextFactory _dbContextFactory; + + public AddProgramScheduleItemHandler( + IDbContextFactory dbContextFactory, + ChannelWriter channel) { - private readonly ChannelWriter _channel; - private readonly IDbContextFactory _dbContextFactory; - - public AddProgramScheduleItemHandler( - IDbContextFactory dbContextFactory, - ChannelWriter channel) - { - _dbContextFactory = dbContextFactory; - _channel = channel; - } - - public async Task> Handle( - AddProgramScheduleItem request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - Validation validation = await Validate(dbContext, request); - return await validation.Apply(ps => PersistItem(dbContext, request, ps)); - } - - private async Task PersistItem( - TvContext dbContext, - AddProgramScheduleItem request, - ProgramSchedule programSchedule) - { - int nextIndex = programSchedule.Items.Select(i => i.Index).DefaultIfEmpty(0).Max() + 1; - - ProgramScheduleItem item = BuildItem(programSchedule, nextIndex, request); - programSchedule.Items.Add(item); - - await dbContext.SaveChangesAsync(); - - // rebuild any playouts that use this schedule - foreach (Playout playout in programSchedule.Playouts) - { - await _channel.WriteAsync(new BuildPlayout(playout.Id, true)); - } - - return ProjectToViewModel(item); - } - - private static Task> Validate( - TvContext dbContext, - AddProgramScheduleItem request) => - ProgramScheduleMustExist(dbContext, request.ProgramScheduleId) - .BindT(programSchedule => PlayoutModeMustBeValid(request, programSchedule)); + _dbContextFactory = dbContextFactory; + _channel = channel; } -} + + public async Task> Handle( + AddProgramScheduleItem request, + CancellationToken cancellationToken) + { + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + Validation validation = await Validate(dbContext, request); + return await LanguageExtensions.Apply(validation, ps => PersistItem(dbContext, request, ps)); + } + + private async Task PersistItem( + TvContext dbContext, + AddProgramScheduleItem request, + ProgramSchedule programSchedule) + { + int nextIndex = programSchedule.Items.Select(i => i.Index).DefaultIfEmpty(0).Max() + 1; + + ProgramScheduleItem item = BuildItem(programSchedule, nextIndex, request); + programSchedule.Items.Add(item); + + await dbContext.SaveChangesAsync(); + + // rebuild any playouts that use this schedule + foreach (Playout playout in programSchedule.Playouts) + { + await _channel.WriteAsync(new BuildPlayout(playout.Id, true)); + } + + return ProjectToViewModel(item); + } + + private static Task> Validate( + TvContext dbContext, + AddProgramScheduleItem request) => + ProgramScheduleMustExist(dbContext, request.ProgramScheduleId) + .BindT(programSchedule => PlayoutModeMustBeValid(request, programSchedule)); +} \ No newline at end of file diff --git a/ErsatzTV.Application/ProgramSchedules/Commands/CreateProgramSchedule.cs b/ErsatzTV.Application/ProgramSchedules/Commands/CreateProgramSchedule.cs index a659b9c2c..ff210024f 100644 --- a/ErsatzTV.Application/ProgramSchedules/Commands/CreateProgramSchedule.cs +++ b/ErsatzTV.Application/ProgramSchedules/Commands/CreateProgramSchedule.cs @@ -1,13 +1,9 @@ using ErsatzTV.Core; -using ErsatzTV.Core.Domain; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.ProgramSchedules.Commands -{ - public record CreateProgramSchedule( - string Name, - bool KeepMultiPartEpisodesTogether, - bool TreatCollectionsAsShows, - bool ShuffleScheduleItems) : IRequest>; -} +namespace ErsatzTV.Application.ProgramSchedules; + +public record CreateProgramSchedule( + string Name, + bool KeepMultiPartEpisodesTogether, + bool TreatCollectionsAsShows, + bool ShuffleScheduleItems) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/ProgramSchedules/Commands/CreateProgramScheduleHandler.cs b/ErsatzTV.Application/ProgramSchedules/Commands/CreateProgramScheduleHandler.cs index ffbd5d82b..03ec417d2 100644 --- a/ErsatzTV.Application/ProgramSchedules/Commands/CreateProgramScheduleHandler.cs +++ b/ErsatzTV.Application/ProgramSchedules/Commands/CreateProgramScheduleHandler.cs @@ -1,75 +1,68 @@ -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Infrastructure.Data; -using LanguageExt; -using MediatR; using Microsoft.EntityFrameworkCore; -using static LanguageExt.Prelude; -using ValidationT_AsyncSync_Extensions = LanguageExt.ValidationT_AsyncSync_Extensions; -namespace ErsatzTV.Application.ProgramSchedules.Commands +namespace ErsatzTV.Application.ProgramSchedules; + +public class CreateProgramScheduleHandler : + IRequestHandler> { - public class CreateProgramScheduleHandler : - IRequestHandler> + private readonly IDbContextFactory _dbContextFactory; + + public CreateProgramScheduleHandler(IDbContextFactory dbContextFactory) => + _dbContextFactory = dbContextFactory; + + public async Task> Handle( + CreateProgramSchedule request, + CancellationToken cancellationToken) { - private readonly IDbContextFactory _dbContextFactory; + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - public CreateProgramScheduleHandler(IDbContextFactory dbContextFactory) => - _dbContextFactory = dbContextFactory; - - public async Task> Handle( - CreateProgramSchedule request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - - Validation validation = await Validate(dbContext, request); - return await validation.Apply(ps => PersistProgramSchedule(dbContext, ps)); - } - - private static async Task PersistProgramSchedule( - TvContext dbContext, - ProgramSchedule programSchedule) - { - await dbContext.ProgramSchedules.AddAsync(programSchedule); - await dbContext.SaveChangesAsync(); - return new CreateProgramScheduleResult(programSchedule.Id); - } - - private static Task> Validate( - TvContext dbContext, - CreateProgramSchedule request) => - ValidationT_AsyncSync_Extensions.MapT( - ValidateName(dbContext, request), - name => - { - bool keepMultiPartEpisodesTogether = request.KeepMultiPartEpisodesTogether; - return new ProgramSchedule - { - Name = name, - KeepMultiPartEpisodesTogether = keepMultiPartEpisodesTogether, - TreatCollectionsAsShows = keepMultiPartEpisodesTogether && request.TreatCollectionsAsShows, - ShuffleScheduleItems = request.ShuffleScheduleItems - }; - }); - - private static async Task> ValidateName( - TvContext dbContext, - CreateProgramSchedule createProgramSchedule) - { - Validation result1 = createProgramSchedule.NotEmpty(c => c.Name) - .Bind(_ => createProgramSchedule.NotLongerThan(50)(c => c.Name)); - - int duplicateNameCount = await dbContext.ProgramSchedules - .CountAsync(ps => ps.Name == createProgramSchedule.Name); - - var result2 = Optional(duplicateNameCount) - .Where(count => count == 0) - .ToValidation("Schedule name must be unique"); - - return (result1, result2).Apply((_, _) => createProgramSchedule.Name); - } + Validation validation = await Validate(dbContext, request); + return await LanguageExtensions.Apply(validation, ps => PersistProgramSchedule(dbContext, ps)); } -} + + private static async Task PersistProgramSchedule( + TvContext dbContext, + ProgramSchedule programSchedule) + { + await dbContext.ProgramSchedules.AddAsync(programSchedule); + await dbContext.SaveChangesAsync(); + return new CreateProgramScheduleResult(programSchedule.Id); + } + + private static Task> Validate( + TvContext dbContext, + CreateProgramSchedule request) => + ValidationT_AsyncSync_Extensions.MapT( + ValidateName(dbContext, request), + name => + { + bool keepMultiPartEpisodesTogether = request.KeepMultiPartEpisodesTogether; + return new ProgramSchedule + { + Name = name, + KeepMultiPartEpisodesTogether = keepMultiPartEpisodesTogether, + TreatCollectionsAsShows = keepMultiPartEpisodesTogether && request.TreatCollectionsAsShows, + ShuffleScheduleItems = request.ShuffleScheduleItems + }; + }); + + private static async Task> ValidateName( + TvContext dbContext, + CreateProgramSchedule createProgramSchedule) + { + Validation result1 = createProgramSchedule.NotEmpty(c => c.Name) + .Bind(_ => createProgramSchedule.NotLongerThan(50)(c => c.Name)); + + int duplicateNameCount = await dbContext.ProgramSchedules + .CountAsync(ps => ps.Name == createProgramSchedule.Name); + + var result2 = Optional(duplicateNameCount) + .Where(count => count == 0) + .ToValidation("Schedule name must be unique"); + + return (result1, result2).Apply((_, _) => createProgramSchedule.Name); + } +} \ No newline at end of file diff --git a/ErsatzTV.Application/ProgramSchedules/Commands/CreateProgramScheduleResult.cs b/ErsatzTV.Application/ProgramSchedules/Commands/CreateProgramScheduleResult.cs index bb6b51cdf..f22b989f9 100644 --- a/ErsatzTV.Application/ProgramSchedules/Commands/CreateProgramScheduleResult.cs +++ b/ErsatzTV.Application/ProgramSchedules/Commands/CreateProgramScheduleResult.cs @@ -1,4 +1,3 @@ -namespace ErsatzTV.Application.ProgramSchedules.Commands -{ - public record CreateProgramScheduleResult(int ProgramScheduleId) : EntityIdResult(ProgramScheduleId); -} +namespace ErsatzTV.Application.ProgramSchedules; + +public record CreateProgramScheduleResult(int ProgramScheduleId) : EntityIdResult(ProgramScheduleId); \ No newline at end of file diff --git a/ErsatzTV.Application/ProgramSchedules/Commands/DeleteProgramSchedule.cs b/ErsatzTV.Application/ProgramSchedules/Commands/DeleteProgramSchedule.cs index b7f40e3b3..3e6ce0b57 100644 --- a/ErsatzTV.Application/ProgramSchedules/Commands/DeleteProgramSchedule.cs +++ b/ErsatzTV.Application/ProgramSchedules/Commands/DeleteProgramSchedule.cs @@ -1,8 +1,5 @@ using ErsatzTV.Core; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.ProgramSchedules.Commands -{ - public record DeleteProgramSchedule(int ProgramScheduleId) : IRequest>; -} +namespace ErsatzTV.Application.ProgramSchedules; + +public record DeleteProgramSchedule(int ProgramScheduleId) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/ProgramSchedules/Commands/DeleteProgramScheduleHandler.cs b/ErsatzTV.Application/ProgramSchedules/Commands/DeleteProgramScheduleHandler.cs index 3a69ce2e4..46631a508 100644 --- a/ErsatzTV.Application/ProgramSchedules/Commands/DeleteProgramScheduleHandler.cs +++ b/ErsatzTV.Application/ProgramSchedules/Commands/DeleteProgramScheduleHandler.cs @@ -1,43 +1,37 @@ -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; 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 +namespace ErsatzTV.Application.ProgramSchedules; + +public class DeleteProgramScheduleHandler : IRequestHandler> { - public class DeleteProgramScheduleHandler : IRequestHandler> + private readonly IDbContextFactory _dbContextFactory; + + public DeleteProgramScheduleHandler(IDbContextFactory dbContextFactory) => + _dbContextFactory = dbContextFactory; + + public async Task> Handle( + DeleteProgramSchedule request, + CancellationToken cancellationToken) { - private readonly IDbContextFactory _dbContextFactory; - - public DeleteProgramScheduleHandler(IDbContextFactory dbContextFactory) => - _dbContextFactory = dbContextFactory; - - public async Task> Handle( - DeleteProgramSchedule request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - Validation validation = await ProgramScheduleMustExist(dbContext, request); - return await validation.Apply(ps => DoDeletion(dbContext, ps)); - } - - private static Task DoDeletion(TvContext dbContext, ProgramSchedule programSchedule) - { - dbContext.ProgramSchedules.Remove(programSchedule); - return dbContext.SaveChangesAsync().ToUnit(); - } - - private Task> ProgramScheduleMustExist( - TvContext dbContext, - DeleteProgramSchedule request) => - dbContext.ProgramSchedules - .SelectOneAsync(ps => ps.Id, ps => ps.Id == request.ProgramScheduleId) - .Map(o => o.ToValidation($"ProgramSchedule {request.ProgramScheduleId} does not exist.")); + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + Validation validation = await ProgramScheduleMustExist(dbContext, request); + return await LanguageExtensions.Apply(validation, ps => DoDeletion(dbContext, ps)); } -} + + private static Task DoDeletion(TvContext dbContext, ProgramSchedule programSchedule) + { + dbContext.ProgramSchedules.Remove(programSchedule); + return dbContext.SaveChangesAsync().ToUnit(); + } + + private Task> ProgramScheduleMustExist( + TvContext dbContext, + DeleteProgramSchedule request) => + dbContext.ProgramSchedules + .SelectOneAsync(ps => ps.Id, ps => ps.Id == request.ProgramScheduleId) + .Map(o => o.ToValidation($"ProgramSchedule {request.ProgramScheduleId} does not exist.")); +} \ No newline at end of file diff --git a/ErsatzTV.Application/ProgramSchedules/Commands/IProgramScheduleItemRequest.cs b/ErsatzTV.Application/ProgramSchedules/Commands/IProgramScheduleItemRequest.cs index e8c9c6bed..354f32a4f 100644 --- a/ErsatzTV.Application/ProgramSchedules/Commands/IProgramScheduleItemRequest.cs +++ b/ErsatzTV.Application/ProgramSchedules/Commands/IProgramScheduleItemRequest.cs @@ -1,27 +1,25 @@ -using System; -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; -namespace ErsatzTV.Application.ProgramSchedules.Commands +namespace ErsatzTV.Application.ProgramSchedules; + +public interface IProgramScheduleItemRequest { - public interface IProgramScheduleItemRequest - { - TimeSpan? StartTime { get; } - ProgramScheduleItemCollectionType CollectionType { get; } - int? CollectionId { get; } - int? MultiCollectionId { get; } - int? SmartCollectionId { get; } - int? MediaItemId { get; } - PlayoutMode PlayoutMode { get; } - PlaybackOrder PlaybackOrder { get; } - int? MultipleCount { get; } - TimeSpan? PlayoutDuration { get; } - TailMode TailMode { get; } - string CustomTitle { get; } - GuideMode GuideMode { get; } - int? PreRollFillerId { get; } - int? MidRollFillerId { get; } - int? PostRollFillerId { get; } - int? TailFillerId { get; } - int? FallbackFillerId { get; } - } -} + TimeSpan? StartTime { get; } + ProgramScheduleItemCollectionType CollectionType { get; } + int? CollectionId { get; } + int? MultiCollectionId { get; } + int? SmartCollectionId { get; } + int? MediaItemId { get; } + PlayoutMode PlayoutMode { get; } + PlaybackOrder PlaybackOrder { get; } + int? MultipleCount { get; } + TimeSpan? PlayoutDuration { get; } + TailMode TailMode { get; } + string CustomTitle { get; } + GuideMode GuideMode { get; } + int? PreRollFillerId { get; } + int? MidRollFillerId { get; } + int? PostRollFillerId { get; } + int? TailFillerId { get; } + int? FallbackFillerId { get; } +} \ No newline at end of file diff --git a/ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs b/ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs index 808b7cfbf..ba075ec77 100644 --- a/ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs +++ b/ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs @@ -1,258 +1,251 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Filler; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; -using LanguageExt; using Microsoft.EntityFrameworkCore; -using static LanguageExt.Prelude; -namespace ErsatzTV.Application.ProgramSchedules.Commands +namespace ErsatzTV.Application.ProgramSchedules; + +public abstract class ProgramScheduleItemCommandBase { - public abstract class ProgramScheduleItemCommandBase + protected static Task> 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("[ProgramScheduleId] does not exist.")); + + protected static async Task> FillerConfigurationMustBeValid( + TvContext dbContext, + IProgramScheduleItemRequest item, + ProgramSchedule programSchedule) { - protected static Task> 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("[ProgramScheduleId] does not exist.")); + var allFillerIds = Optional(item.PreRollFillerId) + .Append(Optional(item.MidRollFillerId)) + .Append(Optional(item.PostRollFillerId)) + .ToList(); - protected static async Task> FillerConfigurationMustBeValid( - TvContext dbContext, - IProgramScheduleItemRequest item, - ProgramSchedule programSchedule) - { - var allFillerIds = Optional(item.PreRollFillerId) - .Append(Optional(item.MidRollFillerId)) - .Append(Optional(item.PostRollFillerId)) - .ToList(); - - List allFiller = await dbContext.FillerPresets - .Filter(fp => allFillerIds.Contains(fp.Id)) - .ToListAsync(); + List allFiller = await dbContext.FillerPresets + .Filter(fp => allFillerIds.Contains(fp.Id)) + .ToListAsync(); - if (allFiller.Count(f => f.PadToNearestMinute.HasValue) > 1) - { - return BaseError.New("Schedule may only contain one filler preset that is configured to pad"); - } - - if (allFiller.Any(fp => fp.PadToNearestMinute.HasValue) && !item.FallbackFillerId.HasValue) - { - return BaseError.New("Fallback filler is required when padding"); - } - - return programSchedule; - } - - protected static Validation PlayoutModeMustBeValid( - IProgramScheduleItemRequest item, - ProgramSchedule programSchedule) + if (allFiller.Count(f => f.PadToNearestMinute.HasValue) > 1) { - if (item.MultiCollectionId.HasValue) - { - switch (item.PlaybackOrder) - { - case PlaybackOrder.Chronological: - case PlaybackOrder.Random: - return BaseError.New($"Invalid playback order for multi collection: '{item.PlaybackOrder}'"); - case PlaybackOrder.Shuffle: - case PlaybackOrder.ShuffleInOrder: - break; - } - } - - switch (item.PlayoutMode) - { - case PlayoutMode.Flood: - case PlayoutMode.One: - break; - case PlayoutMode.Multiple: - if (item.MultipleCount.GetValueOrDefault() < 0) - { - return BaseError.New("[MultipleCount] must be greater than or equal to 0 for playout mode 'multiple'"); - } - - break; - case PlayoutMode.Duration: - if (item.PlayoutDuration is null) - { - return BaseError.New("[PlayoutDuration] is required for playout mode 'duration'"); - } - - if (item.TailMode == TailMode.Filler && item.TailFillerId == null) - { - return BaseError.New("Tail Filler is required with tail mode Filler"); - } - - if (item.TailFillerId != null && item.TailMode != TailMode.Filler) - { - return BaseError.New("Tail Filler will not be used unless tail mode is set to Filler"); - } - - break; - default: - return BaseError.New("[PlayoutMode] is invalid"); - } - - return programSchedule; + return BaseError.New("Schedule may only contain one filler preset that is configured to pad"); } - protected Validation CollectionTypeMustBeValid( - IProgramScheduleItemRequest item, - ProgramSchedule programSchedule) + if (allFiller.Any(fp => fp.PadToNearestMinute.HasValue) && !item.FallbackFillerId.HasValue) { - switch (item.CollectionType) - { - case ProgramScheduleItemCollectionType.Collection: - if (item.CollectionId is null) - { - return BaseError.New("[Collection] is required for collection type 'Collection'"); - } - - break; - case ProgramScheduleItemCollectionType.TelevisionShow: - if (item.MediaItemId is null) - { - return BaseError.New("[MediaItem] is required for collection type 'TelevisionShow'"); - } - - break; - case ProgramScheduleItemCollectionType.TelevisionSeason: - if (item.MediaItemId is null) - { - return BaseError.New("[MediaItem] is required for collection type 'TelevisionSeason'"); - } - - break; - case ProgramScheduleItemCollectionType.Artist: - if (item.MediaItemId is null) - { - return BaseError.New("[MediaItem] is required for collection type 'Artist'"); - } - - break; - case ProgramScheduleItemCollectionType.MultiCollection: - if (item.MultiCollectionId is null) - { - return BaseError.New("[MultiCollection] is required for collection type 'MultiCollection'"); - } - - break; - case ProgramScheduleItemCollectionType.SmartCollection: - if (item.SmartCollectionId is null) - { - return BaseError.New("[SmartCollection] is required for collection type 'SmartCollection'"); - } - - break; - default: - return BaseError.New("[CollectionType] is invalid"); - } - - return programSchedule; + return BaseError.New("Fallback filler is required when padding"); } - protected ProgramScheduleItem BuildItem( - ProgramSchedule programSchedule, - int index, - IProgramScheduleItemRequest item) => - item.PlayoutMode switch - { - PlayoutMode.Flood => new ProgramScheduleItemFlood - { - ProgramScheduleId = programSchedule.Id, - Index = index, - StartTime = FixStartTime(item.StartTime), - CollectionType = item.CollectionType, - CollectionId = item.CollectionId, - MultiCollectionId = item.MultiCollectionId, - SmartCollectionId = item.SmartCollectionId, - MediaItemId = item.MediaItemId, - PlaybackOrder = item.PlaybackOrder, - CustomTitle = item.CustomTitle, - GuideMode = item.GuideMode, - PreRollFillerId = item.PreRollFillerId, - MidRollFillerId = item.MidRollFillerId, - PostRollFillerId = item.PostRollFillerId, - TailFillerId = item.TailFillerId, - FallbackFillerId = item.FallbackFillerId - }, - PlayoutMode.One => new ProgramScheduleItemOne - { - ProgramScheduleId = programSchedule.Id, - Index = index, - StartTime = FixStartTime(item.StartTime), - CollectionType = item.CollectionType, - CollectionId = item.CollectionId, - MultiCollectionId = item.MultiCollectionId, - SmartCollectionId = item.SmartCollectionId, - MediaItemId = item.MediaItemId, - PlaybackOrder = item.PlaybackOrder, - CustomTitle = item.CustomTitle, - GuideMode = item.GuideMode, - PreRollFillerId = item.PreRollFillerId, - MidRollFillerId = item.MidRollFillerId, - PostRollFillerId = item.PostRollFillerId, - TailFillerId = item.TailFillerId, - FallbackFillerId = item.FallbackFillerId - }, - PlayoutMode.Multiple => new ProgramScheduleItemMultiple - { - ProgramScheduleId = programSchedule.Id, - Index = index, - StartTime = FixStartTime(item.StartTime), - CollectionType = item.CollectionType, - CollectionId = item.CollectionId, - MultiCollectionId = item.MultiCollectionId, - SmartCollectionId = item.SmartCollectionId, - MediaItemId = item.MediaItemId, - PlaybackOrder = item.PlaybackOrder, - Count = item.MultipleCount.GetValueOrDefault(), - CustomTitle = item.CustomTitle, - GuideMode = item.GuideMode, - PreRollFillerId = item.PreRollFillerId, - MidRollFillerId = item.MidRollFillerId, - PostRollFillerId = item.PostRollFillerId, - TailFillerId = item.TailFillerId, - FallbackFillerId = item.FallbackFillerId - }, - PlayoutMode.Duration => new ProgramScheduleItemDuration - { - ProgramScheduleId = programSchedule.Id, - Index = index, - StartTime = FixStartTime(item.StartTime), - CollectionType = item.CollectionType, - CollectionId = item.CollectionId, - MultiCollectionId = item.MultiCollectionId, - SmartCollectionId = item.SmartCollectionId, - MediaItemId = item.MediaItemId, - PlaybackOrder = item.PlaybackOrder, - PlayoutDuration = FixDuration(item.PlayoutDuration.GetValueOrDefault()), - TailMode = item.TailMode, - CustomTitle = item.CustomTitle, - GuideMode = item.GuideMode, - PreRollFillerId = item.PreRollFillerId, - MidRollFillerId = item.MidRollFillerId, - PostRollFillerId = item.PostRollFillerId, - TailFillerId = item.TailFillerId, - FallbackFillerId = item.FallbackFillerId - }, - _ => throw new NotSupportedException($"Unsupported playout mode {item.PlayoutMode}") - }; - - private static TimeSpan FixDuration(TimeSpan duration) => - duration >= TimeSpan.FromDays(1) ? duration.Subtract(TimeSpan.FromDays(1)) : duration; - - private static TimeSpan? FixStartTime(TimeSpan? startTime) => - startTime.HasValue && startTime.Value >= TimeSpan.FromDays(1) - ? startTime.Value.Subtract(TimeSpan.FromDays(1)) - : startTime; + return programSchedule; } -} + + protected static Validation PlayoutModeMustBeValid( + IProgramScheduleItemRequest item, + ProgramSchedule programSchedule) + { + if (item.MultiCollectionId.HasValue) + { + switch (item.PlaybackOrder) + { + case PlaybackOrder.Chronological: + case PlaybackOrder.Random: + return BaseError.New($"Invalid playback order for multi collection: '{item.PlaybackOrder}'"); + case PlaybackOrder.Shuffle: + case PlaybackOrder.ShuffleInOrder: + break; + } + } + + switch (item.PlayoutMode) + { + case PlayoutMode.Flood: + case PlayoutMode.One: + break; + case PlayoutMode.Multiple: + if (item.MultipleCount.GetValueOrDefault() < 0) + { + return BaseError.New("[MultipleCount] must be greater than or equal to 0 for playout mode 'multiple'"); + } + + break; + case PlayoutMode.Duration: + if (item.PlayoutDuration is null) + { + return BaseError.New("[PlayoutDuration] is required for playout mode 'duration'"); + } + + if (item.TailMode == TailMode.Filler && item.TailFillerId == null) + { + return BaseError.New("Tail Filler is required with tail mode Filler"); + } + + if (item.TailFillerId != null && item.TailMode != TailMode.Filler) + { + return BaseError.New("Tail Filler will not be used unless tail mode is set to Filler"); + } + + break; + default: + return BaseError.New("[PlayoutMode] is invalid"); + } + + return programSchedule; + } + + protected Validation CollectionTypeMustBeValid( + IProgramScheduleItemRequest item, + ProgramSchedule programSchedule) + { + switch (item.CollectionType) + { + case ProgramScheduleItemCollectionType.Collection: + if (item.CollectionId is null) + { + return BaseError.New("[Collection] is required for collection type 'Collection'"); + } + + break; + case ProgramScheduleItemCollectionType.TelevisionShow: + if (item.MediaItemId is null) + { + return BaseError.New("[MediaItem] is required for collection type 'TelevisionShow'"); + } + + break; + case ProgramScheduleItemCollectionType.TelevisionSeason: + if (item.MediaItemId is null) + { + return BaseError.New("[MediaItem] is required for collection type 'TelevisionSeason'"); + } + + break; + case ProgramScheduleItemCollectionType.Artist: + if (item.MediaItemId is null) + { + return BaseError.New("[MediaItem] is required for collection type 'Artist'"); + } + + break; + case ProgramScheduleItemCollectionType.MultiCollection: + if (item.MultiCollectionId is null) + { + return BaseError.New("[MultiCollection] is required for collection type 'MultiCollection'"); + } + + break; + case ProgramScheduleItemCollectionType.SmartCollection: + if (item.SmartCollectionId is null) + { + return BaseError.New("[SmartCollection] is required for collection type 'SmartCollection'"); + } + + break; + default: + return BaseError.New("[CollectionType] is invalid"); + } + + return programSchedule; + } + + protected ProgramScheduleItem BuildItem( + ProgramSchedule programSchedule, + int index, + IProgramScheduleItemRequest item) => + item.PlayoutMode switch + { + PlayoutMode.Flood => new ProgramScheduleItemFlood + { + ProgramScheduleId = programSchedule.Id, + Index = index, + StartTime = FixStartTime(item.StartTime), + CollectionType = item.CollectionType, + CollectionId = item.CollectionId, + MultiCollectionId = item.MultiCollectionId, + SmartCollectionId = item.SmartCollectionId, + MediaItemId = item.MediaItemId, + PlaybackOrder = item.PlaybackOrder, + CustomTitle = item.CustomTitle, + GuideMode = item.GuideMode, + PreRollFillerId = item.PreRollFillerId, + MidRollFillerId = item.MidRollFillerId, + PostRollFillerId = item.PostRollFillerId, + TailFillerId = item.TailFillerId, + FallbackFillerId = item.FallbackFillerId + }, + PlayoutMode.One => new ProgramScheduleItemOne + { + ProgramScheduleId = programSchedule.Id, + Index = index, + StartTime = FixStartTime(item.StartTime), + CollectionType = item.CollectionType, + CollectionId = item.CollectionId, + MultiCollectionId = item.MultiCollectionId, + SmartCollectionId = item.SmartCollectionId, + MediaItemId = item.MediaItemId, + PlaybackOrder = item.PlaybackOrder, + CustomTitle = item.CustomTitle, + GuideMode = item.GuideMode, + PreRollFillerId = item.PreRollFillerId, + MidRollFillerId = item.MidRollFillerId, + PostRollFillerId = item.PostRollFillerId, + TailFillerId = item.TailFillerId, + FallbackFillerId = item.FallbackFillerId + }, + PlayoutMode.Multiple => new ProgramScheduleItemMultiple + { + ProgramScheduleId = programSchedule.Id, + Index = index, + StartTime = FixStartTime(item.StartTime), + CollectionType = item.CollectionType, + CollectionId = item.CollectionId, + MultiCollectionId = item.MultiCollectionId, + SmartCollectionId = item.SmartCollectionId, + MediaItemId = item.MediaItemId, + PlaybackOrder = item.PlaybackOrder, + Count = item.MultipleCount.GetValueOrDefault(), + CustomTitle = item.CustomTitle, + GuideMode = item.GuideMode, + PreRollFillerId = item.PreRollFillerId, + MidRollFillerId = item.MidRollFillerId, + PostRollFillerId = item.PostRollFillerId, + TailFillerId = item.TailFillerId, + FallbackFillerId = item.FallbackFillerId + }, + PlayoutMode.Duration => new ProgramScheduleItemDuration + { + ProgramScheduleId = programSchedule.Id, + Index = index, + StartTime = FixStartTime(item.StartTime), + CollectionType = item.CollectionType, + CollectionId = item.CollectionId, + MultiCollectionId = item.MultiCollectionId, + SmartCollectionId = item.SmartCollectionId, + MediaItemId = item.MediaItemId, + PlaybackOrder = item.PlaybackOrder, + PlayoutDuration = FixDuration(item.PlayoutDuration.GetValueOrDefault()), + TailMode = item.TailMode, + CustomTitle = item.CustomTitle, + GuideMode = item.GuideMode, + PreRollFillerId = item.PreRollFillerId, + MidRollFillerId = item.MidRollFillerId, + PostRollFillerId = item.PostRollFillerId, + TailFillerId = item.TailFillerId, + FallbackFillerId = item.FallbackFillerId + }, + _ => throw new NotSupportedException($"Unsupported playout mode {item.PlayoutMode}") + }; + + private static TimeSpan FixDuration(TimeSpan duration) => + duration >= TimeSpan.FromDays(1) ? duration.Subtract(TimeSpan.FromDays(1)) : duration; + + private static TimeSpan? FixStartTime(TimeSpan? startTime) => + startTime.HasValue && startTime.Value >= TimeSpan.FromDays(1) + ? startTime.Value.Subtract(TimeSpan.FromDays(1)) + : startTime; +} \ No newline at end of file diff --git a/ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItems.cs b/ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItems.cs index 5afe4097f..7b6d11f64 100644 --- a/ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItems.cs +++ b/ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItems.cs @@ -1,35 +1,30 @@ -using System; -using System.Collections.Generic; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.ProgramSchedules.Commands -{ - public record ReplaceProgramScheduleItem( - int Index, - StartType StartType, - TimeSpan? StartTime, - PlayoutMode PlayoutMode, - ProgramScheduleItemCollectionType CollectionType, - int? CollectionId, - int? MultiCollectionId, - int? SmartCollectionId, - int? MediaItemId, - PlaybackOrder PlaybackOrder, - int? MultipleCount, - TimeSpan? PlayoutDuration, - TailMode TailMode, - string CustomTitle, - GuideMode GuideMode, - int? PreRollFillerId, - int? MidRollFillerId, - int? PostRollFillerId, - int? TailFillerId, - int? FallbackFillerId) : IProgramScheduleItemRequest; +namespace ErsatzTV.Application.ProgramSchedules; - public record ReplaceProgramScheduleItems - (int ProgramScheduleId, List Items) : IRequest< - Either>>; -} +public record ReplaceProgramScheduleItem( + int Index, + StartType StartType, + TimeSpan? StartTime, + PlayoutMode PlayoutMode, + ProgramScheduleItemCollectionType CollectionType, + int? CollectionId, + int? MultiCollectionId, + int? SmartCollectionId, + int? MediaItemId, + PlaybackOrder PlaybackOrder, + int? MultipleCount, + TimeSpan? PlayoutDuration, + TailMode TailMode, + string CustomTitle, + GuideMode GuideMode, + int? PreRollFillerId, + int? MidRollFillerId, + int? PostRollFillerId, + int? TailFillerId, + int? FallbackFillerId) : IProgramScheduleItemRequest; + +public record ReplaceProgramScheduleItems + (int ProgramScheduleId, List Items) : IRequest< + Either>>; \ No newline at end of file diff --git a/ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItemsHandler.cs b/ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItemsHandler.cs index b0ddbc2a0..ac4cb17dd 100644 --- a/ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItemsHandler.cs +++ b/ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItemsHandler.cs @@ -1,139 +1,131 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Channels; -using System.Threading.Tasks; -using ErsatzTV.Application.Playouts.Commands; +using System.Threading.Channels; +using ErsatzTV.Application.Playouts; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Infrastructure.Data; -using LanguageExt; -using MediatR; using Microsoft.EntityFrameworkCore; using static ErsatzTV.Application.ProgramSchedules.Mapper; -using static LanguageExt.Prelude; -namespace ErsatzTV.Application.ProgramSchedules.Commands +namespace ErsatzTV.Application.ProgramSchedules; + +public class ReplaceProgramScheduleItemsHandler : ProgramScheduleItemCommandBase, + IRequestHandler>> { - public class ReplaceProgramScheduleItemsHandler : ProgramScheduleItemCommandBase, - IRequestHandler>> + private readonly IDbContextFactory _dbContextFactory; + private readonly ChannelWriter _channel; + + public ReplaceProgramScheduleItemsHandler( + IDbContextFactory dbContextFactory, + ChannelWriter channel) { - private readonly IDbContextFactory _dbContextFactory; - private readonly ChannelWriter _channel; - - public ReplaceProgramScheduleItemsHandler( - IDbContextFactory dbContextFactory, - ChannelWriter channel) - { - _dbContextFactory = dbContextFactory; - _channel = channel; - } - - public async Task>> Handle( - ReplaceProgramScheduleItems request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - Validation validation = await Validate(dbContext, request); - return await validation.Apply(ps => PersistItems(dbContext, request, ps)); - } - - private async Task> PersistItems( - TvContext dbContext, - ReplaceProgramScheduleItems request, - ProgramSchedule programSchedule) - { - dbContext.RemoveRange(programSchedule.Items); - programSchedule.Items = request.Items.Map(i => BuildItem(programSchedule, i.Index, i)).ToList(); - - await dbContext.SaveChangesAsync(); - - // rebuild any playouts that use this schedule - foreach (Playout playout in programSchedule.Playouts) - { - await _channel.WriteAsync(new BuildPlayout(playout.Id, true)); - } - - return programSchedule.Items.Map(ProjectToViewModel); - } - - private Task> Validate( - TvContext dbContext, - ReplaceProgramScheduleItems request) => - ProgramScheduleMustExist(dbContext, request.ProgramScheduleId) - .BindT(programSchedule => PlayoutModesMustBeValid(request, programSchedule)) - .BindT(programSchedule => CollectionTypesMustBeValid(request, programSchedule)) - .BindT(programSchedule => PlaybackOrdersMustBeValid(request, programSchedule)) - .BindT(programSchedule => FillerConfigurationsMustBeValid(dbContext, request, programSchedule)); - - private static Validation PlayoutModesMustBeValid( - ReplaceProgramScheduleItems request, - ProgramSchedule programSchedule) => - request.Items.Map(item => PlayoutModeMustBeValid(item, programSchedule)).Sequence() - .Map(_ => programSchedule); - - private Validation CollectionTypesMustBeValid( - ReplaceProgramScheduleItems request, - ProgramSchedule programSchedule) => - request.Items.Map(item => CollectionTypeMustBeValid(item, programSchedule)).Sequence() - .Map(_ => programSchedule); - - private static async Task> FillerConfigurationsMustBeValid( - TvContext dbContext, - ReplaceProgramScheduleItems request, - ProgramSchedule programSchedule) - { - foreach (ReplaceProgramScheduleItem item in request.Items) - { - Either result = await FillerConfigurationMustBeValid( - dbContext, - item, - programSchedule); - if (result.IsLeft) - { - return result.ToValidation(); - } - } - - return programSchedule; - } - - private static Validation PlaybackOrdersMustBeValid( - ReplaceProgramScheduleItems request, - ProgramSchedule programSchedule) - { - var keyOrders = new Dictionary>(); - foreach (ReplaceProgramScheduleItem item in request.Items) - { - var key = new CollectionKey( - item.CollectionType, - item.CollectionId, - item.MediaItemId, - item.MultiCollectionId, - item.SmartCollectionId); - - if (keyOrders.TryGetValue(key, out System.Collections.Generic.HashSet playbackOrders)) - { - playbackOrders.Add(item.PlaybackOrder); - keyOrders[key] = playbackOrders; - } - else - { - keyOrders.Add(key, new System.Collections.Generic.HashSet { item.PlaybackOrder }); - } - } - - return Optional(keyOrders.Values.Count(set => set.Count != 1)) - .Filter(count => count == 0) - .Map(_ => programSchedule) - .ToValidation("A collection must not use multiple playback orders"); - } - - private record CollectionKey( - ProgramScheduleItemCollectionType CollectionType, - int? CollectionId, - int? MediaItemId, - int? MultiCollectionId, - int? SmartCollectionId); + _dbContextFactory = dbContextFactory; + _channel = channel; } -} + + public async Task>> Handle( + ReplaceProgramScheduleItems request, + CancellationToken cancellationToken) + { + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + Validation validation = await Validate(dbContext, request); + return await LanguageExtensions.Apply(validation, ps => PersistItems(dbContext, request, ps)); + } + + private async Task> PersistItems( + TvContext dbContext, + ReplaceProgramScheduleItems request, + ProgramSchedule programSchedule) + { + dbContext.RemoveRange(programSchedule.Items); + programSchedule.Items = request.Items.Map(i => BuildItem(programSchedule, i.Index, i)).ToList(); + + await dbContext.SaveChangesAsync(); + + // rebuild any playouts that use this schedule + foreach (Playout playout in programSchedule.Playouts) + { + await _channel.WriteAsync(new BuildPlayout(playout.Id, true)); + } + + return programSchedule.Items.Map(ProjectToViewModel); + } + + private Task> Validate( + TvContext dbContext, + ReplaceProgramScheduleItems request) => + ProgramScheduleMustExist(dbContext, request.ProgramScheduleId) + .BindT(programSchedule => PlayoutModesMustBeValid(request, programSchedule)) + .BindT(programSchedule => CollectionTypesMustBeValid(request, programSchedule)) + .BindT(programSchedule => PlaybackOrdersMustBeValid(request, programSchedule)) + .BindT(programSchedule => FillerConfigurationsMustBeValid(dbContext, request, programSchedule)); + + private static Validation PlayoutModesMustBeValid( + ReplaceProgramScheduleItems request, + ProgramSchedule programSchedule) => + request.Items.Map(item => PlayoutModeMustBeValid(item, programSchedule)).Sequence() + .Map(_ => programSchedule); + + private Validation CollectionTypesMustBeValid( + ReplaceProgramScheduleItems request, + ProgramSchedule programSchedule) => + request.Items.Map(item => CollectionTypeMustBeValid(item, programSchedule)).Sequence() + .Map(_ => programSchedule); + + private static async Task> FillerConfigurationsMustBeValid( + TvContext dbContext, + ReplaceProgramScheduleItems request, + ProgramSchedule programSchedule) + { + foreach (ReplaceProgramScheduleItem item in request.Items) + { + Either result = await FillerConfigurationMustBeValid( + dbContext, + item, + programSchedule); + if (result.IsLeft) + { + return result.ToValidation(); + } + } + + return programSchedule; + } + + private static Validation PlaybackOrdersMustBeValid( + ReplaceProgramScheduleItems request, + ProgramSchedule programSchedule) + { + var keyOrders = new Dictionary>(); + foreach (ReplaceProgramScheduleItem item in request.Items) + { + var key = new CollectionKey( + item.CollectionType, + item.CollectionId, + item.MediaItemId, + item.MultiCollectionId, + item.SmartCollectionId); + + if (keyOrders.TryGetValue(key, out System.Collections.Generic.HashSet playbackOrders)) + { + playbackOrders.Add(item.PlaybackOrder); + keyOrders[key] = playbackOrders; + } + else + { + keyOrders.Add(key, new System.Collections.Generic.HashSet { item.PlaybackOrder }); + } + } + + return Optional(keyOrders.Values.Count(set => set.Count != 1)) + .Filter(count => count == 0) + .Map(_ => programSchedule) + .ToValidation("A collection must not use multiple playback orders"); + } + + private record CollectionKey( + ProgramScheduleItemCollectionType CollectionType, + int? CollectionId, + int? MediaItemId, + int? MultiCollectionId, + int? SmartCollectionId); +} \ No newline at end of file diff --git a/ErsatzTV.Application/ProgramSchedules/Commands/UpdateProgramSchedule.cs b/ErsatzTV.Application/ProgramSchedules/Commands/UpdateProgramSchedule.cs index 6f3881af5..c2c8a1143 100644 --- a/ErsatzTV.Application/ProgramSchedules/Commands/UpdateProgramSchedule.cs +++ b/ErsatzTV.Application/ProgramSchedules/Commands/UpdateProgramSchedule.cs @@ -1,15 +1,11 @@ using ErsatzTV.Core; -using ErsatzTV.Core.Domain; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.ProgramSchedules.Commands -{ - public record UpdateProgramSchedule - ( - int ProgramScheduleId, - string Name, - bool KeepMultiPartEpisodesTogether, - bool TreatCollectionsAsShows, - bool ShuffleScheduleItems) : IRequest>; -} +namespace ErsatzTV.Application.ProgramSchedules; + +public record UpdateProgramSchedule +( + int ProgramScheduleId, + string Name, + bool KeepMultiPartEpisodesTogether, + bool TreatCollectionsAsShows, + bool ShuffleScheduleItems) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/ProgramSchedules/Commands/UpdateProgramScheduleHandler.cs b/ErsatzTV.Application/ProgramSchedules/Commands/UpdateProgramScheduleHandler.cs index 4beb9ff87..ded2771ac 100644 --- a/ErsatzTV.Application/ProgramSchedules/Commands/UpdateProgramScheduleHandler.cs +++ b/ErsatzTV.Application/ProgramSchedules/Commands/UpdateProgramScheduleHandler.cs @@ -1,92 +1,86 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Channels; -using System.Threading.Tasks; -using ErsatzTV.Application.Playouts.Commands; +using System.Threading.Channels; +using ErsatzTV.Application.Playouts; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; -using LanguageExt; -using MediatR; using Microsoft.EntityFrameworkCore; -namespace ErsatzTV.Application.ProgramSchedules.Commands +namespace ErsatzTV.Application.ProgramSchedules; + +public class UpdateProgramScheduleHandler : + IRequestHandler> { - public class UpdateProgramScheduleHandler : - IRequestHandler> + private readonly ChannelWriter _channel; + private readonly IDbContextFactory _dbContextFactory; + + public UpdateProgramScheduleHandler( + IDbContextFactory dbContextFactory, + ChannelWriter channel) { - private readonly ChannelWriter _channel; - private readonly IDbContextFactory _dbContextFactory; - - public UpdateProgramScheduleHandler( - IDbContextFactory dbContextFactory, - ChannelWriter channel) - { - _dbContextFactory = dbContextFactory; - _channel = channel; - } - - public async Task> Handle( - UpdateProgramSchedule request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - - Validation validation = await Validate(dbContext, request); - return await validation.Apply(ps => ApplyUpdateRequest(dbContext, ps, request)); - } - - private async Task ApplyUpdateRequest( - TvContext dbContext, - ProgramSchedule programSchedule, - UpdateProgramSchedule request) - { - // we need to rebuild playouts if the playback order or keep multi-episodes has been modified - bool needToRebuildPlayout = - programSchedule.KeepMultiPartEpisodesTogether != request.KeepMultiPartEpisodesTogether || - programSchedule.TreatCollectionsAsShows != request.TreatCollectionsAsShows || - programSchedule.ShuffleScheduleItems != request.ShuffleScheduleItems; - - programSchedule.Name = request.Name; - programSchedule.KeepMultiPartEpisodesTogether = request.KeepMultiPartEpisodesTogether; - programSchedule.TreatCollectionsAsShows = programSchedule.KeepMultiPartEpisodesTogether && - request.TreatCollectionsAsShows; - programSchedule.ShuffleScheduleItems = request.ShuffleScheduleItems; - - await dbContext.SaveChangesAsync(); - - if (needToRebuildPlayout) - { - List 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(playoutId, true)); - } - } - - return new UpdateProgramScheduleResult(programSchedule.Id); - } - - private static async Task> Validate( - TvContext dbContext, - UpdateProgramSchedule request) => - (await ProgramScheduleMustExist(dbContext, request), ValidateName(request)) - .Apply((programSchedule, _) => programSchedule); - - private static Task> ProgramScheduleMustExist( - TvContext dbContext, - UpdateProgramSchedule request) => - dbContext.ProgramSchedules - .SelectOneAsync(ps => ps.Id, ps => ps.Id == request.ProgramScheduleId) - .Map(o => o.ToValidation("ProgramSchedule does not exist")); - - private static Validation ValidateName(UpdateProgramSchedule request) => - request.NotEmpty(c => c.Name) - .Bind(_ => request.NotLongerThan(50)(c => c.Name)); + _dbContextFactory = dbContextFactory; + _channel = channel; } -} + + public async Task> Handle( + UpdateProgramSchedule request, + CancellationToken cancellationToken) + { + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + + Validation validation = await Validate(dbContext, request); + return await LanguageExtensions.Apply(validation, ps => ApplyUpdateRequest(dbContext, ps, request)); + } + + private async Task ApplyUpdateRequest( + TvContext dbContext, + ProgramSchedule programSchedule, + UpdateProgramSchedule request) + { + // we need to rebuild playouts if the playback order or keep multi-episodes has been modified + bool needToRebuildPlayout = + programSchedule.KeepMultiPartEpisodesTogether != request.KeepMultiPartEpisodesTogether || + programSchedule.TreatCollectionsAsShows != request.TreatCollectionsAsShows || + programSchedule.ShuffleScheduleItems != request.ShuffleScheduleItems; + + programSchedule.Name = request.Name; + programSchedule.KeepMultiPartEpisodesTogether = request.KeepMultiPartEpisodesTogether; + programSchedule.TreatCollectionsAsShows = programSchedule.KeepMultiPartEpisodesTogether && + request.TreatCollectionsAsShows; + programSchedule.ShuffleScheduleItems = request.ShuffleScheduleItems; + + await dbContext.SaveChangesAsync(); + + if (needToRebuildPlayout) + { + List 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(playoutId, true)); + } + } + + return new UpdateProgramScheduleResult(programSchedule.Id); + } + + private static async Task> Validate( + TvContext dbContext, + UpdateProgramSchedule request) => + (await ProgramScheduleMustExist(dbContext, request), ValidateName(request)) + .Apply((programSchedule, _) => programSchedule); + + private static Task> ProgramScheduleMustExist( + TvContext dbContext, + UpdateProgramSchedule request) => + dbContext.ProgramSchedules + .SelectOneAsync(ps => ps.Id, ps => ps.Id == request.ProgramScheduleId) + .Map(o => o.ToValidation("ProgramSchedule does not exist")); + + private static Validation ValidateName(UpdateProgramSchedule request) => + request.NotEmpty(c => c.Name) + .Bind(_ => request.NotLongerThan(50)(c => c.Name)); +} \ No newline at end of file diff --git a/ErsatzTV.Application/ProgramSchedules/Commands/UpdateProgramScheduleResult.cs b/ErsatzTV.Application/ProgramSchedules/Commands/UpdateProgramScheduleResult.cs index 4e15e5d07..f79a12350 100644 --- a/ErsatzTV.Application/ProgramSchedules/Commands/UpdateProgramScheduleResult.cs +++ b/ErsatzTV.Application/ProgramSchedules/Commands/UpdateProgramScheduleResult.cs @@ -1,4 +1,3 @@ -namespace ErsatzTV.Application.ProgramSchedules.Commands -{ - public record UpdateProgramScheduleResult(int ProgramScheduleId) : EntityIdResult(ProgramScheduleId); -} +namespace ErsatzTV.Application.ProgramSchedules; + +public record UpdateProgramScheduleResult(int ProgramScheduleId) : EntityIdResult(ProgramScheduleId); \ No newline at end of file diff --git a/ErsatzTV.Application/ProgramSchedules/Mapper.cs b/ErsatzTV.Application/ProgramSchedules/Mapper.cs index ed8e0e311..089315dd1 100644 --- a/ErsatzTV.Application/ProgramSchedules/Mapper.cs +++ b/ErsatzTV.Application/ProgramSchedules/Mapper.cs @@ -1,190 +1,188 @@ -using System; -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; -namespace ErsatzTV.Application.ProgramSchedules +namespace ErsatzTV.Application.ProgramSchedules; + +internal static class Mapper { - internal static class Mapper - { - internal static ProgramScheduleViewModel ProjectToViewModel(ProgramSchedule programSchedule) => - new( - programSchedule.Id, - programSchedule.Name, - programSchedule.KeepMultiPartEpisodesTogether, - programSchedule.TreatCollectionsAsShows, - programSchedule.ShuffleScheduleItems); + internal static ProgramScheduleViewModel ProjectToViewModel(ProgramSchedule programSchedule) => + new( + programSchedule.Id, + programSchedule.Name, + programSchedule.KeepMultiPartEpisodesTogether, + programSchedule.TreatCollectionsAsShows, + programSchedule.ShuffleScheduleItems); - internal static ProgramScheduleItemViewModel ProjectToViewModel(ProgramScheduleItem programScheduleItem) => - programScheduleItem switch - { - ProgramScheduleItemDuration duration => - new ProgramScheduleItemDurationViewModel( - duration.Id, - duration.Index, - duration.StartType, - duration.StartTime, - duration.CollectionType, - duration.Collection != null - ? MediaCollections.Mapper.ProjectToViewModel(duration.Collection) - : null, - duration.MultiCollection != null - ? MediaCollections.Mapper.ProjectToViewModel(duration.MultiCollection) - : null, - duration.SmartCollection != null - ? MediaCollections.Mapper.ProjectToViewModel(duration.SmartCollection) - : null, - duration.MediaItem switch - { - Show show => MediaItems.Mapper.ProjectToViewModel(show), - Season season => MediaItems.Mapper.ProjectToViewModel(season), - Artist artist => MediaItems.Mapper.ProjectToViewModel(artist), - _ => null - }, - duration.PlaybackOrder, - duration.PlayoutDuration, - duration.TailMode, - duration.CustomTitle, - duration.GuideMode, - duration.PreRollFiller != null - ? Filler.Mapper.ProjectToViewModel(duration.PreRollFiller) - : null, - duration.MidRollFiller != null - ? Filler.Mapper.ProjectToViewModel(duration.MidRollFiller) - : null, - duration.PostRollFiller != null - ? Filler.Mapper.ProjectToViewModel(duration.PostRollFiller) - : null, - duration.TailFiller != null - ? Filler.Mapper.ProjectToViewModel(duration.TailFiller) - : null, - duration.FallbackFiller != null - ? Filler.Mapper.ProjectToViewModel(duration.FallbackFiller) - : null), - ProgramScheduleItemFlood flood => - new ProgramScheduleItemFloodViewModel( - flood.Id, - flood.Index, - flood.StartType, - flood.StartTime, - flood.CollectionType, - flood.Collection != null - ? MediaCollections.Mapper.ProjectToViewModel(flood.Collection) - : null, - flood.MultiCollection != null - ? MediaCollections.Mapper.ProjectToViewModel(flood.MultiCollection) - : null, - flood.SmartCollection != null - ? MediaCollections.Mapper.ProjectToViewModel(flood.SmartCollection) - : null, - flood.MediaItem switch - { - Show show => MediaItems.Mapper.ProjectToViewModel(show), - Season season => MediaItems.Mapper.ProjectToViewModel(season), - Artist artist => MediaItems.Mapper.ProjectToViewModel(artist), - _ => null - }, - flood.PlaybackOrder, - flood.CustomTitle, - flood.GuideMode, - flood.PreRollFiller != null - ? Filler.Mapper.ProjectToViewModel(flood.PreRollFiller) - : null, - flood.MidRollFiller != null - ? Filler.Mapper.ProjectToViewModel(flood.MidRollFiller) - : null, - flood.PostRollFiller != null - ? Filler.Mapper.ProjectToViewModel(flood.PostRollFiller) - : null, - flood.TailFiller != null - ? Filler.Mapper.ProjectToViewModel(flood.TailFiller) - : null, - flood.FallbackFiller != null - ? Filler.Mapper.ProjectToViewModel(flood.FallbackFiller) - : null), - ProgramScheduleItemMultiple multiple => - new ProgramScheduleItemMultipleViewModel( - multiple.Id, - multiple.Index, - multiple.StartType, - multiple.StartTime, - multiple.CollectionType, - multiple.Collection != null - ? MediaCollections.Mapper.ProjectToViewModel(multiple.Collection) - : null, - multiple.MultiCollection != null - ? MediaCollections.Mapper.ProjectToViewModel(multiple.MultiCollection) - : null, - multiple.SmartCollection != null - ? MediaCollections.Mapper.ProjectToViewModel(multiple.SmartCollection) - : null, - multiple.MediaItem switch - { - Show show => MediaItems.Mapper.ProjectToViewModel(show), - Season season => MediaItems.Mapper.ProjectToViewModel(season), - Artist artist => MediaItems.Mapper.ProjectToViewModel(artist), - _ => null - }, - multiple.PlaybackOrder, - multiple.Count, - multiple.CustomTitle, - multiple.GuideMode, - multiple.PreRollFiller != null - ? Filler.Mapper.ProjectToViewModel(multiple.PreRollFiller) - : null, - multiple.MidRollFiller != null - ? Filler.Mapper.ProjectToViewModel(multiple.MidRollFiller) - : null, - multiple.PostRollFiller != null - ? Filler.Mapper.ProjectToViewModel(multiple.PostRollFiller) - : null, - multiple.TailFiller != null - ? Filler.Mapper.ProjectToViewModel(multiple.TailFiller) - : null, - multiple.FallbackFiller != null - ? Filler.Mapper.ProjectToViewModel(multiple.FallbackFiller) - : null), - ProgramScheduleItemOne one => - new ProgramScheduleItemOneViewModel( - one.Id, - one.Index, - one.StartType, - one.StartTime, - one.CollectionType, - one.Collection != null - ? MediaCollections.Mapper.ProjectToViewModel(one.Collection) - : null, - one.MultiCollection != null - ? MediaCollections.Mapper.ProjectToViewModel(one.MultiCollection) - : null, - one.SmartCollection != null - ? MediaCollections.Mapper.ProjectToViewModel(one.SmartCollection) - : null, - one.MediaItem switch - { - Show show => MediaItems.Mapper.ProjectToViewModel(show), - Season season => MediaItems.Mapper.ProjectToViewModel(season), - Artist artist => MediaItems.Mapper.ProjectToViewModel(artist), - _ => null - }, - one.PlaybackOrder, - one.CustomTitle, - one.GuideMode, - one.PreRollFiller != null - ? Filler.Mapper.ProjectToViewModel(one.PreRollFiller) - : null, - one.MidRollFiller != null - ? Filler.Mapper.ProjectToViewModel(one.MidRollFiller) - : null, - one.PostRollFiller != null - ? Filler.Mapper.ProjectToViewModel(one.PostRollFiller) - : null, - one.TailFiller != null - ? Filler.Mapper.ProjectToViewModel(one.TailFiller) - : null, - one.FallbackFiller != null - ? Filler.Mapper.ProjectToViewModel(one.FallbackFiller) - : null), - _ => throw new NotSupportedException( - $"Unsupported program schedule item type {programScheduleItem.GetType().Name}") - }; - } -} + internal static ProgramScheduleItemViewModel ProjectToViewModel(ProgramScheduleItem programScheduleItem) => + programScheduleItem switch + { + ProgramScheduleItemDuration duration => + new ProgramScheduleItemDurationViewModel( + duration.Id, + duration.Index, + duration.StartType, + duration.StartTime, + duration.CollectionType, + duration.Collection != null + ? MediaCollections.Mapper.ProjectToViewModel(duration.Collection) + : null, + duration.MultiCollection != null + ? MediaCollections.Mapper.ProjectToViewModel(duration.MultiCollection) + : null, + duration.SmartCollection != null + ? MediaCollections.Mapper.ProjectToViewModel(duration.SmartCollection) + : null, + duration.MediaItem switch + { + Show show => MediaItems.Mapper.ProjectToViewModel(show), + Season season => MediaItems.Mapper.ProjectToViewModel(season), + Artist artist => MediaItems.Mapper.ProjectToViewModel(artist), + _ => null + }, + duration.PlaybackOrder, + duration.PlayoutDuration, + duration.TailMode, + duration.CustomTitle, + duration.GuideMode, + duration.PreRollFiller != null + ? Filler.Mapper.ProjectToViewModel(duration.PreRollFiller) + : null, + duration.MidRollFiller != null + ? Filler.Mapper.ProjectToViewModel(duration.MidRollFiller) + : null, + duration.PostRollFiller != null + ? Filler.Mapper.ProjectToViewModel(duration.PostRollFiller) + : null, + duration.TailFiller != null + ? Filler.Mapper.ProjectToViewModel(duration.TailFiller) + : null, + duration.FallbackFiller != null + ? Filler.Mapper.ProjectToViewModel(duration.FallbackFiller) + : null), + ProgramScheduleItemFlood flood => + new ProgramScheduleItemFloodViewModel( + flood.Id, + flood.Index, + flood.StartType, + flood.StartTime, + flood.CollectionType, + flood.Collection != null + ? MediaCollections.Mapper.ProjectToViewModel(flood.Collection) + : null, + flood.MultiCollection != null + ? MediaCollections.Mapper.ProjectToViewModel(flood.MultiCollection) + : null, + flood.SmartCollection != null + ? MediaCollections.Mapper.ProjectToViewModel(flood.SmartCollection) + : null, + flood.MediaItem switch + { + Show show => MediaItems.Mapper.ProjectToViewModel(show), + Season season => MediaItems.Mapper.ProjectToViewModel(season), + Artist artist => MediaItems.Mapper.ProjectToViewModel(artist), + _ => null + }, + flood.PlaybackOrder, + flood.CustomTitle, + flood.GuideMode, + flood.PreRollFiller != null + ? Filler.Mapper.ProjectToViewModel(flood.PreRollFiller) + : null, + flood.MidRollFiller != null + ? Filler.Mapper.ProjectToViewModel(flood.MidRollFiller) + : null, + flood.PostRollFiller != null + ? Filler.Mapper.ProjectToViewModel(flood.PostRollFiller) + : null, + flood.TailFiller != null + ? Filler.Mapper.ProjectToViewModel(flood.TailFiller) + : null, + flood.FallbackFiller != null + ? Filler.Mapper.ProjectToViewModel(flood.FallbackFiller) + : null), + ProgramScheduleItemMultiple multiple => + new ProgramScheduleItemMultipleViewModel( + multiple.Id, + multiple.Index, + multiple.StartType, + multiple.StartTime, + multiple.CollectionType, + multiple.Collection != null + ? MediaCollections.Mapper.ProjectToViewModel(multiple.Collection) + : null, + multiple.MultiCollection != null + ? MediaCollections.Mapper.ProjectToViewModel(multiple.MultiCollection) + : null, + multiple.SmartCollection != null + ? MediaCollections.Mapper.ProjectToViewModel(multiple.SmartCollection) + : null, + multiple.MediaItem switch + { + Show show => MediaItems.Mapper.ProjectToViewModel(show), + Season season => MediaItems.Mapper.ProjectToViewModel(season), + Artist artist => MediaItems.Mapper.ProjectToViewModel(artist), + _ => null + }, + multiple.PlaybackOrder, + multiple.Count, + multiple.CustomTitle, + multiple.GuideMode, + multiple.PreRollFiller != null + ? Filler.Mapper.ProjectToViewModel(multiple.PreRollFiller) + : null, + multiple.MidRollFiller != null + ? Filler.Mapper.ProjectToViewModel(multiple.MidRollFiller) + : null, + multiple.PostRollFiller != null + ? Filler.Mapper.ProjectToViewModel(multiple.PostRollFiller) + : null, + multiple.TailFiller != null + ? Filler.Mapper.ProjectToViewModel(multiple.TailFiller) + : null, + multiple.FallbackFiller != null + ? Filler.Mapper.ProjectToViewModel(multiple.FallbackFiller) + : null), + ProgramScheduleItemOne one => + new ProgramScheduleItemOneViewModel( + one.Id, + one.Index, + one.StartType, + one.StartTime, + one.CollectionType, + one.Collection != null + ? MediaCollections.Mapper.ProjectToViewModel(one.Collection) + : null, + one.MultiCollection != null + ? MediaCollections.Mapper.ProjectToViewModel(one.MultiCollection) + : null, + one.SmartCollection != null + ? MediaCollections.Mapper.ProjectToViewModel(one.SmartCollection) + : null, + one.MediaItem switch + { + Show show => MediaItems.Mapper.ProjectToViewModel(show), + Season season => MediaItems.Mapper.ProjectToViewModel(season), + Artist artist => MediaItems.Mapper.ProjectToViewModel(artist), + _ => null + }, + one.PlaybackOrder, + one.CustomTitle, + one.GuideMode, + one.PreRollFiller != null + ? Filler.Mapper.ProjectToViewModel(one.PreRollFiller) + : null, + one.MidRollFiller != null + ? Filler.Mapper.ProjectToViewModel(one.MidRollFiller) + : null, + one.PostRollFiller != null + ? Filler.Mapper.ProjectToViewModel(one.PostRollFiller) + : null, + one.TailFiller != null + ? Filler.Mapper.ProjectToViewModel(one.TailFiller) + : null, + one.FallbackFiller != null + ? Filler.Mapper.ProjectToViewModel(one.FallbackFiller) + : null), + _ => throw new NotSupportedException( + $"Unsupported program schedule item type {programScheduleItem.GetType().Name}") + }; +} \ No newline at end of file diff --git a/ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemDurationViewModel.cs b/ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemDurationViewModel.cs index 8768b0ca6..a74b846fe 100644 --- a/ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemDurationViewModel.cs +++ b/ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemDurationViewModel.cs @@ -1,57 +1,55 @@ -using System; -using ErsatzTV.Application.Filler; +using ErsatzTV.Application.Filler; using ErsatzTV.Application.MediaCollections; using ErsatzTV.Application.MediaItems; using ErsatzTV.Core.Domain; -namespace ErsatzTV.Application.ProgramSchedules -{ - public record ProgramScheduleItemDurationViewModel : ProgramScheduleItemViewModel - { - public ProgramScheduleItemDurationViewModel( - int id, - int index, - StartType startType, - TimeSpan? startTime, - ProgramScheduleItemCollectionType collectionType, - MediaCollectionViewModel collection, - MultiCollectionViewModel multiCollection, - SmartCollectionViewModel smartCollection, - NamedMediaItemViewModel mediaItem, - PlaybackOrder playbackOrder, - TimeSpan playoutDuration, - TailMode tailMode, - string customTitle, - GuideMode guideMode, - FillerPresetViewModel preRollFiller, - FillerPresetViewModel midRollFiller, - FillerPresetViewModel postRollFiller, - FillerPresetViewModel tailFiller, - FillerPresetViewModel fallbackFiller) : base( - id, - index, - startType, - startTime, - PlayoutMode.Duration, - collectionType, - collection, - multiCollection, - smartCollection, - mediaItem, - playbackOrder, - customTitle, - guideMode, - preRollFiller, - midRollFiller, - postRollFiller, - tailFiller, - fallbackFiller) - { - PlayoutDuration = playoutDuration; - TailMode = tailMode; - } +namespace ErsatzTV.Application.ProgramSchedules; - public TimeSpan PlayoutDuration { get; } - public TailMode TailMode { get; } +public record ProgramScheduleItemDurationViewModel : ProgramScheduleItemViewModel +{ + public ProgramScheduleItemDurationViewModel( + int id, + int index, + StartType startType, + TimeSpan? startTime, + ProgramScheduleItemCollectionType collectionType, + MediaCollectionViewModel collection, + MultiCollectionViewModel multiCollection, + SmartCollectionViewModel smartCollection, + NamedMediaItemViewModel mediaItem, + PlaybackOrder playbackOrder, + TimeSpan playoutDuration, + TailMode tailMode, + string customTitle, + GuideMode guideMode, + FillerPresetViewModel preRollFiller, + FillerPresetViewModel midRollFiller, + FillerPresetViewModel postRollFiller, + FillerPresetViewModel tailFiller, + FillerPresetViewModel fallbackFiller) : base( + id, + index, + startType, + startTime, + PlayoutMode.Duration, + collectionType, + collection, + multiCollection, + smartCollection, + mediaItem, + playbackOrder, + customTitle, + guideMode, + preRollFiller, + midRollFiller, + postRollFiller, + tailFiller, + fallbackFiller) + { + PlayoutDuration = playoutDuration; + TailMode = tailMode; } -} + + public TimeSpan PlayoutDuration { get; } + public TailMode TailMode { get; } +} \ No newline at end of file diff --git a/ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemFloodViewModel.cs b/ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemFloodViewModel.cs index 434deec86..f3fd11e24 100644 --- a/ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemFloodViewModel.cs +++ b/ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemFloodViewModel.cs @@ -1,50 +1,48 @@ -using System; -using ErsatzTV.Application.Filler; +using ErsatzTV.Application.Filler; using ErsatzTV.Application.MediaCollections; using ErsatzTV.Application.MediaItems; using ErsatzTV.Core.Domain; -namespace ErsatzTV.Application.ProgramSchedules +namespace ErsatzTV.Application.ProgramSchedules; + +public record ProgramScheduleItemFloodViewModel : ProgramScheduleItemViewModel { - public record ProgramScheduleItemFloodViewModel : ProgramScheduleItemViewModel + public ProgramScheduleItemFloodViewModel( + int id, + int index, + StartType startType, + TimeSpan? startTime, + ProgramScheduleItemCollectionType collectionType, + MediaCollectionViewModel collection, + MultiCollectionViewModel multiCollection, + SmartCollectionViewModel smartCollection, + NamedMediaItemViewModel mediaItem, + PlaybackOrder playbackOrder, + string customTitle, + GuideMode guideMode, + FillerPresetViewModel preRollFiller, + FillerPresetViewModel midRollFiller, + FillerPresetViewModel postRollFiller, + FillerPresetViewModel tailFiller, + FillerPresetViewModel fallbackFiller) : base( + id, + index, + startType, + startTime, + PlayoutMode.Flood, + collectionType, + collection, + multiCollection, + smartCollection, + mediaItem, + playbackOrder, + customTitle, + guideMode, + preRollFiller, + midRollFiller, + postRollFiller, + tailFiller, + fallbackFiller) { - public ProgramScheduleItemFloodViewModel( - int id, - int index, - StartType startType, - TimeSpan? startTime, - ProgramScheduleItemCollectionType collectionType, - MediaCollectionViewModel collection, - MultiCollectionViewModel multiCollection, - SmartCollectionViewModel smartCollection, - NamedMediaItemViewModel mediaItem, - PlaybackOrder playbackOrder, - string customTitle, - GuideMode guideMode, - FillerPresetViewModel preRollFiller, - FillerPresetViewModel midRollFiller, - FillerPresetViewModel postRollFiller, - FillerPresetViewModel tailFiller, - FillerPresetViewModel fallbackFiller) : base( - id, - index, - startType, - startTime, - PlayoutMode.Flood, - collectionType, - collection, - multiCollection, - smartCollection, - mediaItem, - playbackOrder, - customTitle, - guideMode, - preRollFiller, - midRollFiller, - postRollFiller, - tailFiller, - fallbackFiller) - { - } } -} +} \ No newline at end of file diff --git a/ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemMultipleViewModel.cs b/ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemMultipleViewModel.cs index 1f8c149d5..7430c1875 100644 --- a/ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemMultipleViewModel.cs +++ b/ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemMultipleViewModel.cs @@ -1,52 +1,50 @@ -using System; -using ErsatzTV.Application.Filler; +using ErsatzTV.Application.Filler; using ErsatzTV.Application.MediaCollections; using ErsatzTV.Application.MediaItems; using ErsatzTV.Core.Domain; -namespace ErsatzTV.Application.ProgramSchedules -{ - public record ProgramScheduleItemMultipleViewModel : ProgramScheduleItemViewModel - { - public ProgramScheduleItemMultipleViewModel( - int id, - int index, - StartType startType, - TimeSpan? startTime, - ProgramScheduleItemCollectionType collectionType, - MediaCollectionViewModel collection, - MultiCollectionViewModel multiCollection, - SmartCollectionViewModel smartCollection, - NamedMediaItemViewModel mediaItem, - PlaybackOrder playbackOrder, - int count, - string customTitle, - GuideMode guideMode, - FillerPresetViewModel preRollFiller, - FillerPresetViewModel midRollFiller, - FillerPresetViewModel postRollFiller, - FillerPresetViewModel tailFiller, - FillerPresetViewModel fallbackFiller) : base( - id, - index, - startType, - startTime, - PlayoutMode.Multiple, - collectionType, - collection, - multiCollection, - smartCollection, - mediaItem, - playbackOrder, - customTitle, - guideMode, - preRollFiller, - midRollFiller, - postRollFiller, - tailFiller, - fallbackFiller) => - Count = count; +namespace ErsatzTV.Application.ProgramSchedules; - public int Count { get; } - } -} +public record ProgramScheduleItemMultipleViewModel : ProgramScheduleItemViewModel +{ + public ProgramScheduleItemMultipleViewModel( + int id, + int index, + StartType startType, + TimeSpan? startTime, + ProgramScheduleItemCollectionType collectionType, + MediaCollectionViewModel collection, + MultiCollectionViewModel multiCollection, + SmartCollectionViewModel smartCollection, + NamedMediaItemViewModel mediaItem, + PlaybackOrder playbackOrder, + int count, + string customTitle, + GuideMode guideMode, + FillerPresetViewModel preRollFiller, + FillerPresetViewModel midRollFiller, + FillerPresetViewModel postRollFiller, + FillerPresetViewModel tailFiller, + FillerPresetViewModel fallbackFiller) : base( + id, + index, + startType, + startTime, + PlayoutMode.Multiple, + collectionType, + collection, + multiCollection, + smartCollection, + mediaItem, + playbackOrder, + customTitle, + guideMode, + preRollFiller, + midRollFiller, + postRollFiller, + tailFiller, + fallbackFiller) => + Count = count; + + public int Count { get; } +} \ No newline at end of file diff --git a/ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemOneViewModel.cs b/ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemOneViewModel.cs index 08d80ecb1..e7577bc8c 100644 --- a/ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemOneViewModel.cs +++ b/ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemOneViewModel.cs @@ -1,50 +1,48 @@ -using System; -using ErsatzTV.Application.Filler; +using ErsatzTV.Application.Filler; using ErsatzTV.Application.MediaCollections; using ErsatzTV.Application.MediaItems; using ErsatzTV.Core.Domain; -namespace ErsatzTV.Application.ProgramSchedules +namespace ErsatzTV.Application.ProgramSchedules; + +public record ProgramScheduleItemOneViewModel : ProgramScheduleItemViewModel { - public record ProgramScheduleItemOneViewModel : ProgramScheduleItemViewModel + public ProgramScheduleItemOneViewModel( + int id, + int index, + StartType startType, + TimeSpan? startTime, + ProgramScheduleItemCollectionType collectionType, + MediaCollectionViewModel collection, + MultiCollectionViewModel multiCollection, + SmartCollectionViewModel smartCollection, + NamedMediaItemViewModel mediaItem, + PlaybackOrder playbackOrder, + string customTitle, + GuideMode guideMode, + FillerPresetViewModel preRollFiller, + FillerPresetViewModel midRollFiller, + FillerPresetViewModel postRollFiller, + FillerPresetViewModel tailFiller, + FillerPresetViewModel fallbackFiller) : base( + id, + index, + startType, + startTime, + PlayoutMode.One, + collectionType, + collection, + multiCollection, + smartCollection, + mediaItem, + playbackOrder, + customTitle, + guideMode, + preRollFiller, + midRollFiller, + postRollFiller, + tailFiller, + fallbackFiller) { - public ProgramScheduleItemOneViewModel( - int id, - int index, - StartType startType, - TimeSpan? startTime, - ProgramScheduleItemCollectionType collectionType, - MediaCollectionViewModel collection, - MultiCollectionViewModel multiCollection, - SmartCollectionViewModel smartCollection, - NamedMediaItemViewModel mediaItem, - PlaybackOrder playbackOrder, - string customTitle, - GuideMode guideMode, - FillerPresetViewModel preRollFiller, - FillerPresetViewModel midRollFiller, - FillerPresetViewModel postRollFiller, - FillerPresetViewModel tailFiller, - FillerPresetViewModel fallbackFiller) : base( - id, - index, - startType, - startTime, - PlayoutMode.One, - collectionType, - collection, - multiCollection, - smartCollection, - mediaItem, - playbackOrder, - customTitle, - guideMode, - preRollFiller, - midRollFiller, - postRollFiller, - tailFiller, - fallbackFiller) - { - } } -} +} \ No newline at end of file diff --git a/ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemViewModel.cs b/ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemViewModel.cs index cd84d8bf4..558384115 100644 --- a/ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemViewModel.cs +++ b/ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemViewModel.cs @@ -1,45 +1,43 @@ -using System; -using ErsatzTV.Application.Filler; +using ErsatzTV.Application.Filler; using ErsatzTV.Application.MediaCollections; using ErsatzTV.Application.MediaItems; using ErsatzTV.Core.Domain; -namespace ErsatzTV.Application.ProgramSchedules +namespace ErsatzTV.Application.ProgramSchedules; + +public abstract record ProgramScheduleItemViewModel( + int Id, + int Index, + StartType StartType, + TimeSpan? StartTime, + PlayoutMode PlayoutMode, + ProgramScheduleItemCollectionType CollectionType, + MediaCollectionViewModel Collection, + MultiCollectionViewModel MultiCollection, + SmartCollectionViewModel SmartCollection, + NamedMediaItemViewModel MediaItem, + PlaybackOrder PlaybackOrder, + string CustomTitle, + GuideMode GuideMode, + FillerPresetViewModel PreRollFiller, + FillerPresetViewModel MidRollFiller, + FillerPresetViewModel PostRollFiller, + FillerPresetViewModel TailFiller, + FillerPresetViewModel FallbackFiller) { - public abstract record ProgramScheduleItemViewModel( - int Id, - int Index, - StartType StartType, - TimeSpan? StartTime, - PlayoutMode PlayoutMode, - ProgramScheduleItemCollectionType CollectionType, - MediaCollectionViewModel Collection, - MultiCollectionViewModel MultiCollection, - SmartCollectionViewModel SmartCollection, - NamedMediaItemViewModel MediaItem, - PlaybackOrder PlaybackOrder, - string CustomTitle, - GuideMode GuideMode, - FillerPresetViewModel PreRollFiller, - FillerPresetViewModel MidRollFiller, - FillerPresetViewModel PostRollFiller, - FillerPresetViewModel TailFiller, - FillerPresetViewModel FallbackFiller) + public string Name => CollectionType switch { - public string Name => CollectionType switch - { - ProgramScheduleItemCollectionType.Collection => Collection?.Name, - ProgramScheduleItemCollectionType.TelevisionShow => - MediaItem?.Name, // $"{TelevisionShow?.Title} ({TelevisionShow?.Year})", - ProgramScheduleItemCollectionType.TelevisionSeason => - MediaItem?.Name, // $"{TelevisionSeason?.Title} ({TelevisionSeason?.Plot})", - ProgramScheduleItemCollectionType.Artist => - MediaItem?.Name, - ProgramScheduleItemCollectionType.MultiCollection => - MultiCollection?.Name, - ProgramScheduleItemCollectionType.SmartCollection => - SmartCollection?.Name, - _ => string.Empty - }; - } -} + ProgramScheduleItemCollectionType.Collection => Collection?.Name, + ProgramScheduleItemCollectionType.TelevisionShow => + MediaItem?.Name, // $"{TelevisionShow?.Title} ({TelevisionShow?.Year})", + ProgramScheduleItemCollectionType.TelevisionSeason => + MediaItem?.Name, // $"{TelevisionSeason?.Title} ({TelevisionSeason?.Plot})", + ProgramScheduleItemCollectionType.Artist => + MediaItem?.Name, + ProgramScheduleItemCollectionType.MultiCollection => + MultiCollection?.Name, + ProgramScheduleItemCollectionType.SmartCollection => + SmartCollection?.Name, + _ => string.Empty + }; +} \ No newline at end of file diff --git a/ErsatzTV.Application/ProgramSchedules/ProgramScheduleViewModel.cs b/ErsatzTV.Application/ProgramSchedules/ProgramScheduleViewModel.cs index 8840e1859..0202e221e 100644 --- a/ErsatzTV.Application/ProgramSchedules/ProgramScheduleViewModel.cs +++ b/ErsatzTV.Application/ProgramSchedules/ProgramScheduleViewModel.cs @@ -1,9 +1,8 @@ -namespace ErsatzTV.Application.ProgramSchedules -{ - public record ProgramScheduleViewModel( - int Id, - string Name, - bool KeepMultiPartEpisodesTogether, - bool TreatCollectionsAsShows, - bool ShuffleScheduleItems); -} +namespace ErsatzTV.Application.ProgramSchedules; + +public record ProgramScheduleViewModel( + int Id, + string Name, + bool KeepMultiPartEpisodesTogether, + bool TreatCollectionsAsShows, + bool ShuffleScheduleItems); \ No newline at end of file diff --git a/ErsatzTV.Application/ProgramSchedules/Queries/GetAllProgramSchedules.cs b/ErsatzTV.Application/ProgramSchedules/Queries/GetAllProgramSchedules.cs index db8553438..ce8d78131 100644 --- a/ErsatzTV.Application/ProgramSchedules/Queries/GetAllProgramSchedules.cs +++ b/ErsatzTV.Application/ProgramSchedules/Queries/GetAllProgramSchedules.cs @@ -1,7 +1,3 @@ -using System.Collections.Generic; -using MediatR; +namespace ErsatzTV.Application.ProgramSchedules; -namespace ErsatzTV.Application.ProgramSchedules.Queries -{ - public record GetAllProgramSchedules : IRequest>; -} +public record GetAllProgramSchedules : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/ProgramSchedules/Queries/GetAllProgramSchedulesHandler.cs b/ErsatzTV.Application/ProgramSchedules/Queries/GetAllProgramSchedulesHandler.cs index 3d024ac9d..d2879f0ea 100644 --- a/ErsatzTV.Application/ProgramSchedules/Queries/GetAllProgramSchedulesHandler.cs +++ b/ErsatzTV.Application/ProgramSchedules/Queries/GetAllProgramSchedulesHandler.cs @@ -1,33 +1,28 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Infrastructure.Data; -using MediatR; +using ErsatzTV.Infrastructure.Data; using Microsoft.EntityFrameworkCore; -namespace ErsatzTV.Application.ProgramSchedules.Queries +namespace ErsatzTV.Application.ProgramSchedules; + +public class GetAllProgramSchedulesHandler : IRequestHandler> { - public class GetAllProgramSchedulesHandler : IRequestHandler> + private readonly IDbContextFactory _dbContextFactory; + + public GetAllProgramSchedulesHandler(IDbContextFactory dbContextFactory) => + _dbContextFactory = dbContextFactory; + + public async Task> Handle( + GetAllProgramSchedules request, + CancellationToken cancellationToken) { - private readonly IDbContextFactory _dbContextFactory; - - public GetAllProgramSchedulesHandler(IDbContextFactory dbContextFactory) => - _dbContextFactory = dbContextFactory; - - public async Task> Handle( - GetAllProgramSchedules request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - return await dbContext.ProgramSchedules - .Map( - ps => new ProgramScheduleViewModel( - ps.Id, - ps.Name, - ps.KeepMultiPartEpisodesTogether, - ps.TreatCollectionsAsShows, - ps.ShuffleScheduleItems)) - .ToListAsync(cancellationToken); - } + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + return await dbContext.ProgramSchedules + .Map( + ps => new ProgramScheduleViewModel( + ps.Id, + ps.Name, + ps.KeepMultiPartEpisodesTogether, + ps.TreatCollectionsAsShows, + ps.ShuffleScheduleItems)) + .ToListAsync(cancellationToken); } -} +} \ No newline at end of file diff --git a/ErsatzTV.Application/ProgramSchedules/Queries/GetProgramScheduleById.cs b/ErsatzTV.Application/ProgramSchedules/Queries/GetProgramScheduleById.cs index 62265d366..cd0188d21 100644 --- a/ErsatzTV.Application/ProgramSchedules/Queries/GetProgramScheduleById.cs +++ b/ErsatzTV.Application/ProgramSchedules/Queries/GetProgramScheduleById.cs @@ -1,7 +1,3 @@ -using LanguageExt; -using MediatR; +namespace ErsatzTV.Application.ProgramSchedules; -namespace ErsatzTV.Application.ProgramSchedules.Queries -{ - public record GetProgramScheduleById(int Id) : IRequest>; -} +public record GetProgramScheduleById(int Id) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/ProgramSchedules/Queries/GetProgramScheduleByIdHandler.cs b/ErsatzTV.Application/ProgramSchedules/Queries/GetProgramScheduleByIdHandler.cs index b46664b5b..ff08a51b5 100644 --- a/ErsatzTV.Application/ProgramSchedules/Queries/GetProgramScheduleByIdHandler.cs +++ b/ErsatzTV.Application/ProgramSchedules/Queries/GetProgramScheduleByIdHandler.cs @@ -1,30 +1,25 @@ -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Infrastructure.Data; +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 +namespace ErsatzTV.Application.ProgramSchedules; + +public class GetProgramScheduleByIdHandler : + IRequestHandler> { - public class GetProgramScheduleByIdHandler : - IRequestHandler> + private readonly IDbContextFactory _dbContextFactory; + + public GetProgramScheduleByIdHandler(IDbContextFactory dbContextFactory) => + _dbContextFactory = dbContextFactory; + + public async Task> Handle( + GetProgramScheduleById request, + CancellationToken cancellationToken) { - private readonly IDbContextFactory _dbContextFactory; - - public GetProgramScheduleByIdHandler(IDbContextFactory dbContextFactory) => - _dbContextFactory = dbContextFactory; - - public async Task> Handle( - GetProgramScheduleById request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - return await dbContext.ProgramSchedules - .SelectOneAsync(ps => ps.Id, ps => ps.Id == request.Id) - .MapT(ProjectToViewModel); - } + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + return await dbContext.ProgramSchedules + .SelectOneAsync(ps => ps.Id, ps => ps.Id == request.Id) + .MapT(ProjectToViewModel); } -} +} \ No newline at end of file diff --git a/ErsatzTV.Application/ProgramSchedules/Queries/GetProgramScheduleItems.cs b/ErsatzTV.Application/ProgramSchedules/Queries/GetProgramScheduleItems.cs index 9659d054c..9cadcb96e 100644 --- a/ErsatzTV.Application/ProgramSchedules/Queries/GetProgramScheduleItems.cs +++ b/ErsatzTV.Application/ProgramSchedules/Queries/GetProgramScheduleItems.cs @@ -1,7 +1,3 @@ -using System.Collections.Generic; -using MediatR; +namespace ErsatzTV.Application.ProgramSchedules; -namespace ErsatzTV.Application.ProgramSchedules.Queries -{ - public record GetProgramScheduleItems(int Id) : IRequest>; -} +public record GetProgramScheduleItems(int Id) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/ProgramSchedules/Queries/GetProgramScheduleItemsHandler.cs b/ErsatzTV.Application/ProgramSchedules/Queries/GetProgramScheduleItemsHandler.cs index b5123bd8b..3bbb9f5c6 100644 --- a/ErsatzTV.Application/ProgramSchedules/Queries/GetProgramScheduleItemsHandler.cs +++ b/ErsatzTV.Application/ProgramSchedules/Queries/GetProgramScheduleItemsHandler.cs @@ -1,84 +1,77 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; 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 +namespace ErsatzTV.Application.ProgramSchedules; + +public class GetProgramScheduleItemsHandler : + IRequestHandler> { - public class GetProgramScheduleItemsHandler : - IRequestHandler> + private readonly IDbContextFactory _dbContextFactory; + + public GetProgramScheduleItemsHandler(IDbContextFactory dbContextFactory) => + _dbContextFactory = dbContextFactory; + + public async Task> Handle( + GetProgramScheduleItems request, + CancellationToken cancellationToken) { - private readonly IDbContextFactory _dbContextFactory; + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - public GetProgramScheduleItemsHandler(IDbContextFactory dbContextFactory) => - _dbContextFactory = dbContextFactory; + Option maybeProgramSchedule = + await dbContext.ProgramSchedules.SelectOneAsync(ps => ps.Id, ps => ps.Id == request.Id); - public async Task> Handle( - GetProgramScheduleItems request, - CancellationToken cancellationToken) + return await dbContext.ProgramScheduleItems + .Filter(psi => psi.ProgramScheduleId == request.Id) + .Include(i => i.Collection) + .Include(i => i.MultiCollection) + .Include(i => i.SmartCollection) + .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) + .Include(i => i.PreRollFiller) + .Include(i => i.MidRollFiller) + .Include(i => i.PostRollFiller) + .Include(i => i.TailFiller) + .Include(i => i.FallbackFiller) + .ToListAsync(cancellationToken) + .Map( + programScheduleItems => programScheduleItems.Map(ProjectToViewModel) + .Map(psi => EnforceProperties(maybeProgramSchedule, psi)).ToList()); + } + + // shuffled schedule items supports a limited set of properly values + private ProgramScheduleItemViewModel EnforceProperties( + Option maybeProgramSchedule, + ProgramScheduleItemViewModel item) + { + foreach (ProgramSchedule programSchedule in maybeProgramSchedule) { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - - Option maybeProgramSchedule = - await dbContext.ProgramSchedules.SelectOneAsync(ps => ps.Id, ps => ps.Id == request.Id); - - return await dbContext.ProgramScheduleItems - .Filter(psi => psi.ProgramScheduleId == request.Id) - .Include(i => i.Collection) - .Include(i => i.MultiCollection) - .Include(i => i.SmartCollection) - .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) - .Include(i => i.PreRollFiller) - .Include(i => i.MidRollFiller) - .Include(i => i.PostRollFiller) - .Include(i => i.TailFiller) - .Include(i => i.FallbackFiller) - .ToListAsync(cancellationToken) - .Map( - programScheduleItems => programScheduleItems.Map(ProjectToViewModel) - .Map(psi => EnforceProperties(maybeProgramSchedule, psi)).ToList()); - } - - // shuffled schedule items supports a limited set of properly values - private ProgramScheduleItemViewModel EnforceProperties( - Option maybeProgramSchedule, - ProgramScheduleItemViewModel item) - { - foreach (ProgramSchedule programSchedule in maybeProgramSchedule) + if (programSchedule.ShuffleScheduleItems) { - if (programSchedule.ShuffleScheduleItems) + item = item with { StartType = StartType.Dynamic }; + if (item.PlayoutMode == PlayoutMode.Flood) { - item = item with { StartType = StartType.Dynamic }; - if (item.PlayoutMode == PlayoutMode.Flood) - { - item = item with { PlayoutMode = PlayoutMode.One }; - } + item = item with { PlayoutMode = PlayoutMode.One }; } } - - return item; } + + return item; } -} +} \ No newline at end of file diff --git a/ErsatzTV.Application/Resolutions/FFmpegProfileResolutionViewModel.cs b/ErsatzTV.Application/Resolutions/FFmpegProfileResolutionViewModel.cs index 281055252..84eb02f97 100644 --- a/ErsatzTV.Application/Resolutions/FFmpegProfileResolutionViewModel.cs +++ b/ErsatzTV.Application/Resolutions/FFmpegProfileResolutionViewModel.cs @@ -1,4 +1,3 @@ -namespace ErsatzTV.Application.Resolutions -{ - public record ResolutionViewModel(int Id, string Name, int Width, int Height); -} +namespace ErsatzTV.Application.Resolutions; + +public record ResolutionViewModel(int Id, string Name, int Width, int Height); \ No newline at end of file diff --git a/ErsatzTV.Application/Resolutions/Mapper.cs b/ErsatzTV.Application/Resolutions/Mapper.cs index 47f6da43c..175d2b645 100644 --- a/ErsatzTV.Application/Resolutions/Mapper.cs +++ b/ErsatzTV.Application/Resolutions/Mapper.cs @@ -1,10 +1,9 @@ using ErsatzTV.Core.Domain; -namespace ErsatzTV.Application.Resolutions +namespace ErsatzTV.Application.Resolutions; + +internal static class Mapper { - internal static class Mapper - { - internal static ResolutionViewModel ProjectToViewModel(Resolution resolution) => - new(resolution.Id, resolution.Name, resolution.Width, resolution.Height); - } -} + internal static ResolutionViewModel ProjectToViewModel(Resolution resolution) => + new(resolution.Id, resolution.Name, resolution.Width, resolution.Height); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Resolutions/Queries/GetAllResolutions.cs b/ErsatzTV.Application/Resolutions/Queries/GetAllResolutions.cs index ff9ee80ac..95350905b 100644 --- a/ErsatzTV.Application/Resolutions/Queries/GetAllResolutions.cs +++ b/ErsatzTV.Application/Resolutions/Queries/GetAllResolutions.cs @@ -1,7 +1,3 @@ -using System.Collections.Generic; -using MediatR; +namespace ErsatzTV.Application.Resolutions; -namespace ErsatzTV.Application.Resolutions.Queries -{ - public record GetAllResolutions : IRequest>; -} +public record GetAllResolutions : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Resolutions/Queries/GetAllResolutionsHandler.cs b/ErsatzTV.Application/Resolutions/Queries/GetAllResolutionsHandler.cs index b702c8694..8780c8895 100644 --- a/ErsatzTV.Application/Resolutions/Queries/GetAllResolutionsHandler.cs +++ b/ErsatzTV.Application/Resolutions/Queries/GetAllResolutionsHandler.cs @@ -1,30 +1,23 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Infrastructure.Data; -using LanguageExt; -using MediatR; +using ErsatzTV.Infrastructure.Data; using Microsoft.EntityFrameworkCore; using static ErsatzTV.Application.Resolutions.Mapper; -namespace ErsatzTV.Application.Resolutions.Queries +namespace ErsatzTV.Application.Resolutions; + +public class GetAllResolutionsHandler : IRequestHandler> { - public class GetAllResolutionsHandler : IRequestHandler> + private readonly IDbContextFactory _dbContextFactory; + + public GetAllResolutionsHandler(IDbContextFactory dbContextFactory) => + _dbContextFactory = dbContextFactory; + + public async Task> Handle( + GetAllResolutions request, + CancellationToken cancellationToken) { - private readonly IDbContextFactory _dbContextFactory; - - public GetAllResolutionsHandler(IDbContextFactory dbContextFactory) => - _dbContextFactory = dbContextFactory; - - public async Task> Handle( - GetAllResolutions request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - return await dbContext.Resolutions - .ToListAsync(cancellationToken) - .Map(list => list.Map(ProjectToViewModel).ToList()); - } + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + return await dbContext.Resolutions + .ToListAsync(cancellationToken) + .Map(list => list.Map(ProjectToViewModel).ToList()); } -} +} \ No newline at end of file diff --git a/ErsatzTV.Application/Search/Commands/RebuildSearchIndex.cs b/ErsatzTV.Application/Search/Commands/RebuildSearchIndex.cs index 7eafb3e17..c74fb1185 100644 --- a/ErsatzTV.Application/Search/Commands/RebuildSearchIndex.cs +++ b/ErsatzTV.Application/Search/Commands/RebuildSearchIndex.cs @@ -1,6 +1,3 @@ -using LanguageExt; +namespace ErsatzTV.Application.Search; -namespace ErsatzTV.Application.Search.Commands -{ - public record RebuildSearchIndex : MediatR.IRequest, IBackgroundServiceRequest; -} +public record RebuildSearchIndex : MediatR.IRequest, IBackgroundServiceRequest; \ No newline at end of file diff --git a/ErsatzTV.Application/Search/Commands/RebuildSearchIndexHandler.cs b/ErsatzTV.Application/Search/Commands/RebuildSearchIndexHandler.cs index 360f60a65..195acc2ce 100644 --- a/ErsatzTV.Application/Search/Commands/RebuildSearchIndexHandler.cs +++ b/ErsatzTV.Application/Search/Commands/RebuildSearchIndexHandler.cs @@ -1,64 +1,58 @@ -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Search; -using LanguageExt; using Microsoft.Extensions.Logging; -namespace ErsatzTV.Application.Search.Commands +namespace ErsatzTV.Application.Search; + +public class RebuildSearchIndexHandler : MediatR.IRequestHandler { - public class RebuildSearchIndexHandler : MediatR.IRequestHandler + private readonly IConfigElementRepository _configElementRepository; + private readonly ILocalFileSystem _localFileSystem; + private readonly ILogger _logger; + private readonly ISearchIndex _searchIndex; + private readonly ISearchRepository _searchRepository; + + public RebuildSearchIndexHandler( + ISearchIndex searchIndex, + ISearchRepository searchRepository, + IConfigElementRepository configElementRepository, + ILocalFileSystem localFileSystem, + ILogger logger) { - private readonly IConfigElementRepository _configElementRepository; - private readonly ILocalFileSystem _localFileSystem; - private readonly ILogger _logger; - private readonly ISearchIndex _searchIndex; - private readonly ISearchRepository _searchRepository; - - public RebuildSearchIndexHandler( - ISearchIndex searchIndex, - ISearchRepository searchRepository, - IConfigElementRepository configElementRepository, - ILocalFileSystem localFileSystem, - ILogger logger) - { - _searchIndex = searchIndex; - _logger = logger; - _searchRepository = searchRepository; - _configElementRepository = configElementRepository; - _localFileSystem = localFileSystem; - } - - public async Task Handle(RebuildSearchIndex request, CancellationToken cancellationToken) - { - bool indexFolderExists = Directory.Exists(FileSystemLayout.SearchIndexFolder); - - await _searchIndex.Initialize(_localFileSystem); - - if (!indexFolderExists || - await _configElementRepository.GetValue(ConfigElementKey.SearchIndexVersion) < - _searchIndex.Version) - { - _logger.LogInformation("Migrating search index to version {Version}", _searchIndex.Version); - - List itemIds = await _searchRepository.GetItemIdsToIndex(); - await _searchIndex.Rebuild(_searchRepository, itemIds); - - await _configElementRepository.Upsert(ConfigElementKey.SearchIndexVersion, _searchIndex.Version); - - _logger.LogInformation("Done migrating search index"); - } - else - { - _logger.LogInformation("Search index is already version {Version}", _searchIndex.Version); - } - - return Unit.Default; - } + _searchIndex = searchIndex; + _logger = logger; + _searchRepository = searchRepository; + _configElementRepository = configElementRepository; + _localFileSystem = localFileSystem; } -} + + public async Task Handle(RebuildSearchIndex request, CancellationToken cancellationToken) + { + bool indexFolderExists = Directory.Exists(FileSystemLayout.SearchIndexFolder); + + await _searchIndex.Initialize(_localFileSystem); + + if (!indexFolderExists || + await _configElementRepository.GetValue(ConfigElementKey.SearchIndexVersion) < + _searchIndex.Version) + { + _logger.LogInformation("Migrating search index to version {Version}", _searchIndex.Version); + + List itemIds = await _searchRepository.GetItemIdsToIndex(); + await _searchIndex.Rebuild(_searchRepository, itemIds); + + await _configElementRepository.Upsert(ConfigElementKey.SearchIndexVersion, _searchIndex.Version); + + _logger.LogInformation("Done migrating search index"); + } + else + { + _logger.LogInformation("Search index is already version {Version}", _searchIndex.Version); + } + + return Unit.Default; + } +} \ No newline at end of file diff --git a/ErsatzTV.Application/Search/Queries/QuerySearchIndexAllItems.cs b/ErsatzTV.Application/Search/Queries/QuerySearchIndexAllItems.cs index f623432d8..5f75f11a1 100644 --- a/ErsatzTV.Application/Search/Queries/QuerySearchIndexAllItems.cs +++ b/ErsatzTV.Application/Search/Queries/QuerySearchIndexAllItems.cs @@ -1,6 +1,3 @@ -using MediatR; +namespace ErsatzTV.Application.Search; -namespace ErsatzTV.Application.Search.Queries -{ - public record QuerySearchIndexAllItems(string Query) : IRequest; -} +public record QuerySearchIndexAllItems(string Query) : IRequest; \ No newline at end of file diff --git a/ErsatzTV.Application/Search/Queries/QuerySearchIndexAllItemsHandler.cs b/ErsatzTV.Application/Search/Queries/QuerySearchIndexAllItemsHandler.cs index ea479be87..d025948b8 100644 --- a/ErsatzTV.Application/Search/Queries/QuerySearchIndexAllItemsHandler.cs +++ b/ErsatzTV.Application/Search/Queries/QuerySearchIndexAllItemsHandler.cs @@ -1,36 +1,29 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core.Interfaces.Search; +using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Infrastructure.Search; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.Search.Queries +namespace ErsatzTV.Application.Search; + +public class + QuerySearchIndexAllItemsHandler : IRequestHandler { - public class - QuerySearchIndexAllItemsHandler : IRequestHandler - { - private readonly ISearchIndex _searchIndex; + private readonly ISearchIndex _searchIndex; - public QuerySearchIndexAllItemsHandler(ISearchIndex searchIndex) => _searchIndex = searchIndex; + public QuerySearchIndexAllItemsHandler(ISearchIndex searchIndex) => _searchIndex = searchIndex; - public async Task Handle( - QuerySearchIndexAllItems request, - CancellationToken cancellationToken) => - new( - await GetIds(SearchIndex.MovieType, request.Query), - await GetIds(SearchIndex.ShowType, request.Query), - await GetIds(SearchIndex.SeasonType, request.Query), - await GetIds(SearchIndex.EpisodeType, request.Query), - await GetIds(SearchIndex.ArtistType, request.Query), - await GetIds(SearchIndex.MusicVideoType, request.Query), - await GetIds(SearchIndex.OtherVideoType, request.Query), - await GetIds(SearchIndex.SongType, request.Query)); + public async Task Handle( + QuerySearchIndexAllItems request, + CancellationToken cancellationToken) => + new( + await GetIds(SearchIndex.MovieType, request.Query), + await GetIds(SearchIndex.ShowType, request.Query), + await GetIds(SearchIndex.SeasonType, request.Query), + await GetIds(SearchIndex.EpisodeType, request.Query), + await GetIds(SearchIndex.ArtistType, request.Query), + await GetIds(SearchIndex.MusicVideoType, request.Query), + await GetIds(SearchIndex.OtherVideoType, request.Query), + await GetIds(SearchIndex.SongType, request.Query)); - private Task> GetIds(string type, string query) => - _searchIndex.Search($"type:{type} AND ({query})", 0, 0) - .Map(result => result.Items.Map(i => i.Id).ToList()); - } -} + private Task> GetIds(string type, string query) => + _searchIndex.Search($"type:{type} AND ({query})", 0, 0) + .Map(result => result.Items.Map(i => i.Id).ToList()); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Search/Queries/QuerySearchIndexArtists.cs b/ErsatzTV.Application/Search/Queries/QuerySearchIndexArtists.cs index 1ba2b5114..10aafce28 100644 --- a/ErsatzTV.Application/Search/Queries/QuerySearchIndexArtists.cs +++ b/ErsatzTV.Application/Search/Queries/QuerySearchIndexArtists.cs @@ -1,8 +1,6 @@ using ErsatzTV.Application.MediaCards; -using MediatR; -namespace ErsatzTV.Application.Search.Queries -{ - public record QuerySearchIndexArtists - (string Query, int PageNumber, int PageSize) : IRequest; -} +namespace ErsatzTV.Application.Search; + +public record QuerySearchIndexArtists + (string Query, int PageNumber, int PageSize) : IRequest; \ No newline at end of file diff --git a/ErsatzTV.Application/Search/Queries/QuerySearchIndexArtistsHandler.cs b/ErsatzTV.Application/Search/Queries/QuerySearchIndexArtistsHandler.cs index 126ea4e66..d4c3d0373 100644 --- a/ErsatzTV.Application/Search/Queries/QuerySearchIndexArtistsHandler.cs +++ b/ErsatzTV.Application/Search/Queries/QuerySearchIndexArtistsHandler.cs @@ -1,44 +1,37 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Application.MediaCards; +using ErsatzTV.Application.MediaCards; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Search; -using LanguageExt; -using MediatR; using static ErsatzTV.Application.MediaCards.Mapper; -namespace ErsatzTV.Application.Search.Queries +namespace ErsatzTV.Application.Search; + +public class + QuerySearchIndexArtistsHandler : IRequestHandler { - public class - QuerySearchIndexArtistsHandler : IRequestHandler + private readonly IArtistRepository _artistRepository; + private readonly ISearchIndex _searchIndex; + + public QuerySearchIndexArtistsHandler(ISearchIndex searchIndex, IArtistRepository artistRepository) { - private readonly IArtistRepository _artistRepository; - private readonly ISearchIndex _searchIndex; - - public QuerySearchIndexArtistsHandler(ISearchIndex searchIndex, IArtistRepository artistRepository) - { - _searchIndex = searchIndex; - _artistRepository = artistRepository; - } - - public async Task Handle( - QuerySearchIndexArtists request, - CancellationToken cancellationToken) - { - SearchResult searchResult = await _searchIndex.Search( - request.Query, - (request.PageNumber - 1) * request.PageSize, - request.PageSize); - - List items = await _artistRepository - .GetArtistsForCards(searchResult.Items.Map(i => i.Id).ToList()) - .Map(list => list.Map(ProjectToViewModel).ToList()); - - return new ArtistCardResultsViewModel(searchResult.TotalCount, items, searchResult.PageMap); - } + _searchIndex = searchIndex; + _artistRepository = artistRepository; } -} + + public async Task Handle( + QuerySearchIndexArtists request, + CancellationToken cancellationToken) + { + SearchResult searchResult = await _searchIndex.Search( + request.Query, + (request.PageNumber - 1) * request.PageSize, + request.PageSize); + + List items = await _artistRepository + .GetArtistsForCards(searchResult.Items.Map(i => i.Id).ToList()) + .Map(list => list.Map(ProjectToViewModel).ToList()); + + return new ArtistCardResultsViewModel(searchResult.TotalCount, items, searchResult.PageMap); + } +} \ No newline at end of file diff --git a/ErsatzTV.Application/Search/Queries/QuerySearchIndexEpisodes.cs b/ErsatzTV.Application/Search/Queries/QuerySearchIndexEpisodes.cs index 5d0622865..e733f1887 100644 --- a/ErsatzTV.Application/Search/Queries/QuerySearchIndexEpisodes.cs +++ b/ErsatzTV.Application/Search/Queries/QuerySearchIndexEpisodes.cs @@ -1,8 +1,6 @@ using ErsatzTV.Application.MediaCards; -using MediatR; -namespace ErsatzTV.Application.Search.Queries -{ - public record QuerySearchIndexEpisodes - (string Query, int PageNumber, int PageSize) : IRequest; -} +namespace ErsatzTV.Application.Search; + +public record QuerySearchIndexEpisodes + (string Query, int PageNumber, int PageSize) : IRequest; \ No newline at end of file diff --git a/ErsatzTV.Application/Search/Queries/QuerySearchIndexEpisodesHandler.cs b/ErsatzTV.Application/Search/Queries/QuerySearchIndexEpisodesHandler.cs index dea6db740..49f64a3d8 100644 --- a/ErsatzTV.Application/Search/Queries/QuerySearchIndexEpisodesHandler.cs +++ b/ErsatzTV.Application/Search/Queries/QuerySearchIndexEpisodesHandler.cs @@ -1,56 +1,49 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Application.MediaCards; +using ErsatzTV.Application.MediaCards; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Search; -using LanguageExt; -using MediatR; using static ErsatzTV.Application.MediaCards.Mapper; -namespace ErsatzTV.Application.Search.Queries +namespace ErsatzTV.Application.Search; + +public class + QuerySearchIndexEpisodesHandler : IRequestHandler { - public class - QuerySearchIndexEpisodesHandler : IRequestHandler + private readonly IMediaSourceRepository _mediaSourceRepository; + private readonly ISearchIndex _searchIndex; + private readonly ITelevisionRepository _televisionRepository; + + public QuerySearchIndexEpisodesHandler( + ISearchIndex searchIndex, + ITelevisionRepository televisionRepository, + IMediaSourceRepository mediaSourceRepository) { - private readonly IMediaSourceRepository _mediaSourceRepository; - private readonly ISearchIndex _searchIndex; - private readonly ITelevisionRepository _televisionRepository; - - public QuerySearchIndexEpisodesHandler( - ISearchIndex searchIndex, - ITelevisionRepository televisionRepository, - IMediaSourceRepository mediaSourceRepository) - { - _searchIndex = searchIndex; - _televisionRepository = televisionRepository; - _mediaSourceRepository = mediaSourceRepository; - } - - public async Task Handle( - QuerySearchIndexEpisodes request, - CancellationToken cancellationToken) - { - SearchResult searchResult = await _searchIndex.Search( - request.Query, - (request.PageNumber - 1) * request.PageSize, - request.PageSize); - - Option maybeJellyfin = await _mediaSourceRepository.GetAllJellyfin() - .Map(list => list.HeadOrNone()); - - Option maybeEmby = await _mediaSourceRepository.GetAllEmby() - .Map(list => list.HeadOrNone()); - - List items = await _televisionRepository - .GetEpisodesForCards(searchResult.Items.Map(i => i.Id).ToList()) - .Map(list => list.Map(s => ProjectToViewModel(s, maybeJellyfin, maybeEmby, true)).ToList()); - - return new TelevisionEpisodeCardResultsViewModel(searchResult.TotalCount, items, searchResult.PageMap); - } + _searchIndex = searchIndex; + _televisionRepository = televisionRepository; + _mediaSourceRepository = mediaSourceRepository; } -} + + public async Task Handle( + QuerySearchIndexEpisodes request, + CancellationToken cancellationToken) + { + SearchResult searchResult = await _searchIndex.Search( + request.Query, + (request.PageNumber - 1) * request.PageSize, + request.PageSize); + + Option maybeJellyfin = await _mediaSourceRepository.GetAllJellyfin() + .Map(list => list.HeadOrNone()); + + Option maybeEmby = await _mediaSourceRepository.GetAllEmby() + .Map(list => list.HeadOrNone()); + + List items = await _televisionRepository + .GetEpisodesForCards(searchResult.Items.Map(i => i.Id).ToList()) + .Map(list => list.Map(s => ProjectToViewModel(s, maybeJellyfin, maybeEmby, true)).ToList()); + + return new TelevisionEpisodeCardResultsViewModel(searchResult.TotalCount, items, searchResult.PageMap); + } +} \ No newline at end of file diff --git a/ErsatzTV.Application/Search/Queries/QuerySearchIndexMovies.cs b/ErsatzTV.Application/Search/Queries/QuerySearchIndexMovies.cs index 19b2a37c1..0e60cf7ef 100644 --- a/ErsatzTV.Application/Search/Queries/QuerySearchIndexMovies.cs +++ b/ErsatzTV.Application/Search/Queries/QuerySearchIndexMovies.cs @@ -1,8 +1,6 @@ using ErsatzTV.Application.MediaCards; -using MediatR; -namespace ErsatzTV.Application.Search.Queries -{ - public record QuerySearchIndexMovies - (string Query, int PageNumber, int PageSize) : IRequest; -} +namespace ErsatzTV.Application.Search; + +public record QuerySearchIndexMovies + (string Query, int PageNumber, int PageSize) : IRequest; \ No newline at end of file diff --git a/ErsatzTV.Application/Search/Queries/QuerySearchIndexMoviesHandler.cs b/ErsatzTV.Application/Search/Queries/QuerySearchIndexMoviesHandler.cs index 1823ac336..e0468afb2 100644 --- a/ErsatzTV.Application/Search/Queries/QuerySearchIndexMoviesHandler.cs +++ b/ErsatzTV.Application/Search/Queries/QuerySearchIndexMoviesHandler.cs @@ -1,54 +1,47 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Application.MediaCards; +using ErsatzTV.Application.MediaCards; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Search; -using LanguageExt; -using MediatR; using static ErsatzTV.Application.MediaCards.Mapper; -namespace ErsatzTV.Application.Search.Queries +namespace ErsatzTV.Application.Search; + +public class QuerySearchIndexMoviesHandler : IRequestHandler { - public class QuerySearchIndexMoviesHandler : IRequestHandler + private readonly IMediaSourceRepository _mediaSourceRepository; + private readonly IMovieRepository _movieRepository; + private readonly ISearchIndex _searchIndex; + + public QuerySearchIndexMoviesHandler( + ISearchIndex searchIndex, + IMovieRepository movieRepository, + IMediaSourceRepository mediaSourceRepository) { - private readonly IMediaSourceRepository _mediaSourceRepository; - private readonly IMovieRepository _movieRepository; - private readonly ISearchIndex _searchIndex; - - public QuerySearchIndexMoviesHandler( - ISearchIndex searchIndex, - IMovieRepository movieRepository, - IMediaSourceRepository mediaSourceRepository) - { - _searchIndex = searchIndex; - _movieRepository = movieRepository; - _mediaSourceRepository = mediaSourceRepository; - } - - public async Task Handle( - QuerySearchIndexMovies request, - CancellationToken cancellationToken) - { - SearchResult searchResult = await _searchIndex.Search( - request.Query, - (request.PageNumber - 1) * request.PageSize, - request.PageSize); - - Option maybeJellyfin = await _mediaSourceRepository.GetAllJellyfin() - .Map(list => list.HeadOrNone()); - - Option maybeEmby = await _mediaSourceRepository.GetAllEmby() - .Map(list => list.HeadOrNone()); - - List items = await _movieRepository - .GetMoviesForCards(searchResult.Items.Map(i => i.Id).ToList()) - .Map(list => list.Map(m => ProjectToViewModel(m, maybeJellyfin, maybeEmby)).ToList()); - - return new MovieCardResultsViewModel(searchResult.TotalCount, items, searchResult.PageMap); - } + _searchIndex = searchIndex; + _movieRepository = movieRepository; + _mediaSourceRepository = mediaSourceRepository; } -} + + public async Task Handle( + QuerySearchIndexMovies request, + CancellationToken cancellationToken) + { + SearchResult searchResult = await _searchIndex.Search( + request.Query, + (request.PageNumber - 1) * request.PageSize, + request.PageSize); + + Option maybeJellyfin = await _mediaSourceRepository.GetAllJellyfin() + .Map(list => list.HeadOrNone()); + + Option maybeEmby = await _mediaSourceRepository.GetAllEmby() + .Map(list => list.HeadOrNone()); + + List items = await _movieRepository + .GetMoviesForCards(searchResult.Items.Map(i => i.Id).ToList()) + .Map(list => list.Map(m => ProjectToViewModel(m, maybeJellyfin, maybeEmby)).ToList()); + + return new MovieCardResultsViewModel(searchResult.TotalCount, items, searchResult.PageMap); + } +} \ No newline at end of file diff --git a/ErsatzTV.Application/Search/Queries/QuerySearchIndexMusicVideos.cs b/ErsatzTV.Application/Search/Queries/QuerySearchIndexMusicVideos.cs index 8d7fd6df5..0b96bb2ab 100644 --- a/ErsatzTV.Application/Search/Queries/QuerySearchIndexMusicVideos.cs +++ b/ErsatzTV.Application/Search/Queries/QuerySearchIndexMusicVideos.cs @@ -1,8 +1,6 @@ using ErsatzTV.Application.MediaCards; -using MediatR; -namespace ErsatzTV.Application.Search.Queries -{ - public record QuerySearchIndexMusicVideos - (string Query, int PageNumber, int PageSize) : IRequest; -} +namespace ErsatzTV.Application.Search; + +public record QuerySearchIndexMusicVideos + (string Query, int PageNumber, int PageSize) : IRequest; \ No newline at end of file diff --git a/ErsatzTV.Application/Search/Queries/QuerySearchIndexMusicVideosHandler.cs b/ErsatzTV.Application/Search/Queries/QuerySearchIndexMusicVideosHandler.cs index 59739565c..4bfe5dfed 100644 --- a/ErsatzTV.Application/Search/Queries/QuerySearchIndexMusicVideosHandler.cs +++ b/ErsatzTV.Application/Search/Queries/QuerySearchIndexMusicVideosHandler.cs @@ -1,44 +1,37 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Application.MediaCards; +using ErsatzTV.Application.MediaCards; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Search; -using LanguageExt; -using MediatR; using static ErsatzTV.Application.MediaCards.Mapper; -namespace ErsatzTV.Application.Search.Queries +namespace ErsatzTV.Application.Search; + +public class + QuerySearchIndexMusicVideosHandler : IRequestHandler { - public class - QuerySearchIndexMusicVideosHandler : IRequestHandler + private readonly IMusicVideoRepository _musicVideoRepository; + private readonly ISearchIndex _searchIndex; + + public QuerySearchIndexMusicVideosHandler(ISearchIndex searchIndex, IMusicVideoRepository musicVideoRepository) { - private readonly IMusicVideoRepository _musicVideoRepository; - private readonly ISearchIndex _searchIndex; - - public QuerySearchIndexMusicVideosHandler(ISearchIndex searchIndex, IMusicVideoRepository musicVideoRepository) - { - _searchIndex = searchIndex; - _musicVideoRepository = musicVideoRepository; - } - - public async Task Handle( - QuerySearchIndexMusicVideos request, - CancellationToken cancellationToken) - { - SearchResult searchResult = await _searchIndex.Search( - request.Query, - (request.PageNumber - 1) * request.PageSize, - request.PageSize); - - List items = await _musicVideoRepository - .GetMusicVideosForCards(searchResult.Items.Map(i => i.Id).ToList()) - .Map(list => list.Map(ProjectToViewModel).ToList()); - - return new MusicVideoCardResultsViewModel(searchResult.TotalCount, items, searchResult.PageMap); - } + _searchIndex = searchIndex; + _musicVideoRepository = musicVideoRepository; } -} + + public async Task Handle( + QuerySearchIndexMusicVideos request, + CancellationToken cancellationToken) + { + SearchResult searchResult = await _searchIndex.Search( + request.Query, + (request.PageNumber - 1) * request.PageSize, + request.PageSize); + + List items = await _musicVideoRepository + .GetMusicVideosForCards(searchResult.Items.Map(i => i.Id).ToList()) + .Map(list => list.Map(ProjectToViewModel).ToList()); + + return new MusicVideoCardResultsViewModel(searchResult.TotalCount, items, searchResult.PageMap); + } +} \ No newline at end of file diff --git a/ErsatzTV.Application/Search/Queries/QuerySearchIndexOtherVideos.cs b/ErsatzTV.Application/Search/Queries/QuerySearchIndexOtherVideos.cs index f1cae681e..5c3071051 100644 --- a/ErsatzTV.Application/Search/Queries/QuerySearchIndexOtherVideos.cs +++ b/ErsatzTV.Application/Search/Queries/QuerySearchIndexOtherVideos.cs @@ -1,8 +1,6 @@ using ErsatzTV.Application.MediaCards; -using MediatR; -namespace ErsatzTV.Application.Search.Queries -{ - public record QuerySearchIndexOtherVideos - (string Query, int PageNumber, int PageSize) : IRequest; -} +namespace ErsatzTV.Application.Search; + +public record QuerySearchIndexOtherVideos + (string Query, int PageNumber, int PageSize) : IRequest; \ No newline at end of file diff --git a/ErsatzTV.Application/Search/Queries/QuerySearchIndexOtherVideosHandler.cs b/ErsatzTV.Application/Search/Queries/QuerySearchIndexOtherVideosHandler.cs index 109329ff8..c8ddcfd2f 100644 --- a/ErsatzTV.Application/Search/Queries/QuerySearchIndexOtherVideosHandler.cs +++ b/ErsatzTV.Application/Search/Queries/QuerySearchIndexOtherVideosHandler.cs @@ -1,44 +1,37 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Application.MediaCards; +using ErsatzTV.Application.MediaCards; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Search; -using LanguageExt; -using MediatR; using static ErsatzTV.Application.MediaCards.Mapper; -namespace ErsatzTV.Application.Search.Queries +namespace ErsatzTV.Application.Search; + +public class + QuerySearchIndexOtherVideosHandler : IRequestHandler { - public class - QuerySearchIndexOtherVideosHandler : IRequestHandler + private readonly IOtherVideoRepository _otherVideoRepository; + private readonly ISearchIndex _searchIndex; + + public QuerySearchIndexOtherVideosHandler(ISearchIndex searchIndex, IOtherVideoRepository otherVideoRepository) { - private readonly IOtherVideoRepository _otherVideoRepository; - private readonly ISearchIndex _searchIndex; - - public QuerySearchIndexOtherVideosHandler(ISearchIndex searchIndex, IOtherVideoRepository otherVideoRepository) - { - _searchIndex = searchIndex; - _otherVideoRepository = otherVideoRepository; - } - - public async Task Handle( - QuerySearchIndexOtherVideos request, - CancellationToken cancellationToken) - { - SearchResult searchResult = await _searchIndex.Search( - request.Query, - (request.PageNumber - 1) * request.PageSize, - request.PageSize); - - List items = await _otherVideoRepository - .GetOtherVideosForCards(searchResult.Items.Map(i => i.Id).ToList()) - .Map(list => list.Map(ProjectToViewModel).ToList()); - - return new OtherVideoCardResultsViewModel(searchResult.TotalCount, items, searchResult.PageMap); - } + _searchIndex = searchIndex; + _otherVideoRepository = otherVideoRepository; } -} + + public async Task Handle( + QuerySearchIndexOtherVideos request, + CancellationToken cancellationToken) + { + SearchResult searchResult = await _searchIndex.Search( + request.Query, + (request.PageNumber - 1) * request.PageSize, + request.PageSize); + + List items = await _otherVideoRepository + .GetOtherVideosForCards(searchResult.Items.Map(i => i.Id).ToList()) + .Map(list => list.Map(ProjectToViewModel).ToList()); + + return new OtherVideoCardResultsViewModel(searchResult.TotalCount, items, searchResult.PageMap); + } +} \ No newline at end of file diff --git a/ErsatzTV.Application/Search/Queries/QuerySearchIndexSeasons.cs b/ErsatzTV.Application/Search/Queries/QuerySearchIndexSeasons.cs index 573607283..8b1494a30 100644 --- a/ErsatzTV.Application/Search/Queries/QuerySearchIndexSeasons.cs +++ b/ErsatzTV.Application/Search/Queries/QuerySearchIndexSeasons.cs @@ -1,8 +1,6 @@ using ErsatzTV.Application.MediaCards; -using MediatR; -namespace ErsatzTV.Application.Search.Queries -{ - public record QuerySearchIndexSeasons - (string Query, int PageNumber, int PageSize) : IRequest; -} +namespace ErsatzTV.Application.Search; + +public record QuerySearchIndexSeasons + (string Query, int PageNumber, int PageSize) : IRequest; \ No newline at end of file diff --git a/ErsatzTV.Application/Search/Queries/QuerySearchIndexSeasonsHandler.cs b/ErsatzTV.Application/Search/Queries/QuerySearchIndexSeasonsHandler.cs index 1f5530e50..dab5115db 100644 --- a/ErsatzTV.Application/Search/Queries/QuerySearchIndexSeasonsHandler.cs +++ b/ErsatzTV.Application/Search/Queries/QuerySearchIndexSeasonsHandler.cs @@ -1,55 +1,48 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Application.MediaCards; +using ErsatzTV.Application.MediaCards; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Search; -using LanguageExt; -using MediatR; using static ErsatzTV.Application.MediaCards.Mapper; -namespace ErsatzTV.Application.Search.Queries +namespace ErsatzTV.Application.Search; + +public class + QuerySearchIndexSeasonsHandler : IRequestHandler { - public class - QuerySearchIndexSeasonsHandler : IRequestHandler + private readonly IMediaSourceRepository _mediaSourceRepository; + private readonly ISearchIndex _searchIndex; + private readonly ITelevisionRepository _televisionRepository; + + public QuerySearchIndexSeasonsHandler( + ISearchIndex searchIndex, + ITelevisionRepository televisionRepository, + IMediaSourceRepository mediaSourceRepository) { - private readonly IMediaSourceRepository _mediaSourceRepository; - private readonly ISearchIndex _searchIndex; - private readonly ITelevisionRepository _televisionRepository; - - public QuerySearchIndexSeasonsHandler( - ISearchIndex searchIndex, - ITelevisionRepository televisionRepository, - IMediaSourceRepository mediaSourceRepository) - { - _searchIndex = searchIndex; - _televisionRepository = televisionRepository; - _mediaSourceRepository = mediaSourceRepository; - } - - public async Task Handle( - QuerySearchIndexSeasons request, - CancellationToken cancellationToken) - { - SearchResult searchResult = await _searchIndex.Search( - request.Query, - (request.PageNumber - 1) * request.PageSize, - request.PageSize); - - Option maybeJellyfin = await _mediaSourceRepository.GetAllJellyfin() - .Map(list => list.HeadOrNone()); - - Option maybeEmby = await _mediaSourceRepository.GetAllEmby() - .Map(list => list.HeadOrNone()); - - List items = await _televisionRepository - .GetSeasonsForCards(searchResult.Items.Map(i => i.Id).ToList()) - .Map(list => list.Map(s => ProjectToViewModel(s, maybeJellyfin, maybeEmby)).ToList()); - - return new TelevisionSeasonCardResultsViewModel(searchResult.TotalCount, items, searchResult.PageMap); - } + _searchIndex = searchIndex; + _televisionRepository = televisionRepository; + _mediaSourceRepository = mediaSourceRepository; } -} + + public async Task Handle( + QuerySearchIndexSeasons request, + CancellationToken cancellationToken) + { + SearchResult searchResult = await _searchIndex.Search( + request.Query, + (request.PageNumber - 1) * request.PageSize, + request.PageSize); + + Option maybeJellyfin = await _mediaSourceRepository.GetAllJellyfin() + .Map(list => list.HeadOrNone()); + + Option maybeEmby = await _mediaSourceRepository.GetAllEmby() + .Map(list => list.HeadOrNone()); + + List items = await _televisionRepository + .GetSeasonsForCards(searchResult.Items.Map(i => i.Id).ToList()) + .Map(list => list.Map(s => ProjectToViewModel(s, maybeJellyfin, maybeEmby)).ToList()); + + return new TelevisionSeasonCardResultsViewModel(searchResult.TotalCount, items, searchResult.PageMap); + } +} \ No newline at end of file diff --git a/ErsatzTV.Application/Search/Queries/QuerySearchIndexShows.cs b/ErsatzTV.Application/Search/Queries/QuerySearchIndexShows.cs index 156a1af48..a9e66cc47 100644 --- a/ErsatzTV.Application/Search/Queries/QuerySearchIndexShows.cs +++ b/ErsatzTV.Application/Search/Queries/QuerySearchIndexShows.cs @@ -1,8 +1,6 @@ using ErsatzTV.Application.MediaCards; -using MediatR; -namespace ErsatzTV.Application.Search.Queries -{ - public record QuerySearchIndexShows - (string Query, int PageNumber, int PageSize) : IRequest; -} +namespace ErsatzTV.Application.Search; + +public record QuerySearchIndexShows + (string Query, int PageNumber, int PageSize) : IRequest; \ No newline at end of file diff --git a/ErsatzTV.Application/Search/Queries/QuerySearchIndexShowsHandler.cs b/ErsatzTV.Application/Search/Queries/QuerySearchIndexShowsHandler.cs index c95d97f6a..4369fdf55 100644 --- a/ErsatzTV.Application/Search/Queries/QuerySearchIndexShowsHandler.cs +++ b/ErsatzTV.Application/Search/Queries/QuerySearchIndexShowsHandler.cs @@ -1,55 +1,48 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Application.MediaCards; +using ErsatzTV.Application.MediaCards; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Search; -using LanguageExt; -using MediatR; using static ErsatzTV.Application.MediaCards.Mapper; -namespace ErsatzTV.Application.Search.Queries +namespace ErsatzTV.Application.Search; + +public class + QuerySearchIndexShowsHandler : IRequestHandler { - public class - QuerySearchIndexShowsHandler : IRequestHandler + private readonly IMediaSourceRepository _mediaSourceRepository; + private readonly ISearchIndex _searchIndex; + private readonly ITelevisionRepository _televisionRepository; + + public QuerySearchIndexShowsHandler( + ISearchIndex searchIndex, + ITelevisionRepository televisionRepository, + IMediaSourceRepository mediaSourceRepository) { - private readonly IMediaSourceRepository _mediaSourceRepository; - private readonly ISearchIndex _searchIndex; - private readonly ITelevisionRepository _televisionRepository; - - public QuerySearchIndexShowsHandler( - ISearchIndex searchIndex, - ITelevisionRepository televisionRepository, - IMediaSourceRepository mediaSourceRepository) - { - _searchIndex = searchIndex; - _televisionRepository = televisionRepository; - _mediaSourceRepository = mediaSourceRepository; - } - - public async Task Handle( - QuerySearchIndexShows request, - CancellationToken cancellationToken) - { - SearchResult searchResult = await _searchIndex.Search( - request.Query, - (request.PageNumber - 1) * request.PageSize, - request.PageSize); - - Option maybeJellyfin = await _mediaSourceRepository.GetAllJellyfin() - .Map(list => list.HeadOrNone()); - - Option maybeEmby = await _mediaSourceRepository.GetAllEmby() - .Map(list => list.HeadOrNone()); - - List items = await _televisionRepository - .GetShowsForCards(searchResult.Items.Map(i => i.Id).ToList()) - .Map(list => list.Map(s => ProjectToViewModel(s, maybeJellyfin, maybeEmby)).ToList()); - - return new TelevisionShowCardResultsViewModel(searchResult.TotalCount, items, searchResult.PageMap); - } + _searchIndex = searchIndex; + _televisionRepository = televisionRepository; + _mediaSourceRepository = mediaSourceRepository; } -} + + public async Task Handle( + QuerySearchIndexShows request, + CancellationToken cancellationToken) + { + SearchResult searchResult = await _searchIndex.Search( + request.Query, + (request.PageNumber - 1) * request.PageSize, + request.PageSize); + + Option maybeJellyfin = await _mediaSourceRepository.GetAllJellyfin() + .Map(list => list.HeadOrNone()); + + Option maybeEmby = await _mediaSourceRepository.GetAllEmby() + .Map(list => list.HeadOrNone()); + + List items = await _televisionRepository + .GetShowsForCards(searchResult.Items.Map(i => i.Id).ToList()) + .Map(list => list.Map(s => ProjectToViewModel(s, maybeJellyfin, maybeEmby)).ToList()); + + return new TelevisionShowCardResultsViewModel(searchResult.TotalCount, items, searchResult.PageMap); + } +} \ No newline at end of file diff --git a/ErsatzTV.Application/Search/Queries/QuerySearchIndexSongs.cs b/ErsatzTV.Application/Search/Queries/QuerySearchIndexSongs.cs index 713325acd..0331495b6 100644 --- a/ErsatzTV.Application/Search/Queries/QuerySearchIndexSongs.cs +++ b/ErsatzTV.Application/Search/Queries/QuerySearchIndexSongs.cs @@ -1,8 +1,6 @@ using ErsatzTV.Application.MediaCards; -using MediatR; -namespace ErsatzTV.Application.Search.Queries -{ - public record QuerySearchIndexSongs - (string Query, int PageNumber, int PageSize) : IRequest; -} +namespace ErsatzTV.Application.Search; + +public record QuerySearchIndexSongs + (string Query, int PageNumber, int PageSize) : IRequest; \ No newline at end of file diff --git a/ErsatzTV.Application/Search/Queries/QuerySearchIndexSongsHandler.cs b/ErsatzTV.Application/Search/Queries/QuerySearchIndexSongsHandler.cs index 2f4455384..50f4be1b5 100644 --- a/ErsatzTV.Application/Search/Queries/QuerySearchIndexSongsHandler.cs +++ b/ErsatzTV.Application/Search/Queries/QuerySearchIndexSongsHandler.cs @@ -1,44 +1,37 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Application.MediaCards; +using ErsatzTV.Application.MediaCards; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Search; -using LanguageExt; -using MediatR; using static ErsatzTV.Application.MediaCards.Mapper; -namespace ErsatzTV.Application.Search.Queries +namespace ErsatzTV.Application.Search; + +public class + QuerySearchIndexSongsHandler : IRequestHandler { - public class - QuerySearchIndexSongsHandler : IRequestHandler + private readonly ISongRepository _songRepository; + private readonly ISearchIndex _searchIndex; + + public QuerySearchIndexSongsHandler(ISearchIndex searchIndex, ISongRepository songRepository) { - private readonly ISongRepository _songRepository; - private readonly ISearchIndex _searchIndex; - - public QuerySearchIndexSongsHandler(ISearchIndex searchIndex, ISongRepository songRepository) - { - _searchIndex = searchIndex; - _songRepository = songRepository; - } - - public async Task Handle( - QuerySearchIndexSongs request, - CancellationToken cancellationToken) - { - SearchResult searchResult = await _searchIndex.Search( - request.Query, - (request.PageNumber - 1) * request.PageSize, - request.PageSize); - - List items = await _songRepository - .GetSongsForCards(searchResult.Items.Map(i => i.Id).ToList()) - .Map(list => list.Map(ProjectToViewModel).ToList()); - - return new SongCardResultsViewModel(searchResult.TotalCount, items, searchResult.PageMap); - } + _searchIndex = searchIndex; + _songRepository = songRepository; } -} + + public async Task Handle( + QuerySearchIndexSongs request, + CancellationToken cancellationToken) + { + SearchResult searchResult = await _searchIndex.Search( + request.Query, + (request.PageNumber - 1) * request.PageSize, + request.PageSize); + + List items = await _songRepository + .GetSongsForCards(searchResult.Items.Map(i => i.Id).ToList()) + .Map(list => list.Map(ProjectToViewModel).ToList()); + + return new SongCardResultsViewModel(searchResult.TotalCount, items, searchResult.PageMap); + } +} \ No newline at end of file diff --git a/ErsatzTV.Application/Search/SearchResultAllItemsViewModel.cs b/ErsatzTV.Application/Search/SearchResultAllItemsViewModel.cs index 6d5f8a2f9..87b2e544c 100644 --- a/ErsatzTV.Application/Search/SearchResultAllItemsViewModel.cs +++ b/ErsatzTV.Application/Search/SearchResultAllItemsViewModel.cs @@ -1,14 +1,11 @@ -using System.Collections.Generic; +namespace ErsatzTV.Application.Search; -namespace ErsatzTV.Application.Search -{ - public record SearchResultAllItemsViewModel( - List MovieIds, - List ShowIds, - List SeasonIds, - List EpisodeIds, - List ArtistIds, - List MusicVideoIds, - List OtherVideoIds, - List SongIds); -} +public record SearchResultAllItemsViewModel( + List MovieIds, + List ShowIds, + List SeasonIds, + List EpisodeIds, + List ArtistIds, + List MusicVideoIds, + List OtherVideoIds, + List SongIds); \ No newline at end of file diff --git a/ErsatzTV.Application/Search/SearchResultViewModel.cs b/ErsatzTV.Application/Search/SearchResultViewModel.cs index 59e487b9a..22ca86360 100644 --- a/ErsatzTV.Application/Search/SearchResultViewModel.cs +++ b/ErsatzTV.Application/Search/SearchResultViewModel.cs @@ -1,10 +1,7 @@ -using System.Collections.Generic; +namespace ErsatzTV.Application.Search; -namespace ErsatzTV.Application.Search +public class SearchResultViewModel { - public class SearchResultViewModel - { - public int TotalCount { get; set; } - public List Items { get; set; } - } -} + public int TotalCount { get; set; } + public List Items { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Application/Streaming/Commands/StartFFmpegSession.cs b/ErsatzTV.Application/Streaming/Commands/StartFFmpegSession.cs index 18bee0dc7..60892b0fe 100644 --- a/ErsatzTV.Application/Streaming/Commands/StartFFmpegSession.cs +++ b/ErsatzTV.Application/Streaming/Commands/StartFFmpegSession.cs @@ -1,9 +1,7 @@ using ErsatzTV.Core; -using LanguageExt; -namespace ErsatzTV.Application.Streaming.Commands -{ - public record StartFFmpegSession(string ChannelNumber, bool StartAtZero) : - MediatR.IRequest>, - IFFmpegWorkerRequest; -} +namespace ErsatzTV.Application.Streaming; + +public record StartFFmpegSession(string ChannelNumber, bool StartAtZero) : + MediatR.IRequest>, + IFFmpegWorkerRequest; \ No newline at end of file diff --git a/ErsatzTV.Application/Streaming/Commands/StartFFmpegSessionHandler.cs b/ErsatzTV.Application/Streaming/Commands/StartFFmpegSessionHandler.cs index a76db4b68..52bb5a4d3 100644 --- a/ErsatzTV.Application/Streaming/Commands/StartFFmpegSessionHandler.cs +++ b/ErsatzTV.Application/Streaming/Commands/StartFFmpegSessionHandler.cs @@ -1,135 +1,128 @@ -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Errors; using ErsatzTV.Core.FFmpeg; using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using static LanguageExt.Prelude; -namespace ErsatzTV.Application.Streaming.Commands +namespace ErsatzTV.Application.Streaming; + +public class StartFFmpegSessionHandler : MediatR.IRequestHandler> { - public class StartFFmpegSessionHandler : MediatR.IRequestHandler> + private readonly ILogger _logger; + private readonly IServiceScopeFactory _serviceScopeFactory; + private readonly IFFmpegSegmenterService _ffmpegSegmenterService; + private readonly IConfigElementRepository _configElementRepository; + private readonly IHlsPlaylistFilter _hlsPlaylistFilter; + private readonly ILocalFileSystem _localFileSystem; + + public StartFFmpegSessionHandler( + ILocalFileSystem localFileSystem, + ILogger logger, + IServiceScopeFactory serviceScopeFactory, + IFFmpegSegmenterService ffmpegSegmenterService, + IConfigElementRepository configElementRepository, + IHlsPlaylistFilter hlsPlaylistFilter) { - private readonly ILogger _logger; - private readonly IServiceScopeFactory _serviceScopeFactory; - private readonly IFFmpegSegmenterService _ffmpegSegmenterService; - private readonly IConfigElementRepository _configElementRepository; - private readonly IHlsPlaylistFilter _hlsPlaylistFilter; - private readonly ILocalFileSystem _localFileSystem; + _localFileSystem = localFileSystem; + _logger = logger; + _serviceScopeFactory = serviceScopeFactory; + _ffmpegSegmenterService = ffmpegSegmenterService; + _configElementRepository = configElementRepository; + _hlsPlaylistFilter = hlsPlaylistFilter; + } - public StartFFmpegSessionHandler( - ILocalFileSystem localFileSystem, - ILogger logger, - IServiceScopeFactory serviceScopeFactory, - IFFmpegSegmenterService ffmpegSegmenterService, - IConfigElementRepository configElementRepository, - IHlsPlaylistFilter hlsPlaylistFilter) - { - _localFileSystem = localFileSystem; - _logger = logger; - _serviceScopeFactory = serviceScopeFactory; - _ffmpegSegmenterService = ffmpegSegmenterService; - _configElementRepository = configElementRepository; - _hlsPlaylistFilter = hlsPlaylistFilter; - } - - public Task> Handle(StartFFmpegSession request, CancellationToken cancellationToken) => - Validate(request) - .MapT(_ => StartProcess(request)) - // this weirdness is needed to maintain the error type (.ToEitherAsync() just gives BaseError) + public Task> Handle(StartFFmpegSession request, CancellationToken cancellationToken) => + Validate(request) + .MapT(_ => StartProcess(request)) + // this weirdness is needed to maintain the error type (.ToEitherAsync() just gives BaseError) #pragma warning disable VSTHRD103 - .Bind(v => v.ToEither().MapLeft(seq => seq.Head()).MapAsync, Unit>(identity)); + .Bind(v => v.ToEither().MapLeft(seq => seq.Head()).MapAsync, Unit>(identity)); #pragma warning restore VSTHRD103 - private async Task StartProcess(StartFFmpegSession request) + private async Task StartProcess(StartFFmpegSession request) + { + TimeSpan idleTimeout = await _configElementRepository + .GetValue(ConfigElementKey.FFmpegSegmenterTimeout) + .Map(maybeTimeout => maybeTimeout.Match(i => TimeSpan.FromSeconds(i), () => TimeSpan.FromMinutes(1))); + + using IServiceScope scope = _serviceScopeFactory.CreateScope(); + HlsSessionWorker worker = scope.ServiceProvider.GetRequiredService(); + _ffmpegSegmenterService.SessionWorkers.AddOrUpdate(request.ChannelNumber, _ => worker, (_, _) => worker); + + // fire and forget worker + _ = worker.Run(request.ChannelNumber, idleTimeout) + .ContinueWith( + _ => _ffmpegSegmenterService.SessionWorkers.TryRemove( + request.ChannelNumber, + out IHlsSessionWorker _), + TaskScheduler.Default); + + string playlistFileName = Path.Combine( + FileSystemLayout.TranscodeFolder, + request.ChannelNumber, + "live.m3u8"); + + IConfigElementRepository repo = scope.ServiceProvider.GetRequiredService(); + int initialSegmentCount = await repo.GetValue(ConfigElementKey.FFmpegInitialSegmentCount) + .Map(maybeCount => maybeCount.Match(identity, () => 1)); + + await WaitForPlaylistSegments(playlistFileName, initialSegmentCount, worker); + + return Unit.Default; + } + + private async Task WaitForPlaylistSegments(string playlistFileName, int initialSegmentCount, IHlsSessionWorker worker) + { + while (!File.Exists(playlistFileName)) { - TimeSpan idleTimeout = await _configElementRepository - .GetValue(ConfigElementKey.FFmpegSegmenterTimeout) - .Map(maybeTimeout => maybeTimeout.Match(i => TimeSpan.FromSeconds(i), () => TimeSpan.FromMinutes(1))); - - using IServiceScope scope = _serviceScopeFactory.CreateScope(); - HlsSessionWorker worker = scope.ServiceProvider.GetRequiredService(); - _ffmpegSegmenterService.SessionWorkers.AddOrUpdate(request.ChannelNumber, _ => worker, (_, _) => worker); - - // fire and forget worker - _ = worker.Run(request.ChannelNumber, idleTimeout) - .ContinueWith( - _ => _ffmpegSegmenterService.SessionWorkers.TryRemove( - request.ChannelNumber, - out IHlsSessionWorker _), - TaskScheduler.Default); - - string playlistFileName = Path.Combine( - FileSystemLayout.TranscodeFolder, - request.ChannelNumber, - "live.m3u8"); - - IConfigElementRepository repo = scope.ServiceProvider.GetRequiredService(); - int initialSegmentCount = await repo.GetValue(ConfigElementKey.FFmpegInitialSegmentCount) - .Map(maybeCount => maybeCount.Match(identity, () => 1)); - - await WaitForPlaylistSegments(playlistFileName, initialSegmentCount, worker); - - return Unit.Default; + await Task.Delay(TimeSpan.FromMilliseconds(100)); } - private async Task WaitForPlaylistSegments(string playlistFileName, int initialSegmentCount, IHlsSessionWorker worker) + var segmentCount = 0; + while (segmentCount < initialSegmentCount) { - while (!File.Exists(playlistFileName)) - { - await Task.Delay(TimeSpan.FromMilliseconds(100)); - } - - var segmentCount = 0; - while (segmentCount < initialSegmentCount) - { - await Task.Delay(TimeSpan.FromMilliseconds(200)); + await Task.Delay(TimeSpan.FromMilliseconds(200)); - DateTimeOffset now = DateTimeOffset.Now.AddSeconds(-30); - string[] input = await File.ReadAllLinesAsync(playlistFileName); - TrimPlaylistResult result = _hlsPlaylistFilter.TrimPlaylist(worker.PlaylistStart, now, input); - segmentCount = result.SegmentCount; - } - } - - private Task> Validate(StartFFmpegSession request) => - SessionMustBeInactive(request) - .BindT(_ => FolderMustBeEmpty(request)); - - private Task> SessionMustBeInactive(StartFFmpegSession request) - { - var result = Optional(_ffmpegSegmenterService.SessionWorkers.TryAdd(request.ChannelNumber, null)) - .Where(success => success) - .Map(_ => Unit.Default) - .ToValidation(new ChannelSessionAlreadyActive()); - - if (result.IsFail && _ffmpegSegmenterService.SessionWorkers.TryGetValue( - request.ChannelNumber, - out IHlsSessionWorker worker)) - { - worker?.Touch(); - } - - return result.AsTask(); - } - - private Task> FolderMustBeEmpty(StartFFmpegSession request) - { - string folder = Path.Combine(FileSystemLayout.TranscodeFolder, request.ChannelNumber); - _logger.LogDebug("Preparing transcode folder {Folder}", folder); - - _localFileSystem.EnsureFolderExists(folder); - _localFileSystem.EmptyFolder(folder); - - return Task.FromResult>(Unit.Default); + DateTimeOffset now = DateTimeOffset.Now.AddSeconds(-30); + string[] input = await File.ReadAllLinesAsync(playlistFileName); + TrimPlaylistResult result = _hlsPlaylistFilter.TrimPlaylist(worker.PlaylistStart, now, input); + segmentCount = result.SegmentCount; } } -} + + private Task> Validate(StartFFmpegSession request) => + SessionMustBeInactive(request) + .BindT(_ => FolderMustBeEmpty(request)); + + private Task> SessionMustBeInactive(StartFFmpegSession request) + { + var result = Optional(_ffmpegSegmenterService.SessionWorkers.TryAdd(request.ChannelNumber, null)) + .Where(success => success) + .Map(_ => Unit.Default) + .ToValidation(new ChannelSessionAlreadyActive()); + + if (result.IsFail && _ffmpegSegmenterService.SessionWorkers.TryGetValue( + request.ChannelNumber, + out IHlsSessionWorker worker)) + { + worker?.Touch(); + } + + return result.AsTask(); + } + + private Task> FolderMustBeEmpty(StartFFmpegSession request) + { + string folder = Path.Combine(FileSystemLayout.TranscodeFolder, request.ChannelNumber); + _logger.LogDebug("Preparing transcode folder {Folder}", folder); + + _localFileSystem.EnsureFolderExists(folder); + _localFileSystem.EmptyFolder(folder); + + return Task.FromResult>(Unit.Default); + } +} \ No newline at end of file diff --git a/ErsatzTV.Application/Streaming/Commands/TouchFFmpegSession.cs b/ErsatzTV.Application/Streaming/Commands/TouchFFmpegSession.cs index 34d0d5a42..02888f5ee 100644 --- a/ErsatzTV.Application/Streaming/Commands/TouchFFmpegSession.cs +++ b/ErsatzTV.Application/Streaming/Commands/TouchFFmpegSession.cs @@ -1,9 +1,5 @@ using ErsatzTV.Core; -using LanguageExt; -using MediatR; -using Unit = LanguageExt.Unit; -namespace ErsatzTV.Application.Streaming.Commands -{ - public record TouchFFmpegSession(string Path) : IRequest>, IFFmpegWorkerRequest; -} +namespace ErsatzTV.Application.Streaming; + +public record TouchFFmpegSession(string Path) : IRequest>, IFFmpegWorkerRequest; \ No newline at end of file diff --git a/ErsatzTV.Application/Streaming/HlsSessionWorker.cs b/ErsatzTV.Application/Streaming/HlsSessionWorker.cs index dce27c778..a2f7b97c1 100644 --- a/ErsatzTV.Application/Streaming/HlsSessionWorker.cs +++ b/ErsatzTV.Application/Streaming/HlsSessionWorker.cs @@ -1,303 +1,293 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; +using System.Diagnostics; using System.Timers; -using ErsatzTV.Application.Channels.Queries; -using ErsatzTV.Application.Streaming.Queries; +using ErsatzTV.Application.Channels; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.FFmpeg; using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -using MediatR; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Timer = System.Timers.Timer; -using static LanguageExt.Prelude; -namespace ErsatzTV.Application.Streaming +namespace ErsatzTV.Application.Streaming; + +public class HlsSessionWorker : IHlsSessionWorker { - public class HlsSessionWorker : IHlsSessionWorker + private static int _workAheadCount; + private readonly IHlsPlaylistFilter _hlsPlaylistFilter; + private readonly IServiceScopeFactory _serviceScopeFactory; + private readonly ILogger _logger; + private DateTimeOffset _lastAccess; + private DateTimeOffset _transcodedUntil; + private Timer _timer; + private readonly object _sync = new(); + private DateTimeOffset _playlistStart; + private Option _targetFramerate; + + public HlsSessionWorker(IHlsPlaylistFilter hlsPlaylistFilter, IServiceScopeFactory serviceScopeFactory, ILogger logger) { - private static int _workAheadCount; - private readonly IHlsPlaylistFilter _hlsPlaylistFilter; - private readonly IServiceScopeFactory _serviceScopeFactory; - private readonly ILogger _logger; - private DateTimeOffset _lastAccess; - private DateTimeOffset _transcodedUntil; - private Timer _timer; - private readonly object _sync = new(); - private DateTimeOffset _playlistStart; - private Option _targetFramerate; + _hlsPlaylistFilter = hlsPlaylistFilter; + _serviceScopeFactory = serviceScopeFactory; + _logger = logger; + } - public HlsSessionWorker(IHlsPlaylistFilter hlsPlaylistFilter, IServiceScopeFactory serviceScopeFactory, ILogger logger) + public DateTimeOffset PlaylistStart => _playlistStart; + + public void Touch() + { + lock (_sync) { - _hlsPlaylistFilter = hlsPlaylistFilter; - _serviceScopeFactory = serviceScopeFactory; - _logger = logger; + _lastAccess = DateTimeOffset.Now; + + _timer?.Stop(); + _timer?.Start(); } + } - public DateTimeOffset PlaylistStart => _playlistStart; + public async Task Run(string channelNumber, TimeSpan idleTimeout) + { + var cts = new CancellationTokenSource(); + void Cancel(object o, ElapsedEventArgs e) => cts.Cancel(); - public void Touch() + try { lock (_sync) { - _lastAccess = DateTimeOffset.Now; - - _timer?.Stop(); - _timer?.Start(); + _timer = new Timer(idleTimeout.TotalMilliseconds) { AutoReset = false }; + _timer.Elapsed += Cancel; } - } - public async Task Run(string channelNumber, TimeSpan idleTimeout) - { - var cts = new CancellationTokenSource(); - void Cancel(object o, ElapsedEventArgs e) => cts.Cancel(); + CancellationToken cancellationToken = cts.Token; - try - { - lock (_sync) - { - _timer = new Timer(idleTimeout.TotalMilliseconds) { AutoReset = false }; - _timer.Elapsed += Cancel; - } - - CancellationToken cancellationToken = cts.Token; - - _logger.LogInformation("Starting HLS session for channel {Channel}", channelNumber); + _logger.LogInformation("Starting HLS session for channel {Channel}", channelNumber); - using IServiceScope scope = _serviceScopeFactory.CreateScope(); - IMediator mediator = scope.ServiceProvider.GetRequiredService(); + using IServiceScope scope = _serviceScopeFactory.CreateScope(); + IMediator mediator = scope.ServiceProvider.GetRequiredService(); - _targetFramerate = await mediator.Send( - new GetChannelFramerate(channelNumber), - cancellationToken); + _targetFramerate = await mediator.Send( + new GetChannelFramerate(channelNumber), + cancellationToken); - Touch(); - _transcodedUntil = DateTimeOffset.Now; - _playlistStart = _transcodedUntil; + Touch(); + _transcodedUntil = DateTimeOffset.Now; + _playlistStart = _transcodedUntil; - bool initialWorkAhead = Volatile.Read(ref _workAheadCount) < await GetWorkAheadLimit(); - if (!await Transcode(channelNumber, true, !initialWorkAhead, cancellationToken)) + bool initialWorkAhead = Volatile.Read(ref _workAheadCount) < await GetWorkAheadLimit(); + if (!await Transcode(channelNumber, true, !initialWorkAhead, cancellationToken)) + { + return; + } + + while (!cancellationToken.IsCancellationRequested) + { + if (DateTimeOffset.Now - _lastAccess > idleTimeout) { + _logger.LogInformation("Stopping idle HLS session for channel {Channel}", channelNumber); return; } - while (!cancellationToken.IsCancellationRequested) + var transcodedBuffer = TimeSpan.FromSeconds( + Math.Max(0, _transcodedUntil.Subtract(DateTimeOffset.Now).TotalSeconds)); + if (transcodedBuffer <= TimeSpan.FromMinutes(1)) { - if (DateTimeOffset.Now - _lastAccess > idleTimeout) + // only use realtime encoding when we're at least 30 seconds ahead + bool realtime = transcodedBuffer >= TimeSpan.FromSeconds(30); + bool subsequentWorkAhead = + !realtime && Volatile.Read(ref _workAheadCount) < await GetWorkAheadLimit(); + if (!await Transcode(channelNumber, false, !subsequentWorkAhead, cancellationToken)) { - _logger.LogInformation("Stopping idle HLS session for channel {Channel}", channelNumber); return; } - - var transcodedBuffer = TimeSpan.FromSeconds( - Math.Max(0, _transcodedUntil.Subtract(DateTimeOffset.Now).TotalSeconds)); - if (transcodedBuffer <= TimeSpan.FromMinutes(1)) - { - // only use realtime encoding when we're at least 30 seconds ahead - bool realtime = transcodedBuffer >= TimeSpan.FromSeconds(30); - bool subsequentWorkAhead = - !realtime && Volatile.Read(ref _workAheadCount) < await GetWorkAheadLimit(); - if (!await Transcode(channelNumber, false, !subsequentWorkAhead, cancellationToken)) - { - return; - } - } - else - { - await TrimAndDelete(channelNumber, cancellationToken); - await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken); - } - } - } - finally - { - lock (_sync) - { - _timer.Elapsed -= Cancel; - } - } - } - - private async Task Transcode( - string channelNumber, - bool firstProcess, - bool realtime, - CancellationToken cancellationToken) - { - try - { - if (!realtime) - { - Interlocked.Increment(ref _workAheadCount); - _logger.LogInformation("HLS segmenter will work ahead for channel {Channel}", channelNumber); } else { - _logger.LogInformation( - "HLS segmenter will NOT work ahead for channel {Channel}", - channelNumber); + await TrimAndDelete(channelNumber, cancellationToken); + await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken); } + } + } + finally + { + lock (_sync) + { + _timer.Elapsed -= Cancel; + } + } + } - using IServiceScope scope = _serviceScopeFactory.CreateScope(); - IMediator mediator = scope.ServiceProvider.GetRequiredService(); + private async Task Transcode( + string channelNumber, + bool firstProcess, + bool realtime, + CancellationToken cancellationToken) + { + try + { + if (!realtime) + { + Interlocked.Increment(ref _workAheadCount); + _logger.LogInformation("HLS segmenter will work ahead for channel {Channel}", channelNumber); + } + else + { + _logger.LogInformation( + "HLS segmenter will NOT work ahead for channel {Channel}", + channelNumber); + } - long ptsOffset = await GetPtsOffset(mediator, channelNumber, cancellationToken); - // _logger.LogInformation("PTS offset: {PtsOffset}", ptsOffset); + using IServiceScope scope = _serviceScopeFactory.CreateScope(); + IMediator mediator = scope.ServiceProvider.GetRequiredService(); - var request = new GetPlayoutItemProcessByChannelNumber( + long ptsOffset = await GetPtsOffset(mediator, channelNumber, cancellationToken); + // _logger.LogInformation("PTS offset: {PtsOffset}", ptsOffset); + + var request = new GetPlayoutItemProcessByChannelNumber( + channelNumber, + "segmenter", + firstProcess ? DateTimeOffset.Now : _transcodedUntil.AddSeconds(1), + !firstProcess, + realtime, + ptsOffset, + _targetFramerate); + + // _logger.LogInformation("Request {@Request}", request); + + Either result = await mediator.Send(request, cancellationToken); + + // _logger.LogInformation("Result {Result}", result.ToString()); + + foreach (BaseError error in result.LeftAsEnumerable()) + { + _logger.LogWarning( + "Failed to create process for HLS session on channel {Channel}: {Error}", channelNumber, - "segmenter", - firstProcess ? DateTimeOffset.Now : _transcodedUntil.AddSeconds(1), - !firstProcess, - realtime, - ptsOffset, - _targetFramerate); + error.ToString()); - // _logger.LogInformation("Request {@Request}", request); + return false; + } - Either result = await mediator.Send(request, cancellationToken); + foreach (PlayoutItemProcessModel processModel in result.RightAsEnumerable()) + { + await TrimAndDelete(channelNumber, cancellationToken); - // _logger.LogInformation("Result {Result}", result.ToString()); + Process process = processModel.Process; - foreach (BaseError error in result.LeftAsEnumerable()) + _logger.LogInformation( + "ffmpeg hls arguments {FFmpegArguments}", + string.Join(" ", process.StartInfo.ArgumentList)); + + process.Start(); + try { - _logger.LogWarning( - "Failed to create process for HLS session on channel {Channel}: {Error}", - channelNumber, - error.ToString()); + await process.WaitForExitAsync(cancellationToken); + process.WaitForExit(); + } + catch (TaskCanceledException) + { + _logger.LogInformation("Terminating HLS process for channel {Channel}", channelNumber); + process.Kill(); + process.WaitForExit(); return false; } - foreach (PlayoutItemProcessModel processModel in result.RightAsEnumerable()) - { - await TrimAndDelete(channelNumber, cancellationToken); + _logger.LogInformation("HLS process has completed for channel {Channel}", channelNumber); - Process process = processModel.Process; - - _logger.LogInformation( - "ffmpeg hls arguments {FFmpegArguments}", - string.Join(" ", process.StartInfo.ArgumentList)); - - process.Start(); - try - { - await process.WaitForExitAsync(cancellationToken); - process.WaitForExit(); - } - catch (TaskCanceledException) - { - _logger.LogInformation("Terminating HLS process for channel {Channel}", channelNumber); - process.Kill(); - process.WaitForExit(); - - return false; - } - - _logger.LogInformation("HLS process has completed for channel {Channel}", channelNumber); - - _transcodedUntil = processModel.Until; - } + _transcodedUntil = processModel.Until; } - catch (Exception ex) - { - _logger.LogError(ex, "Error transcoding channel {Channel}", channelNumber); - return false; - } - finally - { - Interlocked.Decrement(ref _workAheadCount); - } - - return true; } - - private async Task TrimAndDelete(string channelNumber, CancellationToken cancellationToken) + catch (Exception ex) { - string playlistFileName = Path.Combine( - FileSystemLayout.TranscodeFolder, - channelNumber, - "live.m3u8"); - - if (File.Exists(playlistFileName)) - { - // trim playlist and insert discontinuity before appending with new ffmpeg process - string[] lines = await File.ReadAllLinesAsync(playlistFileName, cancellationToken); - TrimPlaylistResult trimResult = _hlsPlaylistFilter.TrimPlaylistWithDiscontinuity( - _playlistStart, - DateTimeOffset.Now.AddMinutes(-1), - lines); - await File.WriteAllTextAsync(playlistFileName, trimResult.Playlist, cancellationToken); - - // delete old segments - var allSegments = Directory.GetFiles( - Path.Combine(FileSystemLayout.TranscodeFolder, channelNumber), - "live*.ts") - .Map( - file => - { - string fileName = Path.GetFileName(file); - var sequenceNumber = int.Parse(fileName.Replace("live", string.Empty).Split('.')[0]); - return new Segment(file, sequenceNumber); - }) - .ToList(); - - var toDelete = allSegments.Filter(s => s.SequenceNumber < trimResult.Sequence).ToList(); - // if (toDelete.Count > 0) - // { - // _logger.LogInformation( - // "Deleting HLS segments {Min} to {Max} (less than {StartSequence})", - // toDelete.Map(s => s.SequenceNumber).Min(), - // toDelete.Map(s => s.SequenceNumber).Max(), - // trimResult.Sequence); - // } - - foreach (Segment segment in toDelete) - { - File.Delete(segment.File); - } - - _playlistStart = trimResult.PlaylistStart; - } + _logger.LogError(ex, "Error transcoding channel {Channel}", channelNumber); + return false; } - - private async Task GetPtsOffset(IMediator mediator, string channelNumber, CancellationToken cancellationToken) + finally { - var directory = new DirectoryInfo(Path.Combine(FileSystemLayout.TranscodeFolder, channelNumber)); - Option lastSegment = - Optional(directory.GetFiles("*.ts").OrderByDescending(f => f.Name).FirstOrDefault()); - - long result = 0; - foreach (FileInfo segment in lastSegment) - { - Either queryResult = await mediator.Send( - new GetLastPtsDuration(segment.FullName), - cancellationToken); - - foreach (PtsAndDuration ptsAndDuration in queryResult.RightToSeq()) - { - result = ptsAndDuration.Pts + ptsAndDuration.Duration; - } - } - - return result; + Interlocked.Decrement(ref _workAheadCount); } - private async Task GetWorkAheadLimit() - { - using IServiceScope scope = _serviceScopeFactory.CreateScope(); - IConfigElementRepository repo = scope.ServiceProvider.GetRequiredService(); - return await repo.GetValue(ConfigElementKey.FFmpegWorkAheadSegmenters) - .Map(maybeCount => maybeCount.Match(identity, () => 1)); - } - - private record Segment(string File, int SequenceNumber); + return true; } -} + + private async Task TrimAndDelete(string channelNumber, CancellationToken cancellationToken) + { + string playlistFileName = Path.Combine( + FileSystemLayout.TranscodeFolder, + channelNumber, + "live.m3u8"); + + if (File.Exists(playlistFileName)) + { + // trim playlist and insert discontinuity before appending with new ffmpeg process + string[] lines = await File.ReadAllLinesAsync(playlistFileName, cancellationToken); + TrimPlaylistResult trimResult = _hlsPlaylistFilter.TrimPlaylistWithDiscontinuity( + _playlistStart, + DateTimeOffset.Now.AddMinutes(-1), + lines); + await File.WriteAllTextAsync(playlistFileName, trimResult.Playlist, cancellationToken); + + // delete old segments + var allSegments = Directory.GetFiles( + Path.Combine(FileSystemLayout.TranscodeFolder, channelNumber), + "live*.ts") + .Map( + file => + { + string fileName = Path.GetFileName(file); + var sequenceNumber = int.Parse(fileName.Replace("live", string.Empty).Split('.')[0]); + return new Segment(file, sequenceNumber); + }) + .ToList(); + + var toDelete = allSegments.Filter(s => s.SequenceNumber < trimResult.Sequence).ToList(); + // if (toDelete.Count > 0) + // { + // _logger.LogInformation( + // "Deleting HLS segments {Min} to {Max} (less than {StartSequence})", + // toDelete.Map(s => s.SequenceNumber).Min(), + // toDelete.Map(s => s.SequenceNumber).Max(), + // trimResult.Sequence); + // } + + foreach (Segment segment in toDelete) + { + File.Delete(segment.File); + } + + _playlistStart = trimResult.PlaylistStart; + } + } + + private async Task GetPtsOffset(IMediator mediator, string channelNumber, CancellationToken cancellationToken) + { + var directory = new DirectoryInfo(Path.Combine(FileSystemLayout.TranscodeFolder, channelNumber)); + Option lastSegment = + Optional(directory.GetFiles("*.ts").OrderByDescending(f => f.Name).FirstOrDefault()); + + long result = 0; + foreach (FileInfo segment in lastSegment) + { + Either queryResult = await mediator.Send( + new GetLastPtsDuration(segment.FullName), + cancellationToken); + + foreach (PtsAndDuration ptsAndDuration in queryResult.RightToSeq()) + { + result = ptsAndDuration.Pts + ptsAndDuration.Duration; + } + } + + return result; + } + + private async Task GetWorkAheadLimit() + { + using IServiceScope scope = _serviceScopeFactory.CreateScope(); + IConfigElementRepository repo = scope.ServiceProvider.GetRequiredService(); + return await repo.GetValue(ConfigElementKey.FFmpegWorkAheadSegmenters) + .Map(maybeCount => maybeCount.Match(identity, () => 1)); + } + + private record Segment(string File, int SequenceNumber); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Streaming/PlayoutItemProcessModel.cs b/ErsatzTV.Application/Streaming/PlayoutItemProcessModel.cs index 281790e0a..91e113282 100644 --- a/ErsatzTV.Application/Streaming/PlayoutItemProcessModel.cs +++ b/ErsatzTV.Application/Streaming/PlayoutItemProcessModel.cs @@ -1,7 +1,5 @@ -using System; -using System.Diagnostics; +using System.Diagnostics; -namespace ErsatzTV.Application.Streaming -{ - public record PlayoutItemProcessModel(Process Process, DateTimeOffset Until); -} +namespace ErsatzTV.Application.Streaming; + +public record PlayoutItemProcessModel(Process Process, DateTimeOffset Until); \ No newline at end of file diff --git a/ErsatzTV.Application/Streaming/Queries/FFmpegProcessHandler.cs b/ErsatzTV.Application/Streaming/Queries/FFmpegProcessHandler.cs index 7ba2d96a0..d990cf9c6 100644 --- a/ErsatzTV.Application/Streaming/Queries/FFmpegProcessHandler.cs +++ b/ErsatzTV.Application/Streaming/Queries/FFmpegProcessHandler.cs @@ -1,73 +1,65 @@ -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; -using LanguageExt; -using MediatR; using Microsoft.EntityFrameworkCore; -using static LanguageExt.Prelude; -namespace ErsatzTV.Application.Streaming.Queries +namespace ErsatzTV.Application.Streaming; + +public abstract class FFmpegProcessHandler : IRequestHandler> + where T : FFmpegProcessRequest { - public abstract class FFmpegProcessHandler : IRequestHandler> - where T : FFmpegProcessRequest + private readonly IDbContextFactory _dbContextFactory; + + protected FFmpegProcessHandler(IDbContextFactory dbContextFactory) => + _dbContextFactory = dbContextFactory; + + public async Task> Handle(T request, CancellationToken cancellationToken) { - private readonly IDbContextFactory _dbContextFactory; - - protected FFmpegProcessHandler(IDbContextFactory dbContextFactory) => - _dbContextFactory = dbContextFactory; - - public async Task> Handle(T request, CancellationToken cancellationToken) - { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - Validation> validation = await Validate(dbContext, request); - return await validation.Match( - tuple => GetProcess(dbContext, request, tuple.Item1, tuple.Item2), - error => Task.FromResult>(error.Join())); - } - - protected abstract Task> GetProcess( - TvContext dbContext, - T request, - Channel channel, - string ffmpegPath); - - private static async Task>> Validate( - TvContext dbContext, - T request) => - (await ChannelMustExist(dbContext, request), await FFmpegPathMustExist(dbContext)) - .Apply((channel, ffmpegPath) => Tuple(channel, ffmpegPath)); - - private static Task> 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 => - { - channel.StreamingMode = request.Mode.ToLowerInvariant() switch - { - "hls-direct" => StreamingMode.HttpLiveStreamingDirect, - "segmenter" => StreamingMode.HttpLiveStreamingSegmenter, - "ts" => StreamingMode.TransportStreamHybrid, - "ts-legacy" => StreamingMode.TransportStream, - _ => channel.StreamingMode - }; - - return channel; - }) - .Map(o => o.ToValidation($"Channel number {request.ChannelNumber} does not exist.")); - - private static Task> FFmpegPathMustExist(TvContext dbContext) => - dbContext.ConfigElements.GetValue(ConfigElementKey.FFmpegPath) - .FilterT(File.Exists) - .Map(maybePath => maybePath.ToValidation("FFmpeg path does not exist on filesystem")); + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + Validation> validation = await Validate(dbContext, request); + return await validation.Match( + tuple => GetProcess(dbContext, request, tuple.Item1, tuple.Item2), + error => Task.FromResult>(error.Join())); } -} + + protected abstract Task> GetProcess( + TvContext dbContext, + T request, + Channel channel, + string ffmpegPath); + + private static async Task>> Validate( + TvContext dbContext, + T request) => + (await ChannelMustExist(dbContext, request), await FFmpegPathMustExist(dbContext)) + .Apply((channel, ffmpegPath) => Tuple(channel, ffmpegPath)); + + private static Task> 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 => + { + channel.StreamingMode = request.Mode.ToLowerInvariant() switch + { + "hls-direct" => StreamingMode.HttpLiveStreamingDirect, + "segmenter" => StreamingMode.HttpLiveStreamingSegmenter, + "ts" => StreamingMode.TransportStreamHybrid, + "ts-legacy" => StreamingMode.TransportStream, + _ => channel.StreamingMode + }; + + return channel; + }) + .Map(o => o.ToValidation($"Channel number {request.ChannelNumber} does not exist.")); + + private static Task> FFmpegPathMustExist(TvContext dbContext) => + dbContext.ConfigElements.GetValue(ConfigElementKey.FFmpegPath) + .FilterT(File.Exists) + .Map(maybePath => maybePath.ToValidation("FFmpeg path does not exist on filesystem")); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Streaming/Queries/FFmpegProcessRequest.cs b/ErsatzTV.Application/Streaming/Queries/FFmpegProcessRequest.cs index 635a1723f..6ec1dc363 100644 --- a/ErsatzTV.Application/Streaming/Queries/FFmpegProcessRequest.cs +++ b/ErsatzTV.Application/Streaming/Queries/FFmpegProcessRequest.cs @@ -1,16 +1,12 @@ -using System; -using ErsatzTV.Core; -using LanguageExt; -using MediatR; +using ErsatzTV.Core; -namespace ErsatzTV.Application.Streaming.Queries -{ - public record FFmpegProcessRequest - ( - string ChannelNumber, - string Mode, - DateTimeOffset Now, - bool StartAtZero, - bool HlsRealtime, - long PtsOffset) : IRequest>; -} +namespace ErsatzTV.Application.Streaming; + +public record FFmpegProcessRequest +( + string ChannelNumber, + string Mode, + DateTimeOffset Now, + bool StartAtZero, + bool HlsRealtime, + long PtsOffset) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Streaming/Queries/GetConcatPlaylistByChannelNumber.cs b/ErsatzTV.Application/Streaming/Queries/GetConcatPlaylistByChannelNumber.cs index 352a429ad..e2944ca24 100644 --- a/ErsatzTV.Application/Streaming/Queries/GetConcatPlaylistByChannelNumber.cs +++ b/ErsatzTV.Application/Streaming/Queries/GetConcatPlaylistByChannelNumber.cs @@ -1,10 +1,7 @@ using ErsatzTV.Core; using ErsatzTV.Core.FFmpeg; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.Streaming.Queries -{ - public record GetConcatPlaylistByChannelNumber - (string Scheme, string Host, string ChannelNumber) : IRequest>; -} +namespace ErsatzTV.Application.Streaming; + +public record GetConcatPlaylistByChannelNumber + (string Scheme, string Host, string ChannelNumber) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Streaming/Queries/GetConcatPlaylistByChannelNumberHandler.cs b/ErsatzTV.Application/Streaming/Queries/GetConcatPlaylistByChannelNumberHandler.cs index 517c1c2c3..2c1b90f77 100644 --- a/ErsatzTV.Application/Streaming/Queries/GetConcatPlaylistByChannelNumberHandler.cs +++ b/ErsatzTV.Application/Streaming/Queries/GetConcatPlaylistByChannelNumberHandler.cs @@ -1,35 +1,30 @@ -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.FFmpeg; using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.Streaming.Queries +namespace ErsatzTV.Application.Streaming; + +public class + GetConcatPlaylistByChannelNumberHandler : IRequestHandler> { - public class - GetConcatPlaylistByChannelNumberHandler : IRequestHandler> - { - private readonly IChannelRepository _channelRepository; + private readonly IChannelRepository _channelRepository; - public GetConcatPlaylistByChannelNumberHandler(IChannelRepository channelRepository) => - _channelRepository = channelRepository; + public GetConcatPlaylistByChannelNumberHandler(IChannelRepository channelRepository) => + _channelRepository = channelRepository; - public Task> Handle( - GetConcatPlaylistByChannelNumber request, - CancellationToken cancellationToken) => - Validate(request) - .MapT(channel => new ConcatPlaylist(request.Scheme, request.Host, channel.Number)) - .Map(v => v.ToEither()); + public Task> Handle( + GetConcatPlaylistByChannelNumber request, + CancellationToken cancellationToken) => + Validate(request) + .MapT(channel => new ConcatPlaylist(request.Scheme, request.Host, channel.Number)) + .Map(v => v.ToEither()); - private Task> Validate(GetConcatPlaylistByChannelNumber request) => - ChannelMustExist(request); + private Task> Validate(GetConcatPlaylistByChannelNumber request) => + ChannelMustExist(request); - private async Task> ChannelMustExist(GetConcatPlaylistByChannelNumber request) => - (await _channelRepository.GetByNumber(request.ChannelNumber)) - .ToValidation($"Channel number {request.ChannelNumber} does not exist."); - } -} + private async Task> ChannelMustExist(GetConcatPlaylistByChannelNumber request) => + (await _channelRepository.GetByNumber(request.ChannelNumber)) + .ToValidation($"Channel number {request.ChannelNumber} does not exist."); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Streaming/Queries/GetConcatProcessByChannelNumber.cs b/ErsatzTV.Application/Streaming/Queries/GetConcatProcessByChannelNumber.cs index 5a6f6621c..99e38057f 100644 --- a/ErsatzTV.Application/Streaming/Queries/GetConcatProcessByChannelNumber.cs +++ b/ErsatzTV.Application/Streaming/Queries/GetConcatProcessByChannelNumber.cs @@ -1,22 +1,19 @@ -using System; +namespace ErsatzTV.Application.Streaming; -namespace ErsatzTV.Application.Streaming.Queries +public record GetConcatProcessByChannelNumber : FFmpegProcessRequest { - public record GetConcatProcessByChannelNumber : FFmpegProcessRequest + public GetConcatProcessByChannelNumber(string scheme, string host, string channelNumber) : base( + channelNumber, + "ts-legacy", + DateTimeOffset.Now, + false, + true, + 0) { - public GetConcatProcessByChannelNumber(string scheme, string host, string channelNumber) : base( - channelNumber, - "ts-legacy", - DateTimeOffset.Now, - false, - true, - 0) - { - Scheme = scheme; - Host = host; - } - - public string Scheme { get; } - public string Host { get; } + Scheme = scheme; + Host = host; } -} + + public string Scheme { get; } + public string Host { get; } +} \ No newline at end of file diff --git a/ErsatzTV.Application/Streaming/Queries/GetConcatProcessByChannelNumberHandler.cs b/ErsatzTV.Application/Streaming/Queries/GetConcatProcessByChannelNumberHandler.cs index 8284e08d0..ad1de73cf 100644 --- a/ErsatzTV.Application/Streaming/Queries/GetConcatProcessByChannelNumberHandler.cs +++ b/ErsatzTV.Application/Streaming/Queries/GetConcatProcessByChannelNumberHandler.cs @@ -1,48 +1,44 @@ -using System; -using System.Diagnostics; -using System.Threading.Tasks; +using System.Diagnostics; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.FFmpeg; using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; -using LanguageExt; using Microsoft.EntityFrameworkCore; -namespace ErsatzTV.Application.Streaming.Queries +namespace ErsatzTV.Application.Streaming; + +public class GetConcatProcessByChannelNumberHandler : FFmpegProcessHandler { - public class GetConcatProcessByChannelNumberHandler : FFmpegProcessHandler + private readonly IFFmpegProcessServiceFactory _ffmpegProcessServiceFactory; + + public GetConcatProcessByChannelNumberHandler( + IDbContextFactory dbContextFactory, + IFFmpegProcessServiceFactory ffmpegProcessServiceFactory) + : base(dbContextFactory) { - private readonly IFFmpegProcessServiceFactory _ffmpegProcessServiceFactory; - - public GetConcatProcessByChannelNumberHandler( - IDbContextFactory dbContextFactory, - IFFmpegProcessServiceFactory ffmpegProcessServiceFactory) - : base(dbContextFactory) - { - _ffmpegProcessServiceFactory = ffmpegProcessServiceFactory; - } - - protected override async Task> GetProcess( - TvContext dbContext, - GetConcatProcessByChannelNumber request, - Channel channel, - string ffmpegPath) - { - bool saveReports = await dbContext.ConfigElements - .GetValue(ConfigElementKey.FFmpegSaveReports) - .Map(result => result.IfNone(false)); - - IFFmpegProcessService ffmpegProcessService = await _ffmpegProcessServiceFactory.GetService(); - Process process = ffmpegProcessService.ConcatChannel( - ffmpegPath, - saveReports, - channel, - request.Scheme, - request.Host); - - return new PlayoutItemProcessModel(process, DateTimeOffset.MaxValue); - } + _ffmpegProcessServiceFactory = ffmpegProcessServiceFactory; } -} + + protected override async Task> GetProcess( + TvContext dbContext, + GetConcatProcessByChannelNumber request, + Channel channel, + string ffmpegPath) + { + bool saveReports = await dbContext.ConfigElements + .GetValue(ConfigElementKey.FFmpegSaveReports) + .Map(result => result.IfNone(false)); + + IFFmpegProcessService ffmpegProcessService = await _ffmpegProcessServiceFactory.GetService(); + Process process = ffmpegProcessService.ConcatChannel( + ffmpegPath, + saveReports, + channel, + request.Scheme, + request.Host); + + return new PlayoutItemProcessModel(process, DateTimeOffset.MaxValue); + } +} \ No newline at end of file diff --git a/ErsatzTV.Application/Streaming/Queries/GetHlsPlaylistByChannelNumber.cs b/ErsatzTV.Application/Streaming/Queries/GetHlsPlaylistByChannelNumber.cs index e836ddbe9..253491afc 100644 --- a/ErsatzTV.Application/Streaming/Queries/GetHlsPlaylistByChannelNumber.cs +++ b/ErsatzTV.Application/Streaming/Queries/GetHlsPlaylistByChannelNumber.cs @@ -1,9 +1,6 @@ using ErsatzTV.Core; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.Streaming.Queries -{ - public record GetHlsPlaylistByChannelNumber - (string Scheme, string Host, string ChannelNumber, string Mode) : IRequest>; -} +namespace ErsatzTV.Application.Streaming; + +public record GetHlsPlaylistByChannelNumber + (string Scheme, string Host, string ChannelNumber, string Mode) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Streaming/Queries/GetHlsPlaylistByChannelNumberHandler.cs b/ErsatzTV.Application/Streaming/Queries/GetHlsPlaylistByChannelNumberHandler.cs index c42a627fc..1032ef037 100644 --- a/ErsatzTV.Application/Streaming/Queries/GetHlsPlaylistByChannelNumberHandler.cs +++ b/ErsatzTV.Application/Streaming/Queries/GetHlsPlaylistByChannelNumberHandler.cs @@ -1,54 +1,49 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; -using LanguageExt; -using MediatR; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; -namespace ErsatzTV.Application.Streaming.Queries +namespace ErsatzTV.Application.Streaming; + +public class GetHlsPlaylistByChannelNumberHandler : + IRequestHandler> { - public class GetHlsPlaylistByChannelNumberHandler : - IRequestHandler> + private readonly IDbContextFactory _dbContextFactory; + private readonly IMemoryCache _memoryCache; + + public GetHlsPlaylistByChannelNumberHandler( + IDbContextFactory dbContextFactory, + IMemoryCache memoryCache) { - private readonly IDbContextFactory _dbContextFactory; - private readonly IMemoryCache _memoryCache; + _dbContextFactory = dbContextFactory; + _memoryCache = memoryCache; + } - public GetHlsPlaylistByChannelNumberHandler( - IDbContextFactory dbContextFactory, - IMemoryCache memoryCache) - { - _dbContextFactory = dbContextFactory; - _memoryCache = memoryCache; - } + public async Task> Handle( + GetHlsPlaylistByChannelNumber request, + CancellationToken cancellationToken) + { + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + DateTimeOffset now = DateTimeOffset.Now; + Validation validation = await Validate(dbContext, request, now); + return await LanguageExtensions.Apply(validation, parameters => GetPlaylist(dbContext, request, parameters, now)); + } - public async Task> Handle( - GetHlsPlaylistByChannelNumber request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - DateTimeOffset now = DateTimeOffset.Now; - Validation validation = await Validate(dbContext, request, now); - return await validation.Apply(parameters => GetPlaylist(dbContext, request, parameters, now)); - } + private Task GetPlaylist( + TvContext dbContext, + GetHlsPlaylistByChannelNumber request, + Parameters parameters, + DateTimeOffset now) + { + string mode = string.IsNullOrWhiteSpace(request.Mode) + ? string.Empty + : $"&mode={request.Mode}"; - private Task GetPlaylist( - TvContext dbContext, - GetHlsPlaylistByChannelNumber request, - Parameters parameters, - DateTimeOffset now) - { - string mode = string.IsNullOrWhiteSpace(request.Mode) - ? string.Empty - : $"&mode={request.Mode}"; - - long index = GetIndexForChannel(parameters.Channel, parameters.PlayoutItem); - double timeRemaining = Math.Abs((parameters.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} @@ -56,62 +51,61 @@ namespace ErsatzTV.Application.Streaming.Queries #EXTINF:{timeRemaining:F2}, {request.Scheme}://{request.Host}/ffmpeg/stream/{request.ChannelNumber}?index={index}{mode} ".AsTask(); - } + } - private Task> Validate( - TvContext dbContext, - GetHlsPlaylistByChannelNumber request, - DateTimeOffset now) => - ChannelMustExist(dbContext, request) - .BindT(channel => PlayoutItemMustExist(dbContext, channel, now)); + private Task> Validate( + TvContext dbContext, + GetHlsPlaylistByChannelNumber request, + DateTimeOffset now) => + ChannelMustExist(dbContext, request) + .BindT(channel => PlayoutItemMustExist(dbContext, channel, now)); - private static Task> ChannelMustExist( - TvContext dbContext, - GetHlsPlaylistByChannelNumber request) => - dbContext.Channels - .SelectOneAsync(c => c.Number, c => c.Number == request.ChannelNumber) - .Map(o => o.ToValidation($"Channel number {request.ChannelNumber} does not exist.")); + private static Task> ChannelMustExist( + TvContext dbContext, + GetHlsPlaylistByChannelNumber request) => + dbContext.Channels + .SelectOneAsync(c => c.Number, c => c.Number == request.ChannelNumber) + .Map(o => o.ToValidation($"Channel number {request.ChannelNumber} does not exist.")); - private static Task> PlayoutItemMustExist( - TvContext dbContext, - Channel channel, - DateTimeOffset now) => - dbContext.PlayoutItems - .ForChannelAndTime(channel.Id, now) - .MapT(playoutItem => new Parameters(channel, playoutItem)) - .Map(o => o.ToValidation($"Unable to locate playout item for channel {channel.Number}")); + private static Task> PlayoutItemMustExist( + TvContext dbContext, + Channel channel, + DateTimeOffset now) => + dbContext.PlayoutItems + .ForChannelAndTime(channel.Id, now) + .MapT(playoutItem => new Parameters(channel, playoutItem)) + .Map(o => o.ToValidation($"Unable to locate playout item for channel {channel.Number}")); - private long GetIndexForChannel(Channel channel, PlayoutItem playoutItem) + private long GetIndexForChannel(Channel channel, PlayoutItem playoutItem) + { + long ticks = playoutItem.Start.Ticks; + var key = new ChannelIndexKey(channel.Id); + + long index; + if (_memoryCache.TryGetValue(key, out ChannelIndexRecord channelRecord)) { - long ticks = playoutItem.Start.Ticks; - var key = new ChannelIndexKey(channel.Id); - - long index; - if (_memoryCache.TryGetValue(key, out ChannelIndexRecord channelRecord)) + if (channelRecord.StartTicks == ticks) { - if (channelRecord.StartTicks == ticks) - { - index = channelRecord.Index; - } - else - { - index = channelRecord.Index + 1; - _memoryCache.Set(key, new ChannelIndexRecord(ticks, index), TimeSpan.FromDays(1)); - } + index = channelRecord.Index; } else { - index = 1; + index = channelRecord.Index + 1; _memoryCache.Set(key, new ChannelIndexRecord(ticks, index), TimeSpan.FromDays(1)); } - - return index; + } + else + { + index = 1; + _memoryCache.Set(key, new ChannelIndexRecord(ticks, index), TimeSpan.FromDays(1)); } - private record ChannelIndexKey(int ChannelId); - - private record ChannelIndexRecord(long StartTicks, long Index); - - private record Parameters(Channel Channel, PlayoutItem PlayoutItem); + return index; } -} + + private record ChannelIndexKey(int ChannelId); + + private record ChannelIndexRecord(long StartTicks, long Index); + + private record Parameters(Channel Channel, PlayoutItem PlayoutItem); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Streaming/Queries/GetLastPtsDuration.cs b/ErsatzTV.Application/Streaming/Queries/GetLastPtsDuration.cs index bdd289d12..8a424b20e 100644 --- a/ErsatzTV.Application/Streaming/Queries/GetLastPtsDuration.cs +++ b/ErsatzTV.Application/Streaming/Queries/GetLastPtsDuration.cs @@ -1,7 +1,5 @@ using ErsatzTV.Core; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.Streaming.Queries; +namespace ErsatzTV.Application.Streaming; public record GetLastPtsDuration(string FileName) : IRequest>; diff --git a/ErsatzTV.Application/Streaming/Queries/GetLastPtsDurationHandler.cs b/ErsatzTV.Application/Streaming/Queries/GetLastPtsDurationHandler.cs index fce411c73..71348b0d5 100644 --- a/ErsatzTV.Application/Streaming/Queries/GetLastPtsDurationHandler.cs +++ b/ErsatzTV.Application/Streaming/Queries/GetLastPtsDurationHandler.cs @@ -1,16 +1,10 @@ using System.Diagnostics; -using System.IO; -using System.Linq; using System.Text; -using System.Threading; -using System.Threading.Tasks; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.Streaming.Queries; +namespace ErsatzTV.Application.Streaming; public class GetLastPtsDurationHandler : IRequestHandler> { diff --git a/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumber.cs b/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumber.cs index 2c735d656..fbb644843 100644 --- a/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumber.cs +++ b/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumber.cs @@ -1,18 +1,14 @@ -using System; -using LanguageExt; +namespace ErsatzTV.Application.Streaming; -namespace ErsatzTV.Application.Streaming.Queries -{ - public record GetPlayoutItemProcessByChannelNumber(string ChannelNumber, - string Mode, - DateTimeOffset Now, - bool StartAtZero, - bool HlsRealtime, - long PtsOffset, - Option TargetFramerate) : FFmpegProcessRequest(ChannelNumber, - Mode, - Now, - StartAtZero, - HlsRealtime, - PtsOffset); -} +public record GetPlayoutItemProcessByChannelNumber(string ChannelNumber, + string Mode, + DateTimeOffset Now, + bool StartAtZero, + bool HlsRealtime, + long PtsOffset, + Option TargetFramerate) : FFmpegProcessRequest(ChannelNumber, + Mode, + Now, + StartAtZero, + HlsRealtime, + PtsOffset); \ No newline at end of file diff --git a/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs b/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs index e0910b28c..00bbef6e2 100644 --- a/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs +++ b/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Threading.Tasks; +using System.Diagnostics; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Filler; @@ -18,273 +14,160 @@ using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Scheduling; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; -using LanguageExt; using Microsoft.EntityFrameworkCore; -using static LanguageExt.Prelude; -namespace ErsatzTV.Application.Streaming.Queries +namespace ErsatzTV.Application.Streaming; + +public class GetPlayoutItemProcessByChannelNumberHandler : + FFmpegProcessHandler { - public class GetPlayoutItemProcessByChannelNumberHandler : - FFmpegProcessHandler + private readonly IEmbyPathReplacementService _embyPathReplacementService; + private readonly IMediaCollectionRepository _mediaCollectionRepository; + private readonly ITelevisionRepository _televisionRepository; + private readonly IArtistRepository _artistRepository; + private readonly IJellyfinPathReplacementService _jellyfinPathReplacementService; + private readonly IFFmpegProcessServiceFactory _ffmpegProcessServiceFactory; + private readonly ILocalFileSystem _localFileSystem; + private readonly IPlexPathReplacementService _plexPathReplacementService; + private readonly ISongVideoGenerator _songVideoGenerator; + + public GetPlayoutItemProcessByChannelNumberHandler( + IDbContextFactory dbContextFactory, + IFFmpegProcessServiceFactory ffmpegProcessServiceFactory, + ILocalFileSystem localFileSystem, + IPlexPathReplacementService plexPathReplacementService, + IJellyfinPathReplacementService jellyfinPathReplacementService, + IEmbyPathReplacementService embyPathReplacementService, + IMediaCollectionRepository mediaCollectionRepository, + ITelevisionRepository televisionRepository, + IArtistRepository artistRepository, + ISongVideoGenerator songVideoGenerator) + : base(dbContextFactory) { - private readonly IEmbyPathReplacementService _embyPathReplacementService; - private readonly IMediaCollectionRepository _mediaCollectionRepository; - private readonly ITelevisionRepository _televisionRepository; - private readonly IArtistRepository _artistRepository; - private readonly IJellyfinPathReplacementService _jellyfinPathReplacementService; - private readonly IFFmpegProcessServiceFactory _ffmpegProcessServiceFactory; - private readonly ILocalFileSystem _localFileSystem; - private readonly IPlexPathReplacementService _plexPathReplacementService; - private readonly ISongVideoGenerator _songVideoGenerator; + _ffmpegProcessServiceFactory = ffmpegProcessServiceFactory; + _localFileSystem = localFileSystem; + _plexPathReplacementService = plexPathReplacementService; + _jellyfinPathReplacementService = jellyfinPathReplacementService; + _embyPathReplacementService = embyPathReplacementService; + _mediaCollectionRepository = mediaCollectionRepository; + _televisionRepository = televisionRepository; + _artistRepository = artistRepository; + _songVideoGenerator = songVideoGenerator; + } - public GetPlayoutItemProcessByChannelNumberHandler( - IDbContextFactory dbContextFactory, - IFFmpegProcessServiceFactory ffmpegProcessServiceFactory, - ILocalFileSystem localFileSystem, - IPlexPathReplacementService plexPathReplacementService, - IJellyfinPathReplacementService jellyfinPathReplacementService, - IEmbyPathReplacementService embyPathReplacementService, - IMediaCollectionRepository mediaCollectionRepository, - ITelevisionRepository televisionRepository, - IArtistRepository artistRepository, - ISongVideoGenerator songVideoGenerator) - : base(dbContextFactory) - { - _ffmpegProcessServiceFactory = ffmpegProcessServiceFactory; - _localFileSystem = localFileSystem; - _plexPathReplacementService = plexPathReplacementService; - _jellyfinPathReplacementService = jellyfinPathReplacementService; - _embyPathReplacementService = embyPathReplacementService; - _mediaCollectionRepository = mediaCollectionRepository; - _televisionRepository = televisionRepository; - _artistRepository = artistRepository; - _songVideoGenerator = songVideoGenerator; - } - - protected override async Task> GetProcess( - TvContext dbContext, - GetPlayoutItemProcessByChannelNumber request, - Channel channel, - string ffmpegPath) - { - DateTimeOffset now = request.Now; + protected override async Task> GetProcess( + TvContext dbContext, + GetPlayoutItemProcessByChannelNumber request, + Channel channel, + string ffmpegPath) + { + DateTimeOffset now = request.Now; - Either 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) - .Include(i => i.MediaItem) - .ThenInclude(mi => (mi as OtherVideo).MediaVersions) - .ThenInclude(ov => ov.MediaFiles) - .Include(i => i.MediaItem) - .ThenInclude(mi => (mi as OtherVideo).MediaVersions) - .ThenInclude(ov => ov.Streams) - .Include(i => i.MediaItem) - .ThenInclude(mi => (mi as Song).MediaVersions) - .ThenInclude(mv => mv.MediaFiles) - .Include(i => i.MediaItem) - .ThenInclude(mi => (mi as Song).MediaVersions) - .ThenInclude(mv => mv.Streams) - .Include(i => i.MediaItem) - .ThenInclude(mi => (mi as Song).SongMetadata) - .ThenInclude(sm => sm.Artwork) - .ForChannelAndTime(channel.Id, now) - .Map(o => o.ToEither(new UnableToLocatePlayoutItem())) - .BindT(ValidatePlayoutItemPath); + Either 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) + .Include(i => i.MediaItem) + .ThenInclude(mi => (mi as OtherVideo).MediaVersions) + .ThenInclude(ov => ov.MediaFiles) + .Include(i => i.MediaItem) + .ThenInclude(mi => (mi as OtherVideo).MediaVersions) + .ThenInclude(ov => ov.Streams) + .Include(i => i.MediaItem) + .ThenInclude(mi => (mi as Song).MediaVersions) + .ThenInclude(mv => mv.MediaFiles) + .Include(i => i.MediaItem) + .ThenInclude(mi => (mi as Song).MediaVersions) + .ThenInclude(mv => mv.Streams) + .Include(i => i.MediaItem) + .ThenInclude(mi => (mi as Song).SongMetadata) + .ThenInclude(sm => sm.Artwork) + .ForChannelAndTime(channel.Id, now) + .Map(o => o.ToEither(new UnableToLocatePlayoutItem())) + .BindT(ValidatePlayoutItemPath); - if (maybePlayoutItem.LeftAsEnumerable().Any(e => e is UnableToLocatePlayoutItem)) - { - maybePlayoutItem = await CheckForFallbackFiller(dbContext, channel, now); - } - - IFFmpegProcessService ffmpegProcessService = await _ffmpegProcessServiceFactory.GetService(); - - return await maybePlayoutItem.Match( - async playoutItemWithPath => - { - MediaVersion version = playoutItemWithPath.PlayoutItem.MediaItem.GetHeadVersion(); - - string videoPath = playoutItemWithPath.Path; - MediaVersion videoVersion = version; - - string audioPath = playoutItemWithPath.Path; - MediaVersion audioVersion = version; - - Option maybeGlobalWatermark = await dbContext.ConfigElements - .GetValue(ConfigElementKey.FFmpegGlobalWatermarkId) - .BindT( - watermarkId => dbContext.ChannelWatermarks - .SelectOneAsync(w => w.Id, w => w.Id == watermarkId)); - - if (playoutItemWithPath.PlayoutItem.MediaItem is Song song) - { - (videoPath, videoVersion) = await _songVideoGenerator.GenerateSongVideo( - song, - channel, - maybeGlobalWatermark, - ffmpegPath); - } - - bool saveReports = await dbContext.ConfigElements - .GetValue(ConfigElementKey.FFmpegSaveReports) - .Map(result => result.IfNone(false)); - - Process process = await ffmpegProcessService.ForPlayoutItem( - ffmpegPath, - saveReports, - channel, - videoVersion, - audioVersion, - videoPath, - audioPath, - playoutItemWithPath.PlayoutItem.StartOffset, - playoutItemWithPath.PlayoutItem.FinishOffset, - request.StartAtZero ? playoutItemWithPath.PlayoutItem.StartOffset : now, - maybeGlobalWatermark, - channel.FFmpegProfile.VaapiDriver, - channel.FFmpegProfile.VaapiDevice, - request.HlsRealtime, - playoutItemWithPath.PlayoutItem.FillerKind, - playoutItemWithPath.PlayoutItem.InPoint, - playoutItemWithPath.PlayoutItem.OutPoint, - request.PtsOffset, - request.TargetFramerate); - - var result = new PlayoutItemProcessModel(process, playoutItemWithPath.PlayoutItem.FinishOffset); - - return Right(result); - }, - async error => - { - var offlineTranscodeMessage = - $"offline image is unavailable because transcoding is disabled in ffmpeg profile '{channel.FFmpegProfile.Name}'"; - - Option maybeDuration = await Optional(channel.FFmpegProfile.Transcode) - .Where(transcode => transcode) - .Match( - _ => 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.None.AsTask()); - - DateTimeOffset finish = maybeDuration.Match(d => now.Add(d), () => now); - - switch (error) - { - case UnableToLocatePlayoutItem: - if (channel.FFmpegProfile.Transcode) - { - Process errorProcess = await ffmpegProcessService.ForError( - ffmpegPath, - channel, - maybeDuration, - "Channel is Offline", - request.HlsRealtime, - request.PtsOffset); - - return new PlayoutItemProcessModel(errorProcess, finish); - } - else - { - var message = - $"Unable to locate playout item for channel {channel.Number}; {offlineTranscodeMessage}"; - - return BaseError.New(message); - } - case PlayoutItemDoesNotExistOnDisk: - if (channel.FFmpegProfile.Transcode) - { - Process errorProcess = await ffmpegProcessService.ForError( - ffmpegPath, - channel, - maybeDuration, - error.Value, - request.HlsRealtime, - request.PtsOffset); - - return new PlayoutItemProcessModel(errorProcess, finish); - } - else - { - var message = - $"Playout item does not exist on disk for channel {channel.Number}; {offlineTranscodeMessage}"; - - return BaseError.New(message); - } - default: - if (channel.FFmpegProfile.Transcode) - { - Process errorProcess = await ffmpegProcessService.ForError( - ffmpegPath, - channel, - maybeDuration, - "Channel is Offline", - request.HlsRealtime, - request.PtsOffset); - - return new PlayoutItemProcessModel(errorProcess, finish); - } - else - { - var message = - $"Unexpected error locating playout item for channel {channel.Number}; {offlineTranscodeMessage}"; - - return BaseError.New(message); - } - } - }); + if (maybePlayoutItem.LeftAsEnumerable().Any(e => e is UnableToLocatePlayoutItem)) + { + maybePlayoutItem = await CheckForFallbackFiller(dbContext, channel, now); } - private async Task> CheckForFallbackFiller( - TvContext dbContext, - Channel channel, - DateTimeOffset now) - { - // check for channel fallback - Option maybeFallback = await dbContext.FillerPresets - .SelectOneAsync(w => w.Id, w => w.Id == channel.FallbackFillerId); + IFFmpegProcessService ffmpegProcessService = await _ffmpegProcessServiceFactory.GetService(); - // then check for global fallback - if (maybeFallback.IsNone) + return await maybePlayoutItem.Match( + async playoutItemWithPath => { - maybeFallback = await dbContext.ConfigElements - .GetValue(ConfigElementKey.FFmpegGlobalFallbackFillerId) - .BindT(fillerId => dbContext.FillerPresets.SelectOneAsync(w => w.Id, w => w.Id == fillerId)); - } + MediaVersion version = playoutItemWithPath.PlayoutItem.MediaItem.GetHeadVersion(); - foreach (FillerPreset fallbackPreset in maybeFallback) + string videoPath = playoutItemWithPath.Path; + MediaVersion videoVersion = version; + + string audioPath = playoutItemWithPath.Path; + MediaVersion audioVersion = version; + + Option maybeGlobalWatermark = await dbContext.ConfigElements + .GetValue(ConfigElementKey.FFmpegGlobalWatermarkId) + .BindT( + watermarkId => dbContext.ChannelWatermarks + .SelectOneAsync(w => w.Id, w => w.Id == watermarkId)); + + if (playoutItemWithPath.PlayoutItem.MediaItem is Song song) + { + (videoPath, videoVersion) = await _songVideoGenerator.GenerateSongVideo( + song, + channel, + maybeGlobalWatermark, + ffmpegPath); + } + + bool saveReports = await dbContext.ConfigElements + .GetValue(ConfigElementKey.FFmpegSaveReports) + .Map(result => result.IfNone(false)); + + Process process = await ffmpegProcessService.ForPlayoutItem( + ffmpegPath, + saveReports, + channel, + videoVersion, + audioVersion, + videoPath, + audioPath, + playoutItemWithPath.PlayoutItem.StartOffset, + playoutItemWithPath.PlayoutItem.FinishOffset, + request.StartAtZero ? playoutItemWithPath.PlayoutItem.StartOffset : now, + maybeGlobalWatermark, + channel.FFmpegProfile.VaapiDriver, + channel.FFmpegProfile.VaapiDevice, + request.HlsRealtime, + playoutItemWithPath.PlayoutItem.FillerKind, + playoutItemWithPath.PlayoutItem.InPoint, + playoutItemWithPath.PlayoutItem.OutPoint, + request.PtsOffset, + request.TargetFramerate); + + var result = new PlayoutItemProcessModel(process, playoutItemWithPath.PlayoutItem.FinishOffset); + + return Right(result); + }, + async error => { - // turn this into a playout item + var offlineTranscodeMessage = + $"offline image is unavailable because transcoding is disabled in ffmpeg profile '{channel.FFmpegProfile.Name}'"; - var collectionKey = CollectionKey.ForFillerPreset(fallbackPreset); - List items = await MediaItemsForCollection.Collect( - _mediaCollectionRepository, - _televisionRepository, - _artistRepository, - collectionKey); - - // TODO: shuffle? does it really matter since we loop anyway - MediaItem item = items[new Random().Next(items.Count)]; - Option maybeDuration = await Optional(channel.FFmpegProfile.Transcode) .Where(transcode => transcode) .Match( @@ -297,87 +180,197 @@ namespace ErsatzTV.Application.Streaming.Queries .MapT(pi => pi.StartOffset - now), () => Option.None.AsTask()); - MediaVersion version = item.GetHeadVersion(); + DateTimeOffset finish = maybeDuration.Match(d => now.Add(d), () => now); - version.MediaFiles = await dbContext.MediaFiles - .AsNoTracking() - .Filter(mf => mf.MediaVersionId == version.Id) - .ToListAsync(); - - version.Streams = await dbContext.MediaStreams - .AsNoTracking() - .Filter(ms => ms.MediaVersionId == version.Id) - .ToListAsync(); - - DateTimeOffset finish = maybeDuration.Match( - // next playout item exists - // loop until it starts - now.Add, - // no next playout item exists - // loop for 5 minutes if less than 30s, otherwise play full item - () => version.Duration < TimeSpan.FromSeconds(30) - ? now.AddMinutes(5) - : now.Add(version.Duration)); - - var playoutItem = new PlayoutItem + switch (error) { - MediaItem = item, - MediaItemId = item.Id, - Start = now.UtcDateTime, - Finish = finish.UtcDateTime, - FillerKind = FillerKind.Fallback, - InPoint = TimeSpan.Zero, - OutPoint = version.Duration - }; - - return await ValidatePlayoutItemPath(playoutItem); - } + case UnableToLocatePlayoutItem: + if (channel.FFmpegProfile.Transcode) + { + Process errorProcess = await ffmpegProcessService.ForError( + ffmpegPath, + channel, + maybeDuration, + "Channel is Offline", + request.HlsRealtime, + request.PtsOffset); + + return new PlayoutItemProcessModel(errorProcess, finish); + } + else + { + var message = + $"Unable to locate playout item for channel {channel.Number}; {offlineTranscodeMessage}"; - return new UnableToLocatePlayoutItem(); - } + return BaseError.New(message); + } + case PlayoutItemDoesNotExistOnDisk: + if (channel.FFmpegProfile.Transcode) + { + Process errorProcess = await ffmpegProcessService.ForError( + ffmpegPath, + channel, + maybeDuration, + error.Value, + request.HlsRealtime, + request.PtsOffset); - private async Task> ValidatePlayoutItemPath(PlayoutItem playoutItem) - { - string path = await GetPlayoutItemPath(playoutItem); + return new PlayoutItemProcessModel(errorProcess, finish); + } + else + { + var message = + $"Playout item does not exist on disk for channel {channel.Number}; {offlineTranscodeMessage}"; - if (_localFileSystem.FileExists(path)) - { - return new PlayoutItemWithPath(playoutItem, path); - } + return BaseError.New(message); + } + default: + if (channel.FFmpegProfile.Transcode) + { + Process errorProcess = await ffmpegProcessService.ForError( + ffmpegPath, + channel, + maybeDuration, + "Channel is Offline", + request.HlsRealtime, + request.PtsOffset); - return new PlayoutItemDoesNotExistOnDisk(path); - } + return new PlayoutItemProcessModel(errorProcess, finish); + } + else + { + var message = + $"Unexpected error locating playout item for channel {channel.Number}; {offlineTranscodeMessage}"; - private async Task GetPlayoutItemPath(PlayoutItem playoutItem) - { - MediaVersion version = playoutItem.MediaItem.GetHeadVersion(); - - MediaFile file = version.MediaFiles.Head(); - string path = file.Path; - return playoutItem.MediaItem switch - { - PlexMovie plexMovie => await _plexPathReplacementService.GetReplacementPlexPath( - plexMovie.LibraryPathId, - path), - PlexEpisode plexEpisode => await _plexPathReplacementService.GetReplacementPlexPath( - plexEpisode.LibraryPathId, - path), - JellyfinMovie jellyfinMovie => await _jellyfinPathReplacementService.GetReplacementJellyfinPath( - jellyfinMovie.LibraryPathId, - path), - JellyfinEpisode jellyfinEpisode => await _jellyfinPathReplacementService.GetReplacementJellyfinPath( - jellyfinEpisode.LibraryPathId, - path), - EmbyMovie embyMovie => await _embyPathReplacementService.GetReplacementEmbyPath( - embyMovie.LibraryPathId, - path), - EmbyEpisode embyEpisode => await _embyPathReplacementService.GetReplacementEmbyPath( - embyEpisode.LibraryPathId, - path), - _ => path - }; - } - - private record PlayoutItemWithPath(PlayoutItem PlayoutItem, string Path); + return BaseError.New(message); + } + } + }); } -} + + private async Task> CheckForFallbackFiller( + TvContext dbContext, + Channel channel, + DateTimeOffset now) + { + // check for channel fallback + Option maybeFallback = await dbContext.FillerPresets + .SelectOneAsync(w => w.Id, w => w.Id == channel.FallbackFillerId); + + // then check for global fallback + if (maybeFallback.IsNone) + { + maybeFallback = await dbContext.ConfigElements + .GetValue(ConfigElementKey.FFmpegGlobalFallbackFillerId) + .BindT(fillerId => dbContext.FillerPresets.SelectOneAsync(w => w.Id, w => w.Id == fillerId)); + } + + foreach (FillerPreset fallbackPreset in maybeFallback) + { + // turn this into a playout item + + var collectionKey = CollectionKey.ForFillerPreset(fallbackPreset); + List items = await MediaItemsForCollection.Collect( + _mediaCollectionRepository, + _televisionRepository, + _artistRepository, + collectionKey); + + // TODO: shuffle? does it really matter since we loop anyway + MediaItem item = items[new Random().Next(items.Count)]; + + Option maybeDuration = await Optional(channel.FFmpegProfile.Transcode) + .Where(transcode => transcode) + .Match( + _ => 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.None.AsTask()); + + MediaVersion version = item.GetHeadVersion(); + + version.MediaFiles = await dbContext.MediaFiles + .AsNoTracking() + .Filter(mf => mf.MediaVersionId == version.Id) + .ToListAsync(); + + version.Streams = await dbContext.MediaStreams + .AsNoTracking() + .Filter(ms => ms.MediaVersionId == version.Id) + .ToListAsync(); + + DateTimeOffset finish = maybeDuration.Match( + // next playout item exists + // loop until it starts + now.Add, + // no next playout item exists + // loop for 5 minutes if less than 30s, otherwise play full item + () => version.Duration < TimeSpan.FromSeconds(30) + ? now.AddMinutes(5) + : now.Add(version.Duration)); + + var playoutItem = new PlayoutItem + { + MediaItem = item, + MediaItemId = item.Id, + Start = now.UtcDateTime, + Finish = finish.UtcDateTime, + FillerKind = FillerKind.Fallback, + InPoint = TimeSpan.Zero, + OutPoint = version.Duration + }; + + return await ValidatePlayoutItemPath(playoutItem); + } + + return new UnableToLocatePlayoutItem(); + } + + private async Task> ValidatePlayoutItemPath(PlayoutItem playoutItem) + { + string path = await GetPlayoutItemPath(playoutItem); + + if (_localFileSystem.FileExists(path)) + { + return new PlayoutItemWithPath(playoutItem, path); + } + + return new PlayoutItemDoesNotExistOnDisk(path); + } + + private async Task GetPlayoutItemPath(PlayoutItem playoutItem) + { + MediaVersion version = playoutItem.MediaItem.GetHeadVersion(); + + MediaFile file = version.MediaFiles.Head(); + string path = file.Path; + return playoutItem.MediaItem switch + { + PlexMovie plexMovie => await _plexPathReplacementService.GetReplacementPlexPath( + plexMovie.LibraryPathId, + path), + PlexEpisode plexEpisode => await _plexPathReplacementService.GetReplacementPlexPath( + plexEpisode.LibraryPathId, + path), + JellyfinMovie jellyfinMovie => await _jellyfinPathReplacementService.GetReplacementJellyfinPath( + jellyfinMovie.LibraryPathId, + path), + JellyfinEpisode jellyfinEpisode => await _jellyfinPathReplacementService.GetReplacementJellyfinPath( + jellyfinEpisode.LibraryPathId, + path), + EmbyMovie embyMovie => await _embyPathReplacementService.GetReplacementEmbyPath( + embyMovie.LibraryPathId, + path), + EmbyEpisode embyEpisode => await _embyPathReplacementService.GetReplacementEmbyPath( + embyEpisode.LibraryPathId, + path), + _ => path + }; + } + + private record PlayoutItemWithPath(PlayoutItem PlayoutItem, string Path); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Streaming/Queries/GetWrappedProcessByChannelNumber.cs b/ErsatzTV.Application/Streaming/Queries/GetWrappedProcessByChannelNumber.cs index 71b6b7b37..6f3db9bba 100644 --- a/ErsatzTV.Application/Streaming/Queries/GetWrappedProcessByChannelNumber.cs +++ b/ErsatzTV.Application/Streaming/Queries/GetWrappedProcessByChannelNumber.cs @@ -1,22 +1,19 @@ -using System; +namespace ErsatzTV.Application.Streaming; -namespace ErsatzTV.Application.Streaming.Queries +public record GetWrappedProcessByChannelNumber : FFmpegProcessRequest { - public record GetWrappedProcessByChannelNumber : FFmpegProcessRequest + public GetWrappedProcessByChannelNumber(string scheme, string host, string channelNumber) : base( + channelNumber, + "ts", + DateTimeOffset.Now, + false, + true, + 0) { - public GetWrappedProcessByChannelNumber(string scheme, string host, string channelNumber) : base( - channelNumber, - "ts", - DateTimeOffset.Now, - false, - true, - 0) - { - Scheme = scheme; - Host = host; - } - - public string Scheme { get; } - public string Host { get; } + Scheme = scheme; + Host = host; } -} + + public string Scheme { get; } + public string Host { get; } +} \ No newline at end of file diff --git a/ErsatzTV.Application/Streaming/Queries/GetWrappedProcessByChannelNumberHandler.cs b/ErsatzTV.Application/Streaming/Queries/GetWrappedProcessByChannelNumberHandler.cs index 38199363b..3a0e606a8 100644 --- a/ErsatzTV.Application/Streaming/Queries/GetWrappedProcessByChannelNumberHandler.cs +++ b/ErsatzTV.Application/Streaming/Queries/GetWrappedProcessByChannelNumberHandler.cs @@ -1,48 +1,44 @@ -using System; -using System.Diagnostics; -using System.Threading.Tasks; +using System.Diagnostics; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.FFmpeg; using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; -using LanguageExt; using Microsoft.EntityFrameworkCore; -namespace ErsatzTV.Application.Streaming.Queries +namespace ErsatzTV.Application.Streaming; + +public class GetWrappedProcessByChannelNumberHandler : FFmpegProcessHandler { - public class GetWrappedProcessByChannelNumberHandler : FFmpegProcessHandler + private readonly IFFmpegProcessServiceFactory _ffmpegProcessServiceFactory; + + public GetWrappedProcessByChannelNumberHandler( + IDbContextFactory dbContextFactory, + IFFmpegProcessServiceFactory ffmpegProcessServiceFactory) + : base(dbContextFactory) { - private readonly IFFmpegProcessServiceFactory _ffmpegProcessServiceFactory; - - public GetWrappedProcessByChannelNumberHandler( - IDbContextFactory dbContextFactory, - IFFmpegProcessServiceFactory ffmpegProcessServiceFactory) - : base(dbContextFactory) - { - _ffmpegProcessServiceFactory = ffmpegProcessServiceFactory; - } - - protected override async Task> GetProcess( - TvContext dbContext, - GetWrappedProcessByChannelNumber request, - Channel channel, - string ffmpegPath) - { - bool saveReports = await dbContext.ConfigElements - .GetValue(ConfigElementKey.FFmpegSaveReports) - .Map(result => result.IfNone(false)); - - IFFmpegProcessService ffmpegProcessService = await _ffmpegProcessServiceFactory.GetService(); - Process process = ffmpegProcessService.WrapSegmenter( - ffmpegPath, - saveReports, - channel, - request.Scheme, - request.Host); - - return new PlayoutItemProcessModel(process, DateTimeOffset.MaxValue); - } + _ffmpegProcessServiceFactory = ffmpegProcessServiceFactory; } -} + + protected override async Task> GetProcess( + TvContext dbContext, + GetWrappedProcessByChannelNumber request, + Channel channel, + string ffmpegPath) + { + bool saveReports = await dbContext.ConfigElements + .GetValue(ConfigElementKey.FFmpegSaveReports) + .Map(result => result.IfNone(false)); + + IFFmpegProcessService ffmpegProcessService = await _ffmpegProcessServiceFactory.GetService(); + Process process = ffmpegProcessService.WrapSegmenter( + ffmpegPath, + saveReports, + channel, + request.Scheme, + request.Host); + + return new PlayoutItemProcessModel(process, DateTimeOffset.MaxValue); + } +} \ No newline at end of file diff --git a/ErsatzTV.Application/Television/Mapper.cs b/ErsatzTV.Application/Television/Mapper.cs index d712ceb71..6b261d612 100644 --- a/ErsatzTV.Application/Television/Mapper.cs +++ b/ErsatzTV.Application/Television/Mapper.cs @@ -1,119 +1,113 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; +using System.Globalization; using ErsatzTV.Application.MediaCards; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Emby; using ErsatzTV.Core.Jellyfin; using Flurl; -using LanguageExt; -using static LanguageExt.Prelude; -namespace ErsatzTV.Application.Television +namespace ErsatzTV.Application.Television; + +internal static class Mapper { - internal static class Mapper - { - internal static TelevisionShowViewModel ProjectToViewModel( - Show show, - List languages, - Option maybeJellyfin, - Option maybeEmby) => - new( - show.Id, - show.ShowMetadata.HeadOrNone().Map(m => m.Title ?? string.Empty).IfNone(string.Empty), - show.ShowMetadata.HeadOrNone().Map(m => m.Year?.ToString() ?? string.Empty).IfNone(string.Empty), - show.ShowMetadata.HeadOrNone().Map(m => m.Plot ?? string.Empty).IfNone(string.Empty), - show.ShowMetadata.HeadOrNone().Map(m => GetPoster(m, maybeJellyfin, maybeEmby)).IfNone(string.Empty), - show.ShowMetadata.HeadOrNone().Map(m => GetFanArt(m, maybeJellyfin, maybeEmby)).IfNone(string.Empty), - show.ShowMetadata.HeadOrNone().Map(m => m.Genres.Map(g => g.Name).ToList()).IfNone(new List()), - show.ShowMetadata.HeadOrNone().Map(m => m.Tags.Map(g => g.Name).ToList()).IfNone(new List()), - show.ShowMetadata.HeadOrNone().Map(m => m.Studios.Map(s => s.Name).ToList()) - .IfNone(new List()), - show.ShowMetadata.HeadOrNone() - .Map( - m => (m.ContentRating ?? string.Empty).Split("/").Map(s => s.Trim()) - .Where(x => !string.IsNullOrWhiteSpace(x)).ToList()).IfNone(new List()), - LanguagesForShow(languages), - show.ShowMetadata.HeadOrNone() - .Map( - m => m.Actors.OrderBy(a => a.Order).ThenBy(a => a.Id) - .Map(a => MediaCards.Mapper.ProjectToViewModel(a, maybeJellyfin, maybeEmby)) - .ToList()) - .IfNone(new List())); - - internal static TelevisionSeasonViewModel ProjectToViewModel( - Season season, - Option maybeJellyfin, - Option maybeEmby) => - new( - season.Id, - season.ShowId, - season.Show.ShowMetadata.HeadOrNone().Map(m => m.Title ?? string.Empty).IfNone(string.Empty), - season.Show.ShowMetadata.HeadOrNone().Map(m => m.Year?.ToString() ?? string.Empty).IfNone(string.Empty), - season.SeasonNumber == 0 ? "Specials" : $"Season {season.SeasonNumber}", - season.SeasonMetadata.HeadOrNone().Map(m => GetPoster(m, maybeJellyfin, maybeEmby)) - .IfNone(string.Empty), - season.Show.ShowMetadata.HeadOrNone().Map(m => GetFanArt(m, maybeJellyfin, maybeEmby)) - .IfNone(string.Empty)); - - private static string GetPoster( - Metadata metadata, - Option maybeJellyfin, - Option maybeEmby) => - GetArtwork(metadata, ArtworkKind.Poster, maybeJellyfin, maybeEmby); - - private static string GetFanArt( - Metadata metadata, - Option maybeJellyfin, - Option maybeEmby) => - GetArtwork(metadata, ArtworkKind.FanArt, maybeJellyfin, maybeEmby); - - private static string GetArtwork( - Metadata metadata, - ArtworkKind artworkKind, - Option maybeJellyfin, - Option maybeEmby) - { - string artwork = Optional(metadata.Artwork.FirstOrDefault(a => a.ArtworkKind == artworkKind)) - .Match(a => a.Path, string.Empty); - - if (maybeJellyfin.IsSome && artwork.StartsWith("jellyfin://")) - { - Url url = JellyfinUrl.RelativeProxyForArtwork(artwork); - if (artworkKind == ArtworkKind.Poster) - { - url.SetQueryParam("fillHeight", 440); - } - - artwork = url; - } - else if (maybeEmby.IsSome && artwork.StartsWith("emby://")) - { - Url url = EmbyUrl.RelativeProxyForArtwork(artwork); - if (artworkKind == ArtworkKind.Poster) - { - url.SetQueryParam("maxHeight", 440); - } - - artwork = url; - } - - return artwork; - } - - private static List LanguagesForShow(List languages) - { - CultureInfo[] allCultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures); - - return languages - .Distinct() + internal static TelevisionShowViewModel ProjectToViewModel( + Show show, + List languages, + Option maybeJellyfin, + Option maybeEmby) => + new( + show.Id, + show.ShowMetadata.HeadOrNone().Map(m => m.Title ?? string.Empty).IfNone(string.Empty), + show.ShowMetadata.HeadOrNone().Map(m => m.Year?.ToString() ?? string.Empty).IfNone(string.Empty), + show.ShowMetadata.HeadOrNone().Map(m => m.Plot ?? string.Empty).IfNone(string.Empty), + show.ShowMetadata.HeadOrNone().Map(m => GetPoster(m, maybeJellyfin, maybeEmby)).IfNone(string.Empty), + show.ShowMetadata.HeadOrNone().Map(m => GetFanArt(m, maybeJellyfin, maybeEmby)).IfNone(string.Empty), + show.ShowMetadata.HeadOrNone().Map(m => m.Genres.Map(g => g.Name).ToList()).IfNone(new List()), + show.ShowMetadata.HeadOrNone().Map(m => m.Tags.Map(g => g.Name).ToList()).IfNone(new List()), + show.ShowMetadata.HeadOrNone().Map(m => m.Studios.Map(s => s.Name).ToList()) + .IfNone(new List()), + show.ShowMetadata.HeadOrNone() .Map( - lang => allCultures.Filter( - ci => string.Equals(ci.ThreeLetterISOLanguageName, lang, StringComparison.OrdinalIgnoreCase))) - .Sequence() - .Flatten() - .ToList(); + m => (m.ContentRating ?? string.Empty).Split("/").Map(s => s.Trim()) + .Where(x => !string.IsNullOrWhiteSpace(x)).ToList()).IfNone(new List()), + LanguagesForShow(languages), + show.ShowMetadata.HeadOrNone() + .Map( + m => m.Actors.OrderBy(a => a.Order).ThenBy(a => a.Id) + .Map(a => MediaCards.Mapper.ProjectToViewModel(a, maybeJellyfin, maybeEmby)) + .ToList()) + .IfNone(new List())); + + internal static TelevisionSeasonViewModel ProjectToViewModel( + Season season, + Option maybeJellyfin, + Option maybeEmby) => + new( + season.Id, + season.ShowId, + season.Show.ShowMetadata.HeadOrNone().Map(m => m.Title ?? string.Empty).IfNone(string.Empty), + season.Show.ShowMetadata.HeadOrNone().Map(m => m.Year?.ToString() ?? string.Empty).IfNone(string.Empty), + season.SeasonNumber == 0 ? "Specials" : $"Season {season.SeasonNumber}", + season.SeasonMetadata.HeadOrNone().Map(m => GetPoster(m, maybeJellyfin, maybeEmby)) + .IfNone(string.Empty), + season.Show.ShowMetadata.HeadOrNone().Map(m => GetFanArt(m, maybeJellyfin, maybeEmby)) + .IfNone(string.Empty)); + + private static string GetPoster( + Metadata metadata, + Option maybeJellyfin, + Option maybeEmby) => + GetArtwork(metadata, ArtworkKind.Poster, maybeJellyfin, maybeEmby); + + private static string GetFanArt( + Metadata metadata, + Option maybeJellyfin, + Option maybeEmby) => + GetArtwork(metadata, ArtworkKind.FanArt, maybeJellyfin, maybeEmby); + + private static string GetArtwork( + Metadata metadata, + ArtworkKind artworkKind, + Option maybeJellyfin, + Option maybeEmby) + { + string artwork = Optional(metadata.Artwork.FirstOrDefault(a => a.ArtworkKind == artworkKind)) + .Match(a => a.Path, string.Empty); + + if (maybeJellyfin.IsSome && artwork.StartsWith("jellyfin://")) + { + Url url = JellyfinUrl.RelativeProxyForArtwork(artwork); + if (artworkKind == ArtworkKind.Poster) + { + url.SetQueryParam("fillHeight", 440); + } + + artwork = url; } + else if (maybeEmby.IsSome && artwork.StartsWith("emby://")) + { + Url url = EmbyUrl.RelativeProxyForArtwork(artwork); + if (artworkKind == ArtworkKind.Poster) + { + url.SetQueryParam("maxHeight", 440); + } + + artwork = url; + } + + return artwork; } -} + + private static List LanguagesForShow(List languages) + { + CultureInfo[] allCultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures); + + return languages + .Distinct() + .Map( + lang => allCultures.Filter( + ci => string.Equals(ci.ThreeLetterISOLanguageName, lang, StringComparison.OrdinalIgnoreCase))) + .Sequence() + .Flatten() + .ToList(); + } +} \ No newline at end of file diff --git a/ErsatzTV.Application/Television/Queries/GetAllTelevisionSeasons.cs b/ErsatzTV.Application/Television/Queries/GetAllTelevisionSeasons.cs index 07b5940d7..176af750b 100644 --- a/ErsatzTV.Application/Television/Queries/GetAllTelevisionSeasons.cs +++ b/ErsatzTV.Application/Television/Queries/GetAllTelevisionSeasons.cs @@ -1,8 +1,5 @@ -using System.Collections.Generic; -using ErsatzTV.Application.MediaItems; -using MediatR; +using ErsatzTV.Application.MediaItems; -namespace ErsatzTV.Application.Television.Queries -{ - public record GetAllTelevisionSeasons : IRequest>; -} +namespace ErsatzTV.Application.Television; + +public record GetAllTelevisionSeasons : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Television/Queries/GetAllTelevisionSeasonsHandler.cs b/ErsatzTV.Application/Television/Queries/GetAllTelevisionSeasonsHandler.cs index 8f490ec8f..3ab340fb4 100644 --- a/ErsatzTV.Application/Television/Queries/GetAllTelevisionSeasonsHandler.cs +++ b/ErsatzTV.Application/Television/Queries/GetAllTelevisionSeasonsHandler.cs @@ -1,26 +1,19 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Application.MediaItems; +using ErsatzTV.Application.MediaItems; using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -using MediatR; using static ErsatzTV.Application.MediaItems.Mapper; -namespace ErsatzTV.Application.Television.Queries +namespace ErsatzTV.Application.Television; + +public class + GetAllTelevisionSeasonsHandler : IRequestHandler> { - public class - GetAllTelevisionSeasonsHandler : IRequestHandler> - { - private readonly ITelevisionRepository _televisionRepository; + private readonly ITelevisionRepository _televisionRepository; - public GetAllTelevisionSeasonsHandler(ITelevisionRepository televisionRepository) => - _televisionRepository = televisionRepository; + public GetAllTelevisionSeasonsHandler(ITelevisionRepository televisionRepository) => + _televisionRepository = televisionRepository; - public Task> Handle( - GetAllTelevisionSeasons request, - CancellationToken cancellationToken) => - _televisionRepository.GetAllSeasons().Map(list => list.Map(ProjectToViewModel).ToList()); - } -} + public Task> Handle( + GetAllTelevisionSeasons request, + CancellationToken cancellationToken) => + _televisionRepository.GetAllSeasons().Map(list => list.Map(ProjectToViewModel).ToList()); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Television/Queries/GetAllTelevisionShows.cs b/ErsatzTV.Application/Television/Queries/GetAllTelevisionShows.cs index aac8e200f..d0df92d3a 100644 --- a/ErsatzTV.Application/Television/Queries/GetAllTelevisionShows.cs +++ b/ErsatzTV.Application/Television/Queries/GetAllTelevisionShows.cs @@ -1,8 +1,5 @@ -using System.Collections.Generic; -using ErsatzTV.Application.MediaItems; -using MediatR; +using ErsatzTV.Application.MediaItems; -namespace ErsatzTV.Application.Television.Queries -{ - public record GetAllTelevisionShows : IRequest>; -} +namespace ErsatzTV.Application.Television; + +public record GetAllTelevisionShows : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Television/Queries/GetAllTelevisionShowsHandler.cs b/ErsatzTV.Application/Television/Queries/GetAllTelevisionShowsHandler.cs index 608befc30..7d673d5eb 100644 --- a/ErsatzTV.Application/Television/Queries/GetAllTelevisionShowsHandler.cs +++ b/ErsatzTV.Application/Television/Queries/GetAllTelevisionShowsHandler.cs @@ -1,25 +1,18 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Application.MediaItems; +using ErsatzTV.Application.MediaItems; using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -using MediatR; using static ErsatzTV.Application.MediaItems.Mapper; -namespace ErsatzTV.Application.Television.Queries +namespace ErsatzTV.Application.Television; + +public class GetAllTelevisionShowsHandler : IRequestHandler> { - public class GetAllTelevisionShowsHandler : IRequestHandler> - { - private readonly ITelevisionRepository _televisionRepository; + private readonly ITelevisionRepository _televisionRepository; - public GetAllTelevisionShowsHandler(ITelevisionRepository televisionRepository) => - _televisionRepository = televisionRepository; + public GetAllTelevisionShowsHandler(ITelevisionRepository televisionRepository) => + _televisionRepository = televisionRepository; - public Task> Handle( - GetAllTelevisionShows request, - CancellationToken cancellationToken) => - _televisionRepository.GetAllShows().Map(list => list.Map(ProjectToViewModel).ToList()); - } -} + public Task> Handle( + GetAllTelevisionShows request, + CancellationToken cancellationToken) => + _televisionRepository.GetAllShows().Map(list => list.Map(ProjectToViewModel).ToList()); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Television/Queries/GetTelevisionSeasonById.cs b/ErsatzTV.Application/Television/Queries/GetTelevisionSeasonById.cs index e551e2168..c0177958f 100644 --- a/ErsatzTV.Application/Television/Queries/GetTelevisionSeasonById.cs +++ b/ErsatzTV.Application/Television/Queries/GetTelevisionSeasonById.cs @@ -1,7 +1,3 @@ -using LanguageExt; -using MediatR; +namespace ErsatzTV.Application.Television; -namespace ErsatzTV.Application.Television.Queries -{ - public record GetTelevisionSeasonById(int SeasonId) : IRequest>; -} +public record GetTelevisionSeasonById(int SeasonId) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Television/Queries/GetTelevisionSeasonByIdHandler.cs b/ErsatzTV.Application/Television/Queries/GetTelevisionSeasonByIdHandler.cs index efac5ff28..de2ce9e43 100644 --- a/ErsatzTV.Application/Television/Queries/GetTelevisionSeasonByIdHandler.cs +++ b/ErsatzTV.Application/Television/Queries/GetTelevisionSeasonByIdHandler.cs @@ -1,39 +1,34 @@ -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -using MediatR; using static ErsatzTV.Application.Television.Mapper; -namespace ErsatzTV.Application.Television.Queries +namespace ErsatzTV.Application.Television; + +public class + GetTelevisionSeasonByIdHandler : IRequestHandler> { - public class - GetTelevisionSeasonByIdHandler : IRequestHandler> + private readonly IMediaSourceRepository _mediaSourceRepository; + private readonly ITelevisionRepository _televisionRepository; + + public GetTelevisionSeasonByIdHandler( + ITelevisionRepository televisionRepository, + IMediaSourceRepository mediaSourceRepository) { - private readonly IMediaSourceRepository _mediaSourceRepository; - private readonly ITelevisionRepository _televisionRepository; - - public GetTelevisionSeasonByIdHandler( - ITelevisionRepository televisionRepository, - IMediaSourceRepository mediaSourceRepository) - { - _televisionRepository = televisionRepository; - _mediaSourceRepository = mediaSourceRepository; - } - - public async Task> Handle( - GetTelevisionSeasonById request, - CancellationToken cancellationToken) - { - Option maybeJellyfin = await _mediaSourceRepository.GetAllJellyfin() - .Map(list => list.HeadOrNone()); - - Option maybeEmby = await _mediaSourceRepository.GetAllEmby() - .Map(list => list.HeadOrNone()); - - return await _televisionRepository.GetSeason(request.SeasonId) - .MapT(s => ProjectToViewModel(s, maybeJellyfin, maybeEmby)); - } + _televisionRepository = televisionRepository; + _mediaSourceRepository = mediaSourceRepository; } -} + + public async Task> Handle( + GetTelevisionSeasonById request, + CancellationToken cancellationToken) + { + Option maybeJellyfin = await _mediaSourceRepository.GetAllJellyfin() + .Map(list => list.HeadOrNone()); + + Option maybeEmby = await _mediaSourceRepository.GetAllEmby() + .Map(list => list.HeadOrNone()); + + return await _televisionRepository.GetSeason(request.SeasonId) + .MapT(s => ProjectToViewModel(s, maybeJellyfin, maybeEmby)); + } +} \ No newline at end of file diff --git a/ErsatzTV.Application/Television/Queries/GetTelevisionShowById.cs b/ErsatzTV.Application/Television/Queries/GetTelevisionShowById.cs index 5d640f391..cc4e6c62a 100644 --- a/ErsatzTV.Application/Television/Queries/GetTelevisionShowById.cs +++ b/ErsatzTV.Application/Television/Queries/GetTelevisionShowById.cs @@ -1,7 +1,3 @@ -using LanguageExt; -using MediatR; +namespace ErsatzTV.Application.Television; -namespace ErsatzTV.Application.Television.Queries -{ - public record GetTelevisionShowById(int Id) : IRequest>; -} +public record GetTelevisionShowById(int Id) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Television/Queries/GetTelevisionShowByIdHandler.cs b/ErsatzTV.Application/Television/Queries/GetTelevisionShowByIdHandler.cs index fce7c3c3e..527b4f8d7 100644 --- a/ErsatzTV.Application/Television/Queries/GetTelevisionShowByIdHandler.cs +++ b/ErsatzTV.Application/Television/Queries/GetTelevisionShowByIdHandler.cs @@ -1,49 +1,43 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; -using LanguageExt; -using MediatR; using static ErsatzTV.Application.Television.Mapper; -namespace ErsatzTV.Application.Television.Queries +namespace ErsatzTV.Application.Television; + +public class GetTelevisionShowByIdHandler : IRequestHandler> { - public class GetTelevisionShowByIdHandler : IRequestHandler> + private readonly IMediaSourceRepository _mediaSourceRepository; + private readonly ISearchRepository _searchRepository; + private readonly ITelevisionRepository _televisionRepository; + + public GetTelevisionShowByIdHandler( + ITelevisionRepository televisionRepository, + ISearchRepository searchRepository, + IMediaSourceRepository mediaSourceRepository) { - private readonly IMediaSourceRepository _mediaSourceRepository; - private readonly ISearchRepository _searchRepository; - private readonly ITelevisionRepository _televisionRepository; - - public GetTelevisionShowByIdHandler( - ITelevisionRepository televisionRepository, - ISearchRepository searchRepository, - IMediaSourceRepository mediaSourceRepository) - { - _televisionRepository = televisionRepository; - _searchRepository = searchRepository; - _mediaSourceRepository = mediaSourceRepository; - } - - public async Task> Handle( - GetTelevisionShowById request, - CancellationToken cancellationToken) - { - Option maybeShow = await _televisionRepository.GetShow(request.Id); - return await maybeShow.Match>>( - async show => - { - Option maybeJellyfin = await _mediaSourceRepository.GetAllJellyfin() - .Map(list => list.HeadOrNone()); - - Option maybeEmby = await _mediaSourceRepository.GetAllEmby() - .Map(list => list.HeadOrNone()); - - List mediaCodes = await _searchRepository.GetLanguagesForShow(show); - List languageCodes = await _searchRepository.GetAllLanguageCodes(mediaCodes); - return ProjectToViewModel(show, languageCodes, maybeJellyfin, maybeEmby); - }, - () => Task.FromResult(Option.None)); - } + _televisionRepository = televisionRepository; + _searchRepository = searchRepository; + _mediaSourceRepository = mediaSourceRepository; } -} + + public async Task> Handle( + GetTelevisionShowById request, + CancellationToken cancellationToken) + { + Option maybeShow = await _televisionRepository.GetShow(request.Id); + return await maybeShow.Match>>( + async show => + { + Option maybeJellyfin = await _mediaSourceRepository.GetAllJellyfin() + .Map(list => list.HeadOrNone()); + + Option maybeEmby = await _mediaSourceRepository.GetAllEmby() + .Map(list => list.HeadOrNone()); + + List mediaCodes = await _searchRepository.GetLanguagesForShow(show); + List languageCodes = await _searchRepository.GetAllLanguageCodes(mediaCodes); + return ProjectToViewModel(show, languageCodes, maybeJellyfin, maybeEmby); + }, + () => Task.FromResult(Option.None)); + } +} \ No newline at end of file diff --git a/ErsatzTV.Application/Television/TelevisionSeasonViewModel.cs b/ErsatzTV.Application/Television/TelevisionSeasonViewModel.cs index 3bfd651b0..6d9d637f7 100644 --- a/ErsatzTV.Application/Television/TelevisionSeasonViewModel.cs +++ b/ErsatzTV.Application/Television/TelevisionSeasonViewModel.cs @@ -1,11 +1,10 @@ -namespace ErsatzTV.Application.Television -{ - public record TelevisionSeasonViewModel( - int Id, - int ShowId, - string Title, - string Year, - string Name, - string Poster, - string FanArt); -} +namespace ErsatzTV.Application.Television; + +public record TelevisionSeasonViewModel( + int Id, + int ShowId, + string Title, + string Year, + string Name, + string Poster, + string FanArt); \ No newline at end of file diff --git a/ErsatzTV.Application/Television/TelevisionShowViewModel.cs b/ErsatzTV.Application/Television/TelevisionShowViewModel.cs index a22329aaa..c351133aa 100644 --- a/ErsatzTV.Application/Television/TelevisionShowViewModel.cs +++ b/ErsatzTV.Application/Television/TelevisionShowViewModel.cs @@ -1,20 +1,18 @@ -using System.Collections.Generic; -using System.Globalization; +using System.Globalization; using ErsatzTV.Application.MediaCards; -namespace ErsatzTV.Application.Television -{ - public record TelevisionShowViewModel( - int Id, - string Title, - string Year, - string Plot, - string Poster, - string FanArt, - List Genres, - List Tags, - List Studios, - List ContentRatings, - List Languages, - List Actors); -} +namespace ErsatzTV.Application.Television; + +public record TelevisionShowViewModel( + int Id, + string Title, + string Year, + string Plot, + string Poster, + string FanArt, + List Genres, + List Tags, + List Studios, + List ContentRatings, + List Languages, + List Actors); \ No newline at end of file diff --git a/ErsatzTV.Application/Validators/GetMemberName.cs b/ErsatzTV.Application/Validators/GetMemberName.cs index a57fbf8d0..25fb11498 100644 --- a/ErsatzTV.Application/Validators/GetMemberName.cs +++ b/ErsatzTV.Application/Validators/GetMemberName.cs @@ -1,20 +1,18 @@ -using System; -using System.Linq.Expressions; +using System.Linq.Expressions; using System.Reflection; -namespace ErsatzTV -{ - public static partial class Validators - { - private static string GetMemberName(Expression> expression) - { - var member = expression.Body as MemberExpression; - if (member?.Member is PropertyInfo propertyInfo) - { - return propertyInfo.Name; - } +namespace ErsatzTV; - throw new ArgumentException("Expression is not a property"); +public static partial class Validators +{ + private static string GetMemberName(Expression> expression) + { + var member = expression.Body as MemberExpression; + if (member?.Member is PropertyInfo propertyInfo) + { + return propertyInfo.Name; } + + throw new ArgumentException("Expression is not a property"); } -} +} \ No newline at end of file diff --git a/ErsatzTV.Application/Validators/NumericValidation.cs b/ErsatzTV.Application/Validators/NumericValidation.cs index 4bdd8b043..377c8c28d 100644 --- a/ErsatzTV.Application/Validators/NumericValidation.cs +++ b/ErsatzTV.Application/Validators/NumericValidation.cs @@ -1,19 +1,15 @@ -using System; -using System.Linq.Expressions; +using System.Linq.Expressions; using ErsatzTV.Core; -using LanguageExt; -using static LanguageExt.Prelude; -namespace ErsatzTV +namespace ErsatzTV; + +public static partial class Validators { - public static partial class Validators - { - public static Func>, Validation> - AtLeast(this T input, int minimum) => - value => Optional(value) - .Map(i => i.Compile()(input)) - .Where(i => i >= minimum) - .ToValidation( - $"[{GetMemberName(value)}] must be greater or equal to {minimum}"); - } -} + public static Func>, Validation> + AtLeast(this T input, int minimum) => + value => Optional(value) + .Map(i => i.Compile()(input)) + .Where(i => i >= minimum) + .ToValidation( + $"[{GetMemberName(value)}] must be greater or equal to {minimum}"); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Validators/StringValidation.cs b/ErsatzTV.Application/Validators/StringValidation.cs index 2d4f8fcb8..12a749b60 100644 --- a/ErsatzTV.Application/Validators/StringValidation.cs +++ b/ErsatzTV.Application/Validators/StringValidation.cs @@ -1,24 +1,20 @@ -using System; -using System.Linq.Expressions; +using System.Linq.Expressions; using ErsatzTV.Core; -using LanguageExt; -using static LanguageExt.Prelude; -namespace ErsatzTV +namespace ErsatzTV; + +public static partial class Validators { - public static partial class Validators - { - public static Func>, Validation> NotLongerThan( - this T input, - int maxLength) => - expression => Optional(expression) - .Map(exp => exp.Compile()(input)) - .Where(s => s.Length <= maxLength) - .ToValidation($"[{GetMemberName(expression)}] must not be longer than {maxLength}"); + public static Func>, Validation> NotLongerThan( + this T input, + int maxLength) => + expression => Optional(expression) + .Map(exp => exp.Compile()(input)) + .Where(s => s.Length <= maxLength) + .ToValidation($"[{GetMemberName(expression)}] must not be longer than {maxLength}"); - public static Validation NotEmpty(this T input, Expression> expression) => - Optional(expression.Compile()(input)) - .Where(s => !string.IsNullOrWhiteSpace(s)) - .ToValidation($"[{GetMemberName(expression)}] is an empty string"); - } -} + public static Validation NotEmpty(this T input, Expression> expression) => + Optional(expression.Compile()(input)) + .Where(s => !string.IsNullOrWhiteSpace(s)) + .ToValidation($"[{GetMemberName(expression)}] is an empty string"); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Watermarks/Commands/CopyWatermark.cs b/ErsatzTV.Application/Watermarks/Commands/CopyWatermark.cs index 831992032..33ff57d8e 100644 --- a/ErsatzTV.Application/Watermarks/Commands/CopyWatermark.cs +++ b/ErsatzTV.Application/Watermarks/Commands/CopyWatermark.cs @@ -1,9 +1,6 @@ using ErsatzTV.Core; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.Watermarks.Commands -{ - public record CopyWatermark - (int WatermarkId, string Name) : IRequest>; -} +namespace ErsatzTV.Application.Watermarks; + +public record CopyWatermark + (int WatermarkId, string Name) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Watermarks/Commands/CopyWatermarkHandler.cs b/ErsatzTV.Application/Watermarks/Commands/CopyWatermarkHandler.cs index ecf8d2114..9e6882028 100644 --- a/ErsatzTV.Application/Watermarks/Commands/CopyWatermarkHandler.cs +++ b/ErsatzTV.Application/Watermarks/Commands/CopyWatermarkHandler.cs @@ -1,54 +1,49 @@ -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Infrastructure.Data; -using LanguageExt; -using MediatR; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; using static ErsatzTV.Application.Watermarks.Mapper; -namespace ErsatzTV.Application.Watermarks.Commands +namespace ErsatzTV.Application.Watermarks; + +public class CopyWatermarkHandler : + IRequestHandler> { - public class CopyWatermarkHandler : - IRequestHandler> + private readonly IDbContextFactory _dbContextFactory; + + public CopyWatermarkHandler(IDbContextFactory dbContextFactory) => + _dbContextFactory = dbContextFactory; + + public Task> Handle( + CopyWatermark request, + CancellationToken cancellationToken) => + Validate(request) + .MapT(PerformCopy) + .Bind(v => v.ToEitherAsync()); + + private async Task PerformCopy(CopyWatermark request) { - private readonly IDbContextFactory _dbContextFactory; + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + ChannelWatermark channelWatermark = await dbContext.ChannelWatermarks.FindAsync(request.WatermarkId); - public CopyWatermarkHandler(IDbContextFactory dbContextFactory) => - _dbContextFactory = dbContextFactory; + PropertyValues values = dbContext.Entry(channelWatermark).CurrentValues.Clone(); + values["Id"] = 0; - public Task> Handle( - CopyWatermark request, - CancellationToken cancellationToken) => - Validate(request) - .MapT(PerformCopy) - .Bind(v => v.ToEitherAsync()); + var clone = new ChannelWatermark(); + await dbContext.AddAsync(clone); + dbContext.Entry(clone).CurrentValues.SetValues(values); + clone.Name = request.Name; - private async Task PerformCopy(CopyWatermark request) - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - ChannelWatermark channelWatermark = await dbContext.ChannelWatermarks.FindAsync(request.WatermarkId); + await dbContext.SaveChangesAsync(); - PropertyValues values = dbContext.Entry(channelWatermark).CurrentValues.Clone(); - values["Id"] = 0; - - var clone = new ChannelWatermark(); - await dbContext.AddAsync(clone); - dbContext.Entry(clone).CurrentValues.SetValues(values); - clone.Name = request.Name; - - await dbContext.SaveChangesAsync(); - - return ProjectToViewModel(clone); - } - - private static Task> Validate(CopyWatermark request) => - ValidateName(request).AsTask().MapT(_ => request); - - private static Validation ValidateName(CopyWatermark request) => - request.NotEmpty(x => x.Name) - .Bind(_ => request.NotLongerThan(50)(x => x.Name)); + return ProjectToViewModel(clone); } -} + + private static Task> Validate(CopyWatermark request) => + ValidateName(request).AsTask().MapT(_ => request); + + private static Validation ValidateName(CopyWatermark request) => + request.NotEmpty(x => x.Name) + .Bind(_ => request.NotLongerThan(50)(x => x.Name)); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Watermarks/Commands/CreateWatermark.cs b/ErsatzTV.Application/Watermarks/Commands/CreateWatermark.cs index edb34f3ac..73351e27e 100644 --- a/ErsatzTV.Application/Watermarks/Commands/CreateWatermark.cs +++ b/ErsatzTV.Application/Watermarks/Commands/CreateWatermark.cs @@ -1,24 +1,21 @@ using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.FFmpeg.State; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.Watermarks.Commands -{ - public record CreateWatermark( - string Name, - string Image, - ChannelWatermarkMode Mode, - ChannelWatermarkImageSource ImageSource, - WatermarkLocation Location, - WatermarkSize Size, - int Width, - int HorizontalMargin, - int VerticalMargin, - int FrequencyMinutes, - int DurationSeconds, - int Opacity) : IRequest>; +namespace ErsatzTV.Application.Watermarks; - public record CreateWatermarkResult(int WatermarkId) : EntityIdResult(WatermarkId); -} +public record CreateWatermark( + string Name, + string Image, + ChannelWatermarkMode Mode, + ChannelWatermarkImageSource ImageSource, + WatermarkLocation Location, + WatermarkSize Size, + int Width, + int HorizontalMargin, + int VerticalMargin, + int FrequencyMinutes, + int DurationSeconds, + int Opacity) : IRequest>; + +public record CreateWatermarkResult(int WatermarkId) : EntityIdResult(WatermarkId); \ No newline at end of file diff --git a/ErsatzTV.Application/Watermarks/Commands/CreateWatermarkHandler.cs b/ErsatzTV.Application/Watermarks/Commands/CreateWatermarkHandler.cs index 595325e3c..16374483a 100644 --- a/ErsatzTV.Application/Watermarks/Commands/CreateWatermarkHandler.cs +++ b/ErsatzTV.Application/Watermarks/Commands/CreateWatermarkHandler.cs @@ -1,60 +1,55 @@ -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Infrastructure.Data; -using LanguageExt; -using MediatR; using Microsoft.EntityFrameworkCore; -namespace ErsatzTV.Application.Watermarks.Commands +namespace ErsatzTV.Application.Watermarks; + +public class CreateWatermarkHandler : IRequestHandler> { - public class CreateWatermarkHandler : IRequestHandler> + private readonly IDbContextFactory _dbContextFactory; + + public CreateWatermarkHandler(IDbContextFactory dbContextFactory) => + _dbContextFactory = dbContextFactory; + + public async Task> Handle( + CreateWatermark request, + CancellationToken cancellationToken) { - private readonly IDbContextFactory _dbContextFactory; - - public CreateWatermarkHandler(IDbContextFactory dbContextFactory) => - _dbContextFactory = dbContextFactory; - - public async Task> Handle( - CreateWatermark request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - Validation validation = Validate(request); - return await validation.Apply(profile => PersistChannelWatermark(dbContext, profile)); - } - - private static async Task PersistChannelWatermark( - TvContext dbContext, - ChannelWatermark watermark) - { - await dbContext.ChannelWatermarks.AddAsync(watermark); - await dbContext.SaveChangesAsync(); - return new CreateWatermarkResult(watermark.Id); - } - - private static Validation Validate(CreateWatermark request) => - ValidateName(request) - .Map( - _ => new ChannelWatermark - { - Name = request.Name, - Image = request.ImageSource == ChannelWatermarkImageSource.Custom ? request.Image : null, - Mode = request.Mode, - ImageSource = request.ImageSource, - Location = request.Location, - Size = request.Size, - WidthPercent = request.Width, - HorizontalMarginPercent = request.HorizontalMargin, - VerticalMarginPercent = request.VerticalMargin, - FrequencyMinutes = request.FrequencyMinutes, - DurationSeconds = request.DurationSeconds, - Opacity = request.Opacity - }); - - private static Validation ValidateName(CreateWatermark request) => - request.NotEmpty(x => x.Name) - .Bind(_ => request.NotLongerThan(50)(x => x.Name)); + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + Validation validation = Validate(request); + return await LanguageExtensions.Apply(validation, profile => PersistChannelWatermark(dbContext, profile)); } -} + + private static async Task PersistChannelWatermark( + TvContext dbContext, + ChannelWatermark watermark) + { + await dbContext.ChannelWatermarks.AddAsync(watermark); + await dbContext.SaveChangesAsync(); + return new CreateWatermarkResult(watermark.Id); + } + + private static Validation Validate(CreateWatermark request) => + ValidateName(request) + .Map( + _ => new ChannelWatermark + { + Name = request.Name, + Image = request.ImageSource == ChannelWatermarkImageSource.Custom ? request.Image : null, + Mode = request.Mode, + ImageSource = request.ImageSource, + Location = request.Location, + Size = request.Size, + WidthPercent = request.Width, + HorizontalMarginPercent = request.HorizontalMargin, + VerticalMarginPercent = request.VerticalMargin, + FrequencyMinutes = request.FrequencyMinutes, + DurationSeconds = request.DurationSeconds, + Opacity = request.Opacity + }); + + private static Validation ValidateName(CreateWatermark request) => + request.NotEmpty(x => x.Name) + .Bind(_ => request.NotLongerThan(50)(x => x.Name)); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Watermarks/Commands/DeleteWatermark.cs b/ErsatzTV.Application/Watermarks/Commands/DeleteWatermark.cs index 65e684181..52cee8686 100644 --- a/ErsatzTV.Application/Watermarks/Commands/DeleteWatermark.cs +++ b/ErsatzTV.Application/Watermarks/Commands/DeleteWatermark.cs @@ -1,7 +1,5 @@ using ErsatzTV.Core; -using LanguageExt; -namespace ErsatzTV.Application.Watermarks.Commands -{ - public record DeleteWatermark(int WatermarkId) : MediatR.IRequest>; -} +namespace ErsatzTV.Application.Watermarks; + +public record DeleteWatermark(int WatermarkId) : MediatR.IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Watermarks/Commands/DeleteWatermarkHandler.cs b/ErsatzTV.Application/Watermarks/Commands/DeleteWatermarkHandler.cs index 17c58d2e7..3dd3b68fb 100644 --- a/ErsatzTV.Application/Watermarks/Commands/DeleteWatermarkHandler.cs +++ b/ErsatzTV.Application/Watermarks/Commands/DeleteWatermarkHandler.cs @@ -1,45 +1,40 @@ -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; -using LanguageExt; using Microsoft.EntityFrameworkCore; -using Unit = LanguageExt.Unit; -namespace ErsatzTV.Application.Watermarks.Commands +namespace ErsatzTV.Application.Watermarks; + +public class DeleteWatermarkHandler : MediatR.IRequestHandler> { - public class DeleteWatermarkHandler : MediatR.IRequestHandler> + private readonly IDbContextFactory _dbContextFactory; + + public DeleteWatermarkHandler(IDbContextFactory dbContextFactory) => + _dbContextFactory = dbContextFactory; + + public async Task> Handle( + DeleteWatermark request, + CancellationToken cancellationToken) { - private readonly IDbContextFactory _dbContextFactory; - - public DeleteWatermarkHandler(IDbContextFactory dbContextFactory) => - _dbContextFactory = dbContextFactory; - - public async Task> Handle( - DeleteWatermark request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - Validation validation = await WatermarkMustExist(dbContext, request); - return await validation.Apply(p => DoDeletion(dbContext, p)); - } - - private static async Task DoDeletion(TvContext dbContext, ChannelWatermark watermark) - { - await dbContext.Database.ExecuteSqlRawAsync( - $"UPDATE Channel SET WatermarkId = NULL WHERE WatermarkId = {watermark.Id}"); - dbContext.ChannelWatermarks.Remove(watermark); - await dbContext.SaveChangesAsync(); - return Unit.Default; - } - - private static Task> WatermarkMustExist( - TvContext dbContext, - DeleteWatermark request) => - dbContext.ChannelWatermarks - .SelectOneAsync(p => p.Id, p => p.Id == request.WatermarkId) - .Map(o => o.ToValidation($"Watermark {request.WatermarkId} does not exist")); + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + Validation validation = await WatermarkMustExist(dbContext, request); + return await LanguageExtensions.Apply(validation, p => DoDeletion(dbContext, p)); } -} + + private static async Task DoDeletion(TvContext dbContext, ChannelWatermark watermark) + { + await dbContext.Database.ExecuteSqlRawAsync( + $"UPDATE Channel SET WatermarkId = NULL WHERE WatermarkId = {watermark.Id}"); + dbContext.ChannelWatermarks.Remove(watermark); + await dbContext.SaveChangesAsync(); + return Unit.Default; + } + + private static Task> WatermarkMustExist( + TvContext dbContext, + DeleteWatermark request) => + dbContext.ChannelWatermarks + .SelectOneAsync(p => p.Id, p => p.Id == request.WatermarkId) + .Map(o => o.ToValidation($"Watermark {request.WatermarkId} does not exist")); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Watermarks/Commands/UpdateWatermark.cs b/ErsatzTV.Application/Watermarks/Commands/UpdateWatermark.cs index 6ec1744c1..d9b3837a9 100644 --- a/ErsatzTV.Application/Watermarks/Commands/UpdateWatermark.cs +++ b/ErsatzTV.Application/Watermarks/Commands/UpdateWatermark.cs @@ -1,25 +1,22 @@ using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.FFmpeg.State; -using LanguageExt; -using MediatR; -namespace ErsatzTV.Application.Watermarks.Commands -{ - public record UpdateWatermark( - int Id, - string Name, - string Image, - ChannelWatermarkMode Mode, - ChannelWatermarkImageSource ImageSource, - WatermarkLocation Location, - WatermarkSize Size, - int Width, - int HorizontalMargin, - int VerticalMargin, - int FrequencyMinutes, - int DurationSeconds, - int Opacity) : IRequest>; +namespace ErsatzTV.Application.Watermarks; - public record UpdateWatermarkResult(int WatermarkId) : EntityIdResult(WatermarkId); -} +public record UpdateWatermark( + int Id, + string Name, + string Image, + ChannelWatermarkMode Mode, + ChannelWatermarkImageSource ImageSource, + WatermarkLocation Location, + WatermarkSize Size, + int Width, + int HorizontalMargin, + int VerticalMargin, + int FrequencyMinutes, + int DurationSeconds, + int Opacity) : IRequest>; + +public record UpdateWatermarkResult(int WatermarkId) : EntityIdResult(WatermarkId); \ No newline at end of file diff --git a/ErsatzTV.Application/Watermarks/Commands/UpdateWatermarkHandler.cs b/ErsatzTV.Application/Watermarks/Commands/UpdateWatermarkHandler.cs index 15a75a1dc..de6c6fef7 100644 --- a/ErsatzTV.Application/Watermarks/Commands/UpdateWatermarkHandler.cs +++ b/ErsatzTV.Application/Watermarks/Commands/UpdateWatermarkHandler.cs @@ -1,67 +1,62 @@ -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; -using LanguageExt; -using MediatR; using Microsoft.EntityFrameworkCore; -namespace ErsatzTV.Application.Watermarks.Commands +namespace ErsatzTV.Application.Watermarks; + +public class UpdateWatermarkHandler : IRequestHandler> { - public class UpdateWatermarkHandler : IRequestHandler> + private readonly IDbContextFactory _dbContextFactory; + + public UpdateWatermarkHandler(IDbContextFactory dbContextFactory) => + _dbContextFactory = dbContextFactory; + + public async Task> Handle( + UpdateWatermark request, + CancellationToken cancellationToken) { - private readonly IDbContextFactory _dbContextFactory; - - public UpdateWatermarkHandler(IDbContextFactory dbContextFactory) => - _dbContextFactory = dbContextFactory; - - public async Task> Handle( - UpdateWatermark request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - Validation validation = await Validate(dbContext, request); - return await validation.Apply(p => ApplyUpdateRequest(dbContext, p, request)); - } - - private static async Task ApplyUpdateRequest( - TvContext dbContext, - ChannelWatermark p, - UpdateWatermark update) - { - p.Name = update.Name; - p.Image = update.ImageSource == ChannelWatermarkImageSource.Custom ? update.Image : null; - p.Mode = update.Mode; - p.ImageSource = update.ImageSource; - p.Location = update.Location; - p.Size = update.Size; - p.WidthPercent = update.Width; - p.HorizontalMarginPercent = update.HorizontalMargin; - p.VerticalMarginPercent = update.VerticalMargin; - p.FrequencyMinutes = update.FrequencyMinutes; - p.DurationSeconds = update.DurationSeconds; - p.Opacity = update.Opacity; - await dbContext.SaveChangesAsync(); - return new UpdateWatermarkResult(p.Id); - } - - private static async Task> Validate( - TvContext dbContext, - UpdateWatermark request) => - (await WatermarkMustExist(dbContext, request), ValidateName(request)) - .Apply((watermark, _) => watermark); - - private static Task> WatermarkMustExist( - TvContext dbContext, - UpdateWatermark updateWatermark) => - dbContext.ChannelWatermarks - .SelectOneAsync(p => p.Id, p => p.Id == updateWatermark.Id) - .Map(o => o.ToValidation("Watermark does not exist.")); - - private static Validation ValidateName(UpdateWatermark updateWatermark) => - updateWatermark.NotEmpty(x => x.Name) - .Bind(_ => updateWatermark.NotLongerThan(50)(x => x.Name)); + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + Validation validation = await Validate(dbContext, request); + return await LanguageExtensions.Apply(validation, p => ApplyUpdateRequest(dbContext, p, request)); } -} + + private static async Task ApplyUpdateRequest( + TvContext dbContext, + ChannelWatermark p, + UpdateWatermark update) + { + p.Name = update.Name; + p.Image = update.ImageSource == ChannelWatermarkImageSource.Custom ? update.Image : null; + p.Mode = update.Mode; + p.ImageSource = update.ImageSource; + p.Location = update.Location; + p.Size = update.Size; + p.WidthPercent = update.Width; + p.HorizontalMarginPercent = update.HorizontalMargin; + p.VerticalMarginPercent = update.VerticalMargin; + p.FrequencyMinutes = update.FrequencyMinutes; + p.DurationSeconds = update.DurationSeconds; + p.Opacity = update.Opacity; + await dbContext.SaveChangesAsync(); + return new UpdateWatermarkResult(p.Id); + } + + private static async Task> Validate( + TvContext dbContext, + UpdateWatermark request) => + (await WatermarkMustExist(dbContext, request), ValidateName(request)) + .Apply((watermark, _) => watermark); + + private static Task> WatermarkMustExist( + TvContext dbContext, + UpdateWatermark updateWatermark) => + dbContext.ChannelWatermarks + .SelectOneAsync(p => p.Id, p => p.Id == updateWatermark.Id) + .Map(o => o.ToValidation("Watermark does not exist.")); + + private static Validation ValidateName(UpdateWatermark updateWatermark) => + updateWatermark.NotEmpty(x => x.Name) + .Bind(_ => updateWatermark.NotLongerThan(50)(x => x.Name)); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Watermarks/Mapper.cs b/ErsatzTV.Application/Watermarks/Mapper.cs index 360b72533..e59821416 100644 --- a/ErsatzTV.Application/Watermarks/Mapper.cs +++ b/ErsatzTV.Application/Watermarks/Mapper.cs @@ -1,23 +1,22 @@ using ErsatzTV.Core.Domain; -namespace ErsatzTV.Application.Watermarks +namespace ErsatzTV.Application.Watermarks; + +internal static class Mapper { - internal static class Mapper - { - public static WatermarkViewModel ProjectToViewModel(ChannelWatermark watermark) => - new( - watermark.Id, - watermark.Image, - watermark.Name, - watermark.Mode, - watermark.ImageSource, - watermark.Location, - watermark.Size, - watermark.WidthPercent, - watermark.HorizontalMarginPercent, - watermark.VerticalMarginPercent, - watermark.FrequencyMinutes, - watermark.DurationSeconds, - watermark.Opacity); - } -} + public static WatermarkViewModel ProjectToViewModel(ChannelWatermark watermark) => + new( + watermark.Id, + watermark.Image, + watermark.Name, + watermark.Mode, + watermark.ImageSource, + watermark.Location, + watermark.Size, + watermark.WidthPercent, + watermark.HorizontalMarginPercent, + watermark.VerticalMarginPercent, + watermark.FrequencyMinutes, + watermark.DurationSeconds, + watermark.Opacity); +} \ No newline at end of file diff --git a/ErsatzTV.Application/Watermarks/Queries/GetAllWatermarks.cs b/ErsatzTV.Application/Watermarks/Queries/GetAllWatermarks.cs index 0626d4a56..728d88610 100644 --- a/ErsatzTV.Application/Watermarks/Queries/GetAllWatermarks.cs +++ b/ErsatzTV.Application/Watermarks/Queries/GetAllWatermarks.cs @@ -1,7 +1,3 @@ -using System.Collections.Generic; -using MediatR; +namespace ErsatzTV.Application.Watermarks; -namespace ErsatzTV.Application.Watermarks.Queries -{ - public record GetAllWatermarks : IRequest>; -} +public record GetAllWatermarks : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Watermarks/Queries/GetAllWatermarksHandler.cs b/ErsatzTV.Application/Watermarks/Queries/GetAllWatermarksHandler.cs index 75c9cc40f..92910be44 100644 --- a/ErsatzTV.Application/Watermarks/Queries/GetAllWatermarksHandler.cs +++ b/ErsatzTV.Application/Watermarks/Queries/GetAllWatermarksHandler.cs @@ -1,30 +1,23 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Infrastructure.Data; -using LanguageExt; -using MediatR; +using ErsatzTV.Infrastructure.Data; using Microsoft.EntityFrameworkCore; using static ErsatzTV.Application.Watermarks.Mapper; -namespace ErsatzTV.Application.Watermarks.Queries +namespace ErsatzTV.Application.Watermarks; + +public class GetAllWatermarksHandler : IRequestHandler> { - public class GetAllWatermarksHandler : IRequestHandler> + private readonly IDbContextFactory _dbContextFactory; + + public GetAllWatermarksHandler(IDbContextFactory dbContextFactory) => + _dbContextFactory = dbContextFactory; + + public async Task> Handle( + GetAllWatermarks request, + CancellationToken cancellationToken) { - private readonly IDbContextFactory _dbContextFactory; - - public GetAllWatermarksHandler(IDbContextFactory dbContextFactory) => - _dbContextFactory = dbContextFactory; - - public async Task> Handle( - GetAllWatermarks request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - return await dbContext.ChannelWatermarks - .ToListAsync(cancellationToken) - .Map(list => list.Map(ProjectToViewModel).ToList()); - } + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + return await dbContext.ChannelWatermarks + .ToListAsync(cancellationToken) + .Map(list => list.Map(ProjectToViewModel).ToList()); } -} +} \ No newline at end of file diff --git a/ErsatzTV.Application/Watermarks/Queries/GetWatermarkById.cs b/ErsatzTV.Application/Watermarks/Queries/GetWatermarkById.cs index 356b87d2d..aedac4092 100644 --- a/ErsatzTV.Application/Watermarks/Queries/GetWatermarkById.cs +++ b/ErsatzTV.Application/Watermarks/Queries/GetWatermarkById.cs @@ -1,7 +1,3 @@ -using LanguageExt; -using MediatR; +namespace ErsatzTV.Application.Watermarks; -namespace ErsatzTV.Application.Watermarks.Queries -{ - public record GetWatermarkById(int Id) : IRequest>; -} +public record GetWatermarkById(int Id) : IRequest>; \ No newline at end of file diff --git a/ErsatzTV.Application/Watermarks/Queries/GetWatermarkByIdHandler.cs b/ErsatzTV.Application/Watermarks/Queries/GetWatermarkByIdHandler.cs index 9d31b90c3..3635c90af 100644 --- a/ErsatzTV.Application/Watermarks/Queries/GetWatermarkByIdHandler.cs +++ b/ErsatzTV.Application/Watermarks/Queries/GetWatermarkByIdHandler.cs @@ -1,29 +1,24 @@ -using System.Threading; -using System.Threading.Tasks; -using ErsatzTV.Infrastructure.Data; +using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; -using LanguageExt; -using MediatR; using Microsoft.EntityFrameworkCore; using static ErsatzTV.Application.Watermarks.Mapper; -namespace ErsatzTV.Application.Watermarks.Queries +namespace ErsatzTV.Application.Watermarks; + +public class GetWatermarkByIdHandler : IRequestHandler> { - public class GetWatermarkByIdHandler : IRequestHandler> + private readonly IDbContextFactory _dbContextFactory; + + public GetWatermarkByIdHandler(IDbContextFactory dbContextFactory) => + _dbContextFactory = dbContextFactory; + + public async Task> Handle( + GetWatermarkById request, + CancellationToken cancellationToken) { - private readonly IDbContextFactory _dbContextFactory; - - public GetWatermarkByIdHandler(IDbContextFactory dbContextFactory) => - _dbContextFactory = dbContextFactory; - - public async Task> Handle( - GetWatermarkById request, - CancellationToken cancellationToken) - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - return await dbContext.ChannelWatermarks - .SelectOneAsync(w => w.Id, w => w.Id == request.Id) - .MapT(ProjectToViewModel); - } + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + return await dbContext.ChannelWatermarks + .SelectOneAsync(w => w.Id, w => w.Id == request.Id) + .MapT(ProjectToViewModel); } -} +} \ No newline at end of file diff --git a/ErsatzTV.Application/Watermarks/WatermarkViewModel.cs b/ErsatzTV.Application/Watermarks/WatermarkViewModel.cs index f2105aaf9..b040baa6a 100644 --- a/ErsatzTV.Application/Watermarks/WatermarkViewModel.cs +++ b/ErsatzTV.Application/Watermarks/WatermarkViewModel.cs @@ -1,21 +1,20 @@ using ErsatzTV.Core.Domain; using ErsatzTV.FFmpeg.State; -namespace ErsatzTV.Application.Watermarks -{ - public record WatermarkViewModel( - int Id, - string Image, - string Name, - ChannelWatermarkMode Mode, - ChannelWatermarkImageSource ImageSource, - WatermarkLocation Location, - WatermarkSize Size, - int Width, - int HorizontalMargin, - int VerticalMargin, - int FrequencyMinutes, - int DurationSeconds, - int Opacity - ); -} +namespace ErsatzTV.Application.Watermarks; + +public record WatermarkViewModel( + int Id, + string Image, + string Name, + ChannelWatermarkMode Mode, + ChannelWatermarkImageSource ImageSource, + WatermarkLocation Location, + WatermarkSize Size, + int Width, + int HorizontalMargin, + int VerticalMargin, + int FrequencyMinutes, + int DurationSeconds, + int Opacity +); \ No newline at end of file diff --git a/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj b/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj index 75ea6d8b3..d6993f80f 100644 --- a/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj +++ b/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj @@ -3,6 +3,7 @@ net6.0 VSTHRD200 + enable diff --git a/ErsatzTV.Core.Tests/FFmpeg/FFmpegComplexFilterBuilderTests.cs b/ErsatzTV.Core.Tests/FFmpeg/FFmpegComplexFilterBuilderTests.cs index e794dc21a..4f68a8d59 100644 --- a/ErsatzTV.Core.Tests/FFmpeg/FFmpegComplexFilterBuilderTests.cs +++ b/ErsatzTV.Core.Tests/FFmpeg/FFmpegComplexFilterBuilderTests.cs @@ -1,782 +1,777 @@ -using System; -using System.Collections.Generic; -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; using ErsatzTV.Core.FFmpeg; using ErsatzTV.FFmpeg.State; using FluentAssertions; -using LanguageExt; using NUnit.Framework; -using static LanguageExt.Prelude; -namespace ErsatzTV.Core.Tests.FFmpeg +namespace ErsatzTV.Core.Tests.FFmpeg; + +[TestFixture] +public class FFmpegComplexFilterBuilderTests { [TestFixture] - public class FFmpegComplexFilterBuilderTests + public class Build { - [TestFixture] - public class Build + [Test] + public void Should_Return_None_With_No_Filters() { - [Test] - public void Should_Return_None_With_No_Filters() - { - var builder = new FFmpegComplexFilterBuilder(); + var builder = new FFmpegComplexFilterBuilder(); - Option result = builder.Build(false, 0, 0, 0, 1, false); + Option result = builder.Build(false, 0, 0, 0, 1, false); - result.IsNone.Should().BeTrue(); - } + result.IsNone.Should().BeTrue(); + } - [Test] - public void Should_Return_Audio_Filter_With_AudioDuration() - { - var duration = TimeSpan.FromMilliseconds(1000.1); - FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder() - .WithAlignedAudio(duration); + [Test] + public void Should_Return_Audio_Filter_With_AudioDuration() + { + var duration = TimeSpan.FromMilliseconds(1000.1); + FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder() + .WithAlignedAudio(duration); - Option result = builder.Build(false, 0, 0, 0, 1, false); + Option result = builder.Build(false, 0, 0, 0, 1, false); - result.IsSome.Should().BeTrue(); - result.IfSome( - filter => - { - filter.ComplexFilter.Should().Be("[0:1]apad=whole_dur=1000.1ms[a]"); - filter.AudioLabel.Should().Be("[a]"); - filter.VideoLabel.Should().Be("0:0"); - }); - } + result.IsSome.Should().BeTrue(); + result.IfSome( + filter => + { + filter.ComplexFilter.Should().Be("[0:1]apad=whole_dur=1000.1ms[a]"); + filter.AudioLabel.Should().Be("[a]"); + filter.VideoLabel.Should().Be("0:0"); + }); + } - [Test] - // this needs to be a culture where '.' is a group separator - [SetCulture("it-IT")] - public void Should_Return_Audio_Filter_With_AudioDuration_Decimal() + [Test] + // this needs to be a culture where '.' is a group separator + [SetCulture("it-IT")] + public void Should_Return_Audio_Filter_With_AudioDuration_Decimal() + { + var duration = TimeSpan.FromMilliseconds(1000.1); + FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder() + .WithAlignedAudio(duration); + + Option result = builder.Build(false, 0, 0, 0, 1, false); + + result.IsSome.Should().BeTrue(); + result.IfSome( + filter => + { + filter.ComplexFilter.Should().Be("[0:1]apad=whole_dur=1000.1ms[a]"); + filter.AudioLabel.Should().Be("[a]"); + filter.VideoLabel.Should().Be("0:0"); + }); + } + + [Test] + public void Should_Return_Audio_And_Video_Filter() + { + var duration = TimeSpan.FromMinutes(54); + FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder() + .WithAlignedAudio(duration) + .WithDeinterlace(true); + + Option result = builder.Build(false, 0, 0, 0, 1, false); + + result.IsSome.Should().BeTrue(); + result.IfSome( + filter => + { + filter.ComplexFilter.Should().Be( + $"[0:1]apad=whole_dur={duration.TotalMilliseconds}ms[a];[0:0]yadif=1[v]"); + filter.AudioLabel.Should().Be("[a]"); + filter.VideoLabel.Should().Be("[v]"); + }); + } + + [Test] + [TestCase(true, false, false, "[0:0]yadif=1[v]", "[v]")] + [TestCase(true, true, false, "[0:0]yadif=1,scale=1920:1000:flags=fast_bilinear,setsar=1[v]", "[v]")] + [TestCase(true, false, true, "[0:0]yadif=1,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2[v]", "[v]")] + [TestCase( + true, + true, + true, + "[0:0]yadif=1,scale=1920:1000:flags=fast_bilinear,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2[v]", + "[v]")] + [TestCase(false, true, false, "[0:0]scale=1920:1000:flags=fast_bilinear,setsar=1[v]", "[v]")] + [TestCase(false, false, true, "[0:0]setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2[v]", "[v]")] + [TestCase( + false, + true, + true, + "[0:0]scale=1920:1000:flags=fast_bilinear,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2[v]", + "[v]")] + public void Should_Return_Software_Video_Filter( + bool deinterlace, + bool scale, + bool pad, + string expectedVideoFilter, + string expectedVideoLabel) + { + FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder() + .WithDeinterlace(deinterlace); + + if (scale) { - var duration = TimeSpan.FromMilliseconds(1000.1); - FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder() - .WithAlignedAudio(duration); - - Option result = builder.Build(false, 0, 0, 0, 1, false); - - result.IsSome.Should().BeTrue(); - result.IfSome( - filter => - { - filter.ComplexFilter.Should().Be("[0:1]apad=whole_dur=1000.1ms[a]"); - filter.AudioLabel.Should().Be("[a]"); - filter.VideoLabel.Should().Be("0:0"); - }); + builder = builder.WithScaling(new Resolution { Width = 1920, Height = 1000 }); } - [Test] - public void Should_Return_Audio_And_Video_Filter() + if (pad) { - var duration = TimeSpan.FromMinutes(54); - FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder() - .WithAlignedAudio(duration) - .WithDeinterlace(true); - - Option result = builder.Build(false, 0, 0, 0, 1, false); - - result.IsSome.Should().BeTrue(); - result.IfSome( - filter => - { - filter.ComplexFilter.Should().Be( - $"[0:1]apad=whole_dur={duration.TotalMilliseconds}ms[a];[0:0]yadif=1[v]"); - filter.AudioLabel.Should().Be("[a]"); - filter.VideoLabel.Should().Be("[v]"); - }); + builder = builder.WithBlackBars(new Resolution { Width = 1920, Height = 1080 }); } - [Test] - [TestCase(true, false, false, "[0:0]yadif=1[v]", "[v]")] - [TestCase(true, true, false, "[0:0]yadif=1,scale=1920:1000:flags=fast_bilinear,setsar=1[v]", "[v]")] - [TestCase(true, false, true, "[0:0]yadif=1,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2[v]", "[v]")] - [TestCase( - true, - true, - true, - "[0:0]yadif=1,scale=1920:1000:flags=fast_bilinear,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2[v]", - "[v]")] - [TestCase(false, true, false, "[0:0]scale=1920:1000:flags=fast_bilinear,setsar=1[v]", "[v]")] - [TestCase(false, false, true, "[0:0]setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2[v]", "[v]")] - [TestCase( - false, - true, - true, - "[0:0]scale=1920:1000:flags=fast_bilinear,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2[v]", - "[v]")] - public void Should_Return_Software_Video_Filter( - bool deinterlace, - bool scale, - bool pad, - string expectedVideoFilter, - string expectedVideoLabel) + Option result = builder.Build(false, 0, 0, 0, 1, false); + + result.IsSome.Should().BeTrue(); + result.IfSome( + filter => + { + filter.ComplexFilter.Should().Be(expectedVideoFilter); + filter.AudioLabel.Should().Be("0:1"); + filter.VideoLabel.Should().Be(expectedVideoLabel); + }); + } + + [Test] + [TestCase( + false, + false, + false, + WatermarkLocation.BottomLeft, + false, + 100, + "[0:0][1:v]overlay=x=134:y=H-h-54[v]", + "0:1", + "[v]")] + [TestCase( + false, + false, + false, + WatermarkLocation.BottomRight, + false, + 100, + "[0:0][1:v]overlay=x=W-w-134:y=H-h-54[v]", + "0:1", + "[v]")] + [TestCase( + false, + false, + false, + WatermarkLocation.TopLeft, + false, + 100, + "[0:0][1:v]overlay=x=134:y=54[v]", + "0:1", + "[v]")] + [TestCase( + false, + false, + false, + WatermarkLocation.TopRight, + false, + 100, + "[0:0][1:v]overlay=x=W-w-134:y=54[v]", + "0:1", + "[v]")] + [TestCase( + false, + false, + true, + WatermarkLocation.TopLeft, + false, + 100, + "[1:v]format=yuva420p|yuva444p|yuva422p|rgba|abgr|bgra|gbrap|ya8,fade=in:st=300:d=1:alpha=1:enable='between(t,0,314)',fade=out:st=315:d=1:alpha=1:enable='between(t,301,899)',fade=in:st=900:d=1:alpha=1:enable='between(t,316,914)',fade=out:st=915:d=1:alpha=1:enable='between(t,901,1499)',fade=in:st=1500:d=1:alpha=1:enable='between(t,916,1514)',fade=out:st=1515:d=1:alpha=1:enable='between(t,1501,2099)',fade=in:st=2100:d=1:alpha=1:enable='between(t,1516,2114)',fade=out:st=2115:d=1:alpha=1:enable='between(t,2101,2699)',fade=in:st=2700:d=1:alpha=1:enable='between(t,2116,2714)',fade=out:st=2715:d=1:alpha=1:enable='between(t,2701,3300)'[wmp];[0:0][wmp]overlay=x=134:y=54,format=nv12[v]", + "0:1", + "[v]")] + [TestCase( + false, + false, + false, + WatermarkLocation.TopLeft, + true, + 100, + "[1:v]scale=384:-1[wmp];[0:0][wmp]overlay=x=134:y=54[v]", + "0:1", + "[v]")] + [TestCase( + false, + false, + false, + WatermarkLocation.TopLeft, + false, + 90, + "[1:v]format=yuva420p|yuva444p|yuva422p|rgba|abgr|bgra|gbrap|ya8,colorchannelmixer=aa=0.90[wmp];[0:0][wmp]overlay=x=134:y=54[v]", + "0:1", + "[v]")] + [TestCase( + false, + true, + false, + WatermarkLocation.TopLeft, + false, + 100, + "[0:0]yadif=1[vt];[vt][1:v]overlay=x=134:y=54[v]", + "0:1", + "[v]")] + [TestCase( + false, + true, + false, + WatermarkLocation.TopLeft, + true, + 100, + "[0:0]yadif=1[vt];[1:v]scale=384:-1[wmp];[vt][wmp]overlay=x=134:y=54[v]", + "0:1", + "[v]")] + [TestCase( + true, + true, + false, + WatermarkLocation.TopLeft, + false, + 100, + "[0:1]apad=whole_dur=3300000ms[a];[0:0]yadif=1[vt];[vt][1:v]overlay=x=134:y=54[v]", + "[a]", + "[v]")] + [TestCase( + true, + false, + false, + WatermarkLocation.TopLeft, + false, + 100, + "[0:1]apad=whole_dur=3300000ms[a];[0:0][1:v]overlay=x=134:y=54[v]", + "[a]", + "[v]")] + public void Should_Return_Watermark( + bool alignAudio, + bool deinterlace, + bool intermittent, + WatermarkLocation location, + bool scaled, + int opacity, + string expectedVideoFilter, + string expectedAudioLabel, + string expectedVideoLabel) + { + var watermark = new ChannelWatermark { - FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder() - .WithDeinterlace(deinterlace); + Mode = intermittent + ? ChannelWatermarkMode.Intermittent + : ChannelWatermarkMode.Permanent, + DurationSeconds = intermittent ? 15 : 0, + FrequencyMinutes = intermittent ? 10 : 0, + Location = location, + Size = scaled ? WatermarkSize.Scaled : WatermarkSize.ActualSize, + WidthPercent = scaled ? 20 : 0, + Opacity = opacity, + HorizontalMarginPercent = 7, + VerticalMarginPercent = 5 + }; - if (scale) - { - builder = builder.WithScaling(new Resolution { Width = 1920, Height = 1000 }); - } - - if (pad) - { - builder = builder.WithBlackBars(new Resolution { Width = 1920, Height = 1080 }); - } - - Option result = builder.Build(false, 0, 0, 0, 1, false); - - result.IsSome.Should().BeTrue(); - result.IfSome( - filter => - { - filter.ComplexFilter.Should().Be(expectedVideoFilter); - filter.AudioLabel.Should().Be("0:1"); - filter.VideoLabel.Should().Be(expectedVideoLabel); - }); - } - - [Test] - [TestCase( - false, - false, - false, - WatermarkLocation.BottomLeft, - false, - 100, - "[0:0][1:v]overlay=x=134:y=H-h-54[v]", - "0:1", - "[v]")] - [TestCase( - false, - false, - false, - WatermarkLocation.BottomRight, - false, - 100, - "[0:0][1:v]overlay=x=W-w-134:y=H-h-54[v]", - "0:1", - "[v]")] - [TestCase( - false, - false, - false, - WatermarkLocation.TopLeft, - false, - 100, - "[0:0][1:v]overlay=x=134:y=54[v]", - "0:1", - "[v]")] - [TestCase( - false, - false, - false, - WatermarkLocation.TopRight, - false, - 100, - "[0:0][1:v]overlay=x=W-w-134:y=54[v]", - "0:1", - "[v]")] - [TestCase( - false, - false, - true, - WatermarkLocation.TopLeft, - false, - 100, - "[1:v]format=yuva420p|yuva444p|yuva422p|rgba|abgr|bgra|gbrap|ya8,fade=in:st=300:d=1:alpha=1:enable='between(t,0,314)',fade=out:st=315:d=1:alpha=1:enable='between(t,301,899)',fade=in:st=900:d=1:alpha=1:enable='between(t,316,914)',fade=out:st=915:d=1:alpha=1:enable='between(t,901,1499)',fade=in:st=1500:d=1:alpha=1:enable='between(t,916,1514)',fade=out:st=1515:d=1:alpha=1:enable='between(t,1501,2099)',fade=in:st=2100:d=1:alpha=1:enable='between(t,1516,2114)',fade=out:st=2115:d=1:alpha=1:enable='between(t,2101,2699)',fade=in:st=2700:d=1:alpha=1:enable='between(t,2116,2714)',fade=out:st=2715:d=1:alpha=1:enable='between(t,2701,3300)'[wmp];[0:0][wmp]overlay=x=134:y=54,format=nv12[v]", - "0:1", - "[v]")] - [TestCase( - false, - false, - false, - WatermarkLocation.TopLeft, - true, - 100, - "[1:v]scale=384:-1[wmp];[0:0][wmp]overlay=x=134:y=54[v]", - "0:1", - "[v]")] - [TestCase( - false, - false, - false, - WatermarkLocation.TopLeft, - false, - 90, - "[1:v]format=yuva420p|yuva444p|yuva422p|rgba|abgr|bgra|gbrap|ya8,colorchannelmixer=aa=0.90[wmp];[0:0][wmp]overlay=x=134:y=54[v]", - "0:1", - "[v]")] - [TestCase( - false, - true, - false, - WatermarkLocation.TopLeft, - false, - 100, - "[0:0]yadif=1[vt];[vt][1:v]overlay=x=134:y=54[v]", - "0:1", - "[v]")] - [TestCase( - false, - true, - false, - WatermarkLocation.TopLeft, - true, - 100, - "[0:0]yadif=1[vt];[1:v]scale=384:-1[wmp];[vt][wmp]overlay=x=134:y=54[v]", - "0:1", - "[v]")] - [TestCase( - true, - true, - false, - WatermarkLocation.TopLeft, - false, - 100, - "[0:1]apad=whole_dur=3300000ms[a];[0:0]yadif=1[vt];[vt][1:v]overlay=x=134:y=54[v]", - "[a]", - "[v]")] - [TestCase( - true, - false, - false, - WatermarkLocation.TopLeft, - false, - 100, - "[0:1]apad=whole_dur=3300000ms[a];[0:0][1:v]overlay=x=134:y=54[v]", - "[a]", - "[v]")] - public void Should_Return_Watermark( - bool alignAudio, - bool deinterlace, - bool intermittent, - WatermarkLocation location, - bool scaled, - int opacity, - string expectedVideoFilter, - string expectedAudioLabel, - string expectedVideoLabel) - { - var watermark = new ChannelWatermark - { - Mode = intermittent - ? ChannelWatermarkMode.Intermittent - : ChannelWatermarkMode.Permanent, - DurationSeconds = intermittent ? 15 : 0, - FrequencyMinutes = intermittent ? 10 : 0, - Location = location, - Size = scaled ? WatermarkSize.Scaled : WatermarkSize.ActualSize, - WidthPercent = scaled ? 20 : 0, - Opacity = opacity, - HorizontalMarginPercent = 7, - VerticalMarginPercent = 5 - }; - - Option> maybeFadePoints = watermark.Mode == ChannelWatermarkMode.Intermittent - ? Some( - WatermarkCalculator.CalculateFadePoints( - new DateTimeOffset(2022, 01, 31, 12, 25, 0, TimeSpan.FromHours(-5)), - TimeSpan.Zero, - TimeSpan.FromMinutes(55), - TimeSpan.Zero, - watermark.FrequencyMinutes, - watermark.DurationSeconds)) - : None; + Option> maybeFadePoints = watermark.Mode == ChannelWatermarkMode.Intermittent + ? Some( + WatermarkCalculator.CalculateFadePoints( + new DateTimeOffset(2022, 01, 31, 12, 25, 0, TimeSpan.FromHours(-5)), + TimeSpan.Zero, + TimeSpan.FromMinutes(55), + TimeSpan.Zero, + watermark.FrequencyMinutes, + watermark.DurationSeconds)) + : None; - FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder() - .WithWatermark( - Some(watermark), - maybeFadePoints, - new Resolution { Width = 1920, Height = 1080 }, - None) - .WithDeinterlace(deinterlace) - .WithAlignedAudio(alignAudio ? Some(TimeSpan.FromMinutes(55)) : None); + FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder() + .WithWatermark( + Some(watermark), + maybeFadePoints, + new Resolution { Width = 1920, Height = 1080 }, + None) + .WithDeinterlace(deinterlace) + .WithAlignedAudio(alignAudio ? Some(TimeSpan.FromMinutes(55)) : None); - Option result = builder.Build(false, 0, 0, 0, 1, false); + Option result = builder.Build(false, 0, 0, 0, 1, false); - result.IsSome.Should().BeTrue(); - result.IfSome( - filter => - { - filter.ComplexFilter.Should().Be(expectedVideoFilter); - filter.AudioLabel.Should().Be(expectedAudioLabel); - filter.VideoLabel.Should().Be(expectedVideoLabel); - }); - } + result.IsSome.Should().BeTrue(); + result.IfSome( + filter => + { + filter.ComplexFilter.Should().Be(expectedVideoFilter); + filter.AudioLabel.Should().Be(expectedAudioLabel); + filter.VideoLabel.Should().Be(expectedVideoLabel); + }); + } - [Test] - [TestCase( - false, - false, - false, - WatermarkLocation.BottomLeft, - false, - 100, - "[0:0]scale_cuda=format=yuv420p[vt];[1:v]format=yuva420p,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=H-h-54[v]", - "0:1", - "[v]", - false)] - [TestCase( - false, - false, - false, - WatermarkLocation.BottomLeft, - false, - 100, - "[0:0]scale_cuda=1920:1080,setsar=1,hwdownload,format=nv12,format=yuv420p,hwupload_cuda[vt];[1:v]format=yuva420p,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=H-h-54,hwupload[v]", - "0:1", - "[v]", - true)] - [TestCase( - false, - false, - true, - WatermarkLocation.TopLeft, - false, - 100, - "[0:0]scale_cuda=format=yuv420p[vt];[1:v]format=yuva420p,fade=in:st=300:d=1:alpha=1:enable='between(t,0,314)',fade=out:st=315:d=1:alpha=1:enable='between(t,301,899)',fade=in:st=900:d=1:alpha=1:enable='between(t,316,914)',fade=out:st=915:d=1:alpha=1:enable='between(t,901,1499)',fade=in:st=1500:d=1:alpha=1:enable='between(t,916,1514)',fade=out:st=1515:d=1:alpha=1:enable='between(t,1501,2099)',fade=in:st=2100:d=1:alpha=1:enable='between(t,1516,2114)',fade=out:st=2115:d=1:alpha=1:enable='between(t,2101,2699)',fade=in:st=2700:d=1:alpha=1:enable='between(t,2116,2714)',fade=out:st=2715:d=1:alpha=1:enable='between(t,2701,3300)',hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54[v]", - "0:1", - "[v]", - false)] - [TestCase( - false, - false, - true, - WatermarkLocation.TopLeft, - false, - 100, - "[0:0]scale_cuda=1920:1080,setsar=1,hwdownload,format=nv12,format=yuv420p,hwupload_cuda[vt];[1:v]format=yuva420p,fade=in:st=300:d=1:alpha=1:enable='between(t,0,314)',fade=out:st=315:d=1:alpha=1:enable='between(t,301,899)',fade=in:st=900:d=1:alpha=1:enable='between(t,316,914)',fade=out:st=915:d=1:alpha=1:enable='between(t,901,1499)',fade=in:st=1500:d=1:alpha=1:enable='between(t,916,1514)',fade=out:st=1515:d=1:alpha=1:enable='between(t,1501,2099)',fade=in:st=2100:d=1:alpha=1:enable='between(t,1516,2114)',fade=out:st=2115:d=1:alpha=1:enable='between(t,2101,2699)',fade=in:st=2700:d=1:alpha=1:enable='between(t,2116,2714)',fade=out:st=2715:d=1:alpha=1:enable='between(t,2701,3300)',hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54,hwupload[v]", - "0:1", - "[v]", - true)] - [TestCase( - false, - false, - false, - WatermarkLocation.TopLeft, - true, - 100, - "[0:0]scale_cuda=format=yuv420p[vt];[1:v]format=yuva420p,scale=384:-1,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54[v]", - "0:1", - "[v]", - false)] - [TestCase( - false, - false, - false, - WatermarkLocation.TopLeft, - true, - 100, - "[0:0]scale_cuda=1920:1080,setsar=1,hwdownload,format=nv12,format=yuv420p,hwupload_cuda[vt];[1:v]format=yuva420p,scale=384:-1,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54,hwupload[v]", - "0:1", - "[v]", - true)] - [TestCase( - false, - false, - false, - WatermarkLocation.TopLeft, - false, - 90, - "[0:0]scale_cuda=format=yuv420p[vt];[1:v]format=yuva420p,colorchannelmixer=aa=0.90,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54[v]", - "0:1", - "[v]", - false)] - [TestCase( - false, - false, - false, - WatermarkLocation.TopLeft, - false, - 90, - "[0:0]scale_cuda=1920:1080,setsar=1,hwdownload,format=nv12,format=yuv420p,hwupload_cuda[vt];[1:v]format=yuva420p,colorchannelmixer=aa=0.90,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54,hwupload[v]", - "0:1", - "[v]", - true)] - // TODO: do we need these anymore? interlaced content that isn't handled by mpeg2_cuvid? - // [TestCase( - // false, - // true, - // false, - // WatermarkLocation.TopLeft, - // false, - // 100, - // "[0:0]yadif=1[vt];[vt][1:v]overlay=x=134:y=54[v]", - // "0:1", - // "[v]")] - // [TestCase( - // false, - // true, - // false, - // WatermarkLocation.TopLeft, - // true, - // 100, - // "[0:0]yadif=1[vt];[1:v]scale=384:-1[wmp];[vt][wmp]overlay=x=134:y=54[v]", - // "0:1", - // "[v]")] - // [TestCase( - // true, - // true, - // false, - // WatermarkLocation.TopLeft, - // false, - // 100, - // "[0:1]apad=whole_dur=3300000ms[a];[0:0]yadif=1[vt];[vt][1:v]overlay=x=134:y=54[v]", - // "[a]", - // "[v]")] - [TestCase( - true, - false, - false, - WatermarkLocation.TopLeft, - false, - 100, - "[0:1]apad=whole_dur=3300000ms[a];[0:0]scale_cuda=format=yuv420p[vt];[1:v]format=yuva420p,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54[v]", - "[a]", - "[v]", - false)] - [TestCase( - true, - false, - false, - WatermarkLocation.TopLeft, - false, - 100, - "[0:1]apad=whole_dur=3300000ms[a];[0:0]scale_cuda=1920:1080,setsar=1,hwdownload,format=nv12,format=yuv420p,hwupload_cuda[vt];[1:v]format=yuva420p,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54,hwupload[v]", - "[a]", - "[v]", - true)] - public void Should_Return_NVENC_Watermark( - bool alignAudio, - bool deinterlace, - bool intermittent, - WatermarkLocation location, - bool scaled, - int opacity, - string expectedVideoFilter, - string expectedAudioLabel, - string expectedVideoLabel, - bool scaledSource) + [Test] + [TestCase( + false, + false, + false, + WatermarkLocation.BottomLeft, + false, + 100, + "[0:0]scale_cuda=format=yuv420p[vt];[1:v]format=yuva420p,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=H-h-54[v]", + "0:1", + "[v]", + false)] + [TestCase( + false, + false, + false, + WatermarkLocation.BottomLeft, + false, + 100, + "[0:0]scale_cuda=1920:1080,setsar=1,hwdownload,format=nv12,format=yuv420p,hwupload_cuda[vt];[1:v]format=yuva420p,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=H-h-54,hwupload[v]", + "0:1", + "[v]", + true)] + [TestCase( + false, + false, + true, + WatermarkLocation.TopLeft, + false, + 100, + "[0:0]scale_cuda=format=yuv420p[vt];[1:v]format=yuva420p,fade=in:st=300:d=1:alpha=1:enable='between(t,0,314)',fade=out:st=315:d=1:alpha=1:enable='between(t,301,899)',fade=in:st=900:d=1:alpha=1:enable='between(t,316,914)',fade=out:st=915:d=1:alpha=1:enable='between(t,901,1499)',fade=in:st=1500:d=1:alpha=1:enable='between(t,916,1514)',fade=out:st=1515:d=1:alpha=1:enable='between(t,1501,2099)',fade=in:st=2100:d=1:alpha=1:enable='between(t,1516,2114)',fade=out:st=2115:d=1:alpha=1:enable='between(t,2101,2699)',fade=in:st=2700:d=1:alpha=1:enable='between(t,2116,2714)',fade=out:st=2715:d=1:alpha=1:enable='between(t,2701,3300)',hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54[v]", + "0:1", + "[v]", + false)] + [TestCase( + false, + false, + true, + WatermarkLocation.TopLeft, + false, + 100, + "[0:0]scale_cuda=1920:1080,setsar=1,hwdownload,format=nv12,format=yuv420p,hwupload_cuda[vt];[1:v]format=yuva420p,fade=in:st=300:d=1:alpha=1:enable='between(t,0,314)',fade=out:st=315:d=1:alpha=1:enable='between(t,301,899)',fade=in:st=900:d=1:alpha=1:enable='between(t,316,914)',fade=out:st=915:d=1:alpha=1:enable='between(t,901,1499)',fade=in:st=1500:d=1:alpha=1:enable='between(t,916,1514)',fade=out:st=1515:d=1:alpha=1:enable='between(t,1501,2099)',fade=in:st=2100:d=1:alpha=1:enable='between(t,1516,2114)',fade=out:st=2115:d=1:alpha=1:enable='between(t,2101,2699)',fade=in:st=2700:d=1:alpha=1:enable='between(t,2116,2714)',fade=out:st=2715:d=1:alpha=1:enable='between(t,2701,3300)',hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54,hwupload[v]", + "0:1", + "[v]", + true)] + [TestCase( + false, + false, + false, + WatermarkLocation.TopLeft, + true, + 100, + "[0:0]scale_cuda=format=yuv420p[vt];[1:v]format=yuva420p,scale=384:-1,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54[v]", + "0:1", + "[v]", + false)] + [TestCase( + false, + false, + false, + WatermarkLocation.TopLeft, + true, + 100, + "[0:0]scale_cuda=1920:1080,setsar=1,hwdownload,format=nv12,format=yuv420p,hwupload_cuda[vt];[1:v]format=yuva420p,scale=384:-1,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54,hwupload[v]", + "0:1", + "[v]", + true)] + [TestCase( + false, + false, + false, + WatermarkLocation.TopLeft, + false, + 90, + "[0:0]scale_cuda=format=yuv420p[vt];[1:v]format=yuva420p,colorchannelmixer=aa=0.90,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54[v]", + "0:1", + "[v]", + false)] + [TestCase( + false, + false, + false, + WatermarkLocation.TopLeft, + false, + 90, + "[0:0]scale_cuda=1920:1080,setsar=1,hwdownload,format=nv12,format=yuv420p,hwupload_cuda[vt];[1:v]format=yuva420p,colorchannelmixer=aa=0.90,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54,hwupload[v]", + "0:1", + "[v]", + true)] + // TODO: do we need these anymore? interlaced content that isn't handled by mpeg2_cuvid? + // [TestCase( + // false, + // true, + // false, + // WatermarkLocation.TopLeft, + // false, + // 100, + // "[0:0]yadif=1[vt];[vt][1:v]overlay=x=134:y=54[v]", + // "0:1", + // "[v]")] + // [TestCase( + // false, + // true, + // false, + // WatermarkLocation.TopLeft, + // true, + // 100, + // "[0:0]yadif=1[vt];[1:v]scale=384:-1[wmp];[vt][wmp]overlay=x=134:y=54[v]", + // "0:1", + // "[v]")] + // [TestCase( + // true, + // true, + // false, + // WatermarkLocation.TopLeft, + // false, + // 100, + // "[0:1]apad=whole_dur=3300000ms[a];[0:0]yadif=1[vt];[vt][1:v]overlay=x=134:y=54[v]", + // "[a]", + // "[v]")] + [TestCase( + true, + false, + false, + WatermarkLocation.TopLeft, + false, + 100, + "[0:1]apad=whole_dur=3300000ms[a];[0:0]scale_cuda=format=yuv420p[vt];[1:v]format=yuva420p,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54[v]", + "[a]", + "[v]", + false)] + [TestCase( + true, + false, + false, + WatermarkLocation.TopLeft, + false, + 100, + "[0:1]apad=whole_dur=3300000ms[a];[0:0]scale_cuda=1920:1080,setsar=1,hwdownload,format=nv12,format=yuv420p,hwupload_cuda[vt];[1:v]format=yuva420p,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54,hwupload[v]", + "[a]", + "[v]", + true)] + public void Should_Return_NVENC_Watermark( + bool alignAudio, + bool deinterlace, + bool intermittent, + WatermarkLocation location, + bool scaled, + int opacity, + string expectedVideoFilter, + string expectedAudioLabel, + string expectedVideoLabel, + bool scaledSource) + { + var watermark = new ChannelWatermark { - var watermark = new ChannelWatermark - { - Mode = intermittent - ? ChannelWatermarkMode.Intermittent - : ChannelWatermarkMode.Permanent, - DurationSeconds = intermittent ? 15 : 0, - FrequencyMinutes = intermittent ? 10 : 0, - Location = location, - Size = scaled ? WatermarkSize.Scaled : WatermarkSize.ActualSize, - WidthPercent = scaled ? 20 : 0, - Opacity = opacity, - HorizontalMarginPercent = 7, - VerticalMarginPercent = 5 - }; + Mode = intermittent + ? ChannelWatermarkMode.Intermittent + : ChannelWatermarkMode.Permanent, + DurationSeconds = intermittent ? 15 : 0, + FrequencyMinutes = intermittent ? 10 : 0, + Location = location, + Size = scaled ? WatermarkSize.Scaled : WatermarkSize.ActualSize, + WidthPercent = scaled ? 20 : 0, + Opacity = opacity, + HorizontalMarginPercent = 7, + VerticalMarginPercent = 5 + }; - Option> maybeFadePoints = watermark.Mode == ChannelWatermarkMode.Intermittent - ? Some( - WatermarkCalculator.CalculateFadePoints( - new DateTimeOffset(2022, 01, 31, 12, 25, 0, TimeSpan.FromHours(-5)), - TimeSpan.Zero, - TimeSpan.FromMinutes(55), - TimeSpan.Zero, - watermark.FrequencyMinutes, - watermark.DurationSeconds)) - : None; + Option> maybeFadePoints = watermark.Mode == ChannelWatermarkMode.Intermittent + ? Some( + WatermarkCalculator.CalculateFadePoints( + new DateTimeOffset(2022, 01, 31, 12, 25, 0, TimeSpan.FromHours(-5)), + TimeSpan.Zero, + TimeSpan.FromMinutes(55), + TimeSpan.Zero, + watermark.FrequencyMinutes, + watermark.DurationSeconds)) + : None; - FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder() - .WithHardwareAcceleration(HardwareAccelerationKind.Nvenc) - .WithWatermark( - Some(watermark), - maybeFadePoints, - new Resolution { Width = 1920, Height = 1080 }, - None) - .WithDeinterlace(deinterlace) - .WithAlignedAudio(alignAudio ? Some(TimeSpan.FromMinutes(55)) : None); + FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder() + .WithHardwareAcceleration(HardwareAccelerationKind.Nvenc) + .WithWatermark( + Some(watermark), + maybeFadePoints, + new Resolution { Width = 1920, Height = 1080 }, + None) + .WithDeinterlace(deinterlace) + .WithAlignedAudio(alignAudio ? Some(TimeSpan.FromMinutes(55)) : None); - if (scaledSource) - { - builder = builder.WithScaling(new Resolution { Width = 1920, Height = 1080 }); - } - - Option result = builder.Build(false, 0, 0, 0, 1, false); - - result.IsSome.Should().BeTrue(); - result.IfSome( - filter => - { - filter.ComplexFilter.Should().Be(expectedVideoFilter); - filter.AudioLabel.Should().Be(expectedAudioLabel); - filter.VideoLabel.Should().Be(expectedVideoLabel); - }); - } - - [Test] - [TestCase(true, false, false, "[0:0]deinterlace_qsv[v]", "[v]")] - [TestCase( - true, - true, - false, - "[0:0]deinterlace_qsv,scale_qsv=w=1920:h=1000,setsar=1[v]", - "[v]")] - [TestCase( - true, - false, - true, - "[0:0]deinterlace_qsv,setsar=1,hwdownload,format=nv12,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload=extra_hw_frames=64[v]", - "[v]")] - [TestCase( - true, - true, - true, - "[0:0]deinterlace_qsv,scale_qsv=w=1920:h=1000,setsar=1,hwdownload,format=nv12,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload=extra_hw_frames=64[v]", - "[v]")] - [TestCase( - false, - true, - false, - "[0:0]scale_qsv=w=1920:h=1000,setsar=1[v]", - "[v]")] - [TestCase( - false, - false, - true, - "[0:0]setsar=1,hwdownload,format=nv12,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload=extra_hw_frames=64[v]", - "[v]")] - [TestCase( - false, - true, - true, - "[0:0]scale_qsv=w=1920:h=1000,setsar=1,hwdownload,format=nv12,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload=extra_hw_frames=64[v]", - "[v]")] - public void Should_Return_QSV_Video_Filter( - bool deinterlace, - bool scale, - bool pad, - string expectedVideoFilter, - string expectedVideoLabel) + if (scaledSource) { - FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder() - .WithHardwareAcceleration(HardwareAccelerationKind.Qsv) - .WithDeinterlace(deinterlace); - - if (scale) - { - builder = builder.WithScaling(new Resolution { Width = 1920, Height = 1000 }); - } - - if (pad) - { - builder = builder.WithBlackBars(new Resolution { Width = 1920, Height = 1080 }); - } - - Option result = builder.Build(false, 0, 0, 0, 1, false); - - result.IsSome.Should().BeTrue(); - result.IfSome( - filter => - { - filter.ComplexFilter.Should().Be(expectedVideoFilter); - filter.AudioLabel.Should().Be("0:1"); - filter.VideoLabel.Should().Be(expectedVideoLabel); - }); + builder = builder.WithScaling(new Resolution { Width = 1920, Height = 1080 }); } - [Test] - [TestCase(true, false, false, "[0:0]yadif_cuda[v]", "[v]")] - [TestCase( - true, - true, - false, - "[0:0]yadif_cuda,scale_cuda=1920:1000,setsar=1[v]", - "[v]")] - [TestCase( - true, - false, - true, - "[0:0]yadif_cuda,setsar=1,hwdownload,format=nv12,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", - "[v]")] - [TestCase( - true, - true, - true, - "[0:0]yadif_cuda,scale_cuda=1920:1000,setsar=1,hwdownload,format=nv12,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", - "[v]")] - [TestCase( - false, - true, - false, - "[0:0]scale_cuda=1920:1000,setsar=1[v]", - "[v]")] - [TestCase( - false, - false, - true, - "[0:0]setsar=1,hwdownload,format=nv12,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", - "[v]")] - [TestCase( - false, - true, - true, - "[0:0]scale_cuda=1920:1000,setsar=1,hwdownload,format=nv12,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", - "[v]")] - public void Should_Return_NVENC_Video_Filter( - bool deinterlace, - bool scale, - bool pad, - string expectedVideoFilter, - string expectedVideoLabel) + Option result = builder.Build(false, 0, 0, 0, 1, false); + + result.IsSome.Should().BeTrue(); + result.IfSome( + filter => + { + filter.ComplexFilter.Should().Be(expectedVideoFilter); + filter.AudioLabel.Should().Be(expectedAudioLabel); + filter.VideoLabel.Should().Be(expectedVideoLabel); + }); + } + + [Test] + [TestCase(true, false, false, "[0:0]deinterlace_qsv[v]", "[v]")] + [TestCase( + true, + true, + false, + "[0:0]deinterlace_qsv,scale_qsv=w=1920:h=1000,setsar=1[v]", + "[v]")] + [TestCase( + true, + false, + true, + "[0:0]deinterlace_qsv,setsar=1,hwdownload,format=nv12,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload=extra_hw_frames=64[v]", + "[v]")] + [TestCase( + true, + true, + true, + "[0:0]deinterlace_qsv,scale_qsv=w=1920:h=1000,setsar=1,hwdownload,format=nv12,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload=extra_hw_frames=64[v]", + "[v]")] + [TestCase( + false, + true, + false, + "[0:0]scale_qsv=w=1920:h=1000,setsar=1[v]", + "[v]")] + [TestCase( + false, + false, + true, + "[0:0]setsar=1,hwdownload,format=nv12,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload=extra_hw_frames=64[v]", + "[v]")] + [TestCase( + false, + true, + true, + "[0:0]scale_qsv=w=1920:h=1000,setsar=1,hwdownload,format=nv12,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload=extra_hw_frames=64[v]", + "[v]")] + public void Should_Return_QSV_Video_Filter( + bool deinterlace, + bool scale, + bool pad, + string expectedVideoFilter, + string expectedVideoLabel) + { + FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder() + .WithHardwareAcceleration(HardwareAccelerationKind.Qsv) + .WithDeinterlace(deinterlace); + + if (scale) { - FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder() - .WithHardwareAcceleration(HardwareAccelerationKind.Nvenc) - .WithDeinterlace(deinterlace) - .WithInputPixelFormat("h264"); - - if (scale) - { - builder = builder.WithScaling(new Resolution { Width = 1920, Height = 1000 }); - } - - if (pad) - { - builder = builder.WithBlackBars(new Resolution { Width = 1920, Height = 1080 }); - } - - Option result = builder.Build(false, 0, 0, 0, 1, false); - - result.IsSome.Should().BeTrue(); - result.IfSome( - filter => - { - filter.ComplexFilter.Should().Be(expectedVideoFilter); - filter.AudioLabel.Should().Be("0:1"); - filter.VideoLabel.Should().Be(expectedVideoLabel); - }); + builder = builder.WithScaling(new Resolution { Width = 1920, Height = 1000 }); } - [Test] - [TestCase("h264", true, false, false, "[0:0]deinterlace_vaapi[v]", "[v]")] - [TestCase( - "h264", - true, - true, - false, - "[0:0]deinterlace_vaapi,scale_vaapi=format=nv12:w=1920:h=1000,setsar=1[v]", - "[v]")] - [TestCase( - "h264", - true, - false, - true, - "[0:0]deinterlace_vaapi,setsar=1,hwdownload,format=nv12|vaapi,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", - "[v]")] - [TestCase( - "h264", - true, - true, - true, - "[0:0]deinterlace_vaapi,scale_vaapi=format=nv12:w=1920:h=1000,setsar=1,hwdownload,format=nv12|vaapi,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", - "[v]")] - [TestCase( - "h264", - false, - true, - false, - "[0:0]scale_vaapi=format=nv12:w=1920:h=1000,setsar=1[v]", - "[v]")] - [TestCase( - "h264", - false, - false, - true, - "[0:0]setsar=1,hwdownload,format=nv12|vaapi,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", - "[v]")] - [TestCase( - "h264", - false, - true, - true, - "[0:0]scale_vaapi=format=nv12:w=1920:h=1000,setsar=1,hwdownload,format=nv12|vaapi,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", - "[v]")] - [TestCase("mpeg4", true, false, false, "[0:0]hwupload,deinterlace_vaapi[v]", "[v]")] - [TestCase( - "mpeg4", - true, - true, - false, - "[0:0]hwupload,deinterlace_vaapi,scale_vaapi=format=nv12:w=1920:h=1000,setsar=1[v]", - "[v]")] - [TestCase( - "mpeg4", - true, - false, - true, - "[0:0]hwupload,deinterlace_vaapi,setsar=1,hwdownload,format=nv12|vaapi,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", - "[v]")] - [TestCase( - "mpeg4", - true, - true, - true, - "[0:0]hwupload,deinterlace_vaapi,scale_vaapi=format=nv12:w=1920:h=1000,setsar=1,hwdownload,format=nv12|vaapi,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", - "[v]")] - [TestCase( - "mpeg4", - false, - true, - false, - "[0:0]hwupload,scale_vaapi=format=nv12:w=1920:h=1000,setsar=1[v]", - "[v]")] - [TestCase( - "mpeg4", - false, - false, - true, - "[0:0]setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", - "[v]")] - [TestCase( - "mpeg4", - false, - true, - true, - "[0:0]hwupload,scale_vaapi=format=nv12:w=1920:h=1000,setsar=1,hwdownload,format=nv12|vaapi,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", - "[v]")] - public void Should_Return_VAAPI_Video_Filter( - string codec, - bool deinterlace, - bool scale, - bool pad, - string expectedVideoFilter, - string expectedVideoLabel) + if (pad) { - FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder() - .WithHardwareAcceleration(HardwareAccelerationKind.Vaapi) - .WithInputCodec(codec) - .WithDeinterlace(deinterlace); - - if (scale) - { - builder = builder.WithScaling(new Resolution { Width = 1920, Height = 1000 }); - } - - if (pad) - { - builder = builder.WithBlackBars(new Resolution { Width = 1920, Height = 1080 }); - } - - Option result = builder.Build(false, 0, 0, 0, 1, false); - - result.IsSome.Should().BeTrue(); - result.IfSome( - filter => - { - filter.ComplexFilter.Should().Be(expectedVideoFilter); - filter.AudioLabel.Should().Be("0:1"); - filter.VideoLabel.Should().Be(expectedVideoLabel); - }); + builder = builder.WithBlackBars(new Resolution { Width = 1920, Height = 1080 }); } + + Option result = builder.Build(false, 0, 0, 0, 1, false); + + result.IsSome.Should().BeTrue(); + result.IfSome( + filter => + { + filter.ComplexFilter.Should().Be(expectedVideoFilter); + filter.AudioLabel.Should().Be("0:1"); + filter.VideoLabel.Should().Be(expectedVideoLabel); + }); + } + + [Test] + [TestCase(true, false, false, "[0:0]yadif_cuda[v]", "[v]")] + [TestCase( + true, + true, + false, + "[0:0]yadif_cuda,scale_cuda=1920:1000,setsar=1[v]", + "[v]")] + [TestCase( + true, + false, + true, + "[0:0]yadif_cuda,setsar=1,hwdownload,format=nv12,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", + "[v]")] + [TestCase( + true, + true, + true, + "[0:0]yadif_cuda,scale_cuda=1920:1000,setsar=1,hwdownload,format=nv12,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", + "[v]")] + [TestCase( + false, + true, + false, + "[0:0]scale_cuda=1920:1000,setsar=1[v]", + "[v]")] + [TestCase( + false, + false, + true, + "[0:0]setsar=1,hwdownload,format=nv12,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", + "[v]")] + [TestCase( + false, + true, + true, + "[0:0]scale_cuda=1920:1000,setsar=1,hwdownload,format=nv12,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", + "[v]")] + public void Should_Return_NVENC_Video_Filter( + bool deinterlace, + bool scale, + bool pad, + string expectedVideoFilter, + string expectedVideoLabel) + { + FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder() + .WithHardwareAcceleration(HardwareAccelerationKind.Nvenc) + .WithDeinterlace(deinterlace) + .WithInputPixelFormat("h264"); + + if (scale) + { + builder = builder.WithScaling(new Resolution { Width = 1920, Height = 1000 }); + } + + if (pad) + { + builder = builder.WithBlackBars(new Resolution { Width = 1920, Height = 1080 }); + } + + Option result = builder.Build(false, 0, 0, 0, 1, false); + + result.IsSome.Should().BeTrue(); + result.IfSome( + filter => + { + filter.ComplexFilter.Should().Be(expectedVideoFilter); + filter.AudioLabel.Should().Be("0:1"); + filter.VideoLabel.Should().Be(expectedVideoLabel); + }); + } + + [Test] + [TestCase("h264", true, false, false, "[0:0]deinterlace_vaapi[v]", "[v]")] + [TestCase( + "h264", + true, + true, + false, + "[0:0]deinterlace_vaapi,scale_vaapi=format=nv12:w=1920:h=1000,setsar=1[v]", + "[v]")] + [TestCase( + "h264", + true, + false, + true, + "[0:0]deinterlace_vaapi,setsar=1,hwdownload,format=nv12|vaapi,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", + "[v]")] + [TestCase( + "h264", + true, + true, + true, + "[0:0]deinterlace_vaapi,scale_vaapi=format=nv12:w=1920:h=1000,setsar=1,hwdownload,format=nv12|vaapi,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", + "[v]")] + [TestCase( + "h264", + false, + true, + false, + "[0:0]scale_vaapi=format=nv12:w=1920:h=1000,setsar=1[v]", + "[v]")] + [TestCase( + "h264", + false, + false, + true, + "[0:0]setsar=1,hwdownload,format=nv12|vaapi,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", + "[v]")] + [TestCase( + "h264", + false, + true, + true, + "[0:0]scale_vaapi=format=nv12:w=1920:h=1000,setsar=1,hwdownload,format=nv12|vaapi,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", + "[v]")] + [TestCase("mpeg4", true, false, false, "[0:0]hwupload,deinterlace_vaapi[v]", "[v]")] + [TestCase( + "mpeg4", + true, + true, + false, + "[0:0]hwupload,deinterlace_vaapi,scale_vaapi=format=nv12:w=1920:h=1000,setsar=1[v]", + "[v]")] + [TestCase( + "mpeg4", + true, + false, + true, + "[0:0]hwupload,deinterlace_vaapi,setsar=1,hwdownload,format=nv12|vaapi,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", + "[v]")] + [TestCase( + "mpeg4", + true, + true, + true, + "[0:0]hwupload,deinterlace_vaapi,scale_vaapi=format=nv12:w=1920:h=1000,setsar=1,hwdownload,format=nv12|vaapi,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", + "[v]")] + [TestCase( + "mpeg4", + false, + true, + false, + "[0:0]hwupload,scale_vaapi=format=nv12:w=1920:h=1000,setsar=1[v]", + "[v]")] + [TestCase( + "mpeg4", + false, + false, + true, + "[0:0]setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", + "[v]")] + [TestCase( + "mpeg4", + false, + true, + true, + "[0:0]hwupload,scale_vaapi=format=nv12:w=1920:h=1000,setsar=1,hwdownload,format=nv12|vaapi,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]", + "[v]")] + public void Should_Return_VAAPI_Video_Filter( + string codec, + bool deinterlace, + bool scale, + bool pad, + string expectedVideoFilter, + string expectedVideoLabel) + { + FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder() + .WithHardwareAcceleration(HardwareAccelerationKind.Vaapi) + .WithInputCodec(codec) + .WithDeinterlace(deinterlace); + + if (scale) + { + builder = builder.WithScaling(new Resolution { Width = 1920, Height = 1000 }); + } + + if (pad) + { + builder = builder.WithBlackBars(new Resolution { Width = 1920, Height = 1080 }); + } + + Option result = builder.Build(false, 0, 0, 0, 1, false); + + result.IsSome.Should().BeTrue(); + result.IfSome( + filter => + { + filter.ComplexFilter.Should().Be(expectedVideoFilter); + filter.AudioLabel.Should().Be("0:1"); + filter.VideoLabel.Should().Be(expectedVideoLabel); + }); } } -} +} \ No newline at end of file diff --git a/ErsatzTV.Core.Tests/FFmpeg/FFmpegPlaybackSettingsCalculatorTests.cs b/ErsatzTV.Core.Tests/FFmpeg/FFmpegPlaybackSettingsCalculatorTests.cs index e289a18ba..2a05e9277 100644 --- a/ErsatzTV.Core.Tests/FFmpeg/FFmpegPlaybackSettingsCalculatorTests.cs +++ b/ErsatzTV.Core.Tests/FFmpeg/FFmpegPlaybackSettingsCalculatorTests.cs @@ -1,1187 +1,1184 @@ -using System; -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; using ErsatzTV.Core.FFmpeg; using ErsatzTV.Core.Interfaces.FFmpeg; using FluentAssertions; using NUnit.Framework; -using static LanguageExt.Prelude; -namespace ErsatzTV.Core.Tests.FFmpeg +namespace ErsatzTV.Core.Tests.FFmpeg; + +[TestFixture] +public class FFmpegPlaybackSettingsCalculatorTests { [TestFixture] - public class FFmpegPlaybackSettingsCalculatorTests + public class CalculateSettings { - [TestFixture] - public class CalculateSettings - { - private readonly FFmpegPlaybackSettingsCalculator _calculator; + private readonly FFmpegPlaybackSettingsCalculator _calculator; - public CalculateSettings() => _calculator = new FFmpegPlaybackSettingsCalculator(); + public CalculateSettings() => _calculator = new FFmpegPlaybackSettingsCalculator(); - [Test] - public void Should_Not_GenPts_ForHlsSegmenter() - { - FFmpegProfile ffmpegProfile = TestProfile(); + [Test] + public void Should_Not_GenPts_ForHlsSegmenter() + { + FFmpegProfile ffmpegProfile = TestProfile(); - FFmpegPlaybackSettings actual = _calculator.CalculateSettings( - StreamingMode.HttpLiveStreamingSegmenter, - ffmpegProfile, - new MediaVersion(), - new MediaStream(), - new MediaStream(), - DateTimeOffset.Now, - DateTimeOffset.Now, - TimeSpan.Zero, - TimeSpan.Zero, - false, - None); + FFmpegPlaybackSettings actual = _calculator.CalculateSettings( + StreamingMode.HttpLiveStreamingSegmenter, + ffmpegProfile, + new MediaVersion(), + new MediaStream(), + new MediaStream(), + DateTimeOffset.Now, + DateTimeOffset.Now, + TimeSpan.Zero, + TimeSpan.Zero, + false, + None); - actual.FormatFlags.Should().NotContain("+genpts"); - } + actual.FormatFlags.Should().NotContain("+genpts"); + } - [Test] - public void Should_Not_UseSpecifiedThreadCount_ForTransportStream() - { - // MPEG-TS requires realtime output which is hardcoded to a single thread + [Test] + public void Should_Not_UseSpecifiedThreadCount_ForTransportStream() + { + // MPEG-TS requires realtime output which is hardcoded to a single thread - FFmpegProfile ffmpegProfile = TestProfile() with { ThreadCount = 7 }; - - FFmpegPlaybackSettings actual = _calculator.CalculateSettings( - StreamingMode.TransportStream, - ffmpegProfile, - new MediaVersion(), - new MediaStream(), - new MediaStream(), - DateTimeOffset.Now, - DateTimeOffset.Now, - TimeSpan.Zero, - TimeSpan.Zero, - false, - None); - - actual.ThreadCount.Should().Be(1); - } - - [Test] - public void Should_UseSpecifiedThreadCount_ForHttpLiveStreamingSegmenter() - { - FFmpegProfile ffmpegProfile = TestProfile() with { ThreadCount = 7 }; - - FFmpegPlaybackSettings actual = _calculator.CalculateSettings( - StreamingMode.HttpLiveStreamingSegmenter, - ffmpegProfile, - new MediaVersion(), - new MediaStream(), - new MediaStream(), - DateTimeOffset.Now, - DateTimeOffset.Now, - TimeSpan.Zero, - TimeSpan.Zero, - false, - None); - - actual.ThreadCount.Should().Be(7); - } - - [Test] - public void Should_SetFormatFlags_ForTransportStream() - { - FFmpegProfile ffmpegProfile = TestProfile(); - - FFmpegPlaybackSettings actual = _calculator.CalculateSettings( - StreamingMode.TransportStream, - ffmpegProfile, - new MediaVersion(), - new MediaStream(), - new MediaStream(), - DateTimeOffset.Now, - DateTimeOffset.Now, - TimeSpan.Zero, - TimeSpan.Zero, - false, - None); - - string[] expected = { "+genpts", "+discardcorrupt", "+igndts" }; - actual.FormatFlags.Count.Should().Be(expected.Length); - actual.FormatFlags.Should().Contain(expected); - } - - [Test] - public void Should_SetFormatFlags_ForHttpLiveStreaming() - { - FFmpegProfile ffmpegProfile = TestProfile(); - - FFmpegPlaybackSettings actual = _calculator.CalculateSettings( - StreamingMode.HttpLiveStreamingDirect, - ffmpegProfile, - new MediaVersion(), - new MediaStream(), - new MediaStream(), - DateTimeOffset.Now, - DateTimeOffset.Now, - TimeSpan.Zero, - TimeSpan.Zero, - false, - None); - - string[] expected = { "+genpts", "+discardcorrupt", "+igndts" }; - actual.FormatFlags.Count.Should().Be(expected.Length); - actual.FormatFlags.Should().Contain(expected); - } - - [Test] - public void Should_SetRealtime_ForTransportStream() - { - FFmpegProfile ffmpegProfile = TestProfile(); - - FFmpegPlaybackSettings actual = _calculator.CalculateSettings( - StreamingMode.TransportStream, - ffmpegProfile, - new MediaVersion(), - new MediaStream(), - new MediaStream(), - DateTimeOffset.Now, - DateTimeOffset.Now, - TimeSpan.Zero, - TimeSpan.Zero, - false, - None); - - actual.RealtimeOutput.Should().BeTrue(); - } - - [Test] - public void Should_SetRealtime_ForHttpLiveStreaming() - { - FFmpegProfile ffmpegProfile = TestProfile(); - - FFmpegPlaybackSettings actual = _calculator.CalculateSettings( - StreamingMode.HttpLiveStreamingDirect, - ffmpegProfile, - new MediaVersion(), - new MediaStream(), - new MediaStream(), - DateTimeOffset.Now, - DateTimeOffset.Now, - TimeSpan.Zero, - TimeSpan.Zero, - false, - None); - - actual.RealtimeOutput.Should().BeTrue(); - } - - [Test] - public void Should_SetStreamSeek_When_PlaybackIsLate_ForTransportStream() - { - DateTimeOffset now = DateTimeOffset.Now; - - FFmpegProfile ffmpegProfile = TestProfile(); - - FFmpegPlaybackSettings actual = _calculator.CalculateSettings( - StreamingMode.TransportStream, - ffmpegProfile, - new MediaVersion(), - new MediaStream(), - new MediaStream(), - now, - now.AddMinutes(5), - TimeSpan.Zero, - TimeSpan.Zero, - false, - None); - - actual.StreamSeek.IsSome.Should().BeTrue(); - actual.StreamSeek.IfNone(TimeSpan.Zero).Should().Be(TimeSpan.FromMinutes(5)); - } - - [Test] - public void Should_SetStreamSeek_When_PlaybackIsLate_ForHttpLiveStreaming() - { - DateTimeOffset now = DateTimeOffset.Now; - - FFmpegProfile ffmpegProfile = TestProfile(); - - FFmpegPlaybackSettings actual = _calculator.CalculateSettings( - StreamingMode.HttpLiveStreamingDirect, - ffmpegProfile, - new MediaVersion(), - new MediaStream(), - new MediaStream(), - now, - now.AddMinutes(5), - TimeSpan.Zero, - TimeSpan.Zero, - false, - None); - - actual.StreamSeek.IsSome.Should().BeTrue(); - actual.StreamSeek.IfNone(TimeSpan.Zero).Should().Be(TimeSpan.FromMinutes(5)); - } - - [Test] - public void ShouldNot_SetScaledSize_When_NotNormalizingVideo_ForTransportStream() - { - FFmpegProfile ffmpegProfile = TestProfile() with { NormalizeVideo = false }; - - FFmpegPlaybackSettings actual = _calculator.CalculateSettings( - StreamingMode.TransportStream, - ffmpegProfile, - new MediaVersion(), - new MediaStream(), - new MediaStream(), - DateTimeOffset.Now, - DateTimeOffset.Now, - TimeSpan.Zero, - TimeSpan.Zero, - false, - None); - - actual.ScaledSize.IsNone.Should().BeTrue(); - } - - [Test] - public void ShouldNot_SetScaledSize_When_ContentIsCorrectSize_ForTransportStream() - { - FFmpegProfile ffmpegProfile = TestProfile() with - { - NormalizeVideo = true, - Resolution = new Resolution { Width = 1920, Height = 1080 } - }; - - // not anamorphic - var version = new MediaVersion { Width = 1920, Height = 1080, SampleAspectRatio = "1:1" }; - - FFmpegPlaybackSettings actual = _calculator.CalculateSettings( - StreamingMode.TransportStream, - ffmpegProfile, - version, - new MediaStream(), - new MediaStream(), - DateTimeOffset.Now, - DateTimeOffset.Now, - TimeSpan.Zero, - TimeSpan.Zero, - false, - None); - - actual.ScaledSize.IsNone.Should().BeTrue(); - } - - [Test] - public void ShouldNot_SetScaledSize_When_ScaledSizeWouldEqualContentSize_ForTransportStream() - { - FFmpegProfile ffmpegProfile = TestProfile() with - { - NormalizeVideo = true, - Resolution = new Resolution { Width = 1920, Height = 1080 } - }; - - // not anamorphic - var version = new MediaVersion { Width = 1918, Height = 1080, SampleAspectRatio = "1:1" }; - - FFmpegPlaybackSettings actual = _calculator.CalculateSettings( - StreamingMode.TransportStream, - ffmpegProfile, - version, - new MediaStream(), - new MediaStream(), - DateTimeOffset.Now, - DateTimeOffset.Now, - TimeSpan.Zero, - TimeSpan.Zero, - false, - None); - - actual.ScaledSize.IsNone.Should().BeTrue(); - } - - [Test] - public void ShouldNot_PadToDesiredResolution_When_ContentIsCorrectSize_ForTransportStream() - { - FFmpegProfile ffmpegProfile = TestProfile() with - { - NormalizeVideo = true, - Resolution = new Resolution { Width = 1920, Height = 1080 } - }; - - // not anamorphic - var version = new MediaVersion { Width = 1920, Height = 1080, SampleAspectRatio = "1:1" }; - - FFmpegPlaybackSettings actual = _calculator.CalculateSettings( - StreamingMode.TransportStream, - ffmpegProfile, - version, - new MediaStream(), - new MediaStream(), - DateTimeOffset.Now, - DateTimeOffset.Now, - TimeSpan.Zero, - TimeSpan.Zero, - false, - None); - - actual.ScaledSize.IsNone.Should().BeTrue(); - actual.PadToDesiredResolution.Should().BeFalse(); - } - - [Test] - public void Should_PadToDesiredResolution_When_UnscaledContentIsUnderSized_ForTransportStream() - { - FFmpegProfile ffmpegProfile = TestProfile() with - { - Transcode = true, - NormalizeVideo = true, - Resolution = new Resolution { Width = 1920, Height = 1080 } - }; - - // not anamorphic - var version = new MediaVersion { Width = 1918, Height = 1080, SampleAspectRatio = "1:1" }; - - FFmpegPlaybackSettings actual = _calculator.CalculateSettings( - StreamingMode.TransportStream, - ffmpegProfile, - version, - new MediaStream(), - new MediaStream(), - DateTimeOffset.Now, - DateTimeOffset.Now, - TimeSpan.Zero, - TimeSpan.Zero, - false, - None); - - actual.ScaledSize.IsNone.Should().BeTrue(); - actual.PadToDesiredResolution.Should().BeTrue(); - } - - [Test] - public void Should_ScaleToEvenDimensions_ForTransportStream() - { - FFmpegProfile ffmpegProfile = TestProfile() with - { - Transcode = true, - NormalizeVideo = true, - Resolution = new Resolution { Width = 1280, Height = 720 } - }; - - var version = new MediaVersion { Width = 706, Height = 362, SampleAspectRatio = "32:27" }; - - FFmpegPlaybackSettings actual = _calculator.CalculateSettings( - StreamingMode.TransportStream, - ffmpegProfile, - version, - new MediaStream(), - new MediaStream(), - DateTimeOffset.Now, - DateTimeOffset.Now, - TimeSpan.Zero, - TimeSpan.Zero, - false, - None); - - IDisplaySize scaledSize = actual.ScaledSize.IfNone(new MediaVersion { Width = 0, Height = 0 }); - scaledSize.Width.Should().Be(1280); - scaledSize.Height.Should().Be(554); - actual.PadToDesiredResolution.Should().BeTrue(); - } - - [Test] - public void Should_NotPadToDesiredResolution_When_UnscaledContentIsUnderSized_ForHttpLiveStreaming() - { - FFmpegProfile ffmpegProfile = TestProfile() with - { - Transcode = true, - NormalizeVideo = true, - Resolution = new Resolution { Width = 1920, Height = 1080 } - }; - - // not anamorphic - var version = new MediaVersion { Width = 1918, Height = 1080, SampleAspectRatio = "1:1" }; - - FFmpegPlaybackSettings actual = _calculator.CalculateSettings( - StreamingMode.HttpLiveStreamingDirect, - ffmpegProfile, - version, - new MediaStream(), - new MediaStream(), - DateTimeOffset.Now, - DateTimeOffset.Now, - TimeSpan.Zero, - TimeSpan.Zero, - false, - None); - - actual.ScaledSize.IsNone.Should().BeTrue(); - actual.PadToDesiredResolution.Should().BeFalse(); - } - - [Test] - public void Should_NotPadToDesiredResolution_When_NotNormalizingVideo() - { - FFmpegProfile ffmpegProfile = TestProfile() with - { - Transcode = true, - NormalizeVideo = false, - Resolution = new Resolution { Width = 1920, Height = 1080 } - }; - - // not anamorphic - var version = new MediaVersion { Width = 1918, Height = 1080, SampleAspectRatio = "1:1" }; - - FFmpegPlaybackSettings actual = _calculator.CalculateSettings( - StreamingMode.TransportStream, - ffmpegProfile, - version, - new MediaStream(), - new MediaStream(), - DateTimeOffset.Now, - DateTimeOffset.Now, - TimeSpan.Zero, - TimeSpan.Zero, - false, - None); - - actual.ScaledSize.IsNone.Should().BeTrue(); - actual.PadToDesiredResolution.Should().BeFalse(); - } - - [Test] - public void Should_SetDesiredVideoCodec_When_ContentIsPadded_ForTransportStream() - { - var ffmpegProfile = new FFmpegProfile - { - Transcode = true, - NormalizeVideo = true, - Resolution = new Resolution { Width = 1920, Height = 1080 }, - VideoCodec = "testCodec" - }; - - // not anamorphic - var version = new MediaVersion { Width = 1918, Height = 1080, SampleAspectRatio = "1:1" }; - - FFmpegPlaybackSettings actual = _calculator.CalculateSettings( - StreamingMode.TransportStream, - ffmpegProfile, - version, - new MediaStream(), - new MediaStream(), - DateTimeOffset.Now, - DateTimeOffset.Now, - TimeSpan.Zero, - TimeSpan.Zero, - false, - None); - - actual.ScaledSize.IsNone.Should().BeTrue(); - actual.PadToDesiredResolution.Should().BeTrue(); - actual.VideoCodec.Should().Be("testCodec"); - } - - [Test] - public void - Should_SetDesiredVideoCodec_When_ContentIsCorrectSize_And_NormalizingVideo_ForTransportStream() - { - var ffmpegProfile = new FFmpegProfile - { - Transcode = true, - NormalizeVideo = true, - Resolution = new Resolution { Width = 1920, Height = 1080 }, - VideoCodec = "testCodec" - }; - - // not anamorphic - var version = new MediaVersion - { Width = 1920, Height = 1080, SampleAspectRatio = "1:1" }; - - FFmpegPlaybackSettings actual = _calculator.CalculateSettings( - StreamingMode.TransportStream, - ffmpegProfile, - version, - new MediaStream { Codec = "mpeg2video" }, - new MediaStream(), - DateTimeOffset.Now, - DateTimeOffset.Now, - TimeSpan.Zero, - TimeSpan.Zero, - false, - None); - - actual.ScaledSize.IsNone.Should().BeTrue(); - actual.PadToDesiredResolution.Should().BeFalse(); - actual.VideoCodec.Should().Be("testCodec"); - } - - [Test] - public void - Should_SetCopyVideoCodec_When_ContentIsCorrectSize_And_NormalizingVideo_ForHttpLiveStreaming() - { - var ffmpegProfile = new FFmpegProfile - { - Transcode = true, - NormalizeVideo = true, - Resolution = new Resolution { Width = 1920, Height = 1080 }, - VideoCodec = "testCodec" - }; - - // not anamorphic - var version = new MediaVersion - { Width = 1920, Height = 1080, SampleAspectRatio = "1:1" }; - - FFmpegPlaybackSettings actual = _calculator.CalculateSettings( - StreamingMode.HttpLiveStreamingDirect, - ffmpegProfile, - version, - new MediaStream { Codec = "mpeg2video" }, - new MediaStream(), - DateTimeOffset.Now, - DateTimeOffset.Now, - TimeSpan.Zero, - TimeSpan.Zero, - false, - None); - - actual.ScaledSize.IsNone.Should().BeTrue(); - actual.PadToDesiredResolution.Should().BeFalse(); - actual.VideoCodec.Should().Be("copy"); - } - - [Test] - public void Should_SetCopyVideoCodec_When_ContentIsCorrectSize_And_CorrectCodec_ForTransportStream() - { - var ffmpegProfile = new FFmpegProfile - { - Transcode = true, - NormalizeVideo = true, - Resolution = new Resolution { Width = 1920, Height = 1080 }, - VideoCodec = "libx264" - }; - - // not anamorphic - var version = new MediaVersion - { Width = 1920, Height = 1080, SampleAspectRatio = "1:1" }; - - FFmpegPlaybackSettings actual = _calculator.CalculateSettings( - StreamingMode.TransportStream, - ffmpegProfile, - version, - new MediaStream { Codec = "libx264" }, - new MediaStream(), - DateTimeOffset.Now, - DateTimeOffset.Now, - TimeSpan.Zero, - TimeSpan.Zero, - false, - None); - - actual.ScaledSize.IsNone.Should().BeTrue(); - actual.PadToDesiredResolution.Should().BeFalse(); - actual.VideoCodec.Should().Be("copy"); - } - - [Test] - public void - Should_SetCopyVideoCodec_When_ContentIsCorrectSize_And_NotNormalizingVideo_ForTransportStream() - { - var ffmpegProfile = new FFmpegProfile - { - Transcode = true, - NormalizeVideo = false, - Resolution = new Resolution { Width = 1920, Height = 1080 }, - VideoCodec = "libx264" - }; - - // not anamorphic - var version = new MediaVersion - { Width = 1920, Height = 1080, SampleAspectRatio = "1:1" }; - - FFmpegPlaybackSettings actual = _calculator.CalculateSettings( - StreamingMode.TransportStream, - ffmpegProfile, - version, - new MediaStream { Codec = "mpeg2video" }, - new MediaStream(), - DateTimeOffset.Now, - DateTimeOffset.Now, - TimeSpan.Zero, - TimeSpan.Zero, - false, - None); - - actual.ScaledSize.IsNone.Should().BeTrue(); - actual.PadToDesiredResolution.Should().BeFalse(); - actual.VideoCodec.Should().Be("copy"); - } - - [Test] - public void - Should_SetCopyVideoCodec_AndCopyAudioCodec_When_NotTranscoding_ForTransportStream() - { - var ffmpegProfile = new FFmpegProfile - { - Transcode = false, - NormalizeVideo = true, - NormalizeAudio = true, - NormalizeLoudness = true, - Resolution = new Resolution { Width = 1920, Height = 1080 }, - VideoCodec = "libx264" - }; - - // not anamorphic - var version = new MediaVersion - { Width = 1920, Height = 1080, SampleAspectRatio = "1:1" }; - - FFmpegPlaybackSettings actual = _calculator.CalculateSettings( - StreamingMode.TransportStream, - ffmpegProfile, - version, - new MediaStream { Codec = "mpeg2video" }, - new MediaStream(), - DateTimeOffset.Now, - DateTimeOffset.Now, - TimeSpan.Zero, - TimeSpan.Zero, - false, - None); - - actual.ScaledSize.IsNone.Should().BeTrue(); - actual.PadToDesiredResolution.Should().BeFalse(); - actual.VideoCodec.Should().Be("copy"); - actual.NormalizeLoudness.Should().BeFalse(); - actual.AudioCodec.Should().Be("copy"); - } - - [Test] - public void Should_SetVideoBitrate_When_ContentIsPadded_ForTransportStream() - { - var ffmpegProfile = new FFmpegProfile - { - Transcode = true, - NormalizeVideo = true, - Resolution = new Resolution { Width = 1920, Height = 1080 }, - VideoBitrate = 2525 - }; - - // not anamorphic - var version = new MediaVersion { Width = 1918, Height = 1080, SampleAspectRatio = "1:1" }; - - FFmpegPlaybackSettings actual = _calculator.CalculateSettings( - StreamingMode.TransportStream, - ffmpegProfile, - version, - new MediaStream(), - new MediaStream(), - DateTimeOffset.Now, - DateTimeOffset.Now, - TimeSpan.Zero, - TimeSpan.Zero, - false, - None); - - actual.ScaledSize.IsNone.Should().BeTrue(); - actual.PadToDesiredResolution.Should().BeTrue(); - actual.VideoBitrate.IfNone(0).Should().Be(2525); - } - - [Test] - public void Should_SetVideoBitrate_When_ContentIsCorrectSize_And_NormalizingVideo_ForTransportStream() - { - var ffmpegProfile = new FFmpegProfile - { - Transcode = true, - NormalizeVideo = true, - Resolution = new Resolution { Width = 1920, Height = 1080 }, - VideoBitrate = 2525 - }; - - // not anamorphic - var version = new MediaVersion - { Width = 1920, Height = 1080, SampleAspectRatio = "1:1" }; - - FFmpegPlaybackSettings actual = _calculator.CalculateSettings( - StreamingMode.TransportStream, - ffmpegProfile, - version, - new MediaStream { Codec = "mpeg2video" }, - new MediaStream(), - DateTimeOffset.Now, - DateTimeOffset.Now, - TimeSpan.Zero, - TimeSpan.Zero, - false, - None); - - actual.ScaledSize.IsNone.Should().BeTrue(); - actual.PadToDesiredResolution.Should().BeFalse(); - actual.VideoBitrate.IfNone(0).Should().Be(2525); - } - - [Test] - public void Should_SetVideoBufferSize_When_ContentIsPadded_ForTransportStream() - { - var ffmpegProfile = new FFmpegProfile - { - Transcode = true, - NormalizeVideo = true, - Resolution = new Resolution { Width = 1920, Height = 1080 }, - VideoBufferSize = 2525 - }; - - // not anamorphic - var version = new MediaVersion { Width = 1918, Height = 1080, SampleAspectRatio = "1:1" }; - - FFmpegPlaybackSettings actual = _calculator.CalculateSettings( - StreamingMode.TransportStream, - ffmpegProfile, - version, - new MediaStream(), - new MediaStream(), - DateTimeOffset.Now, - DateTimeOffset.Now, - TimeSpan.Zero, - TimeSpan.Zero, - false, - None); - - actual.ScaledSize.IsNone.Should().BeTrue(); - actual.PadToDesiredResolution.Should().BeTrue(); - actual.VideoBufferSize.IfNone(0).Should().Be(2525); - } - - [Test] - public void - Should_SetVideoBufferSize_When_ContentIsCorrectSize_And_NormalizingVideo_ForTransportStream() - { - var ffmpegProfile = new FFmpegProfile - { - Transcode = true, - NormalizeVideo = true, - Resolution = new Resolution { Width = 1920, Height = 1080 }, - VideoBufferSize = 2525 - }; - - // not anamorphic - var version = new MediaVersion - { Width = 1920, Height = 1080, SampleAspectRatio = "1:1" }; - - FFmpegPlaybackSettings actual = _calculator.CalculateSettings( - StreamingMode.TransportStream, - ffmpegProfile, - version, - new MediaStream { Codec = "mpeg2video" }, - new MediaStream(), - DateTimeOffset.Now, - DateTimeOffset.Now, - TimeSpan.Zero, - TimeSpan.Zero, - false, - None); - - actual.ScaledSize.IsNone.Should().BeTrue(); - actual.PadToDesiredResolution.Should().BeFalse(); - actual.VideoBufferSize.IfNone(0).Should().Be(2525); - } - - [Test] - public void Should_SetDesiredAudioCodec_When_NormalizingAudio_With_CorrectCodec_ForTransportStream() - { - FFmpegProfile ffmpegProfile = TestProfile() with - { - Transcode = true, - NormalizeAudio = true, - AudioCodec = "aac" - }; - - var version = new MediaVersion(); - - FFmpegPlaybackSettings actual = _calculator.CalculateSettings( - StreamingMode.TransportStream, - ffmpegProfile, - version, - new MediaStream(), - new MediaStream { Codec = "aac" }, - DateTimeOffset.Now, - DateTimeOffset.Now, - TimeSpan.Zero, - TimeSpan.Zero, - false, - None); - - actual.AudioCodec.Should().Be("aac"); - } - - [Test] - public void Should_SetCopyAudioCodec_When_NotNormalizingAudio_ForTransportStream() - { - FFmpegProfile ffmpegProfile = TestProfile() with - { - NormalizeAudio = false, - AudioCodec = "aac" - }; - - var version = new MediaVersion(); - - FFmpegPlaybackSettings actual = _calculator.CalculateSettings( - StreamingMode.TransportStream, - ffmpegProfile, - version, - new MediaStream(), - new MediaStream { Codec = "ac3" }, - DateTimeOffset.Now, - DateTimeOffset.Now, - TimeSpan.Zero, - TimeSpan.Zero, - false, - None); - - actual.AudioCodec.Should().Be("copy"); - } - - [Test] - public void Should_SetDesiredAudioCodec_When_NormalizingAudio_ForTransportStream() - { - FFmpegProfile ffmpegProfile = TestProfile() with - { - Transcode = true, - NormalizeAudio = true, - AudioCodec = "aac" - }; - - var version = new MediaVersion(); - - FFmpegPlaybackSettings actual = _calculator.CalculateSettings( - StreamingMode.TransportStream, - ffmpegProfile, - version, - new MediaStream(), - new MediaStream { Codec = "ac3" }, - DateTimeOffset.Now, - DateTimeOffset.Now, - TimeSpan.Zero, - TimeSpan.Zero, - false, - None); - - actual.AudioCodec.Should().Be("aac"); - } - - [Test] - public void Should_SetCopyAudioCodec_When_NormalizingAudio_ForHttpLiveStreaming() - { - FFmpegProfile ffmpegProfile = TestProfile() with - { - Transcode = true, - NormalizeAudio = true, - AudioCodec = "aac" - }; - - var version = new MediaVersion(); - - FFmpegPlaybackSettings actual = _calculator.CalculateSettings( - StreamingMode.HttpLiveStreamingDirect, - ffmpegProfile, - version, - new MediaStream(), - new MediaStream { Codec = "ac3" }, - DateTimeOffset.Now, - DateTimeOffset.Now, - TimeSpan.Zero, - TimeSpan.Zero, - false, - None); - - actual.AudioCodec.Should().Be("copy"); - } - - [Test] - public void Should_SetAudioBitrate_When_NormalizingAudio_With_CorrectCodec_ForTransportStream() - { - FFmpegProfile ffmpegProfile = TestProfile() with - { - Transcode = true, - NormalizeAudio = true, - AudioBitrate = 2424, - AudioCodec = "ac3" - }; - - var version = new MediaVersion(); - - FFmpegPlaybackSettings actual = _calculator.CalculateSettings( - StreamingMode.TransportStream, - ffmpegProfile, - version, - new MediaStream(), - new MediaStream { Codec = "ac3" }, - DateTimeOffset.Now, - DateTimeOffset.Now, - TimeSpan.Zero, - TimeSpan.Zero, - false, - None); - - actual.AudioBitrate.IfNone(0).Should().Be(2424); - } - - [Test] - public void Should_SetAudioBufferSize_When_NormalizingAudio_With_CorrectCodec_ForTransportStream() - { - FFmpegProfile ffmpegProfile = TestProfile() with - { - Transcode = true, - NormalizeAudio = true, - AudioBufferSize = 2424, - AudioCodec = "ac3" - }; - - var version = new MediaVersion(); - - FFmpegPlaybackSettings actual = _calculator.CalculateSettings( - StreamingMode.TransportStream, - ffmpegProfile, - version, - new MediaStream(), - new MediaStream { Codec = "ac3" }, - DateTimeOffset.Now, - DateTimeOffset.Now, - TimeSpan.Zero, - TimeSpan.Zero, - false, - None); - - actual.AudioBufferSize.IfNone(0).Should().Be(2424); - } - - [Test] - public void Should_SetAudioChannels_When_NormalizingAudio_With_CorrectCodec_ForTransportStream() - { - FFmpegProfile ffmpegProfile = TestProfile() with - { - Transcode = true, - NormalizeAudio = true, - AudioCodec = "ac3", - AudioChannels = 6 - }; - - var version = new MediaVersion(); - - FFmpegPlaybackSettings actual = _calculator.CalculateSettings( - StreamingMode.TransportStream, - ffmpegProfile, - version, - new MediaStream(), - new MediaStream { Codec = "ac3" }, - DateTimeOffset.Now, - DateTimeOffset.Now, - TimeSpan.Zero, - TimeSpan.Zero, - false, - None); - - actual.AudioChannels.IfNone(0).Should().Be(6); - } - - [Test] - public void Should_SetAudioSampleRate_When_NormalizingAudio_With_CorrectCodec_ForTransportStream() - { - FFmpegProfile ffmpegProfile = TestProfile() with - { - Transcode = true, - NormalizeAudio = true, - AudioCodec = "ac3", - AudioSampleRate = 48 - }; - - var version = new MediaVersion(); - - FFmpegPlaybackSettings actual = _calculator.CalculateSettings( - StreamingMode.TransportStream, - ffmpegProfile, - version, - new MediaStream(), - new MediaStream { Codec = "ac3" }, - DateTimeOffset.Now, - DateTimeOffset.Now, - TimeSpan.Zero, - TimeSpan.Zero, - false, - None); - - actual.AudioSampleRate.IfNone(0).Should().Be(48); - } - - [Test] - public void Should_SetAudioChannels_When_NormalizingAudio_ForTransportStream() - { - FFmpegProfile ffmpegProfile = TestProfile() with - { - Transcode = true, - NormalizeAudio = true, - AudioChannels = 6 - }; - - var version = new MediaVersion(); - - FFmpegPlaybackSettings actual = _calculator.CalculateSettings( - StreamingMode.TransportStream, - ffmpegProfile, - version, - new MediaStream(), - new MediaStream { Codec = "ac3" }, - DateTimeOffset.Now, - DateTimeOffset.Now, - TimeSpan.Zero, - TimeSpan.Zero, - false, - None); - - actual.AudioChannels.IfNone(0).Should().Be(6); - } - - [Test] - public void Should_SetAudioSampleRate_When_NormalizingAudio_ForTransportStream() - { - FFmpegProfile ffmpegProfile = TestProfile() with - { - Transcode = true, - NormalizeAudio = true, - AudioSampleRate = 48 - }; - - var version = new MediaVersion(); - - FFmpegPlaybackSettings actual = _calculator.CalculateSettings( - StreamingMode.TransportStream, - ffmpegProfile, - version, - new MediaStream(), - new MediaStream { Codec = "ac3" }, - DateTimeOffset.Now, - DateTimeOffset.Now, - TimeSpan.Zero, - TimeSpan.Zero, - false, - None); - - actual.AudioSampleRate.IfNone(0).Should().Be(48); - } - - [Test] - public void Should_SetAudioDuration_When_NormalizingAudio_With_CorrectCodec_ForTransportStream() - { - FFmpegProfile ffmpegProfile = TestProfile() with - { - Transcode = true, - NormalizeAudio = true, - AudioSampleRate = 48, - AudioCodec = "ac3" - }; - - var version = new MediaVersion { Duration = TimeSpan.FromMinutes(5) }; // not pulled from here - - FFmpegPlaybackSettings actual = _calculator.CalculateSettings( - StreamingMode.TransportStream, - ffmpegProfile, - version, - new MediaStream(), - new MediaStream { Codec = "ac3" }, - DateTimeOffset.Now, - DateTimeOffset.Now, - TimeSpan.Zero, - TimeSpan.FromMinutes(2), - false, - None); - - actual.AudioDuration.IfNone(TimeSpan.MinValue).Should().Be(TimeSpan.FromMinutes(2)); - } - - [Test] - public void Should_SetNormalizeLoudness_When_NormalizingAudio_ForTransportStream() - { - FFmpegProfile ffmpegProfile = TestProfile() with - { - Transcode = true, - NormalizeAudio = true, - NormalizeLoudness = true - }; - - var version = new MediaVersion(); - - FFmpegPlaybackSettings actual = _calculator.CalculateSettings( - StreamingMode.TransportStream, - ffmpegProfile, - version, - new MediaStream(), - new MediaStream { Codec = "ac3" }, - DateTimeOffset.Now, - DateTimeOffset.Now, - TimeSpan.Zero, - TimeSpan.Zero, - false, - None); - - actual.NormalizeLoudness.Should().BeTrue(); - } - - [Test] - public void Should_NotSetNormalizeLoudness_When_NotNormalizingAudio_ForTransportStream() - { - FFmpegProfile ffmpegProfile = TestProfile() with - { - Transcode = true, - NormalizeAudio = false, - NormalizeLoudness = true - }; - - var version = new MediaVersion(); - - FFmpegPlaybackSettings actual = _calculator.CalculateSettings( - StreamingMode.TransportStream, - ffmpegProfile, - version, - new MediaStream(), - new MediaStream { Codec = "ac3" }, - DateTimeOffset.Now, - DateTimeOffset.Now, - TimeSpan.Zero, - TimeSpan.Zero, - false, - None); - - actual.NormalizeLoudness.Should().BeFalse(); - } + FFmpegProfile ffmpegProfile = TestProfile() with { ThreadCount = 7 }; + + FFmpegPlaybackSettings actual = _calculator.CalculateSettings( + StreamingMode.TransportStream, + ffmpegProfile, + new MediaVersion(), + new MediaStream(), + new MediaStream(), + DateTimeOffset.Now, + DateTimeOffset.Now, + TimeSpan.Zero, + TimeSpan.Zero, + false, + None); + + actual.ThreadCount.Should().Be(1); } - [TestFixture] - public class CalculateSettingsQsv + [Test] + public void Should_UseSpecifiedThreadCount_ForHttpLiveStreamingSegmenter() { - private readonly FFmpegPlaybackSettingsCalculator _calculator; + FFmpegProfile ffmpegProfile = TestProfile() with { ThreadCount = 7 }; - public CalculateSettingsQsv() => _calculator = new FFmpegPlaybackSettingsCalculator(); + FFmpegPlaybackSettings actual = _calculator.CalculateSettings( + StreamingMode.HttpLiveStreamingSegmenter, + ffmpegProfile, + new MediaVersion(), + new MediaStream(), + new MediaStream(), + DateTimeOffset.Now, + DateTimeOffset.Now, + TimeSpan.Zero, + TimeSpan.Zero, + false, + None); - [Test] - public void Should_UseHardwareAcceleration() - { - FFmpegProfile ffmpegProfile = - TestProfile() with { HardwareAcceleration = HardwareAccelerationKind.Qsv }; - - FFmpegPlaybackSettings actual = _calculator.CalculateSettings( - StreamingMode.TransportStream, - ffmpegProfile, - new MediaVersion(), - new MediaStream(), - new MediaStream(), - DateTimeOffset.Now, - DateTimeOffset.Now, - TimeSpan.Zero, - TimeSpan.Zero, - false, - None); - - actual.HardwareAcceleration.Should().Be(HardwareAccelerationKind.Qsv); - } + actual.ThreadCount.Should().Be(7); } - private static FFmpegProfile TestProfile() => - new() { Resolution = new Resolution { Width = 1920, Height = 1080 } }; + [Test] + public void Should_SetFormatFlags_ForTransportStream() + { + FFmpegProfile ffmpegProfile = TestProfile(); + + FFmpegPlaybackSettings actual = _calculator.CalculateSettings( + StreamingMode.TransportStream, + ffmpegProfile, + new MediaVersion(), + new MediaStream(), + new MediaStream(), + DateTimeOffset.Now, + DateTimeOffset.Now, + TimeSpan.Zero, + TimeSpan.Zero, + false, + None); + + string[] expected = { "+genpts", "+discardcorrupt", "+igndts" }; + actual.FormatFlags.Count.Should().Be(expected.Length); + actual.FormatFlags.Should().Contain(expected); + } + + [Test] + public void Should_SetFormatFlags_ForHttpLiveStreaming() + { + FFmpegProfile ffmpegProfile = TestProfile(); + + FFmpegPlaybackSettings actual = _calculator.CalculateSettings( + StreamingMode.HttpLiveStreamingDirect, + ffmpegProfile, + new MediaVersion(), + new MediaStream(), + new MediaStream(), + DateTimeOffset.Now, + DateTimeOffset.Now, + TimeSpan.Zero, + TimeSpan.Zero, + false, + None); + + string[] expected = { "+genpts", "+discardcorrupt", "+igndts" }; + actual.FormatFlags.Count.Should().Be(expected.Length); + actual.FormatFlags.Should().Contain(expected); + } + + [Test] + public void Should_SetRealtime_ForTransportStream() + { + FFmpegProfile ffmpegProfile = TestProfile(); + + FFmpegPlaybackSettings actual = _calculator.CalculateSettings( + StreamingMode.TransportStream, + ffmpegProfile, + new MediaVersion(), + new MediaStream(), + new MediaStream(), + DateTimeOffset.Now, + DateTimeOffset.Now, + TimeSpan.Zero, + TimeSpan.Zero, + false, + None); + + actual.RealtimeOutput.Should().BeTrue(); + } + + [Test] + public void Should_SetRealtime_ForHttpLiveStreaming() + { + FFmpegProfile ffmpegProfile = TestProfile(); + + FFmpegPlaybackSettings actual = _calculator.CalculateSettings( + StreamingMode.HttpLiveStreamingDirect, + ffmpegProfile, + new MediaVersion(), + new MediaStream(), + new MediaStream(), + DateTimeOffset.Now, + DateTimeOffset.Now, + TimeSpan.Zero, + TimeSpan.Zero, + false, + None); + + actual.RealtimeOutput.Should().BeTrue(); + } + + [Test] + public void Should_SetStreamSeek_When_PlaybackIsLate_ForTransportStream() + { + DateTimeOffset now = DateTimeOffset.Now; + + FFmpegProfile ffmpegProfile = TestProfile(); + + FFmpegPlaybackSettings actual = _calculator.CalculateSettings( + StreamingMode.TransportStream, + ffmpegProfile, + new MediaVersion(), + new MediaStream(), + new MediaStream(), + now, + now.AddMinutes(5), + TimeSpan.Zero, + TimeSpan.Zero, + false, + None); + + actual.StreamSeek.IsSome.Should().BeTrue(); + actual.StreamSeek.IfNone(TimeSpan.Zero).Should().Be(TimeSpan.FromMinutes(5)); + } + + [Test] + public void Should_SetStreamSeek_When_PlaybackIsLate_ForHttpLiveStreaming() + { + DateTimeOffset now = DateTimeOffset.Now; + + FFmpegProfile ffmpegProfile = TestProfile(); + + FFmpegPlaybackSettings actual = _calculator.CalculateSettings( + StreamingMode.HttpLiveStreamingDirect, + ffmpegProfile, + new MediaVersion(), + new MediaStream(), + new MediaStream(), + now, + now.AddMinutes(5), + TimeSpan.Zero, + TimeSpan.Zero, + false, + None); + + actual.StreamSeek.IsSome.Should().BeTrue(); + actual.StreamSeek.IfNone(TimeSpan.Zero).Should().Be(TimeSpan.FromMinutes(5)); + } + + [Test] + public void ShouldNot_SetScaledSize_When_NotNormalizingVideo_ForTransportStream() + { + FFmpegProfile ffmpegProfile = TestProfile() with { NormalizeVideo = false }; + + FFmpegPlaybackSettings actual = _calculator.CalculateSettings( + StreamingMode.TransportStream, + ffmpegProfile, + new MediaVersion(), + new MediaStream(), + new MediaStream(), + DateTimeOffset.Now, + DateTimeOffset.Now, + TimeSpan.Zero, + TimeSpan.Zero, + false, + None); + + actual.ScaledSize.IsNone.Should().BeTrue(); + } + + [Test] + public void ShouldNot_SetScaledSize_When_ContentIsCorrectSize_ForTransportStream() + { + FFmpegProfile ffmpegProfile = TestProfile() with + { + NormalizeVideo = true, + Resolution = new Resolution { Width = 1920, Height = 1080 } + }; + + // not anamorphic + var version = new MediaVersion { Width = 1920, Height = 1080, SampleAspectRatio = "1:1" }; + + FFmpegPlaybackSettings actual = _calculator.CalculateSettings( + StreamingMode.TransportStream, + ffmpegProfile, + version, + new MediaStream(), + new MediaStream(), + DateTimeOffset.Now, + DateTimeOffset.Now, + TimeSpan.Zero, + TimeSpan.Zero, + false, + None); + + actual.ScaledSize.IsNone.Should().BeTrue(); + } + + [Test] + public void ShouldNot_SetScaledSize_When_ScaledSizeWouldEqualContentSize_ForTransportStream() + { + FFmpegProfile ffmpegProfile = TestProfile() with + { + NormalizeVideo = true, + Resolution = new Resolution { Width = 1920, Height = 1080 } + }; + + // not anamorphic + var version = new MediaVersion { Width = 1918, Height = 1080, SampleAspectRatio = "1:1" }; + + FFmpegPlaybackSettings actual = _calculator.CalculateSettings( + StreamingMode.TransportStream, + ffmpegProfile, + version, + new MediaStream(), + new MediaStream(), + DateTimeOffset.Now, + DateTimeOffset.Now, + TimeSpan.Zero, + TimeSpan.Zero, + false, + None); + + actual.ScaledSize.IsNone.Should().BeTrue(); + } + + [Test] + public void ShouldNot_PadToDesiredResolution_When_ContentIsCorrectSize_ForTransportStream() + { + FFmpegProfile ffmpegProfile = TestProfile() with + { + NormalizeVideo = true, + Resolution = new Resolution { Width = 1920, Height = 1080 } + }; + + // not anamorphic + var version = new MediaVersion { Width = 1920, Height = 1080, SampleAspectRatio = "1:1" }; + + FFmpegPlaybackSettings actual = _calculator.CalculateSettings( + StreamingMode.TransportStream, + ffmpegProfile, + version, + new MediaStream(), + new MediaStream(), + DateTimeOffset.Now, + DateTimeOffset.Now, + TimeSpan.Zero, + TimeSpan.Zero, + false, + None); + + actual.ScaledSize.IsNone.Should().BeTrue(); + actual.PadToDesiredResolution.Should().BeFalse(); + } + + [Test] + public void Should_PadToDesiredResolution_When_UnscaledContentIsUnderSized_ForTransportStream() + { + FFmpegProfile ffmpegProfile = TestProfile() with + { + Transcode = true, + NormalizeVideo = true, + Resolution = new Resolution { Width = 1920, Height = 1080 } + }; + + // not anamorphic + var version = new MediaVersion { Width = 1918, Height = 1080, SampleAspectRatio = "1:1" }; + + FFmpegPlaybackSettings actual = _calculator.CalculateSettings( + StreamingMode.TransportStream, + ffmpegProfile, + version, + new MediaStream(), + new MediaStream(), + DateTimeOffset.Now, + DateTimeOffset.Now, + TimeSpan.Zero, + TimeSpan.Zero, + false, + None); + + actual.ScaledSize.IsNone.Should().BeTrue(); + actual.PadToDesiredResolution.Should().BeTrue(); + } + + [Test] + public void Should_ScaleToEvenDimensions_ForTransportStream() + { + FFmpegProfile ffmpegProfile = TestProfile() with + { + Transcode = true, + NormalizeVideo = true, + Resolution = new Resolution { Width = 1280, Height = 720 } + }; + + var version = new MediaVersion { Width = 706, Height = 362, SampleAspectRatio = "32:27" }; + + FFmpegPlaybackSettings actual = _calculator.CalculateSettings( + StreamingMode.TransportStream, + ffmpegProfile, + version, + new MediaStream(), + new MediaStream(), + DateTimeOffset.Now, + DateTimeOffset.Now, + TimeSpan.Zero, + TimeSpan.Zero, + false, + None); + + IDisplaySize scaledSize = actual.ScaledSize.IfNone(new MediaVersion { Width = 0, Height = 0 }); + scaledSize.Width.Should().Be(1280); + scaledSize.Height.Should().Be(554); + actual.PadToDesiredResolution.Should().BeTrue(); + } + + [Test] + public void Should_NotPadToDesiredResolution_When_UnscaledContentIsUnderSized_ForHttpLiveStreaming() + { + FFmpegProfile ffmpegProfile = TestProfile() with + { + Transcode = true, + NormalizeVideo = true, + Resolution = new Resolution { Width = 1920, Height = 1080 } + }; + + // not anamorphic + var version = new MediaVersion { Width = 1918, Height = 1080, SampleAspectRatio = "1:1" }; + + FFmpegPlaybackSettings actual = _calculator.CalculateSettings( + StreamingMode.HttpLiveStreamingDirect, + ffmpegProfile, + version, + new MediaStream(), + new MediaStream(), + DateTimeOffset.Now, + DateTimeOffset.Now, + TimeSpan.Zero, + TimeSpan.Zero, + false, + None); + + actual.ScaledSize.IsNone.Should().BeTrue(); + actual.PadToDesiredResolution.Should().BeFalse(); + } + + [Test] + public void Should_NotPadToDesiredResolution_When_NotNormalizingVideo() + { + FFmpegProfile ffmpegProfile = TestProfile() with + { + Transcode = true, + NormalizeVideo = false, + Resolution = new Resolution { Width = 1920, Height = 1080 } + }; + + // not anamorphic + var version = new MediaVersion { Width = 1918, Height = 1080, SampleAspectRatio = "1:1" }; + + FFmpegPlaybackSettings actual = _calculator.CalculateSettings( + StreamingMode.TransportStream, + ffmpegProfile, + version, + new MediaStream(), + new MediaStream(), + DateTimeOffset.Now, + DateTimeOffset.Now, + TimeSpan.Zero, + TimeSpan.Zero, + false, + None); + + actual.ScaledSize.IsNone.Should().BeTrue(); + actual.PadToDesiredResolution.Should().BeFalse(); + } + + [Test] + public void Should_SetDesiredVideoCodec_When_ContentIsPadded_ForTransportStream() + { + var ffmpegProfile = new FFmpegProfile + { + Transcode = true, + NormalizeVideo = true, + Resolution = new Resolution { Width = 1920, Height = 1080 }, + VideoCodec = "testCodec" + }; + + // not anamorphic + var version = new MediaVersion { Width = 1918, Height = 1080, SampleAspectRatio = "1:1" }; + + FFmpegPlaybackSettings actual = _calculator.CalculateSettings( + StreamingMode.TransportStream, + ffmpegProfile, + version, + new MediaStream(), + new MediaStream(), + DateTimeOffset.Now, + DateTimeOffset.Now, + TimeSpan.Zero, + TimeSpan.Zero, + false, + None); + + actual.ScaledSize.IsNone.Should().BeTrue(); + actual.PadToDesiredResolution.Should().BeTrue(); + actual.VideoCodec.Should().Be("testCodec"); + } + + [Test] + public void + Should_SetDesiredVideoCodec_When_ContentIsCorrectSize_And_NormalizingVideo_ForTransportStream() + { + var ffmpegProfile = new FFmpegProfile + { + Transcode = true, + NormalizeVideo = true, + Resolution = new Resolution { Width = 1920, Height = 1080 }, + VideoCodec = "testCodec" + }; + + // not anamorphic + var version = new MediaVersion + { Width = 1920, Height = 1080, SampleAspectRatio = "1:1" }; + + FFmpegPlaybackSettings actual = _calculator.CalculateSettings( + StreamingMode.TransportStream, + ffmpegProfile, + version, + new MediaStream { Codec = "mpeg2video" }, + new MediaStream(), + DateTimeOffset.Now, + DateTimeOffset.Now, + TimeSpan.Zero, + TimeSpan.Zero, + false, + None); + + actual.ScaledSize.IsNone.Should().BeTrue(); + actual.PadToDesiredResolution.Should().BeFalse(); + actual.VideoCodec.Should().Be("testCodec"); + } + + [Test] + public void + Should_SetCopyVideoCodec_When_ContentIsCorrectSize_And_NormalizingVideo_ForHttpLiveStreaming() + { + var ffmpegProfile = new FFmpegProfile + { + Transcode = true, + NormalizeVideo = true, + Resolution = new Resolution { Width = 1920, Height = 1080 }, + VideoCodec = "testCodec" + }; + + // not anamorphic + var version = new MediaVersion + { Width = 1920, Height = 1080, SampleAspectRatio = "1:1" }; + + FFmpegPlaybackSettings actual = _calculator.CalculateSettings( + StreamingMode.HttpLiveStreamingDirect, + ffmpegProfile, + version, + new MediaStream { Codec = "mpeg2video" }, + new MediaStream(), + DateTimeOffset.Now, + DateTimeOffset.Now, + TimeSpan.Zero, + TimeSpan.Zero, + false, + None); + + actual.ScaledSize.IsNone.Should().BeTrue(); + actual.PadToDesiredResolution.Should().BeFalse(); + actual.VideoCodec.Should().Be("copy"); + } + + [Test] + public void Should_SetCopyVideoCodec_When_ContentIsCorrectSize_And_CorrectCodec_ForTransportStream() + { + var ffmpegProfile = new FFmpegProfile + { + Transcode = true, + NormalizeVideo = true, + Resolution = new Resolution { Width = 1920, Height = 1080 }, + VideoCodec = "libx264" + }; + + // not anamorphic + var version = new MediaVersion + { Width = 1920, Height = 1080, SampleAspectRatio = "1:1" }; + + FFmpegPlaybackSettings actual = _calculator.CalculateSettings( + StreamingMode.TransportStream, + ffmpegProfile, + version, + new MediaStream { Codec = "libx264" }, + new MediaStream(), + DateTimeOffset.Now, + DateTimeOffset.Now, + TimeSpan.Zero, + TimeSpan.Zero, + false, + None); + + actual.ScaledSize.IsNone.Should().BeTrue(); + actual.PadToDesiredResolution.Should().BeFalse(); + actual.VideoCodec.Should().Be("copy"); + } + + [Test] + public void + Should_SetCopyVideoCodec_When_ContentIsCorrectSize_And_NotNormalizingVideo_ForTransportStream() + { + var ffmpegProfile = new FFmpegProfile + { + Transcode = true, + NormalizeVideo = false, + Resolution = new Resolution { Width = 1920, Height = 1080 }, + VideoCodec = "libx264" + }; + + // not anamorphic + var version = new MediaVersion + { Width = 1920, Height = 1080, SampleAspectRatio = "1:1" }; + + FFmpegPlaybackSettings actual = _calculator.CalculateSettings( + StreamingMode.TransportStream, + ffmpegProfile, + version, + new MediaStream { Codec = "mpeg2video" }, + new MediaStream(), + DateTimeOffset.Now, + DateTimeOffset.Now, + TimeSpan.Zero, + TimeSpan.Zero, + false, + None); + + actual.ScaledSize.IsNone.Should().BeTrue(); + actual.PadToDesiredResolution.Should().BeFalse(); + actual.VideoCodec.Should().Be("copy"); + } + + [Test] + public void + Should_SetCopyVideoCodec_AndCopyAudioCodec_When_NotTranscoding_ForTransportStream() + { + var ffmpegProfile = new FFmpegProfile + { + Transcode = false, + NormalizeVideo = true, + NormalizeAudio = true, + NormalizeLoudness = true, + Resolution = new Resolution { Width = 1920, Height = 1080 }, + VideoCodec = "libx264" + }; + + // not anamorphic + var version = new MediaVersion + { Width = 1920, Height = 1080, SampleAspectRatio = "1:1" }; + + FFmpegPlaybackSettings actual = _calculator.CalculateSettings( + StreamingMode.TransportStream, + ffmpegProfile, + version, + new MediaStream { Codec = "mpeg2video" }, + new MediaStream(), + DateTimeOffset.Now, + DateTimeOffset.Now, + TimeSpan.Zero, + TimeSpan.Zero, + false, + None); + + actual.ScaledSize.IsNone.Should().BeTrue(); + actual.PadToDesiredResolution.Should().BeFalse(); + actual.VideoCodec.Should().Be("copy"); + actual.NormalizeLoudness.Should().BeFalse(); + actual.AudioCodec.Should().Be("copy"); + } + + [Test] + public void Should_SetVideoBitrate_When_ContentIsPadded_ForTransportStream() + { + var ffmpegProfile = new FFmpegProfile + { + Transcode = true, + NormalizeVideo = true, + Resolution = new Resolution { Width = 1920, Height = 1080 }, + VideoBitrate = 2525 + }; + + // not anamorphic + var version = new MediaVersion { Width = 1918, Height = 1080, SampleAspectRatio = "1:1" }; + + FFmpegPlaybackSettings actual = _calculator.CalculateSettings( + StreamingMode.TransportStream, + ffmpegProfile, + version, + new MediaStream(), + new MediaStream(), + DateTimeOffset.Now, + DateTimeOffset.Now, + TimeSpan.Zero, + TimeSpan.Zero, + false, + None); + + actual.ScaledSize.IsNone.Should().BeTrue(); + actual.PadToDesiredResolution.Should().BeTrue(); + actual.VideoBitrate.IfNone(0).Should().Be(2525); + } + + [Test] + public void Should_SetVideoBitrate_When_ContentIsCorrectSize_And_NormalizingVideo_ForTransportStream() + { + var ffmpegProfile = new FFmpegProfile + { + Transcode = true, + NormalizeVideo = true, + Resolution = new Resolution { Width = 1920, Height = 1080 }, + VideoBitrate = 2525 + }; + + // not anamorphic + var version = new MediaVersion + { Width = 1920, Height = 1080, SampleAspectRatio = "1:1" }; + + FFmpegPlaybackSettings actual = _calculator.CalculateSettings( + StreamingMode.TransportStream, + ffmpegProfile, + version, + new MediaStream { Codec = "mpeg2video" }, + new MediaStream(), + DateTimeOffset.Now, + DateTimeOffset.Now, + TimeSpan.Zero, + TimeSpan.Zero, + false, + None); + + actual.ScaledSize.IsNone.Should().BeTrue(); + actual.PadToDesiredResolution.Should().BeFalse(); + actual.VideoBitrate.IfNone(0).Should().Be(2525); + } + + [Test] + public void Should_SetVideoBufferSize_When_ContentIsPadded_ForTransportStream() + { + var ffmpegProfile = new FFmpegProfile + { + Transcode = true, + NormalizeVideo = true, + Resolution = new Resolution { Width = 1920, Height = 1080 }, + VideoBufferSize = 2525 + }; + + // not anamorphic + var version = new MediaVersion { Width = 1918, Height = 1080, SampleAspectRatio = "1:1" }; + + FFmpegPlaybackSettings actual = _calculator.CalculateSettings( + StreamingMode.TransportStream, + ffmpegProfile, + version, + new MediaStream(), + new MediaStream(), + DateTimeOffset.Now, + DateTimeOffset.Now, + TimeSpan.Zero, + TimeSpan.Zero, + false, + None); + + actual.ScaledSize.IsNone.Should().BeTrue(); + actual.PadToDesiredResolution.Should().BeTrue(); + actual.VideoBufferSize.IfNone(0).Should().Be(2525); + } + + [Test] + public void + Should_SetVideoBufferSize_When_ContentIsCorrectSize_And_NormalizingVideo_ForTransportStream() + { + var ffmpegProfile = new FFmpegProfile + { + Transcode = true, + NormalizeVideo = true, + Resolution = new Resolution { Width = 1920, Height = 1080 }, + VideoBufferSize = 2525 + }; + + // not anamorphic + var version = new MediaVersion + { Width = 1920, Height = 1080, SampleAspectRatio = "1:1" }; + + FFmpegPlaybackSettings actual = _calculator.CalculateSettings( + StreamingMode.TransportStream, + ffmpegProfile, + version, + new MediaStream { Codec = "mpeg2video" }, + new MediaStream(), + DateTimeOffset.Now, + DateTimeOffset.Now, + TimeSpan.Zero, + TimeSpan.Zero, + false, + None); + + actual.ScaledSize.IsNone.Should().BeTrue(); + actual.PadToDesiredResolution.Should().BeFalse(); + actual.VideoBufferSize.IfNone(0).Should().Be(2525); + } + + [Test] + public void Should_SetDesiredAudioCodec_When_NormalizingAudio_With_CorrectCodec_ForTransportStream() + { + FFmpegProfile ffmpegProfile = TestProfile() with + { + Transcode = true, + NormalizeAudio = true, + AudioCodec = "aac" + }; + + var version = new MediaVersion(); + + FFmpegPlaybackSettings actual = _calculator.CalculateSettings( + StreamingMode.TransportStream, + ffmpegProfile, + version, + new MediaStream(), + new MediaStream { Codec = "aac" }, + DateTimeOffset.Now, + DateTimeOffset.Now, + TimeSpan.Zero, + TimeSpan.Zero, + false, + None); + + actual.AudioCodec.Should().Be("aac"); + } + + [Test] + public void Should_SetCopyAudioCodec_When_NotNormalizingAudio_ForTransportStream() + { + FFmpegProfile ffmpegProfile = TestProfile() with + { + NormalizeAudio = false, + AudioCodec = "aac" + }; + + var version = new MediaVersion(); + + FFmpegPlaybackSettings actual = _calculator.CalculateSettings( + StreamingMode.TransportStream, + ffmpegProfile, + version, + new MediaStream(), + new MediaStream { Codec = "ac3" }, + DateTimeOffset.Now, + DateTimeOffset.Now, + TimeSpan.Zero, + TimeSpan.Zero, + false, + None); + + actual.AudioCodec.Should().Be("copy"); + } + + [Test] + public void Should_SetDesiredAudioCodec_When_NormalizingAudio_ForTransportStream() + { + FFmpegProfile ffmpegProfile = TestProfile() with + { + Transcode = true, + NormalizeAudio = true, + AudioCodec = "aac" + }; + + var version = new MediaVersion(); + + FFmpegPlaybackSettings actual = _calculator.CalculateSettings( + StreamingMode.TransportStream, + ffmpegProfile, + version, + new MediaStream(), + new MediaStream { Codec = "ac3" }, + DateTimeOffset.Now, + DateTimeOffset.Now, + TimeSpan.Zero, + TimeSpan.Zero, + false, + None); + + actual.AudioCodec.Should().Be("aac"); + } + + [Test] + public void Should_SetCopyAudioCodec_When_NormalizingAudio_ForHttpLiveStreaming() + { + FFmpegProfile ffmpegProfile = TestProfile() with + { + Transcode = true, + NormalizeAudio = true, + AudioCodec = "aac" + }; + + var version = new MediaVersion(); + + FFmpegPlaybackSettings actual = _calculator.CalculateSettings( + StreamingMode.HttpLiveStreamingDirect, + ffmpegProfile, + version, + new MediaStream(), + new MediaStream { Codec = "ac3" }, + DateTimeOffset.Now, + DateTimeOffset.Now, + TimeSpan.Zero, + TimeSpan.Zero, + false, + None); + + actual.AudioCodec.Should().Be("copy"); + } + + [Test] + public void Should_SetAudioBitrate_When_NormalizingAudio_With_CorrectCodec_ForTransportStream() + { + FFmpegProfile ffmpegProfile = TestProfile() with + { + Transcode = true, + NormalizeAudio = true, + AudioBitrate = 2424, + AudioCodec = "ac3" + }; + + var version = new MediaVersion(); + + FFmpegPlaybackSettings actual = _calculator.CalculateSettings( + StreamingMode.TransportStream, + ffmpegProfile, + version, + new MediaStream(), + new MediaStream { Codec = "ac3" }, + DateTimeOffset.Now, + DateTimeOffset.Now, + TimeSpan.Zero, + TimeSpan.Zero, + false, + None); + + actual.AudioBitrate.IfNone(0).Should().Be(2424); + } + + [Test] + public void Should_SetAudioBufferSize_When_NormalizingAudio_With_CorrectCodec_ForTransportStream() + { + FFmpegProfile ffmpegProfile = TestProfile() with + { + Transcode = true, + NormalizeAudio = true, + AudioBufferSize = 2424, + AudioCodec = "ac3" + }; + + var version = new MediaVersion(); + + FFmpegPlaybackSettings actual = _calculator.CalculateSettings( + StreamingMode.TransportStream, + ffmpegProfile, + version, + new MediaStream(), + new MediaStream { Codec = "ac3" }, + DateTimeOffset.Now, + DateTimeOffset.Now, + TimeSpan.Zero, + TimeSpan.Zero, + false, + None); + + actual.AudioBufferSize.IfNone(0).Should().Be(2424); + } + + [Test] + public void Should_SetAudioChannels_When_NormalizingAudio_With_CorrectCodec_ForTransportStream() + { + FFmpegProfile ffmpegProfile = TestProfile() with + { + Transcode = true, + NormalizeAudio = true, + AudioCodec = "ac3", + AudioChannels = 6 + }; + + var version = new MediaVersion(); + + FFmpegPlaybackSettings actual = _calculator.CalculateSettings( + StreamingMode.TransportStream, + ffmpegProfile, + version, + new MediaStream(), + new MediaStream { Codec = "ac3" }, + DateTimeOffset.Now, + DateTimeOffset.Now, + TimeSpan.Zero, + TimeSpan.Zero, + false, + None); + + actual.AudioChannels.IfNone(0).Should().Be(6); + } + + [Test] + public void Should_SetAudioSampleRate_When_NormalizingAudio_With_CorrectCodec_ForTransportStream() + { + FFmpegProfile ffmpegProfile = TestProfile() with + { + Transcode = true, + NormalizeAudio = true, + AudioCodec = "ac3", + AudioSampleRate = 48 + }; + + var version = new MediaVersion(); + + FFmpegPlaybackSettings actual = _calculator.CalculateSettings( + StreamingMode.TransportStream, + ffmpegProfile, + version, + new MediaStream(), + new MediaStream { Codec = "ac3" }, + DateTimeOffset.Now, + DateTimeOffset.Now, + TimeSpan.Zero, + TimeSpan.Zero, + false, + None); + + actual.AudioSampleRate.IfNone(0).Should().Be(48); + } + + [Test] + public void Should_SetAudioChannels_When_NormalizingAudio_ForTransportStream() + { + FFmpegProfile ffmpegProfile = TestProfile() with + { + Transcode = true, + NormalizeAudio = true, + AudioChannels = 6 + }; + + var version = new MediaVersion(); + + FFmpegPlaybackSettings actual = _calculator.CalculateSettings( + StreamingMode.TransportStream, + ffmpegProfile, + version, + new MediaStream(), + new MediaStream { Codec = "ac3" }, + DateTimeOffset.Now, + DateTimeOffset.Now, + TimeSpan.Zero, + TimeSpan.Zero, + false, + None); + + actual.AudioChannels.IfNone(0).Should().Be(6); + } + + [Test] + public void Should_SetAudioSampleRate_When_NormalizingAudio_ForTransportStream() + { + FFmpegProfile ffmpegProfile = TestProfile() with + { + Transcode = true, + NormalizeAudio = true, + AudioSampleRate = 48 + }; + + var version = new MediaVersion(); + + FFmpegPlaybackSettings actual = _calculator.CalculateSettings( + StreamingMode.TransportStream, + ffmpegProfile, + version, + new MediaStream(), + new MediaStream { Codec = "ac3" }, + DateTimeOffset.Now, + DateTimeOffset.Now, + TimeSpan.Zero, + TimeSpan.Zero, + false, + None); + + actual.AudioSampleRate.IfNone(0).Should().Be(48); + } + + [Test] + public void Should_SetAudioDuration_When_NormalizingAudio_With_CorrectCodec_ForTransportStream() + { + FFmpegProfile ffmpegProfile = TestProfile() with + { + Transcode = true, + NormalizeAudio = true, + AudioSampleRate = 48, + AudioCodec = "ac3" + }; + + var version = new MediaVersion { Duration = TimeSpan.FromMinutes(5) }; // not pulled from here + + FFmpegPlaybackSettings actual = _calculator.CalculateSettings( + StreamingMode.TransportStream, + ffmpegProfile, + version, + new MediaStream(), + new MediaStream { Codec = "ac3" }, + DateTimeOffset.Now, + DateTimeOffset.Now, + TimeSpan.Zero, + TimeSpan.FromMinutes(2), + false, + None); + + actual.AudioDuration.IfNone(TimeSpan.MinValue).Should().Be(TimeSpan.FromMinutes(2)); + } + + [Test] + public void Should_SetNormalizeLoudness_When_NormalizingAudio_ForTransportStream() + { + FFmpegProfile ffmpegProfile = TestProfile() with + { + Transcode = true, + NormalizeAudio = true, + NormalizeLoudness = true + }; + + var version = new MediaVersion(); + + FFmpegPlaybackSettings actual = _calculator.CalculateSettings( + StreamingMode.TransportStream, + ffmpegProfile, + version, + new MediaStream(), + new MediaStream { Codec = "ac3" }, + DateTimeOffset.Now, + DateTimeOffset.Now, + TimeSpan.Zero, + TimeSpan.Zero, + false, + None); + + actual.NormalizeLoudness.Should().BeTrue(); + } + + [Test] + public void Should_NotSetNormalizeLoudness_When_NotNormalizingAudio_ForTransportStream() + { + FFmpegProfile ffmpegProfile = TestProfile() with + { + Transcode = true, + NormalizeAudio = false, + NormalizeLoudness = true + }; + + var version = new MediaVersion(); + + FFmpegPlaybackSettings actual = _calculator.CalculateSettings( + StreamingMode.TransportStream, + ffmpegProfile, + version, + new MediaStream(), + new MediaStream { Codec = "ac3" }, + DateTimeOffset.Now, + DateTimeOffset.Now, + TimeSpan.Zero, + TimeSpan.Zero, + false, + None); + + actual.NormalizeLoudness.Should().BeFalse(); + } } -} + + [TestFixture] + public class CalculateSettingsQsv + { + private readonly FFmpegPlaybackSettingsCalculator _calculator; + + public CalculateSettingsQsv() => _calculator = new FFmpegPlaybackSettingsCalculator(); + + [Test] + public void Should_UseHardwareAcceleration() + { + FFmpegProfile ffmpegProfile = + TestProfile() with { HardwareAcceleration = HardwareAccelerationKind.Qsv }; + + FFmpegPlaybackSettings actual = _calculator.CalculateSettings( + StreamingMode.TransportStream, + ffmpegProfile, + new MediaVersion(), + new MediaStream(), + new MediaStream(), + DateTimeOffset.Now, + DateTimeOffset.Now, + TimeSpan.Zero, + TimeSpan.Zero, + false, + None); + + actual.HardwareAcceleration.Should().Be(HardwareAccelerationKind.Qsv); + } + } + + private static FFmpegProfile TestProfile() => + new() { Resolution = new Resolution { Width = 1920, Height = 1080 } }; +} \ No newline at end of file diff --git a/ErsatzTV.Core.Tests/FFmpeg/HlsPlaylistFilterTests.cs b/ErsatzTV.Core.Tests/FFmpeg/HlsPlaylistFilterTests.cs index 755e3d891..bda2c3acc 100644 --- a/ErsatzTV.Core.Tests/FFmpeg/HlsPlaylistFilterTests.cs +++ b/ErsatzTV.Core.Tests/FFmpeg/HlsPlaylistFilterTests.cs @@ -1,32 +1,31 @@ -using System; -using ErsatzTV.Core.FFmpeg; +using ErsatzTV.Core.FFmpeg; using ErsatzTV.Core.Interfaces.FFmpeg; using FluentAssertions; using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; -namespace ErsatzTV.Core.Tests.FFmpeg +namespace ErsatzTV.Core.Tests.FFmpeg; + +[TestFixture] +public class HlsPlaylistFilterTests { - [TestFixture] - public class HlsPlaylistFilterTests + private HlsPlaylistFilter _hlsPlaylistFilter; + + [SetUp] + public void SetUp() { - private HlsPlaylistFilter _hlsPlaylistFilter; - - [SetUp] - public void SetUp() - { - _hlsPlaylistFilter = new HlsPlaylistFilter( - new Mock().Object, - new Mock>().Object - ); - } + _hlsPlaylistFilter = new HlsPlaylistFilter( + new Mock().Object, + new Mock>().Object + ); + } - [Test] - public void _hlsPlaylistFilter_ShouldRewriteProgramDateTime() - { - var start = new DateTimeOffset(2021, 10, 9, 8, 0, 0, TimeSpan.FromHours(-5)); - string[] input = NormalizeLineEndings(@"#EXTM3U + [Test] + public void _hlsPlaylistFilter_ShouldRewriteProgramDateTime() + { + var start = new DateTimeOffset(2021, 10, 9, 8, 0, 0, TimeSpan.FromHours(-5)); + string[] input = NormalizeLineEndings(@"#EXTM3U #EXT-X-VERSION:6 #EXT-X-TARGETDURATION:4 #EXT-X-MEDIA-SEQUENCE:1137 @@ -42,12 +41,12 @@ live001138.ts #EXT-X-PROGRAM-DATE-TIME:2021-10-08T08:34:57.320-0500 live001139.ts").Split(Environment.NewLine); - TrimPlaylistResult result = _hlsPlaylistFilter.TrimPlaylist(start, start.AddSeconds(-30), input); + TrimPlaylistResult result = _hlsPlaylistFilter.TrimPlaylist(start, start.AddSeconds(-30), input); - result.PlaylistStart.Should().Be(start); - result.Sequence.Should().Be(1137); - result.Playlist.Should().Be(NormalizeLineEndings( - @"#EXTM3U + result.PlaylistStart.Should().Be(start); + result.Sequence.Should().Be(1137); + result.Playlist.Should().Be(NormalizeLineEndings( + @"#EXTM3U #EXT-X-VERSION:6 #EXT-X-TARGETDURATION:4 #EXT-X-MEDIA-SEQUENCE:1137 @@ -64,13 +63,13 @@ live001138.ts #EXT-X-PROGRAM-DATE-TIME:2021-10-09T08:00:08.000-0500 live001139.ts ")); - } + } - [Test] - public void _hlsPlaylistFilter_ShouldLimitSegments() - { - var start = new DateTimeOffset(2021, 10, 9, 8, 0, 0, TimeSpan.FromHours(-5)); - string[] input = NormalizeLineEndings(@"#EXTM3U + [Test] + public void _hlsPlaylistFilter_ShouldLimitSegments() + { + var start = new DateTimeOffset(2021, 10, 9, 8, 0, 0, TimeSpan.FromHours(-5)); + string[] input = NormalizeLineEndings(@"#EXTM3U #EXT-X-VERSION:6 #EXT-X-TARGETDURATION:4 #EXT-X-MEDIA-SEQUENCE:1137 @@ -86,12 +85,12 @@ live001138.ts #EXT-X-PROGRAM-DATE-TIME:2021-10-08T08:34:57.320-0500 live001139.ts").Split(Environment.NewLine); - TrimPlaylistResult result = _hlsPlaylistFilter.TrimPlaylist(start, start.AddSeconds(-30), input, 2); + TrimPlaylistResult result = _hlsPlaylistFilter.TrimPlaylist(start, start.AddSeconds(-30), input, 2); - result.PlaylistStart.Should().Be(start); - result.Sequence.Should().Be(1137); - result.Playlist.Should().Be(NormalizeLineEndings( - @"#EXTM3U + result.PlaylistStart.Should().Be(start); + result.Sequence.Should().Be(1137); + result.Playlist.Should().Be(NormalizeLineEndings( + @"#EXTM3U #EXT-X-VERSION:6 #EXT-X-TARGETDURATION:4 #EXT-X-MEDIA-SEQUENCE:1137 @@ -105,13 +104,13 @@ live001137.ts #EXT-X-PROGRAM-DATE-TIME:2021-10-09T08:00:04.000-0500 live001138.ts ")); - } + } - [Test] - public void _hlsPlaylistFilter_ShouldAddDiscontinuity() - { - var start = new DateTimeOffset(2021, 10, 9, 8, 0, 0, TimeSpan.FromHours(-5)); - string[] input = NormalizeLineEndings(@"#EXTM3U + [Test] + public void _hlsPlaylistFilter_ShouldAddDiscontinuity() + { + var start = new DateTimeOffset(2021, 10, 9, 8, 0, 0, TimeSpan.FromHours(-5)); + string[] input = NormalizeLineEndings(@"#EXTM3U #EXT-X-VERSION:6 #EXT-X-TARGETDURATION:4 #EXT-X-MEDIA-SEQUENCE:1137 @@ -127,17 +126,17 @@ live001138.ts #EXT-X-PROGRAM-DATE-TIME:2021-10-08T08:34:57.320-0500 live001139.ts").Split(Environment.NewLine); - TrimPlaylistResult result = _hlsPlaylistFilter.TrimPlaylist( - start, - start.AddSeconds(-30), - input, - int.MaxValue, - true); + TrimPlaylistResult result = _hlsPlaylistFilter.TrimPlaylist( + start, + start.AddSeconds(-30), + input, + int.MaxValue, + true); - result.PlaylistStart.Should().Be(start); - result.Sequence.Should().Be(1137); - result.Playlist.Should().Be(NormalizeLineEndings( - @"#EXTM3U + result.PlaylistStart.Should().Be(start); + result.Sequence.Should().Be(1137); + result.Playlist.Should().Be(NormalizeLineEndings( + @"#EXTM3U #EXT-X-VERSION:6 #EXT-X-TARGETDURATION:4 #EXT-X-MEDIA-SEQUENCE:1137 @@ -155,13 +154,13 @@ live001138.ts live001139.ts #EXT-X-DISCONTINUITY ")); - } + } - [Test] - public void _hlsPlaylistFilter_ShouldFilterOldSegments() - { - var start = new DateTimeOffset(2021, 10, 9, 8, 0, 0, TimeSpan.FromHours(-5)); - string[] input = NormalizeLineEndings(@"#EXTM3U + [Test] + public void _hlsPlaylistFilter_ShouldFilterOldSegments() + { + var start = new DateTimeOffset(2021, 10, 9, 8, 0, 0, TimeSpan.FromHours(-5)); + string[] input = NormalizeLineEndings(@"#EXTM3U #EXT-X-VERSION:6 #EXT-X-TARGETDURATION:4 #EXT-X-MEDIA-SEQUENCE:1137 @@ -177,12 +176,12 @@ live001138.ts #EXT-X-PROGRAM-DATE-TIME:2021-10-08T08:34:57.320-0500 live001139.ts").Split(Environment.NewLine); - TrimPlaylistResult result = _hlsPlaylistFilter.TrimPlaylist(start, start.AddSeconds(6), input); + TrimPlaylistResult result = _hlsPlaylistFilter.TrimPlaylist(start, start.AddSeconds(6), input); - result.PlaylistStart.Should().Be(start.AddSeconds(8)); - result.Sequence.Should().Be(1139); - result.Playlist.Should().Be(NormalizeLineEndings( - @"#EXTM3U + result.PlaylistStart.Should().Be(start.AddSeconds(8)); + result.Sequence.Should().Be(1139); + result.Playlist.Should().Be(NormalizeLineEndings( + @"#EXTM3U #EXT-X-VERSION:6 #EXT-X-TARGETDURATION:4 #EXT-X-MEDIA-SEQUENCE:1139 @@ -193,13 +192,13 @@ live001139.ts").Split(Environment.NewLine); #EXT-X-PROGRAM-DATE-TIME:2021-10-09T08:00:08.000-0500 live001139.ts ")); - } + } - [Test] - public void _hlsPlaylistFilter_ShouldFilterOldDiscontinuity() - { - var start = new DateTimeOffset(2021, 10, 9, 8, 0, 0, TimeSpan.FromHours(-5)); - string[] input = NormalizeLineEndings(@"#EXTM3U + [Test] + public void _hlsPlaylistFilter_ShouldFilterOldDiscontinuity() + { + var start = new DateTimeOffset(2021, 10, 9, 8, 0, 0, TimeSpan.FromHours(-5)); + string[] input = NormalizeLineEndings(@"#EXTM3U #EXT-X-VERSION:6 #EXT-X-TARGETDURATION:4 #EXT-X-MEDIA-SEQUENCE:1137 @@ -216,12 +215,12 @@ live001138.ts #EXT-X-PROGRAM-DATE-TIME:2021-10-08T08:34:57.320-0500 live001139.ts").Split(Environment.NewLine); - TrimPlaylistResult result = _hlsPlaylistFilter.TrimPlaylist(start, start.AddSeconds(6), input); + TrimPlaylistResult result = _hlsPlaylistFilter.TrimPlaylist(start, start.AddSeconds(6), input); - result.PlaylistStart.Should().Be(start.AddSeconds(8)); - result.Sequence.Should().Be(1139); - result.Playlist.Should().Be(NormalizeLineEndings( - @"#EXTM3U + result.PlaylistStart.Should().Be(start.AddSeconds(8)); + result.Sequence.Should().Be(1139); + result.Playlist.Should().Be(NormalizeLineEndings( + @"#EXTM3U #EXT-X-VERSION:6 #EXT-X-TARGETDURATION:4 #EXT-X-MEDIA-SEQUENCE:1139 @@ -232,14 +231,13 @@ live001139.ts").Split(Environment.NewLine); #EXT-X-PROGRAM-DATE-TIME:2021-10-09T08:00:08.000-0500 live001139.ts ")); - } - - private static string NormalizeLineEndings(string str) - { - return str - .Replace("\r\n", "\n") - .Replace("\r", "\n") - .Replace("\n", Environment.NewLine); - } } -} + + private static string NormalizeLineEndings(string str) + { + return str + .Replace("\r\n", "\n") + .Replace("\r", "\n") + .Replace("\n", Environment.NewLine); + } +} \ No newline at end of file diff --git a/ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs b/ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs index d50095749..92128e8e9 100644 --- a/ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs +++ b/ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs @@ -1,12 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; +using System.Diagnostics; using System.Security.Cryptography; using System.Text; -using System.Threading; -using System.Threading.Tasks; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Filler; using ErsatzTV.Core.FFmpeg; @@ -15,173 +9,171 @@ using ErsatzTV.Core.Interfaces.Images; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Metadata; using FluentAssertions; -using LanguageExt; using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; using Serilog; -using static LanguageExt.Prelude; -namespace ErsatzTV.Core.Tests.FFmpeg +namespace ErsatzTV.Core.Tests.FFmpeg; + +[TestFixture] +[Explicit] +public class TranscodingTests { - [TestFixture] - [Explicit] - public class TranscodingTests - { - private static readonly ILoggerFactory LoggerFactory; + private static readonly ILoggerFactory LoggerFactory; - static TranscodingTests() - { - Log.Logger = new LoggerConfiguration() - .MinimumLevel.Debug() - .WriteTo.Console() - .CreateLogger(); + static TranscodingTests() + { + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Debug() + .WriteTo.Console() + .CreateLogger(); - LoggerFactory = new LoggerFactory().AddSerilog(Log.Logger); + LoggerFactory = new LoggerFactory().AddSerilog(Log.Logger); + } + + [Test] + [Explicit] + public void DeleteTestVideos() + { + foreach (string file in Directory.GetFiles(TestContext.CurrentContext.TestDirectory, "*.mkv")) + { + File.Delete(file); } - [Test] - [Explicit] - public void DeleteTestVideos() + Assert.Pass(); + } + + public record InputFormat(string Encoder, string PixelFormat); + + public enum Padding + { + NoPadding, + WithPadding + } + + public enum Watermark + { + None, + PermanentOpaque, + PermanentTransparent, + IntermittentOpaque, + IntermittentTransparent + // TODO: animated vs static + } + + private class TestData + { + public static Watermark[] Watermarks = { - foreach (string file in Directory.GetFiles(TestContext.CurrentContext.TestDirectory, "*.mkv")) - { - File.Delete(file); - } - - Assert.Pass(); - } - - public record InputFormat(string Encoder, string PixelFormat); - - public enum Padding - { - NoPadding, - WithPadding - } - - public enum Watermark - { - None, - PermanentOpaque, - PermanentTransparent, - IntermittentOpaque, - IntermittentTransparent - // TODO: animated vs static - } - - private class TestData - { - public static Watermark[] Watermarks = - { - Watermark.None, - Watermark.PermanentOpaque, - Watermark.PermanentTransparent - }; + Watermark.None, + Watermark.PermanentOpaque, + Watermark.PermanentTransparent + }; - public static Padding[] Paddings = - { - Padding.NoPadding, - Padding.WithPadding - }; + public static Padding[] Paddings = + { + Padding.NoPadding, + Padding.WithPadding + }; - public static VideoScanKind[] VideoScanKinds = - { - VideoScanKind.Progressive, - VideoScanKind.Interlaced - }; + public static VideoScanKind[] VideoScanKinds = + { + VideoScanKind.Progressive, + VideoScanKind.Interlaced + }; - public static InputFormat[] InputFormats = - { - new("libx264", "yuv420p"), - new("libx264", "yuvj420p"), - new("libx264", "yuv420p10le"), - // new("libx264", "yuv444p10le"), + public static InputFormat[] InputFormats = + { + new("libx264", "yuv420p"), + new("libx264", "yuvj420p"), + new("libx264", "yuv420p10le"), + // new("libx264", "yuv444p10le"), - new("mpeg1video", "yuv420p"), + new("mpeg1video", "yuv420p"), - new("mpeg2video", "yuv420p"), + new("mpeg2video", "yuv420p"), - new("libx265", "yuv420p"), - new("libx265", "yuv420p10le"), + new("libx265", "yuv420p"), + new("libx265", "yuv420p10le"), - new("mpeg4", "yuv420p"), + new("mpeg4", "yuv420p"), - new("libvpx-vp9", "yuv420p"), + new("libvpx-vp9", "yuv420p"), - // new("libaom-av1", "yuv420p") - // av1 yuv420p10le 51 + // new("libaom-av1", "yuv420p") + // av1 yuv420p10le 51 - new("msmpeg4v2", "yuv420p"), - new("msmpeg4v3", "yuv420p") + new("msmpeg4v2", "yuv420p"), + new("msmpeg4v3", "yuv420p") - // wmv3 yuv420p 1 - }; + // wmv3 yuv420p 1 + }; - public static Resolution[] Resolutions = - { - new() { Width = 1920, Height = 1080 }, - new() { Width = 1280, Height = 720 } - }; + public static Resolution[] Resolutions = + { + new() { Width = 1920, Height = 1080 }, + new() { Width = 1280, Height = 720 } + }; - public static string[] SoftwareCodecs = - { - "libx264", - "libx265" - }; + public static string[] SoftwareCodecs = + { + "libx264", + "libx265" + }; - public static HardwareAccelerationKind[] NoAcceleration = - { - HardwareAccelerationKind.None - }; + public static HardwareAccelerationKind[] NoAcceleration = + { + HardwareAccelerationKind.None + }; - public static string[] NvidiaCodecs = - { - "h264_nvenc", - "hevc_nvenc" - }; + public static string[] NvidiaCodecs = + { + "h264_nvenc", + "hevc_nvenc" + }; - public static HardwareAccelerationKind[] NvidiaAcceleration = - { - HardwareAccelerationKind.Nvenc - }; + public static HardwareAccelerationKind[] NvidiaAcceleration = + { + HardwareAccelerationKind.Nvenc + }; - public static string[] VaapiCodecs = - { - "h264_vaapi", - "hevc_vaapi" - }; + public static string[] VaapiCodecs = + { + "h264_vaapi", + "hevc_vaapi" + }; - public static HardwareAccelerationKind[] VaapiAcceleration = - { - HardwareAccelerationKind.Vaapi - }; + public static HardwareAccelerationKind[] VaapiAcceleration = + { + HardwareAccelerationKind.Vaapi + }; - public static string[] VideoToolboxCodecs = - { - "h264_videotoolbox", - "hevc_videotoolbox" - }; + public static string[] VideoToolboxCodecs = + { + "h264_videotoolbox", + "hevc_videotoolbox" + }; - public static HardwareAccelerationKind[] VideoToolboxAcceleration = - { - HardwareAccelerationKind.VideoToolbox - }; + public static HardwareAccelerationKind[] VideoToolboxAcceleration = + { + HardwareAccelerationKind.VideoToolbox + }; - public static string[] QsvCodecs = - { - "h264_qsv", - "hevc_qsv" - }; + public static string[] QsvCodecs = + { + "h264_qsv", + "hevc_qsv" + }; - public static HardwareAccelerationKind[] QsvAcceleration = - { - HardwareAccelerationKind.Qsv - }; - } + public static HardwareAccelerationKind[] QsvAcceleration = + { + HardwareAccelerationKind.Qsv + }; + } - [Test, Combinatorial] - public async Task Transcode( + [Test, Combinatorial] + public async Task Transcode( [ValueSource(typeof(TestData), nameof(TestData.InputFormats))] InputFormat inputFormat, [ValueSource(typeof(TestData), nameof(TestData.Resolutions))] @@ -196,280 +188,279 @@ namespace ErsatzTV.Core.Tests.FFmpeg // [ValueSource(typeof(TestData), nameof(TestData.NoAcceleration))] HardwareAccelerationKind profileAcceleration) [ValueSource(typeof(TestData), nameof(TestData.NvidiaCodecs))] string profileCodec, [ValueSource(typeof(TestData), nameof(TestData.NvidiaAcceleration))] HardwareAccelerationKind profileAcceleration) - // [ValueSource(typeof(TestData), nameof(TestData.VaapiCodecs))] string profileCodec, - // [ValueSource(typeof(TestData), nameof(TestData.VaapiAcceleration))] HardwareAccelerationKind profileAcceleration) - // [ValueSource(typeof(TestData), nameof(TestData.QsvCodecs))] string profileCodec, - // [ValueSource(typeof(TestData), nameof(TestData.QsvAcceleration))] HardwareAccelerationKind profileAcceleration) - // [ValueSource(typeof(TestData), nameof(TestData.VideoToolboxCodecs))] string profileCodec, - // [ValueSource(typeof(TestData), nameof(TestData.VideoToolboxAcceleration))] HardwareAccelerationKind profileAcceleration) + // [ValueSource(typeof(TestData), nameof(TestData.VaapiCodecs))] string profileCodec, + // [ValueSource(typeof(TestData), nameof(TestData.VaapiAcceleration))] HardwareAccelerationKind profileAcceleration) + // [ValueSource(typeof(TestData), nameof(TestData.QsvCodecs))] string profileCodec, + // [ValueSource(typeof(TestData), nameof(TestData.QsvAcceleration))] HardwareAccelerationKind profileAcceleration) + // [ValueSource(typeof(TestData), nameof(TestData.VideoToolboxCodecs))] string profileCodec, + // [ValueSource(typeof(TestData), nameof(TestData.VideoToolboxAcceleration))] HardwareAccelerationKind profileAcceleration) + { + if (inputFormat.Encoder is "mpeg1video" or "msmpeg4v2" or "msmpeg4v3") { - if (inputFormat.Encoder is "mpeg1video" or "msmpeg4v2" or "msmpeg4v3") + if (videoScanKind == VideoScanKind.Interlaced) { - if (videoScanKind == VideoScanKind.Interlaced) - { - Assert.Inconclusive($"{inputFormat.Encoder} does not support interlaced content"); - return; - } - } - - string name = GetStringSha256Hash( - $"{inputFormat.Encoder}_{inputFormat.PixelFormat}_{videoScanKind}_{padding}_{profileResolution}_{profileCodec}_{profileAcceleration}"); - - string file = Path.Combine(TestContext.CurrentContext.TestDirectory, $"{name}.mkv"); - if (!File.Exists(file)) - { - string resolution = padding == Padding.WithPadding ? "1920x1060" : "1920x1080"; - - string videoFilter = videoScanKind == VideoScanKind.Interlaced ? "-vf tinterlace=interleave_top,fieldorder=tff" : string.Empty; - string flags = videoScanKind == VideoScanKind.Interlaced ? "-flags +ildct+ilme" : string.Empty; - - string args = - $"-y -f lavfi -i anoisesrc=color=brown -f lavfi -i testsrc=duration=1:size={resolution}:rate=30 {videoFilter} -c:a aac -c:v {inputFormat.Encoder} -shortest -pix_fmt {inputFormat.PixelFormat} -strict -2 {flags} {file}"; - var p1 = new Process - { - StartInfo = new ProcessStartInfo - { - FileName = ExecutableName("ffmpeg"), - Arguments = args - } - }; - - p1.Start(); - await p1.WaitForExitAsync(); - // ReSharper disable once MethodHasAsyncOverload - p1.WaitForExit(); - p1.ExitCode.Should().Be(0); - } - - var imageCache = new Mock(); - - // always return the static watermark resource - imageCache.Setup( - ic => ic.GetPathForImage( - It.IsAny(), - It.Is(x => x == ArtworkKind.Watermark), - It.IsAny>())) - .Returns(Path.Combine(TestContext.CurrentContext.TestDirectory, "Resources", "ErsatzTV.png")); - - var oldService = new FFmpegProcessService( - new FFmpegPlaybackSettingsCalculator(), - new FakeStreamSelector(), - imageCache.Object, - new Mock().Object, - LoggerFactory.CreateLogger()); - - var service = new FFmpegLibraryProcessService( - oldService, - new FFmpegPlaybackSettingsCalculator(), - new FakeStreamSelector(), - LoggerFactory.CreateLogger()); - - var v = new MediaVersion - { - MediaFiles = new List - { - new() { Path = file } - } - }; - - var metadataRepository = new Mock(); - metadataRepository - .Setup(r => r.UpdateLocalStatistics(It.IsAny(), It.IsAny(), It.IsAny())) - .Callback((_, version, _) => - { - version.MediaFiles = v.MediaFiles; - v = version; - }); - - var localStatisticsProvider = new LocalStatisticsProvider( - metadataRepository.Object, - new LocalFileSystem(LoggerFactory.CreateLogger()), - LoggerFactory.CreateLogger()); - - await localStatisticsProvider.RefreshStatistics( - ExecutableName("ffprobe"), - new Movie - { - MediaVersions = new List - { - new() - { - MediaFiles = new List - { - new() { Path = file } - } - } - } - }); - - DateTimeOffset now = DateTimeOffset.Now; - - Option channelWatermark = Option.None; - switch (watermark) - { - case Watermark.None: - break; - case Watermark.IntermittentOpaque: - channelWatermark = new ChannelWatermark - { - ImageSource = ChannelWatermarkImageSource.Custom, - Mode = ChannelWatermarkMode.Intermittent, - // TODO: how do we make sure this actually appears - FrequencyMinutes = 1, - DurationSeconds = 2, - Opacity = 100 - }; - break; - case Watermark.IntermittentTransparent: - channelWatermark = new ChannelWatermark - { - ImageSource = ChannelWatermarkImageSource.Custom, - Mode = ChannelWatermarkMode.Intermittent, - // TODO: how do we make sure this actually appears - FrequencyMinutes = 1, - DurationSeconds = 2, - Opacity = 80 - }; - break; - case Watermark.PermanentOpaque: - channelWatermark = new ChannelWatermark - { - ImageSource = ChannelWatermarkImageSource.Custom, - Mode = ChannelWatermarkMode.Permanent, - Opacity = 100 - }; - break; - case Watermark.PermanentTransparent: - channelWatermark = new ChannelWatermark - { - ImageSource = ChannelWatermarkImageSource.Custom, - Mode = ChannelWatermarkMode.Permanent, - Opacity = 80 - }; - break; - } - - Process process = await service.ForPlayoutItem( - ExecutableName("ffmpeg"), - false, - new Channel(Guid.NewGuid()) - { - Number = "1", - FFmpegProfile = FFmpegProfile.New("test", profileResolution) with - { - HardwareAcceleration = profileAcceleration, - VideoCodec = profileCodec, - AudioCodec = "aac" - }, - StreamingMode = StreamingMode.TransportStream - }, - v, - v, - file, - file, - now, - now + TimeSpan.FromSeconds(5), - now, - channelWatermark, - VaapiDriver.Default, - "/dev/dri/renderD128", - false, - FillerKind.None, - TimeSpan.Zero, - TimeSpan.FromSeconds(5), - 0, - None); - - process.StartInfo.RedirectStandardError = true; - process.EnableRaisingEvents = true; - - // Console.WriteLine($"ffmpeg arguments {string.Join(" ", process.StartInfo.ArgumentList)}"); - - process.Start().Should().BeTrue(); - - string[] unsupportedMessages = - { - "No support for codec", - "No usable", - "Provided device doesn't support" - }; - - var errorBuffer = new StringBuilder(); - - process.ErrorDataReceived += (_, errorLine) => - { - string data = errorLine.Data ?? string.Empty; - errorBuffer.AppendLine(data); - }; - - process.BeginOutputReadLine(); - process.BeginErrorReadLine(); - // string error = await process.StandardError.ReadToEndAsync(); - - var timeoutSignal = new CancellationTokenSource(TimeSpan.FromSeconds(30)); - try - { - await process.WaitForExitAsync(timeoutSignal.Token); - // ReSharper disable once MethodHasAsyncOverload - process.WaitForExit(); - } - catch (OperationCanceledException) - { - process.Kill(); - - IEnumerable quotedArgs = process.StartInfo.ArgumentList.Map(a => $"\'{a}\'"); - Assert.Fail($"Transcode failure (timeout): ffmpeg {string.Join(" ", quotedArgs)}"); + Assert.Inconclusive($"{inputFormat.Encoder} does not support interlaced content"); return; } + } - var error = errorBuffer.ToString(); - bool isUnsupported = unsupportedMessages.Any(error.Contains); + string name = GetStringSha256Hash( + $"{inputFormat.Encoder}_{inputFormat.PixelFormat}_{videoScanKind}_{padding}_{profileResolution}_{profileCodec}_{profileAcceleration}"); - if (profileAcceleration != HardwareAccelerationKind.None && isUnsupported) + string file = Path.Combine(TestContext.CurrentContext.TestDirectory, $"{name}.mkv"); + if (!File.Exists(file)) + { + string resolution = padding == Padding.WithPadding ? "1920x1060" : "1920x1080"; + + string videoFilter = videoScanKind == VideoScanKind.Interlaced ? "-vf tinterlace=interleave_top,fieldorder=tff" : string.Empty; + string flags = videoScanKind == VideoScanKind.Interlaced ? "-flags +ildct+ilme" : string.Empty; + + string args = + $"-y -f lavfi -i anoisesrc=color=brown -f lavfi -i testsrc=duration=1:size={resolution}:rate=30 {videoFilter} -c:a aac -c:v {inputFormat.Encoder} -shortest -pix_fmt {inputFormat.PixelFormat} -strict -2 {flags} {file}"; + var p1 = new Process { - var quotedArgs = process.StartInfo.ArgumentList.Map(a => $"\'{a}\'").ToList(); - process.ExitCode.Should().Be(1, $"Error message with successful exit code? {string.Join(" ", quotedArgs)}"); - Assert.Warn($"Unsupported on this hardware: ffmpeg {string.Join(" ", quotedArgs)}"); - } - else if (error.Contains("Impossible to convert between")) - { - IEnumerable quotedArgs = process.StartInfo.ArgumentList.Map(a => $"\'{a}\'"); - Assert.Fail($"Transcode failure: ffmpeg {string.Join(" ", quotedArgs)}"); - } - else - { - var quotedArgs = process.StartInfo.ArgumentList.Map(a => $"\'{a}\'").ToList(); - process.ExitCode.Should().Be(0, errorBuffer + Environment.NewLine + string.Join(" ", quotedArgs)); - if (process.ExitCode == 0) + StartInfo = new ProcessStartInfo { - Console.WriteLine(string.Join(" ", quotedArgs)); + FileName = ExecutableName("ffmpeg"), + Arguments = args } - } + }; + + p1.Start(); + await p1.WaitForExitAsync(); + // ReSharper disable once MethodHasAsyncOverload + p1.WaitForExit(); + p1.ExitCode.Should().Be(0); } - - private static string GetStringSha256Hash(string text) + + var imageCache = new Mock(); + + // always return the static watermark resource + imageCache.Setup( + ic => ic.GetPathForImage( + It.IsAny(), + It.Is(x => x == ArtworkKind.Watermark), + It.IsAny>())) + .Returns(Path.Combine(TestContext.CurrentContext.TestDirectory, "Resources", "ErsatzTV.png")); + + var oldService = new FFmpegProcessService( + new FFmpegPlaybackSettingsCalculator(), + new FakeStreamSelector(), + imageCache.Object, + new Mock().Object, + LoggerFactory.CreateLogger()); + + var service = new FFmpegLibraryProcessService( + oldService, + new FFmpegPlaybackSettingsCalculator(), + new FakeStreamSelector(), + LoggerFactory.CreateLogger()); + + var v = new MediaVersion { - if (string.IsNullOrEmpty(text)) + MediaFiles = new List { - return string.Empty; + new() { Path = file } } + }; - using var sha = SHA256.Create(); - byte[] textData = Encoding.UTF8.GetBytes(text); - byte[] hash = sha.ComputeHash(textData); - return BitConverter.ToString(hash).Replace("-", string.Empty); - } + var metadataRepository = new Mock(); + metadataRepository + .Setup(r => r.UpdateLocalStatistics(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((_, version, _) => + { + version.MediaFiles = v.MediaFiles; + v = version; + }); - private class FakeStreamSelector : IFFmpegStreamSelector + var localStatisticsProvider = new LocalStatisticsProvider( + metadataRepository.Object, + new LocalFileSystem(LoggerFactory.CreateLogger()), + LoggerFactory.CreateLogger()); + + await localStatisticsProvider.RefreshStatistics( + ExecutableName("ffprobe"), + new Movie + { + MediaVersions = new List + { + new() + { + MediaFiles = new List + { + new() { Path = file } + } + } + } + }); + + DateTimeOffset now = DateTimeOffset.Now; + + Option channelWatermark = Option.None; + switch (watermark) { - public Task SelectVideoStream(Channel channel, MediaVersion version) => - version.Streams.First(s => s.MediaStreamKind == MediaStreamKind.Video).AsTask(); - - public Task> SelectAudioStream(Channel channel, MediaVersion version) => - Optional(version.Streams.First(s => s.MediaStreamKind == MediaStreamKind.Audio)).AsTask(); + case Watermark.None: + break; + case Watermark.IntermittentOpaque: + channelWatermark = new ChannelWatermark + { + ImageSource = ChannelWatermarkImageSource.Custom, + Mode = ChannelWatermarkMode.Intermittent, + // TODO: how do we make sure this actually appears + FrequencyMinutes = 1, + DurationSeconds = 2, + Opacity = 100 + }; + break; + case Watermark.IntermittentTransparent: + channelWatermark = new ChannelWatermark + { + ImageSource = ChannelWatermarkImageSource.Custom, + Mode = ChannelWatermarkMode.Intermittent, + // TODO: how do we make sure this actually appears + FrequencyMinutes = 1, + DurationSeconds = 2, + Opacity = 80 + }; + break; + case Watermark.PermanentOpaque: + channelWatermark = new ChannelWatermark + { + ImageSource = ChannelWatermarkImageSource.Custom, + Mode = ChannelWatermarkMode.Permanent, + Opacity = 100 + }; + break; + case Watermark.PermanentTransparent: + channelWatermark = new ChannelWatermark + { + ImageSource = ChannelWatermarkImageSource.Custom, + Mode = ChannelWatermarkMode.Permanent, + Opacity = 80 + }; + break; } - private static string ExecutableName(string baseName) => - OperatingSystem.IsWindows() ? $"{baseName}.exe" : baseName; + Process process = await service.ForPlayoutItem( + ExecutableName("ffmpeg"), + false, + new Channel(Guid.NewGuid()) + { + Number = "1", + FFmpegProfile = FFmpegProfile.New("test", profileResolution) with + { + HardwareAcceleration = profileAcceleration, + VideoCodec = profileCodec, + AudioCodec = "aac" + }, + StreamingMode = StreamingMode.TransportStream + }, + v, + v, + file, + file, + now, + now + TimeSpan.FromSeconds(5), + now, + channelWatermark, + VaapiDriver.Default, + "/dev/dri/renderD128", + false, + FillerKind.None, + TimeSpan.Zero, + TimeSpan.FromSeconds(5), + 0, + None); + + process.StartInfo.RedirectStandardError = true; + process.EnableRaisingEvents = true; + + // Console.WriteLine($"ffmpeg arguments {string.Join(" ", process.StartInfo.ArgumentList)}"); + + process.Start().Should().BeTrue(); + + string[] unsupportedMessages = + { + "No support for codec", + "No usable", + "Provided device doesn't support" + }; + + var errorBuffer = new StringBuilder(); + + process.ErrorDataReceived += (_, errorLine) => + { + string data = errorLine.Data ?? string.Empty; + errorBuffer.AppendLine(data); + }; + + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + // string error = await process.StandardError.ReadToEndAsync(); + + var timeoutSignal = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + try + { + await process.WaitForExitAsync(timeoutSignal.Token); + // ReSharper disable once MethodHasAsyncOverload + process.WaitForExit(); + } + catch (OperationCanceledException) + { + process.Kill(); + + IEnumerable quotedArgs = process.StartInfo.ArgumentList.Map(a => $"\'{a}\'"); + Assert.Fail($"Transcode failure (timeout): ffmpeg {string.Join(" ", quotedArgs)}"); + return; + } + + var error = errorBuffer.ToString(); + bool isUnsupported = unsupportedMessages.Any(error.Contains); + + if (profileAcceleration != HardwareAccelerationKind.None && isUnsupported) + { + var quotedArgs = process.StartInfo.ArgumentList.Map(a => $"\'{a}\'").ToList(); + process.ExitCode.Should().Be(1, $"Error message with successful exit code? {string.Join(" ", quotedArgs)}"); + Assert.Warn($"Unsupported on this hardware: ffmpeg {string.Join(" ", quotedArgs)}"); + } + else if (error.Contains("Impossible to convert between")) + { + IEnumerable quotedArgs = process.StartInfo.ArgumentList.Map(a => $"\'{a}\'"); + Assert.Fail($"Transcode failure: ffmpeg {string.Join(" ", quotedArgs)}"); + } + else + { + var quotedArgs = process.StartInfo.ArgumentList.Map(a => $"\'{a}\'").ToList(); + process.ExitCode.Should().Be(0, errorBuffer + Environment.NewLine + string.Join(" ", quotedArgs)); + if (process.ExitCode == 0) + { + Console.WriteLine(string.Join(" ", quotedArgs)); + } + } } -} + + private static string GetStringSha256Hash(string text) + { + if (string.IsNullOrEmpty(text)) + { + return string.Empty; + } + + using var sha = SHA256.Create(); + byte[] textData = Encoding.UTF8.GetBytes(text); + byte[] hash = sha.ComputeHash(textData); + return BitConverter.ToString(hash).Replace("-", string.Empty); + } + + private class FakeStreamSelector : IFFmpegStreamSelector + { + public Task SelectVideoStream(Channel channel, MediaVersion version) => + version.Streams.First(s => s.MediaStreamKind == MediaStreamKind.Video).AsTask(); + + public Task> SelectAudioStream(Channel channel, MediaVersion version) => + Optional(version.Streams.First(s => s.MediaStreamKind == MediaStreamKind.Audio)).AsTask(); + } + + private static string ExecutableName(string baseName) => + OperatingSystem.IsWindows() ? $"{baseName}.exe" : baseName; +} \ No newline at end of file diff --git a/ErsatzTV.Core.Tests/FFmpeg/WatermarkCalculatorTests.cs b/ErsatzTV.Core.Tests/FFmpeg/WatermarkCalculatorTests.cs index c5fa2aa47..5190324e0 100644 --- a/ErsatzTV.Core.Tests/FFmpeg/WatermarkCalculatorTests.cs +++ b/ErsatzTV.Core.Tests/FFmpeg/WatermarkCalculatorTests.cs @@ -1,9 +1,6 @@ -using System; -using System.Collections.Generic; -using ErsatzTV.Core.FFmpeg; +using ErsatzTV.Core.FFmpeg; using FluentAssertions; using NUnit.Framework; -using static LanguageExt.Prelude; namespace ErsatzTV.Core.Tests.FFmpeg; diff --git a/ErsatzTV.Core.Tests/Fakes/FakeFileEntry.cs b/ErsatzTV.Core.Tests/Fakes/FakeFileEntry.cs index fa02434ae..480aff0e4 100644 --- a/ErsatzTV.Core.Tests/Fakes/FakeFileEntry.cs +++ b/ErsatzTV.Core.Tests/Fakes/FakeFileEntry.cs @@ -1,9 +1,6 @@ -using System; +namespace ErsatzTV.Core.Tests.Fakes; -namespace ErsatzTV.Core.Tests.Fakes +public record FakeFileEntry(string Path) { - public record FakeFileEntry(string Path) - { - public DateTime LastWriteTime { get; set; } = SystemTime.MinValueUtc; - } -} + public DateTime LastWriteTime { get; set; } = SystemTime.MinValueUtc; +} \ No newline at end of file diff --git a/ErsatzTV.Core.Tests/Fakes/FakeFolderEntry.cs b/ErsatzTV.Core.Tests/Fakes/FakeFolderEntry.cs index 3fbe5da0a..a2258c6c1 100644 --- a/ErsatzTV.Core.Tests/Fakes/FakeFolderEntry.cs +++ b/ErsatzTV.Core.Tests/Fakes/FakeFolderEntry.cs @@ -1,4 +1,3 @@ -namespace ErsatzTV.Core.Tests.Fakes -{ - public record FakeFolderEntry(string Path); -} +namespace ErsatzTV.Core.Tests.Fakes; + +public record FakeFolderEntry(string Path); \ No newline at end of file diff --git a/ErsatzTV.Core.Tests/Fakes/FakeLocalFileSystem.cs b/ErsatzTV.Core.Tests/Fakes/FakeLocalFileSystem.cs index 7a787e70a..7a9561125 100644 --- a/ErsatzTV.Core.Tests/Fakes/FakeLocalFileSystem.cs +++ b/ErsatzTV.Core.Tests/Fakes/FakeLocalFileSystem.cs @@ -1,83 +1,75 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Metadata; -using LanguageExt; -using static LanguageExt.Prelude; -namespace ErsatzTV.Core.Tests.Fakes +namespace ErsatzTV.Core.Tests.Fakes; + +public class FakeLocalFileSystem : ILocalFileSystem { - public class FakeLocalFileSystem : ILocalFileSystem + public static readonly byte[] TestBytes = { 1, 2, 3, 4, 5 }; + + private readonly List _files; + private readonly List _folders; + + public FakeLocalFileSystem(List files) : this(files, new List()) { - public static readonly byte[] TestBytes = { 1, 2, 3, 4, 5 }; + } - private readonly List _files; - private readonly List _folders; + public FakeLocalFileSystem(List files, List folders) + { + _files = files; - public FakeLocalFileSystem(List files) : this(files, new List()) + var allFolders = new List(folders.Map(f => f.Path)); + foreach (FakeFileEntry file in _files) { + List moreFolders = + Split(new DirectoryInfo(Path.GetDirectoryName(file.Path) ?? string.Empty)); + allFolders.AddRange(moreFolders.Map(i => i.FullName)); } - public FakeLocalFileSystem(List files, List folders) + _folders = allFolders.Distinct().Map(f => new FakeFolderEntry(f)).ToList(); + } + + public Unit EnsureFolderExists(string folder) => Unit.Default; + + public DateTime GetLastWriteTime(string path) => + Optional(_files.SingleOrDefault(f => f.Path == path)) + .Map(f => f.LastWriteTime) + .IfNone(SystemTime.MinValueUtc); + + public bool IsLibraryPathAccessible(LibraryPath libraryPath) => + _folders.Any(f => f.Path == libraryPath.Path); + + public IEnumerable ListSubdirectories(string folder) => + _folders.Map(f => f.Path).Filter(f => f.StartsWith(folder) && Directory.GetParent(f)?.FullName == folder); + + public IEnumerable ListFiles(string folder) => + _files.Map(f => f.Path).Filter(f => Path.GetDirectoryName(f) == folder); + + public bool FileExists(string path) => _files.Any(f => f.Path == path); + public bool FolderExists(string folder) => false; + + public Task ReadAllBytes(string path) => TestBytes.AsTask(); + + public Task> CopyFile(string source, string destination) => + Task.FromResult(Right(Unit.Default)); + + public Unit EmptyFolder(string folder) => Unit.Default; + + private static List Split(DirectoryInfo path) + { + var result = new List(); + if (path == null || string.IsNullOrWhiteSpace(path.FullName)) { - _files = files; - - var allFolders = new List(folders.Map(f => f.Path)); - foreach (FakeFileEntry file in _files) - { - List moreFolders = - Split(new DirectoryInfo(Path.GetDirectoryName(file.Path) ?? string.Empty)); - allFolders.AddRange(moreFolders.Map(i => i.FullName)); - } - - _folders = allFolders.Distinct().Map(f => new FakeFolderEntry(f)).ToList(); - } - - public Unit EnsureFolderExists(string folder) => Unit.Default; - - public DateTime GetLastWriteTime(string path) => - Optional(_files.SingleOrDefault(f => f.Path == path)) - .Map(f => f.LastWriteTime) - .IfNone(SystemTime.MinValueUtc); - - public bool IsLibraryPathAccessible(LibraryPath libraryPath) => - _folders.Any(f => f.Path == libraryPath.Path); - - public IEnumerable ListSubdirectories(string folder) => - _folders.Map(f => f.Path).Filter(f => f.StartsWith(folder) && Directory.GetParent(f)?.FullName == folder); - - public IEnumerable ListFiles(string folder) => - _files.Map(f => f.Path).Filter(f => Path.GetDirectoryName(f) == folder); - - public bool FileExists(string path) => _files.Any(f => f.Path == path); - public bool FolderExists(string folder) => false; - - public Task ReadAllBytes(string path) => TestBytes.AsTask(); - - public Task> CopyFile(string source, string destination) => - Task.FromResult(Right(Unit.Default)); - - public Unit EmptyFolder(string folder) => Unit.Default; - - private static List Split(DirectoryInfo path) - { - var result = new List(); - if (path == null || string.IsNullOrWhiteSpace(path.FullName)) - { - return result; - } - - if (path.Parent != null) - { - result.AddRange(Split(path.Parent)); - } - - result.Add(path); - return result; } + + if (path.Parent != null) + { + result.AddRange(Split(path.Parent)); + } + + result.Add(path); + + return result; } -} +} \ No newline at end of file diff --git a/ErsatzTV.Core.Tests/Fakes/FakeMediaCollectionRepository.cs b/ErsatzTV.Core.Tests/Fakes/FakeMediaCollectionRepository.cs index 697075492..6c03cd6e7 100644 --- a/ErsatzTV.Core.Tests/Fakes/FakeMediaCollectionRepository.cs +++ b/ErsatzTV.Core.Tests/Fakes/FakeMediaCollectionRepository.cs @@ -1,42 +1,36 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Scheduling; -using LanguageExt; -namespace ErsatzTV.Core.Tests.Fakes +namespace ErsatzTV.Core.Tests.Fakes; + +public class FakeMediaCollectionRepository : IMediaCollectionRepository { - public class FakeMediaCollectionRepository : IMediaCollectionRepository - { - private readonly Map> _data; + private readonly Map> _data; - public FakeMediaCollectionRepository(Map> data) => _data = data; + public FakeMediaCollectionRepository(Map> data) => _data = data; - public Task> GetCollectionWithCollectionItemsUntracked(int id) => - throw new NotSupportedException(); + public Task> GetCollectionWithCollectionItemsUntracked(int id) => + throw new NotSupportedException(); - public Task> GetItems(int id) => _data[id].ToList().AsTask(); - public Task> GetMultiCollectionItems(int id) => throw new NotSupportedException(); - public Task> GetSmartCollectionItems(int id) => throw new NotSupportedException(); + public Task> GetItems(int id) => _data[id].ToList().AsTask(); + public Task> GetMultiCollectionItems(int id) => throw new NotSupportedException(); + public Task> GetSmartCollectionItems(int id) => throw new NotSupportedException(); - public Task> GetMultiCollectionCollections(int id) => - throw new NotSupportedException(); + public Task> GetMultiCollectionCollections(int id) => + throw new NotSupportedException(); - public Task> GetFakeMultiCollectionCollections(int? collectionId, int? smartCollectionId) => - throw new NotSupportedException(); + public Task> GetFakeMultiCollectionCollections(int? collectionId, int? smartCollectionId) => + throw new NotSupportedException(); - public Task> PlayoutIdsUsingCollection(int collectionId) => throw new NotSupportedException(); + public Task> PlayoutIdsUsingCollection(int collectionId) => throw new NotSupportedException(); - public Task> PlayoutIdsUsingMultiCollection(int multiCollectionId) => - throw new NotSupportedException(); + public Task> PlayoutIdsUsingMultiCollection(int multiCollectionId) => + throw new NotSupportedException(); - public Task> PlayoutIdsUsingSmartCollection(int smartCollectionId) => - throw new NotSupportedException(); + public Task> PlayoutIdsUsingSmartCollection(int smartCollectionId) => + throw new NotSupportedException(); - public Task IsCustomPlaybackOrder(int collectionId) => false.AsTask(); - public Task> GetNameFromKey(CollectionKey emptyCollection) => Option.None.AsTask(); - } -} + public Task IsCustomPlaybackOrder(int collectionId) => false.AsTask(); + public Task> GetNameFromKey(CollectionKey emptyCollection) => Option.None.AsTask(); +} \ No newline at end of file diff --git a/ErsatzTV.Core.Tests/Fakes/FakeMovieWithPath.cs b/ErsatzTV.Core.Tests/Fakes/FakeMovieWithPath.cs index b4eb8e941..5a028745b 100644 --- a/ErsatzTV.Core.Tests/Fakes/FakeMovieWithPath.cs +++ b/ErsatzTV.Core.Tests/Fakes/FakeMovieWithPath.cs @@ -1,26 +1,24 @@ -using System.Collections.Generic; -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; using ErsatzTV.Core.Metadata; -namespace ErsatzTV.Core.Tests.Fakes +namespace ErsatzTV.Core.Tests.Fakes; + +public class FakeMovieWithPath : MediaItemScanResult { - public class FakeMovieWithPath : MediaItemScanResult - { - public FakeMovieWithPath(string path) - : base( - new Movie + public FakeMovieWithPath(string path) + : base( + new Movie + { + MediaVersions = new List { - MediaVersions = new List + new() { - new() + MediaFiles = new List { - MediaFiles = new List - { - new() { Path = path } - } + new() { Path = path } } } - }) => - IsAdded = true; - } -} + } + }) => + IsAdded = true; +} \ No newline at end of file diff --git a/ErsatzTV.Core.Tests/Fakes/FakeTelevisionRepository.cs b/ErsatzTV.Core.Tests/Fakes/FakeTelevisionRepository.cs index 615d22278..5b4601f3c 100644 --- a/ErsatzTV.Core.Tests/Fakes/FakeTelevisionRepository.cs +++ b/ErsatzTV.Core.Tests/Fakes/FakeTelevisionRepository.cs @@ -1,102 +1,97 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Metadata; -using LanguageExt; -namespace ErsatzTV.Core.Tests.Fakes +namespace ErsatzTV.Core.Tests.Fakes; + +public class FakeTelevisionRepository : ITelevisionRepository { - public class FakeTelevisionRepository : ITelevisionRepository - { - public Task AllShowsExist(List showIds) => throw new NotSupportedException(); - public Task AllSeasonsExist(List seasonIds) => throw new NotSupportedException(); + public Task AllShowsExist(List showIds) => throw new NotSupportedException(); + public Task AllSeasonsExist(List seasonIds) => throw new NotSupportedException(); - public Task AllEpisodesExist(List episodeIds) => throw new NotSupportedException(); + public Task AllEpisodesExist(List episodeIds) => throw new NotSupportedException(); - public Task> GetAllShows() => throw new NotSupportedException(); + public Task> GetAllShows() => throw new NotSupportedException(); - public Task> GetShow(int showId) => throw new NotSupportedException(); + public Task> GetShow(int showId) => throw new NotSupportedException(); - public Task> GetShowsForCards(List ids) => throw new NotSupportedException(); - public Task> GetSeasonsForCards(List ids) => throw new NotSupportedException(); + public Task> GetShowsForCards(List ids) => throw new NotSupportedException(); + public Task> GetSeasonsForCards(List ids) => throw new NotSupportedException(); - public Task> GetEpisodesForCards(List ids) => throw new NotSupportedException(); + public Task> GetEpisodesForCards(List ids) => throw new NotSupportedException(); - public Task> GetShowItems(int showId) => throw new NotSupportedException(); + public Task> GetShowItems(int showId) => throw new NotSupportedException(); - public Task> GetAllSeasons() => throw new NotSupportedException(); + public Task> GetAllSeasons() => throw new NotSupportedException(); - public Task> GetSeason(int seasonId) => throw new NotSupportedException(); + public Task> GetSeason(int seasonId) => throw new NotSupportedException(); - public Task GetSeasonCount(int showId) => throw new NotSupportedException(); + public Task GetSeasonCount(int showId) => throw new NotSupportedException(); - public Task> GetPagedSeasons(int televisionShowId, int pageNumber, int pageSize) => - throw new NotSupportedException(); + public Task> GetPagedSeasons(int televisionShowId, int pageNumber, int pageSize) => + throw new NotSupportedException(); - public Task> GetSeasonItems(int seasonId) => throw new NotSupportedException(); + public Task> GetSeasonItems(int seasonId) => throw new NotSupportedException(); - public Task GetEpisodeCount(int seasonId) => throw new NotSupportedException(); + public Task GetEpisodeCount(int seasonId) => throw new NotSupportedException(); - public Task> GetPagedEpisodes(int seasonId, int pageNumber, int pageSize) => - throw new NotSupportedException(); + public Task> GetPagedEpisodes(int seasonId, int pageNumber, int pageSize) => + throw new NotSupportedException(); - public Task> GetShowByMetadata(int libraryPathId, ShowMetadata metadata) => - throw new NotSupportedException(); + public Task> GetShowByMetadata(int libraryPathId, ShowMetadata metadata) => + throw new NotSupportedException(); - public Task>> - AddShow(int libraryPathId, string showFolder, ShowMetadata metadata) => - throw new NotSupportedException(); + public Task>> + AddShow(int libraryPathId, string showFolder, ShowMetadata metadata) => + throw new NotSupportedException(); - public Task> GetOrAddSeason(Show show, int libraryPathId, int seasonNumber) => - throw new NotSupportedException(); + public Task> GetOrAddSeason(Show show, int libraryPathId, int seasonNumber) => + throw new NotSupportedException(); - public Task> GetOrAddEpisode(Season season, LibraryPath libraryPath, string path) => - throw new NotSupportedException(); + public Task> GetOrAddEpisode(Season season, LibraryPath libraryPath, string path) => + throw new NotSupportedException(); - public Task> FindEpisodePaths(LibraryPath libraryPath) => throw new NotSupportedException(); + public Task> FindEpisodePaths(LibraryPath libraryPath) => throw new NotSupportedException(); - public Task DeleteByPath(LibraryPath libraryPath, string path) => throw new NotSupportedException(); + public Task DeleteByPath(LibraryPath libraryPath, string path) => throw new NotSupportedException(); - public Task DeleteEmptySeasons(LibraryPath libraryPath) => throw new NotSupportedException(); + public Task DeleteEmptySeasons(LibraryPath libraryPath) => throw new NotSupportedException(); - public Task> DeleteEmptyShows(LibraryPath libraryPath) => throw new NotSupportedException(); + public Task> DeleteEmptyShows(LibraryPath libraryPath) => throw new NotSupportedException(); - public Task>> GetOrAddPlexShow( - PlexLibrary library, - PlexShow item) => - throw new NotSupportedException(); + public Task>> GetOrAddPlexShow( + PlexLibrary library, + PlexShow item) => + throw new NotSupportedException(); - public Task> GetOrAddPlexSeason(PlexLibrary library, PlexSeason item) => - throw new NotSupportedException(); + public Task> GetOrAddPlexSeason(PlexLibrary library, PlexSeason item) => + throw new NotSupportedException(); - public Task> GetOrAddPlexEpisode(PlexLibrary library, PlexEpisode item) => - throw new NotSupportedException(); + public Task> GetOrAddPlexEpisode(PlexLibrary library, PlexEpisode item) => + throw new NotSupportedException(); - public Task AddGenre(ShowMetadata metadata, Genre genre) => throw new NotSupportedException(); - public Task AddTag(ShowMetadata metadata, Tag tag) => throw new NotSupportedException(); + public Task AddGenre(ShowMetadata metadata, Genre genre) => throw new NotSupportedException(); + public Task AddTag(ShowMetadata metadata, Tag tag) => throw new NotSupportedException(); - public Task AddStudio(ShowMetadata metadata, Studio studio) => throw new NotSupportedException(); - public Task AddActor(ShowMetadata metadata, Actor actor) => throw new NotSupportedException(); + public Task AddStudio(ShowMetadata metadata, Studio studio) => throw new NotSupportedException(); + public Task AddActor(ShowMetadata metadata, Actor actor) => throw new NotSupportedException(); - public Task AddActor(EpisodeMetadata metadata, Actor actor) => throw new NotSupportedException(); + public Task AddActor(EpisodeMetadata metadata, Actor actor) => throw new NotSupportedException(); - public Task> RemoveMissingPlexShows(PlexLibrary library, List showKeys) => - throw new NotSupportedException(); + public Task> RemoveMissingPlexShows(PlexLibrary library, List showKeys) => + throw new NotSupportedException(); - public Task RemoveMissingPlexSeasons(string showKey, List seasonKeys) => - throw new NotSupportedException(); + public Task RemoveMissingPlexSeasons(string showKey, List seasonKeys) => + throw new NotSupportedException(); - public Task> RemoveMissingPlexEpisodes(string seasonKey, List episodeKeys) => - throw new NotSupportedException(); + public Task> RemoveMissingPlexEpisodes(string seasonKey, List episodeKeys) => + throw new NotSupportedException(); - public Task RemoveMetadata(Episode episode, EpisodeMetadata metadata) => - throw new NotSupportedException(); + public Task RemoveMetadata(Episode episode, EpisodeMetadata metadata) => + throw new NotSupportedException(); - public Task AddDirector(EpisodeMetadata metadata, Director director) => throw new NotSupportedException(); + public Task AddDirector(EpisodeMetadata metadata, Director director) => throw new NotSupportedException(); - public Task AddWriter(EpisodeMetadata metadata, Writer writer) => throw new NotSupportedException(); - public Task UpdatePath(int mediaFileId, string path) => throw new NotSupportedException(); - } -} + public Task AddWriter(EpisodeMetadata metadata, Writer writer) => throw new NotSupportedException(); + public Task UpdatePath(int mediaFileId, string path) => throw new NotSupportedException(); +} \ No newline at end of file diff --git a/ErsatzTV.Core.Tests/GlobalUsings.cs b/ErsatzTV.Core.Tests/GlobalUsings.cs new file mode 100644 index 000000000..9d0867420 --- /dev/null +++ b/ErsatzTV.Core.Tests/GlobalUsings.cs @@ -0,0 +1,4 @@ +global using LanguageExt; +global using static LanguageExt.Prelude; +global using Unit = LanguageExt.Unit; +global using Array = System.Array; \ No newline at end of file diff --git a/ErsatzTV.Core.Tests/Jellyfin/JellyfinPathReplacementServiceTests.cs b/ErsatzTV.Core.Tests/Jellyfin/JellyfinPathReplacementServiceTests.cs index 067c3ac42..3e61e914e 100644 --- a/ErsatzTV.Core.Tests/Jellyfin/JellyfinPathReplacementServiceTests.cs +++ b/ErsatzTV.Core.Tests/Jellyfin/JellyfinPathReplacementServiceTests.cs @@ -1,211 +1,207 @@ -using System.Collections.Generic; -using System.Runtime.InteropServices; -using System.Threading.Tasks; +using System.Runtime.InteropServices; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Runtime; using ErsatzTV.Core.Jellyfin; using FluentAssertions; -using LanguageExt; using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; -namespace ErsatzTV.Core.Tests.Jellyfin +namespace ErsatzTV.Core.Tests.Jellyfin; + +[TestFixture] +public class JellyfinPathReplacementServiceTests { - [TestFixture] - public class JellyfinPathReplacementServiceTests + [Test] + public async Task JellyfinWindows_To_EtvWindows() { - [Test] - public async Task JellyfinWindows_To_EtvWindows() + var replacements = new List { - var replacements = new List + new() { - new() - { - Id = 1, - JellyfinPath = @"C:\Something\Some Shared Folder", - LocalPath = @"C:\Something Else\Some Shared Folder", - JellyfinMediaSource = new JellyfinMediaSource { OperatingSystem = "Windows" } - } - }; + Id = 1, + JellyfinPath = @"C:\Something\Some Shared Folder", + LocalPath = @"C:\Something Else\Some Shared Folder", + JellyfinMediaSource = new JellyfinMediaSource { OperatingSystem = "Windows" } + } + }; - var repo = new Mock(); - repo.Setup(x => x.GetJellyfinPathReplacementsByLibraryId(It.IsAny())).Returns(replacements.AsTask()); + var repo = new Mock(); + repo.Setup(x => x.GetJellyfinPathReplacementsByLibraryId(It.IsAny())).Returns(replacements.AsTask()); - var runtime = new Mock(); - runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(true); + var runtime = new Mock(); + runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(true); - var service = new JellyfinPathReplacementService( - repo.Object, - runtime.Object, - new Mock>().Object); + var service = new JellyfinPathReplacementService( + repo.Object, + runtime.Object, + new Mock>().Object); - string result = await service.GetReplacementJellyfinPath( - 0, - @"C:\Something\Some Shared Folder\Some Movie\Some Movie.mkv"); + string result = await service.GetReplacementJellyfinPath( + 0, + @"C:\Something\Some Shared Folder\Some Movie\Some Movie.mkv"); - result.Should().Be(@"C:\Something Else\Some Shared Folder\Some Movie\Some Movie.mkv"); - } - - [Test] - public async Task JellyfinWindows_To_EtvLinux() - { - var replacements = new List - { - new() - { - Id = 1, - JellyfinPath = @"C:\Something\Some Shared Folder", - LocalPath = @"/mnt/something else/Some Shared Folder", - JellyfinMediaSource = new JellyfinMediaSource { OperatingSystem = "Windows" } - } - }; - - var repo = new Mock(); - repo.Setup(x => x.GetJellyfinPathReplacementsByLibraryId(It.IsAny())).Returns(replacements.AsTask()); - - var runtime = new Mock(); - runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(false); - - var service = new JellyfinPathReplacementService( - repo.Object, - runtime.Object, - new Mock>().Object); - - string result = await service.GetReplacementJellyfinPath( - 0, - @"C:\Something\Some Shared Folder\Some Movie\Some Movie.mkv"); - - result.Should().Be(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv"); - } - - [Test] - public async Task JellyfinWindows_To_EtvLinux_UncPath() - { - var replacements = new List - { - new() - { - Id = 1, - JellyfinPath = @"\\192.168.1.100\Something\Some Shared Folder", - LocalPath = @"/mnt/something else/Some Shared Folder", - JellyfinMediaSource = new JellyfinMediaSource { OperatingSystem = "Windows" } - } - }; - - var repo = new Mock(); - repo.Setup(x => x.GetJellyfinPathReplacementsByLibraryId(It.IsAny())).Returns(replacements.AsTask()); - - var runtime = new Mock(); - runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(false); - - var service = new JellyfinPathReplacementService( - repo.Object, - runtime.Object, - new Mock>().Object); - - string result = await service.GetReplacementJellyfinPath( - 0, - @"\\192.168.1.100\Something\Some Shared Folder\Some Movie\Some Movie.mkv"); - - result.Should().Be(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv"); - } - - [Test] - public async Task JellyfinWindows_To_EtvLinux_UncPathWithTrailingSlash() - { - var replacements = new List - { - new() - { - Id = 1, - JellyfinPath = @"\\192.168.1.100\Something\Some Shared Folder\", - LocalPath = @"/mnt/something else/Some Shared Folder/", - JellyfinMediaSource = new JellyfinMediaSource { OperatingSystem = "Windows" } - } - }; - - var repo = new Mock(); - repo.Setup(x => x.GetJellyfinPathReplacementsByLibraryId(It.IsAny())).Returns(replacements.AsTask()); - - var runtime = new Mock(); - runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(false); - - var service = new JellyfinPathReplacementService( - repo.Object, - runtime.Object, - new Mock>().Object); - - string result = await service.GetReplacementJellyfinPath( - 0, - @"\\192.168.1.100\Something\Some Shared Folder\Some Movie\Some Movie.mkv"); - - result.Should().Be(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv"); - } - - [Test] - public async Task JellyfinLinux_To_EtvWindows() - { - var replacements = new List - { - new() - { - Id = 1, - JellyfinPath = @"/mnt/something/Some Shared Folder", - LocalPath = @"C:\Something Else\Some Shared Folder", - JellyfinMediaSource = new JellyfinMediaSource { OperatingSystem = "Linux" } - } - }; - - var repo = new Mock(); - repo.Setup(x => x.GetJellyfinPathReplacementsByLibraryId(It.IsAny())).Returns(replacements.AsTask()); - - var runtime = new Mock(); - runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(true); - - var service = new JellyfinPathReplacementService( - repo.Object, - runtime.Object, - new Mock>().Object); - - string result = await service.GetReplacementJellyfinPath( - 0, - @"/mnt/something/Some Shared Folder/Some Movie/Some Movie.mkv"); - - result.Should().Be(@"C:\Something Else\Some Shared Folder\Some Movie\Some Movie.mkv"); - } - - [Test] - public async Task JellyfinLinux_To_EtvLinux() - { - var replacements = new List - { - new() - { - Id = 1, - JellyfinPath = @"/mnt/something/Some Shared Folder", - LocalPath = @"/mnt/something else/Some Shared Folder", - JellyfinMediaSource = new JellyfinMediaSource { OperatingSystem = "Linux" } - } - }; - - var repo = new Mock(); - repo.Setup(x => x.GetJellyfinPathReplacementsByLibraryId(It.IsAny())).Returns(replacements.AsTask()); - - var runtime = new Mock(); - runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(false); - - var service = new JellyfinPathReplacementService( - repo.Object, - runtime.Object, - new Mock>().Object); - - string result = await service.GetReplacementJellyfinPath( - 0, - @"/mnt/something/Some Shared Folder/Some Movie/Some Movie.mkv"); - - result.Should().Be(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv"); - } + result.Should().Be(@"C:\Something Else\Some Shared Folder\Some Movie\Some Movie.mkv"); } -} + + [Test] + public async Task JellyfinWindows_To_EtvLinux() + { + var replacements = new List + { + new() + { + Id = 1, + JellyfinPath = @"C:\Something\Some Shared Folder", + LocalPath = @"/mnt/something else/Some Shared Folder", + JellyfinMediaSource = new JellyfinMediaSource { OperatingSystem = "Windows" } + } + }; + + var repo = new Mock(); + repo.Setup(x => x.GetJellyfinPathReplacementsByLibraryId(It.IsAny())).Returns(replacements.AsTask()); + + var runtime = new Mock(); + runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(false); + + var service = new JellyfinPathReplacementService( + repo.Object, + runtime.Object, + new Mock>().Object); + + string result = await service.GetReplacementJellyfinPath( + 0, + @"C:\Something\Some Shared Folder\Some Movie\Some Movie.mkv"); + + result.Should().Be(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv"); + } + + [Test] + public async Task JellyfinWindows_To_EtvLinux_UncPath() + { + var replacements = new List + { + new() + { + Id = 1, + JellyfinPath = @"\\192.168.1.100\Something\Some Shared Folder", + LocalPath = @"/mnt/something else/Some Shared Folder", + JellyfinMediaSource = new JellyfinMediaSource { OperatingSystem = "Windows" } + } + }; + + var repo = new Mock(); + repo.Setup(x => x.GetJellyfinPathReplacementsByLibraryId(It.IsAny())).Returns(replacements.AsTask()); + + var runtime = new Mock(); + runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(false); + + var service = new JellyfinPathReplacementService( + repo.Object, + runtime.Object, + new Mock>().Object); + + string result = await service.GetReplacementJellyfinPath( + 0, + @"\\192.168.1.100\Something\Some Shared Folder\Some Movie\Some Movie.mkv"); + + result.Should().Be(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv"); + } + + [Test] + public async Task JellyfinWindows_To_EtvLinux_UncPathWithTrailingSlash() + { + var replacements = new List + { + new() + { + Id = 1, + JellyfinPath = @"\\192.168.1.100\Something\Some Shared Folder\", + LocalPath = @"/mnt/something else/Some Shared Folder/", + JellyfinMediaSource = new JellyfinMediaSource { OperatingSystem = "Windows" } + } + }; + + var repo = new Mock(); + repo.Setup(x => x.GetJellyfinPathReplacementsByLibraryId(It.IsAny())).Returns(replacements.AsTask()); + + var runtime = new Mock(); + runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(false); + + var service = new JellyfinPathReplacementService( + repo.Object, + runtime.Object, + new Mock>().Object); + + string result = await service.GetReplacementJellyfinPath( + 0, + @"\\192.168.1.100\Something\Some Shared Folder\Some Movie\Some Movie.mkv"); + + result.Should().Be(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv"); + } + + [Test] + public async Task JellyfinLinux_To_EtvWindows() + { + var replacements = new List + { + new() + { + Id = 1, + JellyfinPath = @"/mnt/something/Some Shared Folder", + LocalPath = @"C:\Something Else\Some Shared Folder", + JellyfinMediaSource = new JellyfinMediaSource { OperatingSystem = "Linux" } + } + }; + + var repo = new Mock(); + repo.Setup(x => x.GetJellyfinPathReplacementsByLibraryId(It.IsAny())).Returns(replacements.AsTask()); + + var runtime = new Mock(); + runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(true); + + var service = new JellyfinPathReplacementService( + repo.Object, + runtime.Object, + new Mock>().Object); + + string result = await service.GetReplacementJellyfinPath( + 0, + @"/mnt/something/Some Shared Folder/Some Movie/Some Movie.mkv"); + + result.Should().Be(@"C:\Something Else\Some Shared Folder\Some Movie\Some Movie.mkv"); + } + + [Test] + public async Task JellyfinLinux_To_EtvLinux() + { + var replacements = new List + { + new() + { + Id = 1, + JellyfinPath = @"/mnt/something/Some Shared Folder", + LocalPath = @"/mnt/something else/Some Shared Folder", + JellyfinMediaSource = new JellyfinMediaSource { OperatingSystem = "Linux" } + } + }; + + var repo = new Mock(); + repo.Setup(x => x.GetJellyfinPathReplacementsByLibraryId(It.IsAny())).Returns(replacements.AsTask()); + + var runtime = new Mock(); + runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(false); + + var service = new JellyfinPathReplacementService( + repo.Object, + runtime.Object, + new Mock>().Object); + + string result = await service.GetReplacementJellyfinPath( + 0, + @"/mnt/something/Some Shared Folder/Some Movie/Some Movie.mkv"); + + result.Should().Be(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv"); + } +} \ No newline at end of file diff --git a/ErsatzTV.Core.Tests/Jellyfin/JellyfinUrlTests.cs b/ErsatzTV.Core.Tests/Jellyfin/JellyfinUrlTests.cs index 21c3b672b..0ab7047bb 100644 --- a/ErsatzTV.Core.Tests/Jellyfin/JellyfinUrlTests.cs +++ b/ErsatzTV.Core.Tests/Jellyfin/JellyfinUrlTests.cs @@ -1,121 +1,118 @@ -using System.Collections.Generic; -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; using ErsatzTV.Core.Jellyfin; using FluentAssertions; using Flurl; using NUnit.Framework; -using static LanguageExt.Prelude; -namespace ErsatzTV.Core.Tests.Jellyfin +namespace ErsatzTV.Core.Tests.Jellyfin; + +public class JellyfinUrlTests { - public class JellyfinUrlTests + [Test] + public void Should_Work_Without_Trailing_Slash() { - [Test] - public void Should_Work_Without_Trailing_Slash() + var artwork = "jellyfin://Items/2/Images/3?tag=4"; + var address = "https://some.jellyfin.server"; + var mediaSource = new JellyfinMediaSource { - var artwork = "jellyfin://Items/2/Images/3?tag=4"; - var address = "https://some.jellyfin.server"; - var mediaSource = new JellyfinMediaSource + Connections = new List { - Connections = new List - { - new() { Address = address } - } - }; + new() { Address = address } + } + }; - Url url = JellyfinUrl.ForArtwork(Some(mediaSource), artwork); + Url url = JellyfinUrl.ForArtwork(Some(mediaSource), artwork); - url.ToString().Should().Be("https://some.jellyfin.server/Items/2/Images/3?tag=4"); - } - - [Test] - public void Should_Work_With_Trailing_Slash() - { - var artwork = "jellyfin://Items/2/Images/3?tag=4"; - var address = "https://some.jellyfin.server/"; - var mediaSource = new JellyfinMediaSource - { - Connections = new List - { - new() { Address = address } - } - }; - - Url url = JellyfinUrl.ForArtwork(Some(mediaSource), artwork); - - url.ToString().Should().Be("https://some.jellyfin.server/Items/2/Images/3?tag=4"); - } - - [Test] - public void Should_Work_With_Port_Without_Trailing_Slash() - { - var artwork = "jellyfin://Items/2/Images/3?tag=4"; - var address = "https://some.jellyfin.server:1000"; - var mediaSource = new JellyfinMediaSource - { - Connections = new List - { - new() { Address = address } - } - }; - - Url url = JellyfinUrl.ForArtwork(Some(mediaSource), artwork); - - url.ToString().Should().Be("https://some.jellyfin.server:1000/Items/2/Images/3?tag=4"); - } - - [Test] - public void Should_Work_With_Port_With_Trailing_Slash() - { - var artwork = "jellyfin://Items/2/Images/3?tag=4"; - var address = "https://some.jellyfin.server:1000/"; - var mediaSource = new JellyfinMediaSource - { - Connections = new List - { - new() { Address = address } - } - }; - - Url url = JellyfinUrl.ForArtwork(Some(mediaSource), artwork); - - url.ToString().Should().Be("https://some.jellyfin.server:1000/Items/2/Images/3?tag=4"); - } - - [Test] - public void Should_Work_With_Path_Prefix_Without_Trailing_Slash() - { - var artwork = "jellyfin://Items/2/Images/3?tag=4"; - var address = "https://some.jellyfin.server/jellyfin"; - var mediaSource = new JellyfinMediaSource - { - Connections = new List - { - new() { Address = address } - } - }; - - Url url = JellyfinUrl.ForArtwork(Some(mediaSource), artwork); - - url.ToString().Should().Be("https://some.jellyfin.server/jellyfin/Items/2/Images/3?tag=4"); - } - - [Test] - public void Should_Work_With_Path_Prefix_With_Trailing_Slash() - { - var artwork = "jellyfin://Items/2/Images/3?tag=4"; - var address = "https://some.jellyfin.server/jellyfin/"; - var mediaSource = new JellyfinMediaSource - { - Connections = new List - { - new() { Address = address } - } - }; - - Url url = JellyfinUrl.ForArtwork(Some(mediaSource), artwork); - - url.ToString().Should().Be("https://some.jellyfin.server/jellyfin/Items/2/Images/3?tag=4"); - } + url.ToString().Should().Be("https://some.jellyfin.server/Items/2/Images/3?tag=4"); } -} + + [Test] + public void Should_Work_With_Trailing_Slash() + { + var artwork = "jellyfin://Items/2/Images/3?tag=4"; + var address = "https://some.jellyfin.server/"; + var mediaSource = new JellyfinMediaSource + { + Connections = new List + { + new() { Address = address } + } + }; + + Url url = JellyfinUrl.ForArtwork(Some(mediaSource), artwork); + + url.ToString().Should().Be("https://some.jellyfin.server/Items/2/Images/3?tag=4"); + } + + [Test] + public void Should_Work_With_Port_Without_Trailing_Slash() + { + var artwork = "jellyfin://Items/2/Images/3?tag=4"; + var address = "https://some.jellyfin.server:1000"; + var mediaSource = new JellyfinMediaSource + { + Connections = new List + { + new() { Address = address } + } + }; + + Url url = JellyfinUrl.ForArtwork(Some(mediaSource), artwork); + + url.ToString().Should().Be("https://some.jellyfin.server:1000/Items/2/Images/3?tag=4"); + } + + [Test] + public void Should_Work_With_Port_With_Trailing_Slash() + { + var artwork = "jellyfin://Items/2/Images/3?tag=4"; + var address = "https://some.jellyfin.server:1000/"; + var mediaSource = new JellyfinMediaSource + { + Connections = new List + { + new() { Address = address } + } + }; + + Url url = JellyfinUrl.ForArtwork(Some(mediaSource), artwork); + + url.ToString().Should().Be("https://some.jellyfin.server:1000/Items/2/Images/3?tag=4"); + } + + [Test] + public void Should_Work_With_Path_Prefix_Without_Trailing_Slash() + { + var artwork = "jellyfin://Items/2/Images/3?tag=4"; + var address = "https://some.jellyfin.server/jellyfin"; + var mediaSource = new JellyfinMediaSource + { + Connections = new List + { + new() { Address = address } + } + }; + + Url url = JellyfinUrl.ForArtwork(Some(mediaSource), artwork); + + url.ToString().Should().Be("https://some.jellyfin.server/jellyfin/Items/2/Images/3?tag=4"); + } + + [Test] + public void Should_Work_With_Path_Prefix_With_Trailing_Slash() + { + var artwork = "jellyfin://Items/2/Images/3?tag=4"; + var address = "https://some.jellyfin.server/jellyfin/"; + var mediaSource = new JellyfinMediaSource + { + Connections = new List + { + new() { Address = address } + } + }; + + Url url = JellyfinUrl.ForArtwork(Some(mediaSource), artwork); + + url.ToString().Should().Be("https://some.jellyfin.server/jellyfin/Items/2/Images/3?tag=4"); + } +} \ No newline at end of file diff --git a/ErsatzTV.Core.Tests/Metadata/FallbackMetadataProviderTests.cs b/ErsatzTV.Core.Tests/Metadata/FallbackMetadataProviderTests.cs index ae4a0f621..ffbe73de7 100644 --- a/ErsatzTV.Core.Tests/Metadata/FallbackMetadataProviderTests.cs +++ b/ErsatzTV.Core.Tests/Metadata/FallbackMetadataProviderTests.cs @@ -1,122 +1,120 @@ -using System.Collections.Generic; -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; using ErsatzTV.Core.Metadata; using FluentAssertions; using NUnit.Framework; -namespace ErsatzTV.Core.Tests.Metadata +namespace ErsatzTV.Core.Tests.Metadata; + +[TestFixture] +public class FallbackMetadataProviderTests { - [TestFixture] - public class FallbackMetadataProviderTests + [SetUp] + public void SetUp() => _fallbackMetadataProvider = new FallbackMetadataProvider(); + + private FallbackMetadataProvider _fallbackMetadataProvider; + + [Test] + [TestCase("Awesome Show - s01e02.mkv", 1, 2)] + [TestCase("Awesome Show - S01E02.mkv", 1, 2)] + [TestCase("Awesome Show - s1e2.mkv", 1, 2)] + [TestCase("Awesome Show - S1E2.mkv", 1, 2)] + [TestCase("Awesome Show - s01e02 - Episode Title.mkv", 1, 2)] + [TestCase("Awesome Show - S01E02 - Episode Title.mkv", 1, 2)] + [TestCase("Awesome Show - s1e2 - Episode Title.mkv", 1, 2)] + [TestCase("Awesome Show - S1E2 - Episode Title.mkv", 1, 2)] + [TestCase("Awesome Show (2021) - s01e02 - Episode Title.mkv", 1, 2)] + [TestCase("Awesome Show (2021) - S01E02 - Episode Title.mkv", 1, 2)] + [TestCase("Awesome Show (2021) - s1e2 - Episode Title.mkv", 1, 2)] + [TestCase("Awesome Show (2021) - S1E2 - Episode Title.mkv", 1, 2)] + [TestCase("Awesome Show - s01e02 - Episode Title-720p.mkv", 1, 2)] + [TestCase("Awesome Show - S01E02 - Episode Title-720p.mkv", 1, 2)] + [TestCase("Awesome Show - s1e2 - Episode Title-720p.mkv", 1, 2)] + [TestCase("Awesome Show - S1E2 - Episode Title-720p.mkv", 1, 2)] + [TestCase( + "Awesome Show (2021) - S01E02 - Description; More Description (1080p QUALITY codec GROUP).mkv", + 1, + 2)] + [TestCase( + "Awesome.Show.S01E02.Description.more.Description.QUAlity.codec.CODEC-GROUP.mkv", + 1, + 2)] + public void GetFallbackMetadata_ShouldHandleVariousFormats(string path, int season, int episode) { - [SetUp] - public void SetUp() => _fallbackMetadataProvider = new FallbackMetadataProvider(); - - private FallbackMetadataProvider _fallbackMetadataProvider; - - [Test] - [TestCase("Awesome Show - s01e02.mkv", 1, 2)] - [TestCase("Awesome Show - S01E02.mkv", 1, 2)] - [TestCase("Awesome Show - s1e2.mkv", 1, 2)] - [TestCase("Awesome Show - S1E2.mkv", 1, 2)] - [TestCase("Awesome Show - s01e02 - Episode Title.mkv", 1, 2)] - [TestCase("Awesome Show - S01E02 - Episode Title.mkv", 1, 2)] - [TestCase("Awesome Show - s1e2 - Episode Title.mkv", 1, 2)] - [TestCase("Awesome Show - S1E2 - Episode Title.mkv", 1, 2)] - [TestCase("Awesome Show (2021) - s01e02 - Episode Title.mkv", 1, 2)] - [TestCase("Awesome Show (2021) - S01E02 - Episode Title.mkv", 1, 2)] - [TestCase("Awesome Show (2021) - s1e2 - Episode Title.mkv", 1, 2)] - [TestCase("Awesome Show (2021) - S1E2 - Episode Title.mkv", 1, 2)] - [TestCase("Awesome Show - s01e02 - Episode Title-720p.mkv", 1, 2)] - [TestCase("Awesome Show - S01E02 - Episode Title-720p.mkv", 1, 2)] - [TestCase("Awesome Show - s1e2 - Episode Title-720p.mkv", 1, 2)] - [TestCase("Awesome Show - S1E2 - Episode Title-720p.mkv", 1, 2)] - [TestCase( - "Awesome Show (2021) - S01E02 - Description; More Description (1080p QUALITY codec GROUP).mkv", - 1, - 2)] - [TestCase( - "Awesome.Show.S01E02.Description.more.Description.QUAlity.codec.CODEC-GROUP.mkv", - 1, - 2)] - public void GetFallbackMetadata_ShouldHandleVariousFormats(string path, int season, int episode) - { - List metadata = _fallbackMetadataProvider.GetFallbackMetadata( - new Episode + List metadata = _fallbackMetadataProvider.GetFallbackMetadata( + new Episode + { + LibraryPath = new LibraryPath(), + MediaVersions = new List { - LibraryPath = new LibraryPath(), - MediaVersions = new List + new() { - new() + MediaFiles = new List { - MediaFiles = new List - { - new() { Path = path } - } + new() { Path = path } } } - }); + } + }); - metadata.Count.Should().Be(1); - // TODO: how can we test season number? do we need to? - // metadata.Season.Should().Be(season); - metadata.Head().EpisodeNumber.Should().Be(episode); - } - - [Test] - [TestCase("Awesome Show - s01e02-s01e03.mkv", 1, 2, 3)] - [TestCase("Awesome Show - s01e02-whatever-s01e03-whatever2.mkv", 1, 2, 3)] - [TestCase("Awesome Show - s01e02e03.mkv", 1, 2, 3)] - [TestCase("Awesome Show - s01e02-03.mkv", 1, 2, 3)] - public void GetFallbackMetadata_Should_Handle_Two_Episode_Formats( - string path, - int season, - int episode1, - int episode2) - { - List metadata = _fallbackMetadataProvider.GetFallbackMetadata( - new Episode - { - LibraryPath = new LibraryPath(), - MediaVersions = new List - { - new() - { - MediaFiles = new List - { - new() { Path = path } - } - } - } - }); - - metadata.Count.Should().Be(2); - metadata.Map(m => m.EpisodeNumber).Should().BeEquivalentTo(new[] { episode1, episode2 }); - } - - [Test] - [TestCase("Something (2021).mkv", "Something")] - [TestCase("Something Else (2021).mkv", "Something Else")] - public void GetFallbackMetadata_Should_Set_Proper_Movie_Title(string path, string expectedTitle) - { - MovieMetadata metadata = _fallbackMetadataProvider.GetFallbackMetadata( - new Movie - { - LibraryPath = new LibraryPath(), - MediaVersions = new List - { - new() - { - MediaFiles = new List - { - new() { Path = path } - } - } - } - }); - - metadata.Should().NotBeNull(); - metadata.Title.Should().Be(expectedTitle); - } + metadata.Count.Should().Be(1); + // TODO: how can we test season number? do we need to? + // metadata.Season.Should().Be(season); + metadata.Head().EpisodeNumber.Should().Be(episode); } -} + + [Test] + [TestCase("Awesome Show - s01e02-s01e03.mkv", 1, 2, 3)] + [TestCase("Awesome Show - s01e02-whatever-s01e03-whatever2.mkv", 1, 2, 3)] + [TestCase("Awesome Show - s01e02e03.mkv", 1, 2, 3)] + [TestCase("Awesome Show - s01e02-03.mkv", 1, 2, 3)] + public void GetFallbackMetadata_Should_Handle_Two_Episode_Formats( + string path, + int season, + int episode1, + int episode2) + { + List metadata = _fallbackMetadataProvider.GetFallbackMetadata( + new Episode + { + LibraryPath = new LibraryPath(), + MediaVersions = new List + { + new() + { + MediaFiles = new List + { + new() { Path = path } + } + } + } + }); + + metadata.Count.Should().Be(2); + metadata.Map(m => m.EpisodeNumber).Should().BeEquivalentTo(new[] { episode1, episode2 }); + } + + [Test] + [TestCase("Something (2021).mkv", "Something")] + [TestCase("Something Else (2021).mkv", "Something Else")] + public void GetFallbackMetadata_Should_Set_Proper_Movie_Title(string path, string expectedTitle) + { + MovieMetadata metadata = _fallbackMetadataProvider.GetFallbackMetadata( + new Movie + { + LibraryPath = new LibraryPath(), + MediaVersions = new List + { + new() + { + MediaFiles = new List + { + new() { Path = path } + } + } + } + }); + + metadata.Should().NotBeNull(); + metadata.Title.Should().Be(expectedTitle); + } +} \ No newline at end of file diff --git a/ErsatzTV.Core.Tests/Metadata/LocalStatisticsProviderTests.cs b/ErsatzTV.Core.Tests/Metadata/LocalStatisticsProviderTests.cs index b749d837d..5c24cd547 100644 --- a/ErsatzTV.Core.Tests/Metadata/LocalStatisticsProviderTests.cs +++ b/ErsatzTV.Core.Tests/Metadata/LocalStatisticsProviderTests.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Metadata; @@ -9,29 +7,28 @@ using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; -namespace ErsatzTV.Core.Tests.Metadata +namespace ErsatzTV.Core.Tests.Metadata; + +[TestFixture] +public class LocalStatisticsProviderTests { - [TestFixture] - public class LocalStatisticsProviderTests + [Test] + // this needs to be a culture where '.' is a group separator + [SetCulture("it-IT")] + public void Test() { - [Test] - // this needs to be a culture where '.' is a group separator - [SetCulture("it-IT")] - public void Test() - { - var provider = new LocalStatisticsProvider( - new Mock().Object, - new Mock().Object, - new Mock>().Object); + var provider = new LocalStatisticsProvider( + new Mock().Object, + new Mock().Object, + new Mock>().Object); - var input = new LocalStatisticsProvider.FFprobe( - new LocalStatisticsProvider.FFprobeFormat("123.45", null), - new List(), - new List()); + var input = new LocalStatisticsProvider.FFprobe( + new LocalStatisticsProvider.FFprobeFormat("123.45", null), + new List(), + new List()); - MediaVersion result = provider.ProjectToMediaVersion("test", input); + MediaVersion result = provider.ProjectToMediaVersion("test", input); - result.Duration.Should().Be(TimeSpan.FromSeconds(123.45)); - } + result.Duration.Should().Be(TimeSpan.FromSeconds(123.45)); } -} +} \ No newline at end of file diff --git a/ErsatzTV.Core.Tests/Metadata/MovieFolderScannerTests.cs b/ErsatzTV.Core.Tests/Metadata/MovieFolderScannerTests.cs index d6276f02a..6ab74a585 100644 --- a/ErsatzTV.Core.Tests/Metadata/MovieFolderScannerTests.cs +++ b/ErsatzTV.Core.Tests/Metadata/MovieFolderScannerTests.cs @@ -1,11 +1,5 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices; -using System.Threading.Tasks; +using System.Runtime.InteropServices; using ErsatzTV.Core.Domain; -using ErsatzTV.Core.Errors; using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Interfaces.Images; using ErsatzTV.Core.Interfaces.Metadata; @@ -14,608 +8,604 @@ using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Metadata; using ErsatzTV.Core.Tests.Fakes; using FluentAssertions; -using LanguageExt; using MediatR; using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; -using static LanguageExt.Prelude; -using Unit = LanguageExt.Unit; -namespace ErsatzTV.Core.Tests.Metadata +namespace ErsatzTV.Core.Tests.Metadata; + +[TestFixture] +public class MovieFolderScannerTests { + private static readonly string BadFakeRoot = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? @"C:\Movies-That-Dont-Exist" + : @"/movies-that-dont-exist"; + + private static readonly string FakeRoot = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? @"C:\Movies" + : "/movies"; + + private static readonly string FFprobePath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? @"C:\bin\ffprobe.exe" + : "/bin/ffprobe"; + [TestFixture] - public class MovieFolderScannerTests + public class ScanFolder { - private static readonly string BadFakeRoot = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) - ? @"C:\Movies-That-Dont-Exist" - : @"/movies-that-dont-exist"; - - private static readonly string FakeRoot = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) - ? @"C:\Movies" - : "/movies"; - - private static readonly string FFprobePath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) - ? @"C:\bin\ffprobe.exe" - : "/bin/ffprobe"; - - [TestFixture] - public class ScanFolder + [SetUp] + public void SetUp() { - [SetUp] - public void SetUp() - { - _movieRepository = new Mock(); - _movieRepository.Setup(x => x.GetOrAdd(It.IsAny(), It.IsAny())) - .Returns( - (LibraryPath _, string path) => - Right>(new FakeMovieWithPath(path)).AsTask()); - _movieRepository.Setup(x => x.FindMoviePaths(It.IsAny())) - .Returns(new List().AsEnumerable().AsTask()); - - _mediaItemRepository = new Mock(); - _mediaItemRepository.Setup(x => x.FlagFileNotFound(It.IsAny(), It.IsAny())) - .Returns(new List().AsTask()); - - _localStatisticsProvider = new Mock(); - _localMetadataProvider = new Mock(); - - _localStatisticsProvider.Setup(x => x.RefreshStatistics(It.IsAny(), It.IsAny())) - .Returns((_, _) => Right(true).AsTask()); - - // fallback metadata adds metadata to a movie, so we need to replicate that here - _localMetadataProvider.Setup(x => x.RefreshFallbackMetadata(It.IsAny())) - .Returns( - (MediaItem mediaItem) => - { - ((Movie) mediaItem).MovieMetadata = new List { new() }; - return Task.FromResult(true); - }); - - _imageCache = new Mock(); - } - - private Mock _movieRepository; - private Mock _mediaItemRepository; - private Mock _localStatisticsProvider; - private Mock _localMetadataProvider; - private Mock _imageCache; - - [Test] - public async Task NewMovie_Statistics_And_FallbackMetadata( - [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.VideoFileExtensions))] - string videoExtension) - { - string moviePath = Path.Combine( - FakeRoot, - Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}")); - - MovieFolderScanner service = GetService( - new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now } - ); - var libraryPath = new LibraryPath - { Id = 1, Path = FakeRoot, LibraryFolders = new List() }; - - Either result = await service.ScanFolder( - libraryPath, - FFprobePath, - 0, - 1); - - result.IsRight.Should().BeTrue(); - - _movieRepository.Verify(x => x.GetOrAdd(It.IsAny(), It.IsAny()), Times.Once); - _movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once); - - _localStatisticsProvider.Verify( - x => x.RefreshStatistics( - FFprobePath, - It.Is(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)), - Times.Once); - - _localMetadataProvider.Verify( - x => x.RefreshFallbackMetadata( - It.Is(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)), - Times.Once); - } - - [Test] - public async Task NewMovie_Statistics_And_SidecarMetadata_MovieNameNfo( - [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.VideoFileExtensions))] - string videoExtension) - { - string moviePath = Path.Combine( - FakeRoot, - Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}")); - - string metadataPath = Path.ChangeExtension(moviePath, "nfo"); - - MovieFolderScanner service = GetService( - new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now }, - new FakeFileEntry(metadataPath) - ); - var libraryPath = new LibraryPath - { Id = 1, Path = FakeRoot, LibraryFolders = new List() }; - - Either result = await service.ScanFolder( - libraryPath, - FFprobePath, - 0, - 1); - - result.IsRight.Should().BeTrue(); - - _movieRepository.Verify(x => x.GetOrAdd(It.IsAny(), It.IsAny()), Times.Once); - _movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once); - - _localStatisticsProvider.Verify( - x => x.RefreshStatistics( - FFprobePath, - It.Is(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)), - Times.Once); - - _localMetadataProvider.Verify( - x => x.RefreshSidecarMetadata( - It.Is(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath), - metadataPath), - Times.Once); - } - - [Test] - public async Task NewMovie_Statistics_And_SidecarMetadata_MovieNfo( - [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.VideoFileExtensions))] - string videoExtension) - { - string moviePath = Path.Combine( - FakeRoot, - Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}")); - - string metadataPath = Path.Combine(Path.GetDirectoryName(moviePath) ?? string.Empty, "movie.nfo"); - - MovieFolderScanner service = GetService( - new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now }, - new FakeFileEntry(metadataPath) - ); - var libraryPath = new LibraryPath - { Id = 1, Path = FakeRoot, LibraryFolders = new List() }; - - Either result = await service.ScanFolder( - libraryPath, - FFprobePath, - 0, - 1); - - result.IsRight.Should().BeTrue(); - - _movieRepository.Verify(x => x.GetOrAdd(It.IsAny(), It.IsAny()), Times.Once); - _movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once); - - _localStatisticsProvider.Verify( - x => x.RefreshStatistics( - FFprobePath, - It.Is(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)), - Times.Once); - - _localMetadataProvider.Verify( - x => x.RefreshSidecarMetadata( - It.Is(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath), - metadataPath), - Times.Once); - } - - [Test] - public async Task NewMovie_Statistics_And_FallbackMetadata_And_Poster( - [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.VideoFileExtensions))] - string videoExtension, - [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.ImageFileExtensions))] - string imageExtension) - { - string moviePath = Path.Combine( - FakeRoot, - Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}")); - - string posterPath = Path.Combine( - Path.GetDirectoryName(moviePath) ?? string.Empty, - $"poster.{imageExtension}"); - - MovieFolderScanner service = GetService( - new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now }, - new FakeFileEntry(posterPath) { LastWriteTime = DateTime.Now } - ); - var libraryPath = new LibraryPath - { Id = 1, Path = FakeRoot, LibraryFolders = new List() }; - - Either result = await service.ScanFolder( - libraryPath, - FFprobePath, - 0, - 1); - - result.IsRight.Should().BeTrue(); - - _movieRepository.Verify(x => x.GetOrAdd(It.IsAny(), It.IsAny()), Times.Once); - _movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once); - - _localStatisticsProvider.Verify( - x => x.RefreshStatistics( - FFprobePath, - It.Is(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)), - Times.Once); - - _localMetadataProvider.Verify( - x => x.RefreshFallbackMetadata( - It.Is(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)), - Times.Once); - - _imageCache.Verify( - x => x.CopyArtworkToCache(posterPath, ArtworkKind.Poster), - Times.Once); - } - - [Test] - public async Task NewMovie_Statistics_And_FallbackMetadata_And_FolderPoster( - [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.VideoFileExtensions))] - string videoExtension, - [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.ImageFileExtensions))] - string imageExtension) - { - string moviePath = Path.Combine( - FakeRoot, - Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}")); - - string posterPath = Path.Combine( - Path.GetDirectoryName(moviePath) ?? string.Empty, - $"folder.{imageExtension}"); - - MovieFolderScanner service = GetService( - new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now }, - new FakeFileEntry(posterPath) { LastWriteTime = DateTime.Now } - ); - var libraryPath = new LibraryPath - { Id = 1, Path = FakeRoot, LibraryFolders = new List() }; - - Either result = await service.ScanFolder( - libraryPath, - FFprobePath, - 0, - 1); - - result.IsRight.Should().BeTrue(); - - _movieRepository.Verify(x => x.GetOrAdd(It.IsAny(), It.IsAny()), Times.Once); - _movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once); - - _localStatisticsProvider.Verify( - x => x.RefreshStatistics( - FFprobePath, - It.Is(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)), - Times.Once); - - _localMetadataProvider.Verify( - x => x.RefreshFallbackMetadata( - It.Is(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)), - Times.Once); - - _imageCache.Verify( - x => x.CopyArtworkToCache(posterPath, ArtworkKind.Poster), - Times.Once); - } - - [Test] - public async Task NewMovie_Statistics_And_FallbackMetadata_And_MovieNamePoster( - [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.VideoFileExtensions))] - string videoExtension, - [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.ImageFileExtensions))] - string imageExtension) - { - string moviePath = Path.Combine( - FakeRoot, - Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}")); - - string posterPath = Path.Combine( - Path.GetDirectoryName(moviePath) ?? string.Empty, - $"Movie (2020)-poster.{imageExtension}"); - - MovieFolderScanner service = GetService( - new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now }, - new FakeFileEntry(posterPath) { LastWriteTime = DateTime.Now } - ); - var libraryPath = new LibraryPath - { Id = 1, Path = FakeRoot, LibraryFolders = new List() }; - - Either result = await service.ScanFolder( - libraryPath, - FFprobePath, - 0, - 1); - - result.IsRight.Should().BeTrue(); - - _movieRepository.Verify(x => x.GetOrAdd(It.IsAny(), It.IsAny()), Times.Once); - _movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once); - - _localStatisticsProvider.Verify( - x => x.RefreshStatistics( - FFprobePath, - It.Is(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)), - Times.Once); - - _localMetadataProvider.Verify( - x => x.RefreshFallbackMetadata( - It.Is(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)), - Times.Once); - - _imageCache.Verify( - x => x.CopyArtworkToCache(posterPath, ArtworkKind.Poster), - Times.Once); - } - - [Test] - public async Task Should_Ignore_Extra_Files( - [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.VideoFileExtensions))] - string videoExtension, - [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.ExtraFiles))] - string extraFile) - { - string moviePath = Path.Combine( - FakeRoot, - Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}")); - - MovieFolderScanner service = GetService( - new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now }, - new FakeFileEntry( - Path.Combine( - Path.GetDirectoryName(moviePath) ?? string.Empty, - $"Movie (2020)-{extraFile}{videoExtension}")) - ); - var libraryPath = new LibraryPath - { Id = 1, Path = FakeRoot, LibraryFolders = new List() }; - - Either result = await service.ScanFolder( - libraryPath, - FFprobePath, - 0, - 1); - - result.IsRight.Should().BeTrue(); - - _movieRepository.Verify(x => x.GetOrAdd(It.IsAny(), It.IsAny()), Times.Once); - _movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once); - - _localStatisticsProvider.Verify( - x => x.RefreshStatistics( - FFprobePath, - It.Is(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)), - Times.Once); - - _localMetadataProvider.Verify( - x => x.RefreshFallbackMetadata( - It.Is(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)), - Times.Once); - } - - [Test] - public async Task Should_Ignore_Dot_Underscore_Files( - [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.VideoFileExtensions))] - string videoExtension) - { - string moviePath = Path.Combine( - FakeRoot, - Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}")); - - MovieFolderScanner service = GetService( - new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now }, - new FakeFileEntry( - Path.Combine( - Path.GetDirectoryName(moviePath) ?? string.Empty, - $"._Movie (2020){videoExtension}")) - ); - var libraryPath = new LibraryPath - { Id = 1, Path = FakeRoot, LibraryFolders = new List() }; - - Either result = await service.ScanFolder( - libraryPath, - FFprobePath, - 0, - 1); - - result.IsRight.Should().BeTrue(); - - _movieRepository.Verify(x => x.GetOrAdd(It.IsAny(), It.IsAny()), Times.Once); - _movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once); - - _localStatisticsProvider.Verify( - x => x.RefreshStatistics( - FFprobePath, - It.Is(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)), - Times.Once); - - _localMetadataProvider.Verify( - x => x.RefreshFallbackMetadata( - It.Is(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)), - Times.Once); - } - - [Test] - public async Task Should_Ignore_Extra_Folders( - [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.VideoFileExtensions))] - string videoExtension, - [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.ExtraDirectories))] - string extraFolder) - { - string moviePath = Path.Combine( - FakeRoot, - Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}")); - - MovieFolderScanner service = GetService( - new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now }, - new FakeFileEntry( - Path.Combine( - Path.GetDirectoryName(moviePath) ?? string.Empty, - Path.Combine(extraFolder, $"Movie (2020){videoExtension}"))) - ); - var libraryPath = new LibraryPath - { Id = 1, Path = FakeRoot, LibraryFolders = new List() }; - - Either result = await service.ScanFolder( - libraryPath, - FFprobePath, - 0, - 1); - - result.IsRight.Should().BeTrue(); - - _movieRepository.Verify(x => x.GetOrAdd(It.IsAny(), It.IsAny()), Times.Once); - _movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once); - - _localStatisticsProvider.Verify( - x => x.RefreshStatistics( - FFprobePath, - It.Is(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)), - Times.Once); - - _localMetadataProvider.Verify( - x => x.RefreshFallbackMetadata( - It.Is(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)), - Times.Once); - } - - [Test] - public async Task Should_Work_With_Nested_Folders( - [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.VideoFileExtensions))] - string videoExtension) - { - string moviePath = Path.Combine( - Path.Combine(FakeRoot, "L-P"), - Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}")); - - MovieFolderScanner service = GetService( - new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now } - ); - var libraryPath = new LibraryPath - { Id = 1, Path = FakeRoot, LibraryFolders = new List() }; - - Either result = await service.ScanFolder( - libraryPath, - FFprobePath, - 0, - 1); - - result.IsRight.Should().BeTrue(); - - _movieRepository.Verify(x => x.GetOrAdd(It.IsAny(), It.IsAny()), Times.Once); - _movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once); - - _localStatisticsProvider.Verify( - x => x.RefreshStatistics( - FFprobePath, - It.Is(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)), - Times.Once); - - _localMetadataProvider.Verify( - x => x.RefreshFallbackMetadata( - It.Is(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)), - Times.Once); - } - - [Test] - public async Task RenamedMovie_Should_Delete_Old_Movie() - { - // TODO: handle this case more elegantly - // ideally, detect that the movie was renamed and still delete the old one (or update the path?) - - string movieFolder = Path.Combine(FakeRoot, "Movie (2020)"); - string oldMoviePath = Path.Combine(movieFolder, "Movie (2020).avi"); - - _movieRepository.Setup(x => x.FindMoviePaths(It.IsAny())) - .Returns(new List { oldMoviePath }.AsEnumerable().AsTask()); - - string moviePath = Path.Combine(movieFolder, "Movie (2020).mkv"); - - MovieFolderScanner service = GetService( - new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now } - ); - var libraryPath = new LibraryPath - { Id = 1, Path = FakeRoot, LibraryFolders = new List() }; - - Either result = await service.ScanFolder( - libraryPath, - FFprobePath, - 0, - 1); - - result.IsRight.Should().BeTrue(); - - _mediaItemRepository.Verify( - x => x.FlagFileNotFound(It.IsAny(), It.IsAny()), - Times.Once); - _mediaItemRepository.Verify(x => x.FlagFileNotFound(libraryPath, oldMoviePath), Times.Once); - } - - [Test] - public async Task DeletedMovieAndFolder_Should_Flag_File_Not_Found() - { - string movieFolder = Path.Combine(FakeRoot, "Movie (2020)"); - string oldMoviePath = Path.Combine(movieFolder, "Movie (2020).avi"); - - _movieRepository.Setup(x => x.FindMoviePaths(It.IsAny())) - .Returns(new List { oldMoviePath }.AsEnumerable().AsTask()); - - MovieFolderScanner service = GetService( - new FakeFolderEntry(FakeRoot) - ); - var libraryPath = new LibraryPath - { Id = 1, Path = FakeRoot, LibraryFolders = new List() }; - - Either result = await service.ScanFolder( - libraryPath, - FFprobePath, - 0, - 1); - - result.IsRight.Should().BeTrue(); - - _mediaItemRepository.Verify( - x => x.FlagFileNotFound(It.IsAny(), It.IsAny()), - Times.Once); - _mediaItemRepository.Verify(x => x.FlagFileNotFound(libraryPath, oldMoviePath), Times.Once); - } - - private MovieFolderScanner GetService(params FakeFileEntry[] files) => - new( - new FakeLocalFileSystem(new List(files)), - _movieRepository.Object, - _localStatisticsProvider.Object, - _localMetadataProvider.Object, - new Mock().Object, - _imageCache.Object, - new Mock().Object, - new Mock().Object, - new Mock().Object, - _mediaItemRepository.Object, - new Mock().Object, - null, - new Mock().Object, - new Mock>().Object - ); - - private MovieFolderScanner GetService(params FakeFolderEntry[] folders) => - new( - new FakeLocalFileSystem(new List(), new List(folders)), - _movieRepository.Object, - _localStatisticsProvider.Object, - _localMetadataProvider.Object, - new Mock().Object, - _imageCache.Object, - new Mock().Object, - new Mock().Object, - new Mock().Object, - _mediaItemRepository.Object, - new Mock().Object, - null, - new Mock().Object, - new Mock>().Object - ); + _movieRepository = new Mock(); + _movieRepository.Setup(x => x.GetOrAdd(It.IsAny(), It.IsAny())) + .Returns( + (LibraryPath _, string path) => + Right>(new FakeMovieWithPath(path)).AsTask()); + _movieRepository.Setup(x => x.FindMoviePaths(It.IsAny())) + .Returns(new List().AsEnumerable().AsTask()); + + _mediaItemRepository = new Mock(); + _mediaItemRepository.Setup(x => x.FlagFileNotFound(It.IsAny(), It.IsAny())) + .Returns(new List().AsTask()); + + _localStatisticsProvider = new Mock(); + _localMetadataProvider = new Mock(); + + _localStatisticsProvider.Setup(x => x.RefreshStatistics(It.IsAny(), It.IsAny())) + .Returns((_, _) => Right(true).AsTask()); + + // fallback metadata adds metadata to a movie, so we need to replicate that here + _localMetadataProvider.Setup(x => x.RefreshFallbackMetadata(It.IsAny())) + .Returns( + (MediaItem mediaItem) => + { + ((Movie) mediaItem).MovieMetadata = new List { new() }; + return Task.FromResult(true); + }); + + _imageCache = new Mock(); } + + private Mock _movieRepository; + private Mock _mediaItemRepository; + private Mock _localStatisticsProvider; + private Mock _localMetadataProvider; + private Mock _imageCache; + + [Test] + public async Task NewMovie_Statistics_And_FallbackMetadata( + [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.VideoFileExtensions))] + string videoExtension) + { + string moviePath = Path.Combine( + FakeRoot, + Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}")); + + MovieFolderScanner service = GetService( + new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now } + ); + var libraryPath = new LibraryPath + { Id = 1, Path = FakeRoot, LibraryFolders = new List() }; + + Either result = await service.ScanFolder( + libraryPath, + FFprobePath, + 0, + 1); + + result.IsRight.Should().BeTrue(); + + _movieRepository.Verify(x => x.GetOrAdd(It.IsAny(), It.IsAny()), Times.Once); + _movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once); + + _localStatisticsProvider.Verify( + x => x.RefreshStatistics( + FFprobePath, + It.Is(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)), + Times.Once); + + _localMetadataProvider.Verify( + x => x.RefreshFallbackMetadata( + It.Is(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)), + Times.Once); + } + + [Test] + public async Task NewMovie_Statistics_And_SidecarMetadata_MovieNameNfo( + [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.VideoFileExtensions))] + string videoExtension) + { + string moviePath = Path.Combine( + FakeRoot, + Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}")); + + string metadataPath = Path.ChangeExtension(moviePath, "nfo"); + + MovieFolderScanner service = GetService( + new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now }, + new FakeFileEntry(metadataPath) + ); + var libraryPath = new LibraryPath + { Id = 1, Path = FakeRoot, LibraryFolders = new List() }; + + Either result = await service.ScanFolder( + libraryPath, + FFprobePath, + 0, + 1); + + result.IsRight.Should().BeTrue(); + + _movieRepository.Verify(x => x.GetOrAdd(It.IsAny(), It.IsAny()), Times.Once); + _movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once); + + _localStatisticsProvider.Verify( + x => x.RefreshStatistics( + FFprobePath, + It.Is(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)), + Times.Once); + + _localMetadataProvider.Verify( + x => x.RefreshSidecarMetadata( + It.Is(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath), + metadataPath), + Times.Once); + } + + [Test] + public async Task NewMovie_Statistics_And_SidecarMetadata_MovieNfo( + [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.VideoFileExtensions))] + string videoExtension) + { + string moviePath = Path.Combine( + FakeRoot, + Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}")); + + string metadataPath = Path.Combine(Path.GetDirectoryName(moviePath) ?? string.Empty, "movie.nfo"); + + MovieFolderScanner service = GetService( + new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now }, + new FakeFileEntry(metadataPath) + ); + var libraryPath = new LibraryPath + { Id = 1, Path = FakeRoot, LibraryFolders = new List() }; + + Either result = await service.ScanFolder( + libraryPath, + FFprobePath, + 0, + 1); + + result.IsRight.Should().BeTrue(); + + _movieRepository.Verify(x => x.GetOrAdd(It.IsAny(), It.IsAny()), Times.Once); + _movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once); + + _localStatisticsProvider.Verify( + x => x.RefreshStatistics( + FFprobePath, + It.Is(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)), + Times.Once); + + _localMetadataProvider.Verify( + x => x.RefreshSidecarMetadata( + It.Is(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath), + metadataPath), + Times.Once); + } + + [Test] + public async Task NewMovie_Statistics_And_FallbackMetadata_And_Poster( + [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.VideoFileExtensions))] + string videoExtension, + [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.ImageFileExtensions))] + string imageExtension) + { + string moviePath = Path.Combine( + FakeRoot, + Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}")); + + string posterPath = Path.Combine( + Path.GetDirectoryName(moviePath) ?? string.Empty, + $"poster.{imageExtension}"); + + MovieFolderScanner service = GetService( + new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now }, + new FakeFileEntry(posterPath) { LastWriteTime = DateTime.Now } + ); + var libraryPath = new LibraryPath + { Id = 1, Path = FakeRoot, LibraryFolders = new List() }; + + Either result = await service.ScanFolder( + libraryPath, + FFprobePath, + 0, + 1); + + result.IsRight.Should().BeTrue(); + + _movieRepository.Verify(x => x.GetOrAdd(It.IsAny(), It.IsAny()), Times.Once); + _movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once); + + _localStatisticsProvider.Verify( + x => x.RefreshStatistics( + FFprobePath, + It.Is(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)), + Times.Once); + + _localMetadataProvider.Verify( + x => x.RefreshFallbackMetadata( + It.Is(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)), + Times.Once); + + _imageCache.Verify( + x => x.CopyArtworkToCache(posterPath, ArtworkKind.Poster), + Times.Once); + } + + [Test] + public async Task NewMovie_Statistics_And_FallbackMetadata_And_FolderPoster( + [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.VideoFileExtensions))] + string videoExtension, + [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.ImageFileExtensions))] + string imageExtension) + { + string moviePath = Path.Combine( + FakeRoot, + Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}")); + + string posterPath = Path.Combine( + Path.GetDirectoryName(moviePath) ?? string.Empty, + $"folder.{imageExtension}"); + + MovieFolderScanner service = GetService( + new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now }, + new FakeFileEntry(posterPath) { LastWriteTime = DateTime.Now } + ); + var libraryPath = new LibraryPath + { Id = 1, Path = FakeRoot, LibraryFolders = new List() }; + + Either result = await service.ScanFolder( + libraryPath, + FFprobePath, + 0, + 1); + + result.IsRight.Should().BeTrue(); + + _movieRepository.Verify(x => x.GetOrAdd(It.IsAny(), It.IsAny()), Times.Once); + _movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once); + + _localStatisticsProvider.Verify( + x => x.RefreshStatistics( + FFprobePath, + It.Is(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)), + Times.Once); + + _localMetadataProvider.Verify( + x => x.RefreshFallbackMetadata( + It.Is(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)), + Times.Once); + + _imageCache.Verify( + x => x.CopyArtworkToCache(posterPath, ArtworkKind.Poster), + Times.Once); + } + + [Test] + public async Task NewMovie_Statistics_And_FallbackMetadata_And_MovieNamePoster( + [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.VideoFileExtensions))] + string videoExtension, + [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.ImageFileExtensions))] + string imageExtension) + { + string moviePath = Path.Combine( + FakeRoot, + Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}")); + + string posterPath = Path.Combine( + Path.GetDirectoryName(moviePath) ?? string.Empty, + $"Movie (2020)-poster.{imageExtension}"); + + MovieFolderScanner service = GetService( + new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now }, + new FakeFileEntry(posterPath) { LastWriteTime = DateTime.Now } + ); + var libraryPath = new LibraryPath + { Id = 1, Path = FakeRoot, LibraryFolders = new List() }; + + Either result = await service.ScanFolder( + libraryPath, + FFprobePath, + 0, + 1); + + result.IsRight.Should().BeTrue(); + + _movieRepository.Verify(x => x.GetOrAdd(It.IsAny(), It.IsAny()), Times.Once); + _movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once); + + _localStatisticsProvider.Verify( + x => x.RefreshStatistics( + FFprobePath, + It.Is(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)), + Times.Once); + + _localMetadataProvider.Verify( + x => x.RefreshFallbackMetadata( + It.Is(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)), + Times.Once); + + _imageCache.Verify( + x => x.CopyArtworkToCache(posterPath, ArtworkKind.Poster), + Times.Once); + } + + [Test] + public async Task Should_Ignore_Extra_Files( + [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.VideoFileExtensions))] + string videoExtension, + [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.ExtraFiles))] + string extraFile) + { + string moviePath = Path.Combine( + FakeRoot, + Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}")); + + MovieFolderScanner service = GetService( + new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now }, + new FakeFileEntry( + Path.Combine( + Path.GetDirectoryName(moviePath) ?? string.Empty, + $"Movie (2020)-{extraFile}{videoExtension}")) + ); + var libraryPath = new LibraryPath + { Id = 1, Path = FakeRoot, LibraryFolders = new List() }; + + Either result = await service.ScanFolder( + libraryPath, + FFprobePath, + 0, + 1); + + result.IsRight.Should().BeTrue(); + + _movieRepository.Verify(x => x.GetOrAdd(It.IsAny(), It.IsAny()), Times.Once); + _movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once); + + _localStatisticsProvider.Verify( + x => x.RefreshStatistics( + FFprobePath, + It.Is(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)), + Times.Once); + + _localMetadataProvider.Verify( + x => x.RefreshFallbackMetadata( + It.Is(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)), + Times.Once); + } + + [Test] + public async Task Should_Ignore_Dot_Underscore_Files( + [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.VideoFileExtensions))] + string videoExtension) + { + string moviePath = Path.Combine( + FakeRoot, + Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}")); + + MovieFolderScanner service = GetService( + new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now }, + new FakeFileEntry( + Path.Combine( + Path.GetDirectoryName(moviePath) ?? string.Empty, + $"._Movie (2020){videoExtension}")) + ); + var libraryPath = new LibraryPath + { Id = 1, Path = FakeRoot, LibraryFolders = new List() }; + + Either result = await service.ScanFolder( + libraryPath, + FFprobePath, + 0, + 1); + + result.IsRight.Should().BeTrue(); + + _movieRepository.Verify(x => x.GetOrAdd(It.IsAny(), It.IsAny()), Times.Once); + _movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once); + + _localStatisticsProvider.Verify( + x => x.RefreshStatistics( + FFprobePath, + It.Is(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)), + Times.Once); + + _localMetadataProvider.Verify( + x => x.RefreshFallbackMetadata( + It.Is(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)), + Times.Once); + } + + [Test] + public async Task Should_Ignore_Extra_Folders( + [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.VideoFileExtensions))] + string videoExtension, + [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.ExtraDirectories))] + string extraFolder) + { + string moviePath = Path.Combine( + FakeRoot, + Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}")); + + MovieFolderScanner service = GetService( + new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now }, + new FakeFileEntry( + Path.Combine( + Path.GetDirectoryName(moviePath) ?? string.Empty, + Path.Combine(extraFolder, $"Movie (2020){videoExtension}"))) + ); + var libraryPath = new LibraryPath + { Id = 1, Path = FakeRoot, LibraryFolders = new List() }; + + Either result = await service.ScanFolder( + libraryPath, + FFprobePath, + 0, + 1); + + result.IsRight.Should().BeTrue(); + + _movieRepository.Verify(x => x.GetOrAdd(It.IsAny(), It.IsAny()), Times.Once); + _movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once); + + _localStatisticsProvider.Verify( + x => x.RefreshStatistics( + FFprobePath, + It.Is(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)), + Times.Once); + + _localMetadataProvider.Verify( + x => x.RefreshFallbackMetadata( + It.Is(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)), + Times.Once); + } + + [Test] + public async Task Should_Work_With_Nested_Folders( + [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.VideoFileExtensions))] + string videoExtension) + { + string moviePath = Path.Combine( + Path.Combine(FakeRoot, "L-P"), + Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}")); + + MovieFolderScanner service = GetService( + new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now } + ); + var libraryPath = new LibraryPath + { Id = 1, Path = FakeRoot, LibraryFolders = new List() }; + + Either result = await service.ScanFolder( + libraryPath, + FFprobePath, + 0, + 1); + + result.IsRight.Should().BeTrue(); + + _movieRepository.Verify(x => x.GetOrAdd(It.IsAny(), It.IsAny()), Times.Once); + _movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once); + + _localStatisticsProvider.Verify( + x => x.RefreshStatistics( + FFprobePath, + It.Is(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)), + Times.Once); + + _localMetadataProvider.Verify( + x => x.RefreshFallbackMetadata( + It.Is(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)), + Times.Once); + } + + [Test] + public async Task RenamedMovie_Should_Delete_Old_Movie() + { + // TODO: handle this case more elegantly + // ideally, detect that the movie was renamed and still delete the old one (or update the path?) + + string movieFolder = Path.Combine(FakeRoot, "Movie (2020)"); + string oldMoviePath = Path.Combine(movieFolder, "Movie (2020).avi"); + + _movieRepository.Setup(x => x.FindMoviePaths(It.IsAny())) + .Returns(new List { oldMoviePath }.AsEnumerable().AsTask()); + + string moviePath = Path.Combine(movieFolder, "Movie (2020).mkv"); + + MovieFolderScanner service = GetService( + new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now } + ); + var libraryPath = new LibraryPath + { Id = 1, Path = FakeRoot, LibraryFolders = new List() }; + + Either result = await service.ScanFolder( + libraryPath, + FFprobePath, + 0, + 1); + + result.IsRight.Should().BeTrue(); + + _mediaItemRepository.Verify( + x => x.FlagFileNotFound(It.IsAny(), It.IsAny()), + Times.Once); + _mediaItemRepository.Verify(x => x.FlagFileNotFound(libraryPath, oldMoviePath), Times.Once); + } + + [Test] + public async Task DeletedMovieAndFolder_Should_Flag_File_Not_Found() + { + string movieFolder = Path.Combine(FakeRoot, "Movie (2020)"); + string oldMoviePath = Path.Combine(movieFolder, "Movie (2020).avi"); + + _movieRepository.Setup(x => x.FindMoviePaths(It.IsAny())) + .Returns(new List { oldMoviePath }.AsEnumerable().AsTask()); + + MovieFolderScanner service = GetService( + new FakeFolderEntry(FakeRoot) + ); + var libraryPath = new LibraryPath + { Id = 1, Path = FakeRoot, LibraryFolders = new List() }; + + Either result = await service.ScanFolder( + libraryPath, + FFprobePath, + 0, + 1); + + result.IsRight.Should().BeTrue(); + + _mediaItemRepository.Verify( + x => x.FlagFileNotFound(It.IsAny(), It.IsAny()), + Times.Once); + _mediaItemRepository.Verify(x => x.FlagFileNotFound(libraryPath, oldMoviePath), Times.Once); + } + + private MovieFolderScanner GetService(params FakeFileEntry[] files) => + new( + new FakeLocalFileSystem(new List(files)), + _movieRepository.Object, + _localStatisticsProvider.Object, + _localMetadataProvider.Object, + new Mock().Object, + _imageCache.Object, + new Mock().Object, + new Mock().Object, + new Mock().Object, + _mediaItemRepository.Object, + new Mock().Object, + null, + new Mock().Object, + new Mock>().Object + ); + + private MovieFolderScanner GetService(params FakeFolderEntry[] folders) => + new( + new FakeLocalFileSystem(new List(), new List(folders)), + _movieRepository.Object, + _localStatisticsProvider.Object, + _localMetadataProvider.Object, + new Mock().Object, + _imageCache.Object, + new Mock().Object, + new Mock().Object, + new Mock().Object, + _mediaItemRepository.Object, + new Mock().Object, + null, + new Mock().Object, + new Mock>().Object + ); } -} +} \ No newline at end of file diff --git a/ErsatzTV.Core.Tests/Metadata/Nfo/EpisodeNfoReaderTests.cs b/ErsatzTV.Core.Tests/Metadata/Nfo/EpisodeNfoReaderTests.cs index fdd2f4a74..d76ee29a2 100644 --- a/ErsatzTV.Core.Tests/Metadata/Nfo/EpisodeNfoReaderTests.cs +++ b/ErsatzTV.Core.Tests/Metadata/Nfo/EpisodeNfoReaderTests.cs @@ -1,40 +1,36 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Text; using ErsatzTV.Core.Metadata.Nfo; using FluentAssertions; using NUnit.Framework; -namespace ErsatzTV.Core.Tests.Metadata.Nfo +namespace ErsatzTV.Core.Tests.Metadata.Nfo; + +[TestFixture] +public class EpisodeNfoReaderTests { - [TestFixture] - public class EpisodeNfoReaderTests + [Test] + public async Task One() { - [Test] - public async Task One() - { - var reader = new EpisodeNfoReader(); - var stream = new MemoryStream( - Encoding.UTF8.GetBytes( - @" + var reader = new EpisodeNfoReader(); + var stream = new MemoryStream( + Encoding.UTF8.GetBytes( + @" ")); - List result = await reader.Read(stream); + List result = await reader.Read(stream); - result.Count.Should().Be(1); - } + result.Count.Should().Be(1); + } - [Test] - public async Task Two() - { - var reader = new EpisodeNfoReader(); - var stream = new MemoryStream( - Encoding.UTF8.GetBytes( - @" + [Test] + public async Task Two() + { + var reader = new EpisodeNfoReader(); + var stream = new MemoryStream( + Encoding.UTF8.GetBytes( + @" show @@ -49,61 +45,61 @@ namespace ErsatzTV.Core.Tests.Metadata.Nfo 1 ")); - List result = await reader.Read(stream); + List result = await reader.Read(stream); - result.Count.Should().Be(2); - result.All(nfo => nfo.ShowTitle == "show").Should().BeTrue(); - result.All(nfo => nfo.Season == 1).Should().BeTrue(); - result.Count(nfo => nfo.Title == "episode-one" && nfo.Episode == 1).Should().Be(1); - result.Count(nfo => nfo.Title == "episode-two" && nfo.Episode == 2).Should().Be(1); - } + result.Count.Should().Be(2); + result.All(nfo => nfo.ShowTitle == "show").Should().BeTrue(); + result.All(nfo => nfo.Season == 1).Should().BeTrue(); + result.Count(nfo => nfo.Title == "episode-one" && nfo.Episode == 1).Should().Be(1); + result.Count(nfo => nfo.Title == "episode-two" && nfo.Episode == 2).Should().Be(1); + } - [Test] - public async Task UniqueIds() - { - var reader = new EpisodeNfoReader(); - var stream = new MemoryStream( - Encoding.UTF8.GetBytes( - @" + [Test] + public async Task UniqueIds() + { + var reader = new EpisodeNfoReader(); + var stream = new MemoryStream( + Encoding.UTF8.GetBytes( + @" 12345 tt54321 ")); - List result = await reader.Read(stream); + List result = await reader.Read(stream); - result.Count.Should().Be(1); - result[0].UniqueIds.Count.Should().Be(2); - result[0].UniqueIds.Count(id => id.Default && id.Type == "tvdb" && id.Guid == "12345").Should().Be(1); - result[0].UniqueIds.Count(id => !id.Default && id.Type == "imdb" && id.Guid == "tt54321").Should().Be(1); - } + result.Count.Should().Be(1); + result[0].UniqueIds.Count.Should().Be(2); + result[0].UniqueIds.Count(id => id.Default && id.Type == "tvdb" && id.Guid == "12345").Should().Be(1); + result[0].UniqueIds.Count(id => !id.Default && id.Type == "imdb" && id.Guid == "tt54321").Should().Be(1); + } - [Test] - public async Task No_ContentRating() - { - var reader = new EpisodeNfoReader(); - var stream = new MemoryStream( - Encoding.UTF8.GetBytes( - @" + [Test] + public async Task No_ContentRating() + { + var reader = new EpisodeNfoReader(); + var stream = new MemoryStream( + Encoding.UTF8.GetBytes( + @" ")); - List result = await reader.Read(stream); + List result = await reader.Read(stream); - result.Count.Should().Be(1); - result[0].ContentRating.Should().BeNullOrEmpty(); - } + result.Count.Should().Be(1); + result[0].ContentRating.Should().BeNullOrEmpty(); + } - [Test] - public async Task ContentRating() - { - var reader = new EpisodeNfoReader(); - var stream = new MemoryStream( - Encoding.UTF8.GetBytes( - @" + [Test] + public async Task ContentRating() + { + var reader = new EpisodeNfoReader(); + var stream = new MemoryStream( + Encoding.UTF8.GetBytes( + @" US:Something @@ -112,56 +108,56 @@ namespace ErsatzTV.Core.Tests.Metadata.Nfo US:Something / US:SomethingElse ")); - List result = await reader.Read(stream); + List result = await reader.Read(stream); - result.Count.Should().Be(2); - result.Count(nfo => nfo.ContentRating == "US:Something").Should().Be(1); - result.Count(nfo => nfo.ContentRating == "US:Something / US:SomethingElse").Should().Be(1); - } + result.Count.Should().Be(2); + result.Count(nfo => nfo.ContentRating == "US:Something").Should().Be(1); + result.Count(nfo => nfo.ContentRating == "US:Something / US:SomethingElse").Should().Be(1); + } - [Test] - public async Task No_Plot() - { - var reader = new EpisodeNfoReader(); - var stream = new MemoryStream( - Encoding.UTF8.GetBytes( - @" + [Test] + public async Task No_Plot() + { + var reader = new EpisodeNfoReader(); + var stream = new MemoryStream( + Encoding.UTF8.GetBytes( + @" ")); - List result = await reader.Read(stream); + List result = await reader.Read(stream); - result.Count.Should().Be(1); - result[0].Plot.Should().BeNullOrEmpty(); - } + result.Count.Should().Be(1); + result[0].Plot.Should().BeNullOrEmpty(); + } - [Test] - public async Task Plot() - { - var reader = new EpisodeNfoReader(); - var stream = new MemoryStream( - Encoding.UTF8.GetBytes( - @" + [Test] + public async Task Plot() + { + var reader = new EpisodeNfoReader(); + var stream = new MemoryStream( + Encoding.UTF8.GetBytes( + @" Some Plot ")); - List result = await reader.Read(stream); + List result = await reader.Read(stream); - result.Count.Should().Be(1); - result[0].Plot.Should().Be("Some Plot"); - } + result.Count.Should().Be(1); + result[0].Plot.Should().Be("Some Plot"); + } - [Test] - public async Task Actors() - { - var reader = new EpisodeNfoReader(); - var stream = new MemoryStream( - Encoding.UTF8.GetBytes( - @" + [Test] + public async Task Actors() + { + var reader = new EpisodeNfoReader(); + var stream = new MemoryStream( + Encoding.UTF8.GetBytes( + @" @@ -176,23 +172,23 @@ namespace ErsatzTV.Core.Tests.Metadata.Nfo ")); - List result = await reader.Read(stream); + List result = await reader.Read(stream); - result.Count.Should().Be(1); - result[0].Actors.Count.Should().Be(2); - result[0].Actors.Count(a => a.Name == "Name 1" && a.Role == "Role 1" && a.Thumb == "Thumb 1") - .Should().Be(1); - result[0].Actors.Count(a => a.Name == "Name 2" && a.Role == "Role 2" && a.Thumb == "Thumb 2") - .Should().Be(1); - } + result.Count.Should().Be(1); + result[0].Actors.Count.Should().Be(2); + result[0].Actors.Count(a => a.Name == "Name 1" && a.Role == "Role 1" && a.Thumb == "Thumb 1") + .Should().Be(1); + result[0].Actors.Count(a => a.Name == "Name 2" && a.Role == "Role 2" && a.Thumb == "Thumb 2") + .Should().Be(1); + } - [Test] - public async Task Writers() - { - var reader = new EpisodeNfoReader(); - var stream = new MemoryStream( - Encoding.UTF8.GetBytes( - @" + [Test] + public async Task Writers() + { + var reader = new EpisodeNfoReader(); + var stream = new MemoryStream( + Encoding.UTF8.GetBytes( + @" Writer 1 @@ -202,21 +198,21 @@ namespace ErsatzTV.Core.Tests.Metadata.Nfo Writer 3 ")); - List result = await reader.Read(stream); + List result = await reader.Read(stream); - result.Count.Should().Be(2); - result.Count(nfo => nfo.Writers.Count == 1 && nfo.Writers[0] == "Writer 1").Should().Be(1); - result.Count(nfo => nfo.Writers.Count == 2 && nfo.Writers[0] == "Writer 2" && nfo.Writers[1] == "Writer 3") - .Should().Be(1); - } + result.Count.Should().Be(2); + result.Count(nfo => nfo.Writers.Count == 1 && nfo.Writers[0] == "Writer 1").Should().Be(1); + result.Count(nfo => nfo.Writers.Count == 2 && nfo.Writers[0] == "Writer 2" && nfo.Writers[1] == "Writer 3") + .Should().Be(1); + } - [Test] - public async Task Directors() - { - var reader = new EpisodeNfoReader(); - var stream = new MemoryStream( - Encoding.UTF8.GetBytes( - @" + [Test] + public async Task Directors() + { + var reader = new EpisodeNfoReader(); + var stream = new MemoryStream( + Encoding.UTF8.GetBytes( + @" Director 1 @@ -226,14 +222,13 @@ namespace ErsatzTV.Core.Tests.Metadata.Nfo Director 3 ")); - List result = await reader.Read(stream); + List result = await reader.Read(stream); - result.Count.Should().Be(2); - result.Count(nfo => nfo.Directors.Count == 1 && nfo.Directors[0] == "Director 1").Should().Be(1); - result.Count( - nfo => nfo.Directors.Count == 2 && nfo.Directors[0] == "Director 2" && - nfo.Directors[1] == "Director 3") - .Should().Be(1); - } + result.Count.Should().Be(2); + result.Count(nfo => nfo.Directors.Count == 1 && nfo.Directors[0] == "Director 1").Should().Be(1); + result.Count( + nfo => nfo.Directors.Count == 2 && nfo.Directors[0] == "Director 2" && + nfo.Directors[1] == "Director 3") + .Should().Be(1); } -} +} \ No newline at end of file diff --git a/ErsatzTV.Core.Tests/Plex/PlexPathReplacementServiceTests.cs b/ErsatzTV.Core.Tests/Plex/PlexPathReplacementServiceTests.cs index e4cbd33af..582a4f8c4 100644 --- a/ErsatzTV.Core.Tests/Plex/PlexPathReplacementServiceTests.cs +++ b/ErsatzTV.Core.Tests/Plex/PlexPathReplacementServiceTests.cs @@ -1,211 +1,207 @@ -using System.Collections.Generic; -using System.Runtime.InteropServices; -using System.Threading.Tasks; +using System.Runtime.InteropServices; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Runtime; using ErsatzTV.Core.Plex; using FluentAssertions; -using LanguageExt; using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; -namespace ErsatzTV.Core.Tests.Plex +namespace ErsatzTV.Core.Tests.Plex; + +[TestFixture] +public class PlexPathReplacementServiceTests { - [TestFixture] - public class PlexPathReplacementServiceTests + [Test] + public async Task PlexWindows_To_EtvWindows() { - [Test] - public async Task PlexWindows_To_EtvWindows() + var replacements = new List { - var replacements = new List + new() { - new() - { - Id = 1, - PlexPath = @"C:\Something\Some Shared Folder", - LocalPath = @"C:\Something Else\Some Shared Folder", - PlexMediaSource = new PlexMediaSource { Platform = "Windows" } - } - }; + Id = 1, + PlexPath = @"C:\Something\Some Shared Folder", + LocalPath = @"C:\Something Else\Some Shared Folder", + PlexMediaSource = new PlexMediaSource { Platform = "Windows" } + } + }; - var repo = new Mock(); - repo.Setup(x => x.GetPlexPathReplacementsByLibraryId(It.IsAny())).Returns(replacements.AsTask()); + var repo = new Mock(); + repo.Setup(x => x.GetPlexPathReplacementsByLibraryId(It.IsAny())).Returns(replacements.AsTask()); - var runtime = new Mock(); - runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(true); + var runtime = new Mock(); + runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(true); - var service = new PlexPathReplacementService( - repo.Object, - runtime.Object, - new Mock>().Object); + var service = new PlexPathReplacementService( + repo.Object, + runtime.Object, + new Mock>().Object); - string result = await service.GetReplacementPlexPath( - 0, - @"C:\Something\Some Shared Folder\Some Movie\Some Movie.mkv"); + string result = await service.GetReplacementPlexPath( + 0, + @"C:\Something\Some Shared Folder\Some Movie\Some Movie.mkv"); - result.Should().Be(@"C:\Something Else\Some Shared Folder\Some Movie\Some Movie.mkv"); - } - - [Test] - public async Task PlexWindows_To_EtvLinux() - { - var replacements = new List - { - new() - { - Id = 1, - PlexPath = @"C:\Something\Some Shared Folder", - LocalPath = @"/mnt/something else/Some Shared Folder", - PlexMediaSource = new PlexMediaSource { Platform = "Windows" } - } - }; - - var repo = new Mock(); - repo.Setup(x => x.GetPlexPathReplacementsByLibraryId(It.IsAny())).Returns(replacements.AsTask()); - - var runtime = new Mock(); - runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(false); - - var service = new PlexPathReplacementService( - repo.Object, - runtime.Object, - new Mock>().Object); - - string result = await service.GetReplacementPlexPath( - 0, - @"C:\Something\Some Shared Folder\Some Movie\Some Movie.mkv"); - - result.Should().Be(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv"); - } - - [Test] - public async Task PlexWindows_To_EtvLinux_UncPath() - { - var replacements = new List - { - new() - { - Id = 1, - PlexPath = @"\\192.168.1.100\Something\Some Shared Folder", - LocalPath = @"/mnt/something else/Some Shared Folder", - PlexMediaSource = new PlexMediaSource { Platform = "Windows" } - } - }; - - var repo = new Mock(); - repo.Setup(x => x.GetPlexPathReplacementsByLibraryId(It.IsAny())).Returns(replacements.AsTask()); - - var runtime = new Mock(); - runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(false); - - var service = new PlexPathReplacementService( - repo.Object, - runtime.Object, - new Mock>().Object); - - string result = await service.GetReplacementPlexPath( - 0, - @"\\192.168.1.100\Something\Some Shared Folder\Some Movie\Some Movie.mkv"); - - result.Should().Be(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv"); - } - - [Test] - public async Task PlexWindows_To_EtvLinux_UncPathWithTrailingSlash() - { - var replacements = new List - { - new() - { - Id = 1, - PlexPath = @"\\192.168.1.100\Something\Some Shared Folder\", - LocalPath = @"/mnt/something else/Some Shared Folder/", - PlexMediaSource = new PlexMediaSource { Platform = "Windows" } - } - }; - - var repo = new Mock(); - repo.Setup(x => x.GetPlexPathReplacementsByLibraryId(It.IsAny())).Returns(replacements.AsTask()); - - var runtime = new Mock(); - runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(false); - - var service = new PlexPathReplacementService( - repo.Object, - runtime.Object, - new Mock>().Object); - - string result = await service.GetReplacementPlexPath( - 0, - @"\\192.168.1.100\Something\Some Shared Folder\Some Movie\Some Movie.mkv"); - - result.Should().Be(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv"); - } - - [Test] - public async Task PlexLinux_To_EtvWindows() - { - var replacements = new List - { - new() - { - Id = 1, - PlexPath = @"/mnt/something/Some Shared Folder", - LocalPath = @"C:\Something Else\Some Shared Folder", - PlexMediaSource = new PlexMediaSource { Platform = "Linux" } - } - }; - - var repo = new Mock(); - repo.Setup(x => x.GetPlexPathReplacementsByLibraryId(It.IsAny())).Returns(replacements.AsTask()); - - var runtime = new Mock(); - runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(true); - - var service = new PlexPathReplacementService( - repo.Object, - runtime.Object, - new Mock>().Object); - - string result = await service.GetReplacementPlexPath( - 0, - @"/mnt/something/Some Shared Folder/Some Movie/Some Movie.mkv"); - - result.Should().Be(@"C:\Something Else\Some Shared Folder\Some Movie\Some Movie.mkv"); - } - - [Test] - public async Task PlexLinux_To_EtvLinux() - { - var replacements = new List - { - new() - { - Id = 1, - PlexPath = @"/mnt/something/Some Shared Folder", - LocalPath = @"/mnt/something else/Some Shared Folder", - PlexMediaSource = new PlexMediaSource { Platform = "Linux" } - } - }; - - var repo = new Mock(); - repo.Setup(x => x.GetPlexPathReplacementsByLibraryId(It.IsAny())).Returns(replacements.AsTask()); - - var runtime = new Mock(); - runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(false); - - var service = new PlexPathReplacementService( - repo.Object, - runtime.Object, - new Mock>().Object); - - string result = await service.GetReplacementPlexPath( - 0, - @"/mnt/something/Some Shared Folder/Some Movie/Some Movie.mkv"); - - result.Should().Be(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv"); - } + result.Should().Be(@"C:\Something Else\Some Shared Folder\Some Movie\Some Movie.mkv"); } -} + + [Test] + public async Task PlexWindows_To_EtvLinux() + { + var replacements = new List + { + new() + { + Id = 1, + PlexPath = @"C:\Something\Some Shared Folder", + LocalPath = @"/mnt/something else/Some Shared Folder", + PlexMediaSource = new PlexMediaSource { Platform = "Windows" } + } + }; + + var repo = new Mock(); + repo.Setup(x => x.GetPlexPathReplacementsByLibraryId(It.IsAny())).Returns(replacements.AsTask()); + + var runtime = new Mock(); + runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(false); + + var service = new PlexPathReplacementService( + repo.Object, + runtime.Object, + new Mock>().Object); + + string result = await service.GetReplacementPlexPath( + 0, + @"C:\Something\Some Shared Folder\Some Movie\Some Movie.mkv"); + + result.Should().Be(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv"); + } + + [Test] + public async Task PlexWindows_To_EtvLinux_UncPath() + { + var replacements = new List + { + new() + { + Id = 1, + PlexPath = @"\\192.168.1.100\Something\Some Shared Folder", + LocalPath = @"/mnt/something else/Some Shared Folder", + PlexMediaSource = new PlexMediaSource { Platform = "Windows" } + } + }; + + var repo = new Mock(); + repo.Setup(x => x.GetPlexPathReplacementsByLibraryId(It.IsAny())).Returns(replacements.AsTask()); + + var runtime = new Mock(); + runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(false); + + var service = new PlexPathReplacementService( + repo.Object, + runtime.Object, + new Mock>().Object); + + string result = await service.GetReplacementPlexPath( + 0, + @"\\192.168.1.100\Something\Some Shared Folder\Some Movie\Some Movie.mkv"); + + result.Should().Be(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv"); + } + + [Test] + public async Task PlexWindows_To_EtvLinux_UncPathWithTrailingSlash() + { + var replacements = new List + { + new() + { + Id = 1, + PlexPath = @"\\192.168.1.100\Something\Some Shared Folder\", + LocalPath = @"/mnt/something else/Some Shared Folder/", + PlexMediaSource = new PlexMediaSource { Platform = "Windows" } + } + }; + + var repo = new Mock(); + repo.Setup(x => x.GetPlexPathReplacementsByLibraryId(It.IsAny())).Returns(replacements.AsTask()); + + var runtime = new Mock(); + runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(false); + + var service = new PlexPathReplacementService( + repo.Object, + runtime.Object, + new Mock>().Object); + + string result = await service.GetReplacementPlexPath( + 0, + @"\\192.168.1.100\Something\Some Shared Folder\Some Movie\Some Movie.mkv"); + + result.Should().Be(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv"); + } + + [Test] + public async Task PlexLinux_To_EtvWindows() + { + var replacements = new List + { + new() + { + Id = 1, + PlexPath = @"/mnt/something/Some Shared Folder", + LocalPath = @"C:\Something Else\Some Shared Folder", + PlexMediaSource = new PlexMediaSource { Platform = "Linux" } + } + }; + + var repo = new Mock(); + repo.Setup(x => x.GetPlexPathReplacementsByLibraryId(It.IsAny())).Returns(replacements.AsTask()); + + var runtime = new Mock(); + runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(true); + + var service = new PlexPathReplacementService( + repo.Object, + runtime.Object, + new Mock>().Object); + + string result = await service.GetReplacementPlexPath( + 0, + @"/mnt/something/Some Shared Folder/Some Movie/Some Movie.mkv"); + + result.Should().Be(@"C:\Something Else\Some Shared Folder\Some Movie\Some Movie.mkv"); + } + + [Test] + public async Task PlexLinux_To_EtvLinux() + { + var replacements = new List + { + new() + { + Id = 1, + PlexPath = @"/mnt/something/Some Shared Folder", + LocalPath = @"/mnt/something else/Some Shared Folder", + PlexMediaSource = new PlexMediaSource { Platform = "Linux" } + } + }; + + var repo = new Mock(); + repo.Setup(x => x.GetPlexPathReplacementsByLibraryId(It.IsAny())).Returns(replacements.AsTask()); + + var runtime = new Mock(); + runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(false); + + var service = new PlexPathReplacementService( + repo.Object, + runtime.Object, + new Mock>().Object); + + string result = await service.GetReplacementPlexPath( + 0, + @"/mnt/something/Some Shared Folder/Some Movie/Some Movie.mkv"); + + result.Should().Be(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv"); + } +} \ No newline at end of file diff --git a/ErsatzTV.Core.Tests/Scheduling/ChronologicalContentTests.cs b/ErsatzTV.Core.Tests/Scheduling/ChronologicalContentTests.cs index d4f1eef18..387f24ab9 100644 --- a/ErsatzTV.Core.Tests/Scheduling/ChronologicalContentTests.cs +++ b/ErsatzTV.Core.Tests/Scheduling/ChronologicalContentTests.cs @@ -1,92 +1,87 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; using ErsatzTV.Core.Scheduling; using FluentAssertions; using NUnit.Framework; -using static LanguageExt.Prelude; -namespace ErsatzTV.Core.Tests.Scheduling +namespace ErsatzTV.Core.Tests.Scheduling; + +[TestFixture] +public class ChronologicalContentTests { - [TestFixture] - public class ChronologicalContentTests + [Test] + public void Episodes_Should_Sort_By_Aired() { - [Test] - public void Episodes_Should_Sort_By_Aired() + List contents = Episodes(10); + var state = new CollectionEnumeratorState(); + + var chronologicalContent = new ChronologicalMediaCollectionEnumerator(contents, state); + + for (var i = 1; i <= 10; i++) { - List contents = Episodes(10); - var state = new CollectionEnumeratorState(); - - var chronologicalContent = new ChronologicalMediaCollectionEnumerator(contents, state); - - for (var i = 1; i <= 10; i++) - { - chronologicalContent.Current.IsSome.Should().BeTrue(); - chronologicalContent.Current.Map(x => x.Id).IfNone(-1).Should().Be(i); - chronologicalContent.MoveNext(); - } + chronologicalContent.Current.IsSome.Should().BeTrue(); + chronologicalContent.Current.Map(x => x.Id).IfNone(-1).Should().Be(i); + chronologicalContent.MoveNext(); } - - [Test] - public void State_Index_Should_Increment() - { - List contents = Episodes(10); - var state = new CollectionEnumeratorState(); - - var chronologicalContent = new ChronologicalMediaCollectionEnumerator(contents, state); - - for (var i = 0; i < 10; i++) - { - chronologicalContent.State.Index.Should().Be(i % 10); - chronologicalContent.MoveNext(); - } - } - - [Test] - public void State_Should_Impact_Iterator_Start() - { - List contents = Episodes(10); - var state = new CollectionEnumeratorState { Index = 5 }; - - var chronologicalContent = new ChronologicalMediaCollectionEnumerator(contents, state); - - for (var i = 6; i <= 10; i++) - { - chronologicalContent.Current.IsSome.Should().BeTrue(); - chronologicalContent.Current.Map(x => x.Id).IfNone(-1).Should().Be(i); - chronologicalContent.State.Index.Should().Be(i - 1); - chronologicalContent.MoveNext(); - } - } - - [Test] - [Timeout(1000)] - public void State_Should_Reset_When_Invalid() - { - List contents = Episodes(10); - var state = new CollectionEnumeratorState { Index = 10 }; - - var chronologicalContent = new ChronologicalMediaCollectionEnumerator(contents, state); - - chronologicalContent.State.Index.Should().Be(0); - chronologicalContent.State.Seed.Should().Be(0); - } - - private static List Episodes(int count) => - Range(1, count).Map( - i => (MediaItem) new Episode - { - Id = i, - EpisodeMetadata = new List - { - new() - { - ReleaseDate = new DateTime(2020, 1, i) - } - } - }) - .Reverse() - .ToList(); } -} + + [Test] + public void State_Index_Should_Increment() + { + List contents = Episodes(10); + var state = new CollectionEnumeratorState(); + + var chronologicalContent = new ChronologicalMediaCollectionEnumerator(contents, state); + + for (var i = 0; i < 10; i++) + { + chronologicalContent.State.Index.Should().Be(i % 10); + chronologicalContent.MoveNext(); + } + } + + [Test] + public void State_Should_Impact_Iterator_Start() + { + List contents = Episodes(10); + var state = new CollectionEnumeratorState { Index = 5 }; + + var chronologicalContent = new ChronologicalMediaCollectionEnumerator(contents, state); + + for (var i = 6; i <= 10; i++) + { + chronologicalContent.Current.IsSome.Should().BeTrue(); + chronologicalContent.Current.Map(x => x.Id).IfNone(-1).Should().Be(i); + chronologicalContent.State.Index.Should().Be(i - 1); + chronologicalContent.MoveNext(); + } + } + + [Test] + [Timeout(1000)] + public void State_Should_Reset_When_Invalid() + { + List contents = Episodes(10); + var state = new CollectionEnumeratorState { Index = 10 }; + + var chronologicalContent = new ChronologicalMediaCollectionEnumerator(contents, state); + + chronologicalContent.State.Index.Should().Be(0); + chronologicalContent.State.Seed.Should().Be(0); + } + + private static List Episodes(int count) => + Range(1, count).Map( + i => (MediaItem) new Episode + { + Id = i, + EpisodeMetadata = new List + { + new() + { + ReleaseDate = new DateTime(2020, 1, i) + } + } + }) + .Reverse() + .ToList(); +} \ No newline at end of file diff --git a/ErsatzTV.Core.Tests/Scheduling/CustomOrderContentTests.cs b/ErsatzTV.Core.Tests/Scheduling/CustomOrderContentTests.cs index a03a1522f..27249f1ee 100644 --- a/ErsatzTV.Core.Tests/Scheduling/CustomOrderContentTests.cs +++ b/ErsatzTV.Core.Tests/Scheduling/CustomOrderContentTests.cs @@ -1,100 +1,95 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; using ErsatzTV.Core.Scheduling; using FluentAssertions; using NUnit.Framework; -using static LanguageExt.Prelude; -namespace ErsatzTV.Core.Tests.Scheduling +namespace ErsatzTV.Core.Tests.Scheduling; + +public class CustomOrderContentTests { - public class CustomOrderContentTests + [Test] + public void MediaItems_Should_Sort_By_CustomOrder() { - [Test] - public void MediaItems_Should_Sort_By_CustomOrder() + Collection collection = CreateCollection(10); + List contents = Episodes(10); + var state = new CollectionEnumeratorState(); + + var customOrderContent = new CustomOrderCollectionEnumerator(collection, contents, state); + + for (var i = 10; i >= 1; i--) { - Collection collection = CreateCollection(10); - List contents = Episodes(10); - var state = new CollectionEnumeratorState(); - - var customOrderContent = new CustomOrderCollectionEnumerator(collection, contents, state); - - for (var i = 10; i >= 1; i--) - { - customOrderContent.Current.IsSome.Should().BeTrue(); - customOrderContent.Current.Map(x => x.Id).IfNone(-1).Should().Be(i); - customOrderContent.MoveNext(); - } + customOrderContent.Current.IsSome.Should().BeTrue(); + customOrderContent.Current.Map(x => x.Id).IfNone(-1).Should().Be(i); + customOrderContent.MoveNext(); } - - [Test] - public void State_Index_Should_Increment() - { - Collection collection = CreateCollection(10); - List contents = Episodes(10); - var state = new CollectionEnumeratorState(); - - var customOrderContent = new CustomOrderCollectionEnumerator(collection, contents, state); - - for (var i = 0; i < 10; i++) - { - customOrderContent.State.Index.Should().Be(i % 10); - customOrderContent.MoveNext(); - } - } - - [Test] - public void State_Should_Impact_Iterator_Start() - { - Collection collection = CreateCollection(10); - List contents = Episodes(10); - var state = new CollectionEnumeratorState { Index = 5 }; - - var customOrderContent = new CustomOrderCollectionEnumerator(collection, contents, state); - - for (var i = 5; i >= 1; i--) - { - customOrderContent.Current.IsSome.Should().BeTrue(); - customOrderContent.Current.Map(x => x.Id).IfNone(-1).Should().Be(i); - customOrderContent.State.Index.Should().Be(5 - i + 5); // 5 through 10 - customOrderContent.MoveNext(); - } - } - - private static Collection CreateCollection(int episodeCount) - { - var collection = new Collection { CollectionItems = new List() }; - - for (var i = 1; i <= episodeCount; i++) - { - collection.CollectionItems.Add( - new CollectionItem - { - MediaItemId = i, - // reverse order - CustomIndex = episodeCount - i - }); - } - - return collection; - } - - - private static List Episodes(int count) => - Range(1, count).Map( - i => (MediaItem) new Episode - { - Id = i, - EpisodeMetadata = new List - { - new() - { - ReleaseDate = new DateTime(2020, 1, i) - } - } - }) - .Reverse() - .ToList(); } -} + + [Test] + public void State_Index_Should_Increment() + { + Collection collection = CreateCollection(10); + List contents = Episodes(10); + var state = new CollectionEnumeratorState(); + + var customOrderContent = new CustomOrderCollectionEnumerator(collection, contents, state); + + for (var i = 0; i < 10; i++) + { + customOrderContent.State.Index.Should().Be(i % 10); + customOrderContent.MoveNext(); + } + } + + [Test] + public void State_Should_Impact_Iterator_Start() + { + Collection collection = CreateCollection(10); + List contents = Episodes(10); + var state = new CollectionEnumeratorState { Index = 5 }; + + var customOrderContent = new CustomOrderCollectionEnumerator(collection, contents, state); + + for (var i = 5; i >= 1; i--) + { + customOrderContent.Current.IsSome.Should().BeTrue(); + customOrderContent.Current.Map(x => x.Id).IfNone(-1).Should().Be(i); + customOrderContent.State.Index.Should().Be(5 - i + 5); // 5 through 10 + customOrderContent.MoveNext(); + } + } + + private static Collection CreateCollection(int episodeCount) + { + var collection = new Collection { CollectionItems = new List() }; + + for (var i = 1; i <= episodeCount; i++) + { + collection.CollectionItems.Add( + new CollectionItem + { + MediaItemId = i, + // reverse order + CustomIndex = episodeCount - i + }); + } + + return collection; + } + + + private static List Episodes(int count) => + Range(1, count).Map( + i => (MediaItem) new Episode + { + Id = i, + EpisodeMetadata = new List + { + new() + { + ReleaseDate = new DateTime(2020, 1, i) + } + } + }) + .Reverse() + .ToList(); +} \ No newline at end of file diff --git a/ErsatzTV.Core.Tests/Scheduling/MultiPartEpisodeGrouperTests.cs b/ErsatzTV.Core.Tests/Scheduling/MultiPartEpisodeGrouperTests.cs index 2119d8ec5..a1c9c132e 100644 --- a/ErsatzTV.Core.Tests/Scheduling/MultiPartEpisodeGrouperTests.cs +++ b/ErsatzTV.Core.Tests/Scheduling/MultiPartEpisodeGrouperTests.cs @@ -1,337 +1,333 @@ -using System; -using System.Collections.Generic; -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; using ErsatzTV.Core.Scheduling; using FluentAssertions; using NUnit.Framework; -using static LanguageExt.Prelude; -namespace ErsatzTV.Core.Tests.Scheduling +namespace ErsatzTV.Core.Tests.Scheduling; + +public class MultiPartEpisodeGrouperTests { - public class MultiPartEpisodeGrouperTests + [Test] + [TestCase("Episode 1", "Episode 2 (1)", "Episode 3 (2)", "Episode 4")] + [TestCase("Episode 1 - More", "Episode 2 (1) - Title", "Episode 3 (2) - After", "Episode 4 - Dash")] + [TestCase("Episode 1", "Episode 2 Part 1", "Episode 3 Part 2", "Episode 4")] + [TestCase("Episode 1", "Episode 2 (Part 1)", "Episode 3 (Part 2)", "Episode 4")] + public void NotGrouped_Grouped_NotGrouped(string one, string two, string three, string four) { - [Test] - [TestCase("Episode 1", "Episode 2 (1)", "Episode 3 (2)", "Episode 4")] - [TestCase("Episode 1 - More", "Episode 2 (1) - Title", "Episode 3 (2) - After", "Episode 4 - Dash")] - [TestCase("Episode 1", "Episode 2 Part 1", "Episode 3 Part 2", "Episode 4")] - [TestCase("Episode 1", "Episode 2 (Part 1)", "Episode 3 (Part 2)", "Episode 4")] - public void NotGrouped_Grouped_NotGrouped(string one, string two, string three, string four) + var mediaItems = new List { - var mediaItems = new List - { - NamedEpisode(one, 1, 1, 1), - NamedEpisode(two, 1, 1, 2), - NamedEpisode(three, 1, 1, 3), - NamedEpisode(four, 1, 1, 4) - }; + NamedEpisode(one, 1, 1, 1), + NamedEpisode(two, 1, 1, 2), + NamedEpisode(three, 1, 1, 3), + NamedEpisode(four, 1, 1, 4) + }; - List result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems, false); + List result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems, false); - result.Count.Should().Be(3); - ShouldHaveOneItem(result, mediaItems[0]); - ShouldHaveTwoItems(result, mediaItems[1], mediaItems[2]); - ShouldHaveOneItem(result, mediaItems[3]); - } - - [Test] - [TestCase("Episode 1 (1)", "Episode 2 - Part 2", "Episode 3")] - [TestCase("Episode 1 Part 1", "Episode 2 (2) - More", "Episode 3 - After")] - [TestCase("Episode 1 Part 1", "Episode 2 (II)", "Episode 3")] - [TestCase("Episode 1 Part One", "Episode 2 (II)", "Episode 3")] - [TestCase("Episode 1 (1)", "Episode 2 (Part 2)", "Episode 3")] - public void MixedNaming_Group(string one, string two, string three) - { - var mediaItems = new List - { - NamedEpisode(one, 1, 1, 1), - NamedEpisode(two, 1, 1, 2), - NamedEpisode(three, 1, 1, 3) - }; - - List result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems, false); - - result.Count.Should().Be(2); - ShouldHaveTwoItems(result, mediaItems[0], mediaItems[1]); - ShouldHaveOneItem(result, mediaItems[2]); - } - - [Test] - [TestCase("Episode 1 (5)", "Episode 2 - (6)", "Episode 3")] - [TestCase("Episode 1 Part 5", "Episode 2 Part 6", "Episode 3 - After")] - [TestCase("Episode 1 Part (V)", "Episode 2 (VI)", "Episode 3")] - [TestCase("Episode 1 (Part 5)", "Episode 2 (Part 6)", "Episode 3")] - public void Only_Later_Parts(string one, string two, string three) - { - var mediaItems = new List - { - NamedEpisode(one, 1, 1, 1), - NamedEpisode(two, 1, 1, 2), - NamedEpisode(three, 1, 1, 3) - }; - - List result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems, false); - - result.Count.Should().Be(2); - ShouldHaveTwoItems(result, mediaItems[0], mediaItems[1]); - ShouldHaveOneItem(result, mediaItems[2]); - } - - [Test] - [TestCase("Episode 1 (1)", "Episode 2 (2)", "Episode 3")] - [TestCase("Episode 1 (1) - More", "Episode 2 (2) - Title", "Episode 3 - After")] - [TestCase("Episode 1 Part 1", "Episode 2 Part 2", "Episode 3")] - [TestCase("Episode 1 (Part 1)", "Episode 2 (Part 2)", "Episode 3")] - public void Grouped_NotGrouped(string one, string two, string three) - { - var mediaItems = new List - { - NamedEpisode(one, 1, 1, 1), - NamedEpisode(two, 1, 1, 2), - NamedEpisode(three, 1, 1, 3) - }; - - List result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems, false); - - result.Count.Should().Be(2); - ShouldHaveTwoItems(result, mediaItems[0], mediaItems[1]); - ShouldHaveOneItem(result, mediaItems[2]); - } - - [Test] - [TestCase("Episode 1 (1)", "Episode 2 (2)", "Episode 3", "Episode 4 (1)", "Episode 5 (2)")] - [TestCase( - "Episode 1 (1) - More", - "Episode 2 (2) - Title", - "Episode 3 - After", - "Episode 4 (1) - Dash", - "Episode 5 (2) - Again")] - [TestCase("Episode 1 Part 1", "Episode 2 Part 2", "Episode 3", "Episode 4 Part 1", "Episode 5 Part 2")] - [TestCase("Episode 1 (Part 1)", "Episode 2 (Part 2)", "Episode 3", "Episode 4 (Part 1)", "Episode 5 (Part 2)")] - public void Grouped_NotGrouped_Grouped(string one, string two, string three, string four, string five) - { - var mediaItems = new List - { - NamedEpisode(one, 1, 1, 1), - NamedEpisode(two, 1, 1, 2), - NamedEpisode(three, 1, 1, 3), - NamedEpisode(four, 1, 1, 4), - NamedEpisode(five, 1, 1, 5) - }; - - List result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems, false); - - result.Count.Should().Be(3); - ShouldHaveTwoItems(result, mediaItems[0], mediaItems[1]); - ShouldHaveOneItem(result, mediaItems[2]); - ShouldHaveTwoItems(result, mediaItems[3], mediaItems[4]); - } - - [Test] - [TestCase("Episode 1 (1)", "Episode 2 (2)", "Episode 3 (1)", "Episode 4 (2)")] - [TestCase("Episode 1 (1) - More", "Episode 2 (2) - Title", "Episode 3 (1) - After", "Episode 4 (2) - Dash")] - [TestCase("Episode 1 Part 1", "Episode 2 Part 2", "Episode 3 Part 1", "Episode 4 Part 2")] - [TestCase("Episode 1 (Part 1)", "Episode 2 (Part 2)", "Episode 3 (Part 1)", "Episode 4 (Part 2)")] - public void Grouped_Grouped(string one, string two, string three, string four) - { - var mediaItems = new List - { - NamedEpisode(one, 1, 1, 1), - NamedEpisode(two, 1, 1, 2), - NamedEpisode(three, 1, 1, 3), - NamedEpisode(four, 1, 1, 4) - }; - - List result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems, false); - - result.Count.Should().Be(2); - ShouldHaveTwoItems(result, mediaItems[0], mediaItems[1]); - ShouldHaveTwoItems(result, mediaItems[2], mediaItems[3]); - } - - [Test] - [TestCase("Episode 1", "Episode 2 (2)", "Episode 3 (1)", "Episode 4 (2)")] - [TestCase("Episode 1 - More", "Episode 2 (2) - Title", "Episode 3 (1) - After", "Episode 4 (2) - Dash")] - [TestCase("Episode 1", "Episode 2 Part 2", "Episode 3 Part 1", "Episode 4 Part 2")] - [TestCase("Episode 1", "Episode 2 (Part 2)", "Episode 3 (Part 1)", "Episode 4 (Part 2)")] - public void Part2_Without_Part1(string one, string two, string three, string four) - { - var mediaItems = new List - { - NamedEpisode(one, 1, 1, 1), - NamedEpisode(two, 1, 1, 2), - NamedEpisode(three, 1, 1, 3), - NamedEpisode(four, 1, 1, 4) - }; - - List result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems, false); - - result.Count.Should().Be(3); - ShouldHaveOneItem(result, mediaItems[0]); - ShouldHaveOneItem(result, mediaItems[1]); - ShouldHaveTwoItems(result, mediaItems[2], mediaItems[3]); - } - - [Test] - [TestCase("Episode 1", "Episode 2 (2)", "Episode 3 (3)", "Episode 4")] - [TestCase("Episode 1 - More", "Episode 2 (2) - Title", "Episode 3 (3) - After", "Episode 4 - Dash")] - [TestCase("Episode 1", "Episode 2 Part 2", "Episode 3 Part 3", "Episode 4")] - [TestCase("Episode 1", "Episode 2 (Part 2)", "Episode 3 (Part 3)", "Episode 4")] - public void Part2And3_Without_Part1(string one, string two, string three, string four) - { - var mediaItems = new List - { - NamedEpisode(one, 1, 1, 1), - NamedEpisode(two, 1, 1, 2), - NamedEpisode(three, 1, 1, 3), - NamedEpisode(four, 1, 1, 4) - }; - - List result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems, false); - - result.Count.Should().Be(3); - ShouldHaveOneItem(result, mediaItems[0]); - ShouldHaveTwoItems(result, mediaItems[1], mediaItems[2]); - ShouldHaveOneItem(result, mediaItems[3]); - } - - [Test] - [TestCase("Episode 1 (1)", "Episode 3 (3)", "Episode 4", "Episode 5")] - [TestCase("Episode 1 (1) - More", "Episode 3 (3) - Title", "Episode 4 - After", "Episode 5 - Dash")] - [TestCase("Episode 1 Part 1", "Episode 3 Part 3", "Episode 4", "Episode 5")] - [TestCase("Episode 1 (Part 1)", "Episode 3 (Part 3)", "Episode 4", "Episode 5")] - public void Skip_Part(string one, string two, string three, string four) - { - var mediaItems = new List - { - NamedEpisode(one, 1, 1, 1), - NamedEpisode(two, 1, 1, 3), - NamedEpisode(three, 1, 1, 4), - NamedEpisode(four, 1, 1, 5) - }; - - List result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems, false); - - result.Count.Should().Be(4); - ShouldHaveOneItem(result, mediaItems[0]); - ShouldHaveOneItem(result, mediaItems[1]); - ShouldHaveOneItem(result, mediaItems[2]); - ShouldHaveOneItem(result, mediaItems[3]); - } - - [Test] - [TestCase("Episode 1 (1)", "Episode 3 (1)", "Episode 4 (2)", "Episode 5")] - [TestCase("Episode 1 (1) - More", "Episode 3 (1) - Title", "Episode 4 (2) - After", "Episode 5 - Dash")] - [TestCase("Episode 1 Part 1", "Episode 3 Part 1", "Episode 4 Part 2", "Episode 5")] - [TestCase("Episode 1 (Part 1)", "Episode 3 (Part 1)", "Episode 4 (Part 2)", "Episode 5")] - public void Repeat_Part(string one, string two, string three, string four) - { - var mediaItems = new List - { - NamedEpisode(one, 1, 1, 1), - NamedEpisode(two, 1, 1, 3), - NamedEpisode(three, 1, 1, 4), - NamedEpisode(four, 1, 1, 5) - }; - - List result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems, false); - - result.Count.Should().Be(3); - ShouldHaveOneItem(result, mediaItems[0]); - ShouldHaveTwoItems(result, mediaItems[1], mediaItems[2]); - ShouldHaveOneItem(result, mediaItems[3]); - } - - [Test] - [TestCase("S1 Episode 1 (1)", "S2 Episode 3 (1)", "S1 Episode 2 (2)", "S1 Episode 5")] - [TestCase( - "S1 Episode 1 (1) - More", - "S2 Episode 3 (1) - Title", - "S1 Episode 2 (2) - After", - "S1 Episode 5 - Dash")] - [TestCase("S1 Episode 1 Part 1", "S2 Episode 3 Part 1", "S1 Episode 2 Part 2", "S1 Episode 5")] - [TestCase("S1 Episode 1 (Part 1)", "S2 Episode 3 (Part 1)", "S1 Episode 2 (Part 2)", "S1 Episode 5")] - public void Mixed_Shows_Chronologically(string one, string two, string three, string four) - { - var mediaItems = new List - { - NamedEpisode(one, 1, 1, 1, new DateTime(2020, 1, 1)), - NamedEpisode(two, 2, 1, 3, new DateTime(2020, 1, 2)), - NamedEpisode(three, 1, 1, 2, new DateTime(2020, 1, 3)), - NamedEpisode(four, 1, 1, 5, new DateTime(2020, 1, 4)) - }; - - List result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems, false); - - result.Count.Should().Be(3); - ShouldHaveTwoItems(result, mediaItems[0], mediaItems[2]); - ShouldHaveOneItem(result, mediaItems[1]); - ShouldHaveOneItem(result, mediaItems[3]); - } - - [Test] - [TestCase("S1 Episode 1 (1)", "S2 Episode 3 (2)", "S1 Episode 2 (3)", "S1 Episode 5")] - [TestCase( - "S1 Episode 1 (1) - More", - "S2 Episode 3 (2) - Title", - "S1 Episode 2 (3) - After", - "S1 Episode 5 - Dash")] - [TestCase("S1 Episode 1 Part 1", "S2 Episode 3 Part 2", "S1 Episode 2 Part 3", "S1 Episode 5")] - [TestCase("S1 Episode 1 (Part 1)", "S2 Episode 3 (Part 2)", "S1 Episode 2 (Part 3)", "S1 Episode 5")] - public void Mixed_Shows_Chronologically_Crossover(string one, string two, string three, string four) - { - var mediaItems = new List - { - NamedEpisode(one, 1, 1, 1, new DateTime(2020, 1, 1)), - NamedEpisode(two, 2, 1, 3, new DateTime(2020, 1, 2)), - NamedEpisode(three, 1, 1, 2, new DateTime(2020, 1, 3)), - NamedEpisode(four, 1, 1, 5, new DateTime(2020, 1, 4)) - }; - - List result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems, true); - - result.Count.Should().Be(2); - ShouldHaveMultipleItems(result, mediaItems[0], new List { mediaItems[1], mediaItems[2] }); - ShouldHaveOneItem(result, mediaItems[3]); - } - - private static Episode NamedEpisode( - string title, - int showId, - int season, - int episode, - DateTime? releaseDate = null) => - new() - { - EpisodeMetadata = new List - { - new() { Title = title, ReleaseDate = releaseDate, EpisodeNumber = episode } - }, - Season = new Season - { - SeasonNumber = season, - Show = new Show { Id = showId }, - ShowId = showId - } - }; - - private static void ShouldHaveOneItem(IEnumerable result, MediaItem item) => - result.Filter(g => g.First == item && Optional(g.Additional).Flatten().HeadOrNone() == None) - .Should().HaveCount(1); - - private static void ShouldHaveTwoItems( - IEnumerable result, - MediaItem first, - MediaItem additional) => - result.Filter(g => g.First == first && Optional(g.Additional).Flatten().HeadOrNone() == Some(additional)) - .Should().HaveCount(1); - - private static void ShouldHaveMultipleItems( - IEnumerable result, - MediaItem first, - List additional) => - result.Filter( - g => g.First == first && g.Additional != null && g.Additional.Count == additional.Count && - additional.ForAll(g.Additional.Contains)) - .Should().HaveCount(1); + result.Count.Should().Be(3); + ShouldHaveOneItem(result, mediaItems[0]); + ShouldHaveTwoItems(result, mediaItems[1], mediaItems[2]); + ShouldHaveOneItem(result, mediaItems[3]); } -} + + [Test] + [TestCase("Episode 1 (1)", "Episode 2 - Part 2", "Episode 3")] + [TestCase("Episode 1 Part 1", "Episode 2 (2) - More", "Episode 3 - After")] + [TestCase("Episode 1 Part 1", "Episode 2 (II)", "Episode 3")] + [TestCase("Episode 1 Part One", "Episode 2 (II)", "Episode 3")] + [TestCase("Episode 1 (1)", "Episode 2 (Part 2)", "Episode 3")] + public void MixedNaming_Group(string one, string two, string three) + { + var mediaItems = new List + { + NamedEpisode(one, 1, 1, 1), + NamedEpisode(two, 1, 1, 2), + NamedEpisode(three, 1, 1, 3) + }; + + List result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems, false); + + result.Count.Should().Be(2); + ShouldHaveTwoItems(result, mediaItems[0], mediaItems[1]); + ShouldHaveOneItem(result, mediaItems[2]); + } + + [Test] + [TestCase("Episode 1 (5)", "Episode 2 - (6)", "Episode 3")] + [TestCase("Episode 1 Part 5", "Episode 2 Part 6", "Episode 3 - After")] + [TestCase("Episode 1 Part (V)", "Episode 2 (VI)", "Episode 3")] + [TestCase("Episode 1 (Part 5)", "Episode 2 (Part 6)", "Episode 3")] + public void Only_Later_Parts(string one, string two, string three) + { + var mediaItems = new List + { + NamedEpisode(one, 1, 1, 1), + NamedEpisode(two, 1, 1, 2), + NamedEpisode(three, 1, 1, 3) + }; + + List result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems, false); + + result.Count.Should().Be(2); + ShouldHaveTwoItems(result, mediaItems[0], mediaItems[1]); + ShouldHaveOneItem(result, mediaItems[2]); + } + + [Test] + [TestCase("Episode 1 (1)", "Episode 2 (2)", "Episode 3")] + [TestCase("Episode 1 (1) - More", "Episode 2 (2) - Title", "Episode 3 - After")] + [TestCase("Episode 1 Part 1", "Episode 2 Part 2", "Episode 3")] + [TestCase("Episode 1 (Part 1)", "Episode 2 (Part 2)", "Episode 3")] + public void Grouped_NotGrouped(string one, string two, string three) + { + var mediaItems = new List + { + NamedEpisode(one, 1, 1, 1), + NamedEpisode(two, 1, 1, 2), + NamedEpisode(three, 1, 1, 3) + }; + + List result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems, false); + + result.Count.Should().Be(2); + ShouldHaveTwoItems(result, mediaItems[0], mediaItems[1]); + ShouldHaveOneItem(result, mediaItems[2]); + } + + [Test] + [TestCase("Episode 1 (1)", "Episode 2 (2)", "Episode 3", "Episode 4 (1)", "Episode 5 (2)")] + [TestCase( + "Episode 1 (1) - More", + "Episode 2 (2) - Title", + "Episode 3 - After", + "Episode 4 (1) - Dash", + "Episode 5 (2) - Again")] + [TestCase("Episode 1 Part 1", "Episode 2 Part 2", "Episode 3", "Episode 4 Part 1", "Episode 5 Part 2")] + [TestCase("Episode 1 (Part 1)", "Episode 2 (Part 2)", "Episode 3", "Episode 4 (Part 1)", "Episode 5 (Part 2)")] + public void Grouped_NotGrouped_Grouped(string one, string two, string three, string four, string five) + { + var mediaItems = new List + { + NamedEpisode(one, 1, 1, 1), + NamedEpisode(two, 1, 1, 2), + NamedEpisode(three, 1, 1, 3), + NamedEpisode(four, 1, 1, 4), + NamedEpisode(five, 1, 1, 5) + }; + + List result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems, false); + + result.Count.Should().Be(3); + ShouldHaveTwoItems(result, mediaItems[0], mediaItems[1]); + ShouldHaveOneItem(result, mediaItems[2]); + ShouldHaveTwoItems(result, mediaItems[3], mediaItems[4]); + } + + [Test] + [TestCase("Episode 1 (1)", "Episode 2 (2)", "Episode 3 (1)", "Episode 4 (2)")] + [TestCase("Episode 1 (1) - More", "Episode 2 (2) - Title", "Episode 3 (1) - After", "Episode 4 (2) - Dash")] + [TestCase("Episode 1 Part 1", "Episode 2 Part 2", "Episode 3 Part 1", "Episode 4 Part 2")] + [TestCase("Episode 1 (Part 1)", "Episode 2 (Part 2)", "Episode 3 (Part 1)", "Episode 4 (Part 2)")] + public void Grouped_Grouped(string one, string two, string three, string four) + { + var mediaItems = new List + { + NamedEpisode(one, 1, 1, 1), + NamedEpisode(two, 1, 1, 2), + NamedEpisode(three, 1, 1, 3), + NamedEpisode(four, 1, 1, 4) + }; + + List result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems, false); + + result.Count.Should().Be(2); + ShouldHaveTwoItems(result, mediaItems[0], mediaItems[1]); + ShouldHaveTwoItems(result, mediaItems[2], mediaItems[3]); + } + + [Test] + [TestCase("Episode 1", "Episode 2 (2)", "Episode 3 (1)", "Episode 4 (2)")] + [TestCase("Episode 1 - More", "Episode 2 (2) - Title", "Episode 3 (1) - After", "Episode 4 (2) - Dash")] + [TestCase("Episode 1", "Episode 2 Part 2", "Episode 3 Part 1", "Episode 4 Part 2")] + [TestCase("Episode 1", "Episode 2 (Part 2)", "Episode 3 (Part 1)", "Episode 4 (Part 2)")] + public void Part2_Without_Part1(string one, string two, string three, string four) + { + var mediaItems = new List + { + NamedEpisode(one, 1, 1, 1), + NamedEpisode(two, 1, 1, 2), + NamedEpisode(three, 1, 1, 3), + NamedEpisode(four, 1, 1, 4) + }; + + List result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems, false); + + result.Count.Should().Be(3); + ShouldHaveOneItem(result, mediaItems[0]); + ShouldHaveOneItem(result, mediaItems[1]); + ShouldHaveTwoItems(result, mediaItems[2], mediaItems[3]); + } + + [Test] + [TestCase("Episode 1", "Episode 2 (2)", "Episode 3 (3)", "Episode 4")] + [TestCase("Episode 1 - More", "Episode 2 (2) - Title", "Episode 3 (3) - After", "Episode 4 - Dash")] + [TestCase("Episode 1", "Episode 2 Part 2", "Episode 3 Part 3", "Episode 4")] + [TestCase("Episode 1", "Episode 2 (Part 2)", "Episode 3 (Part 3)", "Episode 4")] + public void Part2And3_Without_Part1(string one, string two, string three, string four) + { + var mediaItems = new List + { + NamedEpisode(one, 1, 1, 1), + NamedEpisode(two, 1, 1, 2), + NamedEpisode(three, 1, 1, 3), + NamedEpisode(four, 1, 1, 4) + }; + + List result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems, false); + + result.Count.Should().Be(3); + ShouldHaveOneItem(result, mediaItems[0]); + ShouldHaveTwoItems(result, mediaItems[1], mediaItems[2]); + ShouldHaveOneItem(result, mediaItems[3]); + } + + [Test] + [TestCase("Episode 1 (1)", "Episode 3 (3)", "Episode 4", "Episode 5")] + [TestCase("Episode 1 (1) - More", "Episode 3 (3) - Title", "Episode 4 - After", "Episode 5 - Dash")] + [TestCase("Episode 1 Part 1", "Episode 3 Part 3", "Episode 4", "Episode 5")] + [TestCase("Episode 1 (Part 1)", "Episode 3 (Part 3)", "Episode 4", "Episode 5")] + public void Skip_Part(string one, string two, string three, string four) + { + var mediaItems = new List + { + NamedEpisode(one, 1, 1, 1), + NamedEpisode(two, 1, 1, 3), + NamedEpisode(three, 1, 1, 4), + NamedEpisode(four, 1, 1, 5) + }; + + List result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems, false); + + result.Count.Should().Be(4); + ShouldHaveOneItem(result, mediaItems[0]); + ShouldHaveOneItem(result, mediaItems[1]); + ShouldHaveOneItem(result, mediaItems[2]); + ShouldHaveOneItem(result, mediaItems[3]); + } + + [Test] + [TestCase("Episode 1 (1)", "Episode 3 (1)", "Episode 4 (2)", "Episode 5")] + [TestCase("Episode 1 (1) - More", "Episode 3 (1) - Title", "Episode 4 (2) - After", "Episode 5 - Dash")] + [TestCase("Episode 1 Part 1", "Episode 3 Part 1", "Episode 4 Part 2", "Episode 5")] + [TestCase("Episode 1 (Part 1)", "Episode 3 (Part 1)", "Episode 4 (Part 2)", "Episode 5")] + public void Repeat_Part(string one, string two, string three, string four) + { + var mediaItems = new List + { + NamedEpisode(one, 1, 1, 1), + NamedEpisode(two, 1, 1, 3), + NamedEpisode(three, 1, 1, 4), + NamedEpisode(four, 1, 1, 5) + }; + + List result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems, false); + + result.Count.Should().Be(3); + ShouldHaveOneItem(result, mediaItems[0]); + ShouldHaveTwoItems(result, mediaItems[1], mediaItems[2]); + ShouldHaveOneItem(result, mediaItems[3]); + } + + [Test] + [TestCase("S1 Episode 1 (1)", "S2 Episode 3 (1)", "S1 Episode 2 (2)", "S1 Episode 5")] + [TestCase( + "S1 Episode 1 (1) - More", + "S2 Episode 3 (1) - Title", + "S1 Episode 2 (2) - After", + "S1 Episode 5 - Dash")] + [TestCase("S1 Episode 1 Part 1", "S2 Episode 3 Part 1", "S1 Episode 2 Part 2", "S1 Episode 5")] + [TestCase("S1 Episode 1 (Part 1)", "S2 Episode 3 (Part 1)", "S1 Episode 2 (Part 2)", "S1 Episode 5")] + public void Mixed_Shows_Chronologically(string one, string two, string three, string four) + { + var mediaItems = new List + { + NamedEpisode(one, 1, 1, 1, new DateTime(2020, 1, 1)), + NamedEpisode(two, 2, 1, 3, new DateTime(2020, 1, 2)), + NamedEpisode(three, 1, 1, 2, new DateTime(2020, 1, 3)), + NamedEpisode(four, 1, 1, 5, new DateTime(2020, 1, 4)) + }; + + List result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems, false); + + result.Count.Should().Be(3); + ShouldHaveTwoItems(result, mediaItems[0], mediaItems[2]); + ShouldHaveOneItem(result, mediaItems[1]); + ShouldHaveOneItem(result, mediaItems[3]); + } + + [Test] + [TestCase("S1 Episode 1 (1)", "S2 Episode 3 (2)", "S1 Episode 2 (3)", "S1 Episode 5")] + [TestCase( + "S1 Episode 1 (1) - More", + "S2 Episode 3 (2) - Title", + "S1 Episode 2 (3) - After", + "S1 Episode 5 - Dash")] + [TestCase("S1 Episode 1 Part 1", "S2 Episode 3 Part 2", "S1 Episode 2 Part 3", "S1 Episode 5")] + [TestCase("S1 Episode 1 (Part 1)", "S2 Episode 3 (Part 2)", "S1 Episode 2 (Part 3)", "S1 Episode 5")] + public void Mixed_Shows_Chronologically_Crossover(string one, string two, string three, string four) + { + var mediaItems = new List + { + NamedEpisode(one, 1, 1, 1, new DateTime(2020, 1, 1)), + NamedEpisode(two, 2, 1, 3, new DateTime(2020, 1, 2)), + NamedEpisode(three, 1, 1, 2, new DateTime(2020, 1, 3)), + NamedEpisode(four, 1, 1, 5, new DateTime(2020, 1, 4)) + }; + + List result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems, true); + + result.Count.Should().Be(2); + ShouldHaveMultipleItems(result, mediaItems[0], new List { mediaItems[1], mediaItems[2] }); + ShouldHaveOneItem(result, mediaItems[3]); + } + + private static Episode NamedEpisode( + string title, + int showId, + int season, + int episode, + DateTime? releaseDate = null) => + new() + { + EpisodeMetadata = new List + { + new() { Title = title, ReleaseDate = releaseDate, EpisodeNumber = episode } + }, + Season = new Season + { + SeasonNumber = season, + Show = new Show { Id = showId }, + ShowId = showId + } + }; + + private static void ShouldHaveOneItem(IEnumerable result, MediaItem item) => + result.Filter(g => g.First == item && Optional(g.Additional).Flatten().HeadOrNone() == None) + .Should().HaveCount(1); + + private static void ShouldHaveTwoItems( + IEnumerable result, + MediaItem first, + MediaItem additional) => + result.Filter(g => g.First == first && Optional(g.Additional).Flatten().HeadOrNone() == Some(additional)) + .Should().HaveCount(1); + + private static void ShouldHaveMultipleItems( + IEnumerable result, + MediaItem first, + List additional) => + result.Filter( + g => g.First == first && g.Additional != null && g.Additional.Count == additional.Count && + additional.ForAll(g.Additional.Contains)) + .Should().HaveCount(1); +} \ No newline at end of file diff --git a/ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs b/ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs index 69d8b26ac..37acd7dc1 100644 --- a/ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs +++ b/ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Filler; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Scheduling; @@ -13,1425 +9,1423 @@ using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; using Serilog; -using static LanguageExt.Prelude; -namespace ErsatzTV.Core.Tests.Scheduling +namespace ErsatzTV.Core.Tests.Scheduling; + +[TestFixture] +public class PlayoutBuilderTests { - [TestFixture] - public class PlayoutBuilderTests + private readonly ILogger _logger; + + public PlayoutBuilderTests() { - private readonly ILogger _logger; - - public PlayoutBuilderTests() + if (Log.Logger.GetType().FullName == "Serilog.Core.Pipeline.SilentLogger") { - if (Log.Logger.GetType().FullName == "Serilog.Core.Pipeline.SilentLogger") - { - Log.Logger = new LoggerConfiguration().MinimumLevel.Debug().WriteTo.Console().CreateLogger(); - Log.Logger.Debug( - "Logger is not configured. Either this is a unit test or you have to configure the logger"); - } - - ServiceProvider serviceProvider = new ServiceCollection() - .AddLogging(builder => builder.AddSerilog(dispose: true)) - .BuildServiceProvider(); - - ILoggerFactory factory = serviceProvider.GetService(); - - _logger = factory.CreateLogger(); + Log.Logger = new LoggerConfiguration().MinimumLevel.Debug().WriteTo.Console().CreateLogger(); + Log.Logger.Debug( + "Logger is not configured. Either this is a unit test or you have to configure the logger"); } - [Test] - [Timeout(2000)] - public async Task OnlyZeroDurationItem_Should_Abort() + ServiceProvider serviceProvider = new ServiceCollection() + .AddLogging(builder => builder.AddSerilog(dispose: true)) + .BuildServiceProvider(); + + ILoggerFactory factory = serviceProvider.GetService(); + + _logger = factory.CreateLogger(); + } + + [Test] + [Timeout(2000)] + public async Task OnlyZeroDurationItem_Should_Abort() + { + var mediaItems = new List { - var mediaItems = new List - { - TestMovie(1, TimeSpan.Zero, DateTime.Today) - }; + TestMovie(1, TimeSpan.Zero, DateTime.Today) + }; - (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Random); - DateTimeOffset start = HoursAfterMidnight(0); - DateTimeOffset finish = start + TimeSpan.FromHours(6); + (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Random); + DateTimeOffset start = HoursAfterMidnight(0); + DateTimeOffset finish = start + TimeSpan.FromHours(6); - Playout result = await builder.BuildPlayoutItems(playout, start, finish); + Playout result = await builder.BuildPlayoutItems(playout, start, finish); - result.Items.Should().BeNull(); - } + result.Items.Should().BeNull(); + } - [Test] - public async Task ZeroDurationItem_Should_BeSkipped() + [Test] + public async Task ZeroDurationItem_Should_BeSkipped() + { + var mediaItems = new List { - var mediaItems = new List - { - TestMovie(1, TimeSpan.Zero, DateTime.Today), - TestMovie(2, TimeSpan.FromHours(6), DateTime.Today) - }; + TestMovie(1, TimeSpan.Zero, DateTime.Today), + TestMovie(2, TimeSpan.FromHours(6), DateTime.Today) + }; - (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Random); - DateTimeOffset start = HoursAfterMidnight(0); - DateTimeOffset finish = start + TimeSpan.FromHours(6); + (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Random); + DateTimeOffset start = HoursAfterMidnight(0); + DateTimeOffset finish = start + TimeSpan.FromHours(6); - Playout result = await builder.BuildPlayoutItems(playout, start, finish); + Playout result = await builder.BuildPlayoutItems(playout, start, finish); - result.Items.Count.Should().Be(1); - result.Items.Head().MediaItemId.Should().Be(2); - result.Items.Head().StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero); - result.Items.Head().FinishOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); - } + result.Items.Count.Should().Be(1); + result.Items.Head().MediaItemId.Should().Be(2); + result.Items.Head().StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero); + result.Items.Head().FinishOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); + } - [Test] - public async Task InitialFlood_Should_StartAtMidnight() + [Test] + public async Task InitialFlood_Should_StartAtMidnight() + { + var mediaItems = new List { - var mediaItems = new List - { - TestMovie(1, TimeSpan.FromHours(6), DateTime.Today) - }; + TestMovie(1, TimeSpan.FromHours(6), DateTime.Today) + }; - (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Random); - DateTimeOffset start = HoursAfterMidnight(0); - DateTimeOffset finish = start + TimeSpan.FromHours(6); + (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Random); + DateTimeOffset start = HoursAfterMidnight(0); + DateTimeOffset finish = start + TimeSpan.FromHours(6); - Playout result = await builder.BuildPlayoutItems(playout, start, finish); + Playout result = await builder.BuildPlayoutItems(playout, start, finish); - result.Items.Count.Should().Be(1); - result.Items.Head().StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero); - result.Items.Head().FinishOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); - } + result.Items.Count.Should().Be(1); + result.Items.Head().StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero); + result.Items.Head().FinishOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); + } - [Test] - public async Task InitialFlood_Should_StartAtMidnight_With_LateStart() + [Test] + public async Task InitialFlood_Should_StartAtMidnight_With_LateStart() + { + var mediaItems = new List { - var mediaItems = new List - { - TestMovie(1, TimeSpan.FromHours(6), DateTime.Today) - }; + TestMovie(1, TimeSpan.FromHours(6), DateTime.Today) + }; - (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Random); - DateTimeOffset start = HoursAfterMidnight(1); - DateTimeOffset finish = start + TimeSpan.FromHours(6); + (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Random); + DateTimeOffset start = HoursAfterMidnight(1); + DateTimeOffset finish = start + TimeSpan.FromHours(6); - Playout result = await builder.BuildPlayoutItems(playout, start, finish); + Playout result = await builder.BuildPlayoutItems(playout, start, finish); - result.Items.Count.Should().Be(2); - result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero); - result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); - result.Items[1].FinishOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(12)); - } + result.Items.Count.Should().Be(2); + result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero); + result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); + result.Items[1].FinishOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(12)); + } - [Test] - public async Task ChronologicalContent_Should_CreateChronologicalItems() + [Test] + public async Task ChronologicalContent_Should_CreateChronologicalItems() + { + var mediaItems = new List { - var mediaItems = new List + TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)), + TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 2, 1)) + }; + + (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Chronological); + DateTimeOffset start = HoursAfterMidnight(0); + DateTimeOffset finish = start + TimeSpan.FromHours(4); + + Playout result = await builder.BuildPlayoutItems(playout, start, finish); + + result.Items.Count.Should().Be(4); + result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero); + result.Items[0].MediaItemId.Should().Be(1); + result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(1)); + result.Items[1].MediaItemId.Should().Be(2); + result.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(2)); + result.Items[2].MediaItemId.Should().Be(1); + result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(3)); + result.Items[3].MediaItemId.Should().Be(2); + } + + [Test] + public async Task ChronologicalFlood_Should_AnchorAndMaintainExistingPlayout() + { + var mediaItems = new List + { + TestMovie(1, TimeSpan.FromHours(6), DateTime.Today), + TestMovie(2, TimeSpan.FromHours(6), DateTime.Today.AddHours(1)) + }; + + (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Chronological); + DateTimeOffset start = HoursAfterMidnight(0); + DateTimeOffset finish = start + TimeSpan.FromHours(6); + + Playout result = await builder.BuildPlayoutItems(playout, start, finish); + + result.Items.Count.Should().Be(1); + result.Items.Head().MediaItemId.Should().Be(1); + + result.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddHours(6)); + + result.ProgramScheduleAnchors.Count.Should().Be(1); + result.ProgramScheduleAnchors.Head().EnumeratorState.Index.Should().Be(1); + + DateTimeOffset start2 = HoursAfterMidnight(1); + DateTimeOffset finish2 = start2 + TimeSpan.FromHours(6); + + Playout result2 = await builder.BuildPlayoutItems(playout, start2, finish2); + + result2.Items.Count.Should().Be(2); + result2.Items.Last().StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); + result2.Items.Last().MediaItemId.Should().Be(2); + + result2.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddHours(12)); + result2.ProgramScheduleAnchors.Count.Should().Be(1); + result2.ProgramScheduleAnchors.Head().EnumeratorState.Index.Should().Be(0); + } + + [Test] + public async Task ChronologicalFlood_Should_AnchorAndReturnNewPlayoutItems() + { + var mediaItems = new List + { + TestMovie(1, TimeSpan.FromHours(6), DateTime.Today), + TestMovie(2, TimeSpan.FromHours(6), DateTime.Today.AddHours(1)) + }; + + (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Chronological); + DateTimeOffset start = HoursAfterMidnight(0); + DateTimeOffset finish = start + TimeSpan.FromHours(6); + + Playout result = await builder.BuildPlayoutItems(playout, start, finish); + + result.Items.Count.Should().Be(1); + result.Items.Head().MediaItemId.Should().Be(1); + + result.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddHours(6)); + result.ProgramScheduleAnchors.Count.Should().Be(1); + result.ProgramScheduleAnchors.Head().EnumeratorState.Index.Should().Be(1); + + DateTimeOffset start2 = HoursAfterMidnight(1); + DateTimeOffset finish2 = start2 + TimeSpan.FromHours(12); + + Playout result2 = await builder.BuildPlayoutItems(playout, start2, finish2); + + result2.Items.Count.Should().Be(3); + result2.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); + result2.Items[1].MediaItemId.Should().Be(2); + result2.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(12)); + result2.Items[2].MediaItemId.Should().Be(1); + + result2.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddHours(18)); + result2.ProgramScheduleAnchors.Count.Should().Be(1); + result2.ProgramScheduleAnchors.Head().EnumeratorState.Index.Should().Be(1); + } + + [Test] + public async Task ShuffleFloodRebuild_Should_IgnoreAnchors() + { + var mediaItems = new List + { + TestMovie(1, TimeSpan.FromHours(1), DateTime.Today), + TestMovie(2, TimeSpan.FromHours(1), DateTime.Today.AddHours(1)), + TestMovie(3, TimeSpan.FromHours(1), DateTime.Today.AddHours(2)), + TestMovie(4, TimeSpan.FromHours(1), DateTime.Today.AddHours(3)), + TestMovie(5, TimeSpan.FromHours(1), DateTime.Today.AddHours(4)), + TestMovie(6, TimeSpan.FromHours(1), DateTime.Today.AddHours(5)) + }; + + (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Shuffle); + DateTimeOffset start = HoursAfterMidnight(0); + DateTimeOffset finish = start + TimeSpan.FromHours(6); + + Playout result = await builder.BuildPlayoutItems(playout, start, finish); + + result.Items.Count.Should().Be(6); + result.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddHours(6)); + + result.ProgramScheduleAnchors.Count.Should().Be(1); + result.ProgramScheduleAnchors.Head().EnumeratorState.Index.Should().Be(0); + + int firstSeedValue = result.ProgramScheduleAnchors.Head().EnumeratorState.Seed; + + DateTimeOffset start2 = HoursAfterMidnight(0); + DateTimeOffset finish2 = start2 + TimeSpan.FromHours(6); + + Playout result2 = await builder.BuildPlayoutItems(playout, start2, finish2, true); + + result2.Items.Count.Should().Be(6); + result2.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddHours(6)); + + result2.ProgramScheduleAnchors.Count.Should().Be(1); + result2.ProgramScheduleAnchors.Head().EnumeratorState.Index.Should().Be(0); + + int secondSeedValue = result2.ProgramScheduleAnchors.Head().EnumeratorState.Seed; + + firstSeedValue.Should().NotBe(secondSeedValue); + } + + [Test] + public async Task ShuffleFlood_Should_MaintainRandomSeed() + { + var mediaItems = new List + { + TestMovie(1, TimeSpan.FromHours(1), DateTime.Today), + TestMovie(2, TimeSpan.FromHours(1), DateTime.Today.AddHours(1)), + TestMovie(3, TimeSpan.FromHours(1), DateTime.Today.AddHours(3)) + }; + + (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Shuffle); + DateTimeOffset start = HoursAfterMidnight(0); + DateTimeOffset finish = start + TimeSpan.FromHours(6); + + Playout result = await builder.BuildPlayoutItems(playout, start, finish); + + result.Items.Count.Should().Be(6); + result.ProgramScheduleAnchors.Count.Should().Be(1); + result.ProgramScheduleAnchors.Head().EnumeratorState.Seed.Should().BeGreaterThan(0); + + int firstSeedValue = result.ProgramScheduleAnchors.Head().EnumeratorState.Seed; + + DateTimeOffset start2 = HoursAfterMidnight(0); + DateTimeOffset finish2 = start2 + TimeSpan.FromHours(6); + + Playout result2 = await builder.BuildPlayoutItems(playout, start2, finish2); + + int secondSeedValue = result2.ProgramScheduleAnchors.Head().EnumeratorState.Seed; + + firstSeedValue.Should().Be(secondSeedValue); + } + + [Test] + public async Task FloodContent_Should_FloodAroundFixedContent_One() + { + var floodCollection = new Collection + { + Id = 1, + Name = "Flood Items", + MediaItems = new List { TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)), TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 2, 1)) - }; + } + }; - (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Chronological); - DateTimeOffset start = HoursAfterMidnight(0); - DateTimeOffset finish = start + TimeSpan.FromHours(4); - - Playout result = await builder.BuildPlayoutItems(playout, start, finish); - - result.Items.Count.Should().Be(4); - result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero); - result.Items[0].MediaItemId.Should().Be(1); - result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(1)); - result.Items[1].MediaItemId.Should().Be(2); - result.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(2)); - result.Items[2].MediaItemId.Should().Be(1); - result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(3)); - result.Items[3].MediaItemId.Should().Be(2); - } - - [Test] - public async Task ChronologicalFlood_Should_AnchorAndMaintainExistingPlayout() + var fixedCollection = new Collection { - var mediaItems = new List + Id = 2, + Name = "Fixed Items", + MediaItems = new List { - TestMovie(1, TimeSpan.FromHours(6), DateTime.Today), - TestMovie(2, TimeSpan.FromHours(6), DateTime.Today.AddHours(1)) - }; + TestMovie(3, TimeSpan.FromHours(2), new DateTime(2020, 1, 1)) + } + }; - (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Chronological); - DateTimeOffset start = HoursAfterMidnight(0); - DateTimeOffset finish = start + TimeSpan.FromHours(6); + var fakeRepository = new FakeMediaCollectionRepository( + Map( + (floodCollection.Id, floodCollection.MediaItems.ToList()), + (fixedCollection.Id, fixedCollection.MediaItems.ToList()))); - Playout result = await builder.BuildPlayoutItems(playout, start, finish); - - result.Items.Count.Should().Be(1); - result.Items.Head().MediaItemId.Should().Be(1); - - result.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddHours(6)); - - result.ProgramScheduleAnchors.Count.Should().Be(1); - result.ProgramScheduleAnchors.Head().EnumeratorState.Index.Should().Be(1); - - DateTimeOffset start2 = HoursAfterMidnight(1); - DateTimeOffset finish2 = start2 + TimeSpan.FromHours(6); - - Playout result2 = await builder.BuildPlayoutItems(playout, start2, finish2); - - result2.Items.Count.Should().Be(2); - result2.Items.Last().StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); - result2.Items.Last().MediaItemId.Should().Be(2); - - result2.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddHours(12)); - result2.ProgramScheduleAnchors.Count.Should().Be(1); - result2.ProgramScheduleAnchors.Head().EnumeratorState.Index.Should().Be(0); - } - - [Test] - public async Task ChronologicalFlood_Should_AnchorAndReturnNewPlayoutItems() + var items = new List { - var mediaItems = new List - { - TestMovie(1, TimeSpan.FromHours(6), DateTime.Today), - TestMovie(2, TimeSpan.FromHours(6), DateTime.Today.AddHours(1)) - }; - - (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Chronological); - DateTimeOffset start = HoursAfterMidnight(0); - DateTimeOffset finish = start + TimeSpan.FromHours(6); - - Playout result = await builder.BuildPlayoutItems(playout, start, finish); - - result.Items.Count.Should().Be(1); - result.Items.Head().MediaItemId.Should().Be(1); - - result.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddHours(6)); - result.ProgramScheduleAnchors.Count.Should().Be(1); - result.ProgramScheduleAnchors.Head().EnumeratorState.Index.Should().Be(1); - - DateTimeOffset start2 = HoursAfterMidnight(1); - DateTimeOffset finish2 = start2 + TimeSpan.FromHours(12); - - Playout result2 = await builder.BuildPlayoutItems(playout, start2, finish2); - - result2.Items.Count.Should().Be(3); - result2.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); - result2.Items[1].MediaItemId.Should().Be(2); - result2.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(12)); - result2.Items[2].MediaItemId.Should().Be(1); - - result2.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddHours(18)); - result2.ProgramScheduleAnchors.Count.Should().Be(1); - result2.ProgramScheduleAnchors.Head().EnumeratorState.Index.Should().Be(1); - } - - [Test] - public async Task ShuffleFloodRebuild_Should_IgnoreAnchors() - { - var mediaItems = new List - { - TestMovie(1, TimeSpan.FromHours(1), DateTime.Today), - TestMovie(2, TimeSpan.FromHours(1), DateTime.Today.AddHours(1)), - TestMovie(3, TimeSpan.FromHours(1), DateTime.Today.AddHours(2)), - TestMovie(4, TimeSpan.FromHours(1), DateTime.Today.AddHours(3)), - TestMovie(5, TimeSpan.FromHours(1), DateTime.Today.AddHours(4)), - TestMovie(6, TimeSpan.FromHours(1), DateTime.Today.AddHours(5)) - }; - - (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Shuffle); - DateTimeOffset start = HoursAfterMidnight(0); - DateTimeOffset finish = start + TimeSpan.FromHours(6); - - Playout result = await builder.BuildPlayoutItems(playout, start, finish); - - result.Items.Count.Should().Be(6); - result.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddHours(6)); - - result.ProgramScheduleAnchors.Count.Should().Be(1); - result.ProgramScheduleAnchors.Head().EnumeratorState.Index.Should().Be(0); - - int firstSeedValue = result.ProgramScheduleAnchors.Head().EnumeratorState.Seed; - - DateTimeOffset start2 = HoursAfterMidnight(0); - DateTimeOffset finish2 = start2 + TimeSpan.FromHours(6); - - Playout result2 = await builder.BuildPlayoutItems(playout, start2, finish2, true); - - result2.Items.Count.Should().Be(6); - result2.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddHours(6)); - - result2.ProgramScheduleAnchors.Count.Should().Be(1); - result2.ProgramScheduleAnchors.Head().EnumeratorState.Index.Should().Be(0); - - int secondSeedValue = result2.ProgramScheduleAnchors.Head().EnumeratorState.Seed; - - firstSeedValue.Should().NotBe(secondSeedValue); - } - - [Test] - public async Task ShuffleFlood_Should_MaintainRandomSeed() - { - var mediaItems = new List - { - TestMovie(1, TimeSpan.FromHours(1), DateTime.Today), - TestMovie(2, TimeSpan.FromHours(1), DateTime.Today.AddHours(1)), - TestMovie(3, TimeSpan.FromHours(1), DateTime.Today.AddHours(3)) - }; - - (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Shuffle); - DateTimeOffset start = HoursAfterMidnight(0); - DateTimeOffset finish = start + TimeSpan.FromHours(6); - - Playout result = await builder.BuildPlayoutItems(playout, start, finish); - - result.Items.Count.Should().Be(6); - result.ProgramScheduleAnchors.Count.Should().Be(1); - result.ProgramScheduleAnchors.Head().EnumeratorState.Seed.Should().BeGreaterThan(0); - - int firstSeedValue = result.ProgramScheduleAnchors.Head().EnumeratorState.Seed; - - DateTimeOffset start2 = HoursAfterMidnight(0); - DateTimeOffset finish2 = start2 + TimeSpan.FromHours(6); - - Playout result2 = await builder.BuildPlayoutItems(playout, start2, finish2); - - int secondSeedValue = result2.ProgramScheduleAnchors.Head().EnumeratorState.Seed; - - firstSeedValue.Should().Be(secondSeedValue); - } - - [Test] - public async Task FloodContent_Should_FloodAroundFixedContent_One() - { - var floodCollection = new Collection - { - Id = 1, - Name = "Flood Items", - MediaItems = new List - { - TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)), - TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 2, 1)) - } - }; - - var fixedCollection = new Collection - { - Id = 2, - Name = "Fixed Items", - MediaItems = new List - { - TestMovie(3, TimeSpan.FromHours(2), new DateTime(2020, 1, 1)) - } - }; - - var fakeRepository = new FakeMediaCollectionRepository( - Map( - (floodCollection.Id, floodCollection.MediaItems.ToList()), - (fixedCollection.Id, fixedCollection.MediaItems.ToList()))); - - var items = new List - { - new ProgramScheduleItemFlood - { - Index = 1, - Collection = floodCollection, - CollectionId = floodCollection.Id, - StartTime = null, - PlaybackOrder = PlaybackOrder.Chronological - }, - new ProgramScheduleItemOne - { - Index = 2, - Collection = fixedCollection, - CollectionId = fixedCollection.Id, - StartTime = TimeSpan.FromHours(3), - PlaybackOrder = PlaybackOrder.Chronological - } - }; - - var playout = new Playout - { - ProgramSchedule = new ProgramSchedule - { - Items = items - }, - Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" } - }; - - var configRepo = new Mock(); - var televisionRepo = new FakeTelevisionRepository(); - var artistRepo = new Mock(); - var builder = new PlayoutBuilder( - configRepo.Object, - fakeRepository, - televisionRepo, - artistRepo.Object, - _logger); - - DateTimeOffset start = HoursAfterMidnight(0); - DateTimeOffset finish = start + TimeSpan.FromHours(6); - - Playout result = await builder.BuildPlayoutItems(playout, start, finish); - - result.Items.Count.Should().Be(5); - result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero); - result.Items[0].MediaItemId.Should().Be(1); - result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(1)); - result.Items[1].MediaItemId.Should().Be(2); - result.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(2)); - result.Items[2].MediaItemId.Should().Be(1); - result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(3)); - result.Items[3].MediaItemId.Should().Be(3); - result.Items[4].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(5)); - result.Items[4].MediaItemId.Should().Be(2); - } - - [Test] - public async Task FloodContent_Should_FloodAroundFixedContent_Multiple() - { - var floodCollection = new Collection - { - Id = 1, - Name = "Flood Items", - MediaItems = new List - { - TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)), - TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 2, 1)) - } - }; - - var fixedCollection = new Collection - { - Id = 2, - Name = "Fixed Items", - MediaItems = new List - { - TestMovie(3, TimeSpan.FromHours(2), new DateTime(2020, 1, 1)), - TestMovie(4, TimeSpan.FromHours(1), new DateTime(2020, 1, 2)) - } - }; - - var fakeRepository = new FakeMediaCollectionRepository( - Map( - (floodCollection.Id, floodCollection.MediaItems.ToList()), - (fixedCollection.Id, fixedCollection.MediaItems.ToList()))); - - var items = new List - { - new ProgramScheduleItemFlood - { - Index = 1, - Collection = floodCollection, - CollectionId = floodCollection.Id, - StartTime = null, - PlaybackOrder = PlaybackOrder.Chronological - }, - new ProgramScheduleItemMultiple - { - Index = 2, - Collection = fixedCollection, - CollectionId = fixedCollection.Id, - StartTime = TimeSpan.FromHours(3), - Count = 2, - PlaybackOrder = PlaybackOrder.Chronological - } - }; - - var playout = new Playout - { - ProgramSchedule = new ProgramSchedule - { - Items = items - }, - Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" } - }; - - var configRepo = new Mock(); - var televisionRepo = new FakeTelevisionRepository(); - var artistRepo = new Mock(); - var builder = new PlayoutBuilder( - configRepo.Object, - fakeRepository, - televisionRepo, - artistRepo.Object, - _logger); - - DateTimeOffset start = HoursAfterMidnight(0); - DateTimeOffset finish = start + TimeSpan.FromHours(7); - - Playout result = await builder.BuildPlayoutItems(playout, start, finish); - - result.Items.Count.Should().Be(6); - - result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero); - result.Items[0].MediaItemId.Should().Be(1); - result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(1)); - result.Items[1].MediaItemId.Should().Be(2); - result.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(2)); - result.Items[2].MediaItemId.Should().Be(1); - - result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(3)); - result.Items[3].MediaItemId.Should().Be(3); - result.Items[4].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(5)); - result.Items[4].MediaItemId.Should().Be(4); - - result.Items[5].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); - result.Items[5].MediaItemId.Should().Be(2); - } - - [Test] - public async Task FloodContent_Should_FloodWithFixedStartTime() - { - var floodCollection = new Collection - { - Id = 1, - Name = "Flood Items", - MediaItems = new List - { - TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)), - TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 2, 1)) - } - }; - - var fixedCollection = new Collection - { - Id = 2, - Name = "Fixed Items", - MediaItems = new List - { - TestMovie(3, TimeSpan.FromHours(2), new DateTime(2020, 1, 1)), - TestMovie(4, TimeSpan.FromHours(1), new DateTime(2020, 1, 2)) - } - }; - - var fakeRepository = new FakeMediaCollectionRepository( - Map( - (floodCollection.Id, floodCollection.MediaItems.ToList()), - (fixedCollection.Id, fixedCollection.MediaItems.ToList()))); - - var items = new List - { - new ProgramScheduleItemFlood - { - Index = 1, - Collection = floodCollection, - CollectionId = floodCollection.Id, - StartTime = TimeSpan.FromHours(7), - PlaybackOrder = PlaybackOrder.Chronological - }, - new ProgramScheduleItemOne - { - Index = 2, - Collection = fixedCollection, - CollectionId = fixedCollection.Id, - StartTime = TimeSpan.FromHours(12), - PlaybackOrder = PlaybackOrder.Chronological - } - }; - - var playout = new Playout - { - ProgramSchedule = new ProgramSchedule - { - Items = items - }, - Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" } - }; - - var configRepo = new Mock(); - var televisionRepo = new FakeTelevisionRepository(); - var artistRepo = new Mock(); - var builder = new PlayoutBuilder( - configRepo.Object, - fakeRepository, - televisionRepo, - artistRepo.Object, - _logger); - - DateTimeOffset start = HoursAfterMidnight(0); - DateTimeOffset finish = start + TimeSpan.FromHours(24); - - Playout result = await builder.BuildPlayoutItems(playout, start, finish); - - result.Items.Count.Should().Be(6); - - result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(7)); - result.Items[0].MediaItemId.Should().Be(1); - result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(8)); - result.Items[1].MediaItemId.Should().Be(2); - result.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(9)); - result.Items[2].MediaItemId.Should().Be(1); - result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(10)); - result.Items[3].MediaItemId.Should().Be(2); - result.Items[4].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(11)); - result.Items[4].MediaItemId.Should().Be(1); - - result.Items[5].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(12)); - result.Items[5].MediaItemId.Should().Be(3); - } - - [Test] - public async Task FloodContent_Should_FloodWithFixedStartTime_FromAnchor() - { - var floodCollection = new Collection - { - Id = 1, - Name = "Flood Items", - MediaItems = new List - { - TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)), - TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 2, 1)) - } - }; - - var fixedCollection = new Collection - { - Id = 2, - Name = "Fixed Items", - MediaItems = new List - { - TestMovie(3, TimeSpan.FromHours(2), new DateTime(2020, 1, 1)), - TestMovie(4, TimeSpan.FromHours(1), new DateTime(2020, 1, 2)) - } - }; - - var fakeRepository = new FakeMediaCollectionRepository( - Map( - (floodCollection.Id, floodCollection.MediaItems.ToList()), - (fixedCollection.Id, fixedCollection.MediaItems.ToList()))); - - var items = new List - { - new ProgramScheduleItemFlood - { - Index = 1, - Collection = floodCollection, - CollectionId = floodCollection.Id, - StartTime = TimeSpan.FromHours(7), - PlaybackOrder = PlaybackOrder.Chronological - }, - new ProgramScheduleItemOne - { - Index = 2, - Collection = fixedCollection, - CollectionId = fixedCollection.Id, - StartTime = TimeSpan.FromHours(12), - PlaybackOrder = PlaybackOrder.Chronological - } - }; - - var playout = new Playout - { - ProgramSchedule = new ProgramSchedule - { - Items = items - }, - Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, - Anchor = new PlayoutAnchor - { - NextStart = HoursAfterMidnight(9).UtcDateTime, - ScheduleItemsEnumeratorState = new CollectionEnumeratorState - { - Index = 0, - Seed = 1 - }, - InFlood = true - } - }; - - var configRepo = new Mock(); - var televisionRepo = new FakeTelevisionRepository(); - var artistRepo = new Mock(); - var builder = new PlayoutBuilder( - configRepo.Object, - fakeRepository, - televisionRepo, - artistRepo.Object, - _logger); - - DateTimeOffset start = HoursAfterMidnight(0); - DateTimeOffset finish = start + TimeSpan.FromHours(32); - - Playout result = await builder.BuildPlayoutItems(playout, start, finish); - - result.Items.Count.Should().Be(5); - - result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(9)); - result.Items[0].MediaItemId.Should().Be(1); - result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(10)); - result.Items[1].MediaItemId.Should().Be(2); - result.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(11)); - result.Items[2].MediaItemId.Should().Be(1); - - result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(12)); - result.Items[3].MediaItemId.Should().Be(3); - - result.Items[4].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(7)); - result.Items[4].MediaItemId.Should().Be(2); - - result.Anchor.InFlood.Should().BeTrue(); - } - - [Test] - public async Task FloodContent_Should_FloodAroundFixedContent_DurationWithoutOfflineTail() - { - var floodCollection = new Collection - { - Id = 1, - Name = "Flood Items", - MediaItems = new List - { - TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)), - TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 2, 1)) - } - }; - - var fixedCollection = new Collection - { - Id = 2, - Name = "Fixed Items", - MediaItems = new List - { - TestMovie(3, TimeSpan.FromHours(0.75), new DateTime(2020, 1, 1)), - TestMovie(4, TimeSpan.FromHours(1.5), new DateTime(2020, 1, 2)) - } - }; - - var fakeRepository = new FakeMediaCollectionRepository( - Map( - (floodCollection.Id, floodCollection.MediaItems.ToList()), - (fixedCollection.Id, fixedCollection.MediaItems.ToList()))); - - var items = new List - { - new ProgramScheduleItemFlood - { - Index = 1, - Collection = floodCollection, - CollectionId = floodCollection.Id, - StartTime = null, - PlaybackOrder = PlaybackOrder.Chronological - }, - new ProgramScheduleItemDuration - { - Index = 2, - Collection = fixedCollection, - CollectionId = fixedCollection.Id, - StartTime = TimeSpan.FromHours(2), - PlayoutDuration = TimeSpan.FromHours(2), - TailMode = TailMode.None, // immediately continue - PlaybackOrder = PlaybackOrder.Chronological - } - }; - - var playout = new Playout - { - ProgramSchedule = new ProgramSchedule - { - Items = items - }, - Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" } - }; - - var configRepo = new Mock(); - var televisionRepo = new FakeTelevisionRepository(); - var artistRepo = new Mock(); - var builder = new PlayoutBuilder( - configRepo.Object, - fakeRepository, - televisionRepo, - artistRepo.Object, - _logger); - - DateTimeOffset start = HoursAfterMidnight(0); - DateTimeOffset finish = start + TimeSpan.FromHours(6); - - Playout result = await builder.BuildPlayoutItems(playout, start, finish); - - result.Items.Count.Should().Be(7); - - result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero); - result.Items[0].MediaItemId.Should().Be(1); - result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(1)); - result.Items[1].MediaItemId.Should().Be(2); - - result.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(2)); - result.Items[2].MediaItemId.Should().Be(3); - - result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(2.75)); - result.Items[3].MediaItemId.Should().Be(1); - result.Items[4].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(3.75)); - result.Items[4].MediaItemId.Should().Be(2); - - result.Items[5].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(4.75)); - result.Items[5].MediaItemId.Should().Be(1); - result.Items[6].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(5.75)); - result.Items[6].MediaItemId.Should().Be(2); - } - - [Test] - public async Task MultipleContent_Should_WrapAroundDynamicContent_DurationWithoutOfflineTail() - { - var multipleCollection = new Collection - { - Id = 1, - Name = "Multiple Items", - MediaItems = new List - { - TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)), - TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 2, 1)) - } - }; - - var dynamicCollection = new Collection - { - Id = 2, - Name = "Dynamic Items", - MediaItems = new List - { - TestMovie(3, TimeSpan.FromHours(0.75), new DateTime(2020, 1, 1)), - TestMovie(4, TimeSpan.FromHours(1.5), new DateTime(2020, 1, 2)) - } - }; - - var fakeRepository = new FakeMediaCollectionRepository( - Map( - (multipleCollection.Id, multipleCollection.MediaItems.ToList()), - (dynamicCollection.Id, dynamicCollection.MediaItems.ToList()))); - - var items = new List - { - new ProgramScheduleItemMultiple - { - Index = 1, - Collection = multipleCollection, - CollectionId = multipleCollection.Id, - StartTime = null, - Count = 2, - PlaybackOrder = PlaybackOrder.Chronological - }, - new ProgramScheduleItemDuration - { - Index = 2, - Collection = dynamicCollection, - CollectionId = dynamicCollection.Id, - StartTime = null, - PlayoutDuration = TimeSpan.FromHours(2), - TailMode = TailMode.None, // immediately continue - PlaybackOrder = PlaybackOrder.Chronological - } - }; - - var playout = new Playout - { - ProgramSchedule = new ProgramSchedule - { - Items = items - }, - Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" } - }; - - var configRepo = new Mock(); - var televisionRepo = new FakeTelevisionRepository(); - var artistRepo = new Mock(); - var builder = new PlayoutBuilder( - configRepo.Object, - fakeRepository, - televisionRepo, - artistRepo.Object, - _logger); - - DateTimeOffset start = HoursAfterMidnight(0); - DateTimeOffset finish = start + TimeSpan.FromHours(6); - - Playout result = await builder.BuildPlayoutItems(playout, start, finish); - - result.Items.Count.Should().Be(6); - - result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero); - result.Items[0].MediaItemId.Should().Be(1); - result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(1)); - result.Items[1].MediaItemId.Should().Be(2); - - result.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(2)); - result.Items[2].MediaItemId.Should().Be(3); - - result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(2.75)); - result.Items[3].MediaItemId.Should().Be(1); - result.Items[4].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(3.75)); - result.Items[4].MediaItemId.Should().Be(2); - - result.Items[5].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(4.75)); - result.Items[5].MediaItemId.Should().Be(4); - } - - [Test] - public async Task Alternating_MultipleContent_Should_Maintain_Counts() - { - var collectionOne = new Collection - { - Id = 1, - Name = "Multiple Items 1", - MediaItems = new List - { - TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)) - } - }; - - var collectionTwo = new Collection - { - Id = 2, - Name = "Multiple Items 2", - MediaItems = new List - { - TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)) - } - }; - - var fakeRepository = new FakeMediaCollectionRepository( - Map( - (collectionOne.Id, collectionOne.MediaItems.ToList()), - (collectionTwo.Id, collectionTwo.MediaItems.ToList()))); - - var items = new List - { - new ProgramScheduleItemMultiple - { - Id = 1, - Index = 1, - Collection = collectionOne, - CollectionId = collectionOne.Id, - StartTime = null, - Count = 3, - PlaybackOrder = PlaybackOrder.Chronological - }, - new ProgramScheduleItemMultiple - { - Id = 2, - Index = 2, - Collection = collectionTwo, - CollectionId = collectionTwo.Id, - StartTime = null, - Count = 3, - PlaybackOrder = PlaybackOrder.Chronological - } - }; - - var playout = new Playout - { - ProgramSchedule = new ProgramSchedule - { - Items = items - }, - Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, - Anchor = new PlayoutAnchor - { - NextStart = HoursAfterMidnight(1).UtcDateTime, - ScheduleItemsEnumeratorState = new CollectionEnumeratorState - { - Index = 0, - Seed = 1 - }, - MultipleRemaining = 2 - } - }; - - var configRepo = new Mock(); - var televisionRepo = new FakeTelevisionRepository(); - var artistRepo = new Mock(); - var builder = new PlayoutBuilder( - configRepo.Object, - fakeRepository, - televisionRepo, - artistRepo.Object, - _logger); - - DateTimeOffset start = HoursAfterMidnight(0); - DateTimeOffset finish = start + TimeSpan.FromHours(5); - - Playout result = await builder.BuildPlayoutItems(playout, start, finish); - - result.Items.Count.Should().Be(4); - - result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(1)); - result.Items[0].MediaItemId.Should().Be(1); - result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(2)); - result.Items[1].MediaItemId.Should().Be(1); - - result.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(3)); - result.Items[2].MediaItemId.Should().Be(2); - result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(4)); - result.Items[3].MediaItemId.Should().Be(2); - - result.Anchor.ScheduleItemsEnumeratorState.Index.Should().Be(1); - result.Anchor.MultipleRemaining.Should().Be(1); - } - - [Test] - public async Task Auto_Zero_MultipleCount() - { - var collectionOne = new Collection - { - Id = 1, - Name = "Multiple Items 1", - MediaItems = new List - { - TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)), - TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)), - TestMovie(3, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)) - } - }; - - var collectionTwo = new Collection - { - Id = 2, - Name = "Multiple Items 2", - MediaItems = new List - { - TestMovie(4, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)), - TestMovie(5, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)), - } - }; - - var fakeRepository = new FakeMediaCollectionRepository( - Map( - (collectionOne.Id, collectionOne.MediaItems.ToList()), - (collectionTwo.Id, collectionTwo.MediaItems.ToList()))); - - var items = new List - { - new ProgramScheduleItemMultiple - { - Id = 1, - Index = 1, - Collection = collectionOne, - CollectionId = collectionOne.Id, - StartTime = null, - Count = 0, - PlaybackOrder = PlaybackOrder.Chronological - }, - new ProgramScheduleItemMultiple - { - Id = 2, - Index = 2, - Collection = collectionTwo, - CollectionId = collectionTwo.Id, - StartTime = null, - Count = 0, - PlaybackOrder = PlaybackOrder.Chronological - } - }; - - var playout = new Playout - { - ProgramSchedule = new ProgramSchedule - { - Items = items - }, - Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, - }; - - var configRepo = new Mock(); - var televisionRepo = new FakeTelevisionRepository(); - var artistRepo = new Mock(); - var builder = new PlayoutBuilder( - configRepo.Object, - fakeRepository, - televisionRepo, - artistRepo.Object, - _logger); - - DateTimeOffset start = HoursAfterMidnight(0); - DateTimeOffset finish = start + TimeSpan.FromHours(5); - - Playout result = await builder.BuildPlayoutItems(playout, start, finish); - - result.Items.Count.Should().Be(5); - - result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(0)); - result.Items[0].MediaItemId.Should().Be(1); - result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(1)); - result.Items[1].MediaItemId.Should().Be(2); - result.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(2)); - result.Items[2].MediaItemId.Should().Be(3); - - result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(3)); - result.Items[3].MediaItemId.Should().Be(4); - result.Items[4].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(4)); - result.Items[4].MediaItemId.Should().Be(5); - - result.Anchor.ScheduleItemsEnumeratorState.Index.Should().Be(0); - result.Anchor.MultipleRemaining.Should().BeNull(); - } - - [Test] - public async Task Alternating_Duration_Should_Maintain_Duration() - { - var collectionOne = new Collection - { - Id = 1, - Name = "Duration Items 1", - MediaItems = new List - { - TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)) - } - }; - - var collectionTwo = new Collection - { - Id = 2, - Name = "Duration Items 2", - MediaItems = new List - { - TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)) - } - }; - - var fakeRepository = new FakeMediaCollectionRepository( - Map( - (collectionOne.Id, collectionOne.MediaItems.ToList()), - (collectionTwo.Id, collectionTwo.MediaItems.ToList()))); - - var items = new List - { - new ProgramScheduleItemDuration - { - Id = 1, - Index = 1, - Collection = collectionOne, - CollectionId = collectionOne.Id, - StartTime = null, - PlayoutDuration = TimeSpan.FromHours(3), - TailMode = TailMode.None, - PlaybackOrder = PlaybackOrder.Chronological - }, - new ProgramScheduleItemDuration - { - Id = 2, - Index = 2, - Collection = collectionTwo, - CollectionId = collectionTwo.Id, - StartTime = null, - PlayoutDuration = TimeSpan.FromHours(3), - TailMode = TailMode.None, - PlaybackOrder = PlaybackOrder.Chronological - } - }; - - var playout = new Playout - { - ProgramSchedule = new ProgramSchedule - { - Items = items - }, - Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, - Anchor = new PlayoutAnchor - { - NextStart = HoursAfterMidnight(1).UtcDateTime, - ScheduleItemsEnumeratorState = new CollectionEnumeratorState - { - Index = 0, - Seed = 1 - }, - DurationFinish = HoursAfterMidnight(3).UtcDateTime - } - }; - - var configRepo = new Mock(); - var televisionRepo = new FakeTelevisionRepository(); - var artistRepo = new Mock(); - var builder = new PlayoutBuilder( - configRepo.Object, - fakeRepository, - televisionRepo, - artistRepo.Object, - _logger); - - DateTimeOffset start = HoursAfterMidnight(0); - DateTimeOffset finish = start + TimeSpan.FromHours(5); - - Playout result = await builder.BuildPlayoutItems(playout, start, finish); - - result.Items.Count.Should().Be(4); - - result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(1)); - result.Items[0].MediaItemId.Should().Be(1); - result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(2)); - result.Items[1].MediaItemId.Should().Be(1); - - result.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(3)); - result.Items[2].MediaItemId.Should().Be(2); - result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(4)); - result.Items[3].MediaItemId.Should().Be(2); - - result.Anchor.ScheduleItemsEnumeratorState.Index.Should().Be(1); - result.Anchor.DurationFinish.Should().Be(HoursAfterMidnight(6).UtcDateTime); - } - - [Test] - public async Task Alternating_Duration_With_Filler_Should_Alternate_Schedule_Items() - { - var collectionOne = new Collection - { - Id = 1, - Name = "Duration Items 1", - MediaItems = new List - { - TestMovie(1, TimeSpan.FromMinutes(55), new DateTime(2020, 1, 1)) - } - }; - - var collectionTwo = new Collection - { - Id = 2, - Name = "Duration Items 2", - MediaItems = new List - { - TestMovie(2, TimeSpan.FromMinutes(55), new DateTime(2020, 1, 1)) - } - }; - - var collectionThree = new Collection - { - Id = 3, - Name = "Filler Items", - MediaItems = new List - { - TestMovie(3, TimeSpan.FromMinutes(5), new DateTime(2020, 1, 1)) - } - }; - - var fakeRepository = new FakeMediaCollectionRepository( - Map( - (collectionOne.Id, collectionOne.MediaItems.ToList()), - (collectionTwo.Id, collectionTwo.MediaItems.ToList()), - (collectionThree.Id, collectionThree.MediaItems.ToList()))); - - var items = new List - { - new ProgramScheduleItemDuration - { - Id = 1, - Index = 1, - Collection = collectionOne, - CollectionId = collectionOne.Id, - StartTime = null, - PlayoutDuration = TimeSpan.FromHours(3), - PlaybackOrder = PlaybackOrder.Chronological, - TailMode = TailMode.Filler, - TailFiller = new FillerPreset - { - FillerKind = FillerKind.Tail, - Collection = collectionThree, - CollectionId = collectionThree.Id - } - }, - new ProgramScheduleItemDuration - { - Id = 2, - Index = 2, - Collection = collectionTwo, - CollectionId = collectionTwo.Id, - StartTime = null, - PlayoutDuration = TimeSpan.FromHours(3), - PlaybackOrder = PlaybackOrder.Chronological, - TailMode = TailMode.Filler, - TailFiller = new FillerPreset - { - FillerKind = FillerKind.Tail, - Collection = collectionThree, - CollectionId = collectionThree.Id - } - } - }; - - var playout = new Playout - { - ProgramSchedule = new ProgramSchedule - { - Items = items - }, - Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, - }; - - var configRepo = new Mock(); - var televisionRepo = new FakeTelevisionRepository(); - var artistRepo = new Mock(); - var builder = new PlayoutBuilder( - configRepo.Object, - fakeRepository, - televisionRepo, - artistRepo.Object, - _logger); - - DateTimeOffset start = HoursAfterMidnight(0); - DateTimeOffset finish = start + TimeSpan.FromHours(6); - - Playout result = await builder.BuildPlayoutItems(playout, start, finish); - - result.Items.Count.Should().Be(12); - - result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromMinutes(0)); - result.Items[0].MediaItemId.Should().Be(1); - result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromMinutes(55)); - result.Items[1].MediaItemId.Should().Be(1); - result.Items[2].StartOffset.TimeOfDay.Should().Be(new TimeSpan(1, 50, 0)); - result.Items[2].MediaItemId.Should().Be(1); - - result.Items[3].StartOffset.TimeOfDay.Should().Be(new TimeSpan(2, 45, 0)); - result.Items[3].MediaItemId.Should().Be(3); - result.Items[4].StartOffset.TimeOfDay.Should().Be(new TimeSpan(2, 50, 0)); - result.Items[4].MediaItemId.Should().Be(3); - result.Items[5].StartOffset.TimeOfDay.Should().Be(new TimeSpan(2, 55, 0)); - result.Items[5].MediaItemId.Should().Be(3); - - result.Items[6].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(3)); - result.Items[6].MediaItemId.Should().Be(2); - result.Items[7].StartOffset.TimeOfDay.Should().Be(new TimeSpan(3, 55, 0)); - result.Items[7].MediaItemId.Should().Be(2); - result.Items[8].StartOffset.TimeOfDay.Should().Be(new TimeSpan(4, 50, 0)); - result.Items[8].MediaItemId.Should().Be(2); - - result.Items[9].StartOffset.TimeOfDay.Should().Be(new TimeSpan(5, 45, 0)); - result.Items[9].MediaItemId.Should().Be(3); - result.Items[10].StartOffset.TimeOfDay.Should().Be(new TimeSpan(5, 50, 0)); - result.Items[10].MediaItemId.Should().Be(3); - result.Items[11].StartOffset.TimeOfDay.Should().Be(new TimeSpan(5, 55, 0)); - result.Items[11].MediaItemId.Should().Be(3); - - result.Anchor.ScheduleItemsEnumeratorState.Index.Should().Be(0); - result.Anchor.DurationFinish.Should().BeNull(); - } - - [Test] - public async Task Duration_Should_Skip_Items_That_Are_Too_Long() - { - var collectionOne = new Collection - { - Id = 1, - Name = "Duration Items 1", - MediaItems = new List - { - TestMovie(1, TimeSpan.FromHours(2), new DateTime(2020, 1, 1)), - TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)), - TestMovie(3, TimeSpan.FromHours(2), new DateTime(2020, 1, 1)), - TestMovie(4, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)) - } - }; - - var fakeRepository = - new FakeMediaCollectionRepository(Map((collectionOne.Id, collectionOne.MediaItems.ToList()))); - - var items = new List - { - new ProgramScheduleItemDuration - { - Id = 1, - Index = 1, - Collection = collectionOne, - CollectionId = collectionOne.Id, - StartTime = null, - PlayoutDuration = TimeSpan.FromHours(1), - PlaybackOrder = PlaybackOrder.Chronological, - TailMode = TailMode.None, - } - }; - - var playout = new Playout - { - ProgramSchedule = new ProgramSchedule - { - Items = items - }, - Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, - }; - - var configRepo = new Mock(); - var televisionRepo = new FakeTelevisionRepository(); - var artistRepo = new Mock(); - var builder = new PlayoutBuilder( - configRepo.Object, - fakeRepository, - televisionRepo, - artistRepo.Object, - _logger); - - DateTimeOffset start = HoursAfterMidnight(0); - DateTimeOffset finish = start + TimeSpan.FromHours(6); - - Playout result = await builder.BuildPlayoutItems(playout, start, finish); - - result.Items.Count.Should().Be(6); - - result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(0)); - result.Items[0].MediaItemId.Should().Be(2); - result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(1)); - result.Items[1].MediaItemId.Should().Be(4); - result.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(2)); - result.Items[2].MediaItemId.Should().Be(2); - result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(3)); - result.Items[3].MediaItemId.Should().Be(4); - result.Items[4].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(4)); - result.Items[4].MediaItemId.Should().Be(2); - result.Items[5].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(5)); - result.Items[5].MediaItemId.Should().Be(4); - - result.Anchor.ScheduleItemsEnumeratorState.Index.Should().Be(0); - result.Anchor.DurationFinish.Should().BeNull(); - } - - private static DateTimeOffset HoursAfterMidnight(int hours) - { - DateTimeOffset now = DateTimeOffset.Now; - return now - now.TimeOfDay + TimeSpan.FromHours(hours); - } - - private static ProgramScheduleItem Flood(Collection mediaCollection, PlaybackOrder playbackOrder) => new ProgramScheduleItemFlood { Index = 1, - Collection = mediaCollection, - CollectionId = mediaCollection.Id, + Collection = floodCollection, + CollectionId = floodCollection.Id, StartTime = null, - PlaybackOrder = playbackOrder - }; - - private static Movie TestMovie(int id, TimeSpan duration, DateTime aired) => - new() + PlaybackOrder = PlaybackOrder.Chronological + }, + new ProgramScheduleItemOne { - Id = id, - MovieMetadata = new List { new() { ReleaseDate = aired } }, - MediaVersions = new List - { - new() { Duration = duration } - } - }; + Index = 2, + Collection = fixedCollection, + CollectionId = fixedCollection.Id, + StartTime = TimeSpan.FromHours(3), + PlaybackOrder = PlaybackOrder.Chronological + } + }; - private TestData TestDataFloodForItems(List mediaItems, PlaybackOrder playbackOrder) + var playout = new Playout { - var mediaCollection = new Collection + ProgramSchedule = new ProgramSchedule { - Id = 1, - MediaItems = mediaItems - }; + Items = items + }, + Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" } + }; - var configRepo = new Mock(); - var collectionRepo = new FakeMediaCollectionRepository(Map((mediaCollection.Id, mediaItems))); - var televisionRepo = new FakeTelevisionRepository(); - var artistRepo = new Mock(); - var builder = new PlayoutBuilder( - configRepo.Object, - collectionRepo, - televisionRepo, - artistRepo.Object, - _logger); + var configRepo = new Mock(); + var televisionRepo = new FakeTelevisionRepository(); + var artistRepo = new Mock(); + var builder = new PlayoutBuilder( + configRepo.Object, + fakeRepository, + televisionRepo, + artistRepo.Object, + _logger); - var items = new List { Flood(mediaCollection, playbackOrder) }; + DateTimeOffset start = HoursAfterMidnight(0); + DateTimeOffset finish = start + TimeSpan.FromHours(6); - var playout = new Playout - { - Id = 1, - ProgramSchedule = new ProgramSchedule { Items = items }, - Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" } - }; + Playout result = await builder.BuildPlayoutItems(playout, start, finish); - return new TestData(builder, playout); - } - - private record TestData(PlayoutBuilder Builder, Playout Playout); + result.Items.Count.Should().Be(5); + result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero); + result.Items[0].MediaItemId.Should().Be(1); + result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(1)); + result.Items[1].MediaItemId.Should().Be(2); + result.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(2)); + result.Items[2].MediaItemId.Should().Be(1); + result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(3)); + result.Items[3].MediaItemId.Should().Be(3); + result.Items[4].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(5)); + result.Items[4].MediaItemId.Should().Be(2); } -} + + [Test] + public async Task FloodContent_Should_FloodAroundFixedContent_Multiple() + { + var floodCollection = new Collection + { + Id = 1, + Name = "Flood Items", + MediaItems = new List + { + TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)), + TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 2, 1)) + } + }; + + var fixedCollection = new Collection + { + Id = 2, + Name = "Fixed Items", + MediaItems = new List + { + TestMovie(3, TimeSpan.FromHours(2), new DateTime(2020, 1, 1)), + TestMovie(4, TimeSpan.FromHours(1), new DateTime(2020, 1, 2)) + } + }; + + var fakeRepository = new FakeMediaCollectionRepository( + Map( + (floodCollection.Id, floodCollection.MediaItems.ToList()), + (fixedCollection.Id, fixedCollection.MediaItems.ToList()))); + + var items = new List + { + new ProgramScheduleItemFlood + { + Index = 1, + Collection = floodCollection, + CollectionId = floodCollection.Id, + StartTime = null, + PlaybackOrder = PlaybackOrder.Chronological + }, + new ProgramScheduleItemMultiple + { + Index = 2, + Collection = fixedCollection, + CollectionId = fixedCollection.Id, + StartTime = TimeSpan.FromHours(3), + Count = 2, + PlaybackOrder = PlaybackOrder.Chronological + } + }; + + var playout = new Playout + { + ProgramSchedule = new ProgramSchedule + { + Items = items + }, + Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" } + }; + + var configRepo = new Mock(); + var televisionRepo = new FakeTelevisionRepository(); + var artistRepo = new Mock(); + var builder = new PlayoutBuilder( + configRepo.Object, + fakeRepository, + televisionRepo, + artistRepo.Object, + _logger); + + DateTimeOffset start = HoursAfterMidnight(0); + DateTimeOffset finish = start + TimeSpan.FromHours(7); + + Playout result = await builder.BuildPlayoutItems(playout, start, finish); + + result.Items.Count.Should().Be(6); + + result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero); + result.Items[0].MediaItemId.Should().Be(1); + result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(1)); + result.Items[1].MediaItemId.Should().Be(2); + result.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(2)); + result.Items[2].MediaItemId.Should().Be(1); + + result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(3)); + result.Items[3].MediaItemId.Should().Be(3); + result.Items[4].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(5)); + result.Items[4].MediaItemId.Should().Be(4); + + result.Items[5].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); + result.Items[5].MediaItemId.Should().Be(2); + } + + [Test] + public async Task FloodContent_Should_FloodWithFixedStartTime() + { + var floodCollection = new Collection + { + Id = 1, + Name = "Flood Items", + MediaItems = new List + { + TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)), + TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 2, 1)) + } + }; + + var fixedCollection = new Collection + { + Id = 2, + Name = "Fixed Items", + MediaItems = new List + { + TestMovie(3, TimeSpan.FromHours(2), new DateTime(2020, 1, 1)), + TestMovie(4, TimeSpan.FromHours(1), new DateTime(2020, 1, 2)) + } + }; + + var fakeRepository = new FakeMediaCollectionRepository( + Map( + (floodCollection.Id, floodCollection.MediaItems.ToList()), + (fixedCollection.Id, fixedCollection.MediaItems.ToList()))); + + var items = new List + { + new ProgramScheduleItemFlood + { + Index = 1, + Collection = floodCollection, + CollectionId = floodCollection.Id, + StartTime = TimeSpan.FromHours(7), + PlaybackOrder = PlaybackOrder.Chronological + }, + new ProgramScheduleItemOne + { + Index = 2, + Collection = fixedCollection, + CollectionId = fixedCollection.Id, + StartTime = TimeSpan.FromHours(12), + PlaybackOrder = PlaybackOrder.Chronological + } + }; + + var playout = new Playout + { + ProgramSchedule = new ProgramSchedule + { + Items = items + }, + Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" } + }; + + var configRepo = new Mock(); + var televisionRepo = new FakeTelevisionRepository(); + var artistRepo = new Mock(); + var builder = new PlayoutBuilder( + configRepo.Object, + fakeRepository, + televisionRepo, + artistRepo.Object, + _logger); + + DateTimeOffset start = HoursAfterMidnight(0); + DateTimeOffset finish = start + TimeSpan.FromHours(24); + + Playout result = await builder.BuildPlayoutItems(playout, start, finish); + + result.Items.Count.Should().Be(6); + + result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(7)); + result.Items[0].MediaItemId.Should().Be(1); + result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(8)); + result.Items[1].MediaItemId.Should().Be(2); + result.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(9)); + result.Items[2].MediaItemId.Should().Be(1); + result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(10)); + result.Items[3].MediaItemId.Should().Be(2); + result.Items[4].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(11)); + result.Items[4].MediaItemId.Should().Be(1); + + result.Items[5].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(12)); + result.Items[5].MediaItemId.Should().Be(3); + } + + [Test] + public async Task FloodContent_Should_FloodWithFixedStartTime_FromAnchor() + { + var floodCollection = new Collection + { + Id = 1, + Name = "Flood Items", + MediaItems = new List + { + TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)), + TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 2, 1)) + } + }; + + var fixedCollection = new Collection + { + Id = 2, + Name = "Fixed Items", + MediaItems = new List + { + TestMovie(3, TimeSpan.FromHours(2), new DateTime(2020, 1, 1)), + TestMovie(4, TimeSpan.FromHours(1), new DateTime(2020, 1, 2)) + } + }; + + var fakeRepository = new FakeMediaCollectionRepository( + Map( + (floodCollection.Id, floodCollection.MediaItems.ToList()), + (fixedCollection.Id, fixedCollection.MediaItems.ToList()))); + + var items = new List + { + new ProgramScheduleItemFlood + { + Index = 1, + Collection = floodCollection, + CollectionId = floodCollection.Id, + StartTime = TimeSpan.FromHours(7), + PlaybackOrder = PlaybackOrder.Chronological + }, + new ProgramScheduleItemOne + { + Index = 2, + Collection = fixedCollection, + CollectionId = fixedCollection.Id, + StartTime = TimeSpan.FromHours(12), + PlaybackOrder = PlaybackOrder.Chronological + } + }; + + var playout = new Playout + { + ProgramSchedule = new ProgramSchedule + { + Items = items + }, + Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, + Anchor = new PlayoutAnchor + { + NextStart = HoursAfterMidnight(9).UtcDateTime, + ScheduleItemsEnumeratorState = new CollectionEnumeratorState + { + Index = 0, + Seed = 1 + }, + InFlood = true + } + }; + + var configRepo = new Mock(); + var televisionRepo = new FakeTelevisionRepository(); + var artistRepo = new Mock(); + var builder = new PlayoutBuilder( + configRepo.Object, + fakeRepository, + televisionRepo, + artistRepo.Object, + _logger); + + DateTimeOffset start = HoursAfterMidnight(0); + DateTimeOffset finish = start + TimeSpan.FromHours(32); + + Playout result = await builder.BuildPlayoutItems(playout, start, finish); + + result.Items.Count.Should().Be(5); + + result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(9)); + result.Items[0].MediaItemId.Should().Be(1); + result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(10)); + result.Items[1].MediaItemId.Should().Be(2); + result.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(11)); + result.Items[2].MediaItemId.Should().Be(1); + + result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(12)); + result.Items[3].MediaItemId.Should().Be(3); + + result.Items[4].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(7)); + result.Items[4].MediaItemId.Should().Be(2); + + result.Anchor.InFlood.Should().BeTrue(); + } + + [Test] + public async Task FloodContent_Should_FloodAroundFixedContent_DurationWithoutOfflineTail() + { + var floodCollection = new Collection + { + Id = 1, + Name = "Flood Items", + MediaItems = new List + { + TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)), + TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 2, 1)) + } + }; + + var fixedCollection = new Collection + { + Id = 2, + Name = "Fixed Items", + MediaItems = new List + { + TestMovie(3, TimeSpan.FromHours(0.75), new DateTime(2020, 1, 1)), + TestMovie(4, TimeSpan.FromHours(1.5), new DateTime(2020, 1, 2)) + } + }; + + var fakeRepository = new FakeMediaCollectionRepository( + Map( + (floodCollection.Id, floodCollection.MediaItems.ToList()), + (fixedCollection.Id, fixedCollection.MediaItems.ToList()))); + + var items = new List + { + new ProgramScheduleItemFlood + { + Index = 1, + Collection = floodCollection, + CollectionId = floodCollection.Id, + StartTime = null, + PlaybackOrder = PlaybackOrder.Chronological + }, + new ProgramScheduleItemDuration + { + Index = 2, + Collection = fixedCollection, + CollectionId = fixedCollection.Id, + StartTime = TimeSpan.FromHours(2), + PlayoutDuration = TimeSpan.FromHours(2), + TailMode = TailMode.None, // immediately continue + PlaybackOrder = PlaybackOrder.Chronological + } + }; + + var playout = new Playout + { + ProgramSchedule = new ProgramSchedule + { + Items = items + }, + Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" } + }; + + var configRepo = new Mock(); + var televisionRepo = new FakeTelevisionRepository(); + var artistRepo = new Mock(); + var builder = new PlayoutBuilder( + configRepo.Object, + fakeRepository, + televisionRepo, + artistRepo.Object, + _logger); + + DateTimeOffset start = HoursAfterMidnight(0); + DateTimeOffset finish = start + TimeSpan.FromHours(6); + + Playout result = await builder.BuildPlayoutItems(playout, start, finish); + + result.Items.Count.Should().Be(7); + + result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero); + result.Items[0].MediaItemId.Should().Be(1); + result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(1)); + result.Items[1].MediaItemId.Should().Be(2); + + result.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(2)); + result.Items[2].MediaItemId.Should().Be(3); + + result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(2.75)); + result.Items[3].MediaItemId.Should().Be(1); + result.Items[4].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(3.75)); + result.Items[4].MediaItemId.Should().Be(2); + + result.Items[5].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(4.75)); + result.Items[5].MediaItemId.Should().Be(1); + result.Items[6].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(5.75)); + result.Items[6].MediaItemId.Should().Be(2); + } + + [Test] + public async Task MultipleContent_Should_WrapAroundDynamicContent_DurationWithoutOfflineTail() + { + var multipleCollection = new Collection + { + Id = 1, + Name = "Multiple Items", + MediaItems = new List + { + TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)), + TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 2, 1)) + } + }; + + var dynamicCollection = new Collection + { + Id = 2, + Name = "Dynamic Items", + MediaItems = new List + { + TestMovie(3, TimeSpan.FromHours(0.75), new DateTime(2020, 1, 1)), + TestMovie(4, TimeSpan.FromHours(1.5), new DateTime(2020, 1, 2)) + } + }; + + var fakeRepository = new FakeMediaCollectionRepository( + Map( + (multipleCollection.Id, multipleCollection.MediaItems.ToList()), + (dynamicCollection.Id, dynamicCollection.MediaItems.ToList()))); + + var items = new List + { + new ProgramScheduleItemMultiple + { + Index = 1, + Collection = multipleCollection, + CollectionId = multipleCollection.Id, + StartTime = null, + Count = 2, + PlaybackOrder = PlaybackOrder.Chronological + }, + new ProgramScheduleItemDuration + { + Index = 2, + Collection = dynamicCollection, + CollectionId = dynamicCollection.Id, + StartTime = null, + PlayoutDuration = TimeSpan.FromHours(2), + TailMode = TailMode.None, // immediately continue + PlaybackOrder = PlaybackOrder.Chronological + } + }; + + var playout = new Playout + { + ProgramSchedule = new ProgramSchedule + { + Items = items + }, + Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" } + }; + + var configRepo = new Mock(); + var televisionRepo = new FakeTelevisionRepository(); + var artistRepo = new Mock(); + var builder = new PlayoutBuilder( + configRepo.Object, + fakeRepository, + televisionRepo, + artistRepo.Object, + _logger); + + DateTimeOffset start = HoursAfterMidnight(0); + DateTimeOffset finish = start + TimeSpan.FromHours(6); + + Playout result = await builder.BuildPlayoutItems(playout, start, finish); + + result.Items.Count.Should().Be(6); + + result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero); + result.Items[0].MediaItemId.Should().Be(1); + result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(1)); + result.Items[1].MediaItemId.Should().Be(2); + + result.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(2)); + result.Items[2].MediaItemId.Should().Be(3); + + result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(2.75)); + result.Items[3].MediaItemId.Should().Be(1); + result.Items[4].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(3.75)); + result.Items[4].MediaItemId.Should().Be(2); + + result.Items[5].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(4.75)); + result.Items[5].MediaItemId.Should().Be(4); + } + + [Test] + public async Task Alternating_MultipleContent_Should_Maintain_Counts() + { + var collectionOne = new Collection + { + Id = 1, + Name = "Multiple Items 1", + MediaItems = new List + { + TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)) + } + }; + + var collectionTwo = new Collection + { + Id = 2, + Name = "Multiple Items 2", + MediaItems = new List + { + TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)) + } + }; + + var fakeRepository = new FakeMediaCollectionRepository( + Map( + (collectionOne.Id, collectionOne.MediaItems.ToList()), + (collectionTwo.Id, collectionTwo.MediaItems.ToList()))); + + var items = new List + { + new ProgramScheduleItemMultiple + { + Id = 1, + Index = 1, + Collection = collectionOne, + CollectionId = collectionOne.Id, + StartTime = null, + Count = 3, + PlaybackOrder = PlaybackOrder.Chronological + }, + new ProgramScheduleItemMultiple + { + Id = 2, + Index = 2, + Collection = collectionTwo, + CollectionId = collectionTwo.Id, + StartTime = null, + Count = 3, + PlaybackOrder = PlaybackOrder.Chronological + } + }; + + var playout = new Playout + { + ProgramSchedule = new ProgramSchedule + { + Items = items + }, + Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, + Anchor = new PlayoutAnchor + { + NextStart = HoursAfterMidnight(1).UtcDateTime, + ScheduleItemsEnumeratorState = new CollectionEnumeratorState + { + Index = 0, + Seed = 1 + }, + MultipleRemaining = 2 + } + }; + + var configRepo = new Mock(); + var televisionRepo = new FakeTelevisionRepository(); + var artistRepo = new Mock(); + var builder = new PlayoutBuilder( + configRepo.Object, + fakeRepository, + televisionRepo, + artistRepo.Object, + _logger); + + DateTimeOffset start = HoursAfterMidnight(0); + DateTimeOffset finish = start + TimeSpan.FromHours(5); + + Playout result = await builder.BuildPlayoutItems(playout, start, finish); + + result.Items.Count.Should().Be(4); + + result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(1)); + result.Items[0].MediaItemId.Should().Be(1); + result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(2)); + result.Items[1].MediaItemId.Should().Be(1); + + result.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(3)); + result.Items[2].MediaItemId.Should().Be(2); + result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(4)); + result.Items[3].MediaItemId.Should().Be(2); + + result.Anchor.ScheduleItemsEnumeratorState.Index.Should().Be(1); + result.Anchor.MultipleRemaining.Should().Be(1); + } + + [Test] + public async Task Auto_Zero_MultipleCount() + { + var collectionOne = new Collection + { + Id = 1, + Name = "Multiple Items 1", + MediaItems = new List + { + TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)), + TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)), + TestMovie(3, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)) + } + }; + + var collectionTwo = new Collection + { + Id = 2, + Name = "Multiple Items 2", + MediaItems = new List + { + TestMovie(4, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)), + TestMovie(5, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)), + } + }; + + var fakeRepository = new FakeMediaCollectionRepository( + Map( + (collectionOne.Id, collectionOne.MediaItems.ToList()), + (collectionTwo.Id, collectionTwo.MediaItems.ToList()))); + + var items = new List + { + new ProgramScheduleItemMultiple + { + Id = 1, + Index = 1, + Collection = collectionOne, + CollectionId = collectionOne.Id, + StartTime = null, + Count = 0, + PlaybackOrder = PlaybackOrder.Chronological + }, + new ProgramScheduleItemMultiple + { + Id = 2, + Index = 2, + Collection = collectionTwo, + CollectionId = collectionTwo.Id, + StartTime = null, + Count = 0, + PlaybackOrder = PlaybackOrder.Chronological + } + }; + + var playout = new Playout + { + ProgramSchedule = new ProgramSchedule + { + Items = items + }, + Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, + }; + + var configRepo = new Mock(); + var televisionRepo = new FakeTelevisionRepository(); + var artistRepo = new Mock(); + var builder = new PlayoutBuilder( + configRepo.Object, + fakeRepository, + televisionRepo, + artistRepo.Object, + _logger); + + DateTimeOffset start = HoursAfterMidnight(0); + DateTimeOffset finish = start + TimeSpan.FromHours(5); + + Playout result = await builder.BuildPlayoutItems(playout, start, finish); + + result.Items.Count.Should().Be(5); + + result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(0)); + result.Items[0].MediaItemId.Should().Be(1); + result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(1)); + result.Items[1].MediaItemId.Should().Be(2); + result.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(2)); + result.Items[2].MediaItemId.Should().Be(3); + + result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(3)); + result.Items[3].MediaItemId.Should().Be(4); + result.Items[4].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(4)); + result.Items[4].MediaItemId.Should().Be(5); + + result.Anchor.ScheduleItemsEnumeratorState.Index.Should().Be(0); + result.Anchor.MultipleRemaining.Should().BeNull(); + } + + [Test] + public async Task Alternating_Duration_Should_Maintain_Duration() + { + var collectionOne = new Collection + { + Id = 1, + Name = "Duration Items 1", + MediaItems = new List + { + TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)) + } + }; + + var collectionTwo = new Collection + { + Id = 2, + Name = "Duration Items 2", + MediaItems = new List + { + TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)) + } + }; + + var fakeRepository = new FakeMediaCollectionRepository( + Map( + (collectionOne.Id, collectionOne.MediaItems.ToList()), + (collectionTwo.Id, collectionTwo.MediaItems.ToList()))); + + var items = new List + { + new ProgramScheduleItemDuration + { + Id = 1, + Index = 1, + Collection = collectionOne, + CollectionId = collectionOne.Id, + StartTime = null, + PlayoutDuration = TimeSpan.FromHours(3), + TailMode = TailMode.None, + PlaybackOrder = PlaybackOrder.Chronological + }, + new ProgramScheduleItemDuration + { + Id = 2, + Index = 2, + Collection = collectionTwo, + CollectionId = collectionTwo.Id, + StartTime = null, + PlayoutDuration = TimeSpan.FromHours(3), + TailMode = TailMode.None, + PlaybackOrder = PlaybackOrder.Chronological + } + }; + + var playout = new Playout + { + ProgramSchedule = new ProgramSchedule + { + Items = items + }, + Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, + Anchor = new PlayoutAnchor + { + NextStart = HoursAfterMidnight(1).UtcDateTime, + ScheduleItemsEnumeratorState = new CollectionEnumeratorState + { + Index = 0, + Seed = 1 + }, + DurationFinish = HoursAfterMidnight(3).UtcDateTime + } + }; + + var configRepo = new Mock(); + var televisionRepo = new FakeTelevisionRepository(); + var artistRepo = new Mock(); + var builder = new PlayoutBuilder( + configRepo.Object, + fakeRepository, + televisionRepo, + artistRepo.Object, + _logger); + + DateTimeOffset start = HoursAfterMidnight(0); + DateTimeOffset finish = start + TimeSpan.FromHours(5); + + Playout result = await builder.BuildPlayoutItems(playout, start, finish); + + result.Items.Count.Should().Be(4); + + result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(1)); + result.Items[0].MediaItemId.Should().Be(1); + result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(2)); + result.Items[1].MediaItemId.Should().Be(1); + + result.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(3)); + result.Items[2].MediaItemId.Should().Be(2); + result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(4)); + result.Items[3].MediaItemId.Should().Be(2); + + result.Anchor.ScheduleItemsEnumeratorState.Index.Should().Be(1); + result.Anchor.DurationFinish.Should().Be(HoursAfterMidnight(6).UtcDateTime); + } + + [Test] + public async Task Alternating_Duration_With_Filler_Should_Alternate_Schedule_Items() + { + var collectionOne = new Collection + { + Id = 1, + Name = "Duration Items 1", + MediaItems = new List + { + TestMovie(1, TimeSpan.FromMinutes(55), new DateTime(2020, 1, 1)) + } + }; + + var collectionTwo = new Collection + { + Id = 2, + Name = "Duration Items 2", + MediaItems = new List + { + TestMovie(2, TimeSpan.FromMinutes(55), new DateTime(2020, 1, 1)) + } + }; + + var collectionThree = new Collection + { + Id = 3, + Name = "Filler Items", + MediaItems = new List + { + TestMovie(3, TimeSpan.FromMinutes(5), new DateTime(2020, 1, 1)) + } + }; + + var fakeRepository = new FakeMediaCollectionRepository( + Map( + (collectionOne.Id, collectionOne.MediaItems.ToList()), + (collectionTwo.Id, collectionTwo.MediaItems.ToList()), + (collectionThree.Id, collectionThree.MediaItems.ToList()))); + + var items = new List + { + new ProgramScheduleItemDuration + { + Id = 1, + Index = 1, + Collection = collectionOne, + CollectionId = collectionOne.Id, + StartTime = null, + PlayoutDuration = TimeSpan.FromHours(3), + PlaybackOrder = PlaybackOrder.Chronological, + TailMode = TailMode.Filler, + TailFiller = new FillerPreset + { + FillerKind = FillerKind.Tail, + Collection = collectionThree, + CollectionId = collectionThree.Id + } + }, + new ProgramScheduleItemDuration + { + Id = 2, + Index = 2, + Collection = collectionTwo, + CollectionId = collectionTwo.Id, + StartTime = null, + PlayoutDuration = TimeSpan.FromHours(3), + PlaybackOrder = PlaybackOrder.Chronological, + TailMode = TailMode.Filler, + TailFiller = new FillerPreset + { + FillerKind = FillerKind.Tail, + Collection = collectionThree, + CollectionId = collectionThree.Id + } + } + }; + + var playout = new Playout + { + ProgramSchedule = new ProgramSchedule + { + Items = items + }, + Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, + }; + + var configRepo = new Mock(); + var televisionRepo = new FakeTelevisionRepository(); + var artistRepo = new Mock(); + var builder = new PlayoutBuilder( + configRepo.Object, + fakeRepository, + televisionRepo, + artistRepo.Object, + _logger); + + DateTimeOffset start = HoursAfterMidnight(0); + DateTimeOffset finish = start + TimeSpan.FromHours(6); + + Playout result = await builder.BuildPlayoutItems(playout, start, finish); + + result.Items.Count.Should().Be(12); + + result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromMinutes(0)); + result.Items[0].MediaItemId.Should().Be(1); + result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromMinutes(55)); + result.Items[1].MediaItemId.Should().Be(1); + result.Items[2].StartOffset.TimeOfDay.Should().Be(new TimeSpan(1, 50, 0)); + result.Items[2].MediaItemId.Should().Be(1); + + result.Items[3].StartOffset.TimeOfDay.Should().Be(new TimeSpan(2, 45, 0)); + result.Items[3].MediaItemId.Should().Be(3); + result.Items[4].StartOffset.TimeOfDay.Should().Be(new TimeSpan(2, 50, 0)); + result.Items[4].MediaItemId.Should().Be(3); + result.Items[5].StartOffset.TimeOfDay.Should().Be(new TimeSpan(2, 55, 0)); + result.Items[5].MediaItemId.Should().Be(3); + + result.Items[6].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(3)); + result.Items[6].MediaItemId.Should().Be(2); + result.Items[7].StartOffset.TimeOfDay.Should().Be(new TimeSpan(3, 55, 0)); + result.Items[7].MediaItemId.Should().Be(2); + result.Items[8].StartOffset.TimeOfDay.Should().Be(new TimeSpan(4, 50, 0)); + result.Items[8].MediaItemId.Should().Be(2); + + result.Items[9].StartOffset.TimeOfDay.Should().Be(new TimeSpan(5, 45, 0)); + result.Items[9].MediaItemId.Should().Be(3); + result.Items[10].StartOffset.TimeOfDay.Should().Be(new TimeSpan(5, 50, 0)); + result.Items[10].MediaItemId.Should().Be(3); + result.Items[11].StartOffset.TimeOfDay.Should().Be(new TimeSpan(5, 55, 0)); + result.Items[11].MediaItemId.Should().Be(3); + + result.Anchor.ScheduleItemsEnumeratorState.Index.Should().Be(0); + result.Anchor.DurationFinish.Should().BeNull(); + } + + [Test] + public async Task Duration_Should_Skip_Items_That_Are_Too_Long() + { + var collectionOne = new Collection + { + Id = 1, + Name = "Duration Items 1", + MediaItems = new List + { + TestMovie(1, TimeSpan.FromHours(2), new DateTime(2020, 1, 1)), + TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)), + TestMovie(3, TimeSpan.FromHours(2), new DateTime(2020, 1, 1)), + TestMovie(4, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)) + } + }; + + var fakeRepository = + new FakeMediaCollectionRepository(Map((collectionOne.Id, collectionOne.MediaItems.ToList()))); + + var items = new List + { + new ProgramScheduleItemDuration + { + Id = 1, + Index = 1, + Collection = collectionOne, + CollectionId = collectionOne.Id, + StartTime = null, + PlayoutDuration = TimeSpan.FromHours(1), + PlaybackOrder = PlaybackOrder.Chronological, + TailMode = TailMode.None, + } + }; + + var playout = new Playout + { + ProgramSchedule = new ProgramSchedule + { + Items = items + }, + Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, + }; + + var configRepo = new Mock(); + var televisionRepo = new FakeTelevisionRepository(); + var artistRepo = new Mock(); + var builder = new PlayoutBuilder( + configRepo.Object, + fakeRepository, + televisionRepo, + artistRepo.Object, + _logger); + + DateTimeOffset start = HoursAfterMidnight(0); + DateTimeOffset finish = start + TimeSpan.FromHours(6); + + Playout result = await builder.BuildPlayoutItems(playout, start, finish); + + result.Items.Count.Should().Be(6); + + result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(0)); + result.Items[0].MediaItemId.Should().Be(2); + result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(1)); + result.Items[1].MediaItemId.Should().Be(4); + result.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(2)); + result.Items[2].MediaItemId.Should().Be(2); + result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(3)); + result.Items[3].MediaItemId.Should().Be(4); + result.Items[4].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(4)); + result.Items[4].MediaItemId.Should().Be(2); + result.Items[5].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(5)); + result.Items[5].MediaItemId.Should().Be(4); + + result.Anchor.ScheduleItemsEnumeratorState.Index.Should().Be(0); + result.Anchor.DurationFinish.Should().BeNull(); + } + + private static DateTimeOffset HoursAfterMidnight(int hours) + { + DateTimeOffset now = DateTimeOffset.Now; + return now - now.TimeOfDay + TimeSpan.FromHours(hours); + } + + private static ProgramScheduleItem Flood(Collection mediaCollection, PlaybackOrder playbackOrder) => + new ProgramScheduleItemFlood + { + Index = 1, + Collection = mediaCollection, + CollectionId = mediaCollection.Id, + StartTime = null, + PlaybackOrder = playbackOrder + }; + + private static Movie TestMovie(int id, TimeSpan duration, DateTime aired) => + new() + { + Id = id, + MovieMetadata = new List { new() { ReleaseDate = aired } }, + MediaVersions = new List + { + new() { Duration = duration } + } + }; + + private TestData TestDataFloodForItems(List mediaItems, PlaybackOrder playbackOrder) + { + var mediaCollection = new Collection + { + Id = 1, + MediaItems = mediaItems + }; + + var configRepo = new Mock(); + var collectionRepo = new FakeMediaCollectionRepository(Map((mediaCollection.Id, mediaItems))); + var televisionRepo = new FakeTelevisionRepository(); + var artistRepo = new Mock(); + var builder = new PlayoutBuilder( + configRepo.Object, + collectionRepo, + televisionRepo, + artistRepo.Object, + _logger); + + var items = new List { Flood(mediaCollection, playbackOrder) }; + + var playout = new Playout + { + Id = 1, + ProgramSchedule = new ProgramSchedule { Items = items }, + Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" } + }; + + return new TestData(builder, playout); + } + + private record TestData(PlayoutBuilder Builder, Playout Playout); +} \ No newline at end of file diff --git a/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerBaseTests.cs b/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerBaseTests.cs index 6022096db..443cdb1ae 100644 --- a/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerBaseTests.cs +++ b/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerBaseTests.cs @@ -1,205 +1,202 @@ -using System; -using System.Collections.Generic; -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Filler; using ErsatzTV.Core.Interfaces.Scheduling; using ErsatzTV.Core.Scheduling; using FluentAssertions; using NUnit.Framework; -namespace ErsatzTV.Core.Tests.Scheduling +namespace ErsatzTV.Core.Tests.Scheduling; + +[TestFixture] +public class PlayoutModeSchedulerBaseTests { - [TestFixture] - public class PlayoutModeSchedulerBaseTests + [Test] + public void CalculateEndTimeWithFiller_Should_Not_Touch_Enumerator() { - [Test] - public void CalculateEndTimeWithFiller_Should_Not_Touch_Enumerator() + var collection = new Collection { - var collection = new Collection - { - Id = 1, - Name = "Filler Items", - MediaItems = new List() - }; + Id = 1, + Name = "Filler Items", + MediaItems = new List() + }; - for (var i = 0; i < 5; i++) - { - collection.MediaItems.Add(TestMovie(i + 1, TimeSpan.FromHours(i + 1), new DateTime(2020, 2, i + 1))); - } - - var fillerPreset = new FillerPreset - { - FillerKind = FillerKind.PreRoll, - FillerMode = FillerMode.Count, - Count = 3, - Collection = collection, - CollectionId = collection.Id - }; - - var enumerator = new ChronologicalMediaCollectionEnumerator( - collection.MediaItems, - new CollectionEnumeratorState { Index = 0, Seed = 1 }); - - DateTimeOffset result = PlayoutModeSchedulerBase - .CalculateEndTimeWithFiller( - new Dictionary - { - { CollectionKey.ForFillerPreset(fillerPreset), enumerator } - }, - new ProgramScheduleItemOne - { - PreRollFiller = fillerPreset - }, - new DateTimeOffset(2020, 2, 1, 12, 0, 0, TimeSpan.FromHours(-5)), - new TimeSpan(0, 12, 30), - new List()); - - result.Should().Be(new DateTimeOffset(2020, 2, 1, 18, 12, 30, TimeSpan.FromHours(-5))); - enumerator.State.Index.Should().Be(0); - enumerator.State.Seed.Should().Be(1); + for (var i = 0; i < 5; i++) + { + collection.MediaItems.Add(TestMovie(i + 1, TimeSpan.FromHours(i + 1), new DateTime(2020, 2, i + 1))); } - [Test] - public void CalculateEndTimeWithFiller_Should_Pad_To_15_Minutes_15() + var fillerPreset = new FillerPreset { - DateTimeOffset result = PlayoutModeSchedulerBase - .CalculateEndTimeWithFiller( - new Dictionary(), - new ProgramScheduleItemOne - { - MidRollFiller = new FillerPreset - { - FillerKind = FillerKind.MidRoll, - FillerMode = FillerMode.Pad, - PadToNearestMinute = 15 - } - }, - new DateTimeOffset(2020, 2, 1, 12, 0, 0, TimeSpan.FromHours(-5)), - new TimeSpan(0, 12, 30), - new List()); + FillerKind = FillerKind.PreRoll, + FillerMode = FillerMode.Count, + Count = 3, + Collection = collection, + CollectionId = collection.Id + }; - result.Should().Be(new DateTimeOffset(2020, 2, 1, 12, 15, 0, TimeSpan.FromHours(-5))); - } - - [Test] - public void CalculateEndTimeWithFiller_Should_Pad_To_15_Minutes_30() - { - DateTimeOffset result = PlayoutModeSchedulerBase - .CalculateEndTimeWithFiller( - new Dictionary(), - new ProgramScheduleItemOne - { - MidRollFiller = new FillerPreset - { - FillerKind = FillerKind.MidRoll, - FillerMode = FillerMode.Pad, - PadToNearestMinute = 15 - } - }, - new DateTimeOffset(2020, 2, 1, 12, 16, 0, TimeSpan.FromHours(-5)), - new TimeSpan(0, 12, 30), - new List()); + var enumerator = new ChronologicalMediaCollectionEnumerator( + collection.MediaItems, + new CollectionEnumeratorState { Index = 0, Seed = 1 }); - result.Should().Be(new DateTimeOffset(2020, 2, 1, 12, 30, 0, TimeSpan.FromHours(-5))); - } - - [Test] - public void CalculateEndTimeWithFiller_Should_Pad_To_15_Minutes_45() - { - DateTimeOffset result = PlayoutModeSchedulerBase - .CalculateEndTimeWithFiller( - new Dictionary(), - new ProgramScheduleItemOne - { - MidRollFiller = new FillerPreset - { - FillerKind = FillerKind.MidRoll, - FillerMode = FillerMode.Pad, - PadToNearestMinute = 15 - } - }, - new DateTimeOffset(2020, 2, 1, 12, 30, 0, TimeSpan.FromHours(-5)), - new TimeSpan(0, 12, 30), - new List()); - - result.Should().Be(new DateTimeOffset(2020, 2, 1, 12, 45, 0, TimeSpan.FromHours(-5))); - } - - [Test] - public void CalculateEndTimeWithFiller_Should_Pad_To_15_Minutes_00() - { - DateTimeOffset result = PlayoutModeSchedulerBase - .CalculateEndTimeWithFiller( - new Dictionary(), - new ProgramScheduleItemOne - { - MidRollFiller = new FillerPreset - { - FillerKind = FillerKind.MidRoll, - FillerMode = FillerMode.Pad, - PadToNearestMinute = 15 - } - }, - new DateTimeOffset(2020, 2, 1, 12, 46, 0, TimeSpan.FromHours(-5)), - new TimeSpan(0, 12, 30), - new List()); - - result.Should().Be(new DateTimeOffset(2020, 2, 1, 13, 0, 0, TimeSpan.FromHours(-5))); - } - - [Test] - public void CalculateEndTimeWithFiller_Should_Pad_To_30_Minutes_30() - { - DateTimeOffset result = PlayoutModeSchedulerBase - .CalculateEndTimeWithFiller( - new Dictionary(), - new ProgramScheduleItemOne - { - MidRollFiller = new FillerPreset - { - FillerKind = FillerKind.MidRoll, - FillerMode = FillerMode.Pad, - PadToNearestMinute = 30 - } - }, - new DateTimeOffset(2020, 2, 1, 12, 0, 0, TimeSpan.FromHours(-5)), - new TimeSpan(0, 12, 30), - new List()); - - result.Should().Be(new DateTimeOffset(2020, 2, 1, 12, 30, 0, TimeSpan.FromHours(-5))); - } - - [Test] - public void CalculateEndTimeWithFiller_Should_Pad_To_30_Minutes_00() - { - DateTimeOffset result = PlayoutModeSchedulerBase - .CalculateEndTimeWithFiller( - new Dictionary(), - new ProgramScheduleItemOne - { - MidRollFiller = new FillerPreset - { - FillerKind = FillerKind.MidRoll, - FillerMode = FillerMode.Pad, - PadToNearestMinute = 30 - } - }, - new DateTimeOffset(2020, 2, 1, 12, 20, 0, TimeSpan.FromHours(-5)), - new TimeSpan(0, 12, 30), - new List()); - - result.Should().Be(new DateTimeOffset(2020, 2, 1, 13, 0, 0, TimeSpan.FromHours(-5))); - } - - private static Movie TestMovie(int id, TimeSpan duration, DateTime aired) => - new() - { - Id = id, - MovieMetadata = new List { new() { ReleaseDate = aired } }, - MediaVersions = new List + DateTimeOffset result = PlayoutModeSchedulerBase + .CalculateEndTimeWithFiller( + new Dictionary { - new() { Duration = duration } - } - }; + { CollectionKey.ForFillerPreset(fillerPreset), enumerator } + }, + new ProgramScheduleItemOne + { + PreRollFiller = fillerPreset + }, + new DateTimeOffset(2020, 2, 1, 12, 0, 0, TimeSpan.FromHours(-5)), + new TimeSpan(0, 12, 30), + new List()); + + result.Should().Be(new DateTimeOffset(2020, 2, 1, 18, 12, 30, TimeSpan.FromHours(-5))); + enumerator.State.Index.Should().Be(0); + enumerator.State.Seed.Should().Be(1); } -} + + [Test] + public void CalculateEndTimeWithFiller_Should_Pad_To_15_Minutes_15() + { + DateTimeOffset result = PlayoutModeSchedulerBase + .CalculateEndTimeWithFiller( + new Dictionary(), + new ProgramScheduleItemOne + { + MidRollFiller = new FillerPreset + { + FillerKind = FillerKind.MidRoll, + FillerMode = FillerMode.Pad, + PadToNearestMinute = 15 + } + }, + new DateTimeOffset(2020, 2, 1, 12, 0, 0, TimeSpan.FromHours(-5)), + new TimeSpan(0, 12, 30), + new List()); + + result.Should().Be(new DateTimeOffset(2020, 2, 1, 12, 15, 0, TimeSpan.FromHours(-5))); + } + + [Test] + public void CalculateEndTimeWithFiller_Should_Pad_To_15_Minutes_30() + { + DateTimeOffset result = PlayoutModeSchedulerBase + .CalculateEndTimeWithFiller( + new Dictionary(), + new ProgramScheduleItemOne + { + MidRollFiller = new FillerPreset + { + FillerKind = FillerKind.MidRoll, + FillerMode = FillerMode.Pad, + PadToNearestMinute = 15 + } + }, + new DateTimeOffset(2020, 2, 1, 12, 16, 0, TimeSpan.FromHours(-5)), + new TimeSpan(0, 12, 30), + new List()); + + result.Should().Be(new DateTimeOffset(2020, 2, 1, 12, 30, 0, TimeSpan.FromHours(-5))); + } + + [Test] + public void CalculateEndTimeWithFiller_Should_Pad_To_15_Minutes_45() + { + DateTimeOffset result = PlayoutModeSchedulerBase + .CalculateEndTimeWithFiller( + new Dictionary(), + new ProgramScheduleItemOne + { + MidRollFiller = new FillerPreset + { + FillerKind = FillerKind.MidRoll, + FillerMode = FillerMode.Pad, + PadToNearestMinute = 15 + } + }, + new DateTimeOffset(2020, 2, 1, 12, 30, 0, TimeSpan.FromHours(-5)), + new TimeSpan(0, 12, 30), + new List()); + + result.Should().Be(new DateTimeOffset(2020, 2, 1, 12, 45, 0, TimeSpan.FromHours(-5))); + } + + [Test] + public void CalculateEndTimeWithFiller_Should_Pad_To_15_Minutes_00() + { + DateTimeOffset result = PlayoutModeSchedulerBase + .CalculateEndTimeWithFiller( + new Dictionary(), + new ProgramScheduleItemOne + { + MidRollFiller = new FillerPreset + { + FillerKind = FillerKind.MidRoll, + FillerMode = FillerMode.Pad, + PadToNearestMinute = 15 + } + }, + new DateTimeOffset(2020, 2, 1, 12, 46, 0, TimeSpan.FromHours(-5)), + new TimeSpan(0, 12, 30), + new List()); + + result.Should().Be(new DateTimeOffset(2020, 2, 1, 13, 0, 0, TimeSpan.FromHours(-5))); + } + + [Test] + public void CalculateEndTimeWithFiller_Should_Pad_To_30_Minutes_30() + { + DateTimeOffset result = PlayoutModeSchedulerBase + .CalculateEndTimeWithFiller( + new Dictionary(), + new ProgramScheduleItemOne + { + MidRollFiller = new FillerPreset + { + FillerKind = FillerKind.MidRoll, + FillerMode = FillerMode.Pad, + PadToNearestMinute = 30 + } + }, + new DateTimeOffset(2020, 2, 1, 12, 0, 0, TimeSpan.FromHours(-5)), + new TimeSpan(0, 12, 30), + new List()); + + result.Should().Be(new DateTimeOffset(2020, 2, 1, 12, 30, 0, TimeSpan.FromHours(-5))); + } + + [Test] + public void CalculateEndTimeWithFiller_Should_Pad_To_30_Minutes_00() + { + DateTimeOffset result = PlayoutModeSchedulerBase + .CalculateEndTimeWithFiller( + new Dictionary(), + new ProgramScheduleItemOne + { + MidRollFiller = new FillerPreset + { + FillerKind = FillerKind.MidRoll, + FillerMode = FillerMode.Pad, + PadToNearestMinute = 30 + } + }, + new DateTimeOffset(2020, 2, 1, 12, 20, 0, TimeSpan.FromHours(-5)), + new TimeSpan(0, 12, 30), + new List()); + + result.Should().Be(new DateTimeOffset(2020, 2, 1, 13, 0, 0, TimeSpan.FromHours(-5))); + } + + private static Movie TestMovie(int id, TimeSpan duration, DateTime aired) => + new() + { + Id = id, + MovieMetadata = new List { new() { ReleaseDate = aired } }, + MediaVersions = new List + { + new() { Duration = duration } + } + }; +} \ No newline at end of file diff --git a/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerDurationTests.cs b/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerDurationTests.cs index 0b9e91db8..f8868b5eb 100644 --- a/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerDurationTests.cs +++ b/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerDurationTests.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Filler; using ErsatzTV.Core.Scheduling; using FluentAssertions; @@ -9,690 +6,689 @@ using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; -namespace ErsatzTV.Core.Tests.Scheduling +namespace ErsatzTV.Core.Tests.Scheduling; + +[TestFixture] +public class PlayoutModeSchedulerDurationTests : SchedulerTestBase { - [TestFixture] - public class PlayoutModeSchedulerDurationTests : SchedulerTestBase + [Test] + public void Should_Fill_Exact_Duration() { - [Test] - public void Should_Fill_Exact_Duration() + Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromHours(1)); + + var scheduleItem = new ProgramScheduleItemDuration { - Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromHours(1)); + Id = 1, + Index = 1, + Collection = collectionOne, + CollectionId = collectionOne.Id, + StartTime = null, + PlayoutDuration = TimeSpan.FromHours(3), + TailMode = TailMode.None, + PlaybackOrder = PlaybackOrder.Chronological + }; - var scheduleItem = new ProgramScheduleItemDuration - { - Id = 1, - Index = 1, - Collection = collectionOne, - CollectionId = collectionOne.Id, - StartTime = null, - PlayoutDuration = TimeSpan.FromHours(3), - TailMode = TailMode.None, - PlaybackOrder = PlaybackOrder.Chronological - }; + var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( + new List { scheduleItem }, + new CollectionEnumeratorState()); - var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( - new List { scheduleItem }, - new CollectionEnumeratorState()); + var enumerator = new ChronologicalMediaCollectionEnumerator( + collectionOne.MediaItems, + new CollectionEnumeratorState()); - var enumerator = new ChronologicalMediaCollectionEnumerator( - collectionOne.MediaItems, - new CollectionEnumeratorState()); - - PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); + PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); - var scheduler = new PlayoutModeSchedulerDuration(new Mock().Object); - (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( - startState, - CollectionEnumerators(scheduleItem, enumerator), - scheduleItem, - NextScheduleItem, - HardStop(scheduleItemsEnumerator)); - - playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3)); - playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); - - playoutBuilderState.NextGuideGroup.Should().Be(4); - playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); - playoutBuilderState.InFlood.Should().BeFalse(); - playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); - playoutBuilderState.InDurationFiller.Should().BeFalse(); - playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); - - enumerator.State.Index.Should().Be(1); - - playoutItems.Count.Should().Be(3); - - playoutItems[0].MediaItemId.Should().Be(1); - playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); - playoutItems[0].GuideGroup.Should().Be(1); - playoutItems[0].FillerKind.Should().Be(FillerKind.None); - playoutItems[0].GuideFinish.HasValue.Should().BeFalse(); - - playoutItems[1].MediaItemId.Should().Be(2); - playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddHours(1)); - playoutItems[1].GuideGroup.Should().Be(2); - playoutItems[1].FillerKind.Should().Be(FillerKind.None); - playoutItems[1].GuideFinish.HasValue.Should().BeFalse(); - - playoutItems[2].MediaItemId.Should().Be(1); - playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.AddHours(2)); - playoutItems[2].GuideGroup.Should().Be(3); - playoutItems[2].FillerKind.Should().Be(FillerKind.None); - playoutItems[2].GuideFinish.HasValue.Should().BeTrue(); - } - - [Test] - public void Should_Fill_Exact_Duration_CustomTitle() - { - Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromHours(1)); - - var scheduleItem = new ProgramScheduleItemDuration - { - Id = 1, - Index = 1, - Collection = collectionOne, - CollectionId = collectionOne.Id, - StartTime = null, - PlayoutDuration = TimeSpan.FromHours(3), - TailMode = TailMode.None, - PlaybackOrder = PlaybackOrder.Chronological, - CustomTitle = "Custom Title" - }; - - var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( - new List { scheduleItem }, - new CollectionEnumeratorState()); - - var enumerator = new ChronologicalMediaCollectionEnumerator( - collectionOne.MediaItems, - new CollectionEnumeratorState()); - - PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); - - var scheduler = new PlayoutModeSchedulerDuration(new Mock().Object); - (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( - startState, - CollectionEnumerators(scheduleItem, enumerator), - scheduleItem, - NextScheduleItem, - HardStop(scheduleItemsEnumerator)); - - playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3)); - playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); - - playoutBuilderState.NextGuideGroup.Should().Be(2); - playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); - playoutBuilderState.InFlood.Should().BeFalse(); - playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); - playoutBuilderState.InDurationFiller.Should().BeFalse(); - playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); - - enumerator.State.Index.Should().Be(1); - - playoutItems.Count.Should().Be(3); - - playoutItems[0].MediaItemId.Should().Be(1); - playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); - playoutItems[0].GuideGroup.Should().Be(1); - playoutItems[0].FillerKind.Should().Be(FillerKind.None); - playoutItems[0].GuideFinish.HasValue.Should().BeFalse(); - playoutItems[0].CustomTitle.Should().Be("Custom Title"); - - playoutItems[1].MediaItemId.Should().Be(2); - playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddHours(1)); - playoutItems[1].GuideGroup.Should().Be(1); - playoutItems[1].FillerKind.Should().Be(FillerKind.None); - playoutItems[1].GuideFinish.HasValue.Should().BeFalse(); - playoutItems[1].CustomTitle.Should().Be("Custom Title"); - - playoutItems[2].MediaItemId.Should().Be(1); - playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.AddHours(2)); - playoutItems[2].GuideGroup.Should().Be(1); - playoutItems[2].FillerKind.Should().Be(FillerKind.None); - playoutItems[2].GuideFinish.HasValue.Should().BeTrue(); - playoutItems[2].CustomTitle.Should().Be("Custom Title"); - } - - [Test] - public void Should_Not_Have_Gap_Duration_Tail_Mode_None() - { - Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55)); - - var scheduleItem = new ProgramScheduleItemDuration - { - Id = 1, - Index = 1, - Collection = collectionOne, - CollectionId = collectionOne.Id, - StartTime = null, - PlayoutDuration = TimeSpan.FromHours(3), - TailMode = TailMode.None, - PlaybackOrder = PlaybackOrder.Chronological - }; - - var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( - new List { scheduleItem }, - new CollectionEnumeratorState()); - - var enumerator = new ChronologicalMediaCollectionEnumerator( - collectionOne.MediaItems, - new CollectionEnumeratorState()); - - PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); - - var scheduler = new PlayoutModeSchedulerDuration(new Mock().Object); - (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( - startState, - CollectionEnumerators(scheduleItem, enumerator), - scheduleItem, - NextScheduleItem, - HardStop(scheduleItemsEnumerator)); - - playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0))); - playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); - - playoutBuilderState.NextGuideGroup.Should().Be(4); - playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); - playoutBuilderState.InFlood.Should().BeFalse(); - playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); - playoutBuilderState.InDurationFiller.Should().BeFalse(); - playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); - - enumerator.State.Index.Should().Be(1); - - playoutItems.Count.Should().Be(3); - - playoutItems[0].MediaItemId.Should().Be(1); - playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); - playoutItems[0].GuideGroup.Should().Be(1); - playoutItems[0].FillerKind.Should().Be(FillerKind.None); - playoutItems[0].GuideFinish.HasValue.Should().BeFalse(); - - playoutItems[1].MediaItemId.Should().Be(2); - playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55)); - playoutItems[1].GuideGroup.Should().Be(2); - playoutItems[1].FillerKind.Should().Be(FillerKind.None); - playoutItems[1].GuideFinish.HasValue.Should().BeFalse(); - - playoutItems[2].MediaItemId.Should().Be(1); - playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0))); - playoutItems[2].GuideGroup.Should().Be(3); - playoutItems[2].FillerKind.Should().Be(FillerKind.None); - playoutItems[2].GuideFinish.HasValue.Should().BeTrue(); - } - - [Test] - public void Should_Have_Gap_Duration_Tail_Mode_Offline_No_Fallback() - { - Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55)); - - var scheduleItem = new ProgramScheduleItemDuration - { - Id = 1, - Index = 1, - Collection = collectionOne, - CollectionId = collectionOne.Id, - StartTime = null, - PlayoutDuration = TimeSpan.FromHours(3), - TailMode = TailMode.Offline, - PlaybackOrder = PlaybackOrder.Chronological - }; - - var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( - new List { scheduleItem }, - new CollectionEnumeratorState()); - - var enumerator = new ChronologicalMediaCollectionEnumerator( - collectionOne.MediaItems, - new CollectionEnumeratorState()); - - PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); - - var scheduler = new PlayoutModeSchedulerDuration(new Mock().Object); - (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( - startState, - CollectionEnumerators(scheduleItem, enumerator), - scheduleItem, - NextScheduleItem, - HardStop(scheduleItemsEnumerator)); - - // duration block should end after exact duration, with gap - playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3)); - playoutItems.Last().FinishOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0))); - - playoutBuilderState.NextGuideGroup.Should().Be(4); - playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); - playoutBuilderState.InFlood.Should().BeFalse(); - playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); - playoutBuilderState.InDurationFiller.Should().BeFalse(); - playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); - - enumerator.State.Index.Should().Be(1); - - playoutItems.Count.Should().Be(3); - - playoutItems[0].MediaItemId.Should().Be(1); - playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); - playoutItems[0].GuideGroup.Should().Be(1); - playoutItems[0].FillerKind.Should().Be(FillerKind.None); - playoutItems[0].GuideFinish.HasValue.Should().BeFalse(); - - playoutItems[1].MediaItemId.Should().Be(2); - playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55)); - playoutItems[1].GuideGroup.Should().Be(2); - playoutItems[1].FillerKind.Should().Be(FillerKind.None); - playoutItems[1].GuideFinish.HasValue.Should().BeFalse(); - - playoutItems[2].MediaItemId.Should().Be(1); - playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0))); - playoutItems[2].GuideGroup.Should().Be(3); - playoutItems[2].FillerKind.Should().Be(FillerKind.None); - playoutItems[2].GuideFinish.HasValue.Should().BeTrue(); - } - - [Test] - public void Should_Not_Have_Gap_Duration_Tail_Mode_Offline_With_Fallback() - { - Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55)); - Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(1)); - - var scheduleItem = new ProgramScheduleItemDuration - { - Id = 1, - Index = 1, - Collection = collectionOne, - CollectionId = collectionOne.Id, - StartTime = null, - PlayoutDuration = TimeSpan.FromHours(3), - TailMode = TailMode.Offline, - PlaybackOrder = PlaybackOrder.Chronological, - FallbackFiller = new FillerPreset - { - FillerKind = FillerKind.Fallback, - Collection = collectionTwo, - CollectionId = collectionTwo.Id - } - }; - - var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( - new List { scheduleItem }, - new CollectionEnumeratorState()); - - var enumerator1 = new ChronologicalMediaCollectionEnumerator( - collectionOne.MediaItems, - new CollectionEnumeratorState()); - - var enumerator2 = new ChronologicalMediaCollectionEnumerator( - collectionTwo.MediaItems, - new CollectionEnumeratorState()); - - PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); - - var scheduler = new PlayoutModeSchedulerDuration(new Mock().Object); - (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( - startState, - CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.FallbackFiller, enumerator2), - scheduleItem, - NextScheduleItem, - HardStop(scheduleItemsEnumerator)); - - playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3)); - playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); - - playoutBuilderState.NextGuideGroup.Should().Be(4); - playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); - playoutBuilderState.InFlood.Should().BeFalse(); - playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); - playoutBuilderState.InDurationFiller.Should().BeFalse(); - playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); - - enumerator1.State.Index.Should().Be(1); - enumerator2.State.Index.Should().Be(1); - - playoutItems.Count.Should().Be(4); - - playoutItems[0].MediaItemId.Should().Be(1); - playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); - playoutItems[0].GuideGroup.Should().Be(1); - playoutItems[0].FillerKind.Should().Be(FillerKind.None); - playoutItems[0].GuideFinish.HasValue.Should().BeFalse(); - - playoutItems[1].MediaItemId.Should().Be(2); - playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55)); - playoutItems[1].GuideGroup.Should().Be(2); - playoutItems[1].FillerKind.Should().Be(FillerKind.None); - playoutItems[1].GuideFinish.HasValue.Should().BeFalse(); - - playoutItems[2].MediaItemId.Should().Be(1); - playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0))); - playoutItems[2].GuideGroup.Should().Be(3); - playoutItems[2].FillerKind.Should().Be(FillerKind.None); - playoutItems[2].GuideFinish.HasValue.Should().BeTrue(); - - playoutItems[3].MediaItemId.Should().Be(3); - playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0))); - playoutItems[3].GuideGroup.Should().Be(3); - playoutItems[3].FillerKind.Should().Be(FillerKind.Fallback); - playoutItems[3].GuideFinish.HasValue.Should().BeFalse(); - } - - [Test] - public void Should_Not_Have_Gap_Duration_Tail_Mode_Filler_Exact_Duration() - { - Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55)); - Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(5)); - - var scheduleItem = new ProgramScheduleItemDuration - { - Id = 1, - Index = 1, - Collection = collectionOne, - CollectionId = collectionOne.Id, - StartTime = null, - PlayoutDuration = TimeSpan.FromHours(3), - TailMode = TailMode.Filler, - PlaybackOrder = PlaybackOrder.Chronological, - TailFiller = new FillerPreset - { - FillerKind = FillerKind.Tail, - Collection = collectionTwo, - CollectionId = collectionTwo.Id - } - }; - - var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( - new List { scheduleItem }, - new CollectionEnumeratorState()); - - var enumerator1 = new ChronologicalMediaCollectionEnumerator( - collectionOne.MediaItems, - new CollectionEnumeratorState()); - - var enumerator2 = new ChronologicalMediaCollectionEnumerator( - collectionTwo.MediaItems, - new CollectionEnumeratorState()); - - PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); - - var scheduler = new PlayoutModeSchedulerDuration(new Mock().Object); - (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( - startState, - CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2), - scheduleItem, - NextScheduleItem, - HardStop(scheduleItemsEnumerator)); - - playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3)); - playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); - - playoutBuilderState.NextGuideGroup.Should().Be(4); - playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); - playoutBuilderState.InFlood.Should().BeFalse(); - playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); - playoutBuilderState.InDurationFiller.Should().BeFalse(); - playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); - - enumerator1.State.Index.Should().Be(1); - enumerator2.State.Index.Should().Be(1); - - playoutItems.Count.Should().Be(6); - - playoutItems[0].MediaItemId.Should().Be(1); - playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); - playoutItems[0].GuideGroup.Should().Be(1); - playoutItems[0].FillerKind.Should().Be(FillerKind.None); - playoutItems[0].GuideFinish.HasValue.Should().BeFalse(); - - playoutItems[1].MediaItemId.Should().Be(2); - playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55)); - playoutItems[1].GuideGroup.Should().Be(2); - playoutItems[1].FillerKind.Should().Be(FillerKind.None); - playoutItems[1].GuideFinish.HasValue.Should().BeFalse(); - - playoutItems[2].MediaItemId.Should().Be(1); - playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0))); - playoutItems[2].GuideGroup.Should().Be(3); - playoutItems[2].FillerKind.Should().Be(FillerKind.None); - playoutItems[2].GuideFinish.HasValue.Should().BeTrue(); - - playoutItems[3].MediaItemId.Should().Be(3); - playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0))); - playoutItems[3].GuideGroup.Should().Be(3); - playoutItems[3].FillerKind.Should().Be(FillerKind.Tail); - playoutItems[3].GuideFinish.HasValue.Should().BeFalse(); - - playoutItems[4].MediaItemId.Should().Be(4); - playoutItems[4].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 50, 0))); - playoutItems[4].GuideGroup.Should().Be(3); - playoutItems[4].FillerKind.Should().Be(FillerKind.Tail); - playoutItems[3].GuideFinish.HasValue.Should().BeFalse(); - - playoutItems[5].MediaItemId.Should().Be(3); - playoutItems[5].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 55, 0))); - playoutItems[5].GuideGroup.Should().Be(3); - playoutItems[5].FillerKind.Should().Be(FillerKind.Tail); - playoutItems[3].GuideFinish.HasValue.Should().BeFalse(); - } - - [Test] - public void Should_Have_Gap_Duration_Tail_Mode_Filler_No_Fallback() - { - Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55)); - Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(4)); - - var scheduleItem = new ProgramScheduleItemDuration - { - Id = 1, - Index = 1, - Collection = collectionOne, - CollectionId = collectionOne.Id, - StartTime = null, - PlayoutDuration = TimeSpan.FromHours(3), - TailMode = TailMode.Filler, - PlaybackOrder = PlaybackOrder.Chronological, - TailFiller = new FillerPreset - { - FillerKind = FillerKind.Tail, - Collection = collectionTwo, - CollectionId = collectionTwo.Id - } - }; - - var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( - new List { scheduleItem }, - new CollectionEnumeratorState()); - - var enumerator1 = new ChronologicalMediaCollectionEnumerator( - collectionOne.MediaItems, - new CollectionEnumeratorState()); - - var enumerator2 = new ChronologicalMediaCollectionEnumerator( - collectionTwo.MediaItems, - new CollectionEnumeratorState()); - - PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); - - var scheduler = new PlayoutModeSchedulerDuration(new Mock().Object); - (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( - startState, - CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2), - scheduleItem, - NextScheduleItem, - HardStop(scheduleItemsEnumerator)); - - playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3)); - playoutItems.Last().FinishOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 57, 0))); - - playoutBuilderState.NextGuideGroup.Should().Be(4); - playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); - playoutBuilderState.InFlood.Should().BeFalse(); - playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); - playoutBuilderState.InDurationFiller.Should().BeFalse(); - playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); - - enumerator1.State.Index.Should().Be(1); - enumerator2.State.Index.Should().Be(1); - - playoutItems.Count.Should().Be(6); - - playoutItems[0].MediaItemId.Should().Be(1); - playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); - playoutItems[0].GuideGroup.Should().Be(1); - playoutItems[0].FillerKind.Should().Be(FillerKind.None); - playoutItems[0].GuideFinish.HasValue.Should().BeFalse(); - - playoutItems[1].MediaItemId.Should().Be(2); - playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55)); - playoutItems[1].GuideGroup.Should().Be(2); - playoutItems[1].FillerKind.Should().Be(FillerKind.None); - playoutItems[1].GuideFinish.HasValue.Should().BeFalse(); - - playoutItems[2].MediaItemId.Should().Be(1); - playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0))); - playoutItems[2].GuideGroup.Should().Be(3); - playoutItems[2].FillerKind.Should().Be(FillerKind.None); - playoutItems[2].GuideFinish.HasValue.Should().BeTrue(); - - playoutItems[3].MediaItemId.Should().Be(3); - playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0))); - playoutItems[3].GuideGroup.Should().Be(3); - playoutItems[3].FillerKind.Should().Be(FillerKind.Tail); - playoutItems[3].GuideFinish.HasValue.Should().BeFalse(); - - playoutItems[4].MediaItemId.Should().Be(4); - playoutItems[4].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 49, 0))); - playoutItems[4].GuideGroup.Should().Be(3); - playoutItems[4].FillerKind.Should().Be(FillerKind.Tail); - playoutItems[4].GuideFinish.HasValue.Should().BeFalse(); - - playoutItems[5].MediaItemId.Should().Be(3); - playoutItems[5].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 53, 0))); - playoutItems[5].GuideGroup.Should().Be(3); - playoutItems[5].FillerKind.Should().Be(FillerKind.Tail); - playoutItems[5].GuideFinish.HasValue.Should().BeFalse(); - } - - [Test] - public void Should_Not_Have_Gap_Duration_Tail_Mode_Filler_With_Fallback() - { - Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55)); - Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(4)); - Collection collectionThree = TwoItemCollection(5, 6, TimeSpan.FromMinutes(1)); - - var scheduleItem = new ProgramScheduleItemDuration - { - Id = 1, - Index = 1, - Collection = collectionOne, - CollectionId = collectionOne.Id, - StartTime = null, - PlayoutDuration = TimeSpan.FromHours(3), - TailMode = TailMode.Filler, - PlaybackOrder = PlaybackOrder.Chronological, - TailFiller = new FillerPreset - { - FillerKind = FillerKind.Tail, - Collection = collectionTwo, - CollectionId = collectionTwo.Id - }, - FallbackFiller = new FillerPreset - { - FillerKind = FillerKind.Fallback, - Collection = collectionThree, - CollectionId = collectionThree.Id - } - }; - - var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( - new List { scheduleItem }, - new CollectionEnumeratorState()); - - var enumerator1 = new ChronologicalMediaCollectionEnumerator( - collectionOne.MediaItems, - new CollectionEnumeratorState()); - - var enumerator2 = new ChronologicalMediaCollectionEnumerator( - collectionTwo.MediaItems, - new CollectionEnumeratorState()); - - var enumerator3 = new ChronologicalMediaCollectionEnumerator( - collectionThree.MediaItems, - new CollectionEnumeratorState()); - - PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); - - var scheduler = new PlayoutModeSchedulerDuration(new Mock().Object); - (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( - startState, - CollectionEnumerators( - scheduleItem, - enumerator1, - scheduleItem.TailFiller, - enumerator2, - scheduleItem.FallbackFiller, - enumerator3), - scheduleItem, - NextScheduleItem, - HardStop(scheduleItemsEnumerator)); - - playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3)); - playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); - - playoutBuilderState.NextGuideGroup.Should().Be(4); - playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); - playoutBuilderState.InFlood.Should().BeFalse(); - playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); - playoutBuilderState.InDurationFiller.Should().BeFalse(); - playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); - - enumerator1.State.Index.Should().Be(1); - enumerator2.State.Index.Should().Be(1); - enumerator3.State.Index.Should().Be(1); - - playoutItems.Count.Should().Be(7); - - playoutItems[0].MediaItemId.Should().Be(1); - playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); - playoutItems[0].GuideGroup.Should().Be(1); - playoutItems[0].FillerKind.Should().Be(FillerKind.None); - playoutItems[0].GuideFinish.HasValue.Should().BeFalse(); - - playoutItems[1].MediaItemId.Should().Be(2); - playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55)); - playoutItems[1].GuideGroup.Should().Be(2); - playoutItems[1].FillerKind.Should().Be(FillerKind.None); - playoutItems[1].GuideFinish.HasValue.Should().BeFalse(); - - playoutItems[2].MediaItemId.Should().Be(1); - playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0))); - playoutItems[2].GuideGroup.Should().Be(3); - playoutItems[2].FillerKind.Should().Be(FillerKind.None); - playoutItems[2].GuideFinish.HasValue.Should().BeTrue(); - - playoutItems[3].MediaItemId.Should().Be(3); - playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0))); - playoutItems[3].GuideGroup.Should().Be(3); - playoutItems[3].FillerKind.Should().Be(FillerKind.Tail); - playoutItems[3].GuideFinish.HasValue.Should().BeFalse(); - - playoutItems[4].MediaItemId.Should().Be(4); - playoutItems[4].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 49, 0))); - playoutItems[4].GuideGroup.Should().Be(3); - playoutItems[4].FillerKind.Should().Be(FillerKind.Tail); - playoutItems[4].GuideFinish.HasValue.Should().BeFalse(); - - playoutItems[5].MediaItemId.Should().Be(3); - playoutItems[5].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 53, 0))); - playoutItems[5].GuideGroup.Should().Be(3); - playoutItems[5].FillerKind.Should().Be(FillerKind.Tail); - playoutItems[5].GuideFinish.HasValue.Should().BeFalse(); - - playoutItems[6].MediaItemId.Should().Be(5); - playoutItems[6].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 57, 0))); - playoutItems[6].GuideGroup.Should().Be(3); - playoutItems[6].FillerKind.Should().Be(FillerKind.Fallback); - playoutItems[6].GuideFinish.HasValue.Should().BeFalse(); - } + var scheduler = new PlayoutModeSchedulerDuration(new Mock().Object); + (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( + startState, + CollectionEnumerators(scheduleItem, enumerator), + scheduleItem, + NextScheduleItem, + HardStop(scheduleItemsEnumerator)); + + playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3)); + playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); + + playoutBuilderState.NextGuideGroup.Should().Be(4); + playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); + playoutBuilderState.InFlood.Should().BeFalse(); + playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); + playoutBuilderState.InDurationFiller.Should().BeFalse(); + playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); + + enumerator.State.Index.Should().Be(1); + + playoutItems.Count.Should().Be(3); + + playoutItems[0].MediaItemId.Should().Be(1); + playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); + playoutItems[0].GuideGroup.Should().Be(1); + playoutItems[0].FillerKind.Should().Be(FillerKind.None); + playoutItems[0].GuideFinish.HasValue.Should().BeFalse(); + + playoutItems[1].MediaItemId.Should().Be(2); + playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddHours(1)); + playoutItems[1].GuideGroup.Should().Be(2); + playoutItems[1].FillerKind.Should().Be(FillerKind.None); + playoutItems[1].GuideFinish.HasValue.Should().BeFalse(); + + playoutItems[2].MediaItemId.Should().Be(1); + playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.AddHours(2)); + playoutItems[2].GuideGroup.Should().Be(3); + playoutItems[2].FillerKind.Should().Be(FillerKind.None); + playoutItems[2].GuideFinish.HasValue.Should().BeTrue(); } -} + + [Test] + public void Should_Fill_Exact_Duration_CustomTitle() + { + Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromHours(1)); + + var scheduleItem = new ProgramScheduleItemDuration + { + Id = 1, + Index = 1, + Collection = collectionOne, + CollectionId = collectionOne.Id, + StartTime = null, + PlayoutDuration = TimeSpan.FromHours(3), + TailMode = TailMode.None, + PlaybackOrder = PlaybackOrder.Chronological, + CustomTitle = "Custom Title" + }; + + var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( + new List { scheduleItem }, + new CollectionEnumeratorState()); + + var enumerator = new ChronologicalMediaCollectionEnumerator( + collectionOne.MediaItems, + new CollectionEnumeratorState()); + + PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); + + var scheduler = new PlayoutModeSchedulerDuration(new Mock().Object); + (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( + startState, + CollectionEnumerators(scheduleItem, enumerator), + scheduleItem, + NextScheduleItem, + HardStop(scheduleItemsEnumerator)); + + playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3)); + playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); + + playoutBuilderState.NextGuideGroup.Should().Be(2); + playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); + playoutBuilderState.InFlood.Should().BeFalse(); + playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); + playoutBuilderState.InDurationFiller.Should().BeFalse(); + playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); + + enumerator.State.Index.Should().Be(1); + + playoutItems.Count.Should().Be(3); + + playoutItems[0].MediaItemId.Should().Be(1); + playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); + playoutItems[0].GuideGroup.Should().Be(1); + playoutItems[0].FillerKind.Should().Be(FillerKind.None); + playoutItems[0].GuideFinish.HasValue.Should().BeFalse(); + playoutItems[0].CustomTitle.Should().Be("Custom Title"); + + playoutItems[1].MediaItemId.Should().Be(2); + playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddHours(1)); + playoutItems[1].GuideGroup.Should().Be(1); + playoutItems[1].FillerKind.Should().Be(FillerKind.None); + playoutItems[1].GuideFinish.HasValue.Should().BeFalse(); + playoutItems[1].CustomTitle.Should().Be("Custom Title"); + + playoutItems[2].MediaItemId.Should().Be(1); + playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.AddHours(2)); + playoutItems[2].GuideGroup.Should().Be(1); + playoutItems[2].FillerKind.Should().Be(FillerKind.None); + playoutItems[2].GuideFinish.HasValue.Should().BeTrue(); + playoutItems[2].CustomTitle.Should().Be("Custom Title"); + } + + [Test] + public void Should_Not_Have_Gap_Duration_Tail_Mode_None() + { + Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55)); + + var scheduleItem = new ProgramScheduleItemDuration + { + Id = 1, + Index = 1, + Collection = collectionOne, + CollectionId = collectionOne.Id, + StartTime = null, + PlayoutDuration = TimeSpan.FromHours(3), + TailMode = TailMode.None, + PlaybackOrder = PlaybackOrder.Chronological + }; + + var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( + new List { scheduleItem }, + new CollectionEnumeratorState()); + + var enumerator = new ChronologicalMediaCollectionEnumerator( + collectionOne.MediaItems, + new CollectionEnumeratorState()); + + PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); + + var scheduler = new PlayoutModeSchedulerDuration(new Mock().Object); + (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( + startState, + CollectionEnumerators(scheduleItem, enumerator), + scheduleItem, + NextScheduleItem, + HardStop(scheduleItemsEnumerator)); + + playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0))); + playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); + + playoutBuilderState.NextGuideGroup.Should().Be(4); + playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); + playoutBuilderState.InFlood.Should().BeFalse(); + playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); + playoutBuilderState.InDurationFiller.Should().BeFalse(); + playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); + + enumerator.State.Index.Should().Be(1); + + playoutItems.Count.Should().Be(3); + + playoutItems[0].MediaItemId.Should().Be(1); + playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); + playoutItems[0].GuideGroup.Should().Be(1); + playoutItems[0].FillerKind.Should().Be(FillerKind.None); + playoutItems[0].GuideFinish.HasValue.Should().BeFalse(); + + playoutItems[1].MediaItemId.Should().Be(2); + playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55)); + playoutItems[1].GuideGroup.Should().Be(2); + playoutItems[1].FillerKind.Should().Be(FillerKind.None); + playoutItems[1].GuideFinish.HasValue.Should().BeFalse(); + + playoutItems[2].MediaItemId.Should().Be(1); + playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0))); + playoutItems[2].GuideGroup.Should().Be(3); + playoutItems[2].FillerKind.Should().Be(FillerKind.None); + playoutItems[2].GuideFinish.HasValue.Should().BeTrue(); + } + + [Test] + public void Should_Have_Gap_Duration_Tail_Mode_Offline_No_Fallback() + { + Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55)); + + var scheduleItem = new ProgramScheduleItemDuration + { + Id = 1, + Index = 1, + Collection = collectionOne, + CollectionId = collectionOne.Id, + StartTime = null, + PlayoutDuration = TimeSpan.FromHours(3), + TailMode = TailMode.Offline, + PlaybackOrder = PlaybackOrder.Chronological + }; + + var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( + new List { scheduleItem }, + new CollectionEnumeratorState()); + + var enumerator = new ChronologicalMediaCollectionEnumerator( + collectionOne.MediaItems, + new CollectionEnumeratorState()); + + PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); + + var scheduler = new PlayoutModeSchedulerDuration(new Mock().Object); + (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( + startState, + CollectionEnumerators(scheduleItem, enumerator), + scheduleItem, + NextScheduleItem, + HardStop(scheduleItemsEnumerator)); + + // duration block should end after exact duration, with gap + playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3)); + playoutItems.Last().FinishOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0))); + + playoutBuilderState.NextGuideGroup.Should().Be(4); + playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); + playoutBuilderState.InFlood.Should().BeFalse(); + playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); + playoutBuilderState.InDurationFiller.Should().BeFalse(); + playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); + + enumerator.State.Index.Should().Be(1); + + playoutItems.Count.Should().Be(3); + + playoutItems[0].MediaItemId.Should().Be(1); + playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); + playoutItems[0].GuideGroup.Should().Be(1); + playoutItems[0].FillerKind.Should().Be(FillerKind.None); + playoutItems[0].GuideFinish.HasValue.Should().BeFalse(); + + playoutItems[1].MediaItemId.Should().Be(2); + playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55)); + playoutItems[1].GuideGroup.Should().Be(2); + playoutItems[1].FillerKind.Should().Be(FillerKind.None); + playoutItems[1].GuideFinish.HasValue.Should().BeFalse(); + + playoutItems[2].MediaItemId.Should().Be(1); + playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0))); + playoutItems[2].GuideGroup.Should().Be(3); + playoutItems[2].FillerKind.Should().Be(FillerKind.None); + playoutItems[2].GuideFinish.HasValue.Should().BeTrue(); + } + + [Test] + public void Should_Not_Have_Gap_Duration_Tail_Mode_Offline_With_Fallback() + { + Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55)); + Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(1)); + + var scheduleItem = new ProgramScheduleItemDuration + { + Id = 1, + Index = 1, + Collection = collectionOne, + CollectionId = collectionOne.Id, + StartTime = null, + PlayoutDuration = TimeSpan.FromHours(3), + TailMode = TailMode.Offline, + PlaybackOrder = PlaybackOrder.Chronological, + FallbackFiller = new FillerPreset + { + FillerKind = FillerKind.Fallback, + Collection = collectionTwo, + CollectionId = collectionTwo.Id + } + }; + + var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( + new List { scheduleItem }, + new CollectionEnumeratorState()); + + var enumerator1 = new ChronologicalMediaCollectionEnumerator( + collectionOne.MediaItems, + new CollectionEnumeratorState()); + + var enumerator2 = new ChronologicalMediaCollectionEnumerator( + collectionTwo.MediaItems, + new CollectionEnumeratorState()); + + PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); + + var scheduler = new PlayoutModeSchedulerDuration(new Mock().Object); + (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( + startState, + CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.FallbackFiller, enumerator2), + scheduleItem, + NextScheduleItem, + HardStop(scheduleItemsEnumerator)); + + playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3)); + playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); + + playoutBuilderState.NextGuideGroup.Should().Be(4); + playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); + playoutBuilderState.InFlood.Should().BeFalse(); + playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); + playoutBuilderState.InDurationFiller.Should().BeFalse(); + playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); + + enumerator1.State.Index.Should().Be(1); + enumerator2.State.Index.Should().Be(1); + + playoutItems.Count.Should().Be(4); + + playoutItems[0].MediaItemId.Should().Be(1); + playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); + playoutItems[0].GuideGroup.Should().Be(1); + playoutItems[0].FillerKind.Should().Be(FillerKind.None); + playoutItems[0].GuideFinish.HasValue.Should().BeFalse(); + + playoutItems[1].MediaItemId.Should().Be(2); + playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55)); + playoutItems[1].GuideGroup.Should().Be(2); + playoutItems[1].FillerKind.Should().Be(FillerKind.None); + playoutItems[1].GuideFinish.HasValue.Should().BeFalse(); + + playoutItems[2].MediaItemId.Should().Be(1); + playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0))); + playoutItems[2].GuideGroup.Should().Be(3); + playoutItems[2].FillerKind.Should().Be(FillerKind.None); + playoutItems[2].GuideFinish.HasValue.Should().BeTrue(); + + playoutItems[3].MediaItemId.Should().Be(3); + playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0))); + playoutItems[3].GuideGroup.Should().Be(3); + playoutItems[3].FillerKind.Should().Be(FillerKind.Fallback); + playoutItems[3].GuideFinish.HasValue.Should().BeFalse(); + } + + [Test] + public void Should_Not_Have_Gap_Duration_Tail_Mode_Filler_Exact_Duration() + { + Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55)); + Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(5)); + + var scheduleItem = new ProgramScheduleItemDuration + { + Id = 1, + Index = 1, + Collection = collectionOne, + CollectionId = collectionOne.Id, + StartTime = null, + PlayoutDuration = TimeSpan.FromHours(3), + TailMode = TailMode.Filler, + PlaybackOrder = PlaybackOrder.Chronological, + TailFiller = new FillerPreset + { + FillerKind = FillerKind.Tail, + Collection = collectionTwo, + CollectionId = collectionTwo.Id + } + }; + + var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( + new List { scheduleItem }, + new CollectionEnumeratorState()); + + var enumerator1 = new ChronologicalMediaCollectionEnumerator( + collectionOne.MediaItems, + new CollectionEnumeratorState()); + + var enumerator2 = new ChronologicalMediaCollectionEnumerator( + collectionTwo.MediaItems, + new CollectionEnumeratorState()); + + PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); + + var scheduler = new PlayoutModeSchedulerDuration(new Mock().Object); + (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( + startState, + CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2), + scheduleItem, + NextScheduleItem, + HardStop(scheduleItemsEnumerator)); + + playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3)); + playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); + + playoutBuilderState.NextGuideGroup.Should().Be(4); + playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); + playoutBuilderState.InFlood.Should().BeFalse(); + playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); + playoutBuilderState.InDurationFiller.Should().BeFalse(); + playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); + + enumerator1.State.Index.Should().Be(1); + enumerator2.State.Index.Should().Be(1); + + playoutItems.Count.Should().Be(6); + + playoutItems[0].MediaItemId.Should().Be(1); + playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); + playoutItems[0].GuideGroup.Should().Be(1); + playoutItems[0].FillerKind.Should().Be(FillerKind.None); + playoutItems[0].GuideFinish.HasValue.Should().BeFalse(); + + playoutItems[1].MediaItemId.Should().Be(2); + playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55)); + playoutItems[1].GuideGroup.Should().Be(2); + playoutItems[1].FillerKind.Should().Be(FillerKind.None); + playoutItems[1].GuideFinish.HasValue.Should().BeFalse(); + + playoutItems[2].MediaItemId.Should().Be(1); + playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0))); + playoutItems[2].GuideGroup.Should().Be(3); + playoutItems[2].FillerKind.Should().Be(FillerKind.None); + playoutItems[2].GuideFinish.HasValue.Should().BeTrue(); + + playoutItems[3].MediaItemId.Should().Be(3); + playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0))); + playoutItems[3].GuideGroup.Should().Be(3); + playoutItems[3].FillerKind.Should().Be(FillerKind.Tail); + playoutItems[3].GuideFinish.HasValue.Should().BeFalse(); + + playoutItems[4].MediaItemId.Should().Be(4); + playoutItems[4].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 50, 0))); + playoutItems[4].GuideGroup.Should().Be(3); + playoutItems[4].FillerKind.Should().Be(FillerKind.Tail); + playoutItems[3].GuideFinish.HasValue.Should().BeFalse(); + + playoutItems[5].MediaItemId.Should().Be(3); + playoutItems[5].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 55, 0))); + playoutItems[5].GuideGroup.Should().Be(3); + playoutItems[5].FillerKind.Should().Be(FillerKind.Tail); + playoutItems[3].GuideFinish.HasValue.Should().BeFalse(); + } + + [Test] + public void Should_Have_Gap_Duration_Tail_Mode_Filler_No_Fallback() + { + Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55)); + Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(4)); + + var scheduleItem = new ProgramScheduleItemDuration + { + Id = 1, + Index = 1, + Collection = collectionOne, + CollectionId = collectionOne.Id, + StartTime = null, + PlayoutDuration = TimeSpan.FromHours(3), + TailMode = TailMode.Filler, + PlaybackOrder = PlaybackOrder.Chronological, + TailFiller = new FillerPreset + { + FillerKind = FillerKind.Tail, + Collection = collectionTwo, + CollectionId = collectionTwo.Id + } + }; + + var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( + new List { scheduleItem }, + new CollectionEnumeratorState()); + + var enumerator1 = new ChronologicalMediaCollectionEnumerator( + collectionOne.MediaItems, + new CollectionEnumeratorState()); + + var enumerator2 = new ChronologicalMediaCollectionEnumerator( + collectionTwo.MediaItems, + new CollectionEnumeratorState()); + + PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); + + var scheduler = new PlayoutModeSchedulerDuration(new Mock().Object); + (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( + startState, + CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2), + scheduleItem, + NextScheduleItem, + HardStop(scheduleItemsEnumerator)); + + playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3)); + playoutItems.Last().FinishOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 57, 0))); + + playoutBuilderState.NextGuideGroup.Should().Be(4); + playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); + playoutBuilderState.InFlood.Should().BeFalse(); + playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); + playoutBuilderState.InDurationFiller.Should().BeFalse(); + playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); + + enumerator1.State.Index.Should().Be(1); + enumerator2.State.Index.Should().Be(1); + + playoutItems.Count.Should().Be(6); + + playoutItems[0].MediaItemId.Should().Be(1); + playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); + playoutItems[0].GuideGroup.Should().Be(1); + playoutItems[0].FillerKind.Should().Be(FillerKind.None); + playoutItems[0].GuideFinish.HasValue.Should().BeFalse(); + + playoutItems[1].MediaItemId.Should().Be(2); + playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55)); + playoutItems[1].GuideGroup.Should().Be(2); + playoutItems[1].FillerKind.Should().Be(FillerKind.None); + playoutItems[1].GuideFinish.HasValue.Should().BeFalse(); + + playoutItems[2].MediaItemId.Should().Be(1); + playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0))); + playoutItems[2].GuideGroup.Should().Be(3); + playoutItems[2].FillerKind.Should().Be(FillerKind.None); + playoutItems[2].GuideFinish.HasValue.Should().BeTrue(); + + playoutItems[3].MediaItemId.Should().Be(3); + playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0))); + playoutItems[3].GuideGroup.Should().Be(3); + playoutItems[3].FillerKind.Should().Be(FillerKind.Tail); + playoutItems[3].GuideFinish.HasValue.Should().BeFalse(); + + playoutItems[4].MediaItemId.Should().Be(4); + playoutItems[4].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 49, 0))); + playoutItems[4].GuideGroup.Should().Be(3); + playoutItems[4].FillerKind.Should().Be(FillerKind.Tail); + playoutItems[4].GuideFinish.HasValue.Should().BeFalse(); + + playoutItems[5].MediaItemId.Should().Be(3); + playoutItems[5].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 53, 0))); + playoutItems[5].GuideGroup.Should().Be(3); + playoutItems[5].FillerKind.Should().Be(FillerKind.Tail); + playoutItems[5].GuideFinish.HasValue.Should().BeFalse(); + } + + [Test] + public void Should_Not_Have_Gap_Duration_Tail_Mode_Filler_With_Fallback() + { + Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55)); + Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(4)); + Collection collectionThree = TwoItemCollection(5, 6, TimeSpan.FromMinutes(1)); + + var scheduleItem = new ProgramScheduleItemDuration + { + Id = 1, + Index = 1, + Collection = collectionOne, + CollectionId = collectionOne.Id, + StartTime = null, + PlayoutDuration = TimeSpan.FromHours(3), + TailMode = TailMode.Filler, + PlaybackOrder = PlaybackOrder.Chronological, + TailFiller = new FillerPreset + { + FillerKind = FillerKind.Tail, + Collection = collectionTwo, + CollectionId = collectionTwo.Id + }, + FallbackFiller = new FillerPreset + { + FillerKind = FillerKind.Fallback, + Collection = collectionThree, + CollectionId = collectionThree.Id + } + }; + + var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( + new List { scheduleItem }, + new CollectionEnumeratorState()); + + var enumerator1 = new ChronologicalMediaCollectionEnumerator( + collectionOne.MediaItems, + new CollectionEnumeratorState()); + + var enumerator2 = new ChronologicalMediaCollectionEnumerator( + collectionTwo.MediaItems, + new CollectionEnumeratorState()); + + var enumerator3 = new ChronologicalMediaCollectionEnumerator( + collectionThree.MediaItems, + new CollectionEnumeratorState()); + + PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); + + var scheduler = new PlayoutModeSchedulerDuration(new Mock().Object); + (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( + startState, + CollectionEnumerators( + scheduleItem, + enumerator1, + scheduleItem.TailFiller, + enumerator2, + scheduleItem.FallbackFiller, + enumerator3), + scheduleItem, + NextScheduleItem, + HardStop(scheduleItemsEnumerator)); + + playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3)); + playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); + + playoutBuilderState.NextGuideGroup.Should().Be(4); + playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); + playoutBuilderState.InFlood.Should().BeFalse(); + playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); + playoutBuilderState.InDurationFiller.Should().BeFalse(); + playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); + + enumerator1.State.Index.Should().Be(1); + enumerator2.State.Index.Should().Be(1); + enumerator3.State.Index.Should().Be(1); + + playoutItems.Count.Should().Be(7); + + playoutItems[0].MediaItemId.Should().Be(1); + playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); + playoutItems[0].GuideGroup.Should().Be(1); + playoutItems[0].FillerKind.Should().Be(FillerKind.None); + playoutItems[0].GuideFinish.HasValue.Should().BeFalse(); + + playoutItems[1].MediaItemId.Should().Be(2); + playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55)); + playoutItems[1].GuideGroup.Should().Be(2); + playoutItems[1].FillerKind.Should().Be(FillerKind.None); + playoutItems[1].GuideFinish.HasValue.Should().BeFalse(); + + playoutItems[2].MediaItemId.Should().Be(1); + playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0))); + playoutItems[2].GuideGroup.Should().Be(3); + playoutItems[2].FillerKind.Should().Be(FillerKind.None); + playoutItems[2].GuideFinish.HasValue.Should().BeTrue(); + + playoutItems[3].MediaItemId.Should().Be(3); + playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0))); + playoutItems[3].GuideGroup.Should().Be(3); + playoutItems[3].FillerKind.Should().Be(FillerKind.Tail); + playoutItems[3].GuideFinish.HasValue.Should().BeFalse(); + + playoutItems[4].MediaItemId.Should().Be(4); + playoutItems[4].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 49, 0))); + playoutItems[4].GuideGroup.Should().Be(3); + playoutItems[4].FillerKind.Should().Be(FillerKind.Tail); + playoutItems[4].GuideFinish.HasValue.Should().BeFalse(); + + playoutItems[5].MediaItemId.Should().Be(3); + playoutItems[5].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 53, 0))); + playoutItems[5].GuideGroup.Should().Be(3); + playoutItems[5].FillerKind.Should().Be(FillerKind.Tail); + playoutItems[5].GuideFinish.HasValue.Should().BeFalse(); + + playoutItems[6].MediaItemId.Should().Be(5); + playoutItems[6].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 57, 0))); + playoutItems[6].GuideGroup.Should().Be(3); + playoutItems[6].FillerKind.Should().Be(FillerKind.Fallback); + playoutItems[6].GuideFinish.HasValue.Should().BeFalse(); + } +} \ No newline at end of file diff --git a/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerFloodTests.cs b/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerFloodTests.cs index ca9745c61..70b636673 100644 --- a/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerFloodTests.cs +++ b/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerFloodTests.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Filler; using ErsatzTV.Core.Scheduling; using FluentAssertions; @@ -9,830 +6,829 @@ using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; -namespace ErsatzTV.Core.Tests.Scheduling +namespace ErsatzTV.Core.Tests.Scheduling; + +[TestFixture] +public class PlayoutModeSchedulerFloodTests : SchedulerTestBase { - [TestFixture] - public class PlayoutModeSchedulerFloodTests : SchedulerTestBase + [Test] + public void Should_Fill_Exactly_To_Next_Schedule_Item() { - [Test] - public void Should_Fill_Exactly_To_Next_Schedule_Item() + Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromHours(1)); + + var scheduleItem = new ProgramScheduleItemFlood { - Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromHours(1)); - - var scheduleItem = new ProgramScheduleItemFlood - { - Id = 1, - Index = 1, - Collection = collectionOne, - CollectionId = collectionOne.Id, - StartTime = null, - PlaybackOrder = PlaybackOrder.Chronological, - TailFiller = null, - FallbackFiller = null - }; - - var enumerator = new ChronologicalMediaCollectionEnumerator( - collectionOne.MediaItems, - new CollectionEnumeratorState()); - - var sortedScheduleItems = new List - { - scheduleItem, - NextScheduleItem - }; - - var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( - sortedScheduleItems, - new CollectionEnumeratorState()); - - PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); - - var scheduler = new PlayoutModeSchedulerFlood(new Mock().Object); - (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( - startState, - CollectionEnumerators(scheduleItem, enumerator), - scheduleItem, - NextScheduleItem, - HardStop(scheduleItemsEnumerator)); - - playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3)); - playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); - - playoutBuilderState.NextGuideGroup.Should().Be(4); - playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); - playoutBuilderState.InFlood.Should().BeFalse(); - playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); - playoutBuilderState.InDurationFiller.Should().BeFalse(); - playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(1); - - enumerator.State.Index.Should().Be(1); - - playoutItems.Count.Should().Be(3); - - playoutItems[0].MediaItemId.Should().Be(1); - playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); - playoutItems[0].GuideGroup.Should().Be(1); - playoutItems[0].FillerKind.Should().Be(FillerKind.None); - - playoutItems[1].MediaItemId.Should().Be(2); - playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddHours(1)); - playoutItems[1].GuideGroup.Should().Be(2); - playoutItems[1].FillerKind.Should().Be(FillerKind.None); - - playoutItems[2].MediaItemId.Should().Be(1); - playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.AddHours(2)); - playoutItems[2].GuideGroup.Should().Be(3); - playoutItems[2].FillerKind.Should().Be(FillerKind.None); - } - - [Test] - public void Should_Fill_Exactly_To_Next_Schedule_Item_Flood() - { - Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromHours(1)); - - var scheduleItem = new ProgramScheduleItemFlood - { - Id = 1, - Index = 1, - Collection = collectionOne, - CollectionId = collectionOne.Id, - StartTime = null, - PlaybackOrder = PlaybackOrder.Chronological, - TailFiller = null, - FallbackFiller = null - }; - - var enumerator = new ChronologicalMediaCollectionEnumerator( - collectionOne.MediaItems, - new CollectionEnumeratorState()); - - var sortedScheduleItems = new List - { - scheduleItem, - // this caused trouble with the peek logic and the IsFlood flag - new ProgramScheduleItemFlood - { - StartTime = TimeSpan.FromHours(3), - } - }; - - var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( - sortedScheduleItems, - new CollectionEnumeratorState()); - - PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); - - var scheduler = new PlayoutModeSchedulerFlood(new Mock().Object); - (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( - startState, - CollectionEnumerators(scheduleItem, enumerator), - scheduleItem, - NextScheduleItem, - HardStop(scheduleItemsEnumerator)); - - playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3)); - playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); - - playoutBuilderState.NextGuideGroup.Should().Be(4); - playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); - playoutBuilderState.InFlood.Should().BeFalse(); - playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); - playoutBuilderState.InDurationFiller.Should().BeFalse(); - playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(1); - - enumerator.State.Index.Should().Be(1); - - playoutItems.Count.Should().Be(3); - - playoutItems[0].MediaItemId.Should().Be(1); - playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); - playoutItems[0].GuideGroup.Should().Be(1); - playoutItems[0].FillerKind.Should().Be(FillerKind.None); - - playoutItems[1].MediaItemId.Should().Be(2); - playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddHours(1)); - playoutItems[1].GuideGroup.Should().Be(2); - playoutItems[1].FillerKind.Should().Be(FillerKind.None); - - playoutItems[2].MediaItemId.Should().Be(1); - playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.AddHours(2)); - playoutItems[2].GuideGroup.Should().Be(3); - playoutItems[2].FillerKind.Should().Be(FillerKind.None); - } - - [Test] - public void Should_Fill_Exactly_To_Next_Schedule_Item_With_Post_Roll_Multiple_One() - { - Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55)); - Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(5)); - - var scheduleItem = new ProgramScheduleItemFlood - { - Id = 1, - Index = 1, - Collection = collectionOne, - CollectionId = collectionOne.Id, - StartTime = null, - PlaybackOrder = PlaybackOrder.Chronological, - PostRollFiller = new FillerPreset - { - FillerKind = FillerKind.PostRoll, - FillerMode = FillerMode.Count, - Count = 1, - Collection = collectionTwo, - CollectionId = collectionTwo.Id - }, - TailFiller = null, - FallbackFiller = null - }; - - var enumerator1 = new ChronologicalMediaCollectionEnumerator( - collectionOne.MediaItems, - new CollectionEnumeratorState()); - - var enumerator2 = new ChronologicalMediaCollectionEnumerator( - collectionTwo.MediaItems, - new CollectionEnumeratorState()); - - var sortedScheduleItems = new List - { - scheduleItem, - NextScheduleItem - }; - - var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( - sortedScheduleItems, - new CollectionEnumeratorState()); - - PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); - - var scheduler = new PlayoutModeSchedulerFlood(new Mock().Object); - (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( - startState, - CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.PostRollFiller, enumerator2), - scheduleItem, - NextScheduleItem, - HardStop(scheduleItemsEnumerator)); - - playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3)); - playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); - - playoutBuilderState.NextGuideGroup.Should().Be(4); - playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); - playoutBuilderState.InFlood.Should().BeFalse(); - playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); - playoutBuilderState.InDurationFiller.Should().BeFalse(); - playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(1); - - enumerator1.State.Index.Should().Be(1); - enumerator2.State.Index.Should().Be(1); - - playoutItems.Count.Should().Be(6); - - playoutItems[0].MediaItemId.Should().Be(1); - playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); - playoutItems[0].GuideGroup.Should().Be(1); - playoutItems[0].FillerKind.Should().Be(FillerKind.None); - - playoutItems[1].MediaItemId.Should().Be(3); - playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55)); - playoutItems[1].GuideGroup.Should().Be(1); - playoutItems[1].FillerKind.Should().Be(FillerKind.PostRoll); - - playoutItems[2].MediaItemId.Should().Be(2); - playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.AddHours(1)); - playoutItems[2].GuideGroup.Should().Be(2); - playoutItems[2].FillerKind.Should().Be(FillerKind.None); - - playoutItems[3].MediaItemId.Should().Be(4); - playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 55, 0))); - playoutItems[3].GuideGroup.Should().Be(2); - playoutItems[3].FillerKind.Should().Be(FillerKind.PostRoll); - - playoutItems[4].MediaItemId.Should().Be(1); - playoutItems[4].StartOffset.Should().Be(startState.CurrentTime.AddHours(2)); - playoutItems[4].GuideGroup.Should().Be(3); - playoutItems[4].FillerKind.Should().Be(FillerKind.None); - - playoutItems[5].MediaItemId.Should().Be(3); - playoutItems[5].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 55, 0))); - playoutItems[5].GuideGroup.Should().Be(3); - playoutItems[5].FillerKind.Should().Be(FillerKind.PostRoll); - } - - [Test] - public void Should_Have_Gap_With_No_Tail_No_Fallback() - { - Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55)); - - var scheduleItem = new ProgramScheduleItemFlood - { - Id = 1, - Index = 1, - Collection = collectionOne, - CollectionId = collectionOne.Id, - StartTime = null, - PlaybackOrder = PlaybackOrder.Chronological, - TailFiller = null, - FallbackFiller = null - }; - - var enumerator = new ChronologicalMediaCollectionEnumerator( - collectionOne.MediaItems, - new CollectionEnumeratorState()); - - var sortedScheduleItems = new List - { - scheduleItem, - NextScheduleItem - }; - - var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( - sortedScheduleItems, - new CollectionEnumeratorState()); - - PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); - - var scheduler = new PlayoutModeSchedulerFlood(new Mock().Object); - (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( - startState, - CollectionEnumerators(scheduleItem, enumerator), - scheduleItem, - NextScheduleItem, - HardStop(scheduleItemsEnumerator)); - - playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0))); - playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); - - playoutBuilderState.NextGuideGroup.Should().Be(4); - playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); - playoutBuilderState.InFlood.Should().BeFalse(); - playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); - playoutBuilderState.InDurationFiller.Should().BeFalse(); - playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(1); - - enumerator.State.Index.Should().Be(1); - - playoutItems.Count.Should().Be(3); - - playoutItems[0].MediaItemId.Should().Be(1); - playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); - playoutItems[0].GuideGroup.Should().Be(1); - playoutItems[0].FillerKind.Should().Be(FillerKind.None); - - playoutItems[1].MediaItemId.Should().Be(2); - playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55)); - playoutItems[1].GuideGroup.Should().Be(2); - playoutItems[1].FillerKind.Should().Be(FillerKind.None); - - playoutItems[2].MediaItemId.Should().Be(1); - playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0))); - playoutItems[2].GuideGroup.Should().Be(3); - playoutItems[2].FillerKind.Should().Be(FillerKind.None); - } - - [Test] - public void Should_Not_Have_Gap_With_Exact_Tail() - { - Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55)); - Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(5)); - - var scheduleItem = new ProgramScheduleItemFlood - { - Id = 1, - Index = 1, - Collection = collectionOne, - CollectionId = collectionOne.Id, - StartTime = null, - PlaybackOrder = PlaybackOrder.Chronological, - TailFiller = new FillerPreset - { - FillerKind = FillerKind.Tail, - Collection = collectionTwo, - CollectionId = collectionTwo.Id - }, - FallbackFiller = null - }; - - var enumerator1 = new ChronologicalMediaCollectionEnumerator( - collectionOne.MediaItems, - new CollectionEnumeratorState()); - - var enumerator2 = new ChronologicalMediaCollectionEnumerator( - collectionTwo.MediaItems, - new CollectionEnumeratorState()); - - var sortedScheduleItems = new List - { - scheduleItem, - NextScheduleItem - }; - - var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( - sortedScheduleItems, - new CollectionEnumeratorState()); - - PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); - - var scheduler = new PlayoutModeSchedulerFlood(new Mock().Object); - (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( - startState, - CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2), - scheduleItem, - NextScheduleItem, - HardStop(scheduleItemsEnumerator)); - - playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3)); - playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); - - playoutBuilderState.NextGuideGroup.Should().Be(4); - playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); - playoutBuilderState.InFlood.Should().BeFalse(); - playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); - playoutBuilderState.InDurationFiller.Should().BeFalse(); - playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(1); - - enumerator1.State.Index.Should().Be(1); - enumerator2.State.Index.Should().Be(1); - - playoutItems.Count.Should().Be(6); - - playoutItems[0].MediaItemId.Should().Be(1); - playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); - playoutItems[0].GuideGroup.Should().Be(1); - playoutItems[0].FillerKind.Should().Be(FillerKind.None); - - playoutItems[1].MediaItemId.Should().Be(2); - playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55)); - playoutItems[1].GuideGroup.Should().Be(2); - playoutItems[1].FillerKind.Should().Be(FillerKind.None); - - playoutItems[2].MediaItemId.Should().Be(1); - playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0))); - playoutItems[2].GuideGroup.Should().Be(3); - playoutItems[2].FillerKind.Should().Be(FillerKind.None); - - playoutItems[3].MediaItemId.Should().Be(3); - playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0))); - playoutItems[3].GuideGroup.Should().Be(3); - playoutItems[3].FillerKind.Should().Be(FillerKind.Tail); - - playoutItems[4].MediaItemId.Should().Be(4); - playoutItems[4].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 50, 0))); - playoutItems[4].GuideGroup.Should().Be(3); - playoutItems[4].FillerKind.Should().Be(FillerKind.Tail); - - playoutItems[5].MediaItemId.Should().Be(3); - playoutItems[5].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 55, 0))); - playoutItems[5].GuideGroup.Should().Be(3); - playoutItems[5].FillerKind.Should().Be(FillerKind.Tail); - } - - [Test] - public void Should_Not_Have_Gap_With_Fallback() - { - Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55)); - Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(5)); - - var scheduleItem = new ProgramScheduleItemFlood - { - Id = 1, - Index = 1, - Collection = collectionOne, - CollectionId = collectionOne.Id, - StartTime = null, - PlaybackOrder = PlaybackOrder.Chronological, - TailFiller = null, - FallbackFiller = new FillerPreset - { - FillerKind = FillerKind.Tail, - Collection = collectionTwo, - CollectionId = collectionTwo.Id - } - }; - - var enumerator1 = new ChronologicalMediaCollectionEnumerator( - collectionOne.MediaItems, - new CollectionEnumeratorState()); - - var enumerator2 = new ChronologicalMediaCollectionEnumerator( - collectionTwo.MediaItems, - new CollectionEnumeratorState()); - - var sortedScheduleItems = new List - { - scheduleItem, - NextScheduleItem - }; - - var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( - sortedScheduleItems, - new CollectionEnumeratorState()); - - PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); - - var scheduler = new PlayoutModeSchedulerFlood(new Mock().Object); - (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( - startState, - CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.FallbackFiller, enumerator2), - scheduleItem, - NextScheduleItem, - HardStop(scheduleItemsEnumerator)); - - playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3)); - playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); - - playoutBuilderState.NextGuideGroup.Should().Be(4); - playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); - playoutBuilderState.InFlood.Should().BeFalse(); - playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); - playoutBuilderState.InDurationFiller.Should().BeFalse(); - playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(1); - - enumerator1.State.Index.Should().Be(1); - enumerator2.State.Index.Should().Be(1); - - playoutItems.Count.Should().Be(4); - - playoutItems[0].MediaItemId.Should().Be(1); - playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); - playoutItems[0].GuideGroup.Should().Be(1); - playoutItems[0].FillerKind.Should().Be(FillerKind.None); - - playoutItems[1].MediaItemId.Should().Be(2); - playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55)); - playoutItems[1].GuideGroup.Should().Be(2); - playoutItems[1].FillerKind.Should().Be(FillerKind.None); - - playoutItems[2].MediaItemId.Should().Be(1); - playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0))); - playoutItems[2].GuideGroup.Should().Be(3); - playoutItems[2].FillerKind.Should().Be(FillerKind.None); - - playoutItems[3].MediaItemId.Should().Be(3); - playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0))); - playoutItems[3].GuideGroup.Should().Be(3); - playoutItems[3].FillerKind.Should().Be(FillerKind.Fallback); - } - - [Test] - public void Should_Have_Gap_With_Tail_No_Fallback() - { - Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55)); - Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(4)); - - var scheduleItem = new ProgramScheduleItemFlood - { - Id = 1, - Index = 1, - Collection = collectionOne, - CollectionId = collectionOne.Id, - StartTime = null, - PlaybackOrder = PlaybackOrder.Chronological, - TailFiller = new FillerPreset - { - FillerKind = FillerKind.Tail, - Collection = collectionTwo, - CollectionId = collectionTwo.Id - }, - FallbackFiller = null - }; - - var enumerator1 = new ChronologicalMediaCollectionEnumerator( - collectionOne.MediaItems, - new CollectionEnumeratorState()); - - var enumerator2 = new ChronologicalMediaCollectionEnumerator( - collectionTwo.MediaItems, - new CollectionEnumeratorState()); - - var sortedScheduleItems = new List - { - scheduleItem, - NextScheduleItem - }; - - var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( - sortedScheduleItems, - new CollectionEnumeratorState()); - - PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); - - var scheduler = new PlayoutModeSchedulerFlood(new Mock().Object); - (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( - startState, - CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2), - scheduleItem, - NextScheduleItem, - HardStop(scheduleItemsEnumerator)); - - playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 57, 0))); - playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); - - playoutBuilderState.NextGuideGroup.Should().Be(4); - playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); - playoutBuilderState.InFlood.Should().BeFalse(); - playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); - playoutBuilderState.InDurationFiller.Should().BeFalse(); - playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(1); - - enumerator1.State.Index.Should().Be(1); - enumerator2.State.Index.Should().Be(1); - - playoutItems.Count.Should().Be(6); - - playoutItems[0].MediaItemId.Should().Be(1); - playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); - playoutItems[0].GuideGroup.Should().Be(1); - playoutItems[0].FillerKind.Should().Be(FillerKind.None); - - playoutItems[1].MediaItemId.Should().Be(2); - playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55)); - playoutItems[1].GuideGroup.Should().Be(2); - playoutItems[1].FillerKind.Should().Be(FillerKind.None); - - playoutItems[2].MediaItemId.Should().Be(1); - playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0))); - playoutItems[2].GuideGroup.Should().Be(3); - playoutItems[2].FillerKind.Should().Be(FillerKind.None); - - playoutItems[3].MediaItemId.Should().Be(3); - playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0))); - playoutItems[3].GuideGroup.Should().Be(3); - playoutItems[3].FillerKind.Should().Be(FillerKind.Tail); - - playoutItems[4].MediaItemId.Should().Be(4); - playoutItems[4].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 49, 0))); - playoutItems[4].GuideGroup.Should().Be(3); - playoutItems[4].FillerKind.Should().Be(FillerKind.Tail); - - playoutItems[5].MediaItemId.Should().Be(3); - playoutItems[5].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 53, 0))); - playoutItems[5].GuideGroup.Should().Be(3); - playoutItems[5].FillerKind.Should().Be(FillerKind.Tail); - } - - [Test] - public void Should_Not_Have_Gap_With_Tail_And_Fallback() - { - Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55)); - Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(4)); - Collection collectionThree = TwoItemCollection(5, 6, TimeSpan.FromMinutes(1)); - - var scheduleItem = new ProgramScheduleItemFlood - { - Id = 1, - Index = 1, - Collection = collectionOne, - CollectionId = collectionOne.Id, - StartTime = null, - PlaybackOrder = PlaybackOrder.Chronological, - TailFiller = new FillerPreset - { - FillerKind = FillerKind.Tail, - Collection = collectionTwo, - CollectionId = collectionTwo.Id - }, - FallbackFiller = new FillerPreset - { - FillerKind = FillerKind.Fallback, - Collection = collectionThree, - CollectionId = collectionThree.Id - } - }; - - var enumerator1 = new ChronologicalMediaCollectionEnumerator( - collectionOne.MediaItems, - new CollectionEnumeratorState()); - - var enumerator2 = new ChronologicalMediaCollectionEnumerator( - collectionTwo.MediaItems, - new CollectionEnumeratorState()); - - var enumerator3 = new ChronologicalMediaCollectionEnumerator( - collectionThree.MediaItems, - new CollectionEnumeratorState()); - - var sortedScheduleItems = new List - { - scheduleItem, - NextScheduleItem - }; - - var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( - sortedScheduleItems, - new CollectionEnumeratorState()); - - PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); - - var scheduler = new PlayoutModeSchedulerFlood(new Mock().Object); - (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( - startState, - CollectionEnumerators( - scheduleItem, - enumerator1, - scheduleItem.TailFiller, - enumerator2, - scheduleItem.FallbackFiller, - enumerator3), - scheduleItem, - NextScheduleItem, - HardStop(scheduleItemsEnumerator)); - - playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3)); - playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); - - playoutBuilderState.NextGuideGroup.Should().Be(4); - playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); - playoutBuilderState.InFlood.Should().BeFalse(); - playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); - playoutBuilderState.InDurationFiller.Should().BeFalse(); - playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(1); - - enumerator1.State.Index.Should().Be(1); - enumerator2.State.Index.Should().Be(1); - enumerator3.State.Index.Should().Be(1); - - playoutItems.Count.Should().Be(7); - - playoutItems[0].MediaItemId.Should().Be(1); - playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); - playoutItems[0].GuideGroup.Should().Be(1); - playoutItems[0].FillerKind.Should().Be(FillerKind.None); - - playoutItems[1].MediaItemId.Should().Be(2); - playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55)); - playoutItems[1].GuideGroup.Should().Be(2); - playoutItems[1].FillerKind.Should().Be(FillerKind.None); - - playoutItems[2].MediaItemId.Should().Be(1); - playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0))); - playoutItems[2].GuideGroup.Should().Be(3); - playoutItems[2].FillerKind.Should().Be(FillerKind.None); - - playoutItems[3].MediaItemId.Should().Be(3); - playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0))); - playoutItems[3].GuideGroup.Should().Be(3); - playoutItems[3].FillerKind.Should().Be(FillerKind.Tail); - - playoutItems[4].MediaItemId.Should().Be(4); - playoutItems[4].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 49, 0))); - playoutItems[4].GuideGroup.Should().Be(3); - playoutItems[4].FillerKind.Should().Be(FillerKind.Tail); - - playoutItems[5].MediaItemId.Should().Be(3); - playoutItems[5].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 53, 0))); - playoutItems[5].GuideGroup.Should().Be(3); - playoutItems[5].FillerKind.Should().Be(FillerKind.Tail); - - playoutItems[6].MediaItemId.Should().Be(5); - playoutItems[6].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 57, 0))); - playoutItems[6].GuideGroup.Should().Be(3); - playoutItems[6].FillerKind.Should().Be(FillerKind.Fallback); - } - - [Test] - public void Should_Not_Have_Gap_With_Unused_Tail_And_Unused_Fallback() - { - Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromHours(1)); - Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(4)); - Collection collectionThree = TwoItemCollection(5, 6, TimeSpan.FromMinutes(1)); - - var scheduleItem = new ProgramScheduleItemFlood - { - Id = 1, - Index = 1, - Collection = collectionOne, - CollectionId = collectionOne.Id, - StartTime = null, - PlaybackOrder = PlaybackOrder.Chronological, - TailFiller = new FillerPreset - { - FillerKind = FillerKind.Tail, - Collection = collectionTwo, - CollectionId = collectionTwo.Id - }, - FallbackFiller = new FillerPreset - { - FillerKind = FillerKind.Fallback, - Collection = collectionThree, - CollectionId = collectionThree.Id - } - }; - - var enumerator1 = new ChronologicalMediaCollectionEnumerator( - collectionOne.MediaItems, - new CollectionEnumeratorState()); - - var enumerator2 = new ChronologicalMediaCollectionEnumerator( - collectionTwo.MediaItems, - new CollectionEnumeratorState()); - - var enumerator3 = new ChronologicalMediaCollectionEnumerator( - collectionThree.MediaItems, - new CollectionEnumeratorState()); - - var sortedScheduleItems = new List - { - scheduleItem, - NextScheduleItem - }; - - var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( - sortedScheduleItems, - new CollectionEnumeratorState()); - - PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); - - var scheduler = new PlayoutModeSchedulerFlood(new Mock().Object); - (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( - startState, - CollectionEnumerators( - scheduleItem, - enumerator1, - scheduleItem.TailFiller, - enumerator2, - scheduleItem.FallbackFiller, - enumerator3), - scheduleItem, - NextScheduleItem, - HardStop(scheduleItemsEnumerator)); - - playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3)); - playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); - - playoutBuilderState.NextGuideGroup.Should().Be(4); - playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); - playoutBuilderState.InFlood.Should().BeFalse(); - playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); - playoutBuilderState.InDurationFiller.Should().BeFalse(); - playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(1); - - enumerator1.State.Index.Should().Be(1); - enumerator2.State.Index.Should().Be(0); - enumerator3.State.Index.Should().Be(0); - - playoutItems.Count.Should().Be(3); - - playoutItems[0].MediaItemId.Should().Be(1); - playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); - playoutItems[0].GuideGroup.Should().Be(1); - playoutItems[0].FillerKind.Should().Be(FillerKind.None); - - playoutItems[1].MediaItemId.Should().Be(2); - playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddHours(1)); - playoutItems[1].GuideGroup.Should().Be(2); - playoutItems[1].FillerKind.Should().Be(FillerKind.None); - - playoutItems[2].MediaItemId.Should().Be(1); - playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.AddHours(2)); - playoutItems[2].GuideGroup.Should().Be(3); - playoutItems[2].FillerKind.Should().Be(FillerKind.None); - } - - protected override ProgramScheduleItem NextScheduleItem => new ProgramScheduleItemOne - { - StartTime = TimeSpan.FromHours(3), + Id = 1, + Index = 1, + Collection = collectionOne, + CollectionId = collectionOne.Id, + StartTime = null, + PlaybackOrder = PlaybackOrder.Chronological, + TailFiller = null, + FallbackFiller = null }; + + var enumerator = new ChronologicalMediaCollectionEnumerator( + collectionOne.MediaItems, + new CollectionEnumeratorState()); + + var sortedScheduleItems = new List + { + scheduleItem, + NextScheduleItem + }; + + var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( + sortedScheduleItems, + new CollectionEnumeratorState()); + + PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); + + var scheduler = new PlayoutModeSchedulerFlood(new Mock().Object); + (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( + startState, + CollectionEnumerators(scheduleItem, enumerator), + scheduleItem, + NextScheduleItem, + HardStop(scheduleItemsEnumerator)); + + playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3)); + playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); + + playoutBuilderState.NextGuideGroup.Should().Be(4); + playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); + playoutBuilderState.InFlood.Should().BeFalse(); + playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); + playoutBuilderState.InDurationFiller.Should().BeFalse(); + playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(1); + + enumerator.State.Index.Should().Be(1); + + playoutItems.Count.Should().Be(3); + + playoutItems[0].MediaItemId.Should().Be(1); + playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); + playoutItems[0].GuideGroup.Should().Be(1); + playoutItems[0].FillerKind.Should().Be(FillerKind.None); + + playoutItems[1].MediaItemId.Should().Be(2); + playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddHours(1)); + playoutItems[1].GuideGroup.Should().Be(2); + playoutItems[1].FillerKind.Should().Be(FillerKind.None); + + playoutItems[2].MediaItemId.Should().Be(1); + playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.AddHours(2)); + playoutItems[2].GuideGroup.Should().Be(3); + playoutItems[2].FillerKind.Should().Be(FillerKind.None); } -} + + [Test] + public void Should_Fill_Exactly_To_Next_Schedule_Item_Flood() + { + Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromHours(1)); + + var scheduleItem = new ProgramScheduleItemFlood + { + Id = 1, + Index = 1, + Collection = collectionOne, + CollectionId = collectionOne.Id, + StartTime = null, + PlaybackOrder = PlaybackOrder.Chronological, + TailFiller = null, + FallbackFiller = null + }; + + var enumerator = new ChronologicalMediaCollectionEnumerator( + collectionOne.MediaItems, + new CollectionEnumeratorState()); + + var sortedScheduleItems = new List + { + scheduleItem, + // this caused trouble with the peek logic and the IsFlood flag + new ProgramScheduleItemFlood + { + StartTime = TimeSpan.FromHours(3), + } + }; + + var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( + sortedScheduleItems, + new CollectionEnumeratorState()); + + PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); + + var scheduler = new PlayoutModeSchedulerFlood(new Mock().Object); + (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( + startState, + CollectionEnumerators(scheduleItem, enumerator), + scheduleItem, + NextScheduleItem, + HardStop(scheduleItemsEnumerator)); + + playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3)); + playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); + + playoutBuilderState.NextGuideGroup.Should().Be(4); + playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); + playoutBuilderState.InFlood.Should().BeFalse(); + playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); + playoutBuilderState.InDurationFiller.Should().BeFalse(); + playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(1); + + enumerator.State.Index.Should().Be(1); + + playoutItems.Count.Should().Be(3); + + playoutItems[0].MediaItemId.Should().Be(1); + playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); + playoutItems[0].GuideGroup.Should().Be(1); + playoutItems[0].FillerKind.Should().Be(FillerKind.None); + + playoutItems[1].MediaItemId.Should().Be(2); + playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddHours(1)); + playoutItems[1].GuideGroup.Should().Be(2); + playoutItems[1].FillerKind.Should().Be(FillerKind.None); + + playoutItems[2].MediaItemId.Should().Be(1); + playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.AddHours(2)); + playoutItems[2].GuideGroup.Should().Be(3); + playoutItems[2].FillerKind.Should().Be(FillerKind.None); + } + + [Test] + public void Should_Fill_Exactly_To_Next_Schedule_Item_With_Post_Roll_Multiple_One() + { + Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55)); + Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(5)); + + var scheduleItem = new ProgramScheduleItemFlood + { + Id = 1, + Index = 1, + Collection = collectionOne, + CollectionId = collectionOne.Id, + StartTime = null, + PlaybackOrder = PlaybackOrder.Chronological, + PostRollFiller = new FillerPreset + { + FillerKind = FillerKind.PostRoll, + FillerMode = FillerMode.Count, + Count = 1, + Collection = collectionTwo, + CollectionId = collectionTwo.Id + }, + TailFiller = null, + FallbackFiller = null + }; + + var enumerator1 = new ChronologicalMediaCollectionEnumerator( + collectionOne.MediaItems, + new CollectionEnumeratorState()); + + var enumerator2 = new ChronologicalMediaCollectionEnumerator( + collectionTwo.MediaItems, + new CollectionEnumeratorState()); + + var sortedScheduleItems = new List + { + scheduleItem, + NextScheduleItem + }; + + var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( + sortedScheduleItems, + new CollectionEnumeratorState()); + + PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); + + var scheduler = new PlayoutModeSchedulerFlood(new Mock().Object); + (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( + startState, + CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.PostRollFiller, enumerator2), + scheduleItem, + NextScheduleItem, + HardStop(scheduleItemsEnumerator)); + + playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3)); + playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); + + playoutBuilderState.NextGuideGroup.Should().Be(4); + playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); + playoutBuilderState.InFlood.Should().BeFalse(); + playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); + playoutBuilderState.InDurationFiller.Should().BeFalse(); + playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(1); + + enumerator1.State.Index.Should().Be(1); + enumerator2.State.Index.Should().Be(1); + + playoutItems.Count.Should().Be(6); + + playoutItems[0].MediaItemId.Should().Be(1); + playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); + playoutItems[0].GuideGroup.Should().Be(1); + playoutItems[0].FillerKind.Should().Be(FillerKind.None); + + playoutItems[1].MediaItemId.Should().Be(3); + playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55)); + playoutItems[1].GuideGroup.Should().Be(1); + playoutItems[1].FillerKind.Should().Be(FillerKind.PostRoll); + + playoutItems[2].MediaItemId.Should().Be(2); + playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.AddHours(1)); + playoutItems[2].GuideGroup.Should().Be(2); + playoutItems[2].FillerKind.Should().Be(FillerKind.None); + + playoutItems[3].MediaItemId.Should().Be(4); + playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 55, 0))); + playoutItems[3].GuideGroup.Should().Be(2); + playoutItems[3].FillerKind.Should().Be(FillerKind.PostRoll); + + playoutItems[4].MediaItemId.Should().Be(1); + playoutItems[4].StartOffset.Should().Be(startState.CurrentTime.AddHours(2)); + playoutItems[4].GuideGroup.Should().Be(3); + playoutItems[4].FillerKind.Should().Be(FillerKind.None); + + playoutItems[5].MediaItemId.Should().Be(3); + playoutItems[5].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 55, 0))); + playoutItems[5].GuideGroup.Should().Be(3); + playoutItems[5].FillerKind.Should().Be(FillerKind.PostRoll); + } + + [Test] + public void Should_Have_Gap_With_No_Tail_No_Fallback() + { + Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55)); + + var scheduleItem = new ProgramScheduleItemFlood + { + Id = 1, + Index = 1, + Collection = collectionOne, + CollectionId = collectionOne.Id, + StartTime = null, + PlaybackOrder = PlaybackOrder.Chronological, + TailFiller = null, + FallbackFiller = null + }; + + var enumerator = new ChronologicalMediaCollectionEnumerator( + collectionOne.MediaItems, + new CollectionEnumeratorState()); + + var sortedScheduleItems = new List + { + scheduleItem, + NextScheduleItem + }; + + var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( + sortedScheduleItems, + new CollectionEnumeratorState()); + + PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); + + var scheduler = new PlayoutModeSchedulerFlood(new Mock().Object); + (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( + startState, + CollectionEnumerators(scheduleItem, enumerator), + scheduleItem, + NextScheduleItem, + HardStop(scheduleItemsEnumerator)); + + playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0))); + playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); + + playoutBuilderState.NextGuideGroup.Should().Be(4); + playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); + playoutBuilderState.InFlood.Should().BeFalse(); + playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); + playoutBuilderState.InDurationFiller.Should().BeFalse(); + playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(1); + + enumerator.State.Index.Should().Be(1); + + playoutItems.Count.Should().Be(3); + + playoutItems[0].MediaItemId.Should().Be(1); + playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); + playoutItems[0].GuideGroup.Should().Be(1); + playoutItems[0].FillerKind.Should().Be(FillerKind.None); + + playoutItems[1].MediaItemId.Should().Be(2); + playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55)); + playoutItems[1].GuideGroup.Should().Be(2); + playoutItems[1].FillerKind.Should().Be(FillerKind.None); + + playoutItems[2].MediaItemId.Should().Be(1); + playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0))); + playoutItems[2].GuideGroup.Should().Be(3); + playoutItems[2].FillerKind.Should().Be(FillerKind.None); + } + + [Test] + public void Should_Not_Have_Gap_With_Exact_Tail() + { + Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55)); + Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(5)); + + var scheduleItem = new ProgramScheduleItemFlood + { + Id = 1, + Index = 1, + Collection = collectionOne, + CollectionId = collectionOne.Id, + StartTime = null, + PlaybackOrder = PlaybackOrder.Chronological, + TailFiller = new FillerPreset + { + FillerKind = FillerKind.Tail, + Collection = collectionTwo, + CollectionId = collectionTwo.Id + }, + FallbackFiller = null + }; + + var enumerator1 = new ChronologicalMediaCollectionEnumerator( + collectionOne.MediaItems, + new CollectionEnumeratorState()); + + var enumerator2 = new ChronologicalMediaCollectionEnumerator( + collectionTwo.MediaItems, + new CollectionEnumeratorState()); + + var sortedScheduleItems = new List + { + scheduleItem, + NextScheduleItem + }; + + var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( + sortedScheduleItems, + new CollectionEnumeratorState()); + + PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); + + var scheduler = new PlayoutModeSchedulerFlood(new Mock().Object); + (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( + startState, + CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2), + scheduleItem, + NextScheduleItem, + HardStop(scheduleItemsEnumerator)); + + playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3)); + playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); + + playoutBuilderState.NextGuideGroup.Should().Be(4); + playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); + playoutBuilderState.InFlood.Should().BeFalse(); + playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); + playoutBuilderState.InDurationFiller.Should().BeFalse(); + playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(1); + + enumerator1.State.Index.Should().Be(1); + enumerator2.State.Index.Should().Be(1); + + playoutItems.Count.Should().Be(6); + + playoutItems[0].MediaItemId.Should().Be(1); + playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); + playoutItems[0].GuideGroup.Should().Be(1); + playoutItems[0].FillerKind.Should().Be(FillerKind.None); + + playoutItems[1].MediaItemId.Should().Be(2); + playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55)); + playoutItems[1].GuideGroup.Should().Be(2); + playoutItems[1].FillerKind.Should().Be(FillerKind.None); + + playoutItems[2].MediaItemId.Should().Be(1); + playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0))); + playoutItems[2].GuideGroup.Should().Be(3); + playoutItems[2].FillerKind.Should().Be(FillerKind.None); + + playoutItems[3].MediaItemId.Should().Be(3); + playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0))); + playoutItems[3].GuideGroup.Should().Be(3); + playoutItems[3].FillerKind.Should().Be(FillerKind.Tail); + + playoutItems[4].MediaItemId.Should().Be(4); + playoutItems[4].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 50, 0))); + playoutItems[4].GuideGroup.Should().Be(3); + playoutItems[4].FillerKind.Should().Be(FillerKind.Tail); + + playoutItems[5].MediaItemId.Should().Be(3); + playoutItems[5].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 55, 0))); + playoutItems[5].GuideGroup.Should().Be(3); + playoutItems[5].FillerKind.Should().Be(FillerKind.Tail); + } + + [Test] + public void Should_Not_Have_Gap_With_Fallback() + { + Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55)); + Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(5)); + + var scheduleItem = new ProgramScheduleItemFlood + { + Id = 1, + Index = 1, + Collection = collectionOne, + CollectionId = collectionOne.Id, + StartTime = null, + PlaybackOrder = PlaybackOrder.Chronological, + TailFiller = null, + FallbackFiller = new FillerPreset + { + FillerKind = FillerKind.Tail, + Collection = collectionTwo, + CollectionId = collectionTwo.Id + } + }; + + var enumerator1 = new ChronologicalMediaCollectionEnumerator( + collectionOne.MediaItems, + new CollectionEnumeratorState()); + + var enumerator2 = new ChronologicalMediaCollectionEnumerator( + collectionTwo.MediaItems, + new CollectionEnumeratorState()); + + var sortedScheduleItems = new List + { + scheduleItem, + NextScheduleItem + }; + + var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( + sortedScheduleItems, + new CollectionEnumeratorState()); + + PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); + + var scheduler = new PlayoutModeSchedulerFlood(new Mock().Object); + (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( + startState, + CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.FallbackFiller, enumerator2), + scheduleItem, + NextScheduleItem, + HardStop(scheduleItemsEnumerator)); + + playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3)); + playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); + + playoutBuilderState.NextGuideGroup.Should().Be(4); + playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); + playoutBuilderState.InFlood.Should().BeFalse(); + playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); + playoutBuilderState.InDurationFiller.Should().BeFalse(); + playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(1); + + enumerator1.State.Index.Should().Be(1); + enumerator2.State.Index.Should().Be(1); + + playoutItems.Count.Should().Be(4); + + playoutItems[0].MediaItemId.Should().Be(1); + playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); + playoutItems[0].GuideGroup.Should().Be(1); + playoutItems[0].FillerKind.Should().Be(FillerKind.None); + + playoutItems[1].MediaItemId.Should().Be(2); + playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55)); + playoutItems[1].GuideGroup.Should().Be(2); + playoutItems[1].FillerKind.Should().Be(FillerKind.None); + + playoutItems[2].MediaItemId.Should().Be(1); + playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0))); + playoutItems[2].GuideGroup.Should().Be(3); + playoutItems[2].FillerKind.Should().Be(FillerKind.None); + + playoutItems[3].MediaItemId.Should().Be(3); + playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0))); + playoutItems[3].GuideGroup.Should().Be(3); + playoutItems[3].FillerKind.Should().Be(FillerKind.Fallback); + } + + [Test] + public void Should_Have_Gap_With_Tail_No_Fallback() + { + Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55)); + Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(4)); + + var scheduleItem = new ProgramScheduleItemFlood + { + Id = 1, + Index = 1, + Collection = collectionOne, + CollectionId = collectionOne.Id, + StartTime = null, + PlaybackOrder = PlaybackOrder.Chronological, + TailFiller = new FillerPreset + { + FillerKind = FillerKind.Tail, + Collection = collectionTwo, + CollectionId = collectionTwo.Id + }, + FallbackFiller = null + }; + + var enumerator1 = new ChronologicalMediaCollectionEnumerator( + collectionOne.MediaItems, + new CollectionEnumeratorState()); + + var enumerator2 = new ChronologicalMediaCollectionEnumerator( + collectionTwo.MediaItems, + new CollectionEnumeratorState()); + + var sortedScheduleItems = new List + { + scheduleItem, + NextScheduleItem + }; + + var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( + sortedScheduleItems, + new CollectionEnumeratorState()); + + PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); + + var scheduler = new PlayoutModeSchedulerFlood(new Mock().Object); + (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( + startState, + CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2), + scheduleItem, + NextScheduleItem, + HardStop(scheduleItemsEnumerator)); + + playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 57, 0))); + playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); + + playoutBuilderState.NextGuideGroup.Should().Be(4); + playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); + playoutBuilderState.InFlood.Should().BeFalse(); + playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); + playoutBuilderState.InDurationFiller.Should().BeFalse(); + playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(1); + + enumerator1.State.Index.Should().Be(1); + enumerator2.State.Index.Should().Be(1); + + playoutItems.Count.Should().Be(6); + + playoutItems[0].MediaItemId.Should().Be(1); + playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); + playoutItems[0].GuideGroup.Should().Be(1); + playoutItems[0].FillerKind.Should().Be(FillerKind.None); + + playoutItems[1].MediaItemId.Should().Be(2); + playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55)); + playoutItems[1].GuideGroup.Should().Be(2); + playoutItems[1].FillerKind.Should().Be(FillerKind.None); + + playoutItems[2].MediaItemId.Should().Be(1); + playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0))); + playoutItems[2].GuideGroup.Should().Be(3); + playoutItems[2].FillerKind.Should().Be(FillerKind.None); + + playoutItems[3].MediaItemId.Should().Be(3); + playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0))); + playoutItems[3].GuideGroup.Should().Be(3); + playoutItems[3].FillerKind.Should().Be(FillerKind.Tail); + + playoutItems[4].MediaItemId.Should().Be(4); + playoutItems[4].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 49, 0))); + playoutItems[4].GuideGroup.Should().Be(3); + playoutItems[4].FillerKind.Should().Be(FillerKind.Tail); + + playoutItems[5].MediaItemId.Should().Be(3); + playoutItems[5].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 53, 0))); + playoutItems[5].GuideGroup.Should().Be(3); + playoutItems[5].FillerKind.Should().Be(FillerKind.Tail); + } + + [Test] + public void Should_Not_Have_Gap_With_Tail_And_Fallback() + { + Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55)); + Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(4)); + Collection collectionThree = TwoItemCollection(5, 6, TimeSpan.FromMinutes(1)); + + var scheduleItem = new ProgramScheduleItemFlood + { + Id = 1, + Index = 1, + Collection = collectionOne, + CollectionId = collectionOne.Id, + StartTime = null, + PlaybackOrder = PlaybackOrder.Chronological, + TailFiller = new FillerPreset + { + FillerKind = FillerKind.Tail, + Collection = collectionTwo, + CollectionId = collectionTwo.Id + }, + FallbackFiller = new FillerPreset + { + FillerKind = FillerKind.Fallback, + Collection = collectionThree, + CollectionId = collectionThree.Id + } + }; + + var enumerator1 = new ChronologicalMediaCollectionEnumerator( + collectionOne.MediaItems, + new CollectionEnumeratorState()); + + var enumerator2 = new ChronologicalMediaCollectionEnumerator( + collectionTwo.MediaItems, + new CollectionEnumeratorState()); + + var enumerator3 = new ChronologicalMediaCollectionEnumerator( + collectionThree.MediaItems, + new CollectionEnumeratorState()); + + var sortedScheduleItems = new List + { + scheduleItem, + NextScheduleItem + }; + + var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( + sortedScheduleItems, + new CollectionEnumeratorState()); + + PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); + + var scheduler = new PlayoutModeSchedulerFlood(new Mock().Object); + (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( + startState, + CollectionEnumerators( + scheduleItem, + enumerator1, + scheduleItem.TailFiller, + enumerator2, + scheduleItem.FallbackFiller, + enumerator3), + scheduleItem, + NextScheduleItem, + HardStop(scheduleItemsEnumerator)); + + playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3)); + playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); + + playoutBuilderState.NextGuideGroup.Should().Be(4); + playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); + playoutBuilderState.InFlood.Should().BeFalse(); + playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); + playoutBuilderState.InDurationFiller.Should().BeFalse(); + playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(1); + + enumerator1.State.Index.Should().Be(1); + enumerator2.State.Index.Should().Be(1); + enumerator3.State.Index.Should().Be(1); + + playoutItems.Count.Should().Be(7); + + playoutItems[0].MediaItemId.Should().Be(1); + playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); + playoutItems[0].GuideGroup.Should().Be(1); + playoutItems[0].FillerKind.Should().Be(FillerKind.None); + + playoutItems[1].MediaItemId.Should().Be(2); + playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55)); + playoutItems[1].GuideGroup.Should().Be(2); + playoutItems[1].FillerKind.Should().Be(FillerKind.None); + + playoutItems[2].MediaItemId.Should().Be(1); + playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0))); + playoutItems[2].GuideGroup.Should().Be(3); + playoutItems[2].FillerKind.Should().Be(FillerKind.None); + + playoutItems[3].MediaItemId.Should().Be(3); + playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0))); + playoutItems[3].GuideGroup.Should().Be(3); + playoutItems[3].FillerKind.Should().Be(FillerKind.Tail); + + playoutItems[4].MediaItemId.Should().Be(4); + playoutItems[4].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 49, 0))); + playoutItems[4].GuideGroup.Should().Be(3); + playoutItems[4].FillerKind.Should().Be(FillerKind.Tail); + + playoutItems[5].MediaItemId.Should().Be(3); + playoutItems[5].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 53, 0))); + playoutItems[5].GuideGroup.Should().Be(3); + playoutItems[5].FillerKind.Should().Be(FillerKind.Tail); + + playoutItems[6].MediaItemId.Should().Be(5); + playoutItems[6].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 57, 0))); + playoutItems[6].GuideGroup.Should().Be(3); + playoutItems[6].FillerKind.Should().Be(FillerKind.Fallback); + } + + [Test] + public void Should_Not_Have_Gap_With_Unused_Tail_And_Unused_Fallback() + { + Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromHours(1)); + Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(4)); + Collection collectionThree = TwoItemCollection(5, 6, TimeSpan.FromMinutes(1)); + + var scheduleItem = new ProgramScheduleItemFlood + { + Id = 1, + Index = 1, + Collection = collectionOne, + CollectionId = collectionOne.Id, + StartTime = null, + PlaybackOrder = PlaybackOrder.Chronological, + TailFiller = new FillerPreset + { + FillerKind = FillerKind.Tail, + Collection = collectionTwo, + CollectionId = collectionTwo.Id + }, + FallbackFiller = new FillerPreset + { + FillerKind = FillerKind.Fallback, + Collection = collectionThree, + CollectionId = collectionThree.Id + } + }; + + var enumerator1 = new ChronologicalMediaCollectionEnumerator( + collectionOne.MediaItems, + new CollectionEnumeratorState()); + + var enumerator2 = new ChronologicalMediaCollectionEnumerator( + collectionTwo.MediaItems, + new CollectionEnumeratorState()); + + var enumerator3 = new ChronologicalMediaCollectionEnumerator( + collectionThree.MediaItems, + new CollectionEnumeratorState()); + + var sortedScheduleItems = new List + { + scheduleItem, + NextScheduleItem + }; + + var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( + sortedScheduleItems, + new CollectionEnumeratorState()); + + PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); + + var scheduler = new PlayoutModeSchedulerFlood(new Mock().Object); + (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( + startState, + CollectionEnumerators( + scheduleItem, + enumerator1, + scheduleItem.TailFiller, + enumerator2, + scheduleItem.FallbackFiller, + enumerator3), + scheduleItem, + NextScheduleItem, + HardStop(scheduleItemsEnumerator)); + + playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3)); + playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); + + playoutBuilderState.NextGuideGroup.Should().Be(4); + playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); + playoutBuilderState.InFlood.Should().BeFalse(); + playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); + playoutBuilderState.InDurationFiller.Should().BeFalse(); + playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(1); + + enumerator1.State.Index.Should().Be(1); + enumerator2.State.Index.Should().Be(0); + enumerator3.State.Index.Should().Be(0); + + playoutItems.Count.Should().Be(3); + + playoutItems[0].MediaItemId.Should().Be(1); + playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); + playoutItems[0].GuideGroup.Should().Be(1); + playoutItems[0].FillerKind.Should().Be(FillerKind.None); + + playoutItems[1].MediaItemId.Should().Be(2); + playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddHours(1)); + playoutItems[1].GuideGroup.Should().Be(2); + playoutItems[1].FillerKind.Should().Be(FillerKind.None); + + playoutItems[2].MediaItemId.Should().Be(1); + playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.AddHours(2)); + playoutItems[2].GuideGroup.Should().Be(3); + playoutItems[2].FillerKind.Should().Be(FillerKind.None); + } + + protected override ProgramScheduleItem NextScheduleItem => new ProgramScheduleItemOne + { + StartTime = TimeSpan.FromHours(3), + }; +} \ No newline at end of file diff --git a/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerMultipleTests.cs b/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerMultipleTests.cs index 28bc07201..bfe37da57 100644 --- a/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerMultipleTests.cs +++ b/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerMultipleTests.cs @@ -1,672 +1,667 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Filler; using ErsatzTV.Core.Scheduling; using FluentAssertions; -using LanguageExt; using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; -namespace ErsatzTV.Core.Tests.Scheduling +namespace ErsatzTV.Core.Tests.Scheduling; + +[TestFixture] +public class PlayoutModeSchedulerMultipleTests : SchedulerTestBase { - [TestFixture] - public class PlayoutModeSchedulerMultipleTests : SchedulerTestBase + [Test] + public void Should_Fill_Exactly_To_Next_Schedule_Item() { - [Test] - public void Should_Fill_Exactly_To_Next_Schedule_Item() + Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromHours(1)); + + var scheduleItem = new ProgramScheduleItemMultiple { - Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromHours(1)); - - var scheduleItem = new ProgramScheduleItemMultiple - { - Id = 1, - Index = 1, - CollectionType = ProgramScheduleItemCollectionType.Collection, - Collection = collectionOne, - CollectionId = collectionOne.Id, - StartTime = null, - PlaybackOrder = PlaybackOrder.Chronological, - TailFiller = null, - FallbackFiller = null, - Count = 3 - }; - - var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( - new List { scheduleItem }, - new CollectionEnumeratorState()); - - var enumerator = new ChronologicalMediaCollectionEnumerator( - collectionOne.MediaItems, - new CollectionEnumeratorState()); - - var collectionMediaItems = new Dictionary> - { - { CollectionKey.ForScheduleItem(scheduleItem), collectionOne.MediaItems } - }.ToMap(); - - PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); - - var scheduler = new PlayoutModeSchedulerMultiple(collectionMediaItems, new Mock().Object); - (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( - startState, - CollectionEnumerators(scheduleItem, enumerator), - scheduleItem, - NextScheduleItem, - HardStop(scheduleItemsEnumerator)); - - playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3)); - playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); - - playoutBuilderState.NextGuideGroup.Should().Be(4); - playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); - playoutBuilderState.InFlood.Should().BeFalse(); - playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); - playoutBuilderState.InDurationFiller.Should().BeFalse(); - playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); - - enumerator.State.Index.Should().Be(1); - - playoutItems.Count.Should().Be(3); - - playoutItems[0].MediaItemId.Should().Be(1); - playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); - playoutItems[0].GuideGroup.Should().Be(1); - playoutItems[0].FillerKind.Should().Be(FillerKind.None); - - playoutItems[1].MediaItemId.Should().Be(2); - playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddHours(1)); - playoutItems[1].GuideGroup.Should().Be(2); - playoutItems[1].FillerKind.Should().Be(FillerKind.None); - - playoutItems[2].MediaItemId.Should().Be(1); - playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.AddHours(2)); - playoutItems[2].GuideGroup.Should().Be(3); - playoutItems[2].FillerKind.Should().Be(FillerKind.None); - } - - [Test] - public void Should_Have_Gap_With_No_Tail_No_Fallback() - { - Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55)); - - var scheduleItem = new ProgramScheduleItemMultiple - { - Id = 1, - Index = 1, - Collection = collectionOne, - CollectionId = collectionOne.Id, - StartTime = null, - PlaybackOrder = PlaybackOrder.Chronological, - TailFiller = null, - FallbackFiller = null, - Count = 3 - }; - - var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( - new List { scheduleItem }, - new CollectionEnumeratorState()); - - var enumerator = new ChronologicalMediaCollectionEnumerator( - collectionOne.MediaItems, - new CollectionEnumeratorState()); - - var collectionMediaItems = new Dictionary> - { - { CollectionKey.ForScheduleItem(scheduleItem), collectionOne.MediaItems } - }.ToMap(); - - PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); - - var scheduler = new PlayoutModeSchedulerMultiple(collectionMediaItems, new Mock().Object); - (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( - startState, - CollectionEnumerators(scheduleItem, enumerator), - scheduleItem, - NextScheduleItem, - HardStop(scheduleItemsEnumerator)); - - playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0))); - playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); - - playoutBuilderState.NextGuideGroup.Should().Be(4); - playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); - playoutBuilderState.InFlood.Should().BeFalse(); - playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); - playoutBuilderState.InDurationFiller.Should().BeFalse(); - playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); - - enumerator.State.Index.Should().Be(1); - - playoutItems.Count.Should().Be(3); - - playoutItems[0].MediaItemId.Should().Be(1); - playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); - playoutItems[0].GuideGroup.Should().Be(1); - playoutItems[0].FillerKind.Should().Be(FillerKind.None); - - playoutItems[1].MediaItemId.Should().Be(2); - playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55)); - playoutItems[1].GuideGroup.Should().Be(2); - playoutItems[1].FillerKind.Should().Be(FillerKind.None); - - playoutItems[2].MediaItemId.Should().Be(1); - playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0))); - playoutItems[2].GuideGroup.Should().Be(3); - playoutItems[2].FillerKind.Should().Be(FillerKind.None); - } - - [Test] - public void Should_Not_Have_Gap_With_Exact_Tail() - { - Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55)); - Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(5)); - - var scheduleItem = new ProgramScheduleItemMultiple - { - Id = 1, - Index = 1, - Collection = collectionOne, - CollectionId = collectionOne.Id, - StartTime = null, - PlaybackOrder = PlaybackOrder.Chronological, - TailFiller = new FillerPreset - { - FillerKind = FillerKind.Tail, - Collection = collectionTwo, - CollectionId = collectionTwo.Id - }, - FallbackFiller = null, - Count = 3 - }; - - var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( - new List { scheduleItem }, - new CollectionEnumeratorState()); - - var enumerator1 = new ChronologicalMediaCollectionEnumerator( - collectionOne.MediaItems, - new CollectionEnumeratorState()); - - var enumerator2 = new ChronologicalMediaCollectionEnumerator( - collectionTwo.MediaItems, - new CollectionEnumeratorState()); - - var collectionMediaItems = new Dictionary> - { - { CollectionKey.ForScheduleItem(scheduleItem), collectionOne.MediaItems }, - { CollectionKey.ForFillerPreset(scheduleItem.TailFiller), collectionTwo.MediaItems } - }.ToMap(); - - PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); - - var scheduler = new PlayoutModeSchedulerMultiple(collectionMediaItems, new Mock().Object); - (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( - startState, - CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2), - scheduleItem, - NextScheduleItem, - HardStop(scheduleItemsEnumerator)); - - playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3)); - playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); - - playoutBuilderState.NextGuideGroup.Should().Be(4); - playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); - playoutBuilderState.InFlood.Should().BeFalse(); - playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); - playoutBuilderState.InDurationFiller.Should().BeFalse(); - playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); - - enumerator1.State.Index.Should().Be(1); - enumerator2.State.Index.Should().Be(1); - - playoutItems.Count.Should().Be(6); - - playoutItems[0].MediaItemId.Should().Be(1); - playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); - playoutItems[0].GuideGroup.Should().Be(1); - playoutItems[0].FillerKind.Should().Be(FillerKind.None); - - playoutItems[1].MediaItemId.Should().Be(2); - playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55)); - playoutItems[1].GuideGroup.Should().Be(2); - playoutItems[1].FillerKind.Should().Be(FillerKind.None); - - playoutItems[2].MediaItemId.Should().Be(1); - playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0))); - playoutItems[2].GuideGroup.Should().Be(3); - playoutItems[2].FillerKind.Should().Be(FillerKind.None); - - playoutItems[3].MediaItemId.Should().Be(3); - playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0))); - playoutItems[3].GuideGroup.Should().Be(3); - playoutItems[3].FillerKind.Should().Be(FillerKind.Tail); - - playoutItems[4].MediaItemId.Should().Be(4); - playoutItems[4].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 50, 0))); - playoutItems[4].GuideGroup.Should().Be(3); - playoutItems[4].FillerKind.Should().Be(FillerKind.Tail); - - playoutItems[5].MediaItemId.Should().Be(3); - playoutItems[5].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 55, 0))); - playoutItems[5].GuideGroup.Should().Be(3); - playoutItems[5].FillerKind.Should().Be(FillerKind.Tail); - } - - [Test] - public void Should_Not_Have_Gap_With_Fallback() - { - Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55)); - Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(5)); - - var scheduleItem = new ProgramScheduleItemMultiple - { - Id = 1, - Index = 1, - Collection = collectionOne, - CollectionId = collectionOne.Id, - StartTime = null, - PlaybackOrder = PlaybackOrder.Chronological, - TailFiller = null, - FallbackFiller = new FillerPreset - { - FillerKind = FillerKind.Tail, - Collection = collectionTwo, - CollectionId = collectionTwo.Id - }, - Count = 3 - }; - - var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( - new List { scheduleItem }, - new CollectionEnumeratorState()); - - var enumerator1 = new ChronologicalMediaCollectionEnumerator( - collectionOne.MediaItems, - new CollectionEnumeratorState()); - - var enumerator2 = new ChronologicalMediaCollectionEnumerator( - collectionTwo.MediaItems, - new CollectionEnumeratorState()); - - var collectionMediaItems = new Dictionary> - { - { CollectionKey.ForScheduleItem(scheduleItem), collectionOne.MediaItems }, - { CollectionKey.ForFillerPreset(scheduleItem.FallbackFiller), collectionTwo.MediaItems } - }.ToMap(); - - PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); - - var scheduler = new PlayoutModeSchedulerMultiple(collectionMediaItems, new Mock().Object); - (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( - startState, - CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.FallbackFiller, enumerator2), - scheduleItem, - NextScheduleItem, - HardStop(scheduleItemsEnumerator)); - - playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3)); - playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); - - playoutBuilderState.NextGuideGroup.Should().Be(4); - playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); - playoutBuilderState.InFlood.Should().BeFalse(); - playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); - playoutBuilderState.InDurationFiller.Should().BeFalse(); - playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); - - enumerator1.State.Index.Should().Be(1); - enumerator2.State.Index.Should().Be(1); - - playoutItems.Count.Should().Be(4); - - playoutItems[0].MediaItemId.Should().Be(1); - playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); - playoutItems[0].GuideGroup.Should().Be(1); - playoutItems[0].FillerKind.Should().Be(FillerKind.None); - - playoutItems[1].MediaItemId.Should().Be(2); - playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55)); - playoutItems[1].GuideGroup.Should().Be(2); - playoutItems[1].FillerKind.Should().Be(FillerKind.None); - - playoutItems[2].MediaItemId.Should().Be(1); - playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0))); - playoutItems[2].GuideGroup.Should().Be(3); - playoutItems[2].FillerKind.Should().Be(FillerKind.None); - - playoutItems[3].MediaItemId.Should().Be(3); - playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0))); - playoutItems[3].GuideGroup.Should().Be(3); - playoutItems[3].FillerKind.Should().Be(FillerKind.Fallback); - } - - [Test] - public void Should_Have_Gap_With_Tail_No_Fallback() - { - Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55)); - Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(4)); - - var scheduleItem = new ProgramScheduleItemMultiple - { - Id = 1, - Index = 1, - Collection = collectionOne, - CollectionId = collectionOne.Id, - StartTime = null, - PlaybackOrder = PlaybackOrder.Chronological, - TailFiller = new FillerPreset - { - FillerKind = FillerKind.Tail, - Collection = collectionTwo, - CollectionId = collectionTwo.Id - }, - FallbackFiller = null, - Count = 3 - }; - - var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( - new List { scheduleItem }, - new CollectionEnumeratorState()); - - var enumerator1 = new ChronologicalMediaCollectionEnumerator( - collectionOne.MediaItems, - new CollectionEnumeratorState()); - - var enumerator2 = new ChronologicalMediaCollectionEnumerator( - collectionTwo.MediaItems, - new CollectionEnumeratorState()); - - var collectionMediaItems = new Dictionary> - { - { CollectionKey.ForScheduleItem(scheduleItem), collectionOne.MediaItems }, - { CollectionKey.ForFillerPreset(scheduleItem.TailFiller), collectionTwo.MediaItems } - }.ToMap(); - - PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); - - var scheduler = new PlayoutModeSchedulerMultiple(collectionMediaItems, new Mock().Object); - (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( - startState, - CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2), - scheduleItem, - NextScheduleItem, - HardStop(scheduleItemsEnumerator)); - - playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 57, 0))); - playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); - - playoutBuilderState.NextGuideGroup.Should().Be(4); - playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); - playoutBuilderState.InFlood.Should().BeFalse(); - playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); - playoutBuilderState.InDurationFiller.Should().BeFalse(); - playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); - - enumerator1.State.Index.Should().Be(1); - enumerator2.State.Index.Should().Be(1); - - playoutItems.Count.Should().Be(6); - - playoutItems[0].MediaItemId.Should().Be(1); - playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); - playoutItems[0].GuideGroup.Should().Be(1); - playoutItems[0].FillerKind.Should().Be(FillerKind.None); - - playoutItems[1].MediaItemId.Should().Be(2); - playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55)); - playoutItems[1].GuideGroup.Should().Be(2); - playoutItems[1].FillerKind.Should().Be(FillerKind.None); - - playoutItems[2].MediaItemId.Should().Be(1); - playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0))); - playoutItems[2].GuideGroup.Should().Be(3); - playoutItems[2].FillerKind.Should().Be(FillerKind.None); - - playoutItems[3].MediaItemId.Should().Be(3); - playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0))); - playoutItems[3].GuideGroup.Should().Be(3); - playoutItems[3].FillerKind.Should().Be(FillerKind.Tail); - - playoutItems[4].MediaItemId.Should().Be(4); - playoutItems[4].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 49, 0))); - playoutItems[4].GuideGroup.Should().Be(3); - playoutItems[4].FillerKind.Should().Be(FillerKind.Tail); - - playoutItems[5].MediaItemId.Should().Be(3); - playoutItems[5].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 53, 0))); - playoutItems[5].GuideGroup.Should().Be(3); - playoutItems[5].FillerKind.Should().Be(FillerKind.Tail); - } - - [Test] - public void Should_Not_Have_Gap_With_Tail_And_Fallback() - { - Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55)); - Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(4)); - Collection collectionThree = TwoItemCollection(5, 6, TimeSpan.FromMinutes(1)); - - var scheduleItem = new ProgramScheduleItemMultiple - { - Id = 1, - Index = 1, - Collection = collectionOne, - CollectionId = collectionOne.Id, - StartTime = null, - PlaybackOrder = PlaybackOrder.Chronological, - TailFiller = new FillerPreset - { - FillerKind = FillerKind.Tail, - Collection = collectionTwo, - CollectionId = collectionTwo.Id - }, - FallbackFiller = new FillerPreset - { - FillerKind = FillerKind.Fallback, - Collection = collectionThree, - CollectionId = collectionThree.Id - }, - Count = 3 - }; - - var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( - new List { scheduleItem }, - new CollectionEnumeratorState()); - - var enumerator1 = new ChronologicalMediaCollectionEnumerator( - collectionOne.MediaItems, - new CollectionEnumeratorState()); - - var enumerator2 = new ChronologicalMediaCollectionEnumerator( - collectionTwo.MediaItems, - new CollectionEnumeratorState()); - - var enumerator3 = new ChronologicalMediaCollectionEnumerator( - collectionThree.MediaItems, - new CollectionEnumeratorState()); - - var collectionMediaItems = new Dictionary> - { - { CollectionKey.ForScheduleItem(scheduleItem), collectionOne.MediaItems }, - { CollectionKey.ForFillerPreset(scheduleItem.TailFiller), collectionTwo.MediaItems }, - { CollectionKey.ForFillerPreset(scheduleItem.FallbackFiller), collectionThree.MediaItems } - }.ToMap(); - - PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); - - var scheduler = new PlayoutModeSchedulerMultiple(collectionMediaItems, new Mock().Object); - (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( - startState, - CollectionEnumerators( - scheduleItem, - enumerator1, - scheduleItem.TailFiller, - enumerator2, - scheduleItem.FallbackFiller, - enumerator3), - scheduleItem, - NextScheduleItem, - HardStop(scheduleItemsEnumerator)); - - playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3)); - playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); - - playoutBuilderState.NextGuideGroup.Should().Be(4); - playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); - playoutBuilderState.InFlood.Should().BeFalse(); - playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); - playoutBuilderState.InDurationFiller.Should().BeFalse(); - playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); - - enumerator1.State.Index.Should().Be(1); - enumerator2.State.Index.Should().Be(1); - enumerator3.State.Index.Should().Be(1); - - playoutItems.Count.Should().Be(7); - - playoutItems[0].MediaItemId.Should().Be(1); - playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); - playoutItems[0].GuideGroup.Should().Be(1); - playoutItems[0].FillerKind.Should().Be(FillerKind.None); - - playoutItems[1].MediaItemId.Should().Be(2); - playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55)); - playoutItems[1].GuideGroup.Should().Be(2); - playoutItems[1].FillerKind.Should().Be(FillerKind.None); - - playoutItems[2].MediaItemId.Should().Be(1); - playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0))); - playoutItems[2].GuideGroup.Should().Be(3); - playoutItems[2].FillerKind.Should().Be(FillerKind.None); - - playoutItems[3].MediaItemId.Should().Be(3); - playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0))); - playoutItems[3].GuideGroup.Should().Be(3); - playoutItems[3].FillerKind.Should().Be(FillerKind.Tail); - - playoutItems[4].MediaItemId.Should().Be(4); - playoutItems[4].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 49, 0))); - playoutItems[4].GuideGroup.Should().Be(3); - playoutItems[4].FillerKind.Should().Be(FillerKind.Tail); - - playoutItems[5].MediaItemId.Should().Be(3); - playoutItems[5].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 53, 0))); - playoutItems[5].GuideGroup.Should().Be(3); - playoutItems[5].FillerKind.Should().Be(FillerKind.Tail); - - playoutItems[6].MediaItemId.Should().Be(5); - playoutItems[6].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 57, 0))); - playoutItems[6].GuideGroup.Should().Be(3); - playoutItems[6].FillerKind.Should().Be(FillerKind.Fallback); - } - - [Test] - public void Should_Not_Have_Gap_With_Unused_Tail_And_Unused_Fallback() - { - Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromHours(1)); - Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(4)); - Collection collectionThree = TwoItemCollection(5, 6, TimeSpan.FromMinutes(1)); - - var scheduleItem = new ProgramScheduleItemMultiple - { - Id = 1, - Index = 1, - Collection = collectionOne, - CollectionId = collectionOne.Id, - StartTime = null, - PlaybackOrder = PlaybackOrder.Chronological, - TailFiller = new FillerPreset - { - FillerKind = FillerKind.Tail, - Collection = collectionTwo, - CollectionId = collectionTwo.Id - }, - FallbackFiller = new FillerPreset - { - FillerKind = FillerKind.Fallback, - Collection = collectionThree, - CollectionId = collectionThree.Id - }, - Count = 3 - }; - - var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( - new List { scheduleItem }, - new CollectionEnumeratorState()); - - var enumerator1 = new ChronologicalMediaCollectionEnumerator( - collectionOne.MediaItems, - new CollectionEnumeratorState()); - - var enumerator2 = new ChronologicalMediaCollectionEnumerator( - collectionTwo.MediaItems, - new CollectionEnumeratorState()); - - var enumerator3 = new ChronologicalMediaCollectionEnumerator( - collectionThree.MediaItems, - new CollectionEnumeratorState()); - - var collectionMediaItems = new Dictionary> - { - { CollectionKey.ForScheduleItem(scheduleItem), collectionOne.MediaItems }, - { CollectionKey.ForFillerPreset(scheduleItem.TailFiller), collectionTwo.MediaItems }, - { CollectionKey.ForFillerPreset(scheduleItem.FallbackFiller), collectionThree.MediaItems } - }.ToMap(); - - PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); - - var scheduler = new PlayoutModeSchedulerMultiple(collectionMediaItems, new Mock().Object); - (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( - startState, - CollectionEnumerators( - scheduleItem, - enumerator1, - scheduleItem.TailFiller, - enumerator2, - scheduleItem.FallbackFiller, - enumerator3), - scheduleItem, - NextScheduleItem, - HardStop(scheduleItemsEnumerator)); - - playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3)); - playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); - - playoutBuilderState.NextGuideGroup.Should().Be(4); - playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); - playoutBuilderState.InFlood.Should().BeFalse(); - playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); - playoutBuilderState.InDurationFiller.Should().BeFalse(); - playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); - - enumerator1.State.Index.Should().Be(1); - enumerator2.State.Index.Should().Be(0); - enumerator3.State.Index.Should().Be(0); - - playoutItems.Count.Should().Be(3); - - playoutItems[0].MediaItemId.Should().Be(1); - playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); - playoutItems[0].GuideGroup.Should().Be(1); - playoutItems[0].FillerKind.Should().Be(FillerKind.None); - - playoutItems[1].MediaItemId.Should().Be(2); - playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddHours(1)); - playoutItems[1].GuideGroup.Should().Be(2); - playoutItems[1].FillerKind.Should().Be(FillerKind.None); - - playoutItems[2].MediaItemId.Should().Be(1); - playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.AddHours(2)); - playoutItems[2].GuideGroup.Should().Be(3); - playoutItems[2].FillerKind.Should().Be(FillerKind.None); - } - - protected override ProgramScheduleItem NextScheduleItem => new ProgramScheduleItemOne - { - StartTime = TimeSpan.FromHours(3) + Id = 1, + Index = 1, + CollectionType = ProgramScheduleItemCollectionType.Collection, + Collection = collectionOne, + CollectionId = collectionOne.Id, + StartTime = null, + PlaybackOrder = PlaybackOrder.Chronological, + TailFiller = null, + FallbackFiller = null, + Count = 3 }; + + var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( + new List { scheduleItem }, + new CollectionEnumeratorState()); + + var enumerator = new ChronologicalMediaCollectionEnumerator( + collectionOne.MediaItems, + new CollectionEnumeratorState()); + + var collectionMediaItems = new Dictionary> + { + { CollectionKey.ForScheduleItem(scheduleItem), collectionOne.MediaItems } + }.ToMap(); + + PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); + + var scheduler = new PlayoutModeSchedulerMultiple(collectionMediaItems, new Mock().Object); + (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( + startState, + CollectionEnumerators(scheduleItem, enumerator), + scheduleItem, + NextScheduleItem, + HardStop(scheduleItemsEnumerator)); + + playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3)); + playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); + + playoutBuilderState.NextGuideGroup.Should().Be(4); + playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); + playoutBuilderState.InFlood.Should().BeFalse(); + playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); + playoutBuilderState.InDurationFiller.Should().BeFalse(); + playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); + + enumerator.State.Index.Should().Be(1); + + playoutItems.Count.Should().Be(3); + + playoutItems[0].MediaItemId.Should().Be(1); + playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); + playoutItems[0].GuideGroup.Should().Be(1); + playoutItems[0].FillerKind.Should().Be(FillerKind.None); + + playoutItems[1].MediaItemId.Should().Be(2); + playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddHours(1)); + playoutItems[1].GuideGroup.Should().Be(2); + playoutItems[1].FillerKind.Should().Be(FillerKind.None); + + playoutItems[2].MediaItemId.Should().Be(1); + playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.AddHours(2)); + playoutItems[2].GuideGroup.Should().Be(3); + playoutItems[2].FillerKind.Should().Be(FillerKind.None); } -} + + [Test] + public void Should_Have_Gap_With_No_Tail_No_Fallback() + { + Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55)); + + var scheduleItem = new ProgramScheduleItemMultiple + { + Id = 1, + Index = 1, + Collection = collectionOne, + CollectionId = collectionOne.Id, + StartTime = null, + PlaybackOrder = PlaybackOrder.Chronological, + TailFiller = null, + FallbackFiller = null, + Count = 3 + }; + + var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( + new List { scheduleItem }, + new CollectionEnumeratorState()); + + var enumerator = new ChronologicalMediaCollectionEnumerator( + collectionOne.MediaItems, + new CollectionEnumeratorState()); + + var collectionMediaItems = new Dictionary> + { + { CollectionKey.ForScheduleItem(scheduleItem), collectionOne.MediaItems } + }.ToMap(); + + PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); + + var scheduler = new PlayoutModeSchedulerMultiple(collectionMediaItems, new Mock().Object); + (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( + startState, + CollectionEnumerators(scheduleItem, enumerator), + scheduleItem, + NextScheduleItem, + HardStop(scheduleItemsEnumerator)); + + playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0))); + playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); + + playoutBuilderState.NextGuideGroup.Should().Be(4); + playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); + playoutBuilderState.InFlood.Should().BeFalse(); + playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); + playoutBuilderState.InDurationFiller.Should().BeFalse(); + playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); + + enumerator.State.Index.Should().Be(1); + + playoutItems.Count.Should().Be(3); + + playoutItems[0].MediaItemId.Should().Be(1); + playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); + playoutItems[0].GuideGroup.Should().Be(1); + playoutItems[0].FillerKind.Should().Be(FillerKind.None); + + playoutItems[1].MediaItemId.Should().Be(2); + playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55)); + playoutItems[1].GuideGroup.Should().Be(2); + playoutItems[1].FillerKind.Should().Be(FillerKind.None); + + playoutItems[2].MediaItemId.Should().Be(1); + playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0))); + playoutItems[2].GuideGroup.Should().Be(3); + playoutItems[2].FillerKind.Should().Be(FillerKind.None); + } + + [Test] + public void Should_Not_Have_Gap_With_Exact_Tail() + { + Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55)); + Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(5)); + + var scheduleItem = new ProgramScheduleItemMultiple + { + Id = 1, + Index = 1, + Collection = collectionOne, + CollectionId = collectionOne.Id, + StartTime = null, + PlaybackOrder = PlaybackOrder.Chronological, + TailFiller = new FillerPreset + { + FillerKind = FillerKind.Tail, + Collection = collectionTwo, + CollectionId = collectionTwo.Id + }, + FallbackFiller = null, + Count = 3 + }; + + var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( + new List { scheduleItem }, + new CollectionEnumeratorState()); + + var enumerator1 = new ChronologicalMediaCollectionEnumerator( + collectionOne.MediaItems, + new CollectionEnumeratorState()); + + var enumerator2 = new ChronologicalMediaCollectionEnumerator( + collectionTwo.MediaItems, + new CollectionEnumeratorState()); + + var collectionMediaItems = new Dictionary> + { + { CollectionKey.ForScheduleItem(scheduleItem), collectionOne.MediaItems }, + { CollectionKey.ForFillerPreset(scheduleItem.TailFiller), collectionTwo.MediaItems } + }.ToMap(); + + PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); + + var scheduler = new PlayoutModeSchedulerMultiple(collectionMediaItems, new Mock().Object); + (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( + startState, + CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2), + scheduleItem, + NextScheduleItem, + HardStop(scheduleItemsEnumerator)); + + playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3)); + playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); + + playoutBuilderState.NextGuideGroup.Should().Be(4); + playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); + playoutBuilderState.InFlood.Should().BeFalse(); + playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); + playoutBuilderState.InDurationFiller.Should().BeFalse(); + playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); + + enumerator1.State.Index.Should().Be(1); + enumerator2.State.Index.Should().Be(1); + + playoutItems.Count.Should().Be(6); + + playoutItems[0].MediaItemId.Should().Be(1); + playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); + playoutItems[0].GuideGroup.Should().Be(1); + playoutItems[0].FillerKind.Should().Be(FillerKind.None); + + playoutItems[1].MediaItemId.Should().Be(2); + playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55)); + playoutItems[1].GuideGroup.Should().Be(2); + playoutItems[1].FillerKind.Should().Be(FillerKind.None); + + playoutItems[2].MediaItemId.Should().Be(1); + playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0))); + playoutItems[2].GuideGroup.Should().Be(3); + playoutItems[2].FillerKind.Should().Be(FillerKind.None); + + playoutItems[3].MediaItemId.Should().Be(3); + playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0))); + playoutItems[3].GuideGroup.Should().Be(3); + playoutItems[3].FillerKind.Should().Be(FillerKind.Tail); + + playoutItems[4].MediaItemId.Should().Be(4); + playoutItems[4].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 50, 0))); + playoutItems[4].GuideGroup.Should().Be(3); + playoutItems[4].FillerKind.Should().Be(FillerKind.Tail); + + playoutItems[5].MediaItemId.Should().Be(3); + playoutItems[5].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 55, 0))); + playoutItems[5].GuideGroup.Should().Be(3); + playoutItems[5].FillerKind.Should().Be(FillerKind.Tail); + } + + [Test] + public void Should_Not_Have_Gap_With_Fallback() + { + Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55)); + Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(5)); + + var scheduleItem = new ProgramScheduleItemMultiple + { + Id = 1, + Index = 1, + Collection = collectionOne, + CollectionId = collectionOne.Id, + StartTime = null, + PlaybackOrder = PlaybackOrder.Chronological, + TailFiller = null, + FallbackFiller = new FillerPreset + { + FillerKind = FillerKind.Tail, + Collection = collectionTwo, + CollectionId = collectionTwo.Id + }, + Count = 3 + }; + + var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( + new List { scheduleItem }, + new CollectionEnumeratorState()); + + var enumerator1 = new ChronologicalMediaCollectionEnumerator( + collectionOne.MediaItems, + new CollectionEnumeratorState()); + + var enumerator2 = new ChronologicalMediaCollectionEnumerator( + collectionTwo.MediaItems, + new CollectionEnumeratorState()); + + var collectionMediaItems = new Dictionary> + { + { CollectionKey.ForScheduleItem(scheduleItem), collectionOne.MediaItems }, + { CollectionKey.ForFillerPreset(scheduleItem.FallbackFiller), collectionTwo.MediaItems } + }.ToMap(); + + PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); + + var scheduler = new PlayoutModeSchedulerMultiple(collectionMediaItems, new Mock().Object); + (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( + startState, + CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.FallbackFiller, enumerator2), + scheduleItem, + NextScheduleItem, + HardStop(scheduleItemsEnumerator)); + + playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3)); + playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); + + playoutBuilderState.NextGuideGroup.Should().Be(4); + playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); + playoutBuilderState.InFlood.Should().BeFalse(); + playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); + playoutBuilderState.InDurationFiller.Should().BeFalse(); + playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); + + enumerator1.State.Index.Should().Be(1); + enumerator2.State.Index.Should().Be(1); + + playoutItems.Count.Should().Be(4); + + playoutItems[0].MediaItemId.Should().Be(1); + playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); + playoutItems[0].GuideGroup.Should().Be(1); + playoutItems[0].FillerKind.Should().Be(FillerKind.None); + + playoutItems[1].MediaItemId.Should().Be(2); + playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55)); + playoutItems[1].GuideGroup.Should().Be(2); + playoutItems[1].FillerKind.Should().Be(FillerKind.None); + + playoutItems[2].MediaItemId.Should().Be(1); + playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0))); + playoutItems[2].GuideGroup.Should().Be(3); + playoutItems[2].FillerKind.Should().Be(FillerKind.None); + + playoutItems[3].MediaItemId.Should().Be(3); + playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0))); + playoutItems[3].GuideGroup.Should().Be(3); + playoutItems[3].FillerKind.Should().Be(FillerKind.Fallback); + } + + [Test] + public void Should_Have_Gap_With_Tail_No_Fallback() + { + Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55)); + Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(4)); + + var scheduleItem = new ProgramScheduleItemMultiple + { + Id = 1, + Index = 1, + Collection = collectionOne, + CollectionId = collectionOne.Id, + StartTime = null, + PlaybackOrder = PlaybackOrder.Chronological, + TailFiller = new FillerPreset + { + FillerKind = FillerKind.Tail, + Collection = collectionTwo, + CollectionId = collectionTwo.Id + }, + FallbackFiller = null, + Count = 3 + }; + + var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( + new List { scheduleItem }, + new CollectionEnumeratorState()); + + var enumerator1 = new ChronologicalMediaCollectionEnumerator( + collectionOne.MediaItems, + new CollectionEnumeratorState()); + + var enumerator2 = new ChronologicalMediaCollectionEnumerator( + collectionTwo.MediaItems, + new CollectionEnumeratorState()); + + var collectionMediaItems = new Dictionary> + { + { CollectionKey.ForScheduleItem(scheduleItem), collectionOne.MediaItems }, + { CollectionKey.ForFillerPreset(scheduleItem.TailFiller), collectionTwo.MediaItems } + }.ToMap(); + + PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); + + var scheduler = new PlayoutModeSchedulerMultiple(collectionMediaItems, new Mock().Object); + (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( + startState, + CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2), + scheduleItem, + NextScheduleItem, + HardStop(scheduleItemsEnumerator)); + + playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 57, 0))); + playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); + + playoutBuilderState.NextGuideGroup.Should().Be(4); + playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); + playoutBuilderState.InFlood.Should().BeFalse(); + playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); + playoutBuilderState.InDurationFiller.Should().BeFalse(); + playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); + + enumerator1.State.Index.Should().Be(1); + enumerator2.State.Index.Should().Be(1); + + playoutItems.Count.Should().Be(6); + + playoutItems[0].MediaItemId.Should().Be(1); + playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); + playoutItems[0].GuideGroup.Should().Be(1); + playoutItems[0].FillerKind.Should().Be(FillerKind.None); + + playoutItems[1].MediaItemId.Should().Be(2); + playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55)); + playoutItems[1].GuideGroup.Should().Be(2); + playoutItems[1].FillerKind.Should().Be(FillerKind.None); + + playoutItems[2].MediaItemId.Should().Be(1); + playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0))); + playoutItems[2].GuideGroup.Should().Be(3); + playoutItems[2].FillerKind.Should().Be(FillerKind.None); + + playoutItems[3].MediaItemId.Should().Be(3); + playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0))); + playoutItems[3].GuideGroup.Should().Be(3); + playoutItems[3].FillerKind.Should().Be(FillerKind.Tail); + + playoutItems[4].MediaItemId.Should().Be(4); + playoutItems[4].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 49, 0))); + playoutItems[4].GuideGroup.Should().Be(3); + playoutItems[4].FillerKind.Should().Be(FillerKind.Tail); + + playoutItems[5].MediaItemId.Should().Be(3); + playoutItems[5].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 53, 0))); + playoutItems[5].GuideGroup.Should().Be(3); + playoutItems[5].FillerKind.Should().Be(FillerKind.Tail); + } + + [Test] + public void Should_Not_Have_Gap_With_Tail_And_Fallback() + { + Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromMinutes(55)); + Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(4)); + Collection collectionThree = TwoItemCollection(5, 6, TimeSpan.FromMinutes(1)); + + var scheduleItem = new ProgramScheduleItemMultiple + { + Id = 1, + Index = 1, + Collection = collectionOne, + CollectionId = collectionOne.Id, + StartTime = null, + PlaybackOrder = PlaybackOrder.Chronological, + TailFiller = new FillerPreset + { + FillerKind = FillerKind.Tail, + Collection = collectionTwo, + CollectionId = collectionTwo.Id + }, + FallbackFiller = new FillerPreset + { + FillerKind = FillerKind.Fallback, + Collection = collectionThree, + CollectionId = collectionThree.Id + }, + Count = 3 + }; + + var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( + new List { scheduleItem }, + new CollectionEnumeratorState()); + + var enumerator1 = new ChronologicalMediaCollectionEnumerator( + collectionOne.MediaItems, + new CollectionEnumeratorState()); + + var enumerator2 = new ChronologicalMediaCollectionEnumerator( + collectionTwo.MediaItems, + new CollectionEnumeratorState()); + + var enumerator3 = new ChronologicalMediaCollectionEnumerator( + collectionThree.MediaItems, + new CollectionEnumeratorState()); + + var collectionMediaItems = new Dictionary> + { + { CollectionKey.ForScheduleItem(scheduleItem), collectionOne.MediaItems }, + { CollectionKey.ForFillerPreset(scheduleItem.TailFiller), collectionTwo.MediaItems }, + { CollectionKey.ForFillerPreset(scheduleItem.FallbackFiller), collectionThree.MediaItems } + }.ToMap(); + + PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); + + var scheduler = new PlayoutModeSchedulerMultiple(collectionMediaItems, new Mock().Object); + (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( + startState, + CollectionEnumerators( + scheduleItem, + enumerator1, + scheduleItem.TailFiller, + enumerator2, + scheduleItem.FallbackFiller, + enumerator3), + scheduleItem, + NextScheduleItem, + HardStop(scheduleItemsEnumerator)); + + playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3)); + playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); + + playoutBuilderState.NextGuideGroup.Should().Be(4); + playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); + playoutBuilderState.InFlood.Should().BeFalse(); + playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); + playoutBuilderState.InDurationFiller.Should().BeFalse(); + playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); + + enumerator1.State.Index.Should().Be(1); + enumerator2.State.Index.Should().Be(1); + enumerator3.State.Index.Should().Be(1); + + playoutItems.Count.Should().Be(7); + + playoutItems[0].MediaItemId.Should().Be(1); + playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); + playoutItems[0].GuideGroup.Should().Be(1); + playoutItems[0].FillerKind.Should().Be(FillerKind.None); + + playoutItems[1].MediaItemId.Should().Be(2); + playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55)); + playoutItems[1].GuideGroup.Should().Be(2); + playoutItems[1].FillerKind.Should().Be(FillerKind.None); + + playoutItems[2].MediaItemId.Should().Be(1); + playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0))); + playoutItems[2].GuideGroup.Should().Be(3); + playoutItems[2].FillerKind.Should().Be(FillerKind.None); + + playoutItems[3].MediaItemId.Should().Be(3); + playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0))); + playoutItems[3].GuideGroup.Should().Be(3); + playoutItems[3].FillerKind.Should().Be(FillerKind.Tail); + + playoutItems[4].MediaItemId.Should().Be(4); + playoutItems[4].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 49, 0))); + playoutItems[4].GuideGroup.Should().Be(3); + playoutItems[4].FillerKind.Should().Be(FillerKind.Tail); + + playoutItems[5].MediaItemId.Should().Be(3); + playoutItems[5].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 53, 0))); + playoutItems[5].GuideGroup.Should().Be(3); + playoutItems[5].FillerKind.Should().Be(FillerKind.Tail); + + playoutItems[6].MediaItemId.Should().Be(5); + playoutItems[6].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 57, 0))); + playoutItems[6].GuideGroup.Should().Be(3); + playoutItems[6].FillerKind.Should().Be(FillerKind.Fallback); + } + + [Test] + public void Should_Not_Have_Gap_With_Unused_Tail_And_Unused_Fallback() + { + Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromHours(1)); + Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(4)); + Collection collectionThree = TwoItemCollection(5, 6, TimeSpan.FromMinutes(1)); + + var scheduleItem = new ProgramScheduleItemMultiple + { + Id = 1, + Index = 1, + Collection = collectionOne, + CollectionId = collectionOne.Id, + StartTime = null, + PlaybackOrder = PlaybackOrder.Chronological, + TailFiller = new FillerPreset + { + FillerKind = FillerKind.Tail, + Collection = collectionTwo, + CollectionId = collectionTwo.Id + }, + FallbackFiller = new FillerPreset + { + FillerKind = FillerKind.Fallback, + Collection = collectionThree, + CollectionId = collectionThree.Id + }, + Count = 3 + }; + + var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( + new List { scheduleItem }, + new CollectionEnumeratorState()); + + var enumerator1 = new ChronologicalMediaCollectionEnumerator( + collectionOne.MediaItems, + new CollectionEnumeratorState()); + + var enumerator2 = new ChronologicalMediaCollectionEnumerator( + collectionTwo.MediaItems, + new CollectionEnumeratorState()); + + var enumerator3 = new ChronologicalMediaCollectionEnumerator( + collectionThree.MediaItems, + new CollectionEnumeratorState()); + + var collectionMediaItems = new Dictionary> + { + { CollectionKey.ForScheduleItem(scheduleItem), collectionOne.MediaItems }, + { CollectionKey.ForFillerPreset(scheduleItem.TailFiller), collectionTwo.MediaItems }, + { CollectionKey.ForFillerPreset(scheduleItem.FallbackFiller), collectionThree.MediaItems } + }.ToMap(); + + PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); + + var scheduler = new PlayoutModeSchedulerMultiple(collectionMediaItems, new Mock().Object); + (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( + startState, + CollectionEnumerators( + scheduleItem, + enumerator1, + scheduleItem.TailFiller, + enumerator2, + scheduleItem.FallbackFiller, + enumerator3), + scheduleItem, + NextScheduleItem, + HardStop(scheduleItemsEnumerator)); + + playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3)); + playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); + + playoutBuilderState.NextGuideGroup.Should().Be(4); + playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); + playoutBuilderState.InFlood.Should().BeFalse(); + playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); + playoutBuilderState.InDurationFiller.Should().BeFalse(); + playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); + + enumerator1.State.Index.Should().Be(1); + enumerator2.State.Index.Should().Be(0); + enumerator3.State.Index.Should().Be(0); + + playoutItems.Count.Should().Be(3); + + playoutItems[0].MediaItemId.Should().Be(1); + playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); + playoutItems[0].GuideGroup.Should().Be(1); + playoutItems[0].FillerKind.Should().Be(FillerKind.None); + + playoutItems[1].MediaItemId.Should().Be(2); + playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddHours(1)); + playoutItems[1].GuideGroup.Should().Be(2); + playoutItems[1].FillerKind.Should().Be(FillerKind.None); + + playoutItems[2].MediaItemId.Should().Be(1); + playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.AddHours(2)); + playoutItems[2].GuideGroup.Should().Be(3); + playoutItems[2].FillerKind.Should().Be(FillerKind.None); + } + + protected override ProgramScheduleItem NextScheduleItem => new ProgramScheduleItemOne + { + StartTime = TimeSpan.FromHours(3) + }; +} \ No newline at end of file diff --git a/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerOneTests.cs b/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerOneTests.cs index 13d5836b2..ab11ed92f 100644 --- a/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerOneTests.cs +++ b/ErsatzTV.Core.Tests/Scheduling/PlayoutModeSchedulerOneTests.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Filler; using ErsatzTV.Core.Scheduling; using FluentAssertions; @@ -9,752 +6,751 @@ using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; -namespace ErsatzTV.Core.Tests.Scheduling +namespace ErsatzTV.Core.Tests.Scheduling; + +[TestFixture] +public class PlayoutModeSchedulerOneTests : SchedulerTestBase { - [TestFixture] - public class PlayoutModeSchedulerOneTests : SchedulerTestBase + [Test] + public void Should_Have_Gap_With_No_Tail_No_Fallback() { - [Test] - public void Should_Have_Gap_With_No_Tail_No_Fallback() + Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromHours(1)); + + var scheduleItem = new ProgramScheduleItemOne { - Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromHours(1)); - - var scheduleItem = new ProgramScheduleItemOne - { - Id = 1, - Index = 1, - Collection = collectionOne, - CollectionId = collectionOne.Id, - StartTime = null, - PlaybackOrder = PlaybackOrder.Chronological, - TailFiller = null, - FallbackFiller = null - }; - - var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( - new List { scheduleItem }, - new CollectionEnumeratorState()); - - var enumerator = new ChronologicalMediaCollectionEnumerator( - collectionOne.MediaItems, - new CollectionEnumeratorState()); - - PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); - - var scheduler = new PlayoutModeSchedulerOne(new Mock().Object); - (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( - startState, - CollectionEnumerators(scheduleItem, enumerator), - scheduleItem, - NextScheduleItem, - HardStop(scheduleItemsEnumerator)); - - playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(1)); - playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); - - playoutBuilderState.NextGuideGroup.Should().Be(2); - playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); - playoutBuilderState.InFlood.Should().BeFalse(); - playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); - playoutBuilderState.InDurationFiller.Should().BeFalse(); - playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); - - enumerator.State.Index.Should().Be(1); - - playoutItems.Count.Should().Be(1); - - playoutItems[0].MediaItemId.Should().Be(1); - playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); - playoutItems[0].GuideGroup.Should().Be(1); - playoutItems[0].FillerKind.Should().Be(FillerKind.None); - } - - [Test] - public void Should_Have_Gap_With_Empty_Tail_Empty_Fallback() - { - Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromHours(1)); - var collectionTwo = new Collection { Id = 2, Name = "Collection 2", MediaItems = new List() }; - var collectionThree = new Collection { Id = 3, Name = "Collection 3", MediaItems = new List() }; - - var scheduleItem = new ProgramScheduleItemOne - { - Id = 1, - Index = 1, - Collection = collectionOne, - CollectionId = collectionOne.Id, - StartTime = null, - PlaybackOrder = PlaybackOrder.Chronological, - TailFiller = new FillerPreset - { - FillerKind = FillerKind.Tail, - CollectionId = collectionTwo.Id, - Collection = collectionTwo - }, - FallbackFiller = new FillerPreset - { - FillerKind = FillerKind.Fallback, - CollectionId = collectionThree.Id, - Collection = collectionThree - } - }; - - var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( - new List { scheduleItem }, - new CollectionEnumeratorState()); - - var enumerator1 = new ChronologicalMediaCollectionEnumerator( - collectionOne.MediaItems, - new CollectionEnumeratorState()); - - var enumerator2 = new ChronologicalMediaCollectionEnumerator( - collectionTwo.MediaItems, - new CollectionEnumeratorState()); - - var enumerator3 = new ChronologicalMediaCollectionEnumerator( - collectionThree.MediaItems, - new CollectionEnumeratorState()); - - PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); - - var scheduler = new PlayoutModeSchedulerOne(new Mock().Object); - (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( - startState, - CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2, scheduleItem.FallbackFiller, enumerator3), - scheduleItem, - NextScheduleItem, - HardStop(scheduleItemsEnumerator)); - - playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(1)); - playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); - - playoutBuilderState.NextGuideGroup.Should().Be(2); - playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); - playoutBuilderState.InFlood.Should().BeFalse(); - playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); - playoutBuilderState.InDurationFiller.Should().BeFalse(); - playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); - - enumerator1.State.Index.Should().Be(1); - enumerator2.State.Index.Should().Be(0); - enumerator3.State.Index.Should().Be(0); - - playoutItems.Count.Should().Be(1); - - playoutItems[0].MediaItemId.Should().Be(1); - playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); - playoutItems[0].GuideGroup.Should().Be(1); - playoutItems[0].FillerKind.Should().Be(FillerKind.None); - } - - [Test] - public void Should_Not_Have_Gap_With_Exact_Tail() - { - Collection collectionOne = TwoItemCollection(1, 2, new TimeSpan(2, 45, 0)); - Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(5)); - - var scheduleItem = new ProgramScheduleItemOne - { - Id = 1, - Index = 1, - Collection = collectionOne, - CollectionId = collectionOne.Id, - StartTime = null, - PlaybackOrder = PlaybackOrder.Chronological, - TailFiller = new FillerPreset - { - FillerKind = FillerKind.Tail, - Collection = collectionTwo, - CollectionId = collectionTwo.Id - }, - FallbackFiller = null - }; - - var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( - new List { scheduleItem }, - new CollectionEnumeratorState()); - - var enumerator1 = new ChronologicalMediaCollectionEnumerator( - collectionOne.MediaItems, - new CollectionEnumeratorState()); - - var enumerator2 = new ChronologicalMediaCollectionEnumerator( - collectionTwo.MediaItems, - new CollectionEnumeratorState()); - - PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); - - var scheduler = new PlayoutModeSchedulerOne(new Mock().Object); - (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( - startState, - CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2), - scheduleItem, - NextScheduleItem, - HardStop(scheduleItemsEnumerator)); - - playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3)); - playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); - - playoutBuilderState.NextGuideGroup.Should().Be(2); - playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); - playoutBuilderState.InFlood.Should().BeFalse(); - playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); - playoutBuilderState.InDurationFiller.Should().BeFalse(); - playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); - - enumerator1.State.Index.Should().Be(1); - enumerator2.State.Index.Should().Be(1); - - playoutItems.Count.Should().Be(4); - - playoutItems[0].MediaItemId.Should().Be(1); - playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); - playoutItems[0].GuideGroup.Should().Be(1); - playoutItems[0].FillerKind.Should().Be(FillerKind.None); - - playoutItems[1].MediaItemId.Should().Be(3); - playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0))); - playoutItems[1].GuideGroup.Should().Be(1); - playoutItems[1].FillerKind.Should().Be(FillerKind.Tail); - - playoutItems[2].MediaItemId.Should().Be(4); - playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 50, 0))); - playoutItems[2].GuideGroup.Should().Be(1); - playoutItems[2].FillerKind.Should().Be(FillerKind.Tail); - - playoutItems[3].MediaItemId.Should().Be(3); - playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 55, 0))); - playoutItems[3].GuideGroup.Should().Be(1); - playoutItems[3].FillerKind.Should().Be(FillerKind.Tail); - } - - [Test] - public void Should_Not_Have_Gap_With_Fallback() - { - Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromHours(1)); - Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(5)); - - var scheduleItem = new ProgramScheduleItemOne - { - Id = 1, - Index = 1, - Collection = collectionOne, - CollectionId = collectionOne.Id, - StartTime = null, - PlaybackOrder = PlaybackOrder.Chronological, - TailFiller = null, - FallbackFiller = new FillerPreset - { - FillerKind = FillerKind.Tail, - Collection = collectionTwo, - CollectionId = collectionTwo.Id - } - }; - - var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( - new List { scheduleItem }, - new CollectionEnumeratorState()); - - var enumerator1 = new ChronologicalMediaCollectionEnumerator( - collectionOne.MediaItems, - new CollectionEnumeratorState()); - - var enumerator2 = new ChronologicalMediaCollectionEnumerator( - collectionTwo.MediaItems, - new CollectionEnumeratorState()); - - PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); - - var scheduler = new PlayoutModeSchedulerOne(new Mock().Object); - (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( - startState, - CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.FallbackFiller, enumerator2), - scheduleItem, - NextScheduleItem, - HardStop(scheduleItemsEnumerator)); - - playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3)); - playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); - - playoutBuilderState.NextGuideGroup.Should().Be(2); - playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); - playoutBuilderState.InFlood.Should().BeFalse(); - playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); - playoutBuilderState.InDurationFiller.Should().BeFalse(); - playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); - - enumerator1.State.Index.Should().Be(1); - enumerator2.State.Index.Should().Be(1); - - playoutItems.Count.Should().Be(2); - - playoutItems[0].MediaItemId.Should().Be(1); - playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); - playoutItems[0].GuideGroup.Should().Be(1); - playoutItems[0].FillerKind.Should().Be(FillerKind.None); - - playoutItems[1].MediaItemId.Should().Be(3); - playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddHours(1)); - playoutItems[1].GuideGroup.Should().Be(1); - playoutItems[1].FillerKind.Should().Be(FillerKind.Fallback); - } - - [Test] - public void Should_Have_Gap_With_Tail_No_Fallback() - { - Collection collectionOne = TwoItemCollection(1, 2, new TimeSpan(2, 45, 0)); - Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(4)); - - var scheduleItem = new ProgramScheduleItemOne - { - Id = 1, - Index = 1, - Collection = collectionOne, - CollectionId = collectionOne.Id, - StartTime = null, - PlaybackOrder = PlaybackOrder.Chronological, - TailFiller = new FillerPreset - { - FillerKind = FillerKind.Tail, - Collection = collectionTwo, - CollectionId = collectionTwo.Id - }, - FallbackFiller = null - }; - - var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( - new List { scheduleItem }, - new CollectionEnumeratorState()); - - var enumerator1 = new ChronologicalMediaCollectionEnumerator( - collectionOne.MediaItems, - new CollectionEnumeratorState()); - - var enumerator2 = new ChronologicalMediaCollectionEnumerator( - collectionTwo.MediaItems, - new CollectionEnumeratorState()); - - PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); - - var scheduler = new PlayoutModeSchedulerOne(new Mock().Object); - (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( - startState, - CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2), - scheduleItem, - NextScheduleItem, - HardStop(scheduleItemsEnumerator)); - - playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 57, 0))); - playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); - - playoutBuilderState.NextGuideGroup.Should().Be(2); - playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); - playoutBuilderState.InFlood.Should().BeFalse(); - playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); - playoutBuilderState.InDurationFiller.Should().BeFalse(); - playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); - - enumerator1.State.Index.Should().Be(1); - enumerator2.State.Index.Should().Be(1); - - playoutItems.Count.Should().Be(4); - - playoutItems[0].MediaItemId.Should().Be(1); - playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); - playoutItems[0].GuideGroup.Should().Be(1); - playoutItems[0].FillerKind.Should().Be(FillerKind.None); - - playoutItems[1].MediaItemId.Should().Be(3); - playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0))); - playoutItems[1].GuideGroup.Should().Be(1); - playoutItems[1].FillerKind.Should().Be(FillerKind.Tail); - - playoutItems[2].MediaItemId.Should().Be(4); - playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 49, 0))); - playoutItems[2].GuideGroup.Should().Be(1); - playoutItems[2].FillerKind.Should().Be(FillerKind.Tail); - - playoutItems[3].MediaItemId.Should().Be(3); - playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 53, 0))); - playoutItems[3].GuideGroup.Should().Be(1); - playoutItems[3].FillerKind.Should().Be(FillerKind.Tail); - } - - [Test] - public void Should_Not_Have_Gap_With_Tail_And_Fallback() - { - Collection collectionOne = TwoItemCollection(1, 2, new TimeSpan(2, 45, 0)); - Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(4)); - Collection collectionThree = TwoItemCollection(5, 6, TimeSpan.FromMinutes(1)); - - var scheduleItem = new ProgramScheduleItemOne - { - Id = 1, - Index = 1, - Collection = collectionOne, - CollectionId = collectionOne.Id, - StartTime = null, - PlaybackOrder = PlaybackOrder.Chronological, - TailFiller = new FillerPreset - { - FillerKind = FillerKind.Tail, - Collection = collectionTwo, - CollectionId = collectionTwo.Id - }, - FallbackFiller = new FillerPreset - { - FillerKind = FillerKind.Fallback, - Collection = collectionThree, - CollectionId = collectionThree.Id - } - }; - - var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( - new List { scheduleItem }, - new CollectionEnumeratorState()); - - var enumerator1 = new ChronologicalMediaCollectionEnumerator( - collectionOne.MediaItems, - new CollectionEnumeratorState()); - - var enumerator2 = new ChronologicalMediaCollectionEnumerator( - collectionTwo.MediaItems, - new CollectionEnumeratorState()); - - var enumerator3 = new ChronologicalMediaCollectionEnumerator( - collectionThree.MediaItems, - new CollectionEnumeratorState()); - - PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); - - var scheduler = new PlayoutModeSchedulerOne(new Mock().Object); - (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( - startState, - CollectionEnumerators( - scheduleItem, - enumerator1, - scheduleItem.TailFiller, - enumerator2, - scheduleItem.FallbackFiller, - enumerator3), - scheduleItem, - NextScheduleItem, - HardStop(scheduleItemsEnumerator)); - - playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3)); - playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); - - playoutBuilderState.NextGuideGroup.Should().Be(2); - playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); - playoutBuilderState.InFlood.Should().BeFalse(); - playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); - playoutBuilderState.InDurationFiller.Should().BeFalse(); - playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); - - enumerator1.State.Index.Should().Be(1); - enumerator2.State.Index.Should().Be(1); - enumerator3.State.Index.Should().Be(1); - - playoutItems.Count.Should().Be(5); - - playoutItems[0].MediaItemId.Should().Be(1); - playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); - playoutItems[0].GuideGroup.Should().Be(1); - playoutItems[0].FillerKind.Should().Be(FillerKind.None); - - playoutItems[1].MediaItemId.Should().Be(3); - playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0))); - playoutItems[1].GuideGroup.Should().Be(1); - playoutItems[1].FillerKind.Should().Be(FillerKind.Tail); - - playoutItems[2].MediaItemId.Should().Be(4); - playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 49, 0))); - playoutItems[2].GuideGroup.Should().Be(1); - playoutItems[2].FillerKind.Should().Be(FillerKind.Tail); - - playoutItems[3].MediaItemId.Should().Be(3); - playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 53, 0))); - playoutItems[3].GuideGroup.Should().Be(1); - playoutItems[3].FillerKind.Should().Be(FillerKind.Tail); - - playoutItems[4].MediaItemId.Should().Be(5); - playoutItems[4].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 57, 0))); - playoutItems[4].GuideGroup.Should().Be(1); - playoutItems[4].FillerKind.Should().Be(FillerKind.Fallback); - } - - [Test] - public void Should_Not_Have_Gap_With_Unused_Tail_And_Unused_Fallback() - { - Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromHours(3)); - Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(4)); - Collection collectionThree = TwoItemCollection(5, 6, TimeSpan.FromMinutes(1)); - - var scheduleItem = new ProgramScheduleItemOne - { - Id = 1, - Index = 1, - Collection = collectionOne, - CollectionId = collectionOne.Id, - StartTime = null, - PlaybackOrder = PlaybackOrder.Chronological, - TailFiller = new FillerPreset - { - FillerKind = FillerKind.Tail, - Collection = collectionTwo, - CollectionId = collectionTwo.Id - }, - FallbackFiller = new FillerPreset - { - FillerKind = FillerKind.Fallback, - Collection = collectionThree, - CollectionId = collectionThree.Id - } - }; - - var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( - new List { scheduleItem }, - new CollectionEnumeratorState()); - - var enumerator1 = new ChronologicalMediaCollectionEnumerator( - collectionOne.MediaItems, - new CollectionEnumeratorState()); - - var enumerator2 = new ChronologicalMediaCollectionEnumerator( - collectionTwo.MediaItems, - new CollectionEnumeratorState()); - - var enumerator3 = new ChronologicalMediaCollectionEnumerator( - collectionThree.MediaItems, - new CollectionEnumeratorState()); - - PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); - - var scheduler = new PlayoutModeSchedulerOne(new Mock().Object); - (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( - startState, - CollectionEnumerators( - scheduleItem, - enumerator1, - scheduleItem.TailFiller, - enumerator2, - scheduleItem.FallbackFiller, - enumerator3), - scheduleItem, - NextScheduleItem, - HardStop(scheduleItemsEnumerator)); - - playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3)); - playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); - - playoutBuilderState.NextGuideGroup.Should().Be(2); - playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); - playoutBuilderState.InFlood.Should().BeFalse(); - playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); - playoutBuilderState.InDurationFiller.Should().BeFalse(); - playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); - - enumerator1.State.Index.Should().Be(1); - enumerator2.State.Index.Should().Be(0); - enumerator3.State.Index.Should().Be(0); - - playoutItems.Count.Should().Be(1); - - playoutItems[0].MediaItemId.Should().Be(1); - playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); - playoutItems[0].GuideGroup.Should().Be(1); - playoutItems[0].FillerKind.Should().Be(FillerKind.None); - } - - [Test] - public void Should_Have_No_Gap_With_Exact_Post_Roll_Pad() - { - Collection collectionOne = TwoItemCollection(1, 2, new TimeSpan(2, 45, 0)); - Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(5)); - Collection collectionThree = TwoItemCollection(5, 6, TimeSpan.FromMinutes(1)); - - var scheduleItem = new ProgramScheduleItemOne - { - Id = 1, - Index = 1, - Collection = collectionOne, - CollectionId = collectionOne.Id, - StartTime = null, - PlaybackOrder = PlaybackOrder.Chronological, - PostRollFiller = new FillerPreset - { - FillerKind = FillerKind.PostRoll, - FillerMode = FillerMode.Pad, - PadToNearestMinute = 30, - Collection = collectionTwo, - CollectionId = collectionTwo.Id - }, - FallbackFiller = new FillerPreset - { - FillerKind = FillerKind.Fallback, - Collection = collectionThree, - CollectionId = collectionThree.Id - } - }; - - var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( - new List { scheduleItem }, - new CollectionEnumeratorState()); - - var enumerator1 = new ChronologicalMediaCollectionEnumerator( - collectionOne.MediaItems, - new CollectionEnumeratorState()); - - var enumerator2 = new ChronologicalMediaCollectionEnumerator( - collectionTwo.MediaItems, - new CollectionEnumeratorState()); - - var enumerator3 = new ChronologicalMediaCollectionEnumerator( - collectionThree.MediaItems, - new CollectionEnumeratorState()); - - PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); - - var scheduler = new PlayoutModeSchedulerOne(new Mock().Object); - (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( - startState, - CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.PostRollFiller, enumerator2, scheduleItem.FallbackFiller, enumerator3), - scheduleItem, - NextScheduleItem, - HardStop(scheduleItemsEnumerator)); - - playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3)); - playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); - - playoutBuilderState.NextGuideGroup.Should().Be(2); - playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); - playoutBuilderState.InFlood.Should().BeFalse(); - playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); - playoutBuilderState.InDurationFiller.Should().BeFalse(); - playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); - - enumerator1.State.Index.Should().Be(1); - enumerator2.State.Index.Should().Be(1); - - playoutItems.Count.Should().Be(4); - - playoutItems[0].MediaItemId.Should().Be(1); - playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); - playoutItems[0].GuideGroup.Should().Be(1); - playoutItems[0].FillerKind.Should().Be(FillerKind.None); - - playoutItems[1].MediaItemId.Should().Be(3); - playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0))); - playoutItems[1].GuideGroup.Should().Be(1); - playoutItems[1].FillerKind.Should().Be(FillerKind.PostRoll); - - playoutItems[2].MediaItemId.Should().Be(4); - playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 50, 0))); - playoutItems[2].GuideGroup.Should().Be(1); - playoutItems[2].FillerKind.Should().Be(FillerKind.PostRoll); - - playoutItems[3].MediaItemId.Should().Be(3); - playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 55, 0))); - playoutItems[3].GuideGroup.Should().Be(1); - playoutItems[3].FillerKind.Should().Be(FillerKind.PostRoll); - } - - [Test] - public void Should_Have_No_Gap_With_Exact_Post_Roll_Pad_With_Chapters() - { - Collection collectionOne = TwoItemCollection(1, 2, new TimeSpan(2, 45, 0), 2); - Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(5)); - Collection collectionThree = TwoItemCollection(5, 6, TimeSpan.FromMinutes(1)); - - var scheduleItem = new ProgramScheduleItemOne - { - Id = 1, - Index = 1, - Collection = collectionOne, - CollectionId = collectionOne.Id, - StartTime = null, - PlaybackOrder = PlaybackOrder.Chronological, - PostRollFiller = new FillerPreset - { - FillerKind = FillerKind.PostRoll, - FillerMode = FillerMode.Pad, - PadToNearestMinute = 30, - Collection = collectionTwo, - CollectionId = collectionTwo.Id - }, - FallbackFiller = new FillerPreset - { - FillerKind = FillerKind.Fallback, - Collection = collectionThree, - CollectionId = collectionThree.Id - } - }; - - var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( - new List { scheduleItem }, - new CollectionEnumeratorState()); - - var enumerator1 = new ChronologicalMediaCollectionEnumerator( - collectionOne.MediaItems, - new CollectionEnumeratorState()); - - var enumerator2 = new ChronologicalMediaCollectionEnumerator( - collectionTwo.MediaItems, - new CollectionEnumeratorState()); - - var enumerator3 = new ChronologicalMediaCollectionEnumerator( - collectionThree.MediaItems, - new CollectionEnumeratorState()); - - PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); - - var scheduler = new PlayoutModeSchedulerOne(new Mock().Object); - (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( - startState, - CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.PostRollFiller, enumerator2, scheduleItem.FallbackFiller, enumerator3), - scheduleItem, - NextScheduleItem, - HardStop(scheduleItemsEnumerator)); - - playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3)); - playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); - - playoutBuilderState.NextGuideGroup.Should().Be(2); - playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); - playoutBuilderState.InFlood.Should().BeFalse(); - playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); - playoutBuilderState.InDurationFiller.Should().BeFalse(); - playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); - - enumerator1.State.Index.Should().Be(1); - enumerator2.State.Index.Should().Be(1); - enumerator3.State.Index.Should().Be(0); - - playoutItems.Count.Should().Be(4); - - playoutItems[0].MediaItemId.Should().Be(1); - playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); - playoutItems[0].GuideGroup.Should().Be(1); - playoutItems[0].FillerKind.Should().Be(FillerKind.None); - - playoutItems[1].MediaItemId.Should().Be(3); - playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0))); - playoutItems[1].GuideGroup.Should().Be(1); - playoutItems[1].FillerKind.Should().Be(FillerKind.PostRoll); - - playoutItems[2].MediaItemId.Should().Be(4); - playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 50, 0))); - playoutItems[2].GuideGroup.Should().Be(1); - playoutItems[2].FillerKind.Should().Be(FillerKind.PostRoll); - - playoutItems[3].MediaItemId.Should().Be(3); - playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 55, 0))); - playoutItems[3].GuideGroup.Should().Be(1); - playoutItems[3].FillerKind.Should().Be(FillerKind.PostRoll); - } - - protected override ProgramScheduleItem NextScheduleItem => new ProgramScheduleItemOne - { - StartTime = TimeSpan.FromHours(3) + Id = 1, + Index = 1, + Collection = collectionOne, + CollectionId = collectionOne.Id, + StartTime = null, + PlaybackOrder = PlaybackOrder.Chronological, + TailFiller = null, + FallbackFiller = null }; + + var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( + new List { scheduleItem }, + new CollectionEnumeratorState()); + + var enumerator = new ChronologicalMediaCollectionEnumerator( + collectionOne.MediaItems, + new CollectionEnumeratorState()); + + PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); + + var scheduler = new PlayoutModeSchedulerOne(new Mock().Object); + (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( + startState, + CollectionEnumerators(scheduleItem, enumerator), + scheduleItem, + NextScheduleItem, + HardStop(scheduleItemsEnumerator)); + + playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(1)); + playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); + + playoutBuilderState.NextGuideGroup.Should().Be(2); + playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); + playoutBuilderState.InFlood.Should().BeFalse(); + playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); + playoutBuilderState.InDurationFiller.Should().BeFalse(); + playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); + + enumerator.State.Index.Should().Be(1); + + playoutItems.Count.Should().Be(1); + + playoutItems[0].MediaItemId.Should().Be(1); + playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); + playoutItems[0].GuideGroup.Should().Be(1); + playoutItems[0].FillerKind.Should().Be(FillerKind.None); } -} + + [Test] + public void Should_Have_Gap_With_Empty_Tail_Empty_Fallback() + { + Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromHours(1)); + var collectionTwo = new Collection { Id = 2, Name = "Collection 2", MediaItems = new List() }; + var collectionThree = new Collection { Id = 3, Name = "Collection 3", MediaItems = new List() }; + + var scheduleItem = new ProgramScheduleItemOne + { + Id = 1, + Index = 1, + Collection = collectionOne, + CollectionId = collectionOne.Id, + StartTime = null, + PlaybackOrder = PlaybackOrder.Chronological, + TailFiller = new FillerPreset + { + FillerKind = FillerKind.Tail, + CollectionId = collectionTwo.Id, + Collection = collectionTwo + }, + FallbackFiller = new FillerPreset + { + FillerKind = FillerKind.Fallback, + CollectionId = collectionThree.Id, + Collection = collectionThree + } + }; + + var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( + new List { scheduleItem }, + new CollectionEnumeratorState()); + + var enumerator1 = new ChronologicalMediaCollectionEnumerator( + collectionOne.MediaItems, + new CollectionEnumeratorState()); + + var enumerator2 = new ChronologicalMediaCollectionEnumerator( + collectionTwo.MediaItems, + new CollectionEnumeratorState()); + + var enumerator3 = new ChronologicalMediaCollectionEnumerator( + collectionThree.MediaItems, + new CollectionEnumeratorState()); + + PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); + + var scheduler = new PlayoutModeSchedulerOne(new Mock().Object); + (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( + startState, + CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2, scheduleItem.FallbackFiller, enumerator3), + scheduleItem, + NextScheduleItem, + HardStop(scheduleItemsEnumerator)); + + playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(1)); + playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); + + playoutBuilderState.NextGuideGroup.Should().Be(2); + playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); + playoutBuilderState.InFlood.Should().BeFalse(); + playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); + playoutBuilderState.InDurationFiller.Should().BeFalse(); + playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); + + enumerator1.State.Index.Should().Be(1); + enumerator2.State.Index.Should().Be(0); + enumerator3.State.Index.Should().Be(0); + + playoutItems.Count.Should().Be(1); + + playoutItems[0].MediaItemId.Should().Be(1); + playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); + playoutItems[0].GuideGroup.Should().Be(1); + playoutItems[0].FillerKind.Should().Be(FillerKind.None); + } + + [Test] + public void Should_Not_Have_Gap_With_Exact_Tail() + { + Collection collectionOne = TwoItemCollection(1, 2, new TimeSpan(2, 45, 0)); + Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(5)); + + var scheduleItem = new ProgramScheduleItemOne + { + Id = 1, + Index = 1, + Collection = collectionOne, + CollectionId = collectionOne.Id, + StartTime = null, + PlaybackOrder = PlaybackOrder.Chronological, + TailFiller = new FillerPreset + { + FillerKind = FillerKind.Tail, + Collection = collectionTwo, + CollectionId = collectionTwo.Id + }, + FallbackFiller = null + }; + + var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( + new List { scheduleItem }, + new CollectionEnumeratorState()); + + var enumerator1 = new ChronologicalMediaCollectionEnumerator( + collectionOne.MediaItems, + new CollectionEnumeratorState()); + + var enumerator2 = new ChronologicalMediaCollectionEnumerator( + collectionTwo.MediaItems, + new CollectionEnumeratorState()); + + PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); + + var scheduler = new PlayoutModeSchedulerOne(new Mock().Object); + (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( + startState, + CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2), + scheduleItem, + NextScheduleItem, + HardStop(scheduleItemsEnumerator)); + + playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3)); + playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); + + playoutBuilderState.NextGuideGroup.Should().Be(2); + playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); + playoutBuilderState.InFlood.Should().BeFalse(); + playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); + playoutBuilderState.InDurationFiller.Should().BeFalse(); + playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); + + enumerator1.State.Index.Should().Be(1); + enumerator2.State.Index.Should().Be(1); + + playoutItems.Count.Should().Be(4); + + playoutItems[0].MediaItemId.Should().Be(1); + playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); + playoutItems[0].GuideGroup.Should().Be(1); + playoutItems[0].FillerKind.Should().Be(FillerKind.None); + + playoutItems[1].MediaItemId.Should().Be(3); + playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0))); + playoutItems[1].GuideGroup.Should().Be(1); + playoutItems[1].FillerKind.Should().Be(FillerKind.Tail); + + playoutItems[2].MediaItemId.Should().Be(4); + playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 50, 0))); + playoutItems[2].GuideGroup.Should().Be(1); + playoutItems[2].FillerKind.Should().Be(FillerKind.Tail); + + playoutItems[3].MediaItemId.Should().Be(3); + playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 55, 0))); + playoutItems[3].GuideGroup.Should().Be(1); + playoutItems[3].FillerKind.Should().Be(FillerKind.Tail); + } + + [Test] + public void Should_Not_Have_Gap_With_Fallback() + { + Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromHours(1)); + Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(5)); + + var scheduleItem = new ProgramScheduleItemOne + { + Id = 1, + Index = 1, + Collection = collectionOne, + CollectionId = collectionOne.Id, + StartTime = null, + PlaybackOrder = PlaybackOrder.Chronological, + TailFiller = null, + FallbackFiller = new FillerPreset + { + FillerKind = FillerKind.Tail, + Collection = collectionTwo, + CollectionId = collectionTwo.Id + } + }; + + var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( + new List { scheduleItem }, + new CollectionEnumeratorState()); + + var enumerator1 = new ChronologicalMediaCollectionEnumerator( + collectionOne.MediaItems, + new CollectionEnumeratorState()); + + var enumerator2 = new ChronologicalMediaCollectionEnumerator( + collectionTwo.MediaItems, + new CollectionEnumeratorState()); + + PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); + + var scheduler = new PlayoutModeSchedulerOne(new Mock().Object); + (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( + startState, + CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.FallbackFiller, enumerator2), + scheduleItem, + NextScheduleItem, + HardStop(scheduleItemsEnumerator)); + + playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3)); + playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); + + playoutBuilderState.NextGuideGroup.Should().Be(2); + playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); + playoutBuilderState.InFlood.Should().BeFalse(); + playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); + playoutBuilderState.InDurationFiller.Should().BeFalse(); + playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); + + enumerator1.State.Index.Should().Be(1); + enumerator2.State.Index.Should().Be(1); + + playoutItems.Count.Should().Be(2); + + playoutItems[0].MediaItemId.Should().Be(1); + playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); + playoutItems[0].GuideGroup.Should().Be(1); + playoutItems[0].FillerKind.Should().Be(FillerKind.None); + + playoutItems[1].MediaItemId.Should().Be(3); + playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddHours(1)); + playoutItems[1].GuideGroup.Should().Be(1); + playoutItems[1].FillerKind.Should().Be(FillerKind.Fallback); + } + + [Test] + public void Should_Have_Gap_With_Tail_No_Fallback() + { + Collection collectionOne = TwoItemCollection(1, 2, new TimeSpan(2, 45, 0)); + Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(4)); + + var scheduleItem = new ProgramScheduleItemOne + { + Id = 1, + Index = 1, + Collection = collectionOne, + CollectionId = collectionOne.Id, + StartTime = null, + PlaybackOrder = PlaybackOrder.Chronological, + TailFiller = new FillerPreset + { + FillerKind = FillerKind.Tail, + Collection = collectionTwo, + CollectionId = collectionTwo.Id + }, + FallbackFiller = null + }; + + var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( + new List { scheduleItem }, + new CollectionEnumeratorState()); + + var enumerator1 = new ChronologicalMediaCollectionEnumerator( + collectionOne.MediaItems, + new CollectionEnumeratorState()); + + var enumerator2 = new ChronologicalMediaCollectionEnumerator( + collectionTwo.MediaItems, + new CollectionEnumeratorState()); + + PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); + + var scheduler = new PlayoutModeSchedulerOne(new Mock().Object); + (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( + startState, + CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2), + scheduleItem, + NextScheduleItem, + HardStop(scheduleItemsEnumerator)); + + playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 57, 0))); + playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); + + playoutBuilderState.NextGuideGroup.Should().Be(2); + playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); + playoutBuilderState.InFlood.Should().BeFalse(); + playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); + playoutBuilderState.InDurationFiller.Should().BeFalse(); + playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); + + enumerator1.State.Index.Should().Be(1); + enumerator2.State.Index.Should().Be(1); + + playoutItems.Count.Should().Be(4); + + playoutItems[0].MediaItemId.Should().Be(1); + playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); + playoutItems[0].GuideGroup.Should().Be(1); + playoutItems[0].FillerKind.Should().Be(FillerKind.None); + + playoutItems[1].MediaItemId.Should().Be(3); + playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0))); + playoutItems[1].GuideGroup.Should().Be(1); + playoutItems[1].FillerKind.Should().Be(FillerKind.Tail); + + playoutItems[2].MediaItemId.Should().Be(4); + playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 49, 0))); + playoutItems[2].GuideGroup.Should().Be(1); + playoutItems[2].FillerKind.Should().Be(FillerKind.Tail); + + playoutItems[3].MediaItemId.Should().Be(3); + playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 53, 0))); + playoutItems[3].GuideGroup.Should().Be(1); + playoutItems[3].FillerKind.Should().Be(FillerKind.Tail); + } + + [Test] + public void Should_Not_Have_Gap_With_Tail_And_Fallback() + { + Collection collectionOne = TwoItemCollection(1, 2, new TimeSpan(2, 45, 0)); + Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(4)); + Collection collectionThree = TwoItemCollection(5, 6, TimeSpan.FromMinutes(1)); + + var scheduleItem = new ProgramScheduleItemOne + { + Id = 1, + Index = 1, + Collection = collectionOne, + CollectionId = collectionOne.Id, + StartTime = null, + PlaybackOrder = PlaybackOrder.Chronological, + TailFiller = new FillerPreset + { + FillerKind = FillerKind.Tail, + Collection = collectionTwo, + CollectionId = collectionTwo.Id + }, + FallbackFiller = new FillerPreset + { + FillerKind = FillerKind.Fallback, + Collection = collectionThree, + CollectionId = collectionThree.Id + } + }; + + var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( + new List { scheduleItem }, + new CollectionEnumeratorState()); + + var enumerator1 = new ChronologicalMediaCollectionEnumerator( + collectionOne.MediaItems, + new CollectionEnumeratorState()); + + var enumerator2 = new ChronologicalMediaCollectionEnumerator( + collectionTwo.MediaItems, + new CollectionEnumeratorState()); + + var enumerator3 = new ChronologicalMediaCollectionEnumerator( + collectionThree.MediaItems, + new CollectionEnumeratorState()); + + PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); + + var scheduler = new PlayoutModeSchedulerOne(new Mock().Object); + (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( + startState, + CollectionEnumerators( + scheduleItem, + enumerator1, + scheduleItem.TailFiller, + enumerator2, + scheduleItem.FallbackFiller, + enumerator3), + scheduleItem, + NextScheduleItem, + HardStop(scheduleItemsEnumerator)); + + playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3)); + playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); + + playoutBuilderState.NextGuideGroup.Should().Be(2); + playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); + playoutBuilderState.InFlood.Should().BeFalse(); + playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); + playoutBuilderState.InDurationFiller.Should().BeFalse(); + playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); + + enumerator1.State.Index.Should().Be(1); + enumerator2.State.Index.Should().Be(1); + enumerator3.State.Index.Should().Be(1); + + playoutItems.Count.Should().Be(5); + + playoutItems[0].MediaItemId.Should().Be(1); + playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); + playoutItems[0].GuideGroup.Should().Be(1); + playoutItems[0].FillerKind.Should().Be(FillerKind.None); + + playoutItems[1].MediaItemId.Should().Be(3); + playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0))); + playoutItems[1].GuideGroup.Should().Be(1); + playoutItems[1].FillerKind.Should().Be(FillerKind.Tail); + + playoutItems[2].MediaItemId.Should().Be(4); + playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 49, 0))); + playoutItems[2].GuideGroup.Should().Be(1); + playoutItems[2].FillerKind.Should().Be(FillerKind.Tail); + + playoutItems[3].MediaItemId.Should().Be(3); + playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 53, 0))); + playoutItems[3].GuideGroup.Should().Be(1); + playoutItems[3].FillerKind.Should().Be(FillerKind.Tail); + + playoutItems[4].MediaItemId.Should().Be(5); + playoutItems[4].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 57, 0))); + playoutItems[4].GuideGroup.Should().Be(1); + playoutItems[4].FillerKind.Should().Be(FillerKind.Fallback); + } + + [Test] + public void Should_Not_Have_Gap_With_Unused_Tail_And_Unused_Fallback() + { + Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromHours(3)); + Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(4)); + Collection collectionThree = TwoItemCollection(5, 6, TimeSpan.FromMinutes(1)); + + var scheduleItem = new ProgramScheduleItemOne + { + Id = 1, + Index = 1, + Collection = collectionOne, + CollectionId = collectionOne.Id, + StartTime = null, + PlaybackOrder = PlaybackOrder.Chronological, + TailFiller = new FillerPreset + { + FillerKind = FillerKind.Tail, + Collection = collectionTwo, + CollectionId = collectionTwo.Id + }, + FallbackFiller = new FillerPreset + { + FillerKind = FillerKind.Fallback, + Collection = collectionThree, + CollectionId = collectionThree.Id + } + }; + + var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( + new List { scheduleItem }, + new CollectionEnumeratorState()); + + var enumerator1 = new ChronologicalMediaCollectionEnumerator( + collectionOne.MediaItems, + new CollectionEnumeratorState()); + + var enumerator2 = new ChronologicalMediaCollectionEnumerator( + collectionTwo.MediaItems, + new CollectionEnumeratorState()); + + var enumerator3 = new ChronologicalMediaCollectionEnumerator( + collectionThree.MediaItems, + new CollectionEnumeratorState()); + + PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); + + var scheduler = new PlayoutModeSchedulerOne(new Mock().Object); + (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( + startState, + CollectionEnumerators( + scheduleItem, + enumerator1, + scheduleItem.TailFiller, + enumerator2, + scheduleItem.FallbackFiller, + enumerator3), + scheduleItem, + NextScheduleItem, + HardStop(scheduleItemsEnumerator)); + + playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3)); + playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); + + playoutBuilderState.NextGuideGroup.Should().Be(2); + playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); + playoutBuilderState.InFlood.Should().BeFalse(); + playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); + playoutBuilderState.InDurationFiller.Should().BeFalse(); + playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); + + enumerator1.State.Index.Should().Be(1); + enumerator2.State.Index.Should().Be(0); + enumerator3.State.Index.Should().Be(0); + + playoutItems.Count.Should().Be(1); + + playoutItems[0].MediaItemId.Should().Be(1); + playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); + playoutItems[0].GuideGroup.Should().Be(1); + playoutItems[0].FillerKind.Should().Be(FillerKind.None); + } + + [Test] + public void Should_Have_No_Gap_With_Exact_Post_Roll_Pad() + { + Collection collectionOne = TwoItemCollection(1, 2, new TimeSpan(2, 45, 0)); + Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(5)); + Collection collectionThree = TwoItemCollection(5, 6, TimeSpan.FromMinutes(1)); + + var scheduleItem = new ProgramScheduleItemOne + { + Id = 1, + Index = 1, + Collection = collectionOne, + CollectionId = collectionOne.Id, + StartTime = null, + PlaybackOrder = PlaybackOrder.Chronological, + PostRollFiller = new FillerPreset + { + FillerKind = FillerKind.PostRoll, + FillerMode = FillerMode.Pad, + PadToNearestMinute = 30, + Collection = collectionTwo, + CollectionId = collectionTwo.Id + }, + FallbackFiller = new FillerPreset + { + FillerKind = FillerKind.Fallback, + Collection = collectionThree, + CollectionId = collectionThree.Id + } + }; + + var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( + new List { scheduleItem }, + new CollectionEnumeratorState()); + + var enumerator1 = new ChronologicalMediaCollectionEnumerator( + collectionOne.MediaItems, + new CollectionEnumeratorState()); + + var enumerator2 = new ChronologicalMediaCollectionEnumerator( + collectionTwo.MediaItems, + new CollectionEnumeratorState()); + + var enumerator3 = new ChronologicalMediaCollectionEnumerator( + collectionThree.MediaItems, + new CollectionEnumeratorState()); + + PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); + + var scheduler = new PlayoutModeSchedulerOne(new Mock().Object); + (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( + startState, + CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.PostRollFiller, enumerator2, scheduleItem.FallbackFiller, enumerator3), + scheduleItem, + NextScheduleItem, + HardStop(scheduleItemsEnumerator)); + + playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3)); + playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); + + playoutBuilderState.NextGuideGroup.Should().Be(2); + playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); + playoutBuilderState.InFlood.Should().BeFalse(); + playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); + playoutBuilderState.InDurationFiller.Should().BeFalse(); + playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); + + enumerator1.State.Index.Should().Be(1); + enumerator2.State.Index.Should().Be(1); + + playoutItems.Count.Should().Be(4); + + playoutItems[0].MediaItemId.Should().Be(1); + playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); + playoutItems[0].GuideGroup.Should().Be(1); + playoutItems[0].FillerKind.Should().Be(FillerKind.None); + + playoutItems[1].MediaItemId.Should().Be(3); + playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0))); + playoutItems[1].GuideGroup.Should().Be(1); + playoutItems[1].FillerKind.Should().Be(FillerKind.PostRoll); + + playoutItems[2].MediaItemId.Should().Be(4); + playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 50, 0))); + playoutItems[2].GuideGroup.Should().Be(1); + playoutItems[2].FillerKind.Should().Be(FillerKind.PostRoll); + + playoutItems[3].MediaItemId.Should().Be(3); + playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 55, 0))); + playoutItems[3].GuideGroup.Should().Be(1); + playoutItems[3].FillerKind.Should().Be(FillerKind.PostRoll); + } + + [Test] + public void Should_Have_No_Gap_With_Exact_Post_Roll_Pad_With_Chapters() + { + Collection collectionOne = TwoItemCollection(1, 2, new TimeSpan(2, 45, 0), 2); + Collection collectionTwo = TwoItemCollection(3, 4, TimeSpan.FromMinutes(5)); + Collection collectionThree = TwoItemCollection(5, 6, TimeSpan.FromMinutes(1)); + + var scheduleItem = new ProgramScheduleItemOne + { + Id = 1, + Index = 1, + Collection = collectionOne, + CollectionId = collectionOne.Id, + StartTime = null, + PlaybackOrder = PlaybackOrder.Chronological, + PostRollFiller = new FillerPreset + { + FillerKind = FillerKind.PostRoll, + FillerMode = FillerMode.Pad, + PadToNearestMinute = 30, + Collection = collectionTwo, + CollectionId = collectionTwo.Id + }, + FallbackFiller = new FillerPreset + { + FillerKind = FillerKind.Fallback, + Collection = collectionThree, + CollectionId = collectionThree.Id + } + }; + + var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator( + new List { scheduleItem }, + new CollectionEnumeratorState()); + + var enumerator1 = new ChronologicalMediaCollectionEnumerator( + collectionOne.MediaItems, + new CollectionEnumeratorState()); + + var enumerator2 = new ChronologicalMediaCollectionEnumerator( + collectionTwo.MediaItems, + new CollectionEnumeratorState()); + + var enumerator3 = new ChronologicalMediaCollectionEnumerator( + collectionThree.MediaItems, + new CollectionEnumeratorState()); + + PlayoutBuilderState startState = StartState(scheduleItemsEnumerator); + + var scheduler = new PlayoutModeSchedulerOne(new Mock().Object); + (PlayoutBuilderState playoutBuilderState, List playoutItems) = scheduler.Schedule( + startState, + CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.PostRollFiller, enumerator2, scheduleItem.FallbackFiller, enumerator3), + scheduleItem, + NextScheduleItem, + HardStop(scheduleItemsEnumerator)); + + playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3)); + playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); + + playoutBuilderState.NextGuideGroup.Should().Be(2); + playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); + playoutBuilderState.InFlood.Should().BeFalse(); + playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); + playoutBuilderState.InDurationFiller.Should().BeFalse(); + playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0); + + enumerator1.State.Index.Should().Be(1); + enumerator2.State.Index.Should().Be(1); + enumerator3.State.Index.Should().Be(0); + + playoutItems.Count.Should().Be(4); + + playoutItems[0].MediaItemId.Should().Be(1); + playoutItems[0].StartOffset.Should().Be(startState.CurrentTime); + playoutItems[0].GuideGroup.Should().Be(1); + playoutItems[0].FillerKind.Should().Be(FillerKind.None); + + playoutItems[1].MediaItemId.Should().Be(3); + playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0))); + playoutItems[1].GuideGroup.Should().Be(1); + playoutItems[1].FillerKind.Should().Be(FillerKind.PostRoll); + + playoutItems[2].MediaItemId.Should().Be(4); + playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 50, 0))); + playoutItems[2].GuideGroup.Should().Be(1); + playoutItems[2].FillerKind.Should().Be(FillerKind.PostRoll); + + playoutItems[3].MediaItemId.Should().Be(3); + playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 55, 0))); + playoutItems[3].GuideGroup.Should().Be(1); + playoutItems[3].FillerKind.Should().Be(FillerKind.PostRoll); + } + + protected override ProgramScheduleItem NextScheduleItem => new ProgramScheduleItemOne + { + StartTime = TimeSpan.FromHours(3) + }; +} \ No newline at end of file diff --git a/ErsatzTV.Core.Tests/Scheduling/RandomizedContentTests.cs b/ErsatzTV.Core.Tests/Scheduling/RandomizedContentTests.cs index accefa35a..cb7f6055c 100644 --- a/ErsatzTV.Core.Tests/Scheduling/RandomizedContentTests.cs +++ b/ErsatzTV.Core.Tests/Scheduling/RandomizedContentTests.cs @@ -1,107 +1,102 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; using ErsatzTV.Core.Scheduling; using FluentAssertions; using NUnit.Framework; -using static LanguageExt.Prelude; -namespace ErsatzTV.Core.Tests.Scheduling +namespace ErsatzTV.Core.Tests.Scheduling; + +[TestFixture] +public class RandomizedContentTests { - [TestFixture] - public class RandomizedContentTests + private const int KnownSeed = 22295; + + private readonly List _expected = new() { - private const int KnownSeed = 22295; + 5, 7, 7, 8, 6, 7, 8, 9, 10, 7, 5, 1, 7, 2, 5, 6, 1, 4, 5, 6, 4, 5, 1, 6, 5, 7, 1, 3, 9, 9, 9, 3, + 3, 2, 3, 4, 5, 6, 9, 3, 6, 9, 7, 1, 2, 10, 3, 8, 3, 8, 8, 3, 1, 5, 4, 3, 6, 4, 6, 2, 9, 8, 3, 1, 8, 5, + 1, 8, 2, 1, 1, 5, 5, 5, 3, 5, 8, 10, 4, 8, 7, 3, 3, 4, 4, 9, 2, 8, 8, 10, 8, 4, 3, 10, 7, 8, 9, 9 + }; - private readonly List _expected = new() + [Test] + public void Episodes_Should_Randomize() + { + List contents = Episodes(10); + + var state = new CollectionEnumeratorState(); + + var randomizedContent = new RandomizedMediaCollectionEnumerator(contents, state); + + var list = new List(); + for (var i = 1; i <= 10; i++) { - 5, 7, 7, 8, 6, 7, 8, 9, 10, 7, 5, 1, 7, 2, 5, 6, 1, 4, 5, 6, 4, 5, 1, 6, 5, 7, 1, 3, 9, 9, 9, 3, - 3, 2, 3, 4, 5, 6, 9, 3, 6, 9, 7, 1, 2, 10, 3, 8, 3, 8, 8, 3, 1, 5, 4, 3, 6, 4, 6, 2, 9, 8, 3, 1, 8, 5, - 1, 8, 2, 1, 1, 5, 5, 5, 3, 5, 8, 10, 4, 8, 7, 3, 3, 4, 4, 9, 2, 8, 8, 10, 8, 4, 3, 10, 7, 8, 9, 9 - }; + randomizedContent.Current.IsSome.Should().BeTrue(); + randomizedContent.Current.Do(c => list.Add(c.Id)); - [Test] - public void Episodes_Should_Randomize() - { - List contents = Episodes(10); - - var state = new CollectionEnumeratorState(); - - var randomizedContent = new RandomizedMediaCollectionEnumerator(contents, state); - - var list = new List(); - for (var i = 1; i <= 10; i++) - { - randomizedContent.Current.IsSome.Should().BeTrue(); - randomizedContent.Current.Do(c => list.Add(c.Id)); - - randomizedContent.MoveNext(); - } - - list.Should().NotEqual(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); - list.Should().NotEqual(new[] { 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 }); + randomizedContent.MoveNext(); } - [Test] - public void State_Index_Should_Increment() - { - List contents = Episodes(10); - var state = new CollectionEnumeratorState(); - - var randomizedContent = new RandomizedMediaCollectionEnumerator(contents, state); - - for (var i = 1; i <= 10; i++) - { - randomizedContent.State.Index.Should().Be(i); - - randomizedContent.MoveNext(); - } - } - - [Test] - public void State_Should_Impact_Iterator_Start() - { - List contents = Episodes(10); - var state = new CollectionEnumeratorState { Index = 5, Seed = KnownSeed }; - - var randomizedContent = new RandomizedMediaCollectionEnumerator(contents, state); - - for (var i = 6; i <= 99; i++) - { - randomizedContent.Current.IsSome.Should().BeTrue(); - // this test data setup/expectation is confusing - randomizedContent.Current.Map(c => c.Id).IfNone(-1).Should().Be(_expected[i - 2]); - randomizedContent.State.Index.Should().Be(i); - - randomizedContent.MoveNext(); - } - } - - [Test] - [Timeout(1000)] - public void State_Index_Should_Continue_Past_End_Of_Items() - { - List contents = Episodes(10); - var state = new CollectionEnumeratorState { Index = 10, Seed = KnownSeed }; - - var _ = new RandomizedMediaCollectionEnumerator(contents, state); - } - - private static List Episodes(int count) => - Range(1, count).Map( - i => (MediaItem) new Episode - { - Id = i, - EpisodeMetadata = new List - { - new() - { - ReleaseDate = new DateTime(2020, 1, i) - } - } - }) - .Reverse() - .ToList(); + list.Should().NotEqual(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); + list.Should().NotEqual(new[] { 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 }); } -} + + [Test] + public void State_Index_Should_Increment() + { + List contents = Episodes(10); + var state = new CollectionEnumeratorState(); + + var randomizedContent = new RandomizedMediaCollectionEnumerator(contents, state); + + for (var i = 1; i <= 10; i++) + { + randomizedContent.State.Index.Should().Be(i); + + randomizedContent.MoveNext(); + } + } + + [Test] + public void State_Should_Impact_Iterator_Start() + { + List contents = Episodes(10); + var state = new CollectionEnumeratorState { Index = 5, Seed = KnownSeed }; + + var randomizedContent = new RandomizedMediaCollectionEnumerator(contents, state); + + for (var i = 6; i <= 99; i++) + { + randomizedContent.Current.IsSome.Should().BeTrue(); + // this test data setup/expectation is confusing + randomizedContent.Current.Map(c => c.Id).IfNone(-1).Should().Be(_expected[i - 2]); + randomizedContent.State.Index.Should().Be(i); + + randomizedContent.MoveNext(); + } + } + + [Test] + [Timeout(1000)] + public void State_Index_Should_Continue_Past_End_Of_Items() + { + List contents = Episodes(10); + var state = new CollectionEnumeratorState { Index = 10, Seed = KnownSeed }; + + var _ = new RandomizedMediaCollectionEnumerator(contents, state); + } + + private static List Episodes(int count) => + Range(1, count).Map( + i => (MediaItem) new Episode + { + Id = i, + EpisodeMetadata = new List + { + new() + { + ReleaseDate = new DateTime(2020, 1, i) + } + } + }) + .Reverse() + .ToList(); +} \ No newline at end of file diff --git a/ErsatzTV.Core.Tests/Scheduling/SchedulerTestBase.cs b/ErsatzTV.Core.Tests/Scheduling/SchedulerTestBase.cs index ef97340f8..4938b367a 100644 --- a/ErsatzTV.Core.Tests/Scheduling/SchedulerTestBase.cs +++ b/ErsatzTV.Core.Tests/Scheduling/SchedulerTestBase.cs @@ -1,93 +1,89 @@ -using System; -using System.Collections.Generic; -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Filler; using ErsatzTV.Core.Interfaces.Scheduling; using ErsatzTV.Core.Scheduling; -using LanguageExt; -namespace ErsatzTV.Core.Tests.Scheduling +namespace ErsatzTV.Core.Tests.Scheduling; + +public abstract class SchedulerTestBase { - public abstract class SchedulerTestBase - { - protected static PlayoutBuilderState StartState(IScheduleItemsEnumerator scheduleItemsEnumerator) => new( - scheduleItemsEnumerator, - Prelude.None, - Prelude.None, - false, - false, - 1, - new DateTimeOffset(new DateTime(2020, 10, 18, 0, 0, 0, DateTimeKind.Local))); + protected static PlayoutBuilderState StartState(IScheduleItemsEnumerator scheduleItemsEnumerator) => new( + scheduleItemsEnumerator, + Prelude.None, + Prelude.None, + false, + false, + 1, + new DateTimeOffset(new DateTime(2020, 10, 18, 0, 0, 0, DateTimeKind.Local))); - protected virtual ProgramScheduleItem NextScheduleItem => new ProgramScheduleItemOne + protected virtual ProgramScheduleItem NextScheduleItem => new ProgramScheduleItemOne + { + StartTime = null + }; + + protected static DateTimeOffset HardStop(IScheduleItemsEnumerator scheduleItemsEnumerator) => + StartState(scheduleItemsEnumerator).CurrentTime.AddHours(6); + + protected static Dictionary CollectionEnumerators( + ProgramScheduleItem scheduleItem, IMediaCollectionEnumerator enumerator) => + new() { - StartTime = null + { CollectionKey.ForScheduleItem(scheduleItem), enumerator } }; - protected static DateTimeOffset HardStop(IScheduleItemsEnumerator scheduleItemsEnumerator) => - StartState(scheduleItemsEnumerator).CurrentTime.AddHours(6); - - protected static Dictionary CollectionEnumerators( - ProgramScheduleItem scheduleItem, IMediaCollectionEnumerator enumerator) => - new() - { - { CollectionKey.ForScheduleItem(scheduleItem), enumerator } - }; - - protected static Dictionary CollectionEnumerators( - ProgramScheduleItem scheduleItem, IMediaCollectionEnumerator enumerator1, - FillerPreset fillerPreset, IMediaCollectionEnumerator enumerator2, - FillerPreset fillerPreset2, IMediaCollectionEnumerator enumerator3) => - new() - { - { CollectionKey.ForScheduleItem(scheduleItem), enumerator1 }, - { CollectionKey.ForFillerPreset(fillerPreset), enumerator2 }, - { CollectionKey.ForFillerPreset(fillerPreset2), enumerator3 } - }; - - private static Movie TestMovie(int id, TimeSpan duration, DateTime aired, int chapterCount = 0) + protected static Dictionary CollectionEnumerators( + ProgramScheduleItem scheduleItem, IMediaCollectionEnumerator enumerator1, + FillerPreset fillerPreset, IMediaCollectionEnumerator enumerator2, + FillerPreset fillerPreset2, IMediaCollectionEnumerator enumerator3) => + new() { - var result = new Movie() - { - Id = id, - MovieMetadata = new List { new() { ReleaseDate = aired } }, - MediaVersions = new List - { - new() { Duration = duration, Chapters = new List() } - } - }; + { CollectionKey.ForScheduleItem(scheduleItem), enumerator1 }, + { CollectionKey.ForFillerPreset(fillerPreset), enumerator2 }, + { CollectionKey.ForFillerPreset(fillerPreset2), enumerator3 } + }; - for (var i = 0; i < chapterCount; i++) + private static Movie TestMovie(int id, TimeSpan duration, DateTime aired, int chapterCount = 0) + { + var result = new Movie() + { + Id = id, + MovieMetadata = new List { new() { ReleaseDate = aired } }, + MediaVersions = new List { - result.MediaVersions.Head().Chapters.Add( - new MediaChapter - { - StartTime = TimeSpan.FromMilliseconds(i * duration.TotalMilliseconds / chapterCount), - EndTime = TimeSpan.FromMilliseconds(i + 1 * duration.TotalMilliseconds / chapterCount) - }); + new() { Duration = duration, Chapters = new List() } } + }; - return result; + for (var i = 0; i < chapterCount; i++) + { + result.MediaVersions.Head().Chapters.Add( + new MediaChapter + { + StartTime = TimeSpan.FromMilliseconds(i * duration.TotalMilliseconds / chapterCount), + EndTime = TimeSpan.FromMilliseconds(i + 1 * duration.TotalMilliseconds / chapterCount) + }); } - protected static Collection TwoItemCollection(int id1, int id2, TimeSpan duration, int chapterCount = 0) => new() - { - Id = id1, - Name = $"Collection of Items {id1}", - MediaItems = new List - { - TestMovie(id1, duration, new DateTime(2020, 1, 1), chapterCount), - TestMovie(id2, duration, new DateTime(2020, 1, 2), chapterCount) - } - }; - - protected static Dictionary CollectionEnumerators( - ProgramScheduleItem scheduleItem, IMediaCollectionEnumerator enumerator1, - FillerPreset fillerPreset, IMediaCollectionEnumerator enumerator2) => - new() - { - { CollectionKey.ForScheduleItem(scheduleItem), enumerator1 }, - { CollectionKey.ForFillerPreset(fillerPreset), enumerator2 } - }; + return result; } -} + + protected static Collection TwoItemCollection(int id1, int id2, TimeSpan duration, int chapterCount = 0) => new() + { + Id = id1, + Name = $"Collection of Items {id1}", + MediaItems = new List + { + TestMovie(id1, duration, new DateTime(2020, 1, 1), chapterCount), + TestMovie(id2, duration, new DateTime(2020, 1, 2), chapterCount) + } + }; + + protected static Dictionary CollectionEnumerators( + ProgramScheduleItem scheduleItem, IMediaCollectionEnumerator enumerator1, + FillerPreset fillerPreset, IMediaCollectionEnumerator enumerator2) => + new() + { + { CollectionKey.ForScheduleItem(scheduleItem), enumerator1 }, + { CollectionKey.ForFillerPreset(fillerPreset), enumerator2 } + }; +} \ No newline at end of file diff --git a/ErsatzTV.Core.Tests/Scheduling/ShuffledContentTests.cs b/ErsatzTV.Core.Tests/Scheduling/ShuffledContentTests.cs index 46ea690cf..306fdf84c 100644 --- a/ErsatzTV.Core.Tests/Scheduling/ShuffledContentTests.cs +++ b/ErsatzTV.Core.Tests/Scheduling/ShuffledContentTests.cs @@ -1,154 +1,149 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; using ErsatzTV.Core.Scheduling; using FluentAssertions; using NUnit.Framework; -using static LanguageExt.Prelude; -namespace ErsatzTV.Core.Tests.Scheduling +namespace ErsatzTV.Core.Tests.Scheduling; + +[TestFixture] +public class ShuffledContentTests { - [TestFixture] - public class ShuffledContentTests + // this seed will produce (shuffle) 1-10 in order + private const int MagicSeed = 670596; + + [Test] + public void Episodes_Should_Not_Duplicate_When_Reshuffling() { - // this seed will produce (shuffle) 1-10 in order - private const int MagicSeed = 670596; + List contents = Episodes(10); - [Test] - public void Episodes_Should_Not_Duplicate_When_Reshuffling() + // normally returns 10 5 7 4 3 6 2 8 9 1 1 (note duplicate 1 at end) + var state = new CollectionEnumeratorState { Seed = 8 }; + + var groupedMediaItems = contents.Map(mi => new GroupedMediaItem(mi, null)).ToList(); + var shuffledContent = new ShuffledMediaCollectionEnumerator(groupedMediaItems, state); + + var list = new List(); + for (var i = 1; i <= 1000; i++) { - List contents = Episodes(10); - - // normally returns 10 5 7 4 3 6 2 8 9 1 1 (note duplicate 1 at end) - var state = new CollectionEnumeratorState { Seed = 8 }; - - var groupedMediaItems = contents.Map(mi => new GroupedMediaItem(mi, null)).ToList(); - var shuffledContent = new ShuffledMediaCollectionEnumerator(groupedMediaItems, state); - - var list = new List(); - for (var i = 1; i <= 1000; i++) - { - shuffledContent.Current.IsSome.Should().BeTrue(); - shuffledContent.Current.Do(x => list.Add(x.Id)); - shuffledContent.MoveNext(); - } - - for (var i = 0; i < list.Count - 1; i++) - { - if (list[i] == list[i + 1]) - { - Assert.Fail("List contains duplicate items"); - } - } + shuffledContent.Current.IsSome.Should().BeTrue(); + shuffledContent.Current.Do(x => list.Add(x.Id)); + shuffledContent.MoveNext(); } - [Test] - [Timeout(2000)] - public void Duplicate_Check_Should_Ignore_Single_Item() + for (var i = 0; i < list.Count - 1; i++) { - List contents = Episodes(1); - - var state = new CollectionEnumeratorState(); - - var groupedMediaItems = contents.Map(mi => new GroupedMediaItem(mi, null)).ToList(); - var shuffledContent = new ShuffledMediaCollectionEnumerator(groupedMediaItems, state); - - var list = new List(); - for (var i = 1; i <= 10; i++) + if (list[i] == list[i + 1]) { - shuffledContent.Current.IsSome.Should().BeTrue(); - shuffledContent.Current.Do(x => list.Add(x.Id)); - shuffledContent.MoveNext(); - } - - list.Should().Equal(1, 1, 1, 1, 1, 1, 1, 1, 1, 1); - } - - [Test] - public void Episodes_Should_Shuffle() - { - List contents = Episodes(10); - - var state = new CollectionEnumeratorState(); - - var groupedMediaItems = contents.Map(mi => new GroupedMediaItem(mi, null)).ToList(); - var shuffledContent = new ShuffledMediaCollectionEnumerator(groupedMediaItems, state); - - var list = new List(); - for (var i = 1; i <= 10; i++) - { - shuffledContent.Current.IsSome.Should().BeTrue(); - shuffledContent.Current.Do(x => list.Add(x.Id)); - shuffledContent.MoveNext(); - } - - list.Should().NotEqual(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); - list.Should().BeEquivalentTo(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); - } - - [Test] - public void State_Index_Should_Increment() - { - List contents = Episodes(10); - var state = new CollectionEnumeratorState(); - - var groupedMediaItems = contents.Map(mi => new GroupedMediaItem(mi, null)).ToList(); - var shuffledContent = new ShuffledMediaCollectionEnumerator(groupedMediaItems, state); - - for (var i = 0; i < 10; i++) - { - shuffledContent.State.Index.Should().Be(i); - shuffledContent.MoveNext(); + Assert.Fail("List contains duplicate items"); } } - - [Test] - public void State_Should_Impact_Iterator_Start() - { - List contents = Episodes(10); - var state = new CollectionEnumeratorState { Index = 5, Seed = MagicSeed }; - - var groupedMediaItems = contents.Map(mi => new GroupedMediaItem(mi, null)).ToList(); - var shuffledContent = new ShuffledMediaCollectionEnumerator(groupedMediaItems, state); - - for (var i = 6; i <= 10; i++) - { - shuffledContent.Current.IsSome.Should().BeTrue(); - shuffledContent.Current.Map(x => x.Id).IfNone(-1).Should().Be(i); - shuffledContent.State.Index.Should().Be(i - 1); - shuffledContent.MoveNext(); - } - } - - [Test] - [Timeout(1000)] - public void State_Should_Reset_When_Invalid() - { - List contents = Episodes(10); - var state = new CollectionEnumeratorState { Index = 10, Seed = MagicSeed }; - - var groupedMediaItems = contents.Map(mi => new GroupedMediaItem(mi, null)).ToList(); - var shuffledContent = new ShuffledMediaCollectionEnumerator(groupedMediaItems, state); - - shuffledContent.State.Index.Should().Be(0); - shuffledContent.State.Seed.Should().NotBe(MagicSeed); - } - - private static List Episodes(int count) => - Range(1, count).Map( - i => (MediaItem) new Episode - { - Id = i, - EpisodeMetadata = new List - { - new() - { - ReleaseDate = new DateTime(2020, 1, i) - } - } - }) - .Reverse() - .ToList(); } -} + + [Test] + [Timeout(2000)] + public void Duplicate_Check_Should_Ignore_Single_Item() + { + List contents = Episodes(1); + + var state = new CollectionEnumeratorState(); + + var groupedMediaItems = contents.Map(mi => new GroupedMediaItem(mi, null)).ToList(); + var shuffledContent = new ShuffledMediaCollectionEnumerator(groupedMediaItems, state); + + var list = new List(); + for (var i = 1; i <= 10; i++) + { + shuffledContent.Current.IsSome.Should().BeTrue(); + shuffledContent.Current.Do(x => list.Add(x.Id)); + shuffledContent.MoveNext(); + } + + list.Should().Equal(1, 1, 1, 1, 1, 1, 1, 1, 1, 1); + } + + [Test] + public void Episodes_Should_Shuffle() + { + List contents = Episodes(10); + + var state = new CollectionEnumeratorState(); + + var groupedMediaItems = contents.Map(mi => new GroupedMediaItem(mi, null)).ToList(); + var shuffledContent = new ShuffledMediaCollectionEnumerator(groupedMediaItems, state); + + var list = new List(); + for (var i = 1; i <= 10; i++) + { + shuffledContent.Current.IsSome.Should().BeTrue(); + shuffledContent.Current.Do(x => list.Add(x.Id)); + shuffledContent.MoveNext(); + } + + list.Should().NotEqual(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); + list.Should().BeEquivalentTo(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); + } + + [Test] + public void State_Index_Should_Increment() + { + List contents = Episodes(10); + var state = new CollectionEnumeratorState(); + + var groupedMediaItems = contents.Map(mi => new GroupedMediaItem(mi, null)).ToList(); + var shuffledContent = new ShuffledMediaCollectionEnumerator(groupedMediaItems, state); + + for (var i = 0; i < 10; i++) + { + shuffledContent.State.Index.Should().Be(i); + shuffledContent.MoveNext(); + } + } + + [Test] + public void State_Should_Impact_Iterator_Start() + { + List contents = Episodes(10); + var state = new CollectionEnumeratorState { Index = 5, Seed = MagicSeed }; + + var groupedMediaItems = contents.Map(mi => new GroupedMediaItem(mi, null)).ToList(); + var shuffledContent = new ShuffledMediaCollectionEnumerator(groupedMediaItems, state); + + for (var i = 6; i <= 10; i++) + { + shuffledContent.Current.IsSome.Should().BeTrue(); + shuffledContent.Current.Map(x => x.Id).IfNone(-1).Should().Be(i); + shuffledContent.State.Index.Should().Be(i - 1); + shuffledContent.MoveNext(); + } + } + + [Test] + [Timeout(1000)] + public void State_Should_Reset_When_Invalid() + { + List contents = Episodes(10); + var state = new CollectionEnumeratorState { Index = 10, Seed = MagicSeed }; + + var groupedMediaItems = contents.Map(mi => new GroupedMediaItem(mi, null)).ToList(); + var shuffledContent = new ShuffledMediaCollectionEnumerator(groupedMediaItems, state); + + shuffledContent.State.Index.Should().Be(0); + shuffledContent.State.Seed.Should().NotBe(MagicSeed); + } + + private static List Episodes(int count) => + Range(1, count).Map( + i => (MediaItem) new Episode + { + Id = i, + EpisodeMetadata = new List + { + new() + { + ReleaseDate = new DateTime(2020, 1, i) + } + } + }) + .Reverse() + .ToList(); +} \ No newline at end of file diff --git a/ErsatzTV.Core.Tests/Scheduling/ShuffledMediaCollectionEnumeratorTests.cs b/ErsatzTV.Core.Tests/Scheduling/ShuffledMediaCollectionEnumeratorTests.cs index 3f861006a..0ab61d337 100644 --- a/ErsatzTV.Core.Tests/Scheduling/ShuffledMediaCollectionEnumeratorTests.cs +++ b/ErsatzTV.Core.Tests/Scheduling/ShuffledMediaCollectionEnumeratorTests.cs @@ -1,90 +1,87 @@ -using System.Collections.Generic; -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; using ErsatzTV.Core.Scheduling; using FluentAssertions; -using LanguageExt; using LanguageExt.UnsafeValueAccess; using NUnit.Framework; -namespace ErsatzTV.Core.Tests.Scheduling +namespace ErsatzTV.Core.Tests.Scheduling; + +[TestFixture] +public class ShuffledMediaCollectionEnumeratorTests { - [TestFixture] - public class ShuffledMediaCollectionEnumeratorTests + private readonly List _mediaItems = new() { - private readonly List _mediaItems = new() - { - new GroupedMediaItem(new MediaItem { Id = 1 }, new List()), - new GroupedMediaItem(new MediaItem { Id = 2 }, new List()), - new GroupedMediaItem(new MediaItem { Id = 3 }, new List()) - }; + new GroupedMediaItem(new MediaItem { Id = 1 }, new List()), + new GroupedMediaItem(new MediaItem { Id = 2 }, new List()), + new GroupedMediaItem(new MediaItem { Id = 3 }, new List()) + }; - [Test] - public void Peek_Zero_Should_Match_Current() - { - var state = new CollectionEnumeratorState { Index = 0, Seed = 0 }; - var enumerator = new ShuffledMediaCollectionEnumerator(_mediaItems, state); + [Test] + public void Peek_Zero_Should_Match_Current() + { + var state = new CollectionEnumeratorState { Index = 0, Seed = 0 }; + var enumerator = new ShuffledMediaCollectionEnumerator(_mediaItems, state); - Option peek = enumerator.Peek(0); - Option current = enumerator.Current; + Option peek = enumerator.Peek(0); + Option current = enumerator.Current; - peek.IsSome.Should().BeTrue(); - current.IsSome.Should().BeTrue(); - peek.ValueUnsafe().Id.Should().Be(1); - current.ValueUnsafe().Id.Should().Be(1); - } - - [Test] - public void Peek_One_Should_Match_Next() - { - var state = new CollectionEnumeratorState { Index = 0, Seed = 0 }; - var enumerator = new ShuffledMediaCollectionEnumerator(_mediaItems, state); - - Option peek = enumerator.Peek(1); - - enumerator.MoveNext(); - Option next = enumerator.Current; - - peek.IsSome.Should().BeTrue(); - next.IsSome.Should().BeTrue(); - peek.ValueUnsafe().Id.Should().Be(2); - next.ValueUnsafe().Id.Should().Be(2); - } - - [Test] - public void Peek_Two_Should_Match_NextNext() - { - var state = new CollectionEnumeratorState { Index = 0, Seed = 0 }; - var enumerator = new ShuffledMediaCollectionEnumerator(_mediaItems, state); - - Option peek = enumerator.Peek(2); - - enumerator.MoveNext(); - enumerator.MoveNext(); - Option next = enumerator.Current; - - peek.IsSome.Should().BeTrue(); - next.IsSome.Should().BeTrue(); - peek.ValueUnsafe().Id.Should().Be(3); - next.ValueUnsafe().Id.Should().Be(3); - } - - [Test] - public void Peek_Three_Should_Match_NextNextNext() - { - var state = new CollectionEnumeratorState { Index = 0, Seed = 0 }; - var enumerator = new ShuffledMediaCollectionEnumerator(_mediaItems, state); - - Option peek = enumerator.Peek(3); - - enumerator.MoveNext(); - enumerator.MoveNext(); - enumerator.MoveNext(); - Option next = enumerator.Current; - - peek.IsSome.Should().BeTrue(); - next.IsSome.Should().BeTrue(); - peek.ValueUnsafe().Id.Should().Be(2); - next.ValueUnsafe().Id.Should().Be(2); - } + peek.IsSome.Should().BeTrue(); + current.IsSome.Should().BeTrue(); + peek.ValueUnsafe().Id.Should().Be(1); + current.ValueUnsafe().Id.Should().Be(1); } -} + + [Test] + public void Peek_One_Should_Match_Next() + { + var state = new CollectionEnumeratorState { Index = 0, Seed = 0 }; + var enumerator = new ShuffledMediaCollectionEnumerator(_mediaItems, state); + + Option peek = enumerator.Peek(1); + + enumerator.MoveNext(); + Option next = enumerator.Current; + + peek.IsSome.Should().BeTrue(); + next.IsSome.Should().BeTrue(); + peek.ValueUnsafe().Id.Should().Be(2); + next.ValueUnsafe().Id.Should().Be(2); + } + + [Test] + public void Peek_Two_Should_Match_NextNext() + { + var state = new CollectionEnumeratorState { Index = 0, Seed = 0 }; + var enumerator = new ShuffledMediaCollectionEnumerator(_mediaItems, state); + + Option peek = enumerator.Peek(2); + + enumerator.MoveNext(); + enumerator.MoveNext(); + Option next = enumerator.Current; + + peek.IsSome.Should().BeTrue(); + next.IsSome.Should().BeTrue(); + peek.ValueUnsafe().Id.Should().Be(3); + next.ValueUnsafe().Id.Should().Be(3); + } + + [Test] + public void Peek_Three_Should_Match_NextNextNext() + { + var state = new CollectionEnumeratorState { Index = 0, Seed = 0 }; + var enumerator = new ShuffledMediaCollectionEnumerator(_mediaItems, state); + + Option peek = enumerator.Peek(3); + + enumerator.MoveNext(); + enumerator.MoveNext(); + enumerator.MoveNext(); + Option next = enumerator.Current; + + peek.IsSome.Should().BeTrue(); + next.IsSome.Should().BeTrue(); + peek.ValueUnsafe().Id.Should().Be(2); + next.ValueUnsafe().Id.Should().Be(2); + } +} \ No newline at end of file diff --git a/ErsatzTV.Core/BaseError.cs b/ErsatzTV.Core/BaseError.cs index b3d3c105c..5e540cdf6 100644 --- a/ErsatzTV.Core/BaseError.cs +++ b/ErsatzTV.Core/BaseError.cs @@ -1,18 +1,15 @@ -using LanguageExt; +namespace ErsatzTV.Core; -namespace ErsatzTV.Core +public class BaseError : NewType { - public class BaseError : NewType + public BaseError(string value) : base(value) { - public BaseError(string value) : base(value) - { - } - - public static implicit operator BaseError(string str) => New(str); } - public static class ErrorExtensions - { - public static BaseError Join(this Seq errors) => string.Join("; ", errors); - } + public static implicit operator BaseError(string str) => New(str); } + +public static class ErrorExtensions +{ + public static BaseError Join(this Seq errors) => string.Join("; ", errors); +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/Channel.cs b/ErsatzTV.Core/Domain/Channel.cs index 13b78ba57..8c80e3030 100644 --- a/ErsatzTV.Core/Domain/Channel.cs +++ b/ErsatzTV.Core/Domain/Channel.cs @@ -1,29 +1,26 @@ -using System; -using System.Collections.Generic; -using ErsatzTV.Core.Domain.Filler; +using ErsatzTV.Core.Domain.Filler; -namespace ErsatzTV.Core.Domain +namespace ErsatzTV.Core.Domain; + +public class Channel { - public class Channel - { - public static string NumberValidator = @"^[0-9]+(\.[0-9])?$"; + public static string NumberValidator = @"^[0-9]+(\.[0-9])?$"; - public Channel(Guid uniqueId) => UniqueId = uniqueId; - public int Id { get; set; } - public Guid UniqueId { get; init; } - public string Number { get; set; } - public string Name { get; set; } - public string Group { get; set; } - public string Categories { get; set; } - public int FFmpegProfileId { get; set; } - public FFmpegProfile FFmpegProfile { get; set; } - public int? WatermarkId { get; set; } - public ChannelWatermark Watermark { get; set; } - public int? FallbackFillerId { get; set; } - public FillerPreset FallbackFiller { get; set; } - public StreamingMode StreamingMode { get; set; } - public List Playouts { get; set; } - public List Artwork { get; set; } - public string PreferredLanguageCode { get; set; } - } -} + public Channel(Guid uniqueId) => UniqueId = uniqueId; + public int Id { get; set; } + public Guid UniqueId { get; init; } + public string Number { get; set; } + public string Name { get; set; } + public string Group { get; set; } + public string Categories { get; set; } + public int FFmpegProfileId { get; set; } + public FFmpegProfile FFmpegProfile { get; set; } + public int? WatermarkId { get; set; } + public ChannelWatermark Watermark { get; set; } + public int? FallbackFillerId { get; set; } + public FillerPreset FallbackFiller { get; set; } + public StreamingMode StreamingMode { get; set; } + public List Playouts { get; set; } + public List Artwork { get; set; } + public string PreferredLanguageCode { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/ChannelWatermark.cs b/ErsatzTV.Core/Domain/ChannelWatermark.cs index 4a94ae328..6f56ce3dd 100644 --- a/ErsatzTV.Core/Domain/ChannelWatermark.cs +++ b/ErsatzTV.Core/Domain/ChannelWatermark.cs @@ -1,34 +1,33 @@ using ErsatzTV.FFmpeg.State; -namespace ErsatzTV.Core.Domain +namespace ErsatzTV.Core.Domain; + +public class ChannelWatermark { - public class ChannelWatermark - { - public int Id { get; set; } - public string Name { get; set; } - public ChannelWatermarkMode Mode { get; set; } - public ChannelWatermarkImageSource ImageSource { get; set; } - public string Image { get; set; } - public WatermarkLocation Location { get; set; } - public WatermarkSize Size { get; set; } - public int WidthPercent { get; set; } - public int HorizontalMarginPercent { get; set; } - public int VerticalMarginPercent { get; set; } - public int FrequencyMinutes { get; set; } - public int DurationSeconds { get; set; } - public int Opacity { get; set; } - } - - public enum ChannelWatermarkMode - { - None = 0, - Permanent = 1, - Intermittent = 2 - } - - public enum ChannelWatermarkImageSource - { - Custom = 0, - ChannelLogo = 1 - } + public int Id { get; set; } + public string Name { get; set; } + public ChannelWatermarkMode Mode { get; set; } + public ChannelWatermarkImageSource ImageSource { get; set; } + public string Image { get; set; } + public WatermarkLocation Location { get; set; } + public WatermarkSize Size { get; set; } + public int WidthPercent { get; set; } + public int HorizontalMarginPercent { get; set; } + public int VerticalMarginPercent { get; set; } + public int FrequencyMinutes { get; set; } + public int DurationSeconds { get; set; } + public int Opacity { get; set; } } + +public enum ChannelWatermarkMode +{ + None = 0, + Permanent = 1, + Intermittent = 2 +} + +public enum ChannelWatermarkImageSource +{ + Custom = 0, + ChannelLogo = 1 +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/Collection/Collection.cs b/ErsatzTV.Core/Domain/Collection/Collection.cs index 19e0ddebb..5dbdb5674 100644 --- a/ErsatzTV.Core/Domain/Collection/Collection.cs +++ b/ErsatzTV.Core/Domain/Collection/Collection.cs @@ -1,15 +1,12 @@ -using System.Collections.Generic; +namespace ErsatzTV.Core.Domain; -namespace ErsatzTV.Core.Domain +public class Collection { - public class Collection - { - public int Id { get; set; } - public string Name { get; set; } - public bool UseCustomPlaybackOrder { get; set; } - public List MediaItems { get; set; } - public List CollectionItems { get; set; } - public List MultiCollections { get; set; } - public List MultiCollectionItems { get; set; } - } -} + public int Id { get; set; } + public string Name { get; set; } + public bool UseCustomPlaybackOrder { get; set; } + public List MediaItems { get; set; } + public List CollectionItems { get; set; } + public List MultiCollections { get; set; } + public List MultiCollectionItems { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/Collection/CollectionItem.cs b/ErsatzTV.Core/Domain/Collection/CollectionItem.cs index af1d04ea7..c66ce687f 100644 --- a/ErsatzTV.Core/Domain/Collection/CollectionItem.cs +++ b/ErsatzTV.Core/Domain/Collection/CollectionItem.cs @@ -1,11 +1,10 @@ -namespace ErsatzTV.Core.Domain +namespace ErsatzTV.Core.Domain; + +public class CollectionItem { - public class CollectionItem - { - public int CollectionId { get; set; } - public Collection Collection { get; set; } - public int MediaItemId { get; set; } - public MediaItem MediaItem { get; set; } - public int? CustomIndex { get; set; } - } -} + public int CollectionId { get; set; } + public Collection Collection { get; set; } + public int MediaItemId { get; set; } + public MediaItem MediaItem { get; set; } + public int? CustomIndex { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/Collection/MultiCollection.cs b/ErsatzTV.Core/Domain/Collection/MultiCollection.cs index dffee695a..ecfab3066 100644 --- a/ErsatzTV.Core/Domain/Collection/MultiCollection.cs +++ b/ErsatzTV.Core/Domain/Collection/MultiCollection.cs @@ -1,14 +1,11 @@ -using System.Collections.Generic; +namespace ErsatzTV.Core.Domain; -namespace ErsatzTV.Core.Domain +public class MultiCollection { - public class MultiCollection - { - public int Id { get; set; } - public string Name { get; set; } - public List Collections { get; set; } - public List SmartCollections { get; set; } - public List MultiCollectionItems { get; set; } - public List MultiCollectionSmartItems { get; set; } - } -} + public int Id { get; set; } + public string Name { get; set; } + public List Collections { get; set; } + public List SmartCollections { get; set; } + public List MultiCollectionItems { get; set; } + public List MultiCollectionSmartItems { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/Collection/MultiCollectionItem.cs b/ErsatzTV.Core/Domain/Collection/MultiCollectionItem.cs index e1a88884c..ce2428992 100644 --- a/ErsatzTV.Core/Domain/Collection/MultiCollectionItem.cs +++ b/ErsatzTV.Core/Domain/Collection/MultiCollectionItem.cs @@ -1,12 +1,11 @@ -namespace ErsatzTV.Core.Domain +namespace ErsatzTV.Core.Domain; + +public class MultiCollectionItem { - public class MultiCollectionItem - { - public int MultiCollectionId { get; set; } - public MultiCollection MultiCollection { get; set; } - public int CollectionId { get; set; } - public Collection Collection { get; set; } - public bool ScheduleAsGroup { get; set; } - public PlaybackOrder PlaybackOrder { get; set; } - } -} + public int MultiCollectionId { get; set; } + public MultiCollection MultiCollection { get; set; } + public int CollectionId { get; set; } + public Collection Collection { get; set; } + public bool ScheduleAsGroup { get; set; } + public PlaybackOrder PlaybackOrder { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/Collection/MultiCollectionSmartItem.cs b/ErsatzTV.Core/Domain/Collection/MultiCollectionSmartItem.cs index 7b98b9f78..7315c4301 100644 --- a/ErsatzTV.Core/Domain/Collection/MultiCollectionSmartItem.cs +++ b/ErsatzTV.Core/Domain/Collection/MultiCollectionSmartItem.cs @@ -1,12 +1,11 @@ -namespace ErsatzTV.Core.Domain +namespace ErsatzTV.Core.Domain; + +public class MultiCollectionSmartItem { - public class MultiCollectionSmartItem - { - public int MultiCollectionId { get; set; } - public MultiCollection MultiCollection { get; set; } - public int SmartCollectionId { get; set; } - public SmartCollection SmartCollection { get; set; } - public bool ScheduleAsGroup { get; set; } - public PlaybackOrder PlaybackOrder { get; set; } - } -} + public int MultiCollectionId { get; set; } + public MultiCollection MultiCollection { get; set; } + public int SmartCollectionId { get; set; } + public SmartCollection SmartCollection { get; set; } + public bool ScheduleAsGroup { get; set; } + public PlaybackOrder PlaybackOrder { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/Collection/SmartCollection.cs b/ErsatzTV.Core/Domain/Collection/SmartCollection.cs index d4604408f..2c05997a7 100644 --- a/ErsatzTV.Core/Domain/Collection/SmartCollection.cs +++ b/ErsatzTV.Core/Domain/Collection/SmartCollection.cs @@ -1,13 +1,10 @@ -using System.Collections.Generic; +namespace ErsatzTV.Core.Domain; -namespace ErsatzTV.Core.Domain +public class SmartCollection { - public class SmartCollection - { - public int Id { get; set; } - public string Name { get; set; } - public string Query { get; set; } - public List MultiCollections { get; set; } - public List MultiCollectionSmartItems { get; set; } - } -} + public int Id { get; set; } + public string Name { get; set; } + public string Query { get; set; } + public List MultiCollections { get; set; } + public List MultiCollectionSmartItems { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/Collection/TraktList.cs b/ErsatzTV.Core/Domain/Collection/TraktList.cs index 5057a9841..d88881d28 100644 --- a/ErsatzTV.Core/Domain/Collection/TraktList.cs +++ b/ErsatzTV.Core/Domain/Collection/TraktList.cs @@ -1,16 +1,13 @@ -using System.Collections.Generic; +namespace ErsatzTV.Core.Domain; -namespace ErsatzTV.Core.Domain +public class TraktList { - public class TraktList - { - public int Id { get; set; } - public int TraktId { get; set; } - public string User { get; set; } - public string List { get; set; } - public string Name { get; set; } - public string Description { get; set; } - public int ItemCount { get; set; } - public List Items { get; set; } - } -} + public int Id { get; set; } + public int TraktId { get; set; } + public string User { get; set; } + public string List { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public int ItemCount { get; set; } + public List Items { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/Collection/TraktListItem.cs b/ErsatzTV.Core/Domain/Collection/TraktListItem.cs index bf0d91c2d..ed6d76ef4 100644 --- a/ErsatzTV.Core/Domain/Collection/TraktListItem.cs +++ b/ErsatzTV.Core/Domain/Collection/TraktListItem.cs @@ -1,31 +1,28 @@ -using System.Collections.Generic; +namespace ErsatzTV.Core.Domain; -namespace ErsatzTV.Core.Domain +public class TraktListItem { - public class TraktListItem + public int Id { get; set; } + public int TraktListId { get; set; } + public TraktList TraktList { get; set; } + + public TraktListItemKind Kind { get; set; } + public int TraktId { get; set; } + public int Rank { get; set; } + public string Title { get; set; } + public int? Year { get; set; } + public int? Season { get; set; } + public int? Episode { get; set; } + public List Guids { get; set; } + + public int? MediaItemId { get; set; } + public MediaItem MediaItem { get; set; } + + public string DisplayTitle => Kind switch { - public int Id { get; set; } - public int TraktListId { get; set; } - public TraktList TraktList { get; set; } - - public TraktListItemKind Kind { get; set; } - public int TraktId { get; set; } - public int Rank { get; set; } - public string Title { get; set; } - public int? Year { get; set; } - public int? Season { get; set; } - public int? Episode { get; set; } - public List Guids { get; set; } - - public int? MediaItemId { get; set; } - public MediaItem MediaItem { get; set; } - - public string DisplayTitle => Kind switch - { - TraktListItemKind.Movie => $"{Title} ({Year})", - TraktListItemKind.Show => $"{Title} ({Year})", - TraktListItemKind.Season => $"{Title} ({Year}) S{Season:00}", - _ => $"{Title} ({Year}) S{Season:00}E{Episode:00}" - }; - } -} + TraktListItemKind.Movie => $"{Title} ({Year})", + TraktListItemKind.Show => $"{Title} ({Year})", + TraktListItemKind.Season => $"{Title} ({Year}) S{Season:00}", + _ => $"{Title} ({Year}) S{Season:00}E{Episode:00}" + }; +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/Collection/TraktListItemGuid.cs b/ErsatzTV.Core/Domain/Collection/TraktListItemGuid.cs index d5652d8af..a60007157 100644 --- a/ErsatzTV.Core/Domain/Collection/TraktListItemGuid.cs +++ b/ErsatzTV.Core/Domain/Collection/TraktListItemGuid.cs @@ -1,10 +1,9 @@ -namespace ErsatzTV.Core.Domain +namespace ErsatzTV.Core.Domain; + +public class TraktListItemGuid { - public class TraktListItemGuid - { - public int Id { get; set; } - public string Guid { get; set; } - public int TraktListItemId { get; set; } - public TraktListItem TraktListItem { get; set; } - } -} + public int Id { get; set; } + public string Guid { get; set; } + public int TraktListItemId { get; set; } + public TraktListItem TraktListItem { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/Collection/TraktListItemKind.cs b/ErsatzTV.Core/Domain/Collection/TraktListItemKind.cs index 8a304694b..1c2d6035d 100644 --- a/ErsatzTV.Core/Domain/Collection/TraktListItemKind.cs +++ b/ErsatzTV.Core/Domain/Collection/TraktListItemKind.cs @@ -1,10 +1,9 @@ -namespace ErsatzTV.Core.Domain +namespace ErsatzTV.Core.Domain; + +public enum TraktListItemKind { - public enum TraktListItemKind - { - Movie, - Show, - Season, - Episode - } -} + Movie, + Show, + Season, + Episode +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/CollectionEnumeratorState.cs b/ErsatzTV.Core/Domain/CollectionEnumeratorState.cs index 9f843f193..5740d7c58 100644 --- a/ErsatzTV.Core/Domain/CollectionEnumeratorState.cs +++ b/ErsatzTV.Core/Domain/CollectionEnumeratorState.cs @@ -1,8 +1,7 @@ -namespace ErsatzTV.Core.Domain +namespace ErsatzTV.Core.Domain; + +public class CollectionEnumeratorState { - public class CollectionEnumeratorState - { - public int Seed { get; set; } - public int Index { get; set; } - } -} + public int Seed { get; set; } + public int Index { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/ConfigElement.cs b/ErsatzTV.Core/Domain/ConfigElement.cs index b410b55c0..258f21a66 100644 --- a/ErsatzTV.Core/Domain/ConfigElement.cs +++ b/ErsatzTV.Core/Domain/ConfigElement.cs @@ -1,9 +1,8 @@ -namespace ErsatzTV.Core.Domain +namespace ErsatzTV.Core.Domain; + +public class ConfigElement { - public class ConfigElement - { - public int Id { get; set; } - public string Key { get; set; } - public string Value { get; set; } - } -} + public int Id { get; set; } + public string Key { get; set; } + public string Value { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/ConfigElementKey.cs b/ErsatzTV.Core/Domain/ConfigElementKey.cs index f1e97145e..34c5f72a9 100644 --- a/ErsatzTV.Core/Domain/ConfigElementKey.cs +++ b/ErsatzTV.Core/Domain/ConfigElementKey.cs @@ -1,38 +1,37 @@ -namespace ErsatzTV.Core.Domain +namespace ErsatzTV.Core.Domain; + +public class ConfigElementKey { - public class ConfigElementKey - { - private ConfigElementKey(string key) => Key = key; + private ConfigElementKey(string key) => Key = key; - public string Key { get; } + public string Key { get; } - public static ConfigElementKey FFmpegPath => new("ffmpeg.ffmpeg_path"); - public static ConfigElementKey FFprobePath => new("ffmpeg.ffprobe_path"); - public static ConfigElementKey FFmpegDefaultProfileId => new("ffmpeg.default_profile_id"); - public static ConfigElementKey FFmpegDefaultResolutionId => new("ffmpeg.default_resolution_id"); - public static ConfigElementKey FFmpegSaveReports => new("ffmpeg.save_reports"); - public static ConfigElementKey FFmpegPreferredLanguageCode => new("ffmpeg.preferred_language_code"); - public static ConfigElementKey FFmpegGlobalWatermarkId => new("ffmpeg.global_watermark_id"); - public static ConfigElementKey FFmpegGlobalFallbackFillerId => new("ffmpeg.global_fallback_filler_id"); - public static ConfigElementKey FFmpegSegmenterTimeout => new("ffmpeg.segmenter.timeout_seconds"); - public static ConfigElementKey FFmpegWorkAheadSegmenters => new("ffmpeg.segmenter.work_ahead_limit"); - public static ConfigElementKey FFmpegInitialSegmentCount => new("ffmpeg.segmenter.initial_segment_count"); - public static ConfigElementKey FFmpegUseLegacyTranscoder => new("ffmpeg.use_legacy_transcoder"); - public static ConfigElementKey SearchIndexVersion => new("search_index.version"); - public static ConfigElementKey HDHRTunerCount => new("hdhr.tuner_count"); - public static ConfigElementKey ChannelsPageSize => new("pages.channels.page_size"); - public static ConfigElementKey CollectionsPageSize => new("pages.collections.page_size"); - public static ConfigElementKey MultiCollectionsPageSize => new("pages.multi_collections.page_size"); - public static ConfigElementKey SmartCollectionsPageSize => new("pages.smart_collections.page_size"); - public static ConfigElementKey SchedulesPageSize => new("pages.schedules.page_size"); - public static ConfigElementKey SchedulesDetailPageSize => new("pages.schedules.detail_page_size"); - public static ConfigElementKey PlayoutsPageSize => new("pages.playouts.page_size"); - public static ConfigElementKey PlayoutsDetailPageSize => new("pages.playouts.detail_page_size"); - public static ConfigElementKey PlayoutsDetailShowFiller => new("pages.playouts.detail_show_filler"); - public static ConfigElementKey LogsPageSize => new("pages.logs.page_size"); - public static ConfigElementKey TraktListsPageSize => new("pages.trakt.lists_page_size"); - public static ConfigElementKey FillerPresetsPageSize => new("pages.filler_presets.page_size"); - public static ConfigElementKey LibraryRefreshInterval => new("scanner.library_refresh_interval"); - public static ConfigElementKey PlayoutDaysToBuild => new("playout.days_to_build"); - } -} + public static ConfigElementKey FFmpegPath => new("ffmpeg.ffmpeg_path"); + public static ConfigElementKey FFprobePath => new("ffmpeg.ffprobe_path"); + public static ConfigElementKey FFmpegDefaultProfileId => new("ffmpeg.default_profile_id"); + public static ConfigElementKey FFmpegDefaultResolutionId => new("ffmpeg.default_resolution_id"); + public static ConfigElementKey FFmpegSaveReports => new("ffmpeg.save_reports"); + public static ConfigElementKey FFmpegPreferredLanguageCode => new("ffmpeg.preferred_language_code"); + public static ConfigElementKey FFmpegGlobalWatermarkId => new("ffmpeg.global_watermark_id"); + public static ConfigElementKey FFmpegGlobalFallbackFillerId => new("ffmpeg.global_fallback_filler_id"); + public static ConfigElementKey FFmpegSegmenterTimeout => new("ffmpeg.segmenter.timeout_seconds"); + public static ConfigElementKey FFmpegWorkAheadSegmenters => new("ffmpeg.segmenter.work_ahead_limit"); + public static ConfigElementKey FFmpegInitialSegmentCount => new("ffmpeg.segmenter.initial_segment_count"); + public static ConfigElementKey FFmpegUseLegacyTranscoder => new("ffmpeg.use_legacy_transcoder"); + public static ConfigElementKey SearchIndexVersion => new("search_index.version"); + public static ConfigElementKey HDHRTunerCount => new("hdhr.tuner_count"); + public static ConfigElementKey ChannelsPageSize => new("pages.channels.page_size"); + public static ConfigElementKey CollectionsPageSize => new("pages.collections.page_size"); + public static ConfigElementKey MultiCollectionsPageSize => new("pages.multi_collections.page_size"); + public static ConfigElementKey SmartCollectionsPageSize => new("pages.smart_collections.page_size"); + public static ConfigElementKey SchedulesPageSize => new("pages.schedules.page_size"); + public static ConfigElementKey SchedulesDetailPageSize => new("pages.schedules.detail_page_size"); + public static ConfigElementKey PlayoutsPageSize => new("pages.playouts.page_size"); + public static ConfigElementKey PlayoutsDetailPageSize => new("pages.playouts.detail_page_size"); + public static ConfigElementKey PlayoutsDetailShowFiller => new("pages.playouts.detail_show_filler"); + public static ConfigElementKey LogsPageSize => new("pages.logs.page_size"); + public static ConfigElementKey TraktListsPageSize => new("pages.trakt.lists_page_size"); + public static ConfigElementKey FillerPresetsPageSize => new("pages.filler_presets.page_size"); + public static ConfigElementKey LibraryRefreshInterval => new("scanner.library_refresh_interval"); + public static ConfigElementKey PlayoutDaysToBuild => new("playout.days_to_build"); +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/FFmpegProfile.cs b/ErsatzTV.Core/Domain/FFmpegProfile.cs index e13025d37..4072b5083 100644 --- a/ErsatzTV.Core/Domain/FFmpegProfile.cs +++ b/ErsatzTV.Core/Domain/FFmpegProfile.cs @@ -1,51 +1,50 @@ using ErsatzTV.Core.FFmpeg; -namespace ErsatzTV.Core.Domain -{ - public record FFmpegProfile - { - public int Id { get; set; } - public string Name { get; set; } - public int ThreadCount { get; set; } - public bool Transcode { get; set; } - public HardwareAccelerationKind HardwareAcceleration { get; set; } - public VaapiDriver VaapiDriver { get; set; } - public string VaapiDevice { get; set; } - public int ResolutionId { get; set; } - public Resolution Resolution { get; set; } - public string VideoCodec { get; set; } - public bool NormalizeVideo { get; set; } - public int VideoBitrate { get; set; } - public int VideoBufferSize { get; set; } - public string AudioCodec { get; set; } - public int AudioBitrate { get; set; } - public int AudioBufferSize { get; set; } - public bool NormalizeLoudness { get; set; } - public int AudioChannels { get; set; } - public int AudioSampleRate { get; set; } - public bool NormalizeAudio { get; set; } - public bool NormalizeFramerate { get; set; } +namespace ErsatzTV.Core.Domain; - public static FFmpegProfile New(string name, Resolution resolution) => - new() - { - Name = name, - ThreadCount = 0, - Transcode = true, - ResolutionId = resolution.Id, - Resolution = resolution, - VideoCodec = "libx264", - AudioCodec = "ac3", - VideoBitrate = 2000, - VideoBufferSize = 4000, - AudioBitrate = 192, - AudioBufferSize = 384, - NormalizeLoudness = true, - AudioChannels = 2, - AudioSampleRate = 48, - NormalizeVideo = true, - NormalizeAudio = true, - HardwareAcceleration = HardwareAccelerationKind.None - }; - } -} +public record FFmpegProfile +{ + public int Id { get; set; } + public string Name { get; set; } + public int ThreadCount { get; set; } + public bool Transcode { get; set; } + public HardwareAccelerationKind HardwareAcceleration { get; set; } + public VaapiDriver VaapiDriver { get; set; } + public string VaapiDevice { get; set; } + public int ResolutionId { get; set; } + public Resolution Resolution { get; set; } + public string VideoCodec { get; set; } + public bool NormalizeVideo { get; set; } + public int VideoBitrate { get; set; } + public int VideoBufferSize { get; set; } + public string AudioCodec { get; set; } + public int AudioBitrate { get; set; } + public int AudioBufferSize { get; set; } + public bool NormalizeLoudness { get; set; } + public int AudioChannels { get; set; } + public int AudioSampleRate { get; set; } + public bool NormalizeAudio { get; set; } + public bool NormalizeFramerate { get; set; } + + public static FFmpegProfile New(string name, Resolution resolution) => + new() + { + Name = name, + ThreadCount = 0, + Transcode = true, + ResolutionId = resolution.Id, + Resolution = resolution, + VideoCodec = "libx264", + AudioCodec = "ac3", + VideoBitrate = 2000, + VideoBufferSize = 4000, + AudioBitrate = 192, + AudioBufferSize = 384, + NormalizeLoudness = true, + AudioChannels = 2, + AudioSampleRate = 48, + NormalizeVideo = true, + NormalizeAudio = true, + HardwareAcceleration = HardwareAccelerationKind.None + }; +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/Filler/FillerKind.cs b/ErsatzTV.Core/Domain/Filler/FillerKind.cs index 2d2242a4b..7f140b20f 100644 --- a/ErsatzTV.Core/Domain/Filler/FillerKind.cs +++ b/ErsatzTV.Core/Domain/Filler/FillerKind.cs @@ -1,12 +1,11 @@ -namespace ErsatzTV.Core.Domain.Filler +namespace ErsatzTV.Core.Domain.Filler; + +public enum FillerKind { - public enum FillerKind - { - None = 0, - PreRoll = 1, - MidRoll = 2, - PostRoll = 3, - Tail = 4, - Fallback = 5 - } -} + None = 0, + PreRoll = 1, + MidRoll = 2, + PostRoll = 3, + Tail = 4, + Fallback = 5 +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/Filler/FillerMode.cs b/ErsatzTV.Core/Domain/Filler/FillerMode.cs index b9360667d..7a086c93a 100644 --- a/ErsatzTV.Core/Domain/Filler/FillerMode.cs +++ b/ErsatzTV.Core/Domain/Filler/FillerMode.cs @@ -1,10 +1,9 @@ -namespace ErsatzTV.Core.Domain.Filler +namespace ErsatzTV.Core.Domain.Filler; + +public enum FillerMode { - public enum FillerMode - { - None = 0, - Duration = 1, - Count = 2, - Pad = 3 - } -} + None = 0, + Duration = 1, + Count = 2, + Pad = 3 +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/Filler/FillerPreset.cs b/ErsatzTV.Core/Domain/Filler/FillerPreset.cs index 84565a212..542c54532 100644 --- a/ErsatzTV.Core/Domain/Filler/FillerPreset.cs +++ b/ErsatzTV.Core/Domain/Filler/FillerPreset.cs @@ -1,24 +1,21 @@ -using System; +namespace ErsatzTV.Core.Domain.Filler; -namespace ErsatzTV.Core.Domain.Filler +public class FillerPreset { - public class FillerPreset - { - public int Id { get; set; } - public string Name { get; set; } - public FillerKind FillerKind { get; set; } - public FillerMode FillerMode { get; set; } - public TimeSpan? Duration { get; set; } - public int? Count { get; set; } - public int? PadToNearestMinute { get; set; } - public ProgramScheduleItemCollectionType CollectionType { get; set; } - public int? CollectionId { get; set; } - public Collection Collection { get; set; } - public int? MediaItemId { get; set; } - public MediaItem MediaItem { get; set; } - public int? MultiCollectionId { get; set; } - public MultiCollection MultiCollection { get; set; } - public int? SmartCollectionId { get; set; } - public SmartCollection SmartCollection { get; set; } - } -} + public int Id { get; set; } + public string Name { get; set; } + public FillerKind FillerKind { get; set; } + public FillerMode FillerMode { get; set; } + public TimeSpan? Duration { get; set; } + public int? Count { get; set; } + public int? PadToNearestMinute { get; set; } + public ProgramScheduleItemCollectionType CollectionType { get; set; } + public int? CollectionId { get; set; } + public Collection Collection { get; set; } + public int? MediaItemId { get; set; } + public MediaItem MediaItem { get; set; } + public int? MultiCollectionId { get; set; } + public MultiCollection MultiCollection { get; set; } + public int? SmartCollectionId { get; set; } + public SmartCollection SmartCollection { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/GuideMode.cs b/ErsatzTV.Core/Domain/GuideMode.cs index d3c4265ab..4c800e8ab 100644 --- a/ErsatzTV.Core/Domain/GuideMode.cs +++ b/ErsatzTV.Core/Domain/GuideMode.cs @@ -1,8 +1,7 @@ -namespace ErsatzTV.Core.Domain +namespace ErsatzTV.Core.Domain; + +public enum GuideMode { - public enum GuideMode - { - Normal = 0, - Filler = 1 - } -} + Normal = 0, + Filler = 1 +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/HardwareAccelerationKind.cs b/ErsatzTV.Core/Domain/HardwareAccelerationKind.cs index b63b223e5..c987b464e 100644 --- a/ErsatzTV.Core/Domain/HardwareAccelerationKind.cs +++ b/ErsatzTV.Core/Domain/HardwareAccelerationKind.cs @@ -1,11 +1,10 @@ -namespace ErsatzTV.Core.Domain +namespace ErsatzTV.Core.Domain; + +public enum HardwareAccelerationKind { - public enum HardwareAccelerationKind - { - None = 0, - Qsv = 1, - Nvenc = 2, - Vaapi = 3, - VideoToolbox = 4 - } -} + None = 0, + Qsv = 1, + Nvenc = 2, + Vaapi = 3, + VideoToolbox = 4 +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/LanguageCode.cs b/ErsatzTV.Core/Domain/LanguageCode.cs index f9411631a..3bd52ead5 100644 --- a/ErsatzTV.Core/Domain/LanguageCode.cs +++ b/ErsatzTV.Core/Domain/LanguageCode.cs @@ -1,12 +1,11 @@ -namespace ErsatzTV.Core.Domain +namespace ErsatzTV.Core.Domain; + +public class LanguageCode { - public class LanguageCode - { - public int Id { get; set; } - public string ThreeCode1 { get; set; } - public string ThreeCode2 { get; set; } - public string TwoCode { get; set; } - public string EnglishName { get; set; } - public string FrenchName { get; set; } - } -} + public int Id { get; set; } + public string ThreeCode1 { get; set; } + public string ThreeCode2 { get; set; } + public string TwoCode { get; set; } + public string EnglishName { get; set; } + public string FrenchName { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/Library/EmbyLibrary.cs b/ErsatzTV.Core/Domain/Library/EmbyLibrary.cs index 13622f29b..c4b55d4a2 100644 --- a/ErsatzTV.Core/Domain/Library/EmbyLibrary.cs +++ b/ErsatzTV.Core/Domain/Library/EmbyLibrary.cs @@ -1,8 +1,7 @@ -namespace ErsatzTV.Core.Domain +namespace ErsatzTV.Core.Domain; + +public class EmbyLibrary : Library { - public class EmbyLibrary : Library - { - public string ItemId { get; set; } - public bool ShouldSyncItems { get; set; } - } -} + public string ItemId { get; set; } + public bool ShouldSyncItems { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/Library/JellyfinLibrary.cs b/ErsatzTV.Core/Domain/Library/JellyfinLibrary.cs index 23d8e5aeb..a7729edcf 100644 --- a/ErsatzTV.Core/Domain/Library/JellyfinLibrary.cs +++ b/ErsatzTV.Core/Domain/Library/JellyfinLibrary.cs @@ -1,8 +1,7 @@ -namespace ErsatzTV.Core.Domain +namespace ErsatzTV.Core.Domain; + +public class JellyfinLibrary : Library { - public class JellyfinLibrary : Library - { - public string ItemId { get; set; } - public bool ShouldSyncItems { get; set; } - } -} + public string ItemId { get; set; } + public bool ShouldSyncItems { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/Library/Library.cs b/ErsatzTV.Core/Domain/Library/Library.cs index 5344357bb..1b447d724 100644 --- a/ErsatzTV.Core/Domain/Library/Library.cs +++ b/ErsatzTV.Core/Domain/Library/Library.cs @@ -1,18 +1,14 @@ -using System; -using System.Collections.Generic; +namespace ErsatzTV.Core.Domain; -namespace ErsatzTV.Core.Domain +public abstract class Library { - public abstract class Library - { - public int Id { get; set; } - public string Name { get; set; } - public LibraryMediaKind MediaKind { get; set; } - public DateTime? LastScan { get; set; } + public int Id { get; set; } + public string Name { get; set; } + public LibraryMediaKind MediaKind { get; set; } + public DateTime? LastScan { get; set; } - public int MediaSourceId { get; set; } - public MediaSource MediaSource { get; set; } + public int MediaSourceId { get; set; } + public MediaSource MediaSource { get; set; } - public List Paths { get; set; } - } -} + public List Paths { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/Library/LibraryFolder.cs b/ErsatzTV.Core/Domain/Library/LibraryFolder.cs index 6860ba3a4..8c37aeb43 100644 --- a/ErsatzTV.Core/Domain/Library/LibraryFolder.cs +++ b/ErsatzTV.Core/Domain/Library/LibraryFolder.cs @@ -1,11 +1,10 @@ -namespace ErsatzTV.Core.Domain +namespace ErsatzTV.Core.Domain; + +public class LibraryFolder { - public class LibraryFolder - { - public int Id { get; set; } - public string Path { get; set; } - public int LibraryPathId { get; set; } - public LibraryPath LibraryPath { get; set; } - public string Etag { get; set; } - } -} + public int Id { get; set; } + public string Path { get; set; } + public int LibraryPathId { get; set; } + public LibraryPath LibraryPath { get; set; } + public string Etag { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/Library/LibraryMediaKind.cs b/ErsatzTV.Core/Domain/Library/LibraryMediaKind.cs index dc55f45e9..7b36cd9f1 100644 --- a/ErsatzTV.Core/Domain/Library/LibraryMediaKind.cs +++ b/ErsatzTV.Core/Domain/Library/LibraryMediaKind.cs @@ -1,11 +1,10 @@ -namespace ErsatzTV.Core.Domain +namespace ErsatzTV.Core.Domain; + +public enum LibraryMediaKind { - public enum LibraryMediaKind - { - Movies = 1, - Shows = 2, - MusicVideos = 3, - OtherVideos = 4, - Songs = 5 - } -} + Movies = 1, + Shows = 2, + MusicVideos = 3, + OtherVideos = 4, + Songs = 5 +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/Library/LibraryPath.cs b/ErsatzTV.Core/Domain/Library/LibraryPath.cs index 291d62004..060e75f1a 100644 --- a/ErsatzTV.Core/Domain/Library/LibraryPath.cs +++ b/ErsatzTV.Core/Domain/Library/LibraryPath.cs @@ -1,18 +1,14 @@ -using System; -using System.Collections.Generic; +namespace ErsatzTV.Core.Domain; -namespace ErsatzTV.Core.Domain +public class LibraryPath { - public class LibraryPath - { - public int Id { get; set; } - public string Path { get; set; } - public DateTime? LastScan { get; set; } + public int Id { get; set; } + public string Path { get; set; } + public DateTime? LastScan { get; set; } - public int LibraryId { get; set; } - public Library Library { get; set; } + public int LibraryId { get; set; } + public Library Library { get; set; } - public List MediaItems { get; set; } - public List LibraryFolders { get; set; } - } -} + public List MediaItems { get; set; } + public List LibraryFolders { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/Library/LocalLibrary.cs b/ErsatzTV.Core/Domain/Library/LocalLibrary.cs index 787f0df50..1213f9ea7 100644 --- a/ErsatzTV.Core/Domain/Library/LocalLibrary.cs +++ b/ErsatzTV.Core/Domain/Library/LocalLibrary.cs @@ -1,6 +1,5 @@ -namespace ErsatzTV.Core.Domain +namespace ErsatzTV.Core.Domain; + +public class LocalLibrary : Library { - public class LocalLibrary : Library - { - } -} +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/Library/PlexLibrary.cs b/ErsatzTV.Core/Domain/Library/PlexLibrary.cs index bbd35c13e..acf403d7c 100644 --- a/ErsatzTV.Core/Domain/Library/PlexLibrary.cs +++ b/ErsatzTV.Core/Domain/Library/PlexLibrary.cs @@ -1,8 +1,7 @@ -namespace ErsatzTV.Core.Domain +namespace ErsatzTV.Core.Domain; + +public class PlexLibrary : Library { - public class PlexLibrary : Library - { - public string Key { get; set; } - public bool ShouldSyncItems { get; set; } - } -} + public string Key { get; set; } + public bool ShouldSyncItems { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/LogEntry.cs b/ErsatzTV.Core/Domain/LogEntry.cs index 6685e8341..1d29ea975 100644 --- a/ErsatzTV.Core/Domain/LogEntry.cs +++ b/ErsatzTV.Core/Domain/LogEntry.cs @@ -1,12 +1,9 @@ -using System; +namespace ErsatzTV.Core.Domain; -namespace ErsatzTV.Core.Domain -{ - public record LogEntry( - int Id, - DateTime Timestamp, - string Level, - string Exception, - string RenderedMessage, - string Properties); -} +public record LogEntry( + int Id, + DateTime Timestamp, + string Level, + string Exception, + string RenderedMessage, + string Properties); \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/MediaItem/Artist.cs b/ErsatzTV.Core/Domain/MediaItem/Artist.cs index c30cc2d2a..d16a6e1ed 100644 --- a/ErsatzTV.Core/Domain/MediaItem/Artist.cs +++ b/ErsatzTV.Core/Domain/MediaItem/Artist.cs @@ -1,10 +1,7 @@ -using System.Collections.Generic; +namespace ErsatzTV.Core.Domain; -namespace ErsatzTV.Core.Domain +public class Artist : MediaItem { - public class Artist : MediaItem - { - public List MusicVideos { get; set; } - public List ArtistMetadata { get; set; } - } -} + public List MusicVideos { get; set; } + public List ArtistMetadata { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/MediaItem/BackgroundImageMediaVersion.cs b/ErsatzTV.Core/Domain/MediaItem/BackgroundImageMediaVersion.cs index 480cfdd04..01ce6395b 100644 --- a/ErsatzTV.Core/Domain/MediaItem/BackgroundImageMediaVersion.cs +++ b/ErsatzTV.Core/Domain/MediaItem/BackgroundImageMediaVersion.cs @@ -1,6 +1,5 @@ -namespace ErsatzTV.Core.Domain +namespace ErsatzTV.Core.Domain; + +public class BackgroundImageMediaVersion : MediaVersion { - public class BackgroundImageMediaVersion : MediaVersion - { - } -} +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/MediaItem/CoverArtMediaVersion.cs b/ErsatzTV.Core/Domain/MediaItem/CoverArtMediaVersion.cs index 16d26ae6e..bcd1adcfa 100644 --- a/ErsatzTV.Core/Domain/MediaItem/CoverArtMediaVersion.cs +++ b/ErsatzTV.Core/Domain/MediaItem/CoverArtMediaVersion.cs @@ -1,6 +1,5 @@ -namespace ErsatzTV.Core.Domain +namespace ErsatzTV.Core.Domain; + +public class CoverArtMediaVersion : MediaVersion { - public class CoverArtMediaVersion : MediaVersion - { - } -} +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/MediaItem/EmbyEpisode.cs b/ErsatzTV.Core/Domain/MediaItem/EmbyEpisode.cs index 482096070..dd25bcd1f 100644 --- a/ErsatzTV.Core/Domain/MediaItem/EmbyEpisode.cs +++ b/ErsatzTV.Core/Domain/MediaItem/EmbyEpisode.cs @@ -1,11 +1,10 @@ using System.Diagnostics; -namespace ErsatzTV.Core.Domain +namespace ErsatzTV.Core.Domain; + +[DebuggerDisplay("{EpisodeMetadata[0].Title}")] +public class EmbyEpisode : Episode { - [DebuggerDisplay("{EpisodeMetadata[0].Title}")] - public class EmbyEpisode : Episode - { - public string ItemId { get; set; } - public string Etag { get; set; } - } -} + public string ItemId { get; set; } + public string Etag { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/MediaItem/EmbyMovie.cs b/ErsatzTV.Core/Domain/MediaItem/EmbyMovie.cs index a82b6b15b..4c888813c 100644 --- a/ErsatzTV.Core/Domain/MediaItem/EmbyMovie.cs +++ b/ErsatzTV.Core/Domain/MediaItem/EmbyMovie.cs @@ -1,8 +1,7 @@ -namespace ErsatzTV.Core.Domain +namespace ErsatzTV.Core.Domain; + +public class EmbyMovie : Movie { - public class EmbyMovie : Movie - { - public string ItemId { get; set; } - public string Etag { get; set; } - } -} + public string ItemId { get; set; } + public string Etag { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/MediaItem/EmbySeason.cs b/ErsatzTV.Core/Domain/MediaItem/EmbySeason.cs index cdd7e7039..07bb5f952 100644 --- a/ErsatzTV.Core/Domain/MediaItem/EmbySeason.cs +++ b/ErsatzTV.Core/Domain/MediaItem/EmbySeason.cs @@ -1,8 +1,7 @@ -namespace ErsatzTV.Core.Domain +namespace ErsatzTV.Core.Domain; + +public class EmbySeason : Season { - public class EmbySeason : Season - { - public string ItemId { get; set; } - public string Etag { get; set; } - } -} + public string ItemId { get; set; } + public string Etag { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/MediaItem/EmbyShow.cs b/ErsatzTV.Core/Domain/MediaItem/EmbyShow.cs index 0ba9dd3f0..15929b185 100644 --- a/ErsatzTV.Core/Domain/MediaItem/EmbyShow.cs +++ b/ErsatzTV.Core/Domain/MediaItem/EmbyShow.cs @@ -1,8 +1,7 @@ -namespace ErsatzTV.Core.Domain +namespace ErsatzTV.Core.Domain; + +public class EmbyShow : Show { - public class EmbyShow : Show - { - public string ItemId { get; set; } - public string Etag { get; set; } - } -} + public string ItemId { get; set; } + public string Etag { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/MediaItem/Episode.cs b/ErsatzTV.Core/Domain/MediaItem/Episode.cs index a7f36508b..d8ffbd794 100644 --- a/ErsatzTV.Core/Domain/MediaItem/Episode.cs +++ b/ErsatzTV.Core/Domain/MediaItem/Episode.cs @@ -1,14 +1,12 @@ -using System.Collections.Generic; -using System.Diagnostics; +using System.Diagnostics; -namespace ErsatzTV.Core.Domain +namespace ErsatzTV.Core.Domain; + +[DebuggerDisplay("{EpisodeMetadata[0].Title ?? \"[unknown episode]\"}")] +public class Episode : MediaItem { - [DebuggerDisplay("{EpisodeMetadata[0].Title ?? \"[unknown episode]\"}")] - public class Episode : MediaItem - { - public int SeasonId { get; set; } - public Season Season { get; set; } - public List EpisodeMetadata { get; set; } - public List MediaVersions { get; set; } - } -} + public int SeasonId { get; set; } + public Season Season { get; set; } + public List EpisodeMetadata { get; set; } + public List MediaVersions { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/MediaItem/FallbackMediaVersion.cs b/ErsatzTV.Core/Domain/MediaItem/FallbackMediaVersion.cs index 56a543cfe..5c7824343 100644 --- a/ErsatzTV.Core/Domain/MediaItem/FallbackMediaVersion.cs +++ b/ErsatzTV.Core/Domain/MediaItem/FallbackMediaVersion.cs @@ -1,6 +1,5 @@ -namespace ErsatzTV.Core.Domain +namespace ErsatzTV.Core.Domain; + +public class FallbackMediaVersion : MediaVersion { - public class FallbackMediaVersion : MediaVersion - { - } -} +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/MediaItem/JellyfinEpisode.cs b/ErsatzTV.Core/Domain/MediaItem/JellyfinEpisode.cs index 5c755cf2d..b79b27a28 100644 --- a/ErsatzTV.Core/Domain/MediaItem/JellyfinEpisode.cs +++ b/ErsatzTV.Core/Domain/MediaItem/JellyfinEpisode.cs @@ -1,11 +1,10 @@ using System.Diagnostics; -namespace ErsatzTV.Core.Domain +namespace ErsatzTV.Core.Domain; + +[DebuggerDisplay("{EpisodeMetadata[0].Title}")] +public class JellyfinEpisode : Episode { - [DebuggerDisplay("{EpisodeMetadata[0].Title}")] - public class JellyfinEpisode : Episode - { - public string ItemId { get; set; } - public string Etag { get; set; } - } -} + public string ItemId { get; set; } + public string Etag { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/MediaItem/JellyfinMovie.cs b/ErsatzTV.Core/Domain/MediaItem/JellyfinMovie.cs index cec2f238d..d5a955e97 100644 --- a/ErsatzTV.Core/Domain/MediaItem/JellyfinMovie.cs +++ b/ErsatzTV.Core/Domain/MediaItem/JellyfinMovie.cs @@ -1,8 +1,7 @@ -namespace ErsatzTV.Core.Domain +namespace ErsatzTV.Core.Domain; + +public class JellyfinMovie : Movie { - public class JellyfinMovie : Movie - { - public string ItemId { get; set; } - public string Etag { get; set; } - } -} + public string ItemId { get; set; } + public string Etag { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/MediaItem/JellyfinSeason.cs b/ErsatzTV.Core/Domain/MediaItem/JellyfinSeason.cs index c8f603fc6..7dda603ee 100644 --- a/ErsatzTV.Core/Domain/MediaItem/JellyfinSeason.cs +++ b/ErsatzTV.Core/Domain/MediaItem/JellyfinSeason.cs @@ -1,8 +1,7 @@ -namespace ErsatzTV.Core.Domain +namespace ErsatzTV.Core.Domain; + +public class JellyfinSeason : Season { - public class JellyfinSeason : Season - { - public string ItemId { get; set; } - public string Etag { get; set; } - } -} + public string ItemId { get; set; } + public string Etag { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/MediaItem/JellyfinShow.cs b/ErsatzTV.Core/Domain/MediaItem/JellyfinShow.cs index a6ae29d80..11d1b2233 100644 --- a/ErsatzTV.Core/Domain/MediaItem/JellyfinShow.cs +++ b/ErsatzTV.Core/Domain/MediaItem/JellyfinShow.cs @@ -1,8 +1,7 @@ -namespace ErsatzTV.Core.Domain +namespace ErsatzTV.Core.Domain; + +public class JellyfinShow : Show { - public class JellyfinShow : Show - { - public string ItemId { get; set; } - public string Etag { get; set; } - } -} + public string ItemId { get; set; } + public string Etag { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/MediaItem/MediaChapter.cs b/ErsatzTV.Core/Domain/MediaItem/MediaChapter.cs index 53560452f..566f366c9 100644 --- a/ErsatzTV.Core/Domain/MediaItem/MediaChapter.cs +++ b/ErsatzTV.Core/Domain/MediaItem/MediaChapter.cs @@ -1,15 +1,12 @@ -using System; +namespace ErsatzTV.Core.Domain; -namespace ErsatzTV.Core.Domain +public class MediaChapter { - public class MediaChapter - { - public int Id { get; set; } - public int MediaVersionId { get; set; } - public MediaVersion MediaVersion { get; set; } - public long ChapterId { get; set; } - public TimeSpan StartTime { get; set; } - public TimeSpan EndTime { get; set; } - public string Title { get; set; } - } -} + public int Id { get; set; } + public int MediaVersionId { get; set; } + public MediaVersion MediaVersion { get; set; } + public long ChapterId { get; set; } + public TimeSpan StartTime { get; set; } + public TimeSpan EndTime { get; set; } + public string Title { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/MediaItem/MediaFile.cs b/ErsatzTV.Core/Domain/MediaItem/MediaFile.cs index 5673ec66e..4f4413bc5 100644 --- a/ErsatzTV.Core/Domain/MediaItem/MediaFile.cs +++ b/ErsatzTV.Core/Domain/MediaItem/MediaFile.cs @@ -1,11 +1,10 @@ -namespace ErsatzTV.Core.Domain -{ - public class MediaFile - { - public int Id { get; set; } - public string Path { get; set; } +namespace ErsatzTV.Core.Domain; - public int MediaVersionId { get; set; } - public MediaVersion MediaVersion { get; set; } - } -} +public class MediaFile +{ + public int Id { get; set; } + public string Path { get; set; } + + public int MediaVersionId { get; set; } + public MediaVersion MediaVersion { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/MediaItem/MediaItem.cs b/ErsatzTV.Core/Domain/MediaItem/MediaItem.cs index b150ec0f9..85002d163 100644 --- a/ErsatzTV.Core/Domain/MediaItem/MediaItem.cs +++ b/ErsatzTV.Core/Domain/MediaItem/MediaItem.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; - -namespace ErsatzTV.Core.Domain; +namespace ErsatzTV.Core.Domain; public class MediaItem { diff --git a/ErsatzTV.Core/Domain/MediaItem/MediaStream.cs b/ErsatzTV.Core/Domain/MediaItem/MediaStream.cs index 115b75587..4998ce3a2 100644 --- a/ErsatzTV.Core/Domain/MediaItem/MediaStream.cs +++ b/ErsatzTV.Core/Domain/MediaItem/MediaStream.cs @@ -1,21 +1,20 @@ -namespace ErsatzTV.Core.Domain +namespace ErsatzTV.Core.Domain; + +public class MediaStream { - public class MediaStream - { - public int Id { get; set; } - public int Index { get; set; } - public string Codec { get; set; } - public string Profile { get; set; } - public MediaStreamKind MediaStreamKind { get; set; } - public string Language { get; set; } - public int Channels { get; set; } - public string Title { get; set; } - public bool Default { get; set; } - public bool Forced { get; set; } - public bool AttachedPic { get; set; } - public string PixelFormat { get; set; } - public int BitsPerRawSample { get; set; } - public int MediaVersionId { get; set; } - public MediaVersion MediaVersion { get; set; } - } -} + public int Id { get; set; } + public int Index { get; set; } + public string Codec { get; set; } + public string Profile { get; set; } + public MediaStreamKind MediaStreamKind { get; set; } + public string Language { get; set; } + public int Channels { get; set; } + public string Title { get; set; } + public bool Default { get; set; } + public bool Forced { get; set; } + public bool AttachedPic { get; set; } + public string PixelFormat { get; set; } + public int BitsPerRawSample { get; set; } + public int MediaVersionId { get; set; } + public MediaVersion MediaVersion { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/MediaItem/MediaStreamKind.cs b/ErsatzTV.Core/Domain/MediaItem/MediaStreamKind.cs index bfee9b14b..eff7970ef 100644 --- a/ErsatzTV.Core/Domain/MediaItem/MediaStreamKind.cs +++ b/ErsatzTV.Core/Domain/MediaItem/MediaStreamKind.cs @@ -1,9 +1,8 @@ -namespace ErsatzTV.Core.Domain +namespace ErsatzTV.Core.Domain; + +public enum MediaStreamKind { - public enum MediaStreamKind - { - Video = 1, - Audio = 2, - Subtitle = 3 - } -} + Video = 1, + Audio = 2, + Subtitle = 3 +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/MediaItem/MediaVersion.cs b/ErsatzTV.Core/Domain/MediaItem/MediaVersion.cs index 4d77198ce..0a4455ea5 100644 --- a/ErsatzTV.Core/Domain/MediaItem/MediaVersion.cs +++ b/ErsatzTV.Core/Domain/MediaItem/MediaVersion.cs @@ -1,24 +1,21 @@ -using System; -using System.Collections.Generic; -using ErsatzTV.Core.Interfaces.FFmpeg; +using ErsatzTV.Core.Interfaces.FFmpeg; -namespace ErsatzTV.Core.Domain +namespace ErsatzTV.Core.Domain; + +public class MediaVersion : IDisplaySize { - public class MediaVersion : IDisplaySize - { - public int Id { get; set; } - public string Name { get; set; } - public List MediaFiles { get; set; } - public List Streams { get; set; } - public List Chapters { get; set; } - public TimeSpan Duration { get; set; } - public string SampleAspectRatio { get; set; } - public string DisplayAspectRatio { get; set; } - public string RFrameRate { get; set; } - public VideoScanKind VideoScanKind { get; set; } - public DateTime DateAdded { get; set; } - public DateTime DateUpdated { get; set; } - public int Width { get; set; } - public int Height { get; set; } - } -} + public int Id { get; set; } + public string Name { get; set; } + public List MediaFiles { get; set; } + public List Streams { get; set; } + public List Chapters { get; set; } + public TimeSpan Duration { get; set; } + public string SampleAspectRatio { get; set; } + public string DisplayAspectRatio { get; set; } + public string RFrameRate { get; set; } + public VideoScanKind VideoScanKind { get; set; } + public DateTime DateAdded { get; set; } + public DateTime DateUpdated { get; set; } + public int Width { get; set; } + public int Height { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/MediaItem/Movie.cs b/ErsatzTV.Core/Domain/MediaItem/Movie.cs index 39e1f3297..55da04a9e 100644 --- a/ErsatzTV.Core/Domain/MediaItem/Movie.cs +++ b/ErsatzTV.Core/Domain/MediaItem/Movie.cs @@ -1,10 +1,7 @@ -using System.Collections.Generic; +namespace ErsatzTV.Core.Domain; -namespace ErsatzTV.Core.Domain +public class Movie : MediaItem { - public class Movie : MediaItem - { - public List MovieMetadata { get; set; } - public List MediaVersions { get; set; } - } -} + public List MovieMetadata { get; set; } + public List MediaVersions { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/MediaItem/MusicVideo.cs b/ErsatzTV.Core/Domain/MediaItem/MusicVideo.cs index ae105b8aa..e2c2234bd 100644 --- a/ErsatzTV.Core/Domain/MediaItem/MusicVideo.cs +++ b/ErsatzTV.Core/Domain/MediaItem/MusicVideo.cs @@ -1,12 +1,9 @@ -using System.Collections.Generic; +namespace ErsatzTV.Core.Domain; -namespace ErsatzTV.Core.Domain +public class MusicVideo : MediaItem { - public class MusicVideo : MediaItem - { - public int ArtistId { get; set; } - public Artist Artist { get; set; } - public List MusicVideoMetadata { get; set; } - public List MediaVersions { get; set; } - } -} + public int ArtistId { get; set; } + public Artist Artist { get; set; } + public List MusicVideoMetadata { get; set; } + public List MediaVersions { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/MediaItem/OtherVideo.cs b/ErsatzTV.Core/Domain/MediaItem/OtherVideo.cs index 5eb5fab36..775c801c9 100644 --- a/ErsatzTV.Core/Domain/MediaItem/OtherVideo.cs +++ b/ErsatzTV.Core/Domain/MediaItem/OtherVideo.cs @@ -1,10 +1,7 @@ -using System.Collections.Generic; +namespace ErsatzTV.Core.Domain; -namespace ErsatzTV.Core.Domain +public class OtherVideo : MediaItem { - public class OtherVideo : MediaItem - { - public List OtherVideoMetadata { get; set; } - public List MediaVersions { get; set; } - } -} + public List OtherVideoMetadata { get; set; } + public List MediaVersions { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/MediaItem/PlexEpisode.cs b/ErsatzTV.Core/Domain/MediaItem/PlexEpisode.cs index 604c273f2..fb53a5349 100644 --- a/ErsatzTV.Core/Domain/MediaItem/PlexEpisode.cs +++ b/ErsatzTV.Core/Domain/MediaItem/PlexEpisode.cs @@ -1,7 +1,6 @@ -namespace ErsatzTV.Core.Domain +namespace ErsatzTV.Core.Domain; + +public class PlexEpisode : Episode { - public class PlexEpisode : Episode - { - public string Key { get; set; } - } -} + public string Key { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/MediaItem/PlexMediaFile.cs b/ErsatzTV.Core/Domain/MediaItem/PlexMediaFile.cs index ee902d82e..41010f8c1 100644 --- a/ErsatzTV.Core/Domain/MediaItem/PlexMediaFile.cs +++ b/ErsatzTV.Core/Domain/MediaItem/PlexMediaFile.cs @@ -1,8 +1,7 @@ -namespace ErsatzTV.Core.Domain +namespace ErsatzTV.Core.Domain; + +public class PlexMediaFile : MediaFile { - public class PlexMediaFile : MediaFile - { - public int PlexId { get; set; } - public string Key { get; set; } - } -} + public int PlexId { get; set; } + public string Key { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/MediaItem/PlexMovie.cs b/ErsatzTV.Core/Domain/MediaItem/PlexMovie.cs index 37c100374..442e9917e 100644 --- a/ErsatzTV.Core/Domain/MediaItem/PlexMovie.cs +++ b/ErsatzTV.Core/Domain/MediaItem/PlexMovie.cs @@ -1,7 +1,6 @@ -namespace ErsatzTV.Core.Domain +namespace ErsatzTV.Core.Domain; + +public class PlexMovie : Movie { - public class PlexMovie : Movie - { - public string Key { get; set; } - } -} + public string Key { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/MediaItem/PlexSeason.cs b/ErsatzTV.Core/Domain/MediaItem/PlexSeason.cs index a6faaeb48..244e60042 100644 --- a/ErsatzTV.Core/Domain/MediaItem/PlexSeason.cs +++ b/ErsatzTV.Core/Domain/MediaItem/PlexSeason.cs @@ -1,7 +1,6 @@ -namespace ErsatzTV.Core.Domain +namespace ErsatzTV.Core.Domain; + +public class PlexSeason : Season { - public class PlexSeason : Season - { - public string Key { get; set; } - } -} + public string Key { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/MediaItem/PlexShow.cs b/ErsatzTV.Core/Domain/MediaItem/PlexShow.cs index 6d4041a78..f2f0de123 100644 --- a/ErsatzTV.Core/Domain/MediaItem/PlexShow.cs +++ b/ErsatzTV.Core/Domain/MediaItem/PlexShow.cs @@ -1,7 +1,6 @@ -namespace ErsatzTV.Core.Domain +namespace ErsatzTV.Core.Domain; + +public class PlexShow : Show { - public class PlexShow : Show - { - public string Key { get; set; } - } -} + public string Key { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/MediaItem/Season.cs b/ErsatzTV.Core/Domain/MediaItem/Season.cs index 6eea46387..bc1819ddf 100644 --- a/ErsatzTV.Core/Domain/MediaItem/Season.cs +++ b/ErsatzTV.Core/Domain/MediaItem/Season.cs @@ -1,14 +1,11 @@ -using System.Collections.Generic; +namespace ErsatzTV.Core.Domain; -namespace ErsatzTV.Core.Domain +public class Season : MediaItem { - public class Season : MediaItem - { - public int SeasonNumber { get; set; } - public int ShowId { get; set; } - public Show Show { get; set; } + public int SeasonNumber { get; set; } + public int ShowId { get; set; } + public Show Show { get; set; } - public List Episodes { get; set; } - public List SeasonMetadata { get; set; } - } -} + public List Episodes { get; set; } + public List SeasonMetadata { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/MediaItem/Show.cs b/ErsatzTV.Core/Domain/MediaItem/Show.cs index 230e6f3d3..4d9ed1a5a 100644 --- a/ErsatzTV.Core/Domain/MediaItem/Show.cs +++ b/ErsatzTV.Core/Domain/MediaItem/Show.cs @@ -1,10 +1,7 @@ -using System.Collections.Generic; +namespace ErsatzTV.Core.Domain; -namespace ErsatzTV.Core.Domain +public class Show : MediaItem { - public class Show : MediaItem - { - public List Seasons { get; set; } - public List ShowMetadata { get; set; } - } -} + public List Seasons { get; set; } + public List ShowMetadata { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/MediaItem/Song.cs b/ErsatzTV.Core/Domain/MediaItem/Song.cs index b85357d8b..ae1db64ea 100644 --- a/ErsatzTV.Core/Domain/MediaItem/Song.cs +++ b/ErsatzTV.Core/Domain/MediaItem/Song.cs @@ -1,10 +1,7 @@ -using System.Collections.Generic; +namespace ErsatzTV.Core.Domain; -namespace ErsatzTV.Core.Domain +public class Song : MediaItem { - public class Song : MediaItem - { - public List SongMetadata { get; set; } - public List MediaVersions { get; set; } - } -} + public List SongMetadata { get; set; } + public List MediaVersions { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/MediaItem/VideoScanKind.cs b/ErsatzTV.Core/Domain/MediaItem/VideoScanKind.cs index 812061ac3..edd1012c6 100644 --- a/ErsatzTV.Core/Domain/MediaItem/VideoScanKind.cs +++ b/ErsatzTV.Core/Domain/MediaItem/VideoScanKind.cs @@ -1,9 +1,8 @@ -namespace ErsatzTV.Core.Domain +namespace ErsatzTV.Core.Domain; + +public enum VideoScanKind { - public enum VideoScanKind - { - Unknown = 0, - Progressive = 1, - Interlaced = 2 - } -} + Unknown = 0, + Progressive = 1, + Interlaced = 2 +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/MediaSource/EmbyConnection.cs b/ErsatzTV.Core/Domain/MediaSource/EmbyConnection.cs index 77b6f1842..e38797d5b 100644 --- a/ErsatzTV.Core/Domain/MediaSource/EmbyConnection.cs +++ b/ErsatzTV.Core/Domain/MediaSource/EmbyConnection.cs @@ -1,10 +1,9 @@ -namespace ErsatzTV.Core.Domain +namespace ErsatzTV.Core.Domain; + +public class EmbyConnection { - public class EmbyConnection - { - public int Id { get; set; } - public string Address { get; set; } - public int EmbyMediaSourceId { get; set; } - public EmbyMediaSource EmbyMediaSource { get; set; } - } -} + public int Id { get; set; } + public string Address { get; set; } + public int EmbyMediaSourceId { get; set; } + public EmbyMediaSource EmbyMediaSource { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/MediaSource/EmbyMediaSource.cs b/ErsatzTV.Core/Domain/MediaSource/EmbyMediaSource.cs index 34404b057..06dc6e926 100644 --- a/ErsatzTV.Core/Domain/MediaSource/EmbyMediaSource.cs +++ b/ErsatzTV.Core/Domain/MediaSource/EmbyMediaSource.cs @@ -1,12 +1,9 @@ -using System.Collections.Generic; +namespace ErsatzTV.Core.Domain; -namespace ErsatzTV.Core.Domain +public class EmbyMediaSource : MediaSource { - public class EmbyMediaSource : MediaSource - { - public string ServerName { get; set; } - public string OperatingSystem { get; set; } - public List Connections { get; set; } - public List PathReplacements { get; set; } - } -} + public string ServerName { get; set; } + public string OperatingSystem { get; set; } + public List Connections { get; set; } + public List PathReplacements { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/MediaSource/EmbyPathReplacement.cs b/ErsatzTV.Core/Domain/MediaSource/EmbyPathReplacement.cs index 00ea50e4f..ec456a698 100644 --- a/ErsatzTV.Core/Domain/MediaSource/EmbyPathReplacement.cs +++ b/ErsatzTV.Core/Domain/MediaSource/EmbyPathReplacement.cs @@ -1,11 +1,10 @@ -namespace ErsatzTV.Core.Domain +namespace ErsatzTV.Core.Domain; + +public class EmbyPathReplacement { - public class EmbyPathReplacement - { - public int Id { get; set; } - public string EmbyPath { get; set; } - public string LocalPath { get; set; } - public int EmbyMediaSourceId { get; set; } - public EmbyMediaSource EmbyMediaSource { get; set; } - } -} + public int Id { get; set; } + public string EmbyPath { get; set; } + public string LocalPath { get; set; } + public int EmbyMediaSourceId { get; set; } + public EmbyMediaSource EmbyMediaSource { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/MediaSource/JellyfinConnection.cs b/ErsatzTV.Core/Domain/MediaSource/JellyfinConnection.cs index f253880c8..21f3d42e1 100644 --- a/ErsatzTV.Core/Domain/MediaSource/JellyfinConnection.cs +++ b/ErsatzTV.Core/Domain/MediaSource/JellyfinConnection.cs @@ -1,10 +1,9 @@ -namespace ErsatzTV.Core.Domain +namespace ErsatzTV.Core.Domain; + +public class JellyfinConnection { - public class JellyfinConnection - { - public int Id { get; set; } - public string Address { get; set; } - public int JellyfinMediaSourceId { get; set; } - public JellyfinMediaSource JellyfinMediaSource { get; set; } - } -} + public int Id { get; set; } + public string Address { get; set; } + public int JellyfinMediaSourceId { get; set; } + public JellyfinMediaSource JellyfinMediaSource { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/MediaSource/JellyfinMediaSource.cs b/ErsatzTV.Core/Domain/MediaSource/JellyfinMediaSource.cs index 31116f42e..940498985 100644 --- a/ErsatzTV.Core/Domain/MediaSource/JellyfinMediaSource.cs +++ b/ErsatzTV.Core/Domain/MediaSource/JellyfinMediaSource.cs @@ -1,12 +1,9 @@ -using System.Collections.Generic; +namespace ErsatzTV.Core.Domain; -namespace ErsatzTV.Core.Domain +public class JellyfinMediaSource : MediaSource { - public class JellyfinMediaSource : MediaSource - { - public string ServerName { get; set; } - public string OperatingSystem { get; set; } - public List Connections { get; set; } - public List PathReplacements { get; set; } - } -} + public string ServerName { get; set; } + public string OperatingSystem { get; set; } + public List Connections { get; set; } + public List PathReplacements { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/MediaSource/JellyfinPathReplacement.cs b/ErsatzTV.Core/Domain/MediaSource/JellyfinPathReplacement.cs index 8717cfc53..c227a4794 100644 --- a/ErsatzTV.Core/Domain/MediaSource/JellyfinPathReplacement.cs +++ b/ErsatzTV.Core/Domain/MediaSource/JellyfinPathReplacement.cs @@ -1,11 +1,10 @@ -namespace ErsatzTV.Core.Domain +namespace ErsatzTV.Core.Domain; + +public class JellyfinPathReplacement { - public class JellyfinPathReplacement - { - public int Id { get; set; } - public string JellyfinPath { get; set; } - public string LocalPath { get; set; } - public int JellyfinMediaSourceId { get; set; } - public JellyfinMediaSource JellyfinMediaSource { get; set; } - } -} + public int Id { get; set; } + public string JellyfinPath { get; set; } + public string LocalPath { get; set; } + public int JellyfinMediaSourceId { get; set; } + public JellyfinMediaSource JellyfinMediaSource { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/MediaSource/LocalMediaSource.cs b/ErsatzTV.Core/Domain/MediaSource/LocalMediaSource.cs index ad0c4642c..9b619a6af 100644 --- a/ErsatzTV.Core/Domain/MediaSource/LocalMediaSource.cs +++ b/ErsatzTV.Core/Domain/MediaSource/LocalMediaSource.cs @@ -1,6 +1,5 @@ -namespace ErsatzTV.Core.Domain +namespace ErsatzTV.Core.Domain; + +public class LocalMediaSource : MediaSource { - public class LocalMediaSource : MediaSource - { - } -} +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/MediaSource/MediaSource.cs b/ErsatzTV.Core/Domain/MediaSource/MediaSource.cs index 62340ebd6..207e7342d 100644 --- a/ErsatzTV.Core/Domain/MediaSource/MediaSource.cs +++ b/ErsatzTV.Core/Domain/MediaSource/MediaSource.cs @@ -1,11 +1,8 @@ -using System.Collections.Generic; +namespace ErsatzTV.Core.Domain; -namespace ErsatzTV.Core.Domain +public abstract class MediaSource { - public abstract class MediaSource - { - public int Id { get; set; } + public int Id { get; set; } - public List Libraries { get; set; } - } -} + public List Libraries { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/MediaSource/PlexConnection.cs b/ErsatzTV.Core/Domain/MediaSource/PlexConnection.cs index 18313b8f2..286026b81 100644 --- a/ErsatzTV.Core/Domain/MediaSource/PlexConnection.cs +++ b/ErsatzTV.Core/Domain/MediaSource/PlexConnection.cs @@ -1,11 +1,10 @@ -namespace ErsatzTV.Core.Domain +namespace ErsatzTV.Core.Domain; + +public class PlexConnection { - public class PlexConnection - { - public int Id { get; set; } - public bool IsActive { get; set; } - public string Uri { get; set; } - public int PlexMediaSourceId { get; set; } - public PlexMediaSource PlexMediaSource { get; set; } - } -} + public int Id { get; set; } + public bool IsActive { get; set; } + public string Uri { get; set; } + public int PlexMediaSourceId { get; set; } + public PlexMediaSource PlexMediaSource { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/MediaSource/PlexMediaSource.cs b/ErsatzTV.Core/Domain/MediaSource/PlexMediaSource.cs index 38c6acf4e..924fc0c52 100644 --- a/ErsatzTV.Core/Domain/MediaSource/PlexMediaSource.cs +++ b/ErsatzTV.Core/Domain/MediaSource/PlexMediaSource.cs @@ -1,17 +1,14 @@ -using System.Collections.Generic; +namespace ErsatzTV.Core.Domain; -namespace ErsatzTV.Core.Domain +public class PlexMediaSource : MediaSource { - public class PlexMediaSource : MediaSource - { - public string ServerName { get; set; } - public string ProductVersion { get; set; } - public string Platform { get; set; } - public string PlatformVersion { get; set; } - public string ClientIdentifier { get; set; } + public string ServerName { get; set; } + public string ProductVersion { get; set; } + public string Platform { get; set; } + public string PlatformVersion { get; set; } + public string ClientIdentifier { get; set; } - // public bool IsOwned { get; set; } - public List Connections { get; set; } - public List PathReplacements { get; set; } - } -} + // public bool IsOwned { get; set; } + public List Connections { get; set; } + public List PathReplacements { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/MediaSource/PlexPathReplacement.cs b/ErsatzTV.Core/Domain/MediaSource/PlexPathReplacement.cs index f5e5c71ff..8b317ef3b 100644 --- a/ErsatzTV.Core/Domain/MediaSource/PlexPathReplacement.cs +++ b/ErsatzTV.Core/Domain/MediaSource/PlexPathReplacement.cs @@ -1,11 +1,10 @@ -namespace ErsatzTV.Core.Domain +namespace ErsatzTV.Core.Domain; + +public class PlexPathReplacement { - public class PlexPathReplacement - { - public int Id { get; set; } - public string PlexPath { get; set; } - public string LocalPath { get; set; } - public int PlexMediaSourceId { get; set; } - public PlexMediaSource PlexMediaSource { get; set; } - } -} + public int Id { get; set; } + public string PlexPath { get; set; } + public string LocalPath { get; set; } + public int PlexMediaSourceId { get; set; } + public PlexMediaSource PlexMediaSource { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/Metadata/Actor.cs b/ErsatzTV.Core/Domain/Metadata/Actor.cs index 6aa671dd1..456db3d0f 100644 --- a/ErsatzTV.Core/Domain/Metadata/Actor.cs +++ b/ErsatzTV.Core/Domain/Metadata/Actor.cs @@ -1,12 +1,11 @@ -namespace ErsatzTV.Core.Domain +namespace ErsatzTV.Core.Domain; + +public class Actor { - public class Actor - { - public int Id { get; set; } - public string Name { get; set; } - public string Role { get; set; } - public int? Order { get; set; } - public int? ArtworkId { get; set; } - public Artwork Artwork { get; set; } - } -} + public int Id { get; set; } + public string Name { get; set; } + public string Role { get; set; } + public int? Order { get; set; } + public int? ArtworkId { get; set; } + public Artwork Artwork { get; set; } +} \ No newline at end of file diff --git a/ErsatzTV.Core/Domain/Metadata/ArtistMetadata.cs b/ErsatzTV.Core/Domain/Metadata/ArtistMetadata.cs index 73ef9c69a..59d7dc7c0 100644 --- a/ErsatzTV.Core/Domain/Metadata/ArtistMetadata.cs +++ b/ErsatzTV.Core/Domain/Metadata/ArtistMetadata.cs @@ -1,15 +1,12 @@ -using System.Collections.Generic; +namespace ErsatzTV.Core.Domain; -namespace ErsatzTV.Core.Domain +public class ArtistMetadata : Metadata { - public class ArtistMetadata : Metadata - { - public string Disambiguation { get; set; } - public string Biography { get; set; } - public string Formed { get; set; } - public int ArtistId { get; set; } - public Artist Artist { get; set; } - public List