diff --git a/CHANGELOG.md b/CHANGELOG.md index bbddeee5..af79d83a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] +### Fixed +- Fix `HLS Segmenter` bug where it would drift off of the schedule if a playout was changed while the segmenter was running ### Added - Add `Preferred Subtitle Language` and `Subtitle Mode` to channel settings @@ -15,6 +17,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Changed - Remove legacy transcoder logic option; all channels will use the new transcoder logic - Renamed channel setting `Preferred Language` to `Preferred Audio Language` +- Reworked playout build logic to maintain collection progress in some scenarios. There are now three build modes: + - `Continue` - add new items to the end of an existing playout + - This mode is used when playouts are automatically extended in the background + - `Refresh` - this mode will try to maintain collection progress while rebuilding the entire playout + - This mode is used when a schedule is updated, or when collection modifications trigger a playout rebuild + - `Reset` - this mode will rebuild the entire playout and will NOT maintain progress + - This mode is only used when the `Reset Playout` button is clicked on the Playouts page ## [0.4.5-alpha] - 2022-03-29 ### Fixed diff --git a/ErsatzTV.Application/Configuration/Commands/UpdatePlayoutDaysToBuildHandler.cs b/ErsatzTV.Application/Configuration/Commands/UpdatePlayoutDaysToBuildHandler.cs index 77f20539..c6a3119b 100644 --- a/ErsatzTV.Application/Configuration/Commands/UpdatePlayoutDaysToBuildHandler.cs +++ b/ErsatzTV.Application/Configuration/Commands/UpdatePlayoutDaysToBuildHandler.cs @@ -3,13 +3,13 @@ using ErsatzTV.Application.Playouts; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; +using ErsatzTV.Core.Scheduling; using ErsatzTV.Infrastructure.Data; using Microsoft.EntityFrameworkCore; namespace ErsatzTV.Application.Configuration; -public class - UpdatePlayoutDaysToBuildHandler : MediatR.IRequestHandler> +public class UpdatePlayoutDaysToBuildHandler : IRequestHandler> { private readonly IConfigElementRepository _configElementRepository; private readonly IDbContextFactory _dbContextFactory; @@ -37,16 +37,16 @@ public class private async Task ApplyUpdate(TvContext dbContext, int daysToBuild) { await _configElementRepository.Upsert(ConfigElementKey.PlayoutDaysToBuild, daysToBuild); - - // build all playouts to proper number of days + + // continue 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)); + await _workerChannel.WriteAsync(new BuildPlayout(playoutId, PlayoutBuildMode.Continue)); } - + return Unit.Default; } diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddArtistToCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/AddArtistToCollectionHandler.cs index e27fe693..8af663b2 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/AddArtistToCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/AddArtistToCollectionHandler.cs @@ -3,6 +3,7 @@ using ErsatzTV.Application.Playouts; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; +using ErsatzTV.Core.Scheduling; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; using Microsoft.EntityFrameworkCore; @@ -10,7 +11,7 @@ using Microsoft.EntityFrameworkCore; namespace ErsatzTV.Application.MediaCollections; public class AddArtistToCollectionHandler : - MediatR.IRequestHandler> + IRequestHandler> { private readonly ChannelWriter _channel; private readonly IDbContextFactory _dbContextFactory; @@ -30,9 +31,9 @@ public class AddArtistToCollectionHandler : AddArtistToCollection request, CancellationToken cancellationToken) { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); Validation validation = await Validate(dbContext, request); - return await LanguageExtensions.Apply(validation, parameters => ApplyAddArtistRequest(dbContext, parameters)); + return await validation.Apply(parameters => ApplyAddArtistRequest(dbContext, parameters)); } private async Task ApplyAddArtistRequest(TvContext dbContext, Parameters parameters) @@ -40,11 +41,11 @@ public class AddArtistToCollectionHandler : parameters.Collection.MediaItems.Add(parameters.Artist); if (await dbContext.SaveChangesAsync() > 0) { - // rebuild all playouts that use this collection + // refresh all playouts that use this collection foreach (int playoutId in await _mediaCollectionRepository - .PlayoutIdsUsingCollection(parameters.Collection.Id)) + .PlayoutIdsUsingCollection(parameters.Collection.Id)) { - await _channel.WriteAsync(new BuildPlayout(playoutId, true)); + await _channel.WriteAsync(new BuildPlayout(playoutId, PlayoutBuildMode.Refresh)); } } diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddEpisodeToCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/AddEpisodeToCollectionHandler.cs index 1db8dd48..125cbd5e 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/AddEpisodeToCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/AddEpisodeToCollectionHandler.cs @@ -3,6 +3,7 @@ using ErsatzTV.Application.Playouts; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; +using ErsatzTV.Core.Scheduling; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; using Microsoft.EntityFrameworkCore; @@ -30,9 +31,9 @@ public class AddEpisodeToCollectionHandler : AddEpisodeToCollection request, CancellationToken cancellationToken) { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); Validation validation = await Validate(dbContext, request); - return await LanguageExtensions.Apply(validation, parameters => ApplyAddTelevisionEpisodeRequest(dbContext, parameters)); + return await validation.Apply(parameters => ApplyAddTelevisionEpisodeRequest(dbContext, parameters)); } private async Task ApplyAddTelevisionEpisodeRequest(TvContext dbContext, Parameters parameters) @@ -40,11 +41,11 @@ public class AddEpisodeToCollectionHandler : parameters.Collection.MediaItems.Add(parameters.Episode); if (await dbContext.SaveChangesAsync() > 0) { - // rebuild all playouts that use this collection + // refresh all playouts that use this collection foreach (int playoutId in await _mediaCollectionRepository - .PlayoutIdsUsingCollection(parameters.Collection.Id)) + .PlayoutIdsUsingCollection(parameters.Collection.Id)) { - await _channel.WriteAsync(new BuildPlayout(playoutId, true)); + await _channel.WriteAsync(new BuildPlayout(playoutId, PlayoutBuildMode.Refresh)); } } diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollectionHandler.cs index a2fbdd22..d8fa5f9d 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollectionHandler.cs @@ -3,6 +3,7 @@ using ErsatzTV.Application.Playouts; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; +using ErsatzTV.Core.Scheduling; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; using Microsoft.EntityFrameworkCore; @@ -36,10 +37,9 @@ public class AddItemsToCollectionHandler : AddItemsToCollection request, CancellationToken cancellationToken) { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); Validation validation = await Validate(dbContext, request); - return await LanguageExtensions.Apply(validation, c => ApplyAddItemsRequest(dbContext, c, request)); + return await validation.Apply(c => ApplyAddItemsRequest(dbContext, c, request)); } private async Task ApplyAddItemsRequest(TvContext dbContext, Collection collection, AddItemsToCollection request) @@ -63,11 +63,11 @@ public class AddItemsToCollectionHandler : if (await dbContext.SaveChangesAsync() > 0) { - // rebuild all playouts that use this collection + // refresh all playouts that use this collection foreach (int playoutId in await _mediaCollectionRepository - .PlayoutIdsUsingCollection(request.CollectionId)) + .PlayoutIdsUsingCollection(request.CollectionId)) { - await _channel.WriteAsync(new BuildPlayout(playoutId, true)); + await _channel.WriteAsync(new BuildPlayout(playoutId, PlayoutBuildMode.Refresh)); } } diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddMovieToCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/AddMovieToCollectionHandler.cs index 11b8616e..8f27288e 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/AddMovieToCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/AddMovieToCollectionHandler.cs @@ -3,6 +3,7 @@ using ErsatzTV.Application.Playouts; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; +using ErsatzTV.Core.Scheduling; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; using Microsoft.EntityFrameworkCore; @@ -30,9 +31,9 @@ public class AddMovieToCollectionHandler : AddMovieToCollection request, CancellationToken cancellationToken) { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); Validation validation = await Validate(dbContext, request); - return await LanguageExtensions.Apply(validation, parameters => ApplyAddMovieRequest(dbContext, parameters)); + return await validation.Apply(parameters => ApplyAddMovieRequest(dbContext, parameters)); } private async Task ApplyAddMovieRequest(TvContext dbContext, Parameters parameters) @@ -40,11 +41,11 @@ public class AddMovieToCollectionHandler : parameters.Collection.MediaItems.Add(parameters.Movie); if (await dbContext.SaveChangesAsync() > 0) { - // rebuild all playouts that use this collection + // refresh all playouts that use this collection foreach (int playoutId in await _mediaCollectionRepository - .PlayoutIdsUsingCollection(parameters.Collection.Id)) + .PlayoutIdsUsingCollection(parameters.Collection.Id)) { - await _channel.WriteAsync(new BuildPlayout(playoutId, true)); + await _channel.WriteAsync(new BuildPlayout(playoutId, PlayoutBuildMode.Refresh)); } } diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddMusicVideoToCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/AddMusicVideoToCollectionHandler.cs index d4a35913..8e77bf4c 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/AddMusicVideoToCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/AddMusicVideoToCollectionHandler.cs @@ -3,6 +3,7 @@ using ErsatzTV.Application.Playouts; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; +using ErsatzTV.Core.Scheduling; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; using Microsoft.EntityFrameworkCore; @@ -30,9 +31,9 @@ public class AddMusicVideoToCollectionHandler : AddMusicVideoToCollection request, CancellationToken cancellationToken) { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); Validation validation = await Validate(dbContext, request); - return await LanguageExtensions.Apply(validation, parameters => ApplyAddMusicVideoRequest(dbContext, parameters)); + return await validation.Apply(parameters => ApplyAddMusicVideoRequest(dbContext, parameters)); } private async Task ApplyAddMusicVideoRequest(TvContext dbContext, Parameters parameters) @@ -40,11 +41,11 @@ public class AddMusicVideoToCollectionHandler : parameters.Collection.MediaItems.Add(parameters.MusicVideo); if (await dbContext.SaveChangesAsync() > 0) { - // rebuild all playouts that use this collection + // refresh all playouts that use this collection foreach (int playoutId in await _mediaCollectionRepository - .PlayoutIdsUsingCollection(parameters.Collection.Id)) + .PlayoutIdsUsingCollection(parameters.Collection.Id)) { - await _channel.WriteAsync(new BuildPlayout(playoutId, true)); + await _channel.WriteAsync(new BuildPlayout(playoutId, PlayoutBuildMode.Refresh)); } } diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddOtherVideoToCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/AddOtherVideoToCollectionHandler.cs index 24313e6b..cf1f3fc8 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/AddOtherVideoToCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/AddOtherVideoToCollectionHandler.cs @@ -3,6 +3,7 @@ using ErsatzTV.Application.Playouts; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; +using ErsatzTV.Core.Scheduling; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; using Microsoft.EntityFrameworkCore; @@ -30,9 +31,9 @@ public class AddOtherVideoToCollectionHandler : AddOtherVideoToCollection request, CancellationToken cancellationToken) { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); Validation validation = await Validate(dbContext, request); - return await LanguageExtensions.Apply(validation, parameters => ApplyAddOtherVideoRequest(dbContext, parameters)); + return await validation.Apply(parameters => ApplyAddOtherVideoRequest(dbContext, parameters)); } private async Task ApplyAddOtherVideoRequest(TvContext dbContext, Parameters parameters) @@ -40,11 +41,11 @@ public class AddOtherVideoToCollectionHandler : parameters.Collection.MediaItems.Add(parameters.OtherVideo); if (await dbContext.SaveChangesAsync() > 0) { - // rebuild all playouts that use this collection + // refresh all playouts that use this collection foreach (int playoutId in await _mediaCollectionRepository - .PlayoutIdsUsingCollection(parameters.Collection.Id)) + .PlayoutIdsUsingCollection(parameters.Collection.Id)) { - await _channel.WriteAsync(new BuildPlayout(playoutId, true)); + await _channel.WriteAsync(new BuildPlayout(playoutId, PlayoutBuildMode.Refresh)); } } diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddSeasonToCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/AddSeasonToCollectionHandler.cs index 893b463d..89e4b32c 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/AddSeasonToCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/AddSeasonToCollectionHandler.cs @@ -3,6 +3,7 @@ using ErsatzTV.Application.Playouts; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; +using ErsatzTV.Core.Scheduling; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; using Microsoft.EntityFrameworkCore; @@ -30,9 +31,9 @@ public class AddSeasonToCollectionHandler : AddSeasonToCollection request, CancellationToken cancellationToken) { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); Validation validation = await Validate(dbContext, request); - return await LanguageExtensions.Apply(validation, parameters => ApplyAddSeasonRequest(dbContext, parameters)); + return await validation.Apply(parameters => ApplyAddSeasonRequest(dbContext, parameters)); } private async Task ApplyAddSeasonRequest(TvContext dbContext, Parameters parameters) @@ -40,11 +41,11 @@ public class AddSeasonToCollectionHandler : parameters.Collection.MediaItems.Add(parameters.Season); if (await dbContext.SaveChangesAsync() > 0) { - // rebuild all playouts that use this collection + // refresh all playouts that use this collection foreach (int playoutId in await _mediaCollectionRepository - .PlayoutIdsUsingCollection(parameters.Collection.Id)) + .PlayoutIdsUsingCollection(parameters.Collection.Id)) { - await _channel.WriteAsync(new BuildPlayout(playoutId, true)); + await _channel.WriteAsync(new BuildPlayout(playoutId, PlayoutBuildMode.Refresh)); } } diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddShowToCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/AddShowToCollectionHandler.cs index a6ea3bfd..1ca2ff69 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/AddShowToCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/AddShowToCollectionHandler.cs @@ -3,6 +3,7 @@ using ErsatzTV.Application.Playouts; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; +using ErsatzTV.Core.Scheduling; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; using Microsoft.EntityFrameworkCore; @@ -30,9 +31,9 @@ public class AddShowToCollectionHandler : AddShowToCollection request, CancellationToken cancellationToken) { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); Validation validation = await Validate(dbContext, request); - return await LanguageExtensions.Apply(validation, parameters => ApplyAddShowRequest(dbContext, parameters)); + return await validation.Apply(parameters => ApplyAddShowRequest(dbContext, parameters)); } private async Task ApplyAddShowRequest(TvContext dbContext, Parameters parameters) @@ -40,11 +41,11 @@ public class AddShowToCollectionHandler : parameters.Collection.MediaItems.Add(parameters.Show); if (await dbContext.SaveChangesAsync() > 0) { - // rebuild all playouts that use this collection + // refresh all playouts that use this collection foreach (int playoutId in await _mediaCollectionRepository - .PlayoutIdsUsingCollection(parameters.Collection.Id)) + .PlayoutIdsUsingCollection(parameters.Collection.Id)) { - await _channel.WriteAsync(new BuildPlayout(playoutId, true)); + await _channel.WriteAsync(new BuildPlayout(playoutId, PlayoutBuildMode.Refresh)); } } diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddSongToCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/AddSongToCollectionHandler.cs index 69a277e6..fb55067c 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/AddSongToCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/AddSongToCollectionHandler.cs @@ -3,6 +3,7 @@ using ErsatzTV.Application.Playouts; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; +using ErsatzTV.Core.Scheduling; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; using Microsoft.EntityFrameworkCore; @@ -40,11 +41,11 @@ public class AddSongToCollectionHandler : parameters.Collection.MediaItems.Add(parameters.Song); if (await dbContext.SaveChangesAsync() > 0) { - // rebuild all playouts that use this collection + // refresh all playouts that use this collection foreach (int playoutId in await _mediaCollectionRepository - .PlayoutIdsUsingCollection(parameters.Collection.Id)) + .PlayoutIdsUsingCollection(parameters.Collection.Id)) { - await _channel.WriteAsync(new BuildPlayout(playoutId, true)); + await _channel.WriteAsync(new BuildPlayout(playoutId, PlayoutBuildMode.Refresh)); } } diff --git a/ErsatzTV.Application/MediaCollections/Commands/RemoveItemsFromCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/RemoveItemsFromCollectionHandler.cs index 2c453775..4afc7c2e 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/RemoveItemsFromCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/RemoveItemsFromCollectionHandler.cs @@ -3,14 +3,14 @@ using ErsatzTV.Application.Playouts; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; +using ErsatzTV.Core.Scheduling; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; using Microsoft.EntityFrameworkCore; namespace ErsatzTV.Application.MediaCollections; -public class RemoveItemsFromCollectionHandler : - MediatR.IRequestHandler> +public class RemoveItemsFromCollectionHandler : IRequestHandler> { private readonly ChannelWriter _channel; private readonly IDbContextFactory _dbContextFactory; @@ -30,9 +30,9 @@ public class RemoveItemsFromCollectionHandler : RemoveItemsFromCollection request, CancellationToken cancellationToken) { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); Validation validation = await Validate(dbContext, request); - return await LanguageExtensions.Apply(validation, c => ApplyRemoveItemsRequest(dbContext, request, c)); + return await validation.Apply(c => ApplyRemoveItemsRequest(dbContext, request, c)); } private async Task ApplyRemoveItemsRequest( @@ -48,10 +48,10 @@ public class RemoveItemsFromCollectionHandler : if (itemsToRemove.Any() && await dbContext.SaveChangesAsync() > 0) { - // rebuild all playouts that use this collection + // refresh all playouts that use this collection foreach (int playoutId in await _mediaCollectionRepository.PlayoutIdsUsingCollection(collection.Id)) { - await _channel.WriteAsync(new BuildPlayout(playoutId, true)); + await _channel.WriteAsync(new BuildPlayout(playoutId, PlayoutBuildMode.Refresh)); } } diff --git a/ErsatzTV.Application/MediaCollections/Commands/UpdateCollectionCustomOrderHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/UpdateCollectionCustomOrderHandler.cs index 903983a9..b269d0ee 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/UpdateCollectionCustomOrderHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/UpdateCollectionCustomOrderHandler.cs @@ -3,14 +3,14 @@ using ErsatzTV.Application.Playouts; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; +using ErsatzTV.Core.Scheduling; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; using Microsoft.EntityFrameworkCore; namespace ErsatzTV.Application.MediaCollections; -public class UpdateCollectionCustomOrderHandler : - MediatR.IRequestHandler> +public class UpdateCollectionCustomOrderHandler : IRequestHandler> { private readonly ChannelWriter _channel; private readonly IDbContextFactory _dbContextFactory; @@ -30,9 +30,9 @@ public class UpdateCollectionCustomOrderHandler : UpdateCollectionCustomOrder request, CancellationToken cancellationToken) { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); Validation validation = await Validate(dbContext, request); - return await LanguageExtensions.Apply(validation, c => ApplyUpdateRequest(dbContext, c, request)); + return await validation.Apply(c => ApplyUpdateRequest(dbContext, c, request)); } private async Task ApplyUpdateRequest( @@ -53,11 +53,11 @@ public class UpdateCollectionCustomOrderHandler : if (await dbContext.SaveChangesAsync() > 0) { - // rebuild all playouts that use this collection + // refresh all playouts that use this collection foreach (int playoutId in await _mediaCollectionRepository .PlayoutIdsUsingCollection(request.CollectionId)) { - await _channel.WriteAsync(new BuildPlayout(playoutId, true)); + await _channel.WriteAsync(new BuildPlayout(playoutId, PlayoutBuildMode.Refresh)); } } diff --git a/ErsatzTV.Application/MediaCollections/Commands/UpdateCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/UpdateCollectionHandler.cs index 067fa4e0..735e824d 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/UpdateCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/UpdateCollectionHandler.cs @@ -3,13 +3,14 @@ using ErsatzTV.Application.Playouts; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; +using ErsatzTV.Core.Scheduling; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; using Microsoft.EntityFrameworkCore; namespace ErsatzTV.Application.MediaCollections; -public class UpdateCollectionHandler : MediatR.IRequestHandler> +public class UpdateCollectionHandler : IRequestHandler> { private readonly ChannelWriter _channel; private readonly IDbContextFactory _dbContextFactory; @@ -29,9 +30,9 @@ public class UpdateCollectionHandler : MediatR.IRequestHandler validation = await Validate(dbContext, request); - return await LanguageExtensions.Apply(validation, c => ApplyUpdateRequest(dbContext, c, request)); + return await validation.Apply(c => ApplyUpdateRequest(dbContext, c, request)); } private async Task ApplyUpdateRequest(TvContext dbContext, Collection c, UpdateCollection request) @@ -44,11 +45,11 @@ public class UpdateCollectionHandler : MediatR.IRequestHandler 0 && request.UseCustomPlaybackOrder.IsSome) { - // rebuild all playouts that use this collection + // refresh all playouts that use this collection foreach (int playoutId in await _mediaCollectionRepository.PlayoutIdsUsingCollection( request.CollectionId)) { - await _channel.WriteAsync(new BuildPlayout(playoutId, true)); + await _channel.WriteAsync(new BuildPlayout(playoutId, PlayoutBuildMode.Refresh)); } } diff --git a/ErsatzTV.Application/MediaCollections/Commands/UpdateMultiCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/UpdateMultiCollectionHandler.cs index f56093f8..ef8516df 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/UpdateMultiCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/UpdateMultiCollectionHandler.cs @@ -3,13 +3,14 @@ using ErsatzTV.Application.Playouts; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; +using ErsatzTV.Core.Scheduling; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; using Microsoft.EntityFrameworkCore; namespace ErsatzTV.Application.MediaCollections; -public class UpdateMultiCollectionHandler : MediatR.IRequestHandler> +public class UpdateMultiCollectionHandler : IRequestHandler> { private readonly ChannelWriter _channel; private readonly IDbContextFactory _dbContextFactory; @@ -29,15 +30,15 @@ public class UpdateMultiCollectionHandler : MediatR.IRequestHandler validation = await Validate(dbContext, request); - return await LanguageExtensions.Apply(validation, c => ApplyUpdateRequest(dbContext, c, 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(); @@ -45,22 +46,23 @@ public class UpdateMultiCollectionHandler : MediatR.IRequestHandler 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 - }) + .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) { @@ -79,22 +81,23 @@ public class UpdateMultiCollectionHandler : MediatR.IRequestHandler 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 - }) + .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) { @@ -112,11 +115,11 @@ public class UpdateMultiCollectionHandler : MediatR.IRequestHandler 0) { - // rebuild all playouts that use this collection + // refresh all playouts that use this collection foreach (int playoutId in await _mediaCollectionRepository.PlayoutIdsUsingMultiCollection( request.MultiCollectionId)) { - await _channel.WriteAsync(new BuildPlayout(playoutId, true)); + await _channel.WriteAsync(new BuildPlayout(playoutId, PlayoutBuildMode.Refresh)); } } @@ -138,7 +141,9 @@ public class UpdateMultiCollectionHandler : MediatR.IRequestHandler c.Id, c => c.Id == updateCollection.MultiCollectionId) .Map(o => o.ToValidation("MultiCollection does not exist.")); - private static async Task> ValidateName(TvContext dbContext, UpdateMultiCollection updateMultiCollection) + private static async Task> ValidateName( + TvContext dbContext, + UpdateMultiCollection updateMultiCollection) { List allNames = await dbContext.MultiCollections .Filter(mc => mc.Id != updateMultiCollection.MultiCollectionId) diff --git a/ErsatzTV.Application/MediaCollections/Commands/UpdateSmartCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/UpdateSmartCollectionHandler.cs index e48a8956..f2c460bf 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/UpdateSmartCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/UpdateSmartCollectionHandler.cs @@ -3,13 +3,14 @@ using ErsatzTV.Application.Playouts; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; +using ErsatzTV.Core.Scheduling; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; using Microsoft.EntityFrameworkCore; namespace ErsatzTV.Application.MediaCollections; -public class UpdateSmartCollectionHandler : MediatR.IRequestHandler> +public class UpdateSmartCollectionHandler : IRequestHandler> { private readonly ChannelWriter _channel; private readonly IDbContextFactory _dbContextFactory; @@ -29,9 +30,9 @@ public class UpdateSmartCollectionHandler : MediatR.IRequestHandler validation = await Validate(dbContext, request); - return await LanguageExtensions.Apply(validation, c => ApplyUpdateRequest(dbContext, c, request)); + return await validation.Apply(c => ApplyUpdateRequest(dbContext, c, request)); } private async Task ApplyUpdateRequest(TvContext dbContext, SmartCollection c, UpdateSmartCollection request) @@ -41,10 +42,10 @@ public class UpdateSmartCollectionHandler : MediatR.IRequestHandler 0) { - // rebuild all playouts that use this smart collection + // refresh all playouts that use this smart collection foreach (int playoutId in await _mediaCollectionRepository.PlayoutIdsUsingSmartCollection(request.Id)) { - await _channel.WriteAsync(new BuildPlayout(playoutId, true)); + await _channel.WriteAsync(new BuildPlayout(playoutId, PlayoutBuildMode.Refresh)); } } diff --git a/ErsatzTV.Application/Playouts/Commands/BuildPlayout.cs b/ErsatzTV.Application/Playouts/Commands/BuildPlayout.cs index d3134c9f..15c7881c 100644 --- a/ErsatzTV.Application/Playouts/Commands/BuildPlayout.cs +++ b/ErsatzTV.Application/Playouts/Commands/BuildPlayout.cs @@ -1,6 +1,7 @@ using ErsatzTV.Core; +using ErsatzTV.Core.Scheduling; namespace ErsatzTV.Application.Playouts; -public record BuildPlayout(int PlayoutId, bool Rebuild = false) : MediatR.IRequest>, +public record BuildPlayout(int PlayoutId, PlayoutBuildMode Mode) : 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 d9e7c196..121222c7 100644 --- a/ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs +++ b/ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs @@ -1,6 +1,7 @@ using Bugsnag; using ErsatzTV.Core; using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Interfaces.Scheduling; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; @@ -13,12 +14,18 @@ public class BuildPlayoutHandler : MediatR.IRequestHandler _dbContextFactory; private readonly IPlayoutBuilder _playoutBuilder; + private readonly IFFmpegSegmenterService _ffmpegSegmenterService; - public BuildPlayoutHandler(IClient client, IDbContextFactory dbContextFactory, IPlayoutBuilder playoutBuilder) + public BuildPlayoutHandler( + IClient client, + IDbContextFactory dbContextFactory, + IPlayoutBuilder playoutBuilder, + IFFmpegSegmenterService ffmpegSegmenterService) { _client = client; _dbContextFactory = dbContextFactory; _playoutBuilder = playoutBuilder; + _ffmpegSegmenterService = ffmpegSegmenterService; } public async Task> Handle(BuildPlayout request, CancellationToken cancellationToken) @@ -32,8 +39,11 @@ public class BuildPlayoutHandler : MediatR.IRequestHandler 0) + { + _ffmpegSegmenterService.PlayoutUpdated(playout.Channel.Number); + } } catch (Exception ex) { @@ -42,7 +52,6 @@ public class BuildPlayoutHandler : MediatR.IRequestHandler> Validate(TvContext dbContext, BuildPlayout request) => PlayoutMustExist(dbContext, request); diff --git a/ErsatzTV.Application/Playouts/Commands/CreatePlayoutHandler.cs b/ErsatzTV.Application/Playouts/Commands/CreatePlayoutHandler.cs index 1b346d1d..0fb1403b 100644 --- a/ErsatzTV.Application/Playouts/Commands/CreatePlayoutHandler.cs +++ b/ErsatzTV.Application/Playouts/Commands/CreatePlayoutHandler.cs @@ -1,6 +1,7 @@ using System.Threading.Channels; using ErsatzTV.Core; using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Scheduling; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; using Microsoft.EntityFrameworkCore; @@ -25,17 +26,16 @@ public class CreatePlayoutHandler : IRequestHandler validation = await Validate(dbContext, request); - return await LanguageExtensions.Apply(validation, playout => PersistPlayout(dbContext, playout)); + 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)); + await _channel.WriteAsync(new BuildPlayout(playout.Id, PlayoutBuildMode.Reset)); return new CreatePlayoutResponse(playout.Id); } diff --git a/ErsatzTV.Application/ProgramSchedules/Commands/AddProgramScheduleItemHandler.cs b/ErsatzTV.Application/ProgramSchedules/Commands/AddProgramScheduleItemHandler.cs index d495c23a..2eaf769b 100644 --- a/ErsatzTV.Application/ProgramSchedules/Commands/AddProgramScheduleItemHandler.cs +++ b/ErsatzTV.Application/ProgramSchedules/Commands/AddProgramScheduleItemHandler.cs @@ -2,6 +2,7 @@ using ErsatzTV.Application.Playouts; using ErsatzTV.Core; using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Scheduling; using ErsatzTV.Infrastructure.Data; using Microsoft.EntityFrameworkCore; using static ErsatzTV.Application.ProgramSchedules.Mapper; @@ -26,9 +27,9 @@ public class AddProgramScheduleItemHandler : ProgramScheduleItemCommandBase, AddProgramScheduleItem request, CancellationToken cancellationToken) { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); Validation validation = await Validate(dbContext, request); - return await LanguageExtensions.Apply(validation, ps => PersistItem(dbContext, request, ps)); + return await validation.Apply(ps => PersistItem(dbContext, request, ps)); } private async Task PersistItem( @@ -43,10 +44,10 @@ public class AddProgramScheduleItemHandler : ProgramScheduleItemCommandBase, await dbContext.SaveChangesAsync(); - // rebuild any playouts that use this schedule + // refresh any playouts that use this schedule foreach (Playout playout in programSchedule.Playouts) { - await _channel.WriteAsync(new BuildPlayout(playout.Id, true)); + await _channel.WriteAsync(new BuildPlayout(playout.Id, PlayoutBuildMode.Refresh)); } return ProjectToViewModel(item); diff --git a/ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItemsHandler.cs b/ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItemsHandler.cs index ac4cb17d..f3084b9c 100644 --- a/ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItemsHandler.cs +++ b/ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItemsHandler.cs @@ -2,6 +2,7 @@ using ErsatzTV.Application.Playouts; using ErsatzTV.Core; using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Scheduling; using ErsatzTV.Infrastructure.Data; using Microsoft.EntityFrameworkCore; using static ErsatzTV.Application.ProgramSchedules.Mapper; @@ -26,9 +27,9 @@ public class ReplaceProgramScheduleItemsHandler : ProgramScheduleItemCommandBase ReplaceProgramScheduleItems request, CancellationToken cancellationToken) { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); Validation validation = await Validate(dbContext, request); - return await LanguageExtensions.Apply(validation, ps => PersistItems(dbContext, request, ps)); + return await validation.Apply(ps => PersistItems(dbContext, request, ps)); } private async Task> PersistItems( @@ -41,10 +42,10 @@ public class ReplaceProgramScheduleItemsHandler : ProgramScheduleItemCommandBase await dbContext.SaveChangesAsync(); - // rebuild any playouts that use this schedule + // refresh any playouts that use this schedule foreach (Playout playout in programSchedule.Playouts) { - await _channel.WriteAsync(new BuildPlayout(playout.Id, true)); + await _channel.WriteAsync(new BuildPlayout(playout.Id, PlayoutBuildMode.Refresh)); } return programSchedule.Items.Map(ProjectToViewModel); diff --git a/ErsatzTV.Application/ProgramSchedules/Commands/UpdateProgramScheduleHandler.cs b/ErsatzTV.Application/ProgramSchedules/Commands/UpdateProgramScheduleHandler.cs index ded2771a..3540ef9f 100644 --- a/ErsatzTV.Application/ProgramSchedules/Commands/UpdateProgramScheduleHandler.cs +++ b/ErsatzTV.Application/ProgramSchedules/Commands/UpdateProgramScheduleHandler.cs @@ -2,6 +2,7 @@ using ErsatzTV.Application.Playouts; using ErsatzTV.Core; using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Scheduling; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; using Microsoft.EntityFrameworkCore; @@ -27,9 +28,8 @@ public class UpdateProgramScheduleHandler : 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)); + return await validation.Apply(ps => ApplyUpdateRequest(dbContext, ps, request)); } private async Task ApplyUpdateRequest( @@ -37,8 +37,8 @@ public class UpdateProgramScheduleHandler : ProgramSchedule programSchedule, UpdateProgramSchedule request) { - // we need to rebuild playouts if the playback order or keep multi-episodes has been modified - bool needToRebuildPlayout = + // we need to refresh playouts if the playback order or keep multi-episodes has been modified + bool needToRefreshPlayout = programSchedule.KeepMultiPartEpisodesTogether != request.KeepMultiPartEpisodesTogether || programSchedule.TreatCollectionsAsShows != request.TreatCollectionsAsShows || programSchedule.ShuffleScheduleItems != request.ShuffleScheduleItems; @@ -51,7 +51,7 @@ public class UpdateProgramScheduleHandler : await dbContext.SaveChangesAsync(); - if (needToRebuildPlayout) + if (needToRefreshPlayout) { List playoutIds = await dbContext.Playouts .Filter(p => p.ProgramScheduleId == programSchedule.Id) @@ -60,7 +60,7 @@ public class UpdateProgramScheduleHandler : foreach (int playoutId in playoutIds) { - await _channel.WriteAsync(new BuildPlayout(playoutId, true)); + await _channel.WriteAsync(new BuildPlayout(playoutId, PlayoutBuildMode.Refresh)); } } diff --git a/ErsatzTV.Application/Streaming/HlsSessionWorker.cs b/ErsatzTV.Application/Streaming/HlsSessionWorker.cs index 88c656ba..451b317e 100644 --- a/ErsatzTV.Application/Streaming/HlsSessionWorker.cs +++ b/ErsatzTV.Application/Streaming/HlsSessionWorker.cs @@ -30,6 +30,7 @@ public class HlsSessionWorker : IHlsSessionWorker private DateTimeOffset _playlistStart; private Option _targetFramerate; private string _channelNumber; + private bool _firstProcess; public HlsSessionWorker( IHlsPlaylistFilter hlsPlaylistFilter, @@ -70,6 +71,8 @@ public class HlsSessionWorker : IHlsSessionWorker } } + public void PlayoutUpdated() => _firstProcess = true; + public async Task Run(string channelNumber, TimeSpan idleTimeout, CancellationToken incomingCancellationToken) { var cts = CancellationTokenSource.CreateLinkedTokenSource(incomingCancellationToken); @@ -105,8 +108,10 @@ public class HlsSessionWorker : IHlsSessionWorker _transcodedUntil = DateTimeOffset.Now; _playlistStart = _transcodedUntil; + _firstProcess = true; + bool initialWorkAhead = Volatile.Read(ref _workAheadCount) < await GetWorkAheadLimit(); - if (!await Transcode(true, !initialWorkAhead, cancellationToken)) + if (!await Transcode(!initialWorkAhead, cancellationToken)) { return; } @@ -127,7 +132,7 @@ public class HlsSessionWorker : IHlsSessionWorker bool realtime = transcodedBuffer >= TimeSpan.FromSeconds(30); bool subsequentWorkAhead = !realtime && Volatile.Read(ref _workAheadCount) < await GetWorkAheadLimit(); - if (!await Transcode(false, !subsequentWorkAhead, cancellationToken)) + if (!await Transcode(!subsequentWorkAhead, cancellationToken)) { return; } @@ -149,7 +154,6 @@ public class HlsSessionWorker : IHlsSessionWorker } private async Task Transcode( - bool firstProcess, bool realtime, CancellationToken cancellationToken) { @@ -177,8 +181,8 @@ public class HlsSessionWorker : IHlsSessionWorker var request = new GetPlayoutItemProcessByChannelNumber( _channelNumber, "segmenter", - firstProcess ? DateTimeOffset.Now : _transcodedUntil.AddSeconds(1), - !firstProcess, + _firstProcess ? DateTimeOffset.Now : _transcodedUntil.AddSeconds(1), + !_firstProcess, realtime, ptsOffset, _targetFramerate); @@ -220,6 +224,7 @@ public class HlsSessionWorker : IHlsSessionWorker { _logger.LogInformation("HLS process has completed for channel {Channel}", _channelNumber); _transcodedUntil = processModel.Until; + _firstProcess = false; return true; } else @@ -237,7 +242,7 @@ public class HlsSessionWorker : IHlsSessionWorker _channelNumber, commandResult.ExitCode, commandResult.StandardError); - + Either maybeOfflineProcess = await mediator.Send( new GetErrorProcess( _channelNumber, @@ -252,7 +257,7 @@ public class HlsSessionWorker : IHlsSessionWorker foreach (PlayoutItemProcessModel errorProcessModel in maybeOfflineProcess.RightAsEnumerable()) { Process errorProcess = errorProcessModel.Process; - + _logger.LogInformation( "ffmpeg hls error arguments {FFmpegArguments}", string.Join(" ", errorProcess.StartInfo.ArgumentList)); @@ -261,8 +266,12 @@ public class HlsSessionWorker : IHlsSessionWorker .WithArguments(errorProcess.StartInfo.ArgumentList) .WithValidation(CommandResultValidation.None) .ExecuteBufferedAsync(cancellationToken); - - return commandResult.ExitCode == 0; + + if (commandResult.ExitCode == 0) + { + _firstProcess = false; + return true; + } } return false; diff --git a/ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs b/ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs index 37acd7dc..2e88add5 100644 --- a/ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs +++ b/ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs @@ -4,1428 +4,2081 @@ using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Scheduling; using ErsatzTV.Core.Tests.Fakes; using FluentAssertions; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; using Serilog; -namespace ErsatzTV.Core.Tests.Scheduling; - -[TestFixture] -public class PlayoutBuilderTests +namespace ErsatzTV.Core.Tests.Scheduling { - private readonly ILogger _logger; - - public PlayoutBuilderTests() + [TestFixture] + public class PlayoutBuilderTests { - 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"); - } + private readonly ILogger _logger; - ServiceProvider serviceProvider = new ServiceCollection() - .AddLogging(builder => builder.AddSerilog(dispose: true)) - .BuildServiceProvider(); + public PlayoutBuilderTests() + { + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Debug() + .WriteTo.Console() + .CreateLogger(); - ILoggerFactory factory = serviceProvider.GetService(); + ILoggerFactory loggerFactory = new LoggerFactory().AddSerilog(Log.Logger); - _logger = factory.CreateLogger(); - } + _logger = loggerFactory?.CreateLogger(); + } - [Test] - [Timeout(2000)] - public async Task OnlyZeroDurationItem_Should_Abort() - { - var mediaItems = new List + [TestFixture] + public class NewPlayout : PlayoutBuilderTests { - TestMovie(1, TimeSpan.Zero, DateTime.Today) - }; - (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Random); - DateTimeOffset start = HoursAfterMidnight(0); - DateTimeOffset finish = start + TimeSpan.FromHours(6); + [Test] + [Timeout(2000)] + public async Task OnlyZeroDurationItem_Should_Abort() + { + var mediaItems = new List + { + TestMovie(1, TimeSpan.Zero, DateTime.Today) + }; - Playout result = await builder.BuildPlayoutItems(playout, start, finish); + (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Random); - result.Items.Should().BeNull(); - } + Playout result = await builder.Build(playout, PlayoutBuildMode.Reset); - [Test] - public async Task ZeroDurationItem_Should_BeSkipped() - { - var mediaItems = new List - { - TestMovie(1, TimeSpan.Zero, DateTime.Today), - TestMovie(2, TimeSpan.FromHours(6), DateTime.Today) - }; + result.Items.Should().BeEmpty(); + } - (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Random); - DateTimeOffset start = HoursAfterMidnight(0); - DateTimeOffset finish = start + TimeSpan.FromHours(6); + [Test] + public async Task ZeroDurationItem_Should_BeSkipped() + { + var mediaItems = new List + { + TestMovie(1, TimeSpan.Zero, DateTime.Today), + TestMovie(2, TimeSpan.FromHours(6), DateTime.Today) + }; - Playout result = await builder.BuildPlayoutItems(playout, start, finish); + (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Random); + DateTimeOffset start = HoursAfterMidnight(0); + DateTimeOffset finish = start + 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)); - } + Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish); - [Test] - public async Task InitialFlood_Should_StartAtMidnight() - { - var mediaItems = new List - { - TestMovie(1, TimeSpan.FromHours(6), DateTime.Today) - }; + 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)); + } - (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Random); - DateTimeOffset start = HoursAfterMidnight(0); - DateTimeOffset finish = start + TimeSpan.FromHours(6); + [Test] + public async Task InitialFlood_Should_StartAtMidnight() + { + var mediaItems = new List + { + TestMovie(1, TimeSpan.FromHours(6), DateTime.Today) + }; - Playout result = await builder.BuildPlayoutItems(playout, start, finish); + (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Random); + DateTimeOffset start = HoursAfterMidnight(0); + DateTimeOffset finish = start + 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)); - } + Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish); - [Test] - public async Task InitialFlood_Should_StartAtMidnight_With_LateStart() - { - var mediaItems = new List - { - TestMovie(1, TimeSpan.FromHours(6), DateTime.Today) - }; + 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)); + } - (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Random); - DateTimeOffset start = HoursAfterMidnight(1); - DateTimeOffset finish = start + TimeSpan.FromHours(6); + [Test] + public async Task InitialFlood_Should_StartAtMidnight_With_LateStart() + { + var mediaItems = new List + { + TestMovie(1, TimeSpan.FromHours(6), DateTime.Today) + }; - Playout result = await builder.BuildPlayoutItems(playout, start, finish); + (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Random); + DateTimeOffset start = HoursAfterMidnight(1); + DateTimeOffset finish = start + TimeSpan.FromHours(6); - 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)); - } + Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish); - [Test] - public async Task ChronologicalContent_Should_CreateChronologicalItems() - { - 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); - } + 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 ChronologicalFlood_Should_AnchorAndMaintainExistingPlayout() - { - var mediaItems = new List - { - TestMovie(1, TimeSpan.FromHours(6), DateTime.Today), - TestMovie(2, TimeSpan.FromHours(6), DateTime.Today.AddHours(1)) - }; + [Test] + public async Task ChronologicalContent_Should_CreateChronologicalItems() + { + 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.Build(playout, PlayoutBuildMode.Reset, 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); + } - (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Chronological); - DateTimeOffset start = HoursAfterMidnight(0); - DateTimeOffset finish = start + TimeSpan.FromHours(6); + [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)) + }; - Playout result = await builder.BuildPlayoutItems(playout, start, finish); + (PlayoutBuilder builder, Playout playout) = + TestDataFloodForItems(mediaItems, PlaybackOrder.Chronological); + DateTimeOffset start = HoursAfterMidnight(0); + DateTimeOffset finish = start + TimeSpan.FromHours(6); - result.Items.Count.Should().Be(1); - result.Items.Head().MediaItemId.Should().Be(1); + Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish); - result.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddHours(6)); + result.Items.Count.Should().Be(1); + result.Items.Head().MediaItemId.Should().Be(1); - result.ProgramScheduleAnchors.Count.Should().Be(1); - result.ProgramScheduleAnchors.Head().EnumeratorState.Index.Should().Be(1); + result.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddHours(6)); - DateTimeOffset start2 = HoursAfterMidnight(1); - DateTimeOffset finish2 = start2 + TimeSpan.FromHours(6); + result.ProgramScheduleAnchors.Count.Should().Be(1); + result.ProgramScheduleAnchors.Head().EnumeratorState.Index.Should().Be(1); - Playout result2 = await builder.BuildPlayoutItems(playout, start2, finish2); + DateTimeOffset start2 = HoursAfterMidnight(1); + DateTimeOffset finish2 = start2 + TimeSpan.FromHours(6); - result2.Items.Count.Should().Be(2); - result2.Items.Last().StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); - result2.Items.Last().MediaItemId.Should().Be(2); + Playout result2 = await builder.Build(playout, PlayoutBuildMode.Continue, start2, finish2); - result2.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddHours(12)); - result2.ProgramScheduleAnchors.Count.Should().Be(1); - result2.ProgramScheduleAnchors.Head().EnumeratorState.Index.Should().Be(0); - } + result2.Items.Count.Should().Be(2); + result2.Items.Last().StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); + result2.Items.Last().MediaItemId.Should().Be(2); - [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)) - }; + result2.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddHours(12)); + result2.ProgramScheduleAnchors.Count.Should().Be(1); + result2.ProgramScheduleAnchors.Head().EnumeratorState.Index.Should().Be(0); + } - (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Chronological); - DateTimeOffset start = HoursAfterMidnight(0); - DateTimeOffset finish = start + TimeSpan.FromHours(6); + [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)) + }; - Playout result = await builder.BuildPlayoutItems(playout, start, finish); + (PlayoutBuilder builder, Playout playout) = + TestDataFloodForItems(mediaItems, PlaybackOrder.Chronological); + DateTimeOffset start = HoursAfterMidnight(0); + DateTimeOffset finish = start + TimeSpan.FromHours(6); - result.Items.Count.Should().Be(1); - result.Items.Head().MediaItemId.Should().Be(1); + Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish); - result.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddHours(6)); - result.ProgramScheduleAnchors.Count.Should().Be(1); - result.ProgramScheduleAnchors.Head().EnumeratorState.Index.Should().Be(1); + result.Items.Count.Should().Be(1); + result.Items.Head().MediaItemId.Should().Be(1); - DateTimeOffset start2 = HoursAfterMidnight(1); - DateTimeOffset finish2 = start2 + TimeSpan.FromHours(12); + result.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddHours(6)); + result.ProgramScheduleAnchors.Count.Should().Be(1); + result.ProgramScheduleAnchors.Head().EnumeratorState.Index.Should().Be(1); - Playout result2 = await builder.BuildPlayoutItems(playout, start2, finish2); + DateTimeOffset start2 = HoursAfterMidnight(1); + DateTimeOffset finish2 = start2 + TimeSpan.FromHours(12); - 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); + Playout result2 = await builder.Build(playout, PlayoutBuildMode.Continue, start2, finish2); - result2.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddHours(18)); - result2.ProgramScheduleAnchors.Count.Should().Be(1); - result2.ProgramScheduleAnchors.Head().EnumeratorState.Index.Should().Be(1); - } + 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); - [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)) - }; + result2.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddHours(18)); + result2.ProgramScheduleAnchors.Count.Should().Be(1); + result2.ProgramScheduleAnchors.Head().EnumeratorState.Index.Should().Be(1); + } - (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Shuffle); - DateTimeOffset start = HoursAfterMidnight(0); - DateTimeOffset finish = start + TimeSpan.FromHours(6); + [Test] + public async Task ShuffleFloodReset_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)) + }; - Playout result = await builder.BuildPlayoutItems(playout, start, finish); + (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Shuffle); + DateTimeOffset start = HoursAfterMidnight(0); + DateTimeOffset finish = start + TimeSpan.FromHours(6); - result.Items.Count.Should().Be(6); - result.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddHours(6)); + Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish); - result.ProgramScheduleAnchors.Count.Should().Be(1); - result.ProgramScheduleAnchors.Head().EnumeratorState.Index.Should().Be(0); + result.Items.Count.Should().Be(6); + result.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddHours(6)); - int firstSeedValue = result.ProgramScheduleAnchors.Head().EnumeratorState.Seed; + result.ProgramScheduleAnchors.Count.Should().Be(1); + result.ProgramScheduleAnchors.Head().EnumeratorState.Index.Should().Be(0); - DateTimeOffset start2 = HoursAfterMidnight(0); - DateTimeOffset finish2 = start2 + TimeSpan.FromHours(6); + int firstSeedValue = result.ProgramScheduleAnchors.Head().EnumeratorState.Seed; - Playout result2 = await builder.BuildPlayoutItems(playout, start2, finish2, true); + DateTimeOffset start2 = HoursAfterMidnight(0); + DateTimeOffset finish2 = start2 + TimeSpan.FromHours(6); - result2.Items.Count.Should().Be(6); - result2.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddHours(6)); + Playout result2 = await builder.Build(playout, PlayoutBuildMode.Reset, start2, finish2); - result2.ProgramScheduleAnchors.Count.Should().Be(1); - result2.ProgramScheduleAnchors.Head().EnumeratorState.Index.Should().Be(0); + result2.Items.Count.Should().Be(6); + result2.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddHours(6)); - int secondSeedValue = result2.ProgramScheduleAnchors.Head().EnumeratorState.Seed; + result2.ProgramScheduleAnchors.Count.Should().Be(1); + result2.ProgramScheduleAnchors.Head().EnumeratorState.Index.Should().Be(0); - firstSeedValue.Should().NotBe(secondSeedValue); - } + int secondSeedValue = result2.ProgramScheduleAnchors.Head().EnumeratorState.Seed; - [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)) - }; + firstSeedValue.Should().NotBe(secondSeedValue); + } - (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Shuffle); - DateTimeOffset start = HoursAfterMidnight(0); - DateTimeOffset finish = start + TimeSpan.FromHours(6); + [Test] + public async Task ContinuePlayout_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)) + }; - Playout result = await builder.BuildPlayoutItems(playout, start, finish); + (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Shuffle); + DateTimeOffset start = HoursAfterMidnight(0); + DateTimeOffset finish = start + TimeSpan.FromHours(6); - result.Items.Count.Should().Be(6); - result.ProgramScheduleAnchors.Count.Should().Be(1); - result.ProgramScheduleAnchors.Head().EnumeratorState.Seed.Should().BeGreaterThan(0); + Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish); - int firstSeedValue = result.ProgramScheduleAnchors.Head().EnumeratorState.Seed; + result.Items.Count.Should().Be(6); + result.ProgramScheduleAnchors.Count.Should().Be(1); + result.ProgramScheduleAnchors.Head().EnumeratorState.Seed.Should().BeGreaterThan(0); - DateTimeOffset start2 = HoursAfterMidnight(0); - DateTimeOffset finish2 = start2 + TimeSpan.FromHours(6); + int firstSeedValue = result.ProgramScheduleAnchors.Head().EnumeratorState.Seed; - Playout result2 = await builder.BuildPlayoutItems(playout, start2, finish2); + DateTimeOffset start2 = HoursAfterMidnight(0); + DateTimeOffset finish2 = start2 + TimeSpan.FromHours(6); - int secondSeedValue = result2.ProgramScheduleAnchors.Head().EnumeratorState.Seed; + Playout result2 = await builder.Build(playout, PlayoutBuildMode.Continue, start2, finish2); - firstSeedValue.Should().Be(secondSeedValue); - } + int secondSeedValue = result2.ProgramScheduleAnchors.Head().EnumeratorState.Seed; - [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)) + firstSeedValue.Should().Be(secondSeedValue); } - }; - var fixedCollection = new Collection - { - Id = 2, - Name = "Fixed Items", - MediaItems = new List + [Test] + public async Task FloodContent_Should_FloodAroundFixedContent_One() { - TestMovie(3, TimeSpan.FromHours(2), new DateTime(2020, 1, 1)) + 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" }, + ProgramScheduleAnchors = new List(), + Items = new List() + }; + + 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.Build(playout, PlayoutBuildMode.Reset, 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); } - }; - - 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 + [Test] + public async Task FloodContent_Should_FloodAroundFixedContent_Multiple() { - Index = 2, - Collection = fixedCollection, - CollectionId = fixedCollection.Id, - StartTime = TimeSpan.FromHours(3), - PlaybackOrder = PlaybackOrder.Chronological + 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" }, + ProgramScheduleAnchors = new List(), + Items = new List() + }; + + 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.Build(playout, PlayoutBuildMode.Reset, 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); } - }; - 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 + [Test] + public async Task FloodContent_Should_FloodWithFixedStartTime() { - TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)), - TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 2, 1)) + 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" }, + ProgramScheduleAnchors = new List(), + Items = new List() + }; + + 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.Build(playout, PlayoutBuildMode.Reset, 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); } - }; - var fixedCollection = new Collection - { - Id = 2, - Name = "Fixed Items", - MediaItems = new List + [Test] + public async Task ContinuePlayout_FloodContent_Should_FloodWithFixedStartTime_FromAnchor() { - TestMovie(3, TimeSpan.FromHours(2), new DateTime(2020, 1, 1)), - TestMovie(4, TimeSpan.FromHours(1), new DateTime(2020, 1, 2)) + 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 + }, + ProgramScheduleAnchors = new List(), + Items = new List() + }; + + 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.Build(playout, PlayoutBuildMode.Continue, 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(); } - }; - 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 + [Test] + public async Task FloodContent_Should_FloodAroundFixedContent_DurationWithoutOfflineTail() { - Index = 2, - Collection = fixedCollection, - CollectionId = fixedCollection.Id, - StartTime = TimeSpan.FromHours(3), - Count = 2, - PlaybackOrder = PlaybackOrder.Chronological + 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" }, + ProgramScheduleAnchors = new List(), + Items = new List() + }; + + 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.Build(playout, PlayoutBuildMode.Reset, 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); } - }; - var playout = new Playout - { - ProgramSchedule = new ProgramSchedule + [Test] + public async Task MultipleContent_Should_WrapAroundDynamicContent_DurationWithoutOfflineTail() { - 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 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" }, + ProgramScheduleAnchors = new List(), + Items = new List() + }; + + 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.Build(playout, PlayoutBuildMode.Reset, 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); } - }; - var fixedCollection = new Collection - { - Id = 2, - Name = "Fixed Items", - MediaItems = new List + [Test] + public async Task ContinuePlayout_Alternating_MultipleContent_Should_Maintain_Counts() { - TestMovie(3, TimeSpan.FromHours(2), new DateTime(2020, 1, 1)), - TestMovie(4, TimeSpan.FromHours(1), new DateTime(2020, 1, 2)) + 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 + }, + ProgramScheduleAnchors = new List(), + Items = new List() + }; + + 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.Build(playout, PlayoutBuildMode.Continue, 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); } - }; - - var fakeRepository = new FakeMediaCollectionRepository( - Map( - (floodCollection.Id, floodCollection.MediaItems.ToList()), - (fixedCollection.Id, fixedCollection.MediaItems.ToList()))); - var items = new List - { - new ProgramScheduleItemFlood + [Test] + public async Task Auto_Zero_MultipleCount() { - 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 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" }, + ProgramScheduleAnchors = new List(), + Items = new List() + }; + + 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.Build(playout, PlayoutBuildMode.Reset, 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(); } - }; - var playout = new Playout - { - ProgramSchedule = new ProgramSchedule + [Test] + public async Task ContinuePlayout_Alternating_Duration_Should_Maintain_Duration() { - 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); - } + 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 + }, + ProgramScheduleAnchors = new List(), + Items = new List() + }; + + 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.Build(playout, PlayoutBuildMode.Continue, 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 FloodContent_Should_FloodWithFixedStartTime_FromAnchor() - { - var floodCollection = new Collection - { - Id = 1, - Name = "Flood Items", - MediaItems = new List + [Test] + public async Task Alternating_Duration_With_Filler_Should_Alternate_Schedule_Items() { - TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)), - TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 2, 1)) + 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" }, + ProgramScheduleAnchors = new List(), + Items = new List() + }; + + 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.Build(playout, PlayoutBuildMode.Reset, 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(); } - }; - var fixedCollection = new Collection - { - Id = 2, - Name = "Fixed Items", - MediaItems = new List + [Test] + public async Task Duration_Should_Skip_Items_That_Are_Too_Long() { - TestMovie(3, TimeSpan.FromHours(2), new DateTime(2020, 1, 1)), - TestMovie(4, TimeSpan.FromHours(1), new DateTime(2020, 1, 2)) + 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" }, + ProgramScheduleAnchors = new List(), + Items = new List() + }; + + 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.Build(playout, PlayoutBuildMode.Reset, 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(); } - }; - 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 + [Test] + public async Task Two_Day_Playout_Should_Create_Date_Anchors_For_Midnight() { - Index = 2, - Collection = fixedCollection, - CollectionId = fixedCollection.Id, - StartTime = TimeSpan.FromHours(12), - PlaybackOrder = PlaybackOrder.Chronological + var mediaItems = new List + { + TestMovie(1, TimeSpan.FromHours(6), new DateTime(2002, 1, 1)), + TestMovie(2, TimeSpan.FromHours(6), new DateTime(2003, 1, 1)), + TestMovie(3, TimeSpan.FromHours(6), new DateTime(2004, 1, 1)) + }; + + (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Chronological); + DateTimeOffset start = HoursAfterMidnight(0); + DateTimeOffset finish = start + TimeSpan.FromDays(2); + + Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish); + + result.Items.Count.Should().Be(8); + result.Items[0].MediaItemId.Should().Be(1); + result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero); + result.Items[0].FinishOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); + result.Items[1].MediaItemId.Should().Be(2); + result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); + result.Items[1].FinishOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(12)); + result.Items[2].MediaItemId.Should().Be(3); + result.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(12)); + result.Items[2].FinishOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(18)); + result.Items[3].MediaItemId.Should().Be(1); + result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(18)); + result.Items[3].FinishOffset.TimeOfDay.Should().Be(TimeSpan.Zero); + result.Items[4].MediaItemId.Should().Be(2); + result.Items[4].StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero); + result.Items[4].FinishOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); + result.Items[5].MediaItemId.Should().Be(3); + result.Items[5].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); + result.Items[5].FinishOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(12)); + result.Items[6].MediaItemId.Should().Be(1); + result.Items[6].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(12)); + result.Items[6].FinishOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(18)); + result.Items[7].MediaItemId.Should().Be(2); + result.Items[7].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(18)); + result.Items[7].FinishOffset.TimeOfDay.Should().Be(TimeSpan.Zero); + + result.ProgramScheduleAnchors.Count.Should().Be(2); + PlayoutProgramScheduleAnchor mid = result.ProgramScheduleAnchors.First(a => a.AnchorDate is not null); + PlayoutProgramScheduleAnchor end = result.ProgramScheduleAnchors.First(a => a.AnchorDate is null); + mid.EnumeratorState.Index.Should().Be(4 % 3); + end.EnumeratorState.Index.Should().Be(8 % 3); + mid.EnumeratorState.Seed.Should().Be(end.EnumeratorState.Seed); } - }; + } - var playout = new Playout + [TestFixture] + public class ResetPlayout : PlayoutBuilderTests { - ProgramSchedule = new ProgramSchedule + [Test] + public async Task ShuffleFlood_Should_IgnoreAnchors() { - Items = items - }, - Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, - Anchor = new PlayoutAnchor - { - NextStart = HoursAfterMidnight(9).UtcDateTime, - ScheduleItemsEnumeratorState = new CollectionEnumeratorState + var mediaItems = new List { - Index = 0, - Seed = 1 - }, - InFlood = true - } - }; + 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)) + }; - var configRepo = new Mock(); - var televisionRepo = new FakeTelevisionRepository(); - var artistRepo = new Mock(); - var builder = new PlayoutBuilder( - configRepo.Object, - fakeRepository, - televisionRepo, - artistRepo.Object, - _logger); + (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Shuffle); + DateTimeOffset start = HoursAfterMidnight(0); + DateTimeOffset finish = start + TimeSpan.FromHours(6); - DateTimeOffset start = HoursAfterMidnight(0); - DateTimeOffset finish = start + TimeSpan.FromHours(32); + Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish); - Playout result = await builder.BuildPlayoutItems(playout, start, finish); + result.Items.Count.Should().Be(6); + result.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddHours(6)); - result.Items.Count.Should().Be(5); + result.ProgramScheduleAnchors.Count.Should().Be(1); + result.ProgramScheduleAnchors.Head().EnumeratorState.Index.Should().Be(0); - 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); + int firstSeedValue = result.ProgramScheduleAnchors.Head().EnumeratorState.Seed; - result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(12)); - result.Items[3].MediaItemId.Should().Be(3); + DateTimeOffset start2 = HoursAfterMidnight(0); + DateTimeOffset finish2 = start2 + TimeSpan.FromHours(6); - result.Items[4].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(7)); - result.Items[4].MediaItemId.Should().Be(2); + Playout result2 = await builder.Build(playout, PlayoutBuildMode.Reset, start2, finish2); - result.Anchor.InFlood.Should().BeTrue(); - } + result2.Items.Count.Should().Be(6); + result2.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddHours(6)); - [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)) - } - }; + result2.ProgramScheduleAnchors.Count.Should().Be(1); + result2.ProgramScheduleAnchors.Head().EnumeratorState.Index.Should().Be(0); - 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)) - } - }; + int secondSeedValue = result2.ProgramScheduleAnchors.Head().EnumeratorState.Seed; - var fakeRepository = new FakeMediaCollectionRepository( - Map( - (floodCollection.Id, floodCollection.MediaItems.ToList()), - (fixedCollection.Id, fixedCollection.MediaItems.ToList()))); + firstSeedValue.Should().NotBe(secondSeedValue); + } + } - var items = new List + [TestFixture] + public class RefreshPlayout : PlayoutBuilderTests { - new ProgramScheduleItemFlood + [Test] + public async Task Two_Day_Playout_Should_Refresh_From_Midnight_Anchor() { - 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 collectionOne = new Collection + { + Id = 1, + Name = "Duration Items 1", + MediaItems = new List + { + TestMovie(1, TimeSpan.FromHours(6), new DateTime(2002, 1, 1)), + TestMovie(2, TimeSpan.FromHours(6), new DateTime(2003, 1, 1)), + TestMovie(3, TimeSpan.FromHours(6), new DateTime(2004, 1, 1)) + } + }; + + var fakeRepository = + new FakeMediaCollectionRepository(Map((collectionOne.Id, collectionOne.MediaItems.ToList()))); + + var items = new List + { + new ProgramScheduleItemFlood + { + Id = 1, + Index = 1, + Collection = collectionOne, + CollectionId = collectionOne.Id, + StartTime = null, + PlaybackOrder = PlaybackOrder.Chronological + } + }; + + var playout = new Playout + { + ProgramSchedule = new ProgramSchedule + { + Items = items + }, + Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, + + // this should be ignored + Anchor = new PlayoutAnchor + { + NextStart = HoursAfterMidnight(1).UtcDateTime, + ScheduleItemsEnumeratorState = new CollectionEnumeratorState + { + Index = 0, + Seed = 1 + }, + DurationFinish = HoursAfterMidnight(3).UtcDateTime + }, + + ProgramScheduleAnchors = new List(), + Items = new List() + }; + + playout.ProgramScheduleAnchors.Add( + new PlayoutProgramScheduleAnchor + { + AnchorDate = HoursAfterMidnight(24).UtcDateTime, + Collection = collectionOne, + CollectionId = collectionOne.Id, + CollectionType = ProgramScheduleItemCollectionType.Collection, + EnumeratorState = new CollectionEnumeratorState + { + Index = 1, + Seed = 12345 + }, + Playout = playout, + ProgramSchedule = playout.ProgramSchedule, + }); + + 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(24); + DateTimeOffset finish = start + TimeSpan.FromDays(1); + + Playout result = await builder.Build(playout, PlayoutBuildMode.Refresh, start, finish); + + result.Items.Count.Should().Be(4); + result.Items[0].MediaItemId.Should().Be(2); + result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero); + result.Items[0].FinishOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); + result.Items[1].MediaItemId.Should().Be(3); + result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); + result.Items[1].FinishOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(12)); + result.Items[2].MediaItemId.Should().Be(1); + result.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(12)); + result.Items[2].FinishOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(18)); + result.Items[3].MediaItemId.Should().Be(2); + result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(18)); + result.Items[3].FinishOffset.TimeOfDay.Should().Be(TimeSpan.Zero); } - }; + } - var playout = new Playout + [TestFixture] + public class ContinuePlayout : PlayoutBuilderTests { - ProgramSchedule = new ProgramSchedule + [Test] + public async Task ChronologicalFlood_Should_AnchorAndMaintainExistingPlayout() { - 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); - } + var mediaItems = new List + { + TestMovie(1, TimeSpan.FromHours(6), DateTime.Today), + TestMovie(2, TimeSpan.FromHours(6), DateTime.Today.AddHours(1)) + }; - [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)) - } - }; + (PlayoutBuilder builder, Playout playout) = + TestDataFloodForItems(mediaItems, PlaybackOrder.Chronological); + DateTimeOffset start = HoursAfterMidnight(0); + DateTimeOffset finish = start + TimeSpan.FromHours(6); - 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)) - } - }; + Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish); - var fakeRepository = new FakeMediaCollectionRepository( - Map( - (multipleCollection.Id, multipleCollection.MediaItems.ToList()), - (dynamicCollection.Id, dynamicCollection.MediaItems.ToList()))); + result.Items.Count.Should().Be(1); + result.Items.Head().MediaItemId.Should().Be(1); - 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 - } - }; + result.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddHours(6)); - 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); - } + result.ProgramScheduleAnchors.Count.Should().Be(1); + result.ProgramScheduleAnchors.Head().EnumeratorState.Index.Should().Be(1); - [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)) - } - }; + DateTimeOffset start2 = HoursAfterMidnight(1); + DateTimeOffset finish2 = start2 + TimeSpan.FromHours(6); - var collectionTwo = new Collection - { - Id = 2, - Name = "Multiple Items 2", - MediaItems = new List - { - TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)) - } - }; + Playout result2 = await builder.Build(playout, PlayoutBuildMode.Continue, start2, finish2); - var fakeRepository = new FakeMediaCollectionRepository( - Map( - (collectionOne.Id, collectionOne.MediaItems.ToList()), - (collectionTwo.Id, collectionTwo.MediaItems.ToList()))); + result2.Items.Count.Should().Be(2); + result2.Items.Last().StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); + result2.Items.Last().MediaItemId.Should().Be(2); - 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 + result2.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddHours(12)); + result2.ProgramScheduleAnchors.Count.Should().Be(1); + result2.ProgramScheduleAnchors.Head().EnumeratorState.Index.Should().Be(0); } - }; - var playout = new Playout - { - ProgramSchedule = new ProgramSchedule + [Test] + public async Task ChronologicalFlood_Should_AnchorAndReturnNewPlayoutItems() { - Items = items - }, - Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, - Anchor = new PlayoutAnchor - { - NextStart = HoursAfterMidnight(1).UtcDateTime, - ScheduleItemsEnumeratorState = new CollectionEnumeratorState + var mediaItems = new List { - Index = 0, - Seed = 1 - }, - MultipleRemaining = 2 - } - }; + TestMovie(1, TimeSpan.FromHours(6), DateTime.Today), + TestMovie(2, TimeSpan.FromHours(6), DateTime.Today.AddHours(1)) + }; - var configRepo = new Mock(); - var televisionRepo = new FakeTelevisionRepository(); - var artistRepo = new Mock(); - var builder = new PlayoutBuilder( - configRepo.Object, - fakeRepository, - televisionRepo, - artistRepo.Object, - _logger); + (PlayoutBuilder builder, Playout playout) = + TestDataFloodForItems(mediaItems, PlaybackOrder.Chronological); + DateTimeOffset start = HoursAfterMidnight(0); + DateTimeOffset finish = start + TimeSpan.FromHours(6); - DateTimeOffset start = HoursAfterMidnight(0); - DateTimeOffset finish = start + TimeSpan.FromHours(5); + Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish); - Playout result = await builder.BuildPlayoutItems(playout, start, finish); + result.Items.Count.Should().Be(1); + result.Items.Head().MediaItemId.Should().Be(1); - result.Items.Count.Should().Be(4); + result.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddHours(6)); + result.ProgramScheduleAnchors.Count.Should().Be(1); + result.ProgramScheduleAnchors.Head().EnumeratorState.Index.Should().Be(1); - 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); + DateTimeOffset start2 = HoursAfterMidnight(1); + DateTimeOffset finish2 = start2 + TimeSpan.FromHours(12); - 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); + Playout result2 = await builder.Build(playout, PlayoutBuildMode.Continue, start2, finish2); - result.Anchor.ScheduleItemsEnumeratorState.Index.Should().Be(1); - result.Anchor.MultipleRemaining.Should().Be(1); - } + 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); - [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)) + result2.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddHours(18)); + result2.ProgramScheduleAnchors.Count.Should().Be(1); + result2.ProgramScheduleAnchors.Head().EnumeratorState.Index.Should().Be(1); } - }; - var collectionTwo = new Collection - { - Id = 2, - Name = "Multiple Items 2", - MediaItems = new List + [Test] + public async Task ShuffleFlood_Should_MaintainRandomSeed() { - TestMovie(4, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)), - TestMovie(5, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)), - } - }; + 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)) + }; - var fakeRepository = new FakeMediaCollectionRepository( - Map( - (collectionOne.Id, collectionOne.MediaItems.ToList()), - (collectionTwo.Id, collectionTwo.MediaItems.ToList()))); + (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Shuffle); + DateTimeOffset start = HoursAfterMidnight(0); + DateTimeOffset finish = start + TimeSpan.FromHours(6); - 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 - } - }; + Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish); - 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(); - } + result.Items.Count.Should().Be(6); + result.ProgramScheduleAnchors.Count.Should().Be(1); + result.ProgramScheduleAnchors.Head().EnumeratorState.Seed.Should().BeGreaterThan(0); - [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)) - } - }; + int firstSeedValue = result.ProgramScheduleAnchors.Head().EnumeratorState.Seed; - var collectionTwo = new Collection - { - Id = 2, - Name = "Duration Items 2", - MediaItems = new List - { - TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)) - } - }; + DateTimeOffset start2 = HoursAfterMidnight(0); + DateTimeOffset finish2 = start2 + TimeSpan.FromHours(6); - var fakeRepository = new FakeMediaCollectionRepository( - Map( - (collectionOne.Id, collectionOne.MediaItems.ToList()), - (collectionTwo.Id, collectionTwo.MediaItems.ToList()))); + Playout result2 = await builder.Build(playout, PlayoutBuildMode.Continue, start2, finish2); - 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 + int secondSeedValue = result2.ProgramScheduleAnchors.Head().EnumeratorState.Seed; + + firstSeedValue.Should().Be(secondSeedValue); } - }; - var playout = new Playout - { - ProgramSchedule = new ProgramSchedule - { - Items = items - }, - Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, - Anchor = new PlayoutAnchor + [Test] + public async Task FloodContent_Should_FloodWithFixedStartTime_FromAnchor() { - NextStart = HoursAfterMidnight(1).UtcDateTime, - ScheduleItemsEnumeratorState = new CollectionEnumeratorState + 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 { - Index = 0, - Seed = 1 - }, - DurationFinish = HoursAfterMidnight(3).UtcDateTime + 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 + }, + ProgramScheduleAnchors = new List(), + Items = new List() + }; + + 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.Build(playout, PlayoutBuildMode.Continue, 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(); } - }; - - 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 + [Test] + public async Task Alternating_MultipleContent_Should_Maintain_Counts() { - 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 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 + }, + ProgramScheduleAnchors = new List(), + Items = new List() + }; + + 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.Build(playout, PlayoutBuildMode.Continue, 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); } - }; - - var collectionThree = new Collection - { - Id = 3, - Name = "Filler Items", - MediaItems = new List + + [Test] + public async Task Alternating_Duration_Should_Maintain_Duration() { - TestMovie(3, TimeSpan.FromMinutes(5), new DateTime(2020, 1, 1)) + 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 + }, + ProgramScheduleAnchors = new List(), + Items = new List() + }; + + 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.Build(playout, PlayoutBuildMode.Continue, 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); } - }; - - var fakeRepository = new FakeMediaCollectionRepository( - Map( - (collectionOne.Id, collectionOne.MediaItems.ToList()), - (collectionTwo.Id, collectionTwo.MediaItems.ToList()), - (collectionThree.Id, collectionThree.MediaItems.ToList()))); - - var items = new List + } + + private static DateTimeOffset HoursAfterMidnight(int hours) { - new ProgramScheduleItemDuration + DateTimeOffset now = DateTimeOffset.Now; + return now - now.TimeOfDay + TimeSpan.FromHours(hours); + } + + private static ProgramScheduleItem Flood(Collection mediaCollection, PlaybackOrder playbackOrder) => + new ProgramScheduleItemFlood { - Id = 1, Index = 1, - Collection = collectionOne, - CollectionId = collectionOne.Id, + Collection = mediaCollection, + CollectionId = mediaCollection.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 + PlaybackOrder = playbackOrder + }; + + private static Movie TestMovie(int id, TimeSpan duration, DateTime aired) => + new() { - 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 + Id = id, + MovieMetadata = new List { new() { ReleaseDate = aired } }, + MediaVersions = new List + { + new() { Duration = duration } } - } - }; - - 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 + private TestData TestDataFloodForItems(List mediaItems, PlaybackOrder playbackOrder) { - new ProgramScheduleItemDuration + var mediaCollection = new Collection { 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 + 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 { - 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 } - } - }; + Id = 1, + ProgramSchedule = new ProgramSchedule { Items = items }, + Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, + Items = new List(), + ProgramScheduleAnchors = new List() + }; - 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); + } - return new TestData(builder, playout); + private record TestData(PlayoutBuilder Builder, Playout Playout); } - - private record TestData(PlayoutBuilder Builder, Playout Playout); -} \ No newline at end of file +} diff --git a/ErsatzTV.Core/Domain/PlayoutItem.cs b/ErsatzTV.Core/Domain/PlayoutItem.cs index efa2f94c..f59a25a1 100644 --- a/ErsatzTV.Core/Domain/PlayoutItem.cs +++ b/ErsatzTV.Core/Domain/PlayoutItem.cs @@ -3,7 +3,7 @@ using ErsatzTV.Core.Domain.Filler; namespace ErsatzTV.Core.Domain; -[DebuggerDisplay("{MediaItemId} - {Start} - {Finish}")] +[DebuggerDisplay("{MediaItemId} - {StartOffset} - {FinishOffset}")] public class PlayoutItem { public int Id { get; set; } @@ -23,6 +23,7 @@ public class PlayoutItem public DateTimeOffset StartOffset => new DateTimeOffset(Start, TimeSpan.Zero).ToLocalTime(); public DateTimeOffset FinishOffset => new DateTimeOffset(Finish, TimeSpan.Zero).ToLocalTime(); + public DateTimeOffset? GuideFinishOffset => GuideFinish.HasValue ? new DateTimeOffset(GuideFinish.Value, TimeSpan.Zero).ToLocalTime() : null; diff --git a/ErsatzTV.Core/Domain/PlayoutProgramScheduleAnchor.cs b/ErsatzTV.Core/Domain/PlayoutProgramScheduleAnchor.cs index 7ce991d8..e6c3593e 100644 --- a/ErsatzTV.Core/Domain/PlayoutProgramScheduleAnchor.cs +++ b/ErsatzTV.Core/Domain/PlayoutProgramScheduleAnchor.cs @@ -1,12 +1,26 @@ -namespace ErsatzTV.Core.Domain; +using Destructurama.Attributed; + +namespace ErsatzTV.Core.Domain; public class PlayoutProgramScheduleAnchor { public int Id { get; set; } public int PlayoutId { get; set; } + + [NotLogged] public Playout Playout { get; set; } + public int ProgramScheduleId { get; set; } + + [NotLogged] public ProgramSchedule ProgramSchedule { get; set; } + + public DateTime? AnchorDate { get; set; } + + public DateTimeOffset? AnchorDateOffset => AnchorDate.HasValue + ? new DateTimeOffset(AnchorDate.Value, TimeSpan.Zero).ToLocalTime() + : null; + public ProgramScheduleItemCollectionType CollectionType { get; set; } public int? CollectionId { get; set; } public Collection Collection { get; set; } diff --git a/ErsatzTV.Core/ErsatzTV.Core.csproj b/ErsatzTV.Core/ErsatzTV.Core.csproj index 7bd60f2c..1eb09ded 100644 --- a/ErsatzTV.Core/ErsatzTV.Core.csproj +++ b/ErsatzTV.Core/ErsatzTV.Core.csproj @@ -8,6 +8,7 @@ + diff --git a/ErsatzTV.Core/FFmpeg/FFmpegSegmenterService.cs b/ErsatzTV.Core/FFmpeg/FFmpegSegmenterService.cs index d28293de..fcab77f1 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegSegmenterService.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegSegmenterService.cs @@ -1,12 +1,17 @@ using System.Collections.Concurrent; using ErsatzTV.Core.Interfaces.FFmpeg; +using Microsoft.Extensions.Logging; namespace ErsatzTV.Core.FFmpeg; public class FFmpegSegmenterService : IFFmpegSegmenterService { - public FFmpegSegmenterService() + private readonly ILogger _logger; + + public FFmpegSegmenterService(ILogger logger) { + _logger = logger; + SessionWorkers = new ConcurrentDictionary(); } @@ -19,4 +24,19 @@ public class FFmpegSegmenterService : IFFmpegSegmenterService worker?.Touch(); } } -} \ No newline at end of file + + public void PlayoutUpdated(string channelNumber) + { + if (SessionWorkers.TryGetValue(channelNumber, out IHlsSessionWorker worker)) + { + if (worker != null) + { + _logger.LogInformation( + "Playout has been updated for channel {ChannelNumber}, HLS segmenter will skip ahead to catch up", + channelNumber); + + worker.PlayoutUpdated(); + } + } + } +} diff --git a/ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegSegmenterService.cs b/ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegSegmenterService.cs index d9a0a473..b8e125d2 100644 --- a/ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegSegmenterService.cs +++ b/ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegSegmenterService.cs @@ -7,4 +7,5 @@ public interface IFFmpegSegmenterService ConcurrentDictionary SessionWorkers { get; } void TouchChannel(string channelNumber); + void PlayoutUpdated(string channelNumber); } \ No newline at end of file diff --git a/ErsatzTV.Core/Interfaces/FFmpeg/IHlsSessionWorker.cs b/ErsatzTV.Core/Interfaces/FFmpeg/IHlsSessionWorker.cs index 08fcb071..39d5ac7d 100644 --- a/ErsatzTV.Core/Interfaces/FFmpeg/IHlsSessionWorker.cs +++ b/ErsatzTV.Core/Interfaces/FFmpeg/IHlsSessionWorker.cs @@ -4,7 +4,7 @@ namespace ErsatzTV.Core.Interfaces.FFmpeg; public interface IHlsSessionWorker { - DateTimeOffset PlaylistStart { get; } void Touch(); Task> TrimPlaylist(DateTimeOffset filterBefore, CancellationToken cancellationToken); + void PlayoutUpdated(); } \ No newline at end of file diff --git a/ErsatzTV.Core/Interfaces/Scheduling/IPlayoutBuilder.cs b/ErsatzTV.Core/Interfaces/Scheduling/IPlayoutBuilder.cs index ed6112ea..9724770f 100644 --- a/ErsatzTV.Core/Interfaces/Scheduling/IPlayoutBuilder.cs +++ b/ErsatzTV.Core/Interfaces/Scheduling/IPlayoutBuilder.cs @@ -1,14 +1,9 @@ -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Scheduling; namespace ErsatzTV.Core.Interfaces.Scheduling; public interface IPlayoutBuilder { - Task BuildPlayoutItems(Playout playout, bool rebuild = false); - - Task BuildPlayoutItems( - Playout playout, - DateTimeOffset playoutStart, - DateTimeOffset playoutFinish, - bool rebuild = false); + Task Build(Playout playout, PlayoutBuildMode mode); } \ No newline at end of file diff --git a/ErsatzTV.Core/Scheduling/PlayoutBuildMode.cs b/ErsatzTV.Core/Scheduling/PlayoutBuildMode.cs new file mode 100644 index 00000000..82b3bcc0 --- /dev/null +++ b/ErsatzTV.Core/Scheduling/PlayoutBuildMode.cs @@ -0,0 +1,13 @@ +namespace ErsatzTV.Core.Scheduling; + +public enum PlayoutBuildMode +{ + // this continues building playout into the future + Continue = 1, + + // this rebuilds a playout but will maintain collection progress + Refresh = 2, + + // this rebuilds a playout and clears all state + Reset = 3 +} diff --git a/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs b/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs index 44efae4d..938895b2 100644 --- a/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs +++ b/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs @@ -32,18 +32,190 @@ public class PlayoutBuilder : IPlayoutBuilder _logger = logger; } - public async Task BuildPlayoutItems(Playout playout, bool rebuild = false) + public async Task Build(Playout playout, PlayoutBuildMode mode) { - DateTimeOffset now = DateTimeOffset.Now; - Option daysToBuild = await _configElementRepository.GetValue(ConfigElementKey.PlayoutDaysToBuild); - return await BuildPlayoutItems(playout, now, now.AddDays(await daysToBuild.IfNoneAsync(2)), rebuild); + foreach (PlayoutParameters parameters in await Validate(playout)) + { + // for testing purposes + // if (mode == PlayoutBuildMode.Reset) + // { + // return await Build(playout, mode, parameters with { Start = parameters.Start.AddDays(-2) }); + // } + + return await Build(playout, mode, parameters); + } + + return playout; } - public async Task BuildPlayoutItems( + private Task Build(Playout playout, PlayoutBuildMode mode, PlayoutParameters parameters) => + mode switch + { + PlayoutBuildMode.Refresh => RefreshPlayout(playout, parameters), + PlayoutBuildMode.Reset => ResetPlayout(playout, parameters), + _ => ContinuePlayout(playout, parameters) + }; + + internal async Task Build( Playout playout, - DateTimeOffset playoutStart, - DateTimeOffset playoutFinish, - bool rebuild = false) + PlayoutBuildMode mode, + DateTimeOffset start, + DateTimeOffset finish) + { + foreach (PlayoutParameters parameters in await Validate(playout)) + { + return await Build(playout, mode, parameters with { Start = start, Finish = finish }); + } + + return playout; + } + + private async Task RefreshPlayout(Playout playout, PlayoutParameters parameters) + { + _logger.LogDebug( + "Refreshing playout {PlayoutId} for channel {ChannelNumber} - {ChannelName}", + playout.Id, + playout.Channel.Number, + playout.Channel.Name); + + playout.Items.Clear(); + playout.Anchor = null; + + // foreach (PlayoutProgramScheduleAnchor anchor in playout.ProgramScheduleAnchors) + // { + // anchor.Playout = null; + // anchor.Collection = null; + // anchor.ProgramSchedule = null; + // } + + // _logger.LogDebug("All anchors: {@Anchors}", playout.ProgramScheduleAnchors); + + // remove null anchor date ("continue" anchors) + playout.ProgramScheduleAnchors.RemoveAll(a => a.AnchorDate is null); + + // _logger.LogDebug("Checkpoint anchors: {@Anchors}", playout.ProgramScheduleAnchors); + + // remove old checkpoints + playout.ProgramScheduleAnchors.RemoveAll( + a => a.AnchorDateOffset.IfNone(SystemTime.MaxValueUtc) < parameters.Start.Date); + + // remove new checkpoints + playout.ProgramScheduleAnchors.RemoveAll( + a => a.AnchorDateOffset.IfNone(SystemTime.MinValueUtc).Date > parameters.Start.Date); + + // _logger.LogDebug("Remaining anchors: {@Anchors}", playout.ProgramScheduleAnchors); + + var allAnchors = playout.ProgramScheduleAnchors.ToList(); + + var collectionIds = playout.ProgramScheduleAnchors.Map(a => Optional(a.CollectionId)).Somes().ToHashSet(); + + var multiCollectionIds = + playout.ProgramScheduleAnchors.Map(a => Optional(a.MultiCollectionId)).Somes().ToHashSet(); + + var smartCollectionIds = + playout.ProgramScheduleAnchors.Map(a => Optional(a.SmartCollectionId)).Somes().ToHashSet(); + + var mediaItemIds = playout.ProgramScheduleAnchors.Map(a => Optional(a.MediaItemId)).Somes().ToHashSet(); + + playout.ProgramScheduleAnchors.Clear(); + + foreach (int collectionId in collectionIds) + { + PlayoutProgramScheduleAnchor minAnchor = allAnchors.Filter(a => a.CollectionId == collectionId) + .MinBy(a => a.AnchorDateOffset.IfNone(DateTimeOffset.MaxValue).Ticks); + playout.ProgramScheduleAnchors.Add(minAnchor); + } + + foreach (int multiCollectionId in multiCollectionIds) + { + PlayoutProgramScheduleAnchor minAnchor = allAnchors.Filter(a => a.MultiCollectionId == multiCollectionId) + .MinBy(a => a.AnchorDateOffset.IfNone(DateTimeOffset.MaxValue).Ticks); + playout.ProgramScheduleAnchors.Add(minAnchor); + } + + foreach (int smartCollectionId in smartCollectionIds) + { + PlayoutProgramScheduleAnchor minAnchor = allAnchors.Filter(a => a.SmartCollectionId == smartCollectionId) + .MinBy(a => a.AnchorDateOffset.IfNone(DateTimeOffset.MaxValue).Ticks); + playout.ProgramScheduleAnchors.Add(minAnchor); + } + + foreach (int mediaItemId in mediaItemIds) + { + PlayoutProgramScheduleAnchor minAnchor = allAnchors.Filter(a => a.MediaItemId == mediaItemId) + .MinBy(a => a.AnchorDateOffset.IfNone(DateTimeOffset.MaxValue).Ticks); + playout.ProgramScheduleAnchors.Add(minAnchor); + } + + // _logger.LogDebug("Oldest anchors for each collection: {@Anchors}", playout.ProgramScheduleAnchors); + + // convert checkpoints to non-checkpoints + // foreach (PlayoutProgramScheduleAnchor anchor in playout.ProgramScheduleAnchors) + // { + // anchor.AnchorDate = null; + // } + + // _logger.LogDebug("Final anchors: {@Anchors}", playout.ProgramScheduleAnchors); + + Option maybeAnchorDate = playout.ProgramScheduleAnchors + .Map(a => Optional(a.AnchorDate)) + .Somes() + .HeadOrNone(); + + foreach (DateTime anchorDate in maybeAnchorDate) + { + playout.Anchor = new PlayoutAnchor + { + NextStart = anchorDate + }; + } + + return await BuildPlayoutItems( + playout, + parameters.Start, + parameters.Finish, + parameters.CollectionMediaItems); + } + + private async Task ResetPlayout(Playout playout, PlayoutParameters parameters) + { + _logger.LogDebug( + "Resetting playout {PlayoutId} for channel {ChannelNumber} - {ChannelName}", + playout.Id, + playout.Channel.Number, + playout.Channel.Name); + + playout.Items.Clear(); + playout.Anchor = null; + playout.ProgramScheduleAnchors.Clear(); + + await BuildPlayoutItems( + playout, + parameters.Start, + parameters.Finish, + parameters.CollectionMediaItems); + + return playout; + } + + private async Task ContinuePlayout(Playout playout, PlayoutParameters parameters) + { + _logger.LogDebug( + "Building playout {PlayoutId} for channel {ChannelNumber} - {ChannelName}", + playout.Id, + playout.Channel.Number, + playout.Channel.Name); + + await BuildPlayoutItems( + playout, + parameters.Start, + parameters.Finish, + parameters.CollectionMediaItems); + + return playout; + } + + private async Task> Validate(Playout playout) { Map> collectionMediaItems = await GetCollectionMediaItems(playout); if (!collectionMediaItems.Any()) @@ -53,16 +225,9 @@ public class PlayoutBuilder : IPlayoutBuilder playout.Channel.Name, playout.ProgramSchedule.Name); - return playout; + return None; } - _logger.LogDebug( - "{Action} playout {PlayoutId} for channel {ChannelNumber} - {ChannelName}", - rebuild ? "Rebuilding" : "Building", - playout.Id, - playout.Channel.Number, - playout.Channel.Name); - Option maybeEmptyCollection = await CheckForEmptyCollections(collectionMediaItems); foreach (CollectionKey emptyCollection in maybeEmptyCollection) { @@ -84,19 +249,76 @@ public class PlayoutBuilder : IPlayoutBuilder emptyCollection); } - return playout; + return None; } playout.Items ??= new List(); playout.ProgramScheduleAnchors ??= new List(); - if (rebuild) + Option daysToBuild = await _configElementRepository.GetValue(ConfigElementKey.PlayoutDaysToBuild); + + DateTimeOffset now = DateTimeOffset.Now; + + return new PlayoutParameters( + now, + now.AddDays(await daysToBuild.IfNoneAsync(2)), + collectionMediaItems); + } + + private async Task BuildPlayoutItems( + Playout playout, + DateTimeOffset playoutStart, + DateTimeOffset playoutFinish, + Map> collectionMediaItems) + { + DateTimeOffset trimBefore = playoutStart.AddHours(-4); + DateTimeOffset trimAfter = playoutFinish; + + DateTimeOffset start = playoutStart; + DateTimeOffset finish = playoutStart.Date.AddDays(1); + + _logger.LogDebug( + "Trim before: {TrimBefore}, Start: {Start}, Finish: {Finish}, PlayoutFinish: {PlayoutFinish}", + trimBefore, + start, + finish, + playoutFinish); + + // build each day with "continue" anchors + while (finish < playoutFinish) + { + _logger.LogDebug("Building playout from {Start} to {Finish}", start, finish); + playout = await BuildPlayoutItems(playout, start, finish, collectionMediaItems, true); + + start = playout.Anchor.NextStartOffset; + finish = finish.AddDays(1); + } + + if (start < playoutFinish) { - playout.Items.Clear(); - playout.Anchor = null; - playout.ProgramScheduleAnchors.Clear(); + // build one final time without continue anchors + _logger.LogDebug("Building final playout from {Start} to {Finish}", start, playoutFinish); + playout = await BuildPlayoutItems( + playout, + start, + playoutFinish, + collectionMediaItems, + false); } + // remove any items outside the desired range + playout.Items.RemoveAll(old => old.FinishOffset < trimBefore || old.StartOffset > trimAfter); + + return playout; + } + + private async Task BuildPlayoutItems( + Playout playout, + DateTimeOffset playoutStart, + DateTimeOffset playoutFinish, + Map> collectionMediaItems, + bool saveAnchorDate) + { var sortedScheduleItems = playout.ProgramSchedule.Items.OrderBy(i => i.Index).ToList(); CollectionEnumeratorState scheduleItemsEnumeratorState = playout.Anchor?.ScheduleItemsEnumeratorState ?? new CollectionEnumeratorState @@ -122,13 +344,13 @@ public class PlayoutBuilder : IPlayoutBuilder // start at the previously-decided time DateTimeOffset currentTime = startAnchor.NextStartOffset.ToLocalTime(); - _logger.LogDebug( - "Starting playout {PlayoutId} for channel {ChannelNumber} - {ChannelName} at {StartTime}", - playout.Id, - playout.Channel.Number, - playout.Channel.Name, - currentTime); - + // _logger.LogDebug( + // "Starting playout ({PlayoutId}) for channel {ChannelNumber} - {ChannelName} at {StartTime}", + // playout.Id, + // playout.Channel.Number, + // playout.Channel.Name, + // currentTime); + // removing any items scheduled past the start anchor // this could happen if the app was closed after scheduling items // but before saving the anchor @@ -204,13 +426,6 @@ public class PlayoutBuilder : IPlayoutBuilder // once more to get playout anchor ProgramScheduleItem anchorScheduleItem = playoutBuilderState.ScheduleItemsEnumerator.Current; - // build program schedule anchors - playout.ProgramScheduleAnchors = BuildProgramScheduleAnchors(playout, collectionEnumerators); - - // remove any items outside the desired range - playout.Items.RemoveAll( - old => old.FinishOffset < playoutStart.AddHours(-4) || old.StartOffset > playoutFinish); - if (playout.Items.Any()) { DateTimeOffset maxStartTime = playout.Items.Max(i => i.FinishOffset); @@ -223,19 +438,27 @@ public class PlayoutBuilder : IPlayoutBuilder playout.Anchor = new PlayoutAnchor { ScheduleItemsEnumeratorState = playoutBuilderState.ScheduleItemsEnumerator.State, - NextStart = PlayoutModeSchedulerBase.GetStartTimeAfter(playoutBuilderState, anchorScheduleItem) + NextStart = PlayoutModeSchedulerBase + .GetStartTimeAfter(playoutBuilderState, anchorScheduleItem) .UtcDateTime, - MultipleRemaining = playoutBuilderState.MultipleRemaining.IsSome - ? playoutBuilderState.MultipleRemaining.ValueUnsafe() - : null, - DurationFinish = playoutBuilderState.DurationFinish.IsSome - ? playoutBuilderState.DurationFinish.ValueUnsafe().UtcDateTime - : null, InFlood = playoutBuilderState.InFlood, InDurationFiller = playoutBuilderState.InDurationFiller, NextGuideGroup = playoutBuilderState.NextGuideGroup }; + foreach (int multipleRemaining in playoutBuilderState.MultipleRemaining) + { + playout.Anchor.MultipleRemaining = multipleRemaining; + } + + foreach (DateTimeOffset durationFinish in playoutBuilderState.DurationFinish) + { + playout.Anchor.DurationFinish = durationFinish.UtcDateTime; + } + + // build program schedule anchors + playout.ProgramScheduleAnchors = BuildProgramScheduleAnchors(playout, collectionEnumerators, saveAnchorDate); + return playout; } @@ -311,7 +534,7 @@ public class PlayoutBuilder : IPlayoutBuilder return collectionMediaItems.Find(c => !c.Value.Any()).Map(c => c.Key); } - + private static PlayoutAnchor FindStartAnchor( Playout playout, DateTimeOffset start, @@ -341,7 +564,8 @@ public class PlayoutBuilder : IPlayoutBuilder private static List BuildProgramScheduleAnchors( Playout playout, - Dictionary collectionEnumerators) + Dictionary collectionEnumerators, + bool saveAnchorDate) { var result = new List(); @@ -350,7 +574,8 @@ public class PlayoutBuilder : IPlayoutBuilder Option maybeExisting = playout.ProgramScheduleAnchors.FirstOrDefault( a => a.CollectionType == collectionKey.CollectionType && a.CollectionId == collectionKey.CollectionId - && a.MediaItemId == collectionKey.MediaItemId); + && a.MediaItemId == collectionKey.MediaItemId + && a.AnchorDate is null); var maybeEnumeratorState = collectionEnumerators.GroupBy(e => e.Key, e => e.Value.State).ToDictionary( mcs => mcs.Key, @@ -376,9 +601,20 @@ public class PlayoutBuilder : IPlayoutBuilder EnumeratorState = maybeEnumeratorState[collectionKey] }); + if (saveAnchorDate) + { + scheduleAnchor.AnchorDate = playout.Anchor?.NextStart; + } + result.Add(scheduleAnchor); } + foreach (PlayoutProgramScheduleAnchor continueAnchor in playout.ProgramScheduleAnchors.Where( + a => a.AnchorDate is not null)) + { + result.Add(continueAnchor); + } + return result; } @@ -416,7 +652,7 @@ public class PlayoutBuilder : IPlayoutBuilder state); } } - + switch (playbackOrder) { case PlaybackOrder.Chronological: diff --git a/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerDuration.cs b/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerDuration.cs index fbe56608..6545cd33 100644 --- a/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerDuration.cs +++ b/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerDuration.cs @@ -74,9 +74,9 @@ public class PlayoutModeSchedulerDuration : PlayoutModeSchedulerBase playoutItem.GuideFinish = du.UtcDateTime); - + + durationUntil.Do(du => playoutItem.GuideFinish = du.UtcDateTime); + DateTimeOffset durationFinish = nextState.DurationFinish.IfNone(SystemTime.MaxValueUtc); DateTimeOffset itemEndTimeWithFiller = CalculateEndTimeWithFiller( collectionEnumerators, @@ -192,7 +192,7 @@ public class PlayoutModeSchedulerDuration : PlayoutModeSchedulerBase p.FinishOffset); if (Math.Abs((itemEndTimeWithFiller - actualEndTime).TotalSeconds) > 1) @@ -97,9 +97,9 @@ public class PlayoutModeSchedulerFlood : PlayoutModeSchedulerBase(); - + PlayoutBuilderState nextState = playoutBuilderState with { MultipleRemaining = playoutBuilderState.MultipleRemaining.IfNone(scheduleItem.Count) @@ -74,7 +74,7 @@ public class PlayoutModeSchedulerMultiple : PlayoutModeSchedulerBase()); } } \ No newline at end of file diff --git a/ErsatzTV.Core/Scheduling/PlayoutParameters.cs b/ErsatzTV.Core/Scheduling/PlayoutParameters.cs new file mode 100644 index 00000000..d61037ec --- /dev/null +++ b/ErsatzTV.Core/Scheduling/PlayoutParameters.cs @@ -0,0 +1,8 @@ +using ErsatzTV.Core.Domain; + +namespace ErsatzTV.Core.Scheduling; + +public record PlayoutParameters( + DateTimeOffset Start, + DateTimeOffset Finish, + Map> CollectionMediaItems); \ No newline at end of file diff --git a/ErsatzTV.Infrastructure/Data/Configurations/PlayoutProgramScheduleAnchorConfiguration.cs b/ErsatzTV.Infrastructure/Data/Configurations/PlayoutProgramScheduleAnchorConfiguration.cs index 660bf230..a16acedc 100644 --- a/ErsatzTV.Infrastructure/Data/Configurations/PlayoutProgramScheduleAnchorConfiguration.cs +++ b/ErsatzTV.Infrastructure/Data/Configurations/PlayoutProgramScheduleAnchorConfiguration.cs @@ -29,5 +29,8 @@ public class PlayoutProgramScheduleAnchorConfiguration : IEntityTypeConfiguratio .HasForeignKey(i => i.MediaItemId) .OnDelete(DeleteBehavior.Cascade) .IsRequired(false); + + builder.Property(i => i.AnchorDate) + .IsRequired(false); } } \ No newline at end of file diff --git a/ErsatzTV.Infrastructure/Migrations/20220209084802_Add_PlayoutProgramScheduleAnchor_AnchorDate.Designer.cs b/ErsatzTV.Infrastructure/Migrations/20220209084802_Add_PlayoutProgramScheduleAnchor_AnchorDate.Designer.cs new file mode 100644 index 00000000..a354b610 --- /dev/null +++ b/ErsatzTV.Infrastructure/Migrations/20220209084802_Add_PlayoutProgramScheduleAnchor_AnchorDate.Designer.cs @@ -0,0 +1,3864 @@ +// +using System; +using ErsatzTV.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace ErsatzTV.Infrastructure.Migrations +{ + [DbContext(typeof(TvContext))] + [Migration("20220209084802_Add_PlayoutProgramScheduleAnchor_AnchorDate")] + partial class Add_PlayoutProgramScheduleAnchor_AnchorDate + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "6.0.1"); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Actor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArtistMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ArtworkId") + .HasColumnType("INTEGER"); + + b.Property("EpisodeMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MovieMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MusicVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("OtherVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Role") + .HasColumnType("TEXT"); + + b.Property("SeasonMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ShowMetadataId") + .HasColumnType("INTEGER"); + + b.Property("SongMetadataId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ArtistMetadataId"); + + b.HasIndex("ArtworkId") + .IsUnique(); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("OtherVideoMetadataId"); + + b.HasIndex("SeasonMetadataId"); + + b.HasIndex("ShowMetadataId"); + + b.HasIndex("SongMetadataId"); + + b.ToTable("Actor", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ArtistMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArtistId") + .HasColumnType("INTEGER"); + + b.Property("Biography") + .HasColumnType("TEXT"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("Disambiguation") + .HasColumnType("TEXT"); + + b.Property("Formed") + .HasColumnType("TEXT"); + + b.Property("MetadataKind") + .HasColumnType("INTEGER"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("SortTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ArtistId"); + + b.ToTable("ArtistMetadata", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Artwork", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArtistMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ArtworkKind") + .HasColumnType("INTEGER"); + + b.Property("BlurHash43") + .HasColumnType("TEXT"); + + b.Property("BlurHash54") + .HasColumnType("TEXT"); + + b.Property("BlurHash64") + .HasColumnType("TEXT"); + + b.Property("ChannelId") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("EpisodeMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MovieMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MusicVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("OtherVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("SeasonMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ShowMetadataId") + .HasColumnType("INTEGER"); + + b.Property("SongMetadataId") + .HasColumnType("INTEGER"); + + b.Property("SourcePath") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ArtistMetadataId"); + + b.HasIndex("ChannelId"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("OtherVideoMetadataId"); + + b.HasIndex("SeasonMetadataId"); + + b.HasIndex("ShowMetadataId"); + + b.HasIndex("SongMetadataId"); + + b.ToTable("Artwork", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Channel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("FFmpegProfileId") + .HasColumnType("INTEGER"); + + b.Property("FallbackFillerId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("TEXT"); + + b.Property("PreferredLanguageCode") + .HasColumnType("TEXT"); + + b.Property("StreamingMode") + .HasColumnType("INTEGER"); + + b.Property("UniqueId") + .HasColumnType("TEXT"); + + b.Property("WatermarkId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("FFmpegProfileId"); + + b.HasIndex("FallbackFillerId"); + + b.HasIndex("Number") + .IsUnique(); + + b.HasIndex("WatermarkId"); + + b.ToTable("Channel", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ChannelWatermark", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DurationSeconds") + .HasColumnType("INTEGER"); + + b.Property("FrequencyMinutes") + .HasColumnType("INTEGER"); + + b.Property("HorizontalMarginPercent") + .HasColumnType("INTEGER"); + + b.Property("Image") + .HasColumnType("TEXT"); + + b.Property("ImageSource") + .HasColumnType("INTEGER"); + + b.Property("Location") + .HasColumnType("INTEGER"); + + b.Property("Mode") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Opacity") + .HasColumnType("INTEGER"); + + b.Property("Size") + .HasColumnType("INTEGER"); + + b.Property("VerticalMarginPercent") + .HasColumnType("INTEGER"); + + b.Property("WidthPercent") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("ChannelWatermark", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Collection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("UseCustomPlaybackOrder") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.CollectionItem", b => + { + b.Property("CollectionId") + .HasColumnType("INTEGER"); + + b.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b.Property("CustomIndex") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionId", "MediaItemId"); + + b.HasIndex("MediaItemId"); + + b.ToTable("CollectionItem", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ConfigElement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("ConfigElement", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Director", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("EpisodeMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MovieMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.ToTable("Director", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Address") + .HasColumnType("TEXT"); + + b.Property("EmbyMediaSourceId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("EmbyMediaSourceId"); + + b.ToTable("EmbyConnection", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyPathReplacement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("EmbyMediaSourceId") + .HasColumnType("INTEGER"); + + b.Property("EmbyPath") + .HasColumnType("TEXT"); + + b.Property("LocalPath") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("EmbyMediaSourceId"); + + b.ToTable("EmbyPathReplacement", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EpisodeMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("EpisodeId") + .HasColumnType("INTEGER"); + + b.Property("EpisodeNumber") + .HasColumnType("INTEGER"); + + b.Property("MetadataKind") + .HasColumnType("INTEGER"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("Outline") + .HasColumnType("TEXT"); + + b.Property("Plot") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("SortTitle") + .HasColumnType("TEXT"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("EpisodeId"); + + b.ToTable("EpisodeMetadata", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.FFmpegProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AudioBitrate") + .HasColumnType("INTEGER"); + + b.Property("AudioBufferSize") + .HasColumnType("INTEGER"); + + b.Property("AudioChannels") + .HasColumnType("INTEGER"); + + b.Property("AudioCodec") + .HasColumnType("TEXT"); + + b.Property("AudioSampleRate") + .HasColumnType("INTEGER"); + + b.Property("HardwareAcceleration") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizeAudio") + .HasColumnType("INTEGER"); + + b.Property("NormalizeFramerate") + .HasColumnType("INTEGER"); + + b.Property("NormalizeLoudness") + .HasColumnType("INTEGER"); + + b.Property("NormalizeVideo") + .HasColumnType("INTEGER"); + + b.Property("ResolutionId") + .HasColumnType("INTEGER"); + + b.Property("ThreadCount") + .HasColumnType("INTEGER"); + + b.Property("Transcode") + .HasColumnType("INTEGER"); + + b.Property("VaapiDevice") + .HasColumnType("TEXT"); + + b.Property("VaapiDriver") + .HasColumnType("INTEGER"); + + b.Property("VideoBitrate") + .HasColumnType("INTEGER"); + + b.Property("VideoBufferSize") + .HasColumnType("INTEGER"); + + b.Property("VideoCodec") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ResolutionId"); + + b.ToTable("FFmpegProfile", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Filler.FillerPreset", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CollectionId") + .HasColumnType("INTEGER"); + + b.Property("CollectionType") + .HasColumnType("INTEGER"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.Property("Duration") + .HasColumnType("TEXT"); + + b.Property("FillerKind") + .HasColumnType("INTEGER"); + + b.Property("FillerMode") + .HasColumnType("INTEGER"); + + b.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b.Property("MultiCollectionId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("PadToNearestMinute") + .HasColumnType("INTEGER"); + + b.Property("SmartCollectionId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CollectionId"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("MultiCollectionId"); + + b.HasIndex("SmartCollectionId"); + + b.ToTable("FillerPreset", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Genre", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArtistMetadataId") + .HasColumnType("INTEGER"); + + b.Property("EpisodeMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MovieMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MusicVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OtherVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("SeasonMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ShowMetadataId") + .HasColumnType("INTEGER"); + + b.Property("SongMetadataId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ArtistMetadataId"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("OtherVideoMetadataId"); + + b.HasIndex("SeasonMetadataId"); + + b.HasIndex("ShowMetadataId"); + + b.HasIndex("SongMetadataId"); + + b.ToTable("Genre"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Address") + .HasColumnType("TEXT"); + + b.Property("JellyfinMediaSourceId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("JellyfinMediaSourceId"); + + b.ToTable("JellyfinConnection", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinPathReplacement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("JellyfinMediaSourceId") + .HasColumnType("INTEGER"); + + b.Property("JellyfinPath") + .HasColumnType("TEXT"); + + b.Property("LocalPath") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("JellyfinMediaSourceId"); + + b.ToTable("JellyfinPathReplacement", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LanguageCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("EnglishName") + .HasColumnType("TEXT"); + + b.Property("FrenchName") + .HasColumnType("TEXT"); + + b.Property("ThreeCode1") + .HasColumnType("TEXT"); + + b.Property("ThreeCode2") + .HasColumnType("TEXT"); + + b.Property("TwoCode") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("LanguageCode", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Library", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastScan") + .HasColumnType("TEXT"); + + b.Property("MediaKind") + .HasColumnType("INTEGER"); + + b.Property("MediaSourceId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MediaSourceId"); + + b.ToTable("Library", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LibraryFolder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Etag") + .HasColumnType("TEXT"); + + b.Property("LibraryPathId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryPathId"); + + b.ToTable("LibraryFolder", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LibraryPath", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastScan") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("LibraryPath", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaChapter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("EndTime") + .HasColumnType("TEXT"); + + b.Property("MediaVersionId") + .HasColumnType("INTEGER"); + + b.Property("StartTime") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MediaVersionId"); + + b.ToTable("MediaChapter", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("MediaVersionId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MediaVersionId"); + + b.HasIndex("Path") + .IsUnique(); + + b.ToTable("MediaFile", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LibraryPathId") + .HasColumnType("INTEGER"); + + b.Property("State") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("LibraryPathId"); + + b.ToTable("MediaItem", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("MediaSource", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AttachedPic") + .HasColumnType("INTEGER"); + + b.Property("BitsPerRawSample") + .HasColumnType("INTEGER"); + + b.Property("Channels") + .HasColumnType("INTEGER"); + + b.Property("Codec") + .HasColumnType("TEXT"); + + b.Property("Default") + .HasColumnType("INTEGER"); + + b.Property("Forced") + .HasColumnType("INTEGER"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("MediaStreamKind") + .HasColumnType("INTEGER"); + + b.Property("MediaVersionId") + .HasColumnType("INTEGER"); + + b.Property("PixelFormat") + .HasColumnType("TEXT"); + + b.Property("Profile") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MediaVersionId"); + + b.ToTable("MediaStream", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("DisplayAspectRatio") + .HasColumnType("TEXT"); + + b.Property("Duration") + .HasColumnType("TEXT"); + + b.Property("EpisodeId") + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("MovieId") + .HasColumnType("INTEGER"); + + b.Property("MusicVideoId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OtherVideoId") + .HasColumnType("INTEGER"); + + b.Property("RFrameRate") + .HasColumnType("TEXT"); + + b.Property("SampleAspectRatio") + .HasColumnType("TEXT"); + + b.Property("SongId") + .HasColumnType("INTEGER"); + + b.Property("VideoScanKind") + .HasColumnType("INTEGER"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("EpisodeId"); + + b.HasIndex("MovieId"); + + b.HasIndex("MusicVideoId"); + + b.HasIndex("OtherVideoId"); + + b.HasIndex("SongId"); + + b.ToTable("MediaVersion", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MetadataGuid", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArtistMetadataId") + .HasColumnType("INTEGER"); + + b.Property("EpisodeMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Guid") + .HasColumnType("TEXT"); + + b.Property("MovieMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MusicVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("OtherVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("SeasonMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ShowMetadataId") + .HasColumnType("INTEGER"); + + b.Property("SongMetadataId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ArtistMetadataId"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("OtherVideoMetadataId"); + + b.HasIndex("SeasonMetadataId"); + + b.HasIndex("ShowMetadataId"); + + b.HasIndex("SongMetadataId"); + + b.ToTable("MetadataGuid", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Mood", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArtistMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ArtistMetadataId"); + + b.ToTable("Mood"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MovieMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ContentRating") + .HasColumnType("TEXT"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("MetadataKind") + .HasColumnType("INTEGER"); + + b.Property("MovieId") + .HasColumnType("INTEGER"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("Outline") + .HasColumnType("TEXT"); + + b.Property("Plot") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("SortTitle") + .HasColumnType("TEXT"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("MovieId"); + + b.ToTable("MovieMetadata", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MultiCollection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("MultiCollection", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MultiCollectionItem", b => + { + b.Property("MultiCollectionId") + .HasColumnType("INTEGER"); + + b.Property("CollectionId") + .HasColumnType("INTEGER"); + + b.Property("PlaybackOrder") + .HasColumnType("INTEGER"); + + b.Property("ScheduleAsGroup") + .HasColumnType("INTEGER"); + + b.HasKey("MultiCollectionId", "CollectionId"); + + b.HasIndex("CollectionId"); + + b.ToTable("MultiCollectionItem", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MultiCollectionSmartItem", b => + { + b.Property("MultiCollectionId") + .HasColumnType("INTEGER"); + + b.Property("SmartCollectionId") + .HasColumnType("INTEGER"); + + b.Property("PlaybackOrder") + .HasColumnType("INTEGER"); + + b.Property("ScheduleAsGroup") + .HasColumnType("INTEGER"); + + b.HasKey("MultiCollectionId", "SmartCollectionId"); + + b.HasIndex("SmartCollectionId"); + + b.ToTable("MultiCollectionSmartItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideoMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Album") + .HasColumnType("TEXT"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("MetadataKind") + .HasColumnType("INTEGER"); + + b.Property("MusicVideoId") + .HasColumnType("INTEGER"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("Plot") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("SortTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("MusicVideoId"); + + b.ToTable("MusicVideoMetadata", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.OtherVideoMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("MetadataKind") + .HasColumnType("INTEGER"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("OtherVideoId") + .HasColumnType("INTEGER"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("SortTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OtherVideoId"); + + b.ToTable("OtherVideoMetadata", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("INTEGER"); + + b.Property("DailyRebuildTime") + .HasColumnType("TEXT"); + + b.Property("ProgramScheduleId") + .HasColumnType("INTEGER"); + + b.Property("ProgramSchedulePlayoutType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChannelId"); + + b.HasIndex("ProgramScheduleId"); + + b.ToTable("Playout", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterTitle") + .HasColumnType("TEXT"); + + b.Property("CustomTitle") + .HasColumnType("TEXT"); + + b.Property("FillerKind") + .HasColumnType("INTEGER"); + + b.Property("Finish") + .HasColumnType("TEXT"); + + b.Property("GuideFinish") + .HasColumnType("TEXT"); + + b.Property("GuideGroup") + .HasColumnType("INTEGER"); + + b.Property("InPoint") + .HasColumnType("TEXT"); + + b.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b.Property("OutPoint") + .HasColumnType("TEXT"); + + b.Property("PlayoutId") + .HasColumnType("INTEGER"); + + b.Property("Start") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("PlayoutId"); + + b.ToTable("PlayoutItem", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutProgramScheduleAnchor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AnchorDate") + .HasColumnType("TEXT"); + + b.Property("CollectionId") + .HasColumnType("INTEGER"); + + b.Property("CollectionType") + .HasColumnType("INTEGER"); + + b.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b.Property("MultiCollectionId") + .HasColumnType("INTEGER"); + + b.Property("PlayoutId") + .HasColumnType("INTEGER"); + + b.Property("ProgramScheduleId") + .HasColumnType("INTEGER"); + + b.Property("SmartCollectionId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CollectionId"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("MultiCollectionId"); + + b.HasIndex("PlayoutId"); + + b.HasIndex("ProgramScheduleId"); + + b.HasIndex("SmartCollectionId"); + + b.ToTable("PlayoutProgramScheduleAnchor", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("PlexMediaSourceId") + .HasColumnType("INTEGER"); + + b.Property("Uri") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PlexMediaSourceId"); + + b.ToTable("PlexConnection", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexPathReplacement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LocalPath") + .HasColumnType("TEXT"); + + b.Property("PlexMediaSourceId") + .HasColumnType("INTEGER"); + + b.Property("PlexPath") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PlexMediaSourceId"); + + b.ToTable("PlexPathReplacement", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("KeepMultiPartEpisodesTogether") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("TreatCollectionsAsShows") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("ProgramSchedule", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CollectionId") + .HasColumnType("INTEGER"); + + b.Property("CollectionType") + .HasColumnType("INTEGER"); + + b.Property("CustomTitle") + .HasColumnType("TEXT"); + + b.Property("FallbackFillerId") + .HasColumnType("INTEGER"); + + b.Property("GuideMode") + .HasColumnType("INTEGER"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b.Property("MidRollFillerId") + .HasColumnType("INTEGER"); + + b.Property("MultiCollectionId") + .HasColumnType("INTEGER"); + + b.Property("PlaybackOrder") + .HasColumnType("INTEGER"); + + b.Property("PostRollFillerId") + .HasColumnType("INTEGER"); + + b.Property("PreRollFillerId") + .HasColumnType("INTEGER"); + + b.Property("ProgramScheduleId") + .HasColumnType("INTEGER"); + + b.Property("SmartCollectionId") + .HasColumnType("INTEGER"); + + b.Property("StartTime") + .HasColumnType("TEXT"); + + b.Property("TailFillerId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CollectionId"); + + b.HasIndex("FallbackFillerId"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("MidRollFillerId"); + + b.HasIndex("MultiCollectionId"); + + b.HasIndex("PostRollFillerId"); + + b.HasIndex("PreRollFillerId"); + + b.HasIndex("ProgramScheduleId"); + + b.HasIndex("SmartCollectionId"); + + b.HasIndex("TailFillerId"); + + b.ToTable("ProgramScheduleItem", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Resolution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Resolution", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.SeasonMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("MetadataKind") + .HasColumnType("INTEGER"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("Outline") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("SeasonId") + .HasColumnType("INTEGER"); + + b.Property("SortTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId"); + + b.ToTable("SeasonMetadata", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ShowMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ContentRating") + .HasColumnType("TEXT"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("MetadataKind") + .HasColumnType("INTEGER"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("Outline") + .HasColumnType("TEXT"); + + b.Property("Plot") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("ShowId") + .HasColumnType("INTEGER"); + + b.Property("SortTitle") + .HasColumnType("TEXT"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ShowId"); + + b.ToTable("ShowMetadata", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.SmartCollection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Query") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("SmartCollection", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.SongMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Album") + .HasColumnType("TEXT"); + + b.Property("Artist") + .HasColumnType("TEXT"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("MetadataKind") + .HasColumnType("INTEGER"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("SongId") + .HasColumnType("INTEGER"); + + b.Property("SortTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Track") + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SongId"); + + b.ToTable("SongMetadata", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Studio", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArtistMetadataId") + .HasColumnType("INTEGER"); + + b.Property("EpisodeMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MovieMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MusicVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OtherVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("SeasonMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ShowMetadataId") + .HasColumnType("INTEGER"); + + b.Property("SongMetadataId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ArtistMetadataId"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("OtherVideoMetadataId"); + + b.HasIndex("SeasonMetadataId"); + + b.HasIndex("ShowMetadataId"); + + b.HasIndex("SongMetadataId"); + + b.ToTable("Studio", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Style", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArtistMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ArtistMetadataId"); + + b.ToTable("Style"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArtistMetadataId") + .HasColumnType("INTEGER"); + + b.Property("EpisodeMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MovieMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MusicVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OtherVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("SeasonMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ShowMetadataId") + .HasColumnType("INTEGER"); + + b.Property("SongMetadataId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ArtistMetadataId"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("OtherVideoMetadataId"); + + b.HasIndex("SeasonMetadataId"); + + b.HasIndex("ShowMetadataId"); + + b.HasIndex("SongMetadataId"); + + b.ToTable("Tag"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TraktList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("ItemCount") + .HasColumnType("INTEGER"); + + b.Property("List") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("TraktId") + .HasColumnType("INTEGER"); + + b.Property("User") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("TraktList", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TraktListItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Episode") + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b.Property("Rank") + .HasColumnType("INTEGER"); + + b.Property("Season") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TraktId") + .HasColumnType("INTEGER"); + + b.Property("TraktListId") + .HasColumnType("INTEGER"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("TraktListId"); + + b.ToTable("TraktListItem", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TraktListItemGuid", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Guid") + .HasColumnType("TEXT"); + + b.Property("TraktListItemId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("TraktListItemId"); + + b.ToTable("TraktListItemGuid", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Writer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("EpisodeMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MovieMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.ToTable("Writer", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Artist", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.ToTable("Artist", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyLibrary", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Library"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ShouldSyncItems") + .HasColumnType("INTEGER"); + + b.ToTable("EmbyLibrary", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyMediaSource", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaSource"); + + b.Property("OperatingSystem") + .HasColumnType("TEXT"); + + b.Property("ServerName") + .HasColumnType("TEXT"); + + b.ToTable("EmbyMediaSource", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Episode", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.Property("SeasonId") + .HasColumnType("INTEGER"); + + b.HasIndex("SeasonId"); + + b.ToTable("Episode", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinLibrary", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Library"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ShouldSyncItems") + .HasColumnType("INTEGER"); + + b.ToTable("JellyfinLibrary", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinMediaSource", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaSource"); + + b.Property("OperatingSystem") + .HasColumnType("TEXT"); + + b.Property("ServerName") + .HasColumnType("TEXT"); + + b.ToTable("JellyfinMediaSource", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LocalLibrary", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Library"); + + b.ToTable("LocalLibrary", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LocalMediaSource", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaSource"); + + b.ToTable("LocalMediaSource", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Movie", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.ToTable("Movie", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideo", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.Property("ArtistId") + .HasColumnType("INTEGER"); + + b.HasIndex("ArtistId"); + + b.ToTable("MusicVideo", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.OtherVideo", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.ToTable("OtherVideo", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexLibrary", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Library"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("ShouldSyncItems") + .HasColumnType("INTEGER"); + + b.ToTable("PlexLibrary", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaFile", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaFile"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("PlexId") + .HasColumnType("INTEGER"); + + b.ToTable("PlexMediaFile", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSource", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaSource"); + + b.Property("ClientIdentifier") + .HasColumnType("TEXT"); + + b.Property("Platform") + .HasColumnType("TEXT"); + + b.Property("PlatformVersion") + .HasColumnType("TEXT"); + + b.Property("ProductVersion") + .HasColumnType("TEXT"); + + b.Property("ServerName") + .HasColumnType("TEXT"); + + b.ToTable("PlexMediaSource", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemDuration", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); + + b.Property("PlayoutDuration") + .HasColumnType("TEXT"); + + b.Property("TailMode") + .HasColumnType("INTEGER"); + + b.ToTable("ProgramScheduleDurationItem", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemFlood", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); + + b.ToTable("ProgramScheduleFloodItem", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemMultiple", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.ToTable("ProgramScheduleMultipleItem", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemOne", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); + + b.ToTable("ProgramScheduleOneItem", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Season", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.Property("ShowId") + .HasColumnType("INTEGER"); + + b.HasIndex("ShowId"); + + b.ToTable("Season", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Show", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.ToTable("Show", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Song", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.ToTable("Song", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyEpisode", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Episode"); + + b.Property("Etag") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.ToTable("EmbyEpisode", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyMovie", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Movie"); + + b.Property("Etag") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.ToTable("EmbyMovie", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbySeason", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Season"); + + b.Property("Etag") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.ToTable("EmbySeason", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyShow", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Show"); + + b.Property("Etag") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.ToTable("EmbyShow", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinEpisode", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Episode"); + + b.Property("Etag") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.ToTable("JellyfinEpisode", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinMovie", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Movie"); + + b.Property("Etag") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.ToTable("JellyfinMovie", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinSeason", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Season"); + + b.Property("Etag") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.ToTable("JellyfinSeason", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinShow", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Show"); + + b.Property("Etag") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.ToTable("JellyfinShow", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexEpisode", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Episode"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.ToTable("PlexEpisode", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMovie", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Movie"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.ToTable("PlexMovie", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexSeason", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Season"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.ToTable("PlexSeason", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexShow", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Show"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.ToTable("PlexShow", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Actor", b => + { + b.HasOne("ErsatzTV.Core.Domain.ArtistMetadata", null) + .WithMany("Actors") + .HasForeignKey("ArtistMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.Artwork", "Artwork") + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.Actor", "ArtworkId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null) + .WithMany("Actors") + .HasForeignKey("EpisodeMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MovieMetadata", null) + .WithMany("Actors") + .HasForeignKey("MovieMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MusicVideoMetadata", null) + .WithMany("Actors") + .HasForeignKey("MusicVideoMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.OtherVideoMetadata", null) + .WithMany("Actors") + .HasForeignKey("OtherVideoMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null) + .WithMany("Actors") + .HasForeignKey("SeasonMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.ShowMetadata", null) + .WithMany("Actors") + .HasForeignKey("ShowMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SongMetadata", null) + .WithMany("Actors") + .HasForeignKey("SongMetadataId"); + + b.Navigation("Artwork"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ArtistMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.Artist", "Artist") + .WithMany("ArtistMetadata") + .HasForeignKey("ArtistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Artist"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Artwork", b => + { + b.HasOne("ErsatzTV.Core.Domain.ArtistMetadata", null) + .WithMany("Artwork") + .HasForeignKey("ArtistMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.Channel", null) + .WithMany("Artwork") + .HasForeignKey("ChannelId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null) + .WithMany("Artwork") + .HasForeignKey("EpisodeMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MovieMetadata", null) + .WithMany("Artwork") + .HasForeignKey("MovieMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MusicVideoMetadata", null) + .WithMany("Artwork") + .HasForeignKey("MusicVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.OtherVideoMetadata", null) + .WithMany("Artwork") + .HasForeignKey("OtherVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null) + .WithMany("Artwork") + .HasForeignKey("SeasonMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.ShowMetadata", null) + .WithMany("Artwork") + .HasForeignKey("ShowMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SongMetadata", null) + .WithMany("Artwork") + .HasForeignKey("SongMetadataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Channel", b => + { + b.HasOne("ErsatzTV.Core.Domain.FFmpegProfile", "FFmpegProfile") + .WithMany() + .HasForeignKey("FFmpegProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.Filler.FillerPreset", "FallbackFiller") + .WithMany() + .HasForeignKey("FallbackFillerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("ErsatzTV.Core.Domain.ChannelWatermark", "Watermark") + .WithMany() + .HasForeignKey("WatermarkId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("FFmpegProfile"); + + b.Navigation("FallbackFiller"); + + b.Navigation("Watermark"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.CollectionItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.Collection", "Collection") + .WithMany("CollectionItems") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem") + .WithMany("CollectionItems") + .HasForeignKey("MediaItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("MediaItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Director", b => + { + b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null) + .WithMany("Directors") + .HasForeignKey("EpisodeMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MovieMetadata", null) + .WithMany("Directors") + .HasForeignKey("MovieMetadataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyConnection", b => + { + b.HasOne("ErsatzTV.Core.Domain.EmbyMediaSource", "EmbyMediaSource") + .WithMany("Connections") + .HasForeignKey("EmbyMediaSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EmbyMediaSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyPathReplacement", b => + { + b.HasOne("ErsatzTV.Core.Domain.EmbyMediaSource", "EmbyMediaSource") + .WithMany("PathReplacements") + .HasForeignKey("EmbyMediaSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EmbyMediaSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EpisodeMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.Episode", "Episode") + .WithMany("EpisodeMetadata") + .HasForeignKey("EpisodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Episode"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.FFmpegProfile", b => + { + b.HasOne("ErsatzTV.Core.Domain.Resolution", "Resolution") + .WithMany() + .HasForeignKey("ResolutionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Resolution"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Filler.FillerPreset", b => + { + b.HasOne("ErsatzTV.Core.Domain.Collection", "Collection") + .WithMany() + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem") + .WithMany() + .HasForeignKey("MediaItemId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MultiCollection", "MultiCollection") + .WithMany() + .HasForeignKey("MultiCollectionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SmartCollection", "SmartCollection") + .WithMany() + .HasForeignKey("SmartCollectionId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Collection"); + + b.Navigation("MediaItem"); + + b.Navigation("MultiCollection"); + + b.Navigation("SmartCollection"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Genre", b => + { + b.HasOne("ErsatzTV.Core.Domain.ArtistMetadata", null) + .WithMany("Genres") + .HasForeignKey("ArtistMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null) + .WithMany("Genres") + .HasForeignKey("EpisodeMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.MovieMetadata", null) + .WithMany("Genres") + .HasForeignKey("MovieMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MusicVideoMetadata", null) + .WithMany("Genres") + .HasForeignKey("MusicVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.OtherVideoMetadata", null) + .WithMany("Genres") + .HasForeignKey("OtherVideoMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null) + .WithMany("Genres") + .HasForeignKey("SeasonMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.ShowMetadata", null) + .WithMany("Genres") + .HasForeignKey("ShowMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SongMetadata", null) + .WithMany("Genres") + .HasForeignKey("SongMetadataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinConnection", b => + { + b.HasOne("ErsatzTV.Core.Domain.JellyfinMediaSource", "JellyfinMediaSource") + .WithMany("Connections") + .HasForeignKey("JellyfinMediaSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JellyfinMediaSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinPathReplacement", b => + { + b.HasOne("ErsatzTV.Core.Domain.JellyfinMediaSource", "JellyfinMediaSource") + .WithMany("PathReplacements") + .HasForeignKey("JellyfinMediaSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JellyfinMediaSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Library", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaSource", "MediaSource") + .WithMany("Libraries") + .HasForeignKey("MediaSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MediaSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LibraryFolder", b => + { + b.HasOne("ErsatzTV.Core.Domain.LibraryPath", "LibraryPath") + .WithMany("LibraryFolders") + .HasForeignKey("LibraryPathId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("LibraryPath"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LibraryPath", b => + { + b.HasOne("ErsatzTV.Core.Domain.Library", "Library") + .WithMany("Paths") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaChapter", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaVersion", "MediaVersion") + .WithMany("Chapters") + .HasForeignKey("MediaVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MediaVersion"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaFile", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaVersion", "MediaVersion") + .WithMany("MediaFiles") + .HasForeignKey("MediaVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MediaVersion"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.LibraryPath", "LibraryPath") + .WithMany("MediaItems") + .HasForeignKey("LibraryPathId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("LibraryPath"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaStream", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaVersion", "MediaVersion") + .WithMany("Streams") + .HasForeignKey("MediaVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MediaVersion"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaVersion", b => + { + b.HasOne("ErsatzTV.Core.Domain.Episode", null) + .WithMany("MediaVersions") + .HasForeignKey("EpisodeId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.Movie", null) + .WithMany("MediaVersions") + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MusicVideo", null) + .WithMany("MediaVersions") + .HasForeignKey("MusicVideoId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.OtherVideo", null) + .WithMany("MediaVersions") + .HasForeignKey("OtherVideoId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.Song", null) + .WithMany("MediaVersions") + .HasForeignKey("SongId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MetadataGuid", b => + { + b.HasOne("ErsatzTV.Core.Domain.ArtistMetadata", null) + .WithMany("Guids") + .HasForeignKey("ArtistMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null) + .WithMany("Guids") + .HasForeignKey("EpisodeMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MovieMetadata", null) + .WithMany("Guids") + .HasForeignKey("MovieMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MusicVideoMetadata", null) + .WithMany("Guids") + .HasForeignKey("MusicVideoMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.OtherVideoMetadata", null) + .WithMany("Guids") + .HasForeignKey("OtherVideoMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null) + .WithMany("Guids") + .HasForeignKey("SeasonMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.ShowMetadata", null) + .WithMany("Guids") + .HasForeignKey("ShowMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SongMetadata", null) + .WithMany("Guids") + .HasForeignKey("SongMetadataId"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Mood", b => + { + b.HasOne("ErsatzTV.Core.Domain.ArtistMetadata", null) + .WithMany("Moods") + .HasForeignKey("ArtistMetadataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MovieMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.Movie", "Movie") + .WithMany("MovieMetadata") + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Movie"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MultiCollectionItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.Collection", "Collection") + .WithMany("MultiCollectionItems") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.MultiCollection", "MultiCollection") + .WithMany("MultiCollectionItems") + .HasForeignKey("MultiCollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("MultiCollection"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MultiCollectionSmartItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.MultiCollection", "MultiCollection") + .WithMany("MultiCollectionSmartItems") + .HasForeignKey("MultiCollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.SmartCollection", "SmartCollection") + .WithMany("MultiCollectionSmartItems") + .HasForeignKey("SmartCollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MultiCollection"); + + b.Navigation("SmartCollection"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideoMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.MusicVideo", "MusicVideo") + .WithMany("MusicVideoMetadata") + .HasForeignKey("MusicVideoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MusicVideo"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.OtherVideoMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.OtherVideo", "OtherVideo") + .WithMany("OtherVideoMetadata") + .HasForeignKey("OtherVideoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OtherVideo"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b => + { + b.HasOne("ErsatzTV.Core.Domain.Channel", "Channel") + .WithMany("Playouts") + .HasForeignKey("ChannelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule") + .WithMany("Playouts") + .HasForeignKey("ProgramScheduleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("ErsatzTV.Core.Domain.PlayoutAnchor", "Anchor", b1 => + { + b1.Property("PlayoutId") + .HasColumnType("INTEGER"); + + b1.Property("DurationFinish") + .HasColumnType("TEXT"); + + b1.Property("InDurationFiller") + .HasColumnType("INTEGER"); + + b1.Property("InFlood") + .HasColumnType("INTEGER"); + + b1.Property("MultipleRemaining") + .HasColumnType("INTEGER"); + + b1.Property("NextGuideGroup") + .HasColumnType("INTEGER"); + + b1.Property("NextScheduleItemId") + .HasColumnType("INTEGER"); + + b1.Property("NextStart") + .HasColumnType("TEXT"); + + b1.HasKey("PlayoutId"); + + b1.HasIndex("NextScheduleItemId"); + + b1.ToTable("PlayoutAnchor", (string)null); + + b1.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", "NextScheduleItem") + .WithMany() + .HasForeignKey("NextScheduleItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b1.WithOwner() + .HasForeignKey("PlayoutId"); + + b1.Navigation("NextScheduleItem"); + }); + + b.Navigation("Anchor"); + + b.Navigation("Channel"); + + b.Navigation("ProgramSchedule"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem") + .WithMany() + .HasForeignKey("MediaItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout") + .WithMany("Items") + .HasForeignKey("PlayoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MediaItem"); + + b.Navigation("Playout"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutProgramScheduleAnchor", b => + { + b.HasOne("ErsatzTV.Core.Domain.Collection", "Collection") + .WithMany() + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem") + .WithMany() + .HasForeignKey("MediaItemId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MultiCollection", "MultiCollection") + .WithMany() + .HasForeignKey("MultiCollectionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout") + .WithMany("ProgramScheduleAnchors") + .HasForeignKey("PlayoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule") + .WithMany() + .HasForeignKey("ProgramScheduleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.SmartCollection", "SmartCollection") + .WithMany() + .HasForeignKey("SmartCollectionId"); + + b.OwnsOne("ErsatzTV.Core.Domain.CollectionEnumeratorState", "EnumeratorState", b1 => + { + b1.Property("PlayoutProgramScheduleAnchorId") + .HasColumnType("INTEGER"); + + b1.Property("Index") + .HasColumnType("INTEGER"); + + b1.Property("Seed") + .HasColumnType("INTEGER"); + + b1.HasKey("PlayoutProgramScheduleAnchorId"); + + b1.ToTable("CollectionEnumeratorState", (string)null); + + b1.WithOwner() + .HasForeignKey("PlayoutProgramScheduleAnchorId"); + }); + + b.Navigation("Collection"); + + b.Navigation("EnumeratorState"); + + b.Navigation("MediaItem"); + + b.Navigation("MultiCollection"); + + b.Navigation("Playout"); + + b.Navigation("ProgramSchedule"); + + b.Navigation("SmartCollection"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexConnection", b => + { + b.HasOne("ErsatzTV.Core.Domain.PlexMediaSource", "PlexMediaSource") + .WithMany("Connections") + .HasForeignKey("PlexMediaSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PlexMediaSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexPathReplacement", b => + { + b.HasOne("ErsatzTV.Core.Domain.PlexMediaSource", "PlexMediaSource") + .WithMany("PathReplacements") + .HasForeignKey("PlexMediaSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PlexMediaSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.Collection", "Collection") + .WithMany() + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.Filler.FillerPreset", "FallbackFiller") + .WithMany() + .HasForeignKey("FallbackFillerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem") + .WithMany() + .HasForeignKey("MediaItemId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.Filler.FillerPreset", "MidRollFiller") + .WithMany() + .HasForeignKey("MidRollFillerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("ErsatzTV.Core.Domain.MultiCollection", "MultiCollection") + .WithMany() + .HasForeignKey("MultiCollectionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.Filler.FillerPreset", "PostRollFiller") + .WithMany() + .HasForeignKey("PostRollFillerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("ErsatzTV.Core.Domain.Filler.FillerPreset", "PreRollFiller") + .WithMany() + .HasForeignKey("PreRollFillerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule") + .WithMany("Items") + .HasForeignKey("ProgramScheduleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.SmartCollection", "SmartCollection") + .WithMany() + .HasForeignKey("SmartCollectionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.Filler.FillerPreset", "TailFiller") + .WithMany() + .HasForeignKey("TailFillerId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Collection"); + + b.Navigation("FallbackFiller"); + + b.Navigation("MediaItem"); + + b.Navigation("MidRollFiller"); + + b.Navigation("MultiCollection"); + + b.Navigation("PostRollFiller"); + + b.Navigation("PreRollFiller"); + + b.Navigation("ProgramSchedule"); + + b.Navigation("SmartCollection"); + + b.Navigation("TailFiller"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.SeasonMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.Season", "Season") + .WithMany("SeasonMetadata") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ShowMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.Show", "Show") + .WithMany("ShowMetadata") + .HasForeignKey("ShowId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Show"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.SongMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.Song", "Song") + .WithMany("SongMetadata") + .HasForeignKey("SongId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Song"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Studio", b => + { + b.HasOne("ErsatzTV.Core.Domain.ArtistMetadata", null) + .WithMany("Studios") + .HasForeignKey("ArtistMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null) + .WithMany("Studios") + .HasForeignKey("EpisodeMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.MovieMetadata", null) + .WithMany("Studios") + .HasForeignKey("MovieMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MusicVideoMetadata", null) + .WithMany("Studios") + .HasForeignKey("MusicVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.OtherVideoMetadata", null) + .WithMany("Studios") + .HasForeignKey("OtherVideoMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null) + .WithMany("Studios") + .HasForeignKey("SeasonMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.ShowMetadata", null) + .WithMany("Studios") + .HasForeignKey("ShowMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SongMetadata", null) + .WithMany("Studios") + .HasForeignKey("SongMetadataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Style", b => + { + b.HasOne("ErsatzTV.Core.Domain.ArtistMetadata", null) + .WithMany("Styles") + .HasForeignKey("ArtistMetadataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Tag", b => + { + b.HasOne("ErsatzTV.Core.Domain.ArtistMetadata", null) + .WithMany("Tags") + .HasForeignKey("ArtistMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null) + .WithMany("Tags") + .HasForeignKey("EpisodeMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.MovieMetadata", null) + .WithMany("Tags") + .HasForeignKey("MovieMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MusicVideoMetadata", null) + .WithMany("Tags") + .HasForeignKey("MusicVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.OtherVideoMetadata", null) + .WithMany("Tags") + .HasForeignKey("OtherVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null) + .WithMany("Tags") + .HasForeignKey("SeasonMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.ShowMetadata", null) + .WithMany("Tags") + .HasForeignKey("ShowMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SongMetadata", null) + .WithMany("Tags") + .HasForeignKey("SongMetadataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TraktListItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem") + .WithMany("TraktListItems") + .HasForeignKey("MediaItemId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("ErsatzTV.Core.Domain.TraktList", "TraktList") + .WithMany("Items") + .HasForeignKey("TraktListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MediaItem"); + + b.Navigation("TraktList"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TraktListItemGuid", b => + { + b.HasOne("ErsatzTV.Core.Domain.TraktListItem", "TraktListItem") + .WithMany("Guids") + .HasForeignKey("TraktListItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TraktListItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Writer", b => + { + b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null) + .WithMany("Writers") + .HasForeignKey("EpisodeMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MovieMetadata", null) + .WithMany("Writers") + .HasForeignKey("MovieMetadataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Artist", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.Artist", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyLibrary", b => + { + b.HasOne("ErsatzTV.Core.Domain.Library", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.EmbyLibrary", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyMediaSource", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaSource", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.EmbyMediaSource", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Episode", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.Episode", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.Season", "Season") + .WithMany("Episodes") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinLibrary", b => + { + b.HasOne("ErsatzTV.Core.Domain.Library", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.JellyfinLibrary", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinMediaSource", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaSource", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.JellyfinMediaSource", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LocalLibrary", b => + { + b.HasOne("ErsatzTV.Core.Domain.Library", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.LocalLibrary", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LocalMediaSource", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaSource", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.LocalMediaSource", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Movie", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.Movie", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideo", b => + { + b.HasOne("ErsatzTV.Core.Domain.Artist", "Artist") + .WithMany("MusicVideos") + .HasForeignKey("ArtistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.MusicVideo", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Artist"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.OtherVideo", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.OtherVideo", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexLibrary", b => + { + b.HasOne("ErsatzTV.Core.Domain.Library", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.PlexLibrary", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaFile", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaFile", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.PlexMediaFile", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSource", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaSource", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.PlexMediaSource", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemDuration", b => + { + b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemDuration", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemFlood", b => + { + b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemFlood", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemMultiple", b => + { + b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemMultiple", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemOne", b => + { + b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemOne", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Season", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.Season", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.Show", "Show") + .WithMany("Seasons") + .HasForeignKey("ShowId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Show"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Show", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.Show", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Song", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.Song", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyEpisode", b => + { + b.HasOne("ErsatzTV.Core.Domain.Episode", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.EmbyEpisode", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyMovie", b => + { + b.HasOne("ErsatzTV.Core.Domain.Movie", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.EmbyMovie", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbySeason", b => + { + b.HasOne("ErsatzTV.Core.Domain.Season", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.EmbySeason", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyShow", b => + { + b.HasOne("ErsatzTV.Core.Domain.Show", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.EmbyShow", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinEpisode", b => + { + b.HasOne("ErsatzTV.Core.Domain.Episode", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.JellyfinEpisode", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinMovie", b => + { + b.HasOne("ErsatzTV.Core.Domain.Movie", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.JellyfinMovie", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinSeason", b => + { + b.HasOne("ErsatzTV.Core.Domain.Season", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.JellyfinSeason", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinShow", b => + { + b.HasOne("ErsatzTV.Core.Domain.Show", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.JellyfinShow", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexEpisode", b => + { + b.HasOne("ErsatzTV.Core.Domain.Episode", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.PlexEpisode", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMovie", b => + { + b.HasOne("ErsatzTV.Core.Domain.Movie", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.PlexMovie", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexSeason", b => + { + b.HasOne("ErsatzTV.Core.Domain.Season", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.PlexSeason", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexShow", b => + { + b.HasOne("ErsatzTV.Core.Domain.Show", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.PlexShow", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ArtistMetadata", b => + { + b.Navigation("Actors"); + + b.Navigation("Artwork"); + + b.Navigation("Genres"); + + b.Navigation("Guids"); + + b.Navigation("Moods"); + + b.Navigation("Studios"); + + b.Navigation("Styles"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Channel", b => + { + b.Navigation("Artwork"); + + b.Navigation("Playouts"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Collection", b => + { + b.Navigation("CollectionItems"); + + b.Navigation("MultiCollectionItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EpisodeMetadata", b => + { + b.Navigation("Actors"); + + b.Navigation("Artwork"); + + b.Navigation("Directors"); + + b.Navigation("Genres"); + + b.Navigation("Guids"); + + b.Navigation("Studios"); + + b.Navigation("Tags"); + + b.Navigation("Writers"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Library", b => + { + b.Navigation("Paths"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LibraryPath", b => + { + b.Navigation("LibraryFolders"); + + b.Navigation("MediaItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaItem", b => + { + b.Navigation("CollectionItems"); + + b.Navigation("TraktListItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaSource", b => + { + b.Navigation("Libraries"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaVersion", b => + { + b.Navigation("Chapters"); + + b.Navigation("MediaFiles"); + + b.Navigation("Streams"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MovieMetadata", b => + { + b.Navigation("Actors"); + + b.Navigation("Artwork"); + + b.Navigation("Directors"); + + b.Navigation("Genres"); + + b.Navigation("Guids"); + + b.Navigation("Studios"); + + b.Navigation("Tags"); + + b.Navigation("Writers"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MultiCollection", b => + { + b.Navigation("MultiCollectionItems"); + + b.Navigation("MultiCollectionSmartItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideoMetadata", b => + { + b.Navigation("Actors"); + + b.Navigation("Artwork"); + + b.Navigation("Genres"); + + b.Navigation("Guids"); + + b.Navigation("Studios"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.OtherVideoMetadata", b => + { + b.Navigation("Actors"); + + b.Navigation("Artwork"); + + b.Navigation("Genres"); + + b.Navigation("Guids"); + + b.Navigation("Studios"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b => + { + b.Navigation("Items"); + + b.Navigation("ProgramScheduleAnchors"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramSchedule", b => + { + b.Navigation("Items"); + + b.Navigation("Playouts"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.SeasonMetadata", b => + { + b.Navigation("Actors"); + + b.Navigation("Artwork"); + + b.Navigation("Genres"); + + b.Navigation("Guids"); + + b.Navigation("Studios"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ShowMetadata", b => + { + b.Navigation("Actors"); + + b.Navigation("Artwork"); + + b.Navigation("Genres"); + + b.Navigation("Guids"); + + b.Navigation("Studios"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.SmartCollection", b => + { + b.Navigation("MultiCollectionSmartItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.SongMetadata", b => + { + b.Navigation("Actors"); + + b.Navigation("Artwork"); + + b.Navigation("Genres"); + + b.Navigation("Guids"); + + b.Navigation("Studios"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TraktList", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TraktListItem", b => + { + b.Navigation("Guids"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Artist", b => + { + b.Navigation("ArtistMetadata"); + + b.Navigation("MusicVideos"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyMediaSource", b => + { + b.Navigation("Connections"); + + b.Navigation("PathReplacements"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Episode", b => + { + b.Navigation("EpisodeMetadata"); + + b.Navigation("MediaVersions"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinMediaSource", b => + { + b.Navigation("Connections"); + + b.Navigation("PathReplacements"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Movie", b => + { + b.Navigation("MediaVersions"); + + b.Navigation("MovieMetadata"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideo", b => + { + b.Navigation("MediaVersions"); + + b.Navigation("MusicVideoMetadata"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.OtherVideo", b => + { + b.Navigation("MediaVersions"); + + b.Navigation("OtherVideoMetadata"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSource", b => + { + b.Navigation("Connections"); + + b.Navigation("PathReplacements"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Season", b => + { + b.Navigation("Episodes"); + + b.Navigation("SeasonMetadata"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Show", b => + { + b.Navigation("Seasons"); + + b.Navigation("ShowMetadata"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Song", b => + { + b.Navigation("MediaVersions"); + + b.Navigation("SongMetadata"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ErsatzTV.Infrastructure/Migrations/20220209084802_Add_PlayoutProgramScheduleAnchor_AnchorDate.cs b/ErsatzTV.Infrastructure/Migrations/20220209084802_Add_PlayoutProgramScheduleAnchor_AnchorDate.cs new file mode 100644 index 00000000..871e2d2b --- /dev/null +++ b/ErsatzTV.Infrastructure/Migrations/20220209084802_Add_PlayoutProgramScheduleAnchor_AnchorDate.cs @@ -0,0 +1,26 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ErsatzTV.Infrastructure.Migrations +{ + public partial class Add_PlayoutProgramScheduleAnchor_AnchorDate : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "AnchorDate", + table: "PlayoutProgramScheduleAnchor", + type: "TEXT", + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "AnchorDate", + table: "PlayoutProgramScheduleAnchor"); + } + } +} diff --git a/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs b/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs index c10be7ce..30b484f1 100644 --- a/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs +++ b/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs @@ -1346,6 +1346,9 @@ namespace ErsatzTV.Infrastructure.Migrations .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); + b.Property("AnchorDate") + .HasColumnType("TEXT"); + b.Property("CollectionId") .HasColumnType("INTEGER"); diff --git a/ErsatzTV/Pages/Playouts.razor b/ErsatzTV/Pages/Playouts.razor index 226fae8c..a4d57ea9 100644 --- a/ErsatzTV/Pages/Playouts.razor +++ b/ErsatzTV/Pages/Playouts.razor @@ -2,6 +2,7 @@ @using ErsatzTV.Application.Playouts @using ErsatzTV.Application.Configuration @implements IDisposable +@using ErsatzTV.Core.Scheduling @inject IDialogService _dialog @inject IMediator _mediator @@ -44,14 +45,14 @@ @* @context.ProgramSchedulePlayoutType *@
- + + OnClick="@(_ => ResetPlayout(context))"> - + + OnClick="@(_ => ScheduleReset(context))"> @@ -114,11 +115,14 @@ if (_showFiller != value) { _showFiller = value; - _detailTable.ReloadServerData(); + if (_selectedPlayoutId != null) + { + _detailTable.ReloadServerData(); + } } } } - + public void Dispose() { _cts.Cancel(); @@ -141,14 +145,6 @@ await _detailTable.ReloadServerData(); } - private async Task ShowFillerChanged() - { - if (_selectedPlayoutId != null) - { - await _detailTable.ReloadServerData(); - } - } - private async Task DeletePlayout(PlayoutNameViewModel playout) { var parameters = new DialogParameters { { "EntityType", "playout" }, { "EntityName", $"{playout.ScheduleName} on {playout.ChannelNumber} - {playout.ChannelName}" } }; @@ -167,9 +163,9 @@ } } - private async Task RebuildPlayout(PlayoutNameViewModel playout) + private async Task ResetPlayout(PlayoutNameViewModel playout) { - await _mediator.Send(new BuildPlayout(playout.PlayoutId, true), _cts.Token); + await _mediator.Send(new BuildPlayout(playout.PlayoutId, PlayoutBuildMode.Reset), _cts.Token); await _table.ReloadServerData(); if (_selectedPlayoutId == playout.PlayoutId) { @@ -177,19 +173,21 @@ } } - private async Task ScheduleRebuild(PlayoutNameViewModel playout) + private async Task ScheduleReset(PlayoutNameViewModel playout) { var parameters = new DialogParameters { { "PlayoutId", playout.PlayoutId }, { "ChannelName", playout.ChannelName }, { "ScheduleName", playout.ScheduleName }, - { "DailyRebuildTime", playout.DailyRebuildTime } + { "DailyResetTime", playout.DailyRebuildTime } }; var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall }; - IDialogReference dialog = _dialog.Show("Schedule Playout Rebuild", parameters, options); + IDialogReference dialog = _dialog.Show("Schedule Playout Reset", parameters, options); await dialog.Result; + + await _table.ReloadServerData(); } private async Task> ServerReload(TableState state) diff --git a/ErsatzTV/Program.cs b/ErsatzTV/Program.cs index 9587f845..dd7dc761 100644 --- a/ErsatzTV/Program.cs +++ b/ErsatzTV/Program.cs @@ -1,3 +1,4 @@ +using Destructurama; using ErsatzTV.Core; using Serilog; @@ -35,6 +36,7 @@ public class Program { Log.Logger = new LoggerConfiguration() .ReadFrom.Configuration(Configuration) + .Destructure.UsingAttributes() .Enrich.FromLogContext() .WriteTo.SQLite(FileSystemLayout.LogDatabasePath, retentionPeriod: TimeSpan.FromDays(1)) .WriteTo.File(FileSystemLayout.LogFilePath, rollingInterval: RollingInterval.Day) diff --git a/ErsatzTV/Services/SchedulerService.cs b/ErsatzTV/Services/SchedulerService.cs index 03ddd767..805bd102 100644 --- a/ErsatzTV/Services/SchedulerService.cs +++ b/ErsatzTV/Services/SchedulerService.cs @@ -9,6 +9,7 @@ using ErsatzTV.Application.Plex; using ErsatzTV.Application.Search; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Locking; +using ErsatzTV.Core.Scheduling; using ErsatzTV.Infrastructure.Data; using Microsoft.EntityFrameworkCore; @@ -39,7 +40,7 @@ public class SchedulerService : BackgroundService protected override async Task ExecuteAsync(CancellationToken cancellationToken) { DateTime firstRun = DateTime.Now; - + // run once immediately at startup if (!cancellationToken.IsCancellationRequested) { @@ -66,9 +67,10 @@ public class SchedulerService : BackgroundService var roundedMinute = (int)(Math.Round(DateTime.Now.Minute / 5.0) * 5); if (roundedMinute % 30 == 0) { - // check for playouts to rebuild every 30 minutes - await RebuildPlayouts(cancellationToken); + // check for playouts to reset every 30 minutes + await ResetPlayouts(cancellationToken); } + if (roundedMinute % 60 == 0 && DateTime.Now.Subtract(firstRun) > TimeSpan.FromHours(1)) { // do other work every hour (on the hour) @@ -112,7 +114,7 @@ public class SchedulerService : BackgroundService } } - private async Task RebuildPlayouts(CancellationToken cancellationToken) + private async Task ResetPlayouts(CancellationToken cancellationToken) { try { @@ -129,14 +131,16 @@ public class SchedulerService : BackgroundService if (DateTime.Now.Subtract(DateTime.Today.Add(playout.DailyRebuildTime ?? TimeSpan.FromDays(7))) < TimeSpan.FromMinutes(5)) { - await _workerChannel.WriteAsync(new BuildPlayout(playout.Id, true), cancellationToken); + await _workerChannel.WriteAsync( + new BuildPlayout(playout.Id, PlayoutBuildMode.Reset), + cancellationToken); } } } catch (Exception ex) { _logger.LogWarning(ex, "Error during scheduler run"); - + try { using (IServiceScope scope = _serviceScopeFactory.CreateScope()) @@ -162,7 +166,9 @@ public class SchedulerService : BackgroundService .ToListAsync(cancellationToken); foreach (int playoutId in playouts.OrderBy(p => decimal.Parse(p.Channel.Number)).Map(p => p.Id)) { - await _workerChannel.WriteAsync(new BuildPlayout(playoutId), cancellationToken); + await _workerChannel.WriteAsync( + new BuildPlayout(playoutId, PlayoutBuildMode.Continue), + cancellationToken); } } diff --git a/ErsatzTV/Shared/SchedulePlayoutRebuild.razor b/ErsatzTV/Shared/SchedulePlayoutReset.razor similarity index 85% rename from ErsatzTV/Shared/SchedulePlayoutRebuild.razor rename to ErsatzTV/Shared/SchedulePlayoutReset.razor index a7b685bb..a102bab0 100644 --- a/ErsatzTV/Shared/SchedulePlayoutRebuild.razor +++ b/ErsatzTV/Shared/SchedulePlayoutReset.razor @@ -2,7 +2,7 @@ @implements IDisposable @inject IMediator _mediator @inject ISnackbar _snackbar -@inject ILogger _logger +@inject ILogger _logger @@ -12,8 +12,8 @@ @FormatText() - - Do not automatically rebuild + + Do not automatically reset @for (var i = 1; i < 48; i++) { var time = TimeSpan.FromHours(i * 0.5); @@ -39,7 +39,7 @@ [Parameter] public int PlayoutId { get; set; } - + [Parameter] public string ChannelName { get; set; } @@ -47,16 +47,16 @@ public string ScheduleName { get; set; } [Parameter] - public Option DailyRebuildTime { get; set; } + public Option DailyResetTime { get; set; } - private string FormatText() => $"Enter the time that the playout on channel {ChannelName} with schedule {ScheduleName} should rebuild every day"; + private string FormatText() => $"Enter the time that the playout on channel {ChannelName} with schedule {ScheduleName} should reset every day"; private record DummyModel; private readonly DummyModel _dummyModel = new(); - private Option _rebuildTime; - + private Option _resetTime; + public void Dispose() { _cts.Cancel(); @@ -65,13 +65,13 @@ protected override void OnParametersSet() { - _rebuildTime = DailyRebuildTime; + _resetTime = DailyResetTime; } private async Task Submit() { Either maybeResult = - await _mediator.Send(new UpdatePlayout(PlayoutId, _rebuildTime), _cts.Token); + await _mediator.Send(new UpdatePlayout(PlayoutId, _resetTime), _cts.Token); maybeResult.Match( playout => { MudDialog.Close(DialogResult.Ok(playout)); },