diff --git a/CHANGELOG.md b/CHANGELOG.md index 66e9c9255..69682be5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Added - Disable playout buttons and show spinning indicator when a playout is being modified (built/extended, or subtitles are being extracted) - Automatically reload playout details table when playout build is complete +- Add `Discard To Fill Attempts` setting to duration playout mode + - This setting only has an effect when it's configured to be greater than zero + - When the current item is longer than the remaining duration, it will be discarded and ETV will try to fit the next item in the collection, up to the configured number of times + - When the remaining duration is shorter than all items in the collection, the normal filler logic will be used ### Fixed - Skip checking for subtitles to extract when subtitles are not enabled on a channel/schedule item diff --git a/ErsatzTV.Application/ProgramSchedules/Commands/AddProgramScheduleItem.cs b/ErsatzTV.Application/ProgramSchedules/Commands/AddProgramScheduleItem.cs index 1c81a76e7..54f9f3fd8 100644 --- a/ErsatzTV.Application/ProgramSchedules/Commands/AddProgramScheduleItem.cs +++ b/ErsatzTV.Application/ProgramSchedules/Commands/AddProgramScheduleItem.cs @@ -17,6 +17,7 @@ public record AddProgramScheduleItem( int? MultipleCount, TimeSpan? PlayoutDuration, TailMode TailMode, + int? DiscardToFillAttempts, string CustomTitle, GuideMode GuideMode, int? PreRollFillerId, diff --git a/ErsatzTV.Application/ProgramSchedules/Commands/IProgramScheduleItemRequest.cs b/ErsatzTV.Application/ProgramSchedules/Commands/IProgramScheduleItemRequest.cs index 86651db02..9f0a07fb9 100644 --- a/ErsatzTV.Application/ProgramSchedules/Commands/IProgramScheduleItemRequest.cs +++ b/ErsatzTV.Application/ProgramSchedules/Commands/IProgramScheduleItemRequest.cs @@ -15,6 +15,7 @@ public interface IProgramScheduleItemRequest int? MultipleCount { get; } TimeSpan? PlayoutDuration { get; } TailMode TailMode { get; } + int? DiscardToFillAttempts { get; } string CustomTitle { get; } GuideMode GuideMode { get; } int? PreRollFillerId { get; } diff --git a/ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs b/ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs index 7d46a7c8b..282ce40d0 100644 --- a/ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs +++ b/ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs @@ -83,6 +83,11 @@ public abstract class ProgramScheduleItemCommandBase return BaseError.New("[PlayoutDuration] is required for playout mode 'duration'"); } + if (item.DiscardToFillAttempts is null) + { + return BaseError.New("[DiscardToFillAttempts] is required for playout mode 'duration'"); + } + if (item.TailMode == TailMode.Filler && item.TailFillerId == null) { return BaseError.New("Tail Filler is required with tail mode Filler"); @@ -248,6 +253,7 @@ public abstract class ProgramScheduleItemCommandBase PlaybackOrder = item.PlaybackOrder, PlayoutDuration = FixDuration(item.PlayoutDuration.GetValueOrDefault()), TailMode = item.TailMode, + DiscardToFillAttempts = item.DiscardToFillAttempts.GetValueOrDefault(), CustomTitle = item.CustomTitle, GuideMode = item.GuideMode, PreRollFillerId = item.PreRollFillerId, diff --git a/ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItems.cs b/ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItems.cs index f2f9a34ec..1dee7d429 100644 --- a/ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItems.cs +++ b/ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItems.cs @@ -17,6 +17,7 @@ public record ReplaceProgramScheduleItem( int? MultipleCount, TimeSpan? PlayoutDuration, TailMode TailMode, + int? DiscardToFillAttempts, string CustomTitle, GuideMode GuideMode, int? PreRollFillerId, diff --git a/ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItemsHandler.cs b/ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItemsHandler.cs index 1fed7d638..6cdd04f53 100644 --- a/ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItemsHandler.cs +++ b/ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItemsHandler.cs @@ -29,7 +29,7 @@ public class ReplaceProgramScheduleItemsHandler : ProgramScheduleItemCommandBase { 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( diff --git a/ErsatzTV.Application/ProgramSchedules/Mapper.cs b/ErsatzTV.Application/ProgramSchedules/Mapper.cs index 979d32b84..ded627b8f 100644 --- a/ErsatzTV.Application/ProgramSchedules/Mapper.cs +++ b/ErsatzTV.Application/ProgramSchedules/Mapper.cs @@ -42,6 +42,7 @@ internal static class Mapper duration.PlaybackOrder, duration.PlayoutDuration, duration.TailMode, + duration.DiscardToFillAttempts, duration.CustomTitle, duration.GuideMode, duration.PreRollFiller != null diff --git a/ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemDurationViewModel.cs b/ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemDurationViewModel.cs index 00709f775..e74510b7e 100644 --- a/ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemDurationViewModel.cs +++ b/ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemDurationViewModel.cs @@ -21,6 +21,7 @@ public record ProgramScheduleItemDurationViewModel : ProgramScheduleItemViewMode PlaybackOrder playbackOrder, TimeSpan playoutDuration, TailMode tailMode, + int discardToFillAttempts, string customTitle, GuideMode guideMode, FillerPresetViewModel preRollFiller, @@ -59,8 +60,10 @@ public record ProgramScheduleItemDurationViewModel : ProgramScheduleItemViewMode { PlayoutDuration = playoutDuration; TailMode = tailMode; + DiscardToFillAttempts = discardToFillAttempts; } public TimeSpan PlayoutDuration { get; } public TailMode TailMode { get; } + public int DiscardToFillAttempts { get; } } diff --git a/ErsatzTV.Core/Domain/ProgramScheduleItemDuration.cs b/ErsatzTV.Core/Domain/ProgramScheduleItemDuration.cs index aec469956..33f62fde5 100644 --- a/ErsatzTV.Core/Domain/ProgramScheduleItemDuration.cs +++ b/ErsatzTV.Core/Domain/ProgramScheduleItemDuration.cs @@ -4,4 +4,5 @@ public class ProgramScheduleItemDuration : ProgramScheduleItem { public TimeSpan PlayoutDuration { get; set; } public TailMode TailMode { get; set; } + public int DiscardToFillAttempts { get; set; } } diff --git a/ErsatzTV.Core/Extensions/MediaItemExtensions.cs b/ErsatzTV.Core/Extensions/MediaItemExtensions.cs index 79d301f1d..47ea573be 100644 --- a/ErsatzTV.Core/Extensions/MediaItemExtensions.cs +++ b/ErsatzTV.Core/Extensions/MediaItemExtensions.cs @@ -7,6 +7,17 @@ namespace ErsatzTV.Core.Extensions; public static class MediaItemExtensions { + public static Option GetDuration(this MediaItem mediaItem) => + mediaItem switch + { + Movie m => m.MediaVersions.HeadOrNone().Map(v => v.Duration), + Episode e => e.MediaVersions.HeadOrNone().Map(v => v.Duration), + MusicVideo mv => mv.MediaVersions.HeadOrNone().Map(v => v.Duration), + OtherVideo ov => ov.MediaVersions.HeadOrNone().Map(v => v.Duration), + Song s => s.MediaVersions.HeadOrNone().Map(v => v.Duration), + _ => None + }; + public static MediaVersion GetHeadVersion(this MediaItem mediaItem) => mediaItem switch { diff --git a/ErsatzTV.Core/Interfaces/Scheduling/IMediaCollectionEnumerator.cs b/ErsatzTV.Core/Interfaces/Scheduling/IMediaCollectionEnumerator.cs index 8ec41963a..ab9e53c0b 100644 --- a/ErsatzTV.Core/Interfaces/Scheduling/IMediaCollectionEnumerator.cs +++ b/ErsatzTV.Core/Interfaces/Scheduling/IMediaCollectionEnumerator.cs @@ -8,4 +8,5 @@ public interface IMediaCollectionEnumerator Option Current { get; } void MoveNext(); Option Peek(int offset); + Option MinimumDuration { get; } } diff --git a/ErsatzTV.Core/Scheduling/ChronologicalMediaCollectionEnumerator.cs b/ErsatzTV.Core/Scheduling/ChronologicalMediaCollectionEnumerator.cs index 93267b7d1..4a6fb58e1 100644 --- a/ErsatzTV.Core/Scheduling/ChronologicalMediaCollectionEnumerator.cs +++ b/ErsatzTV.Core/Scheduling/ChronologicalMediaCollectionEnumerator.cs @@ -1,4 +1,5 @@ using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Extensions; using ErsatzTV.Core.Interfaces.Scheduling; namespace ErsatzTV.Core.Scheduling; @@ -6,12 +7,15 @@ namespace ErsatzTV.Core.Scheduling; public sealed class ChronologicalMediaCollectionEnumerator : IMediaCollectionEnumerator { private readonly IList _sortedMediaItems; + private readonly Lazy> _lazyMinimumDuration; public ChronologicalMediaCollectionEnumerator( IEnumerable mediaItems, CollectionEnumeratorState state) { _sortedMediaItems = mediaItems.OrderBy(identity, new ChronologicalMediaComparer()).ToList(); + _lazyMinimumDuration = new Lazy>( + () => _sortedMediaItems.Bind(i => i.GetDuration()).HeadOrNone()); State = new CollectionEnumeratorState { Seed = state.Seed }; @@ -35,4 +39,6 @@ public sealed class ChronologicalMediaCollectionEnumerator : IMediaCollectionEnu public Option Peek(int offset) => _sortedMediaItems.Any() ? _sortedMediaItems[(State.Index + offset) % _sortedMediaItems.Count] : None; + + public Option MinimumDuration => _lazyMinimumDuration.Value; } diff --git a/ErsatzTV.Core/Scheduling/CustomOrderCollectionEnumerator.cs b/ErsatzTV.Core/Scheduling/CustomOrderCollectionEnumerator.cs index 90ea63433..9f3e7dd21 100644 --- a/ErsatzTV.Core/Scheduling/CustomOrderCollectionEnumerator.cs +++ b/ErsatzTV.Core/Scheduling/CustomOrderCollectionEnumerator.cs @@ -1,4 +1,5 @@ using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Extensions; using ErsatzTV.Core.Interfaces.Scheduling; namespace ErsatzTV.Core.Scheduling; @@ -6,6 +7,7 @@ namespace ErsatzTV.Core.Scheduling; public class CustomOrderCollectionEnumerator : IMediaCollectionEnumerator { private readonly IList _sortedMediaItems; + private readonly Lazy> _lazyMinimumDuration; public CustomOrderCollectionEnumerator( Collection collection, @@ -17,6 +19,8 @@ public class CustomOrderCollectionEnumerator : IMediaCollectionEnumerator .OrderBy(ci => ci.CustomIndex) .Map(ci => mediaItems.First(mi => mi.Id == ci.MediaItemId)) .ToList(); + _lazyMinimumDuration = new Lazy>( + () => _sortedMediaItems.Bind(i => i.GetDuration()).HeadOrNone()); State = new CollectionEnumeratorState { Seed = state.Seed }; while (State.Index < state.Index) @@ -33,4 +37,6 @@ public class CustomOrderCollectionEnumerator : IMediaCollectionEnumerator public Option Peek(int offset) => throw new NotSupportedException(); + + public Option MinimumDuration => _lazyMinimumDuration.Value; } diff --git a/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerDuration.cs b/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerDuration.cs index 59dbde148..c84167902 100644 --- a/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerDuration.cs +++ b/ErsatzTV.Core/Scheduling/PlayoutModeSchedulerDuration.cs @@ -26,6 +26,7 @@ public class PlayoutModeSchedulerDuration : PlayoutModeSchedulerBase durationUntil = None; + int discardAttempts = 0; IMediaCollectionEnumerator contentEnumerator = collectionEnumerators[CollectionKey.ForScheduleItem(scheduleItem)]; @@ -68,77 +69,108 @@ public class PlayoutModeSchedulerDuration : PlayoutModeSchedulerBase 0 && + remainingDuration >= contentEnumerator.MinimumDuration.IfNone(TimeSpan.Zero) && + itemDuration > remainingDuration) { - MediaItemId = mediaItem.Id, - Start = itemStartTime.UtcDateTime, - Finish = itemStartTime.UtcDateTime + itemDuration, - InPoint = TimeSpan.Zero, - OutPoint = itemDuration, - GuideGroup = nextState.NextGuideGroup, - FillerKind = scheduleItem.GuideMode == GuideMode.Filler - ? FillerKind.GuideMode - : FillerKind.None, - CustomTitle = scheduleItem.CustomTitle, - WatermarkId = scheduleItem.WatermarkId, - PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode, - PreferredAudioTitle = scheduleItem.PreferredAudioTitle, - PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode, - SubtitleMode = scheduleItem.SubtitleMode - }; - - durationUntil.Do(du => playoutItem.GuideFinish = du.UtcDateTime); - - DateTimeOffset durationFinish = nextState.DurationFinish.IfNone(SystemTime.MaxValueUtc); - DateTimeOffset itemEndTimeWithFiller = CalculateEndTimeWithFiller( - collectionEnumerators, - scheduleItem, - itemStartTime, - itemDuration, - itemChapters); - willFinishInTime = itemStartTime > durationFinish || - itemEndTimeWithFiller <= durationFinish; - if (willFinishInTime) - { - // LogScheduledItem(scheduleItem, mediaItem, itemStartTime); - playoutItems.AddRange( - AddFiller( - nextState, - collectionEnumerators, - scheduleItem, - playoutItem, - itemChapters, - cancellationToken)); - - nextState = nextState with + discardAttempts++; + if (discardAttempts > scheduleItem.DiscardToFillAttempts) { - CurrentTime = itemEndTimeWithFiller, + nextState = nextState with + { + DurationFinish = None + }; - // only bump guide group if we don't have a custom title - NextGuideGroup = string.IsNullOrWhiteSpace(scheduleItem.CustomTitle) - ? nextState.IncrementGuideGroup - : nextState.NextGuideGroup - }; + nextState.ScheduleItemsEnumerator.MoveNext(); + } + else + { + _logger.LogDebug( + "Skipping playout item {Title} with duration {Duration} that is longer than remaining duration {RemainingDuration}", + PlayoutBuilder.DisplayTitle(mediaItem), + itemDuration, + remainingDuration); - contentEnumerator.MoveNext(); + contentEnumerator.MoveNext(); + } } else { - TimeSpan durationBlock = itemEndTimeWithFiller - itemStartTime; - if (itemEndTimeWithFiller - itemStartTime > scheduleItem.PlayoutDuration) - { - _logger.LogWarning( - "Unable to schedule duration block of {DurationBlock} which is longer than the configured playout duration {PlayoutDuration}", - durationBlock, - scheduleItem.PlayoutDuration); - } + discardAttempts = 0; - nextState = nextState with + var playoutItem = new PlayoutItem { - DurationFinish = None + MediaItemId = mediaItem.Id, + Start = itemStartTime.UtcDateTime, + Finish = itemStartTime.UtcDateTime + itemDuration, + InPoint = TimeSpan.Zero, + OutPoint = itemDuration, + GuideGroup = nextState.NextGuideGroup, + FillerKind = scheduleItem.GuideMode == GuideMode.Filler + ? FillerKind.GuideMode + : FillerKind.None, + CustomTitle = scheduleItem.CustomTitle, + WatermarkId = scheduleItem.WatermarkId, + PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode, + PreferredAudioTitle = scheduleItem.PreferredAudioTitle, + PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode, + SubtitleMode = scheduleItem.SubtitleMode }; - nextState.ScheduleItemsEnumerator.MoveNext(); + durationUntil.Do(du => playoutItem.GuideFinish = du.UtcDateTime); + + DateTimeOffset durationFinish = nextState.DurationFinish.IfNone(SystemTime.MaxValueUtc); + DateTimeOffset itemEndTimeWithFiller = CalculateEndTimeWithFiller( + collectionEnumerators, + scheduleItem, + itemStartTime, + itemDuration, + itemChapters); + willFinishInTime = itemStartTime > durationFinish || + itemEndTimeWithFiller <= durationFinish; + if (willFinishInTime) + { + // LogScheduledItem(scheduleItem, mediaItem, itemStartTime); + playoutItems.AddRange( + AddFiller( + nextState, + collectionEnumerators, + scheduleItem, + playoutItem, + itemChapters, + cancellationToken)); + + nextState = nextState with + { + CurrentTime = itemEndTimeWithFiller, + + // only bump guide group if we don't have a custom title + NextGuideGroup = string.IsNullOrWhiteSpace(scheduleItem.CustomTitle) + ? nextState.IncrementGuideGroup + : nextState.NextGuideGroup + }; + + contentEnumerator.MoveNext(); + } + else + { + TimeSpan durationBlock = itemEndTimeWithFiller - itemStartTime; + if (itemEndTimeWithFiller - itemStartTime > scheduleItem.PlayoutDuration) + { + _logger.LogWarning( + "Unable to schedule duration block of {DurationBlock} which is longer than the configured playout duration {PlayoutDuration}", + durationBlock, + scheduleItem.PlayoutDuration); + } + + nextState = nextState with + { + DurationFinish = None + }; + + nextState.ScheduleItemsEnumerator.MoveNext(); + } } } diff --git a/ErsatzTV.Core/Scheduling/RandomizedMediaCollectionEnumerator.cs b/ErsatzTV.Core/Scheduling/RandomizedMediaCollectionEnumerator.cs index c17aab2f5..ab60fdcb2 100644 --- a/ErsatzTV.Core/Scheduling/RandomizedMediaCollectionEnumerator.cs +++ b/ErsatzTV.Core/Scheduling/RandomizedMediaCollectionEnumerator.cs @@ -1,4 +1,5 @@ using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Extensions; using ErsatzTV.Core.Interfaces.Scheduling; namespace ErsatzTV.Core.Scheduling; @@ -6,12 +7,14 @@ namespace ErsatzTV.Core.Scheduling; public class RandomizedMediaCollectionEnumerator : IMediaCollectionEnumerator { private readonly IList _mediaItems; + private readonly Lazy> _lazyMinimumDuration; private readonly Random _random; private int _index; public RandomizedMediaCollectionEnumerator(IList mediaItems, CollectionEnumeratorState state) { _mediaItems = mediaItems; + _lazyMinimumDuration = new Lazy>(() => _mediaItems.Bind(i => i.GetDuration()).HeadOrNone()); _random = new Random(state.Seed); State = new CollectionEnumeratorState { Seed = state.Seed }; @@ -35,4 +38,6 @@ public class RandomizedMediaCollectionEnumerator : IMediaCollectionEnumerator public Option Peek(int offset) => throw new NotSupportedException(); + + public Option MinimumDuration => _lazyMinimumDuration.Value; } diff --git a/ErsatzTV.Core/Scheduling/SeasonEpisodeMediaCollectionEnumerator.cs b/ErsatzTV.Core/Scheduling/SeasonEpisodeMediaCollectionEnumerator.cs index bdc679b2f..c9d5fa747 100644 --- a/ErsatzTV.Core/Scheduling/SeasonEpisodeMediaCollectionEnumerator.cs +++ b/ErsatzTV.Core/Scheduling/SeasonEpisodeMediaCollectionEnumerator.cs @@ -1,4 +1,5 @@ using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Extensions; using ErsatzTV.Core.Interfaces.Scheduling; namespace ErsatzTV.Core.Scheduling; @@ -6,12 +7,15 @@ namespace ErsatzTV.Core.Scheduling; public sealed class SeasonEpisodeMediaCollectionEnumerator : IMediaCollectionEnumerator { private readonly IList _sortedMediaItems; + private readonly Lazy> _lazyMinimumDuration; public SeasonEpisodeMediaCollectionEnumerator( IEnumerable mediaItems, CollectionEnumeratorState state) { _sortedMediaItems = mediaItems.OrderBy(identity, new SeasonEpisodeMediaComparer()).ToList(); + _lazyMinimumDuration = new Lazy>( + () => _sortedMediaItems.Bind(i => i.GetDuration()).HeadOrNone()); State = new CollectionEnumeratorState { Seed = state.Seed }; @@ -35,4 +39,6 @@ public sealed class SeasonEpisodeMediaCollectionEnumerator : IMediaCollectionEnu public Option Peek(int offset) => _sortedMediaItems.Any() ? _sortedMediaItems[(State.Index + offset) % _sortedMediaItems.Count] : None; + + public Option MinimumDuration => _lazyMinimumDuration.Value; } diff --git a/ErsatzTV.Core/Scheduling/ShuffleInOrderCollectionEnumerator.cs b/ErsatzTV.Core/Scheduling/ShuffleInOrderCollectionEnumerator.cs index e629a3bae..4685c6291 100644 --- a/ErsatzTV.Core/Scheduling/ShuffleInOrderCollectionEnumerator.cs +++ b/ErsatzTV.Core/Scheduling/ShuffleInOrderCollectionEnumerator.cs @@ -1,4 +1,5 @@ using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Extensions; using ErsatzTV.Core.Interfaces.Scheduling; namespace ErsatzTV.Core.Scheduling; @@ -11,6 +12,7 @@ public class ShuffleInOrderCollectionEnumerator : IMediaCollectionEnumerator private readonly bool _randomStartPoint; private Random _random; private IList _shuffled; + private readonly Lazy> _lazyMinimumDuration; public ShuffleInOrderCollectionEnumerator( IList collections, @@ -31,6 +33,7 @@ public class ShuffleInOrderCollectionEnumerator : IMediaCollectionEnumerator _random = new Random(state.Seed); _shuffled = Shuffle(_collections, _random); + _lazyMinimumDuration = new Lazy>(() => _shuffled.Bind(i => i.GetDuration()).HeadOrNone()); State = new CollectionEnumeratorState { Seed = state.Seed }; while (State.Index < state.Index) @@ -203,4 +206,6 @@ public class ShuffleInOrderCollectionEnumerator : IMediaCollectionEnumerator public int Index { get; set; } public IList> Items { get; set; } } + + public Option MinimumDuration => _lazyMinimumDuration.Value; } diff --git a/ErsatzTV.Core/Scheduling/ShuffledMediaCollectionEnumerator.cs b/ErsatzTV.Core/Scheduling/ShuffledMediaCollectionEnumerator.cs index badc44a6a..106b20c78 100644 --- a/ErsatzTV.Core/Scheduling/ShuffledMediaCollectionEnumerator.cs +++ b/ErsatzTV.Core/Scheduling/ShuffledMediaCollectionEnumerator.cs @@ -1,4 +1,5 @@ using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Extensions; using ErsatzTV.Core.Interfaces.Scheduling; namespace ErsatzTV.Core.Scheduling; @@ -8,6 +9,7 @@ public class ShuffledMediaCollectionEnumerator : IMediaCollectionEnumerator private readonly CancellationToken _cancellationToken; private readonly int _mediaItemCount; private readonly IList _mediaItems; + private readonly Lazy> _lazyMinimumDuration; private CloneableRandom _random; private IList _shuffled; @@ -28,6 +30,7 @@ public class ShuffledMediaCollectionEnumerator : IMediaCollectionEnumerator _random = new CloneableRandom(state.Seed); _shuffled = Shuffle(_mediaItems, _random); + _lazyMinimumDuration = new Lazy>(() => _shuffled.Bind(i => i.GetDuration()).HeadOrNone()); State = new CollectionEnumeratorState { Seed = state.Seed }; while (State.Index < state.Index) @@ -105,4 +108,6 @@ public class ShuffledMediaCollectionEnumerator : IMediaCollectionEnumerator return GroupedMediaItem.FlattenGroups(copy, _mediaItemCount); } + + public Option MinimumDuration => _lazyMinimumDuration.Value; } diff --git a/ErsatzTV.Infrastructure/Migrations/20230613201543_Add_ProgramScheduleItemDuration_DiscardToFillAttempts.Designer.cs b/ErsatzTV.Infrastructure/Migrations/20230613201543_Add_ProgramScheduleItemDuration_DiscardToFillAttempts.Designer.cs new file mode 100644 index 000000000..2baf104c6 --- /dev/null +++ b/ErsatzTV.Infrastructure/Migrations/20230613201543_Add_ProgramScheduleItemDuration_DiscardToFillAttempts.Designer.cs @@ -0,0 +1,4419 @@ +// +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("20230613201543_Add_ProgramScheduleItemDuration_DiscardToFillAttempts")] + partial class Add_ProgramScheduleItemDuration_DiscardToFillAttempts + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.7"); + + 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("Categories") + .HasColumnType("TEXT"); + + b.Property("FFmpegProfileId") + .HasColumnType("INTEGER"); + + b.Property("FallbackFillerId") + .HasColumnType("INTEGER"); + + b.Property("Group") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("ErsatzTV"); + + b.Property("MusicVideoCreditsMode") + .HasColumnType("INTEGER"); + + b.Property("MusicVideoCreditsTemplate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("TEXT"); + + b.Property("PreferredAudioLanguageCode") + .HasColumnType("TEXT"); + + b.Property("PreferredAudioTitle") + .HasColumnType("TEXT"); + + b.Property("PreferredSubtitleLanguageCode") + .HasColumnType("TEXT"); + + b.Property("StreamingMode") + .HasColumnType("INTEGER"); + + b.Property("SubtitleMode") + .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("PlaceWithinSourceContent") + .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("MusicVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OtherVideoMetadataId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("OtherVideoMetadataId"); + + b.ToTable("Director", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyCollection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Etag") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("EmbyCollection", (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("AudioFormat") + .HasColumnType("INTEGER"); + + b.Property("AudioSampleRate") + .HasColumnType("INTEGER"); + + b.Property("BitDepth") + .HasColumnType("INTEGER"); + + b.Property("DeinterlaceVideo") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("HardwareAcceleration") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizeFramerate") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(false); + + b.Property("NormalizeLoudness") + .HasColumnType("INTEGER"); + + b.Property("QsvExtraHardwareFrames") + .HasColumnType("INTEGER"); + + b.Property("ResolutionId") + .HasColumnType("INTEGER"); + + b.Property("ThreadCount") + .HasColumnType("INTEGER"); + + b.Property("VaapiDevice") + .HasColumnType("TEXT"); + + b.Property("VaapiDriver") + .HasColumnType("INTEGER"); + + b.Property("VideoBitrate") + .HasColumnType("INTEGER"); + + b.Property("VideoBufferSize") + .HasColumnType("INTEGER"); + + b.Property("VideoFormat") + .HasColumnType("INTEGER"); + + 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("AllowWatermarks") + .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.JellyfinCollection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Etag") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("JellyfinCollection", (string)null); + }); + + 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); + + b.UseTptMappingStrategy(); + }); + + 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); + + b.UseTptMappingStrategy(); + }); + + 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); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("MediaSource", (string)null); + + b.UseTptMappingStrategy(); + }); + + 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("ColorPrimaries") + .HasColumnType("TEXT"); + + b.Property("ColorRange") + .HasColumnType("TEXT"); + + b.Property("ColorSpace") + .HasColumnType("TEXT"); + + b.Property("ColorTransfer") + .HasColumnType("TEXT"); + + b.Property("Default") + .HasColumnType("INTEGER"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + 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("MimeType") + .HasColumnType("TEXT"); + + 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.MusicVideoArtist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("MusicVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MusicVideoMetadataId"); + + b.ToTable("MusicVideoArtist"); + }); + + 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("Track") + .HasColumnType("INTEGER"); + + 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("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("OtherVideoId") + .HasColumnType("INTEGER"); + + 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("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("DisableWatermarks") + .HasColumnType("INTEGER"); + + 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("PreferredAudioLanguageCode") + .HasColumnType("TEXT"); + + b.Property("PreferredAudioTitle") + .HasColumnType("TEXT"); + + b.Property("PreferredSubtitleLanguageCode") + .HasColumnType("TEXT"); + + b.Property("Start") + .HasColumnType("TEXT"); + + b.Property("SubtitleMode") + .HasColumnType("INTEGER"); + + b.Property("WatermarkId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("PlayoutId"); + + b.HasIndex("WatermarkId"); + + 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("SmartCollectionId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CollectionId"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("MultiCollectionId"); + + b.HasIndex("PlayoutId"); + + 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("RandomStartPoint") + .HasColumnType("INTEGER"); + + b.Property("ShuffleScheduleItems") + .HasColumnType("INTEGER"); + + b.Property("TreatCollectionsAsShows") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("ProgramSchedule", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleAlternate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DaysOfMonth") + .HasColumnType("TEXT"); + + b.Property("DaysOfWeek") + .HasColumnType("TEXT"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("MonthsOfYear") + .HasColumnType("TEXT"); + + b.Property("PlayoutId") + .HasColumnType("INTEGER"); + + b.Property("ProgramScheduleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PlayoutId"); + + b.HasIndex("ProgramScheduleId"); + + b.ToTable("ProgramScheduleAlternate", (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("PreferredAudioLanguageCode") + .HasColumnType("TEXT"); + + b.Property("PreferredAudioTitle") + .HasColumnType("TEXT"); + + b.Property("PreferredSubtitleLanguageCode") + .HasColumnType("TEXT"); + + b.Property("ProgramScheduleId") + .HasColumnType("INTEGER"); + + b.Property("SmartCollectionId") + .HasColumnType("INTEGER"); + + b.Property("StartTime") + .HasColumnType("TEXT"); + + b.Property("SubtitleMode") + .HasColumnType("INTEGER"); + + b.Property("TailFillerId") + .HasColumnType("INTEGER"); + + b.Property("WatermarkId") + .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.HasIndex("WatermarkId"); + + b.ToTable("ProgramScheduleItem", (string)null); + + b.UseTptMappingStrategy(); + }); + + 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("AlbumArtist") + .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.Subtitle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArtistMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Codec") + .HasColumnType("TEXT"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("Default") + .HasColumnType("INTEGER"); + + b.Property("EpisodeMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Forced") + .HasColumnType("INTEGER"); + + b.Property("IsExtracted") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(false); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("MovieMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MusicVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("OtherVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("SDH") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(false); + + b.Property("SeasonMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ShowMetadataId") + .HasColumnType("INTEGER"); + + b.Property("SongMetadataId") + .HasColumnType("INTEGER"); + + b.Property("StreamIndex") + .HasColumnType("INTEGER"); + + b.Property("SubtitleKind") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + 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("Subtitle", (string)null); + }); + + 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("ExternalCollectionId") + .HasColumnType("TEXT"); + + 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.Property("OtherVideoMetadataId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("OtherVideoMetadataId"); + + b.ToTable("Writer", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Emby.EmbyPathInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("EmbyLibraryId") + .HasColumnType("INTEGER"); + + b.Property("NetworkPath") + .HasColumnType("TEXT"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("EmbyLibraryId"); + + b.ToTable("EmbyPathInfo"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Jellyfin.JellyfinPathInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("JellyfinLibraryId") + .HasColumnType("INTEGER"); + + b.Property("NetworkPath") + .HasColumnType("TEXT"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("JellyfinLibraryId"); + + b.ToTable("JellyfinPathInfo"); + }); + + 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.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.LocalLibrary", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Library"); + + b.ToTable("LocalLibrary", (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.Artist", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.ToTable("Artist", (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.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.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.EmbyMediaSource", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaSource"); + + b.Property("LastCollectionsScan") + .HasColumnType("TEXT"); + + b.Property("OperatingSystem") + .HasColumnType("TEXT"); + + b.Property("ServerName") + .HasColumnType("TEXT"); + + b.ToTable("EmbyMediaSource", (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.LocalMediaSource", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaSource"); + + b.ToTable("LocalMediaSource", (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("DiscardToFillAttempts") + .HasColumnType("INTEGER"); + + 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.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.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.PlexEpisode", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Episode"); + + b.Property("Etag") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.ToTable("PlexEpisode", (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.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.PlexMovie", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Movie"); + + b.Property("Etag") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.ToTable("PlexMovie", (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.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.PlexSeason", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Season"); + + b.Property("Etag") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.ToTable("PlexSeason", (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.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.PlexShow", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Show"); + + b.Property("Etag") + .HasColumnType("TEXT"); + + 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") + .OnDelete(DeleteBehavior.Cascade); + + 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") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.OtherVideoMetadata", null) + .WithMany("Actors") + .HasForeignKey("OtherVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null) + .WithMany("Actors") + .HasForeignKey("SeasonMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + 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") + .OnDelete(DeleteBehavior.Cascade); + + 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); + + b.HasOne("ErsatzTV.Core.Domain.MusicVideoMetadata", null) + .WithMany("Directors") + .HasForeignKey("MusicVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.OtherVideoMetadata", null) + .WithMany("Directors") + .HasForeignKey("OtherVideoMetadataId") + .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") + .OnDelete(DeleteBehavior.Cascade); + + 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") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null) + .WithMany("Genres") + .HasForeignKey("SeasonMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + 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") + .OnDelete(DeleteBehavior.Cascade); + + 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") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.OtherVideoMetadata", null) + .WithMany("Guids") + .HasForeignKey("OtherVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + 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") + .OnDelete(DeleteBehavior.Cascade); + }); + + 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.MusicVideoArtist", b => + { + b.HasOne("ErsatzTV.Core.Domain.MusicVideoMetadata", null) + .WithMany("Artists") + .HasForeignKey("MusicVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + 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("NextStart") + .HasColumnType("TEXT"); + + b1.HasKey("PlayoutId"); + + b1.ToTable("PlayoutAnchor", (string)null); + + b1.WithOwner() + .HasForeignKey("PlayoutId"); + + b1.OwnsOne("ErsatzTV.Core.Domain.CollectionEnumeratorState", "ScheduleItemsEnumeratorState", b2 => + { + b2.Property("PlayoutAnchorPlayoutId") + .HasColumnType("INTEGER"); + + b2.Property("Index") + .HasColumnType("INTEGER"); + + b2.Property("Seed") + .HasColumnType("INTEGER"); + + b2.HasKey("PlayoutAnchorPlayoutId"); + + b2.ToTable("ScheduleItemsEnumeratorState", (string)null); + + b2.WithOwner() + .HasForeignKey("PlayoutAnchorPlayoutId"); + }); + + b1.Navigation("ScheduleItemsEnumeratorState"); + }); + + 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.HasOne("ErsatzTV.Core.Domain.ChannelWatermark", "Watermark") + .WithMany() + .HasForeignKey("WatermarkId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("MediaItem"); + + b.Navigation("Playout"); + + b.Navigation("Watermark"); + }); + + 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.SmartCollection", "SmartCollection") + .WithMany() + .HasForeignKey("SmartCollectionId") + .OnDelete(DeleteBehavior.Cascade); + + 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("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.ProgramScheduleAlternate", b => + { + b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout") + .WithMany("ProgramScheduleAlternates") + .HasForeignKey("PlayoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule") + .WithMany("ProgramScheduleAlternates") + .HasForeignKey("ProgramScheduleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Playout"); + + b.Navigation("ProgramSchedule"); + }); + + 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.HasOne("ErsatzTV.Core.Domain.ChannelWatermark", "Watermark") + .WithMany() + .HasForeignKey("WatermarkId") + .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"); + + b.Navigation("Watermark"); + }); + + 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") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null) + .WithMany("Studios") + .HasForeignKey("EpisodeMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + 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") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null) + .WithMany("Studios") + .HasForeignKey("SeasonMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + 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.Subtitle", b => + { + b.HasOne("ErsatzTV.Core.Domain.ArtistMetadata", null) + .WithMany("Subtitles") + .HasForeignKey("ArtistMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null) + .WithMany("Subtitles") + .HasForeignKey("EpisodeMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MovieMetadata", null) + .WithMany("Subtitles") + .HasForeignKey("MovieMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MusicVideoMetadata", null) + .WithMany("Subtitles") + .HasForeignKey("MusicVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.OtherVideoMetadata", null) + .WithMany("Subtitles") + .HasForeignKey("OtherVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null) + .WithMany("Subtitles") + .HasForeignKey("SeasonMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.ShowMetadata", null) + .WithMany("Subtitles") + .HasForeignKey("ShowMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SongMetadata", null) + .WithMany("Subtitles") + .HasForeignKey("SongMetadataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Tag", b => + { + b.HasOne("ErsatzTV.Core.Domain.ArtistMetadata", null) + .WithMany("Tags") + .HasForeignKey("ArtistMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null) + .WithMany("Tags") + .HasForeignKey("EpisodeMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + 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") + .OnDelete(DeleteBehavior.Cascade); + + 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); + + b.HasOne("ErsatzTV.Core.Domain.OtherVideoMetadata", null) + .WithMany("Writers") + .HasForeignKey("OtherVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Emby.EmbyPathInfo", b => + { + b.HasOne("ErsatzTV.Core.Domain.EmbyLibrary", null) + .WithMany("PathInfos") + .HasForeignKey("EmbyLibraryId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Jellyfin.JellyfinPathInfo", b => + { + b.HasOne("ErsatzTV.Core.Domain.JellyfinLibrary", null) + .WithMany("PathInfos") + .HasForeignKey("JellyfinLibraryId") + .OnDelete(DeleteBehavior.Cascade); + }); + + 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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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("Subtitles"); + + 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("Subtitles"); + + 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("Subtitles"); + + 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("Artists"); + + b.Navigation("Artwork"); + + b.Navigation("Directors"); + + b.Navigation("Genres"); + + b.Navigation("Guids"); + + b.Navigation("Studios"); + + b.Navigation("Subtitles"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.OtherVideoMetadata", b => + { + b.Navigation("Actors"); + + b.Navigation("Artwork"); + + b.Navigation("Directors"); + + b.Navigation("Genres"); + + b.Navigation("Guids"); + + b.Navigation("Studios"); + + b.Navigation("Subtitles"); + + b.Navigation("Tags"); + + b.Navigation("Writers"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b => + { + b.Navigation("Items"); + + b.Navigation("ProgramScheduleAlternates"); + + b.Navigation("ProgramScheduleAnchors"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramSchedule", b => + { + b.Navigation("Items"); + + b.Navigation("Playouts"); + + b.Navigation("ProgramScheduleAlternates"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.SeasonMetadata", b => + { + b.Navigation("Actors"); + + b.Navigation("Artwork"); + + b.Navigation("Genres"); + + b.Navigation("Guids"); + + b.Navigation("Studios"); + + b.Navigation("Subtitles"); + + 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("Subtitles"); + + 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("Subtitles"); + + 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.EmbyLibrary", b => + { + b.Navigation("PathInfos"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinLibrary", b => + { + b.Navigation("PathInfos"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Artist", b => + { + b.Navigation("ArtistMetadata"); + + b.Navigation("MusicVideos"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Episode", b => + { + b.Navigation("EpisodeMetadata"); + + b.Navigation("MediaVersions"); + }); + + 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.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"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyMediaSource", b => + { + b.Navigation("Connections"); + + b.Navigation("PathReplacements"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinMediaSource", b => + { + b.Navigation("Connections"); + + b.Navigation("PathReplacements"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSource", b => + { + b.Navigation("Connections"); + + b.Navigation("PathReplacements"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ErsatzTV.Infrastructure/Migrations/20230613201543_Add_ProgramScheduleItemDuration_DiscardToFillAttempts.cs b/ErsatzTV.Infrastructure/Migrations/20230613201543_Add_ProgramScheduleItemDuration_DiscardToFillAttempts.cs new file mode 100644 index 000000000..3481263ee --- /dev/null +++ b/ErsatzTV.Infrastructure/Migrations/20230613201543_Add_ProgramScheduleItemDuration_DiscardToFillAttempts.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ErsatzTV.Infrastructure.Migrations +{ + /// + public partial class Add_ProgramScheduleItemDuration_DiscardToFillAttempts : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "DiscardToFillAttempts", + table: "ProgramScheduleDurationItem", + type: "INTEGER", + nullable: false, + defaultValue: 0); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "DiscardToFillAttempts", + table: "ProgramScheduleDurationItem"); + } + } +} diff --git a/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs b/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs index 8e38c5954..b0e792e78 100644 --- a/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs +++ b/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs @@ -15,7 +15,7 @@ namespace ErsatzTV.Infrastructure.Migrations protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "7.0.4"); + modelBuilder.HasAnnotation("ProductVersion", "7.0.7"); modelBuilder.Entity("ErsatzTV.Core.Domain.Actor", b => { @@ -2503,6 +2503,9 @@ namespace ErsatzTV.Infrastructure.Migrations { b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); + b.Property("DiscardToFillAttempts") + .HasColumnType("INTEGER"); + b.Property("PlayoutDuration") .HasColumnType("TEXT"); diff --git a/ErsatzTV.Infrastructure/Scheduling/MultiEpisodeShuffleCollectionEnumerator.cs b/ErsatzTV.Infrastructure/Scheduling/MultiEpisodeShuffleCollectionEnumerator.cs index 6853e258c..56937a428 100644 --- a/ErsatzTV.Infrastructure/Scheduling/MultiEpisodeShuffleCollectionEnumerator.cs +++ b/ErsatzTV.Infrastructure/Scheduling/MultiEpisodeShuffleCollectionEnumerator.cs @@ -1,4 +1,5 @@ using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Extensions; using ErsatzTV.Core.Interfaces.Scheduling; using ErsatzTV.Core.Interfaces.Scripting; using ErsatzTV.Core.Scheduling; @@ -15,6 +16,7 @@ public class MultiEpisodeShuffleCollectionEnumerator : IMediaCollectionEnumerato private readonly List _ungrouped; private CloneableRandom _random; private IList _shuffled; + private readonly Lazy> _lazyMinimumDuration; public MultiEpisodeShuffleCollectionEnumerator( IList mediaItems, @@ -72,6 +74,7 @@ public class MultiEpisodeShuffleCollectionEnumerator : IMediaCollectionEnumerato _random = new CloneableRandom(state.Seed); _shuffled = Shuffle(_random); + _lazyMinimumDuration = new Lazy>(() => _shuffled.Bind(i => i.GetDuration()).HeadOrNone()); State = new CollectionEnumeratorState { Seed = state.Seed }; while (State.Index < state.Index) @@ -197,4 +200,6 @@ public class MultiEpisodeShuffleCollectionEnumerator : IMediaCollectionEnumerato return copy; } + + public Option MinimumDuration => _lazyMinimumDuration.Value; } diff --git a/ErsatzTV/Pages/Artist.razor b/ErsatzTV/Pages/Artist.razor index 057a64716..870224c0e 100644 --- a/ErsatzTV/Pages/Artist.razor +++ b/ErsatzTV/Pages/Artist.razor @@ -258,7 +258,7 @@ DialogResult result = await dialog.Result; if (!result.Canceled && result.Data is ProgramScheduleViewModel schedule) { - await _mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, PlayoutMode.One, ProgramScheduleItemCollectionType.Artist, null, null, null, ArtistId, PlaybackOrder.Shuffle, null, null, TailMode.None, null, GuideMode.Normal, null, null, null, null, null, null, null, null, null, null), _cts.Token); + await _mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, PlayoutMode.One, ProgramScheduleItemCollectionType.Artist, null, null, null, ArtistId, PlaybackOrder.Shuffle, null, null, TailMode.None, null, null, GuideMode.Normal, null, null, null, null, null, null, null, null, null, null), _cts.Token); _navigationManager.NavigateTo($"schedules/{schedule.Id}/items"); } } diff --git a/ErsatzTV/Pages/ScheduleItemsEditor.razor b/ErsatzTV/Pages/ScheduleItemsEditor.razor index 3df48db68..246d63026 100644 --- a/ErsatzTV/Pages/ScheduleItemsEditor.razor +++ b/ErsatzTV/Pages/ScheduleItemsEditor.razor @@ -228,6 +228,7 @@ Offline Filler + Normal @@ -489,6 +490,7 @@ case ProgramScheduleItemDurationViewModel duration: result.PlayoutDuration = duration.PlayoutDuration; result.TailMode = duration.TailMode; + result.DiscardToFillAttempts = duration.DiscardToFillAttempts; break; } @@ -546,6 +548,7 @@ item.MultipleCount, item.PlayoutDuration, item.TailMode, + item.DiscardToFillAttempts, item.CustomTitle, item.GuideMode, item.PreRollFiller?.Id, diff --git a/ErsatzTV/Pages/TelevisionEpisodeList.razor b/ErsatzTV/Pages/TelevisionEpisodeList.razor index 3107d7088..d03d08c1b 100644 --- a/ErsatzTV/Pages/TelevisionEpisodeList.razor +++ b/ErsatzTV/Pages/TelevisionEpisodeList.razor @@ -241,7 +241,7 @@ DialogResult result = await dialog.Result; if (!result.Canceled && result.Data is ProgramScheduleViewModel schedule) { - await _mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, PlayoutMode.One, ProgramScheduleItemCollectionType.TelevisionSeason, null, null, null, SeasonId, PlaybackOrder.Shuffle, null, null, TailMode.None, null, GuideMode.Normal, null, null, null, null, null, null, null, null, null, null), _cts.Token); + await _mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, PlayoutMode.One, ProgramScheduleItemCollectionType.TelevisionSeason, null, null, null, SeasonId, PlaybackOrder.Shuffle, null, null, TailMode.None, null, null, GuideMode.Normal, null, null, null, null, null, null, null, null, null, null), _cts.Token); _navigationManager.NavigateTo($"schedules/{schedule.Id}/items"); } } diff --git a/ErsatzTV/Pages/TelevisionSeasonList.razor b/ErsatzTV/Pages/TelevisionSeasonList.razor index cee9be8b3..763440fbf 100644 --- a/ErsatzTV/Pages/TelevisionSeasonList.razor +++ b/ErsatzTV/Pages/TelevisionSeasonList.razor @@ -228,7 +228,7 @@ DialogResult result = await dialog.Result; if (!result.Canceled && result.Data is ProgramScheduleViewModel schedule) { - await _mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, PlayoutMode.One, ProgramScheduleItemCollectionType.TelevisionShow, null, null, null, ShowId, PlaybackOrder.Shuffle, null, null, TailMode.None, null, GuideMode.Normal, null, null, null, null, null, null, null, null, null, null), _cts.Token); + await _mediator.Send(new AddProgramScheduleItem(schedule.Id, StartType.Dynamic, null, PlayoutMode.One, ProgramScheduleItemCollectionType.TelevisionShow, null, null, null, ShowId, PlaybackOrder.Shuffle, null, null, TailMode.None, null, null, GuideMode.Normal, null, null, null, null, null, null, null, null, null, null), _cts.Token); _navigationManager.NavigateTo($"schedules/{schedule.Id}/items"); } } diff --git a/ErsatzTV/Validators/ProgramScheduleItemEditViewModelValidator.cs b/ErsatzTV/Validators/ProgramScheduleItemEditViewModelValidator.cs index 7deb1967f..bece694b3 100644 --- a/ErsatzTV/Validators/ProgramScheduleItemEditViewModelValidator.cs +++ b/ErsatzTV/Validators/ProgramScheduleItemEditViewModelValidator.cs @@ -16,6 +16,10 @@ public class ProgramScheduleItemEditViewModelValidator : AbstractValidator RuleFor(i => i.MultipleCount).NotNull().GreaterThanOrEqualTo(0)); When( i => i.PlayoutMode == PlayoutMode.Duration, - () => RuleFor(i => i.PlayoutDuration).NotNull()); + () => + { + RuleFor(i => i.PlayoutDuration).NotNull(); + RuleFor(i => i.DiscardToFillAttempts).NotNull().GreaterThanOrEqualTo(0); + }); } } diff --git a/ErsatzTV/ViewModels/ProgramScheduleItemEditViewModel.cs b/ErsatzTV/ViewModels/ProgramScheduleItemEditViewModel.cs index f542a69cd..aed7f2074 100644 --- a/ErsatzTV/ViewModels/ProgramScheduleItemEditViewModel.cs +++ b/ErsatzTV/ViewModels/ProgramScheduleItemEditViewModel.cs @@ -13,6 +13,7 @@ public class ProgramScheduleItemEditViewModel : INotifyPropertyChanged { private ProgramScheduleItemCollectionType _collectionType; private int? _multipleCount; + private int? _discardToFillAttempts; private TimeSpan? _playoutDuration; private TimeSpan? _startTime; @@ -96,7 +97,12 @@ public class ProgramScheduleItemEditViewModel : INotifyPropertyChanged } public TailMode TailMode { get; set; } - + + public int? DiscardToFillAttempts + { + get => PlayoutMode == PlayoutMode.Duration ? _discardToFillAttempts ?? 0 : null; + set => _discardToFillAttempts = value; + } public string CustomTitle { get; set; } public GuideMode GuideMode { get; set; }