From 8c7e81c863cd7e7866bc4b1fbc6a5cb89e7af591 Mon Sep 17 00:00:00 2001 From: Timothy Date: Fri, 17 Jul 2026 09:22:15 +0200 Subject: [PATCH 01/12] =?UTF-8?q?feat(70):=20WeightedShuffle=20playback=20?= =?UTF-8?q?order=20=E2=80=94=20weighted=20/=20fair-share=20distribution?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds PlaybackOrder.WeightedShuffle = 9 (Classic engine only) plus the per-source weight it distributes by. Why a new order rather than making ShuffleInOrder weight-aware: ShuffleInOrder's balanced shuffle pads sources to equal length with Option.None spacers, and spacers emit nothing — so one cycle plays every item exactly once and airtime stays proportional to collection size. It prevents *clumping*, not *domination*. Retrofitting weights onto it would silently change shipped users' output. WeightedShuffleCollectionEnumerator picks a source by smooth weighted round-robin (acc += weight; richest wins; pays the total), then takes that source's next item. Weights 3:1 emit A A B A; ties break to the earliest source in list order. Fair-share is the equal-weights default, so one mechanism covers both behaviors in the issue. One rotation is sized so the source needing the most picks works through all its items at its share; smaller sources loop within it — that looping is what makes equal weights mean equal airtime regardless of library size. The sequence stays a pure function of (Seed, Index), so it restores by replay like its siblings and needs no per-source persisted counters. It consumes ShuffleSourceBuilder.GetCollectionItemsForShuffleInOrder unchanged — the schedule-entity-free entry point #380 reserved for this issue. Source identity is destroyed on the Shuffle path (MultiCollectionGrouper collapses to GroupedMediaItem + Distinct), so the weight is only reachable via CollectionWithItems on the ShuffleInOrder-shaped path. Weight is persisted on MultiCollectionItem AND MultiCollectionSmartItem — the mirror is mandatory; omitting the smart item silently un-weights smart-collection members. Both carry a DB default of 1: without it existing rows would migrate to 0, and a 0-weight source is dropped by the enumerator. Refs #70 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Domain/Collection/MultiCollectionItem.cs | 6 + .../Collection/MultiCollectionSmartItem.cs | 6 + ErsatzTV.Core/Domain/PlaybackOrder.cs | 6 +- .../Scheduling/CollectionWithItems.cs | 3 +- ErsatzTV.Core/Scheduling/PlayoutBuilder.cs | 10 +- .../WeightedShuffleCollectionEnumerator.cs | 231 + ...1_AddMultiCollectionItemWeight.Designer.cs | 7250 +++++++++++++++++ ...0717072111_AddMultiCollectionItemWeight.cs | 40 + .../Migrations/TvContextModelSnapshot.cs | 10 + ...6_AddMultiCollectionItemWeight.Designer.cs | 7075 ++++++++++++++++ ...0717071946_AddMultiCollectionItemWeight.cs | 40 + .../Migrations/TvContextModelSnapshot.cs | 10 + .../MultiCollectionConfiguration.cs | 18 +- .../Repositories/MediaCollectionRepository.cs | 9 +- 14 files changed, 14706 insertions(+), 8 deletions(-) create mode 100644 ErsatzTV.Core/Scheduling/WeightedShuffleCollectionEnumerator.cs create mode 100644 ErsatzTV.Infrastructure.MySql/Migrations/20260717072111_AddMultiCollectionItemWeight.Designer.cs create mode 100644 ErsatzTV.Infrastructure.MySql/Migrations/20260717072111_AddMultiCollectionItemWeight.cs create mode 100644 ErsatzTV.Infrastructure.Sqlite/Migrations/20260717071946_AddMultiCollectionItemWeight.Designer.cs create mode 100644 ErsatzTV.Infrastructure.Sqlite/Migrations/20260717071946_AddMultiCollectionItemWeight.cs diff --git a/ErsatzTV.Core/Domain/Collection/MultiCollectionItem.cs b/ErsatzTV.Core/Domain/Collection/MultiCollectionItem.cs index 0b928b719..1b6019e63 100644 --- a/ErsatzTV.Core/Domain/Collection/MultiCollectionItem.cs +++ b/ErsatzTV.Core/Domain/Collection/MultiCollectionItem.cs @@ -8,4 +8,10 @@ public class MultiCollectionItem public Collection Collection { get; set; } public bool ScheduleAsGroup { get; set; } public PlaybackOrder PlaybackOrder { get; set; } + + /// + /// Relative share of airtime under (#70). Defaults to 1, + /// which is fair-share. Ignored by every other playback order. + /// + public int Weight { get; set; } = 1; } diff --git a/ErsatzTV.Core/Domain/Collection/MultiCollectionSmartItem.cs b/ErsatzTV.Core/Domain/Collection/MultiCollectionSmartItem.cs index ed09c6bfe..447554a64 100644 --- a/ErsatzTV.Core/Domain/Collection/MultiCollectionSmartItem.cs +++ b/ErsatzTV.Core/Domain/Collection/MultiCollectionSmartItem.cs @@ -8,4 +8,10 @@ public class MultiCollectionSmartItem public SmartCollection SmartCollection { get; set; } public bool ScheduleAsGroup { get; set; } public PlaybackOrder PlaybackOrder { get; set; } + + /// + /// Relative share of airtime under (#70). Defaults to 1, + /// which is fair-share. Ignored by every other playback order. + /// + public int Weight { get; set; } = 1; } diff --git a/ErsatzTV.Core/Domain/PlaybackOrder.cs b/ErsatzTV.Core/Domain/PlaybackOrder.cs index 7655a6e8c..2c2b249f9 100644 --- a/ErsatzTV.Core/Domain/PlaybackOrder.cs +++ b/ErsatzTV.Core/Domain/PlaybackOrder.cs @@ -11,5 +11,9 @@ public enum PlaybackOrder MultiEpisodeShuffle = 5, SeasonEpisode = 6, RandomRotation = 7, - Marathon = 8 + Marathon = 8, + + // weighted / fair-share distribution (#70); Classic schedule items only -- write-path validation + // rejects it elsewhere, because the other engines silently drop or substitute an unknown order + WeightedShuffle = 9 } diff --git a/ErsatzTV.Core/Scheduling/CollectionWithItems.cs b/ErsatzTV.Core/Scheduling/CollectionWithItems.cs index e41fb6932..950d0eb37 100644 --- a/ErsatzTV.Core/Scheduling/CollectionWithItems.cs +++ b/ErsatzTV.Core/Scheduling/CollectionWithItems.cs @@ -9,4 +9,5 @@ public record CollectionWithItems( List MediaItems, bool ScheduleAsGroup, PlaybackOrder PlaybackOrder, - bool UseCustomOrder); + bool UseCustomOrder, + int Weight = 1); diff --git a/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs b/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs index bc97939f3..13f0c8bfe 100644 --- a/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs +++ b/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs @@ -1,4 +1,4 @@ -using System.IO.Abstractions; +using System.IO.Abstractions; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Filler; using ErsatzTV.Core.Errors; @@ -1383,6 +1383,14 @@ public class PlayoutBuilder : IPlayoutBuilder state, activeSchedule.RandomStartPoint, cancellationToken); + case PlaybackOrder.WeightedShuffle: + return new WeightedShuffleCollectionEnumerator( + await ShuffleSourceBuilder.GetCollectionItemsForShuffleInOrder( + _mediaCollectionRepository, + collectionKey, + cancellationToken), + state, + cancellationToken); case PlaybackOrder.MultiEpisodeShuffle when collectionKey.CollectionType == CollectionType.TelevisionShow && collectionKey.MediaItemId.HasValue: diff --git a/ErsatzTV.Core/Scheduling/WeightedShuffleCollectionEnumerator.cs b/ErsatzTV.Core/Scheduling/WeightedShuffleCollectionEnumerator.cs new file mode 100644 index 000000000..21672fdff --- /dev/null +++ b/ErsatzTV.Core/Scheduling/WeightedShuffleCollectionEnumerator.cs @@ -0,0 +1,231 @@ +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Extensions; +using ErsatzTV.Core.Interfaces.Scheduling; + +namespace ErsatzTV.Core.Scheduling; + +/// +/// Weighted / fair-share distribution (issue #70). Picks a *source* by smooth weighted round-robin, +/// then takes that source's next item. A source's is its +/// relative share of airtime: weights 3:1 emit A A B A per rotation. +/// +/// Fair-share is the equal-weights degenerate case (the default, since Weight defaults to 1): +/// every source airs equally often regardless of library size, so a small show loops while a +/// large one works through its items. This is what distinguishes it from +/// , whose balanced shuffle plays every item +/// exactly once per cycle and therefore leaves airtime proportional to collection size +/// (it prevents clumping, not domination). +/// +/// +/// Deterministic and stateless: the emitted sequence is a pure function of +/// (, ), +/// so it restores by replay exactly like its siblings and needs no per-source persisted counters. +/// +/// +public class WeightedShuffleCollectionEnumerator : IMediaCollectionEnumerator +{ + /// + /// Upper bound on the precomputed rotation, so a large collection paired with a low weight + /// (picks needed grows as items * totalWeight / weight) can't allocate without limit. Hitting the + /// clamp only means the largest source doesn't finish its items within one rotation before the + /// reseed; the weight ratio itself is unaffected. + /// + private const int MaxCycleLength = 100_000; + + private readonly CancellationToken _cancellationToken; + private readonly IList _collections; + private readonly Lazy> _lazyMinimumDuration; + private Random _random; + private MediaItem[] _shuffled; + + public WeightedShuffleCollectionEnumerator( + IList collections, + CollectionEnumeratorState state, + CancellationToken cancellationToken) + { + CurrentIncludeInProgramGuide = Option.None; + + _collections = collections; + _cancellationToken = cancellationToken; + + int cycleLength = CycleLength(collections); + if (cycleLength > 0 && state.Index >= cycleLength) + { + state.Index = 0; + state.Seed = new Random(state.Seed).Next(); + } + + _random = new Random(state.Seed); + _shuffled = Build(_collections, _random); + _lazyMinimumDuration = + new Lazy>(() => + _shuffled.Bind(i => i.GetNonZeroDuration()).OrderBy(identity).HeadOrNone()); + + State = new CollectionEnumeratorState { Seed = state.Seed }; + while (State.Index < state.Index) + { + MoveNext(Option.None); + } + } + + public void ResetState(CollectionEnumeratorState state) + { + // only rebuild if needed + if (State.Seed != state.Seed) + { + _random = new Random(state.Seed); + _shuffled = Build(_collections, _random); + } + + State.Seed = state.Seed; + State.Index = state.Index; + } + + public string SchedulingContextName => "Weighted Shuffle"; + + public CollectionEnumeratorState State { get; } + + public Option Current => _shuffled.Length != 0 ? _shuffled[State.Index % _shuffled.Length] : None; + + public Option CurrentIncludeInProgramGuide { get; } + + public void MoveNext(Option scheduledAt) + { + if (_shuffled.Length == 0) + { + return; + } + + if ((State.Index + 1) % _shuffled.Length == 0) + { + Option tail = Current; + + State.Index = 0; + do + { + State.Seed = _random.Next(); + _random = new Random(State.Seed); + _shuffled = Build(_collections, _random); + } while (!_cancellationToken.IsCancellationRequested && _collections.Count > 1 && + _shuffled.Length > 0 && Current.Map(x => x.Id) == tail.Map(x => x.Id)); + } + else + { + State.Index++; + } + + if (_shuffled.Length > 0) + { + State.Index %= _shuffled.Length; + } + } + + public Option MinimumDuration => _lazyMinimumDuration.Value; + + public int Count => _shuffled.Length; + + /// + /// Length of one rotation: enough picks for the source that needs the most of them to work through + /// all of its items at its share of the rotation. Sources needing fewer picks loop within the + /// rotation — that looping is exactly what makes equal weights mean equal airtime. + /// + private static int CycleLength(IList collections) + { + List active = ActiveSources(collections); + if (active.Count == 0) + { + return 0; + } + + int totalWeight = active.Sum(c => c.Weight); + + var length = 0; + foreach (CollectionWithItems collection in active) + { + // picks needed for this source to emit every item once, given it wins Weight of every totalWeight picks + var needed = (int)Math.Min( + MaxCycleLength, + Math.Ceiling(collection.MediaItems.Count * (double)totalWeight / collection.Weight)); + length = Math.Max(length, needed); + } + + return length; + } + + private static List ActiveSources(IList collections) => + collections.Filter(c => c.MediaItems.Count > 0 && c.Weight > 0).ToList(); + + private static MediaItem[] Build(IList collections, Random random) + { + List active = ActiveSources(collections); + if (active.Count == 0) + { + return []; + } + + var sources = active + .Map(c => new WeightedSource { Weight = c.Weight, Items = OrderItems(c, random) }) + .ToList(); + + int totalWeight = sources.Sum(s => s.Weight); + int cycleLength = CycleLength(collections); + + var result = new List(cycleLength); + for (var i = 0; i < cycleLength; i++) + { + // smooth weighted round-robin: every source gains its weight, the richest wins and pays the total. + // Strict '>' keeps ties on the earliest source in list order, which makes the sequence deterministic. + WeightedSource pick = null; + foreach (WeightedSource source in sources) + { + source.Accumulator += source.Weight; + if (pick is null || source.Accumulator > pick.Accumulator) + { + pick = source; + } + } + + pick.Accumulator -= totalWeight; + result.Add(pick.Items[pick.Cursor % pick.Items.Length]); + pick.Cursor++; + } + + return result.ToArray(); + } + + private static MediaItem[] OrderItems(CollectionWithItems collectionWithItems, Random random) + { + // A custom-ordered collection is an explicit user sequence, so it is honored rather than shuffled + // (same rule as ShuffleInOrderCollectionEnumerator). Otherwise the source's items are shuffled per + // rotation, which is what makes the reseed on wrap produce a different rotation. + if (collectionWithItems.UseCustomOrder) + { + return collectionWithItems.MediaItems.ToArray(); + } + + return Shuffle(collectionWithItems.MediaItems, random); + } + + private static MediaItem[] Shuffle(IEnumerable list, Random random) + { + MediaItem[] copy = list.ToArray(); + + int n = copy.Length; + while (n > 1) + { + n--; + int k = random.Next(n + 1); + (copy[k], copy[n]) = (copy[n], copy[k]); + } + + return copy; + } + + private class WeightedSource + { + public int Weight { get; init; } + public MediaItem[] Items { get; init; } + public int Cursor { get; set; } + public int Accumulator { get; set; } + } +} diff --git a/ErsatzTV.Infrastructure.MySql/Migrations/20260717072111_AddMultiCollectionItemWeight.Designer.cs b/ErsatzTV.Infrastructure.MySql/Migrations/20260717072111_AddMultiCollectionItemWeight.Designer.cs new file mode 100644 index 000000000..ac07b0dd8 --- /dev/null +++ b/ErsatzTV.Infrastructure.MySql/Migrations/20260717072111_AddMultiCollectionItemWeight.Designer.cs @@ -0,0 +1,7250 @@ +// +using System; +using ErsatzTV.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace ErsatzTV.Infrastructure.MySql.Migrations +{ + [DbContext(typeof(TvContext))] + [Migration("20260717072111_AddMultiCollectionItemWeight")] + partial class AddMultiCollectionItemWeight + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.12") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Actor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ArtistMetadataId") + .HasColumnType("int"); + + b.Property("ArtworkId") + .HasColumnType("int"); + + b.Property("EpisodeMetadataId") + .HasColumnType("int"); + + b.Property("ImageMetadataId") + .HasColumnType("int"); + + b.Property("MovieMetadataId") + .HasColumnType("int"); + + b.Property("MusicVideoMetadataId") + .HasColumnType("int"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("OtherVideoMetadataId") + .HasColumnType("int"); + + b.Property("RemoteStreamMetadataId") + .HasColumnType("int"); + + b.Property("Role") + .HasColumnType("longtext"); + + b.Property("SeasonMetadataId") + .HasColumnType("int"); + + b.Property("ShowMetadataId") + .HasColumnType("int"); + + b.Property("SongMetadataId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ArtistMetadataId"); + + b.HasIndex("ArtworkId") + .IsUnique(); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("ImageMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("OtherVideoMetadataId"); + + b.HasIndex("RemoteStreamMetadataId"); + + 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("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ArtistId") + .HasColumnType("int"); + + b.Property("Biography") + .HasColumnType("longtext"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)"); + + b.Property("DateUpdated") + .HasColumnType("datetime(6)"); + + b.Property("Disambiguation") + .HasColumnType("longtext"); + + b.Property("Formed") + .HasColumnType("longtext"); + + b.Property("MetadataKind") + .HasColumnType("int"); + + b.Property("OriginalTitle") + .HasColumnType("longtext"); + + b.Property("ReleaseDate") + .HasColumnType("datetime(6)"); + + b.Property("SortTitle") + .HasColumnType("longtext"); + + b.Property("Title") + .HasColumnType("varchar(255)") + .UseCollation("utf8mb4_general_ci"); + + b.Property("Year") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ArtistId"); + + b.HasIndex("Title"); + + b.ToTable("ArtistMetadata", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Artwork", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ArtistMetadataId") + .HasColumnType("int"); + + b.Property("ArtworkKind") + .HasColumnType("int"); + + b.Property("BlurHash43") + .HasColumnType("longtext"); + + b.Property("BlurHash54") + .HasColumnType("longtext"); + + b.Property("BlurHash64") + .HasColumnType("longtext"); + + b.Property("ChannelId") + .HasColumnType("int"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)"); + + b.Property("DateUpdated") + .HasColumnType("datetime(6)"); + + b.Property("EpisodeMetadataId") + .HasColumnType("int"); + + b.Property("ImageMetadataId") + .HasColumnType("int"); + + b.Property("IsMetadataOrphan") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("tinyint(1)") + .HasComputedColumnSql("CASE WHEN COALESCE(ArtistMetadataId, ChannelId, EpisodeMetadataId, MovieMetadataId, MusicVideoMetadataId, OtherVideoMetadataId, SeasonMetadataId, ShowMetadataId, SongMetadataId, ImageMetadataId, RemoteStreamMetadataId) IS NULL THEN 1 ELSE NULL END", false); + + b.Property("MovieMetadataId") + .HasColumnType("int"); + + b.Property("MusicVideoMetadataId") + .HasColumnType("int"); + + b.Property("OriginalContentType") + .HasColumnType("longtext"); + + b.Property("OtherVideoMetadataId") + .HasColumnType("int"); + + b.Property("Path") + .HasColumnType("longtext"); + + b.Property("RemoteStreamMetadataId") + .HasColumnType("int"); + + b.Property("SeasonMetadataId") + .HasColumnType("int"); + + b.Property("ShowMetadataId") + .HasColumnType("int"); + + b.Property("SongMetadataId") + .HasColumnType("int"); + + b.Property("SourcePath") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("ArtistMetadataId"); + + b.HasIndex("ChannelId"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("ImageMetadataId"); + + b.HasIndex("IsMetadataOrphan"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("OtherVideoMetadataId"); + + b.HasIndex("RemoteStreamMetadataId"); + + b.HasIndex("SeasonMetadataId"); + + b.HasIndex("ShowMetadataId"); + + b.HasIndex("SongMetadataId"); + + b.ToTable("Artwork", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.BlockItemGraphicsElement", b => + { + b.Property("BlockItemId") + .HasColumnType("int"); + + b.Property("GraphicsElementId") + .HasColumnType("int"); + + b.HasKey("BlockItemId", "GraphicsElementId"); + + b.HasIndex("GraphicsElementId"); + + b.ToTable("BlockItemGraphicsElement"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.BlockItemWatermark", b => + { + b.Property("BlockItemId") + .HasColumnType("int"); + + b.Property("WatermarkId") + .HasColumnType("int"); + + b.HasKey("BlockItemId", "WatermarkId"); + + b.HasIndex("WatermarkId"); + + b.ToTable("BlockItemWatermark"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Channel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Categories") + .HasColumnType("longtext"); + + b.Property("FFmpegProfileId") + .HasColumnType("int"); + + b.Property("FallbackFillerId") + .HasColumnType("int"); + + b.Property("Group") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("longtext") + .HasDefaultValue("ErsatzTV"); + + b.Property("IdleBehavior") + .HasColumnType("int"); + + b.Property("IsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("MirrorSourceChannelId") + .HasColumnType("int"); + + b.Property("MusicVideoCreditsMode") + .HasColumnType("int"); + + b.Property("MusicVideoCreditsTemplate") + .HasColumnType("longtext"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .UseCollation("utf8mb4_general_ci"); + + b.Property("Number") + .HasColumnType("varchar(255)"); + + b.Property("PlayoutMode") + .HasColumnType("int"); + + b.Property("PlayoutOffset") + .HasColumnType("time(6)"); + + b.Property("PlayoutSource") + .HasColumnType("int"); + + b.Property("PreferredAudioLanguageCode") + .HasColumnType("longtext"); + + b.Property("PreferredAudioTitle") + .HasColumnType("longtext"); + + b.Property("PreferredSubtitleLanguageCode") + .HasColumnType("longtext"); + + b.Property("ShowInEpg") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("SlugSeconds") + .HasColumnType("double"); + + b.Property("SongVideoMode") + .HasColumnType("int"); + + b.Property("SortNumber") + .HasColumnType("double"); + + b.Property("StreamSelector") + .HasColumnType("longtext"); + + b.Property("StreamSelectorMode") + .HasColumnType("int"); + + b.Property("StreamingMode") + .HasColumnType("int"); + + b.Property("SubtitleMode") + .HasColumnType("int"); + + b.Property("TranscodeMode") + .HasColumnType("int"); + + b.Property("UniqueId") + .HasColumnType("char(36)"); + + b.Property("WatermarkId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FFmpegProfileId"); + + b.HasIndex("FallbackFillerId"); + + b.HasIndex("MirrorSourceChannelId"); + + b.HasIndex("Number") + .IsUnique(); + + b.HasIndex("WatermarkId"); + + b.ToTable("Channel", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ChannelTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Description") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(500) + .HasColumnType("varchar(500)") + .HasDefaultValue(""); + + b.Property("FFmpegProfileId") + .HasColumnType("int"); + + b.Property("FallbackFillerId") + .HasColumnType("int"); + + b.Property("FixedStartTimeBehavior") + .HasColumnType("int"); + + b.Property("IdleBehavior") + .HasColumnType("int"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("MidRollFillerId") + .HasColumnType("int"); + + b.Property("MusicVideoCreditsMode") + .HasColumnType("int"); + + b.Property("MusicVideoCreditsTemplate") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .UseCollation("utf8mb4_general_ci"); + + b.Property("PlayoutMode") + .HasColumnType("int"); + + b.Property("PlayoutSource") + .HasColumnType("int"); + + b.Property("PostRollFillerId") + .HasColumnType("int"); + + b.Property("PreRollFillerId") + .HasColumnType("int"); + + b.Property("PreferredAudioLanguageCode") + .HasColumnType("longtext"); + + b.Property("PreferredAudioTitle") + .HasColumnType("longtext"); + + b.Property("PreferredSubtitleLanguageCode") + .HasColumnType("longtext"); + + b.Property("RandomStartPoint") + .HasColumnType("tinyint(1)"); + + b.Property("ShuffleScheduleItems") + .HasColumnType("tinyint(1)"); + + b.Property("SongVideoMode") + .HasColumnType("int"); + + b.Property("StreamSelector") + .HasColumnType("longtext"); + + b.Property("StreamSelectorMode") + .HasColumnType("int"); + + b.Property("StreamingMode") + .HasColumnType("int"); + + b.Property("SubtitleMode") + .HasColumnType("int"); + + b.Property("TranscodeMode") + .HasColumnType("int"); + + b.Property("WatermarkId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FFmpegProfileId"); + + b.HasIndex("FallbackFillerId"); + + b.HasIndex("MidRollFillerId"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("PostRollFillerId"); + + b.HasIndex("PreRollFillerId"); + + b.HasIndex("WatermarkId"); + + b.ToTable("ChannelTemplate", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ChannelWatermark", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("DurationSeconds") + .HasColumnType("int"); + + b.Property("FrequencyMinutes") + .HasColumnType("int"); + + b.Property("HorizontalMarginPercent") + .HasColumnType("double"); + + b.Property("Image") + .HasColumnType("longtext"); + + b.Property("ImageSource") + .HasColumnType("int"); + + b.Property("Location") + .HasColumnType("int"); + + b.Property("Mode") + .HasColumnType("int"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .UseCollation("utf8mb4_general_ci"); + + b.Property("Opacity") + .HasColumnType("int"); + + b.Property("OpacityExpression") + .HasColumnType("longtext"); + + b.Property("OriginalContentType") + .HasColumnType("longtext"); + + b.Property("PlaceWithinSourceContent") + .HasColumnType("tinyint(1)"); + + b.Property("Size") + .HasColumnType("int"); + + b.Property("VerticalMarginPercent") + .HasColumnType("double"); + + b.Property("WidthPercent") + .HasColumnType("double"); + + b.Property("ZIndex") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("ChannelWatermark", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Collection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .UseCollation("utf8mb4_general_ci"); + + b.Property("UseCustomPlaybackOrder") + .HasColumnType("tinyint(1)"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.CollectionItem", b => + { + b.Property("CollectionId") + .HasColumnType("int"); + + b.Property("MediaItemId") + .HasColumnType("int"); + + b.Property("CustomIndex") + .HasColumnType("int"); + + b.HasKey("CollectionId", "MediaItemId"); + + b.HasIndex("MediaItemId"); + + b.ToTable("CollectionItem", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ConfigElement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Key") + .HasColumnType("varchar(255)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("ConfigElement", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.DecoGraphicsElement", b => + { + b.Property("DecoId") + .HasColumnType("int"); + + b.Property("GraphicsElementId") + .HasColumnType("int"); + + b.HasKey("DecoId", "GraphicsElementId"); + + b.HasIndex("GraphicsElementId"); + + b.ToTable("DecoGraphicsElement"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.DecoWatermark", b => + { + b.Property("DecoId") + .HasColumnType("int"); + + b.Property("WatermarkId") + .HasColumnType("int"); + + b.HasKey("DecoId", "WatermarkId"); + + b.HasIndex("WatermarkId"); + + b.ToTable("DecoWatermark"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Director", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("EpisodeMetadataId") + .HasColumnType("int"); + + b.Property("MovieMetadataId") + .HasColumnType("int"); + + b.Property("MusicVideoMetadataId") + .HasColumnType("int"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("OtherVideoMetadataId") + .HasColumnType("int"); + + 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("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Etag") + .HasColumnType("longtext"); + + b.Property("ItemId") + .HasColumnType("longtext"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("EmbyCollection", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Address") + .HasColumnType("longtext"); + + b.Property("EmbyMediaSourceId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("EmbyMediaSourceId"); + + b.ToTable("EmbyConnection", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyPathReplacement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("EmbyMediaSourceId") + .HasColumnType("int"); + + b.Property("EmbyPath") + .HasColumnType("longtext"); + + b.Property("LocalPath") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("EmbyMediaSourceId"); + + b.ToTable("EmbyPathReplacement", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EpisodeMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("DateAdded") + .HasColumnType("datetime(6)"); + + b.Property("DateUpdated") + .HasColumnType("datetime(6)"); + + b.Property("EpisodeId") + .HasColumnType("int"); + + b.Property("EpisodeNumber") + .HasColumnType("int"); + + b.Property("MetadataKind") + .HasColumnType("int"); + + b.Property("OriginalTitle") + .HasColumnType("longtext"); + + b.Property("Outline") + .HasColumnType("longtext"); + + b.Property("Plot") + .HasColumnType("longtext"); + + b.Property("ReleaseDate") + .HasColumnType("datetime(6)"); + + b.Property("SortTitle") + .HasColumnType("longtext"); + + b.Property("Tagline") + .HasColumnType("longtext"); + + b.Property("Title") + .HasColumnType("longtext"); + + b.Property("Year") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("EpisodeId"); + + b.ToTable("EpisodeMetadata", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.FFmpegProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AllowBFrames") + .HasColumnType("tinyint(1)"); + + b.Property("AudioBitrate") + .HasColumnType("int"); + + b.Property("AudioBufferSize") + .HasColumnType("int"); + + b.Property("AudioChannels") + .HasColumnType("int"); + + b.Property("AudioFormat") + .HasColumnType("int"); + + b.Property("AudioSampleRate") + .HasColumnType("int"); + + b.Property("BitDepth") + .HasColumnType("int"); + + b.Property("DeinterlaceVideo") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("HardwareAcceleration") + .HasColumnType("int"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("NormalizeAudio") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("NormalizeColors") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("NormalizeFramerate") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false); + + b.Property("NormalizeLoudnessMode") + .HasColumnType("int"); + + b.Property("NormalizeVideo") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("PadMode") + .HasColumnType("int"); + + b.Property("QsvExtraHardwareFrames") + .HasColumnType("int"); + + b.Property("ResolutionId") + .HasColumnType("int"); + + b.Property("ScalingBehavior") + .HasColumnType("int"); + + b.Property("TargetLoudness") + .HasColumnType("double"); + + b.Property("ThreadCount") + .HasColumnType("int"); + + b.Property("TonemapAlgorithm") + .HasColumnType("int"); + + b.Property("VaapiDevice") + .HasColumnType("longtext"); + + b.Property("VaapiDisplay") + .HasColumnType("longtext"); + + b.Property("VaapiDriver") + .HasColumnType("int"); + + b.Property("VideoBitrate") + .HasColumnType("int"); + + b.Property("VideoBufferSize") + .HasColumnType("int"); + + b.Property("VideoFormat") + .HasColumnType("int"); + + b.Property("VideoPreset") + .HasColumnType("longtext"); + + b.Property("VideoProfile") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("ResolutionId"); + + b.ToTable("FFmpegProfile", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Filler.FillerPreset", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AllowWatermarks") + .HasColumnType("tinyint(1)"); + + b.Property("CollectionId") + .HasColumnType("int"); + + b.Property("CollectionType") + .HasColumnType("int"); + + b.Property("Count") + .HasColumnType("int"); + + b.Property("Duration") + .HasColumnType("time(6)"); + + b.Property("Expression") + .HasColumnType("longtext"); + + b.Property("FillerKind") + .HasColumnType("int"); + + b.Property("FillerMode") + .HasColumnType("int"); + + b.Property("MediaItemId") + .HasColumnType("int"); + + b.Property("MultiCollectionId") + .HasColumnType("int"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .UseCollation("utf8mb4_general_ci"); + + b.Property("PadToNearestMinute") + .HasColumnType("int"); + + b.Property("PlaylistId") + .HasColumnType("int"); + + b.Property("SmartCollectionId") + .HasColumnType("int"); + + b.Property("UseChaptersAsMediaItems") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("CollectionId"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("MultiCollectionId"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("PlaylistId"); + + b.HasIndex("SmartCollectionId"); + + b.ToTable("FillerPreset", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Genre", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ArtistMetadataId") + .HasColumnType("int"); + + b.Property("EpisodeMetadataId") + .HasColumnType("int"); + + b.Property("ImageMetadataId") + .HasColumnType("int"); + + b.Property("MovieMetadataId") + .HasColumnType("int"); + + b.Property("MusicVideoMetadataId") + .HasColumnType("int"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("OtherVideoMetadataId") + .HasColumnType("int"); + + b.Property("RemoteStreamMetadataId") + .HasColumnType("int"); + + b.Property("SeasonMetadataId") + .HasColumnType("int"); + + b.Property("ShowMetadataId") + .HasColumnType("int"); + + b.Property("SongMetadataId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ArtistMetadataId"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("ImageMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("OtherVideoMetadataId"); + + b.HasIndex("RemoteStreamMetadataId"); + + b.HasIndex("SeasonMetadataId"); + + b.HasIndex("ShowMetadataId"); + + b.HasIndex("SongMetadataId"); + + b.ToTable("Genre"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.GraphicsElement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Path") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("GraphicsElement", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ImageFolderDuration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("DurationSeconds") + .HasColumnType("double"); + + b.Property("LibraryFolderId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("LibraryFolderId") + .IsUnique(); + + b.ToTable("ImageFolderDuration", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ImageMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("DateAdded") + .HasColumnType("datetime(6)"); + + b.Property("DateUpdated") + .HasColumnType("datetime(6)"); + + b.Property("DurationSeconds") + .HasColumnType("double"); + + b.Property("ImageId") + .HasColumnType("int"); + + b.Property("MetadataKind") + .HasColumnType("int"); + + b.Property("OriginalTitle") + .HasColumnType("longtext"); + + b.Property("ReleaseDate") + .HasColumnType("datetime(6)"); + + b.Property("SortTitle") + .HasColumnType("longtext"); + + b.Property("Title") + .HasColumnType("longtext"); + + b.Property("Year") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ImageId"); + + b.ToTable("ImageMetadata", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinCollection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Etag") + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("varchar(36)"); + + b.Property("ItemId") + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("varchar(36)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("ItemId"); + + b.ToTable("JellyfinCollection", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Address") + .HasColumnType("longtext"); + + b.Property("JellyfinMediaSourceId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("JellyfinMediaSourceId"); + + b.ToTable("JellyfinConnection", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinPathReplacement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("JellyfinMediaSourceId") + .HasColumnType("int"); + + b.Property("JellyfinPath") + .HasColumnType("longtext"); + + b.Property("LocalPath") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("JellyfinMediaSourceId"); + + b.ToTable("JellyfinPathReplacement", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LanguageCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("EnglishName") + .HasColumnType("longtext"); + + b.Property("FrenchName") + .HasColumnType("longtext"); + + b.Property("ThreeCode1") + .HasColumnType("longtext"); + + b.Property("ThreeCode2") + .HasColumnType("longtext"); + + b.Property("TwoCode") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("LanguageCode", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Library", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("LastScan") + .HasColumnType("datetime(6)"); + + b.Property("MediaKind") + .HasColumnType("int"); + + b.Property("MediaSourceId") + .HasColumnType("int"); + + b.Property("Name") + .HasColumnType("longtext"); + + 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("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Etag") + .HasColumnType("longtext"); + + b.Property("LibraryPathId") + .HasColumnType("int"); + + b.Property("ParentId") + .HasColumnType("int"); + + b.Property("Path") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("LibraryPathId"); + + b.HasIndex("ParentId"); + + b.ToTable("LibraryFolder", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LibraryPath", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("LastScan") + .HasColumnType("datetime(6)"); + + b.Property("LibraryId") + .HasColumnType("int"); + + b.Property("Path") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("LibraryPath", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaChapter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ChapterId") + .HasColumnType("bigint"); + + b.Property("EndTime") + .HasColumnType("time(6)"); + + b.Property("MediaVersionId") + .HasColumnType("int"); + + b.Property("StartTime") + .HasColumnType("time(6)"); + + b.Property("Title") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("MediaVersionId"); + + b.ToTable("MediaChapter", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("LibraryFolderId") + .HasColumnType("int"); + + b.Property("MediaVersionId") + .HasColumnType("int"); + + b.Property("Path") + .HasColumnType("longtext"); + + b.Property("PathHash") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("LibraryFolderId"); + + b.HasIndex("MediaVersionId"); + + b.HasIndex("PathHash") + .IsUnique(); + + b.ToTable("MediaFile", (string)null); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("LibraryPathId") + .HasColumnType("int"); + + b.Property("State") + .HasColumnType("int"); + + 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("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.HasKey("Id"); + + b.ToTable("MediaSource", (string)null); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AttachedPic") + .HasColumnType("tinyint(1)"); + + b.Property("BitsPerRawSample") + .HasColumnType("int"); + + b.Property("Channels") + .HasColumnType("int"); + + b.Property("Codec") + .HasColumnType("longtext"); + + b.Property("ColorPrimaries") + .HasColumnType("longtext"); + + b.Property("ColorRange") + .HasColumnType("longtext"); + + b.Property("ColorSpace") + .HasColumnType("longtext"); + + b.Property("ColorTransfer") + .HasColumnType("longtext"); + + b.Property("Default") + .HasColumnType("tinyint(1)"); + + b.Property("FileName") + .HasColumnType("longtext"); + + b.Property("Forced") + .HasColumnType("tinyint(1)"); + + b.Property("Index") + .HasColumnType("int"); + + b.Property("Language") + .HasColumnType("longtext"); + + b.Property("MediaStreamKind") + .HasColumnType("int"); + + b.Property("MediaVersionId") + .HasColumnType("int"); + + b.Property("MimeType") + .HasColumnType("longtext"); + + b.Property("PixelFormat") + .HasColumnType("longtext"); + + b.Property("Profile") + .HasColumnType("longtext"); + + b.Property("Title") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("MediaVersionId"); + + b.ToTable("MediaStream", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("DateAdded") + .HasColumnType("datetime(6)"); + + b.Property("DateUpdated") + .HasColumnType("datetime(6)"); + + b.Property("DisplayAspectRatio") + .HasColumnType("longtext"); + + b.Property("Duration") + .HasColumnType("time(6)"); + + b.Property("EpisodeId") + .HasColumnType("int"); + + b.Property("Height") + .HasColumnType("int"); + + b.Property("ImageId") + .HasColumnType("int"); + + b.Property("InterlacedRatio") + .HasColumnType("double"); + + b.Property("MovieId") + .HasColumnType("int"); + + b.Property("MusicVideoId") + .HasColumnType("int"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("OtherVideoId") + .HasColumnType("int"); + + b.Property("RFrameRate") + .HasColumnType("longtext"); + + b.Property("RemoteStreamId") + .HasColumnType("int"); + + b.Property("SampleAspectRatio") + .HasColumnType("longtext"); + + b.Property("SongId") + .HasColumnType("int"); + + b.Property("VideoScanKind") + .HasColumnType("int"); + + b.Property("Width") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("EpisodeId"); + + b.HasIndex("ImageId"); + + b.HasIndex("MovieId"); + + b.HasIndex("MusicVideoId"); + + b.HasIndex("OtherVideoId"); + + b.HasIndex("RemoteStreamId"); + + b.HasIndex("SongId"); + + b.ToTable("MediaVersion", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MetadataGuid", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ArtistMetadataId") + .HasColumnType("int"); + + b.Property("EpisodeMetadataId") + .HasColumnType("int"); + + b.Property("Guid") + .HasMaxLength(128) + .HasColumnType("varchar(128)") + .UseCollation("utf8mb4_general_ci"); + + b.Property("ImageMetadataId") + .HasColumnType("int"); + + b.Property("MovieMetadataId") + .HasColumnType("int"); + + b.Property("MusicVideoMetadataId") + .HasColumnType("int"); + + b.Property("OtherVideoMetadataId") + .HasColumnType("int"); + + b.Property("RemoteStreamMetadataId") + .HasColumnType("int"); + + b.Property("SeasonMetadataId") + .HasColumnType("int"); + + b.Property("ShowMetadataId") + .HasColumnType("int"); + + b.Property("SongMetadataId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ArtistMetadataId"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("Guid"); + + b.HasIndex("ImageMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("OtherVideoMetadataId"); + + b.HasIndex("RemoteStreamMetadataId"); + + 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("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ArtistMetadataId") + .HasColumnType("int"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("ArtistMetadataId"); + + b.ToTable("Mood"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MovieMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ContentRating") + .HasColumnType("longtext"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)"); + + b.Property("DateUpdated") + .HasColumnType("datetime(6)"); + + b.Property("MetadataKind") + .HasColumnType("int"); + + b.Property("MovieId") + .HasColumnType("int"); + + b.Property("OriginalTitle") + .HasColumnType("longtext"); + + b.Property("Outline") + .HasColumnType("longtext"); + + b.Property("Plot") + .HasColumnType("longtext"); + + b.Property("ReleaseDate") + .HasColumnType("datetime(6)"); + + b.Property("SortTitle") + .HasColumnType("longtext"); + + b.Property("Tagline") + .HasColumnType("longtext"); + + b.Property("Title") + .HasColumnType("varchar(255)") + .UseCollation("utf8mb4_general_ci"); + + b.Property("Year") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("MovieId"); + + b.HasIndex("Title"); + + b.ToTable("MovieMetadata", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MultiCollection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .UseCollation("utf8mb4_general_ci"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("MultiCollection", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MultiCollectionItem", b => + { + b.Property("MultiCollectionId") + .HasColumnType("int"); + + b.Property("CollectionId") + .HasColumnType("int"); + + b.Property("PlaybackOrder") + .HasColumnType("int"); + + b.Property("ScheduleAsGroup") + .HasColumnType("tinyint(1)"); + + b.Property("Weight") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(1); + + b.HasKey("MultiCollectionId", "CollectionId"); + + b.HasIndex("CollectionId"); + + b.ToTable("MultiCollectionItem", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MultiCollectionSmartItem", b => + { + b.Property("MultiCollectionId") + .HasColumnType("int"); + + b.Property("SmartCollectionId") + .HasColumnType("int"); + + b.Property("PlaybackOrder") + .HasColumnType("int"); + + b.Property("ScheduleAsGroup") + .HasColumnType("tinyint(1)"); + + b.Property("Weight") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(1); + + b.HasKey("MultiCollectionId", "SmartCollectionId"); + + b.HasIndex("SmartCollectionId"); + + b.ToTable("MultiCollectionSmartItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideoArtist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("MusicVideoMetadataId") + .HasColumnType("int"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("MusicVideoMetadataId"); + + b.ToTable("MusicVideoArtist"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideoMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Album") + .HasColumnType("longtext"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)"); + + b.Property("DateUpdated") + .HasColumnType("datetime(6)"); + + b.Property("MetadataKind") + .HasColumnType("int"); + + b.Property("MusicVideoId") + .HasColumnType("int"); + + b.Property("OriginalTitle") + .HasColumnType("longtext"); + + b.Property("Plot") + .HasColumnType("longtext"); + + b.Property("ReleaseDate") + .HasColumnType("datetime(6)"); + + b.Property("SortTitle") + .HasColumnType("longtext"); + + b.Property("Title") + .HasColumnType("longtext"); + + b.Property("Track") + .HasColumnType("int"); + + b.Property("Year") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("MusicVideoId"); + + b.ToTable("MusicVideoMetadata", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.OtherVideoMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ContentRating") + .HasColumnType("longtext"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)"); + + b.Property("DateUpdated") + .HasColumnType("datetime(6)"); + + b.Property("MetadataKind") + .HasColumnType("int"); + + b.Property("OriginalTitle") + .HasColumnType("longtext"); + + b.Property("OtherVideoId") + .HasColumnType("int"); + + b.Property("Outline") + .HasColumnType("longtext"); + + b.Property("Plot") + .HasColumnType("longtext"); + + b.Property("ReleaseDate") + .HasColumnType("datetime(6)"); + + b.Property("SortTitle") + .HasColumnType("longtext"); + + b.Property("Tagline") + .HasColumnType("longtext"); + + b.Property("Title") + .HasColumnType("longtext"); + + b.Property("Year") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("OtherVideoId"); + + b.ToTable("OtherVideoMetadata", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Playlist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .UseCollation("utf8mb4_general_ci"); + + b.Property("PlaylistGroupId") + .HasColumnType("int"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("PlaylistGroupId", "Name") + .IsUnique(); + + b.ToTable("Playlist", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlaylistGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("PlaylistGroup", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlaylistItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CollectionId") + .HasColumnType("int"); + + b.Property("CollectionType") + .HasColumnType("int"); + + b.Property("Count") + .HasColumnType("int"); + + b.Property("IncludeInProgramGuide") + .HasColumnType("tinyint(1)"); + + b.Property("Index") + .HasColumnType("int"); + + b.Property("MediaItemId") + .HasColumnType("int"); + + b.Property("MultiCollectionId") + .HasColumnType("int"); + + b.Property("PlayAll") + .HasColumnType("tinyint(1)"); + + b.Property("PlaybackOrder") + .HasColumnType("int"); + + b.Property("PlaylistId") + .HasColumnType("int"); + + b.Property("SmartCollectionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CollectionId"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("MultiCollectionId"); + + b.HasIndex("PlaylistId"); + + b.HasIndex("SmartCollectionId"); + + b.ToTable("PlaylistItem", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ChannelId") + .HasColumnType("int"); + + b.Property("DailyRebuildTime") + .HasColumnType("time(6)"); + + b.Property("DecoId") + .HasColumnType("int"); + + b.Property("OnDemandCheckpoint") + .HasColumnType("datetime(6)"); + + b.Property("ProgramScheduleId") + .HasColumnType("int"); + + b.Property("ScheduleFile") + .HasColumnType("longtext"); + + b.Property("ScheduleKind") + .HasColumnType("int"); + + b.Property("Seed") + .HasColumnType("int"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ChannelId"); + + b.HasIndex("DecoId"); + + b.HasIndex("ProgramScheduleId"); + + b.ToTable("Playout", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutBuildStatus", b => + { + b.Property("PlayoutId") + .HasColumnType("int"); + + b.Property("LastBuild") + .HasColumnType("datetime(6)"); + + b.Property("Message") + .HasColumnType("longtext"); + + b.Property("Success") + .HasColumnType("tinyint(1)"); + + b.HasKey("PlayoutId"); + + b.ToTable("PlayoutBuildStatus", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutGap", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Finish") + .HasColumnType("datetime(6)"); + + b.Property("PlayoutId") + .HasColumnType("int"); + + b.Property("Start") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("PlayoutId"); + + b.HasIndex("Start", "Finish") + .HasDatabaseName("IX_PlayoutGap_Start_Finish"); + + b.ToTable("PlayoutGap", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BlockKey") + .HasColumnType("longtext"); + + b.Property("ChapterTitle") + .HasColumnType("longtext"); + + b.Property("CollectionEtag") + .HasColumnType("longtext"); + + b.Property("CollectionKey") + .HasColumnType("longtext"); + + b.Property("CustomTitle") + .HasColumnType("longtext"); + + b.Property("DisableWatermarks") + .HasColumnType("tinyint(1)"); + + b.Property("FillerKind") + .HasColumnType("int"); + + b.Property("Finish") + .HasColumnType("datetime(6)"); + + b.Property("GuideFinish") + .HasColumnType("datetime(6)"); + + b.Property("GuideGroup") + .HasColumnType("int"); + + b.Property("GuideStart") + .HasColumnType("datetime(6)"); + + b.Property("InPoint") + .HasColumnType("time(6)"); + + b.Property("MediaItemId") + .HasColumnType("int"); + + b.Property("OutPoint") + .HasColumnType("time(6)"); + + b.Property("PlayoutId") + .HasColumnType("int"); + + b.Property("PreferredAudioLanguageCode") + .HasColumnType("longtext"); + + b.Property("PreferredAudioTitle") + .HasColumnType("longtext"); + + b.Property("PreferredSubtitleLanguageCode") + .HasColumnType("longtext"); + + b.Property("SchedulingContext") + .HasColumnType("longtext"); + + b.Property("Start") + .HasColumnType("datetime(6)"); + + b.Property("SubtitleMode") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("PlayoutId"); + + b.HasIndex("Start", "Finish") + .HasDatabaseName("IX_PlayoutItem_Start_Finish"); + + b.ToTable("PlayoutItem", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItemGraphicsElement", b => + { + b.Property("PlayoutItemId") + .HasColumnType("int"); + + b.Property("GraphicsElementId") + .HasColumnType("int"); + + b.Property("Variables") + .HasColumnType("longtext"); + + b.HasKey("PlayoutItemId", "GraphicsElementId"); + + b.HasIndex("GraphicsElementId"); + + b.ToTable("PlayoutItemGraphicsElement"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItemWatermark", b => + { + b.Property("PlayoutItemId") + .HasColumnType("int"); + + b.Property("WatermarkId") + .HasColumnType("int"); + + b.HasKey("PlayoutItemId", "WatermarkId"); + + b.HasIndex("WatermarkId"); + + b.ToTable("PlayoutItemWatermark"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutProgramScheduleAnchor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AnchorDate") + .HasColumnType("datetime(6)"); + + b.Property("CollectionId") + .HasColumnType("int"); + + b.Property("CollectionType") + .HasColumnType("int"); + + b.Property("FakeCollectionKey") + .HasColumnType("longtext"); + + b.Property("MediaItemId") + .HasColumnType("int"); + + b.Property("MultiCollectionId") + .HasColumnType("int"); + + b.Property("PlaylistId") + .HasColumnType("int"); + + b.Property("PlayoutId") + .HasColumnType("int"); + + b.Property("RerunCollectionId") + .HasColumnType("int"); + + b.Property("SearchQuery") + .HasColumnType("longtext"); + + b.Property("SmartCollectionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CollectionId"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("MultiCollectionId"); + + b.HasIndex("PlaylistId"); + + b.HasIndex("PlayoutId"); + + b.HasIndex("RerunCollectionId"); + + b.HasIndex("SmartCollectionId"); + + b.ToTable("PlayoutProgramScheduleAnchor", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutScheduleItemFillGroupIndex", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("PlayoutId") + .HasColumnType("int"); + + b.Property("ProgramScheduleItemId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("PlayoutId"); + + b.HasIndex("ProgramScheduleItemId"); + + b.ToTable("PlayoutScheduleItemFillGroupIndex", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexCollection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Etag") + .HasColumnType("longtext"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("PlexCollection", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("PlexMediaSourceId") + .HasColumnType("int"); + + b.Property("Uri") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("PlexMediaSourceId"); + + b.ToTable("PlexConnection", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexPathReplacement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("LocalPath") + .HasColumnType("longtext"); + + b.Property("PlexMediaSourceId") + .HasColumnType("int"); + + b.Property("PlexPath") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("PlexMediaSourceId"); + + b.ToTable("PlexPathReplacement", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("FixedStartTimeBehavior") + .HasColumnType("int"); + + b.Property("KeepMultiPartEpisodesTogether") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .UseCollation("utf8mb4_general_ci"); + + b.Property("RandomStartPoint") + .HasColumnType("tinyint(1)"); + + b.Property("ShuffleScheduleItems") + .HasColumnType("tinyint(1)"); + + b.Property("TreatCollectionsAsShows") + .HasColumnType("tinyint(1)"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("ProgramSchedule", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleAlternate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("DaysOfMonth") + .HasColumnType("longtext"); + + b.Property("DaysOfWeek") + .HasColumnType("longtext"); + + b.Property("EndDay") + .HasColumnType("int"); + + b.Property("EndMonth") + .HasColumnType("int"); + + b.Property("EndYear") + .HasColumnType("int"); + + b.Property("Index") + .HasColumnType("int"); + + b.Property("LimitToDateRange") + .HasColumnType("tinyint(1)"); + + b.Property("MonthsOfYear") + .HasColumnType("longtext"); + + b.Property("PlayoutId") + .HasColumnType("int"); + + b.Property("ProgramScheduleId") + .HasColumnType("int"); + + b.Property("StartDay") + .HasColumnType("int"); + + b.Property("StartMonth") + .HasColumnType("int"); + + b.Property("StartYear") + .HasColumnType("int"); + + 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("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CollectionId") + .HasColumnType("int"); + + b.Property("CollectionType") + .HasColumnType("int"); + + b.Property("CustomTitle") + .HasColumnType("longtext"); + + b.Property("FakeCollectionKey") + .HasColumnType("longtext"); + + b.Property("FallbackFillerId") + .HasColumnType("int"); + + b.Property("FillWithGroupMode") + .HasColumnType("int"); + + b.Property("FixedStartTimeBehavior") + .HasColumnType("int"); + + b.Property("GuideMode") + .HasColumnType("int"); + + b.Property("Index") + .HasColumnType("int"); + + b.Property("MarathonBatchSize") + .HasColumnType("int"); + + b.Property("MarathonGroupBy") + .HasColumnType("int"); + + b.Property("MarathonShuffleGroups") + .HasColumnType("tinyint(1)"); + + b.Property("MarathonShuffleItems") + .HasColumnType("tinyint(1)"); + + b.Property("MediaItemId") + .HasColumnType("int"); + + b.Property("MidRollFillerId") + .HasColumnType("int"); + + b.Property("MultiCollectionId") + .HasColumnType("int"); + + b.Property("PlaybackOrder") + .HasColumnType("int"); + + b.Property("PlaylistId") + .HasColumnType("int"); + + b.Property("PostRollFillerId") + .HasColumnType("int"); + + b.Property("PreRollFillerId") + .HasColumnType("int"); + + b.Property("PreferredAudioLanguageCode") + .HasColumnType("longtext"); + + b.Property("PreferredAudioTitle") + .HasColumnType("longtext"); + + b.Property("PreferredSubtitleLanguageCode") + .HasColumnType("longtext"); + + b.Property("ProgramScheduleId") + .HasColumnType("int"); + + b.Property("RerunCollectionId") + .HasColumnType("int"); + + b.Property("SearchQuery") + .HasColumnType("longtext"); + + b.Property("SearchTitle") + .HasColumnType("longtext"); + + b.Property("SmartCollectionId") + .HasColumnType("int"); + + b.Property("StartTime") + .HasColumnType("time(6)"); + + b.Property("SubtitleMode") + .HasColumnType("int"); + + b.Property("TailFillerId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CollectionId"); + + b.HasIndex("FallbackFillerId"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("MidRollFillerId"); + + b.HasIndex("MultiCollectionId"); + + b.HasIndex("PlaylistId"); + + b.HasIndex("PostRollFillerId"); + + b.HasIndex("PreRollFillerId"); + + b.HasIndex("ProgramScheduleId"); + + b.HasIndex("RerunCollectionId"); + + b.HasIndex("SmartCollectionId"); + + b.HasIndex("TailFillerId"); + + b.ToTable("ProgramScheduleItem", (string)null); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemGraphicsElement", b => + { + b.Property("ProgramScheduleItemId") + .HasColumnType("int"); + + b.Property("GraphicsElementId") + .HasColumnType("int"); + + b.HasKey("ProgramScheduleItemId", "GraphicsElementId"); + + b.HasIndex("GraphicsElementId"); + + b.ToTable("ProgramScheduleItemGraphicsElement"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemWatermark", b => + { + b.Property("ProgramScheduleItemId") + .HasColumnType("int"); + + b.Property("WatermarkId") + .HasColumnType("int"); + + b.HasKey("ProgramScheduleItemId", "WatermarkId"); + + b.HasIndex("WatermarkId"); + + b.ToTable("ProgramScheduleItemWatermark"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.RemoteStreamMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ContentRating") + .HasColumnType("longtext"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)"); + + b.Property("DateUpdated") + .HasColumnType("datetime(6)"); + + b.Property("MetadataKind") + .HasColumnType("int"); + + b.Property("OriginalTitle") + .HasColumnType("longtext"); + + b.Property("Plot") + .HasColumnType("longtext"); + + b.Property("ReleaseDate") + .HasColumnType("datetime(6)"); + + b.Property("RemoteStreamId") + .HasColumnType("int"); + + b.Property("SortTitle") + .HasColumnType("longtext"); + + b.Property("Title") + .HasColumnType("longtext"); + + b.Property("Year") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("RemoteStreamId"); + + b.ToTable("RemoteStreamMetadata", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.RerunCollection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CollectionId") + .HasColumnType("int"); + + b.Property("CollectionType") + .HasColumnType("int"); + + b.Property("FirstRunPlaybackOrder") + .HasColumnType("int"); + + b.Property("MediaItemId") + .HasColumnType("int"); + + b.Property("MultiCollectionId") + .HasColumnType("int"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .UseCollation("utf8mb4_general_ci"); + + b.Property("RerunPlaybackOrder") + .HasColumnType("int"); + + b.Property("SmartCollectionId") + .HasColumnType("int"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CollectionId"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("MultiCollectionId"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("SmartCollectionId"); + + b.ToTable("RerunCollection", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Resolution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Height") + .HasColumnType("int"); + + b.Property("IsCustom") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Width") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Resolution", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.Block", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BlockGroupId") + .HasColumnType("int"); + + b.Property("DateUpdated") + .HasColumnType("datetime(6)"); + + b.Property("Minutes") + .HasColumnType("int"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .UseCollation("utf8mb4_general_ci"); + + b.Property("StopScheduling") + .HasColumnType("int"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BlockGroupId", "Name") + .IsUnique(); + + b.ToTable("Block", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.BlockGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .UseCollation("utf8mb4_general_ci"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("BlockGroup", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.BlockItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BlockId") + .HasColumnType("int"); + + b.Property("CollectionId") + .HasColumnType("int"); + + b.Property("CollectionType") + .HasColumnType("int"); + + b.Property("DisableWatermarks") + .HasColumnType("tinyint(1)"); + + b.Property("IncludeInProgramGuide") + .HasColumnType("tinyint(1)"); + + b.Property("Index") + .HasColumnType("int"); + + b.Property("MediaItemId") + .HasColumnType("int"); + + b.Property("MultiCollectionId") + .HasColumnType("int"); + + b.Property("PlaybackOrder") + .HasColumnType("int"); + + b.Property("SearchQuery") + .HasColumnType("longtext"); + + b.Property("SearchTitle") + .HasColumnType("longtext"); + + b.Property("SmartCollectionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BlockId"); + + b.HasIndex("CollectionId"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("MultiCollectionId"); + + b.HasIndex("SmartCollectionId"); + + b.ToTable("BlockItem", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.Deco", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BreakContentMode") + .HasColumnType("int"); + + b.Property("DeadAirFallbackCollectionId") + .HasColumnType("int"); + + b.Property("DeadAirFallbackCollectionType") + .HasColumnType("int"); + + b.Property("DeadAirFallbackMediaItemId") + .HasColumnType("int"); + + b.Property("DeadAirFallbackMode") + .HasColumnType("int"); + + b.Property("DeadAirFallbackMultiCollectionId") + .HasColumnType("int"); + + b.Property("DeadAirFallbackSmartCollectionId") + .HasColumnType("int"); + + b.Property("DecoGroupId") + .HasColumnType("int"); + + b.Property("DefaultFillerCollectionId") + .HasColumnType("int"); + + b.Property("DefaultFillerCollectionType") + .HasColumnType("int"); + + b.Property("DefaultFillerMediaItemId") + .HasColumnType("int"); + + b.Property("DefaultFillerMode") + .HasColumnType("int"); + + b.Property("DefaultFillerMultiCollectionId") + .HasColumnType("int"); + + b.Property("DefaultFillerSmartCollectionId") + .HasColumnType("int"); + + b.Property("DefaultFillerTrimToFit") + .HasColumnType("tinyint(1)"); + + b.Property("GraphicsElementsMode") + .HasColumnType("int"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .UseCollation("utf8mb4_general_ci"); + + b.Property("UseGraphicsElementsDuringFiller") + .HasColumnType("tinyint(1)"); + + b.Property("UseWatermarkDuringFiller") + .HasColumnType("tinyint(1)"); + + b.Property("WatermarkMode") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DeadAirFallbackCollectionId"); + + b.HasIndex("DeadAirFallbackMediaItemId"); + + b.HasIndex("DeadAirFallbackMultiCollectionId"); + + b.HasIndex("DeadAirFallbackSmartCollectionId"); + + b.HasIndex("DefaultFillerCollectionId"); + + b.HasIndex("DefaultFillerMediaItemId"); + + b.HasIndex("DefaultFillerMultiCollectionId"); + + b.HasIndex("DefaultFillerSmartCollectionId"); + + b.HasIndex("DecoGroupId", "Name") + .IsUnique(); + + b.ToTable("Deco", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoBreakContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CollectionId") + .HasColumnType("int"); + + b.Property("CollectionType") + .HasColumnType("int"); + + b.Property("DecoId") + .HasColumnType("int"); + + b.Property("MediaItemId") + .HasColumnType("int"); + + b.Property("MultiCollectionId") + .HasColumnType("int"); + + b.Property("Placement") + .HasColumnType("int"); + + b.Property("PlaylistId") + .HasColumnType("int"); + + b.Property("SmartCollectionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CollectionId"); + + b.HasIndex("DecoId"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("MultiCollectionId"); + + b.HasIndex("PlaylistId"); + + b.HasIndex("SmartCollectionId"); + + b.ToTable("DecoBreakContent", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .UseCollation("utf8mb4_general_ci"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("DecoGroup", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("DateUpdated") + .HasColumnType("datetime(6)"); + + b.Property("DecoTemplateGroupId") + .HasColumnType("int"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .UseCollation("utf8mb4_general_ci"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DecoTemplateGroupId", "Name") + .IsUnique(); + + b.ToTable("DecoTemplate", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoTemplateGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .UseCollation("utf8mb4_general_ci"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("DecoTemplateGroup", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoTemplateItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("DecoId") + .HasColumnType("int"); + + b.Property("DecoTemplateId") + .HasColumnType("int"); + + b.Property("EndTime") + .HasColumnType("time(6)"); + + b.Property("StartTime") + .HasColumnType("time(6)"); + + b.HasKey("Id"); + + b.HasIndex("DecoId"); + + b.HasIndex("DecoTemplateId"); + + b.ToTable("DecoTemplateItem", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.PlayoutHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BlockId") + .HasColumnType("int"); + + b.Property("ChildKey") + .HasColumnType("longtext"); + + b.Property("Details") + .HasColumnType("longtext"); + + b.Property("Finish") + .HasColumnType("datetime(6)"); + + b.Property("Index") + .HasColumnType("int"); + + b.Property("IsCurrentChild") + .HasColumnType("tinyint(1)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("PlaybackOrder") + .HasColumnType("int"); + + b.Property("PlayoutId") + .HasColumnType("int"); + + b.Property("When") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("BlockId"); + + b.HasIndex("PlayoutId"); + + b.ToTable("PlayoutHistory", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.PlayoutTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("DateUpdated") + .HasColumnType("datetime(6)"); + + b.Property("DaysOfMonth") + .HasColumnType("longtext"); + + b.Property("DaysOfWeek") + .HasColumnType("longtext"); + + b.Property("DecoTemplateId") + .HasColumnType("int"); + + b.Property("EndDay") + .HasColumnType("int"); + + b.Property("EndMonth") + .HasColumnType("int"); + + b.Property("EndYear") + .HasColumnType("int"); + + b.Property("Index") + .HasColumnType("int"); + + b.Property("LimitToDateRange") + .HasColumnType("tinyint(1)"); + + b.Property("MonthsOfYear") + .HasColumnType("longtext"); + + b.Property("PlayoutId") + .HasColumnType("int"); + + b.Property("StartDay") + .HasColumnType("int"); + + b.Property("StartMonth") + .HasColumnType("int"); + + b.Property("StartYear") + .HasColumnType("int"); + + b.Property("TemplateId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("DecoTemplateId"); + + b.HasIndex("PlayoutId"); + + b.HasIndex("TemplateId"); + + b.ToTable("PlayoutTemplate", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.RerunHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("MediaItemId") + .HasColumnType("int"); + + b.Property("PlayoutId") + .HasColumnType("int"); + + b.Property("RerunCollectionId") + .HasColumnType("int"); + + b.Property("When") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("PlayoutId"); + + b.HasIndex("RerunCollectionId"); + + b.ToTable("RerunHistory", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.Template", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("DateUpdated") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .UseCollation("utf8mb4_general_ci"); + + b.Property("TemplateGroupId") + .HasColumnType("int"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("TemplateGroupId", "Name") + .IsUnique(); + + b.ToTable("Template", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.TemplateGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .UseCollation("utf8mb4_general_ci"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("TemplateGroup", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.TemplateItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("BlockId") + .HasColumnType("int"); + + b.Property("StartTime") + .HasColumnType("time(6)"); + + b.Property("TemplateId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BlockId"); + + b.HasIndex("TemplateId"); + + b.ToTable("TemplateItem", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.SeasonMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("DateAdded") + .HasColumnType("datetime(6)"); + + b.Property("DateUpdated") + .HasColumnType("datetime(6)"); + + b.Property("MetadataKind") + .HasColumnType("int"); + + b.Property("OriginalTitle") + .HasColumnType("longtext"); + + b.Property("Outline") + .HasColumnType("longtext"); + + b.Property("ReleaseDate") + .HasColumnType("datetime(6)"); + + b.Property("SeasonId") + .HasColumnType("int"); + + b.Property("SortTitle") + .HasColumnType("longtext"); + + b.Property("Title") + .HasColumnType("longtext"); + + b.Property("Year") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId"); + + b.ToTable("SeasonMetadata", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ShowMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ContentRating") + .HasColumnType("longtext"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)"); + + b.Property("DateUpdated") + .HasColumnType("datetime(6)"); + + b.Property("MetadataKind") + .HasColumnType("int"); + + b.Property("OriginalTitle") + .HasColumnType("longtext"); + + b.Property("Outline") + .HasColumnType("longtext"); + + b.Property("Plot") + .HasColumnType("longtext"); + + b.Property("ReleaseDate") + .HasColumnType("datetime(6)"); + + b.Property("ShowId") + .HasColumnType("int"); + + b.Property("SortTitle") + .HasColumnType("longtext"); + + b.Property("Tagline") + .HasColumnType("longtext"); + + b.Property("Title") + .HasColumnType("varchar(255)") + .UseCollation("utf8mb4_general_ci"); + + b.Property("Year") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ShowId"); + + b.HasIndex("Title"); + + b.ToTable("ShowMetadata", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.SmartCollection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .UseCollation("utf8mb4_general_ci"); + + b.Property("Query") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("SmartCollection", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.SongMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Album") + .HasColumnType("longtext"); + + b.PrimitiveCollection("AlbumArtists") + .HasColumnType("longtext"); + + b.PrimitiveCollection("Artists") + .HasColumnType("longtext"); + + b.Property("Comment") + .HasColumnType("longtext"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)"); + + b.Property("DateUpdated") + .HasColumnType("datetime(6)"); + + b.Property("MetadataKind") + .HasColumnType("int"); + + b.Property("OriginalTitle") + .HasColumnType("longtext"); + + b.Property("ReleaseDate") + .HasColumnType("datetime(6)"); + + b.Property("SongId") + .HasColumnType("int"); + + b.Property("SortTitle") + .HasColumnType("longtext"); + + b.Property("Title") + .HasColumnType("longtext"); + + b.Property("Track") + .HasColumnType("longtext"); + + b.Property("Year") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SongId"); + + b.ToTable("SongMetadata", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Studio", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ArtistMetadataId") + .HasColumnType("int"); + + b.Property("EpisodeMetadataId") + .HasColumnType("int"); + + b.Property("ImageMetadataId") + .HasColumnType("int"); + + b.Property("MovieMetadataId") + .HasColumnType("int"); + + b.Property("MusicVideoMetadataId") + .HasColumnType("int"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("OtherVideoMetadataId") + .HasColumnType("int"); + + b.Property("RemoteStreamMetadataId") + .HasColumnType("int"); + + b.Property("SeasonMetadataId") + .HasColumnType("int"); + + b.Property("ShowMetadataId") + .HasColumnType("int"); + + b.Property("SongMetadataId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ArtistMetadataId"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("ImageMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("OtherVideoMetadataId"); + + b.HasIndex("RemoteStreamMetadataId"); + + 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("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ArtistMetadataId") + .HasColumnType("int"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("ArtistMetadataId"); + + b.ToTable("Style"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Subtitle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ArtistMetadataId") + .HasColumnType("int"); + + b.Property("Codec") + .HasColumnType("longtext"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)"); + + b.Property("DateUpdated") + .HasColumnType("datetime(6)"); + + b.Property("Default") + .HasColumnType("tinyint(1)"); + + b.Property("EpisodeMetadataId") + .HasColumnType("int"); + + b.Property("Forced") + .HasColumnType("tinyint(1)"); + + b.Property("ImageMetadataId") + .HasColumnType("int"); + + b.Property("IsExtracted") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false); + + b.Property("Language") + .HasColumnType("longtext"); + + b.Property("MovieMetadataId") + .HasColumnType("int"); + + b.Property("MusicVideoMetadataId") + .HasColumnType("int"); + + b.Property("OtherVideoMetadataId") + .HasColumnType("int"); + + b.Property("Path") + .HasColumnType("longtext"); + + b.Property("RemoteStreamMetadataId") + .HasColumnType("int"); + + b.Property("SDH") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false); + + b.Property("SeasonMetadataId") + .HasColumnType("int"); + + b.Property("ShowMetadataId") + .HasColumnType("int"); + + b.Property("SongMetadataId") + .HasColumnType("int"); + + b.Property("StreamIndex") + .HasColumnType("int"); + + b.Property("SubtitleKind") + .HasColumnType("int"); + + b.Property("Title") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("ArtistMetadataId"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("ImageMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("OtherVideoMetadataId"); + + b.HasIndex("RemoteStreamMetadataId"); + + 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("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ArtistMetadataId") + .HasColumnType("int"); + + b.Property("EpisodeMetadataId") + .HasColumnType("int"); + + b.Property("ExternalCollectionId") + .HasColumnType("longtext"); + + b.Property("ExternalTypeId") + .HasColumnType("longtext"); + + b.Property("ImageMetadataId") + .HasColumnType("int"); + + b.Property("MovieMetadataId") + .HasColumnType("int"); + + b.Property("MusicVideoMetadataId") + .HasColumnType("int"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("OtherVideoMetadataId") + .HasColumnType("int"); + + b.Property("RemoteStreamMetadataId") + .HasColumnType("int"); + + b.Property("SeasonMetadataId") + .HasColumnType("int"); + + b.Property("ShowMetadataId") + .HasColumnType("int"); + + b.Property("SongMetadataId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ArtistMetadataId"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("ImageMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("OtherVideoMetadataId"); + + b.HasIndex("RemoteStreamMetadataId"); + + b.HasIndex("SeasonMetadataId"); + + b.HasIndex("ShowMetadataId"); + + b.HasIndex("SongMetadataId"); + + b.ToTable("Tag"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TraktList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AutoRefresh") + .HasColumnType("tinyint(1)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("GeneratePlaylist") + .HasColumnType("tinyint(1)"); + + b.Property("ItemCount") + .HasColumnType("int"); + + b.Property("LastMatch") + .HasColumnType("datetime(6)"); + + b.Property("LastUpdate") + .HasColumnType("datetime(6)"); + + b.Property("List") + .HasColumnType("longtext"); + + b.Property("Name") + .HasColumnType("varchar(255)") + .UseCollation("utf8mb4_general_ci"); + + b.Property("PlaylistId") + .HasColumnType("int"); + + b.Property("TraktId") + .HasColumnType("int"); + + b.Property("User") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.HasIndex("PlaylistId"); + + b.ToTable("TraktList", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TraktListItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Episode") + .HasColumnType("int"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("MediaItemId") + .HasColumnType("int"); + + b.Property("Rank") + .HasColumnType("int"); + + b.Property("Season") + .HasColumnType("int"); + + b.Property("Title") + .HasColumnType("longtext"); + + b.Property("TraktId") + .HasColumnType("int"); + + b.Property("TraktListId") + .HasColumnType("int"); + + b.Property("Year") + .HasColumnType("int"); + + 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("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Guid") + .HasColumnType("longtext"); + + b.Property("TraktListItemId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("TraktListItemId"); + + b.ToTable("TraktListItemGuid", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Writer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("EpisodeMetadataId") + .HasColumnType("int"); + + b.Property("MovieMetadataId") + .HasColumnType("int"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("OtherVideoMetadataId") + .HasColumnType("int"); + + 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("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("EmbyLibraryId") + .HasColumnType("int"); + + b.Property("NetworkPath") + .HasColumnType("longtext"); + + b.Property("Path") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("EmbyLibraryId"); + + b.ToTable("EmbyPathInfo"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Jellyfin.JellyfinPathInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("JellyfinLibraryId") + .HasColumnType("int"); + + b.Property("NetworkPath") + .HasColumnType("longtext"); + + b.Property("Path") + .HasColumnType("longtext"); + + 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("longtext"); + + b.Property("ShouldSyncItems") + .HasColumnType("tinyint(1)"); + + b.ToTable("EmbyLibrary", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinLibrary", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Library"); + + b.Property("ItemId") + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("varchar(36)"); + + b.Property("ShouldSyncItems") + .HasColumnType("tinyint(1)"); + + b.HasIndex("ItemId"); + + 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("longtext"); + + b.Property("LastNetworksScan") + .HasColumnType("datetime(6)"); + + b.Property("ShouldSyncItems") + .HasColumnType("tinyint(1)"); + + b.ToTable("PlexLibrary", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaFile", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaFile"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("PlexId") + .HasColumnType("int"); + + 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("int"); + + b.HasIndex("SeasonId"); + + b.ToTable("Episode", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Image", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.ToTable("Image", (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("int"); + + 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.RemoteStream", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.Property("Duration") + .HasColumnType("time(6)"); + + b.Property("FallbackQuery") + .HasColumnType("longtext"); + + b.Property("IsLive") + .HasColumnType("tinyint(1)"); + + b.Property("Script") + .HasColumnType("longtext"); + + b.Property("Url") + .HasColumnType("longtext"); + + b.ToTable("RemoteStream", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Season", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.Property("SeasonNumber") + .HasColumnType("int"); + + b.Property("ShowId") + .HasColumnType("int"); + + 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("datetime(6)"); + + b.Property("OperatingSystem") + .HasColumnType("longtext"); + + b.Property("ServerName") + .HasColumnType("longtext"); + + b.ToTable("EmbyMediaSource", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinMediaSource", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaSource"); + + b.Property("LastCollectionsScan") + .HasColumnType("datetime(6)"); + + b.Property("OperatingSystem") + .HasColumnType("longtext"); + + b.Property("ServerName") + .HasColumnType("longtext"); + + 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("longtext"); + + b.Property("LastCollectionsScan") + .HasColumnType("datetime(6)"); + + b.Property("Platform") + .HasColumnType("longtext"); + + b.Property("PlatformVersion") + .HasColumnType("longtext"); + + b.Property("ProductVersion") + .HasColumnType("longtext"); + + b.Property("ServerName") + .HasColumnType("longtext"); + + b.ToTable("PlexMediaSource", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemDuration", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); + + b.Property("DiscardToFillAttempts") + .HasColumnType("int"); + + b.Property("PlayoutDuration") + .HasColumnType("time(6)"); + + b.Property("TailMode") + .HasColumnType("int"); + + 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("longtext"); + + b.Property("MultipleMode") + .HasColumnType("int"); + + 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("longtext"); + + b.Property("ItemId") + .HasColumnType("longtext"); + + b.ToTable("EmbyEpisode", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinEpisode", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Episode"); + + b.Property("Etag") + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("varchar(36)"); + + b.Property("ItemId") + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("varchar(36)"); + + b.HasIndex("ItemId"); + + b.ToTable("JellyfinEpisode", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexEpisode", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Episode"); + + b.Property("Etag") + .HasColumnType("longtext"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.ToTable("PlexEpisode", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyMovie", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Movie"); + + b.Property("Etag") + .HasColumnType("longtext"); + + b.Property("ItemId") + .HasColumnType("longtext"); + + b.ToTable("EmbyMovie", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinMovie", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Movie"); + + b.Property("Etag") + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("varchar(36)"); + + b.Property("ItemId") + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("varchar(36)"); + + b.HasIndex("ItemId"); + + b.ToTable("JellyfinMovie", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMovie", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Movie"); + + b.Property("Etag") + .HasColumnType("longtext"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.ToTable("PlexMovie", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexOtherVideo", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.OtherVideo"); + + b.Property("Etag") + .HasColumnType("longtext"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.ToTable("PlexOtherVideo", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbySeason", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Season"); + + b.Property("Etag") + .HasColumnType("longtext"); + + b.Property("ItemId") + .HasColumnType("longtext"); + + b.ToTable("EmbySeason", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinSeason", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Season"); + + b.Property("Etag") + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("varchar(36)"); + + b.Property("ItemId") + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("varchar(36)"); + + b.HasIndex("ItemId"); + + b.ToTable("JellyfinSeason", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexSeason", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Season"); + + b.Property("Etag") + .HasColumnType("longtext"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.ToTable("PlexSeason", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyShow", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Show"); + + b.Property("Etag") + .HasColumnType("longtext"); + + b.Property("ItemId") + .HasColumnType("longtext"); + + b.ToTable("EmbyShow", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinShow", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Show"); + + b.Property("Etag") + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("varchar(36)"); + + b.Property("ItemId") + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("varchar(36)"); + + b.HasIndex("ItemId"); + + b.ToTable("JellyfinShow", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexShow", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Show"); + + b.Property("Etag") + .HasColumnType("longtext"); + + b.Property("Key") + .HasColumnType("longtext"); + + 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.ImageMetadata", null) + .WithMany("Actors") + .HasForeignKey("ImageMetadataId") + .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.RemoteStreamMetadata", null) + .WithMany("Actors") + .HasForeignKey("RemoteStreamMetadataId") + .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.ImageMetadata", null) + .WithMany("Artwork") + .HasForeignKey("ImageMetadataId") + .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.RemoteStreamMetadata", null) + .WithMany("Artwork") + .HasForeignKey("RemoteStreamMetadataId") + .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.BlockItemGraphicsElement", b => + { + b.HasOne("ErsatzTV.Core.Domain.Scheduling.BlockItem", "BlockItem") + .WithMany("BlockItemGraphicsElements") + .HasForeignKey("BlockItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.GraphicsElement", "GraphicsElement") + .WithMany("BlockItemGraphicsElements") + .HasForeignKey("GraphicsElementId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BlockItem"); + + b.Navigation("GraphicsElement"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.BlockItemWatermark", b => + { + b.HasOne("ErsatzTV.Core.Domain.Scheduling.BlockItem", "BlockItem") + .WithMany("BlockItemWatermarks") + .HasForeignKey("BlockItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.ChannelWatermark", "Watermark") + .WithMany("BlockItemWatermarks") + .HasForeignKey("WatermarkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BlockItem"); + + b.Navigation("Watermark"); + }); + + 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.Channel", "MirrorSourceChannel") + .WithMany() + .HasForeignKey("MirrorSourceChannelId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("ErsatzTV.Core.Domain.ChannelWatermark", "Watermark") + .WithMany() + .HasForeignKey("WatermarkId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("FFmpegProfile"); + + b.Navigation("FallbackFiller"); + + b.Navigation("MirrorSourceChannel"); + + b.Navigation("Watermark"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ChannelTemplate", b => + { + b.HasOne("ErsatzTV.Core.Domain.FFmpegProfile", "FFmpegProfile") + .WithMany() + .HasForeignKey("FFmpegProfileId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.Filler.FillerPreset", "FallbackFiller") + .WithMany() + .HasForeignKey("FallbackFillerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("ErsatzTV.Core.Domain.Filler.FillerPreset", "MidRollFiller") + .WithMany() + .HasForeignKey("MidRollFillerId") + .OnDelete(DeleteBehavior.SetNull); + + 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.ChannelWatermark", "Watermark") + .WithMany() + .HasForeignKey("WatermarkId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("FFmpegProfile"); + + b.Navigation("FallbackFiller"); + + b.Navigation("MidRollFiller"); + + b.Navigation("PostRollFiller"); + + b.Navigation("PreRollFiller"); + + 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.DecoGraphicsElement", b => + { + b.HasOne("ErsatzTV.Core.Domain.Scheduling.Deco", "Deco") + .WithMany("DecoGraphicsElements") + .HasForeignKey("DecoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.GraphicsElement", "GraphicsElement") + .WithMany("DecoGraphicsElements") + .HasForeignKey("GraphicsElementId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Deco"); + + b.Navigation("GraphicsElement"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.DecoWatermark", b => + { + b.HasOne("ErsatzTV.Core.Domain.Scheduling.Deco", "Deco") + .WithMany("DecoWatermarks") + .HasForeignKey("DecoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.ChannelWatermark", "Watermark") + .WithMany("DecoWatermarks") + .HasForeignKey("WatermarkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Deco"); + + b.Navigation("Watermark"); + }); + + 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.Playlist", "Playlist") + .WithMany() + .HasForeignKey("PlaylistId") + .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("Playlist"); + + 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.ImageMetadata", null) + .WithMany("Genres") + .HasForeignKey("ImageMetadataId") + .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.RemoteStreamMetadata", null) + .WithMany("Genres") + .HasForeignKey("RemoteStreamMetadataId") + .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.ImageFolderDuration", b => + { + b.HasOne("ErsatzTV.Core.Domain.LibraryFolder", "LibraryFolder") + .WithOne("ImageFolderDuration") + .HasForeignKey("ErsatzTV.Core.Domain.ImageFolderDuration", "LibraryFolderId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("LibraryFolder"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ImageMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.Image", "Image") + .WithMany("ImageMetadata") + .HasForeignKey("ImageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Image"); + }); + + 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.HasOne("ErsatzTV.Core.Domain.LibraryFolder", "Parent") + .WithMany("Children") + .HasForeignKey("ParentId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("LibraryPath"); + + b.Navigation("Parent"); + }); + + 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.LibraryFolder", "LibraryFolder") + .WithMany("MediaFiles") + .HasForeignKey("LibraryFolderId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ErsatzTV.Core.Domain.MediaVersion", "MediaVersion") + .WithMany("MediaFiles") + .HasForeignKey("MediaVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("LibraryFolder"); + + 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.Image", null) + .WithMany("MediaVersions") + .HasForeignKey("ImageId") + .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.RemoteStream", null) + .WithMany("MediaVersions") + .HasForeignKey("RemoteStreamId") + .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.ImageMetadata", null) + .WithMany("Guids") + .HasForeignKey("ImageMetadataId") + .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.RemoteStreamMetadata", null) + .WithMany("Guids") + .HasForeignKey("RemoteStreamMetadataId") + .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.Playlist", b => + { + b.HasOne("ErsatzTV.Core.Domain.PlaylistGroup", "PlaylistGroup") + .WithMany("Playlists") + .HasForeignKey("PlaylistGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PlaylistGroup"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlaylistItem", 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.Playlist", "Playlist") + .WithMany("Items") + .HasForeignKey("PlaylistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.SmartCollection", "SmartCollection") + .WithMany() + .HasForeignKey("SmartCollectionId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Collection"); + + b.Navigation("MediaItem"); + + b.Navigation("MultiCollection"); + + b.Navigation("Playlist"); + + b.Navigation("SmartCollection"); + }); + + 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.Scheduling.Deco", "Deco") + .WithMany("Playouts") + .HasForeignKey("DecoId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule") + .WithMany("Playouts") + .HasForeignKey("ProgramScheduleId") + .OnDelete(DeleteBehavior.Cascade); + + b.OwnsOne("ErsatzTV.Core.Domain.PlayoutAnchor", "Anchor", b1 => + { + b1.Property("PlayoutId") + .HasColumnType("int"); + + b1.Property("Context") + .HasColumnType("longtext"); + + b1.Property("DurationFinish") + .HasColumnType("datetime(6)"); + + b1.Property("InDurationFiller") + .HasColumnType("tinyint(1)"); + + b1.Property("InFlood") + .HasColumnType("tinyint(1)"); + + b1.Property("MultipleRemaining") + .HasColumnType("int"); + + b1.Property("NextGuideGroup") + .HasColumnType("int"); + + b1.Property("NextInstructionIndex") + .HasColumnType("int"); + + b1.Property("NextStart") + .HasColumnType("datetime(6)"); + + b1.HasKey("PlayoutId"); + + b1.ToTable("PlayoutAnchor", (string)null); + + b1.WithOwner() + .HasForeignKey("PlayoutId"); + + b1.OwnsOne("ErsatzTV.Core.Domain.CollectionEnumeratorState", "ScheduleItemsEnumeratorState", b2 => + { + b2.Property("PlayoutAnchorPlayoutId") + .HasColumnType("int"); + + b2.Property("Index") + .HasColumnType("int"); + + b2.Property("Seed") + .HasColumnType("int"); + + b2.HasKey("PlayoutAnchorPlayoutId"); + + b2.ToTable("ScheduleItemsEnumeratorState", (string)null); + + b2.WithOwner() + .HasForeignKey("PlayoutAnchorPlayoutId"); + }); + + b1.Navigation("ScheduleItemsEnumeratorState"); + }); + + b.Navigation("Anchor"); + + b.Navigation("Channel"); + + b.Navigation("Deco"); + + b.Navigation("ProgramSchedule"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutBuildStatus", b => + { + b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout") + .WithOne("BuildStatus") + .HasForeignKey("ErsatzTV.Core.Domain.PlayoutBuildStatus", "PlayoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Playout"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutGap", b => + { + b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout") + .WithMany("Gaps") + .HasForeignKey("PlayoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Playout"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem") + .WithMany() + .HasForeignKey("MediaItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout") + .WithMany("Items") + .HasForeignKey("PlayoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MediaItem"); + + b.Navigation("Playout"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItemGraphicsElement", b => + { + b.HasOne("ErsatzTV.Core.Domain.GraphicsElement", "GraphicsElement") + .WithMany("PlayoutItemGraphicsElements") + .HasForeignKey("GraphicsElementId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.PlayoutItem", "PlayoutItem") + .WithMany("PlayoutItemGraphicsElements") + .HasForeignKey("PlayoutItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("GraphicsElement"); + + b.Navigation("PlayoutItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItemWatermark", b => + { + b.HasOne("ErsatzTV.Core.Domain.PlayoutItem", "PlayoutItem") + .WithMany("PlayoutItemWatermarks") + .HasForeignKey("PlayoutItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.ChannelWatermark", "Watermark") + .WithMany("PlayoutItemWatermarks") + .HasForeignKey("WatermarkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PlayoutItem"); + + 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.Playlist", "Playlist") + .WithMany() + .HasForeignKey("PlaylistId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout") + .WithMany("ProgramScheduleAnchors") + .HasForeignKey("PlayoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.RerunCollection", "RerunCollection") + .WithMany() + .HasForeignKey("RerunCollectionId") + .OnDelete(DeleteBehavior.Cascade); + + 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("int"); + + b1.Property("Index") + .HasColumnType("int"); + + b1.Property("Seed") + .HasColumnType("int"); + + 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("Playlist"); + + b.Navigation("Playout"); + + b.Navigation("RerunCollection"); + + b.Navigation("SmartCollection"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutScheduleItemFillGroupIndex", b => + { + b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout") + .WithMany("FillGroupIndices") + .HasForeignKey("PlayoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", "ProgramScheduleItem") + .WithMany() + .HasForeignKey("ProgramScheduleItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("ErsatzTV.Core.Domain.CollectionEnumeratorState", "EnumeratorState", b1 => + { + b1.Property("PlayoutScheduleItemFillGroupIndexId") + .HasColumnType("int"); + + b1.Property("Index") + .HasColumnType("int"); + + b1.Property("Seed") + .HasColumnType("int"); + + b1.HasKey("PlayoutScheduleItemFillGroupIndexId"); + + b1.ToTable("FillGroupEnumeratorState", (string)null); + + b1.WithOwner() + .HasForeignKey("PlayoutScheduleItemFillGroupIndexId"); + }); + + b.Navigation("EnumeratorState"); + + b.Navigation("Playout"); + + b.Navigation("ProgramScheduleItem"); + }); + + 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.Playlist", "Playlist") + .WithMany() + .HasForeignKey("PlaylistId") + .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); + + b.HasOne("ErsatzTV.Core.Domain.RerunCollection", "RerunCollection") + .WithMany() + .HasForeignKey("RerunCollectionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SmartCollection", "SmartCollection") + .WithMany() + .HasForeignKey("SmartCollectionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.Filler.FillerPreset", "TailFiller") + .WithMany() + .HasForeignKey("TailFillerId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Collection"); + + b.Navigation("FallbackFiller"); + + b.Navigation("MediaItem"); + + b.Navigation("MidRollFiller"); + + b.Navigation("MultiCollection"); + + b.Navigation("Playlist"); + + b.Navigation("PostRollFiller"); + + b.Navigation("PreRollFiller"); + + b.Navigation("ProgramSchedule"); + + b.Navigation("RerunCollection"); + + b.Navigation("SmartCollection"); + + b.Navigation("TailFiller"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemGraphicsElement", b => + { + b.HasOne("ErsatzTV.Core.Domain.GraphicsElement", "GraphicsElement") + .WithMany("ProgramScheduleItemGraphicsElements") + .HasForeignKey("GraphicsElementId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", "ProgramScheduleItem") + .WithMany("ProgramScheduleItemGraphicsElements") + .HasForeignKey("ProgramScheduleItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("GraphicsElement"); + + b.Navigation("ProgramScheduleItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemWatermark", b => + { + b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", "ProgramScheduleItem") + .WithMany("ProgramScheduleItemWatermarks") + .HasForeignKey("ProgramScheduleItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.ChannelWatermark", "Watermark") + .WithMany("ProgramScheduleItemWatermarks") + .HasForeignKey("WatermarkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ProgramScheduleItem"); + + b.Navigation("Watermark"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.RemoteStreamMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.RemoteStream", "RemoteStream") + .WithMany("RemoteStreamMetadata") + .HasForeignKey("RemoteStreamId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("RemoteStream"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.RerunCollection", 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.Scheduling.Block", b => + { + b.HasOne("ErsatzTV.Core.Domain.Scheduling.BlockGroup", "BlockGroup") + .WithMany("Blocks") + .HasForeignKey("BlockGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BlockGroup"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.BlockItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.Scheduling.Block", "Block") + .WithMany("Items") + .HasForeignKey("BlockId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + 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("Block"); + + b.Navigation("Collection"); + + b.Navigation("MediaItem"); + + b.Navigation("MultiCollection"); + + b.Navigation("SmartCollection"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.Deco", b => + { + b.HasOne("ErsatzTV.Core.Domain.Collection", "DeadAirFallbackCollection") + .WithMany() + .HasForeignKey("DeadAirFallbackCollectionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MediaItem", "DeadAirFallbackMediaItem") + .WithMany() + .HasForeignKey("DeadAirFallbackMediaItemId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MultiCollection", "DeadAirFallbackMultiCollection") + .WithMany() + .HasForeignKey("DeadAirFallbackMultiCollectionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SmartCollection", "DeadAirFallbackSmartCollection") + .WithMany() + .HasForeignKey("DeadAirFallbackSmartCollectionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.Scheduling.DecoGroup", "DecoGroup") + .WithMany("Decos") + .HasForeignKey("DecoGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.Collection", "DefaultFillerCollection") + .WithMany() + .HasForeignKey("DefaultFillerCollectionId"); + + b.HasOne("ErsatzTV.Core.Domain.MediaItem", "DefaultFillerMediaItem") + .WithMany() + .HasForeignKey("DefaultFillerMediaItemId"); + + b.HasOne("ErsatzTV.Core.Domain.MultiCollection", "DefaultFillerMultiCollection") + .WithMany() + .HasForeignKey("DefaultFillerMultiCollectionId"); + + b.HasOne("ErsatzTV.Core.Domain.SmartCollection", "DefaultFillerSmartCollection") + .WithMany() + .HasForeignKey("DefaultFillerSmartCollectionId"); + + b.Navigation("DeadAirFallbackCollection"); + + b.Navigation("DeadAirFallbackMediaItem"); + + b.Navigation("DeadAirFallbackMultiCollection"); + + b.Navigation("DeadAirFallbackSmartCollection"); + + b.Navigation("DecoGroup"); + + b.Navigation("DefaultFillerCollection"); + + b.Navigation("DefaultFillerMediaItem"); + + b.Navigation("DefaultFillerMultiCollection"); + + b.Navigation("DefaultFillerSmartCollection"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoBreakContent", b => + { + b.HasOne("ErsatzTV.Core.Domain.Collection", "Collection") + .WithMany() + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.Scheduling.Deco", "Deco") + .WithMany("BreakContent") + .HasForeignKey("DecoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + 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.Playlist", "Playlist") + .WithMany() + .HasForeignKey("PlaylistId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SmartCollection", "SmartCollection") + .WithMany() + .HasForeignKey("SmartCollectionId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Collection"); + + b.Navigation("Deco"); + + b.Navigation("MediaItem"); + + b.Navigation("MultiCollection"); + + b.Navigation("Playlist"); + + b.Navigation("SmartCollection"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoTemplate", b => + { + b.HasOne("ErsatzTV.Core.Domain.Scheduling.DecoTemplateGroup", "DecoTemplateGroup") + .WithMany("DecoTemplates") + .HasForeignKey("DecoTemplateGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DecoTemplateGroup"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoTemplateItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.Scheduling.Deco", "Deco") + .WithMany() + .HasForeignKey("DecoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.Scheduling.DecoTemplate", "DecoTemplate") + .WithMany("Items") + .HasForeignKey("DecoTemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Deco"); + + b.Navigation("DecoTemplate"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.PlayoutHistory", b => + { + b.HasOne("ErsatzTV.Core.Domain.Scheduling.Block", "Block") + .WithMany("PlayoutHistory") + .HasForeignKey("BlockId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout") + .WithMany("PlayoutHistory") + .HasForeignKey("PlayoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Block"); + + b.Navigation("Playout"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.PlayoutTemplate", b => + { + b.HasOne("ErsatzTV.Core.Domain.Scheduling.DecoTemplate", "DecoTemplate") + .WithMany("PlayoutTemplates") + .HasForeignKey("DecoTemplateId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout") + .WithMany("Templates") + .HasForeignKey("PlayoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.Scheduling.Template", "Template") + .WithMany("PlayoutTemplates") + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DecoTemplate"); + + b.Navigation("Playout"); + + b.Navigation("Template"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.RerunHistory", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem") + .WithMany() + .HasForeignKey("MediaItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout") + .WithMany() + .HasForeignKey("PlayoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.RerunCollection", "RerunCollection") + .WithMany() + .HasForeignKey("RerunCollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MediaItem"); + + b.Navigation("Playout"); + + b.Navigation("RerunCollection"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.Template", b => + { + b.HasOne("ErsatzTV.Core.Domain.Scheduling.TemplateGroup", "TemplateGroup") + .WithMany("Templates") + .HasForeignKey("TemplateGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TemplateGroup"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.TemplateItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.Scheduling.Block", "Block") + .WithMany("TemplateItems") + .HasForeignKey("BlockId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.Scheduling.Template", "Template") + .WithMany("Items") + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Block"); + + b.Navigation("Template"); + }); + + 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.ImageMetadata", null) + .WithMany("Studios") + .HasForeignKey("ImageMetadataId") + .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.RemoteStreamMetadata", null) + .WithMany("Studios") + .HasForeignKey("RemoteStreamMetadataId") + .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.ImageMetadata", null) + .WithMany("Subtitles") + .HasForeignKey("ImageMetadataId") + .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.RemoteStreamMetadata", null) + .WithMany("Subtitles") + .HasForeignKey("RemoteStreamMetadataId") + .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.ImageMetadata", null) + .WithMany("Tags") + .HasForeignKey("ImageMetadataId") + .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.RemoteStreamMetadata", null) + .WithMany("Tags") + .HasForeignKey("RemoteStreamMetadataId") + .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.TraktList", b => + { + b.HasOne("ErsatzTV.Core.Domain.Playlist", "Playlist") + .WithMany() + .HasForeignKey("PlaylistId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Playlist"); + }); + + 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.Image", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.Image", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Movie", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.Movie", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideo", b => + { + b.HasOne("ErsatzTV.Core.Domain.Artist", "Artist") + .WithMany("MusicVideos") + .HasForeignKey("ArtistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.MusicVideo", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Artist"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.OtherVideo", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.OtherVideo", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.RemoteStream", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.RemoteStream", "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.PlexOtherVideo", b => + { + b.HasOne("ErsatzTV.Core.Domain.OtherVideo", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.PlexOtherVideo", "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.ChannelWatermark", b => + { + b.Navigation("BlockItemWatermarks"); + + b.Navigation("DecoWatermarks"); + + b.Navigation("PlayoutItemWatermarks"); + + b.Navigation("ProgramScheduleItemWatermarks"); + }); + + 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.GraphicsElement", b => + { + b.Navigation("BlockItemGraphicsElements"); + + b.Navigation("DecoGraphicsElements"); + + b.Navigation("PlayoutItemGraphicsElements"); + + b.Navigation("ProgramScheduleItemGraphicsElements"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ImageMetadata", 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.Library", b => + { + b.Navigation("Paths"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LibraryFolder", b => + { + b.Navigation("Children"); + + b.Navigation("ImageFolderDuration"); + + b.Navigation("MediaFiles"); + }); + + 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.Playlist", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlaylistGroup", b => + { + b.Navigation("Playlists"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b => + { + b.Navigation("BuildStatus"); + + b.Navigation("FillGroupIndices"); + + b.Navigation("Gaps"); + + b.Navigation("Items"); + + b.Navigation("PlayoutHistory"); + + b.Navigation("ProgramScheduleAlternates"); + + b.Navigation("ProgramScheduleAnchors"); + + b.Navigation("Templates"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItem", b => + { + b.Navigation("PlayoutItemGraphicsElements"); + + b.Navigation("PlayoutItemWatermarks"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramSchedule", b => + { + b.Navigation("Items"); + + b.Navigation("Playouts"); + + b.Navigation("ProgramScheduleAlternates"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItem", b => + { + b.Navigation("ProgramScheduleItemGraphicsElements"); + + b.Navigation("ProgramScheduleItemWatermarks"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.RemoteStreamMetadata", 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.Scheduling.Block", b => + { + b.Navigation("Items"); + + b.Navigation("PlayoutHistory"); + + b.Navigation("TemplateItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.BlockGroup", b => + { + b.Navigation("Blocks"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.BlockItem", b => + { + b.Navigation("BlockItemGraphicsElements"); + + b.Navigation("BlockItemWatermarks"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.Deco", b => + { + b.Navigation("BreakContent"); + + b.Navigation("DecoGraphicsElements"); + + b.Navigation("DecoWatermarks"); + + b.Navigation("Playouts"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoGroup", b => + { + b.Navigation("Decos"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoTemplate", b => + { + b.Navigation("Items"); + + b.Navigation("PlayoutTemplates"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoTemplateGroup", b => + { + b.Navigation("DecoTemplates"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.Template", b => + { + b.Navigation("Items"); + + b.Navigation("PlayoutTemplates"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.TemplateGroup", b => + { + b.Navigation("Templates"); + }); + + 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.Image", b => + { + b.Navigation("ImageMetadata"); + + 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.RemoteStream", b => + { + b.Navigation("MediaVersions"); + + b.Navigation("RemoteStreamMetadata"); + }); + + 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.MySql/Migrations/20260717072111_AddMultiCollectionItemWeight.cs b/ErsatzTV.Infrastructure.MySql/Migrations/20260717072111_AddMultiCollectionItemWeight.cs new file mode 100644 index 000000000..5e835e18f --- /dev/null +++ b/ErsatzTV.Infrastructure.MySql/Migrations/20260717072111_AddMultiCollectionItemWeight.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ErsatzTV.Infrastructure.MySql.Migrations +{ + /// + public partial class AddMultiCollectionItemWeight : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Weight", + table: "MultiCollectionSmartItem", + type: "int", + nullable: false, + defaultValue: 1); + + migrationBuilder.AddColumn( + name: "Weight", + table: "MultiCollectionItem", + type: "int", + nullable: false, + defaultValue: 1); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Weight", + table: "MultiCollectionSmartItem"); + + migrationBuilder.DropColumn( + name: "Weight", + table: "MultiCollectionItem"); + } + } +} diff --git a/ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs b/ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs index 463ec2f51..33ca2307a 100644 --- a/ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs +++ b/ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs @@ -1806,6 +1806,11 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations b.Property("ScheduleAsGroup") .HasColumnType("tinyint(1)"); + b.Property("Weight") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(1); + b.HasKey("MultiCollectionId", "CollectionId"); b.HasIndex("CollectionId"); @@ -1827,6 +1832,11 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations b.Property("ScheduleAsGroup") .HasColumnType("tinyint(1)"); + b.Property("Weight") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(1); + b.HasKey("MultiCollectionId", "SmartCollectionId"); b.HasIndex("SmartCollectionId"); diff --git a/ErsatzTV.Infrastructure.Sqlite/Migrations/20260717071946_AddMultiCollectionItemWeight.Designer.cs b/ErsatzTV.Infrastructure.Sqlite/Migrations/20260717071946_AddMultiCollectionItemWeight.Designer.cs new file mode 100644 index 000000000..45d317d1a --- /dev/null +++ b/ErsatzTV.Infrastructure.Sqlite/Migrations/20260717071946_AddMultiCollectionItemWeight.Designer.cs @@ -0,0 +1,7075 @@ +// +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.Sqlite.Migrations +{ + [DbContext(typeof(TvContext))] + [Migration("20260717071946_AddMultiCollectionItemWeight")] + partial class AddMultiCollectionItemWeight + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.12"); + + 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("ImageMetadataId") + .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("RemoteStreamMetadataId") + .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("ImageMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("OtherVideoMetadataId"); + + b.HasIndex("RemoteStreamMetadataId"); + + 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") + .UseCollation("NOCASE"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ArtistId"); + + b.HasIndex("Title"); + + 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("ImageMetadataId") + .HasColumnType("INTEGER"); + + b.Property("IsMetadataOrphan") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("INTEGER") + .HasComputedColumnSql("CASE WHEN COALESCE(ArtistMetadataId, ChannelId, EpisodeMetadataId, MovieMetadataId, MusicVideoMetadataId, OtherVideoMetadataId, SeasonMetadataId, ShowMetadataId, SongMetadataId, ImageMetadataId, RemoteStreamMetadataId) IS NULL THEN 1 ELSE NULL END", false); + + b.Property("MovieMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MusicVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("OriginalContentType") + .HasColumnType("TEXT"); + + b.Property("OtherVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("RemoteStreamMetadataId") + .HasColumnType("INTEGER"); + + 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("ImageMetadataId"); + + b.HasIndex("IsMetadataOrphan"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("OtherVideoMetadataId"); + + b.HasIndex("RemoteStreamMetadataId"); + + b.HasIndex("SeasonMetadataId"); + + b.HasIndex("ShowMetadataId"); + + b.HasIndex("SongMetadataId"); + + b.ToTable("Artwork", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.BlockItemGraphicsElement", b => + { + b.Property("BlockItemId") + .HasColumnType("INTEGER"); + + b.Property("GraphicsElementId") + .HasColumnType("INTEGER"); + + b.HasKey("BlockItemId", "GraphicsElementId"); + + b.HasIndex("GraphicsElementId"); + + b.ToTable("BlockItemGraphicsElement"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.BlockItemWatermark", b => + { + b.Property("BlockItemId") + .HasColumnType("INTEGER"); + + b.Property("WatermarkId") + .HasColumnType("INTEGER"); + + b.HasKey("BlockItemId", "WatermarkId"); + + b.HasIndex("WatermarkId"); + + b.ToTable("BlockItemWatermark"); + }); + + 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("IdleBehavior") + .HasColumnType("INTEGER"); + + b.Property("IsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("MirrorSourceChannelId") + .HasColumnType("INTEGER"); + + b.Property("MusicVideoCreditsMode") + .HasColumnType("INTEGER"); + + b.Property("MusicVideoCreditsTemplate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .UseCollation("NOCASE"); + + b.Property("Number") + .HasColumnType("TEXT"); + + b.Property("PlayoutMode") + .HasColumnType("INTEGER"); + + b.Property("PlayoutOffset") + .HasColumnType("TEXT"); + + b.Property("PlayoutSource") + .HasColumnType("INTEGER"); + + b.Property("PreferredAudioLanguageCode") + .HasColumnType("TEXT"); + + b.Property("PreferredAudioTitle") + .HasColumnType("TEXT"); + + b.Property("PreferredSubtitleLanguageCode") + .HasColumnType("TEXT"); + + b.Property("ShowInEpg") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("SlugSeconds") + .HasColumnType("REAL"); + + b.Property("SongVideoMode") + .HasColumnType("INTEGER"); + + b.Property("SortNumber") + .HasColumnType("REAL"); + + b.Property("StreamSelector") + .HasColumnType("TEXT"); + + b.Property("StreamSelectorMode") + .HasColumnType("INTEGER"); + + b.Property("StreamingMode") + .HasColumnType("INTEGER"); + + b.Property("SubtitleMode") + .HasColumnType("INTEGER"); + + b.Property("TranscodeMode") + .HasColumnType("INTEGER"); + + b.Property("UniqueId") + .HasColumnType("TEXT"); + + b.Property("WatermarkId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("FFmpegProfileId"); + + b.HasIndex("FallbackFillerId"); + + b.HasIndex("MirrorSourceChannelId"); + + b.HasIndex("Number") + .IsUnique(); + + b.HasIndex("WatermarkId"); + + b.ToTable("Channel", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ChannelTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Description") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(500) + .HasColumnType("varchar(500)") + .HasDefaultValue(""); + + b.Property("FFmpegProfileId") + .HasColumnType("INTEGER"); + + b.Property("FallbackFillerId") + .HasColumnType("INTEGER"); + + b.Property("FixedStartTimeBehavior") + .HasColumnType("INTEGER"); + + b.Property("IdleBehavior") + .HasColumnType("INTEGER"); + + b.Property("IsSystem") + .HasColumnType("INTEGER"); + + b.Property("MidRollFillerId") + .HasColumnType("INTEGER"); + + b.Property("MusicVideoCreditsMode") + .HasColumnType("INTEGER"); + + b.Property("MusicVideoCreditsTemplate") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .UseCollation("NOCASE"); + + b.Property("PlayoutMode") + .HasColumnType("INTEGER"); + + b.Property("PlayoutSource") + .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("RandomStartPoint") + .HasColumnType("INTEGER"); + + b.Property("ShuffleScheduleItems") + .HasColumnType("INTEGER"); + + b.Property("SongVideoMode") + .HasColumnType("INTEGER"); + + b.Property("StreamSelector") + .HasColumnType("TEXT"); + + b.Property("StreamSelectorMode") + .HasColumnType("INTEGER"); + + b.Property("StreamingMode") + .HasColumnType("INTEGER"); + + b.Property("SubtitleMode") + .HasColumnType("INTEGER"); + + b.Property("TranscodeMode") + .HasColumnType("INTEGER"); + + b.Property("WatermarkId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("FFmpegProfileId"); + + b.HasIndex("FallbackFillerId"); + + b.HasIndex("MidRollFillerId"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("PostRollFillerId"); + + b.HasIndex("PreRollFillerId"); + + b.HasIndex("WatermarkId"); + + b.ToTable("ChannelTemplate", (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("REAL"); + + b.Property("Image") + .HasColumnType("TEXT"); + + b.Property("ImageSource") + .HasColumnType("INTEGER"); + + b.Property("Location") + .HasColumnType("INTEGER"); + + b.Property("Mode") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .UseCollation("NOCASE"); + + b.Property("Opacity") + .HasColumnType("INTEGER"); + + b.Property("OpacityExpression") + .HasColumnType("TEXT"); + + b.Property("OriginalContentType") + .HasColumnType("TEXT"); + + b.Property("PlaceWithinSourceContent") + .HasColumnType("INTEGER"); + + b.Property("Size") + .HasColumnType("INTEGER"); + + b.Property("VerticalMarginPercent") + .HasColumnType("REAL"); + + b.Property("WidthPercent") + .HasColumnType("REAL"); + + b.Property("ZIndex") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("ChannelWatermark", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Collection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .UseCollation("NOCASE"); + + b.Property("UseCustomPlaybackOrder") + .HasColumnType("INTEGER"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + 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.DecoGraphicsElement", b => + { + b.Property("DecoId") + .HasColumnType("INTEGER"); + + b.Property("GraphicsElementId") + .HasColumnType("INTEGER"); + + b.HasKey("DecoId", "GraphicsElementId"); + + b.HasIndex("GraphicsElementId"); + + b.ToTable("DecoGraphicsElement"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.DecoWatermark", b => + { + b.Property("DecoId") + .HasColumnType("INTEGER"); + + b.Property("WatermarkId") + .HasColumnType("INTEGER"); + + b.HasKey("DecoId", "WatermarkId"); + + b.HasIndex("WatermarkId"); + + b.ToTable("DecoWatermark"); + }); + + 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("AllowBFrames") + .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("NormalizeAudio") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("NormalizeColors") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("NormalizeFramerate") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(false); + + b.Property("NormalizeLoudnessMode") + .HasColumnType("INTEGER"); + + b.Property("NormalizeVideo") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("PadMode") + .HasColumnType("INTEGER"); + + b.Property("QsvExtraHardwareFrames") + .HasColumnType("INTEGER"); + + b.Property("ResolutionId") + .HasColumnType("INTEGER"); + + b.Property("ScalingBehavior") + .HasColumnType("INTEGER"); + + b.Property("TargetLoudness") + .HasColumnType("REAL"); + + b.Property("ThreadCount") + .HasColumnType("INTEGER"); + + b.Property("TonemapAlgorithm") + .HasColumnType("INTEGER"); + + b.Property("VaapiDevice") + .HasColumnType("TEXT"); + + b.Property("VaapiDisplay") + .HasColumnType("TEXT"); + + b.Property("VaapiDriver") + .HasColumnType("INTEGER"); + + b.Property("VideoBitrate") + .HasColumnType("INTEGER"); + + b.Property("VideoBufferSize") + .HasColumnType("INTEGER"); + + b.Property("VideoFormat") + .HasColumnType("INTEGER"); + + b.Property("VideoPreset") + .HasColumnType("TEXT"); + + b.Property("VideoProfile") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ResolutionId"); + + b.ToTable("FFmpegProfile", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Filler.FillerPreset", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("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("Expression") + .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") + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .UseCollation("NOCASE"); + + b.Property("PadToNearestMinute") + .HasColumnType("INTEGER"); + + b.Property("PlaylistId") + .HasColumnType("INTEGER"); + + b.Property("SmartCollectionId") + .HasColumnType("INTEGER"); + + b.Property("UseChaptersAsMediaItems") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CollectionId"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("MultiCollectionId"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("PlaylistId"); + + 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("ImageMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MovieMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MusicVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OtherVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("RemoteStreamMetadataId") + .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("ImageMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("OtherVideoMetadataId"); + + b.HasIndex("RemoteStreamMetadataId"); + + b.HasIndex("SeasonMetadataId"); + + b.HasIndex("ShowMetadataId"); + + b.HasIndex("SongMetadataId"); + + b.ToTable("Genre"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.GraphicsElement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("GraphicsElement", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ImageFolderDuration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DurationSeconds") + .HasColumnType("REAL"); + + b.Property("LibraryFolderId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("LibraryFolderId") + .IsUnique(); + + b.ToTable("ImageFolderDuration", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ImageMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("DurationSeconds") + .HasColumnType("REAL"); + + b.Property("ImageId") + .HasColumnType("INTEGER"); + + 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("ImageId"); + + b.ToTable("ImageMetadata", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinCollection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Etag") + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ItemId"); + + 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("ParentId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryPathId"); + + b.HasIndex("ParentId"); + + 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("LibraryFolderId") + .HasColumnType("INTEGER"); + + b.Property("MediaVersionId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("PathHash") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryFolderId"); + + b.HasIndex("MediaVersionId"); + + b.HasIndex("PathHash") + .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("ImageId") + .HasColumnType("INTEGER"); + + b.Property("InterlacedRatio") + .HasColumnType("REAL"); + + 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("RemoteStreamId") + .HasColumnType("INTEGER"); + + 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("ImageId"); + + b.HasIndex("MovieId"); + + b.HasIndex("MusicVideoId"); + + b.HasIndex("OtherVideoId"); + + b.HasIndex("RemoteStreamId"); + + 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") + .HasMaxLength(128) + .HasColumnType("varchar(128)") + .UseCollation("NOCASE"); + + b.Property("ImageMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MovieMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MusicVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("OtherVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("RemoteStreamMetadataId") + .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("Guid"); + + b.HasIndex("ImageMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("OtherVideoMetadataId"); + + b.HasIndex("RemoteStreamMetadataId"); + + 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") + .UseCollation("NOCASE"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("MovieId"); + + b.HasIndex("Title"); + + b.ToTable("MovieMetadata", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MultiCollection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .UseCollation("NOCASE"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + 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.Property("Weight") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(1); + + 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.Property("Weight") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(1); + + 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.Playlist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("IsSystem") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .UseCollation("NOCASE"); + + b.Property("PlaylistGroupId") + .HasColumnType("INTEGER"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PlaylistGroupId", "Name") + .IsUnique(); + + b.ToTable("Playlist", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlaylistGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("IsSystem") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("PlaylistGroup", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlaylistItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CollectionId") + .HasColumnType("INTEGER"); + + b.Property("CollectionType") + .HasColumnType("INTEGER"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.Property("IncludeInProgramGuide") + .HasColumnType("INTEGER"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b.Property("MultiCollectionId") + .HasColumnType("INTEGER"); + + b.Property("PlayAll") + .HasColumnType("INTEGER"); + + b.Property("PlaybackOrder") + .HasColumnType("INTEGER"); + + b.Property("PlaylistId") + .HasColumnType("INTEGER"); + + b.Property("SmartCollectionId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CollectionId"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("MultiCollectionId"); + + b.HasIndex("PlaylistId"); + + b.HasIndex("SmartCollectionId"); + + b.ToTable("PlaylistItem", (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("DecoId") + .HasColumnType("INTEGER"); + + b.Property("OnDemandCheckpoint") + .HasColumnType("TEXT"); + + b.Property("ProgramScheduleId") + .HasColumnType("INTEGER"); + + b.Property("ScheduleFile") + .HasColumnType("TEXT"); + + b.Property("ScheduleKind") + .HasColumnType("INTEGER"); + + b.Property("Seed") + .HasColumnType("INTEGER"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChannelId"); + + b.HasIndex("DecoId"); + + b.HasIndex("ProgramScheduleId"); + + b.ToTable("Playout", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutBuildStatus", b => + { + b.Property("PlayoutId") + .HasColumnType("INTEGER"); + + b.Property("LastBuild") + .HasColumnType("TEXT"); + + b.Property("Message") + .HasColumnType("TEXT"); + + b.Property("Success") + .HasColumnType("INTEGER"); + + b.HasKey("PlayoutId"); + + b.ToTable("PlayoutBuildStatus", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutGap", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Finish") + .HasColumnType("TEXT"); + + b.Property("PlayoutId") + .HasColumnType("INTEGER"); + + b.Property("Start") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PlayoutId"); + + b.HasIndex("Start", "Finish") + .HasDatabaseName("IX_PlayoutGap_Start_Finish"); + + b.ToTable("PlayoutGap", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BlockKey") + .HasColumnType("TEXT"); + + b.Property("ChapterTitle") + .HasColumnType("TEXT"); + + b.Property("CollectionEtag") + .HasColumnType("TEXT"); + + b.Property("CollectionKey") + .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("GuideStart") + .HasColumnType("TEXT"); + + 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("SchedulingContext") + .HasColumnType("TEXT"); + + b.Property("Start") + .HasColumnType("TEXT"); + + b.Property("SubtitleMode") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("PlayoutId"); + + b.HasIndex("Start", "Finish") + .HasDatabaseName("IX_PlayoutItem_Start_Finish"); + + b.ToTable("PlayoutItem", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItemGraphicsElement", b => + { + b.Property("PlayoutItemId") + .HasColumnType("INTEGER"); + + b.Property("GraphicsElementId") + .HasColumnType("INTEGER"); + + b.Property("Variables") + .HasColumnType("TEXT"); + + b.HasKey("PlayoutItemId", "GraphicsElementId"); + + b.HasIndex("GraphicsElementId"); + + b.ToTable("PlayoutItemGraphicsElement"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItemWatermark", b => + { + b.Property("PlayoutItemId") + .HasColumnType("INTEGER"); + + b.Property("WatermarkId") + .HasColumnType("INTEGER"); + + b.HasKey("PlayoutItemId", "WatermarkId"); + + b.HasIndex("WatermarkId"); + + b.ToTable("PlayoutItemWatermark"); + }); + + 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("FakeCollectionKey") + .HasColumnType("TEXT"); + + b.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b.Property("MultiCollectionId") + .HasColumnType("INTEGER"); + + b.Property("PlaylistId") + .HasColumnType("INTEGER"); + + b.Property("PlayoutId") + .HasColumnType("INTEGER"); + + b.Property("RerunCollectionId") + .HasColumnType("INTEGER"); + + b.Property("SearchQuery") + .HasColumnType("TEXT"); + + b.Property("SmartCollectionId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CollectionId"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("MultiCollectionId"); + + b.HasIndex("PlaylistId"); + + b.HasIndex("PlayoutId"); + + b.HasIndex("RerunCollectionId"); + + b.HasIndex("SmartCollectionId"); + + b.ToTable("PlayoutProgramScheduleAnchor", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutScheduleItemFillGroupIndex", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("PlayoutId") + .HasColumnType("INTEGER"); + + b.Property("ProgramScheduleItemId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PlayoutId"); + + b.HasIndex("ProgramScheduleItemId"); + + b.ToTable("PlayoutScheduleItemFillGroupIndex", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexCollection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Etag") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("PlexCollection", (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("FixedStartTimeBehavior") + .HasColumnType("INTEGER"); + + b.Property("KeepMultiPartEpisodesTogether") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .UseCollation("NOCASE"); + + b.Property("RandomStartPoint") + .HasColumnType("INTEGER"); + + b.Property("ShuffleScheduleItems") + .HasColumnType("INTEGER"); + + b.Property("TreatCollectionsAsShows") + .HasColumnType("INTEGER"); + + b.Property("Version") + .IsConcurrencyToken() + .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("EndDay") + .HasColumnType("INTEGER"); + + b.Property("EndMonth") + .HasColumnType("INTEGER"); + + b.Property("EndYear") + .HasColumnType("INTEGER"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("LimitToDateRange") + .HasColumnType("INTEGER"); + + b.Property("MonthsOfYear") + .HasColumnType("TEXT"); + + b.Property("PlayoutId") + .HasColumnType("INTEGER"); + + b.Property("ProgramScheduleId") + .HasColumnType("INTEGER"); + + b.Property("StartDay") + .HasColumnType("INTEGER"); + + b.Property("StartMonth") + .HasColumnType("INTEGER"); + + b.Property("StartYear") + .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("FakeCollectionKey") + .HasColumnType("TEXT"); + + b.Property("FallbackFillerId") + .HasColumnType("INTEGER"); + + b.Property("FillWithGroupMode") + .HasColumnType("INTEGER"); + + b.Property("FixedStartTimeBehavior") + .HasColumnType("INTEGER"); + + b.Property("GuideMode") + .HasColumnType("INTEGER"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("MarathonBatchSize") + .HasColumnType("INTEGER"); + + b.Property("MarathonGroupBy") + .HasColumnType("INTEGER"); + + b.Property("MarathonShuffleGroups") + .HasColumnType("INTEGER"); + + b.Property("MarathonShuffleItems") + .HasColumnType("INTEGER"); + + b.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b.Property("MidRollFillerId") + .HasColumnType("INTEGER"); + + b.Property("MultiCollectionId") + .HasColumnType("INTEGER"); + + b.Property("PlaybackOrder") + .HasColumnType("INTEGER"); + + b.Property("PlaylistId") + .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("RerunCollectionId") + .HasColumnType("INTEGER"); + + b.Property("SearchQuery") + .HasColumnType("TEXT"); + + b.Property("SearchTitle") + .HasColumnType("TEXT"); + + b.Property("SmartCollectionId") + .HasColumnType("INTEGER"); + + b.Property("StartTime") + .HasColumnType("TEXT"); + + b.Property("SubtitleMode") + .HasColumnType("INTEGER"); + + b.Property("TailFillerId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CollectionId"); + + b.HasIndex("FallbackFillerId"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("MidRollFillerId"); + + b.HasIndex("MultiCollectionId"); + + b.HasIndex("PlaylistId"); + + b.HasIndex("PostRollFillerId"); + + b.HasIndex("PreRollFillerId"); + + b.HasIndex("ProgramScheduleId"); + + b.HasIndex("RerunCollectionId"); + + b.HasIndex("SmartCollectionId"); + + b.HasIndex("TailFillerId"); + + b.ToTable("ProgramScheduleItem", (string)null); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemGraphicsElement", b => + { + b.Property("ProgramScheduleItemId") + .HasColumnType("INTEGER"); + + b.Property("GraphicsElementId") + .HasColumnType("INTEGER"); + + b.HasKey("ProgramScheduleItemId", "GraphicsElementId"); + + b.HasIndex("GraphicsElementId"); + + b.ToTable("ProgramScheduleItemGraphicsElement"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemWatermark", b => + { + b.Property("ProgramScheduleItemId") + .HasColumnType("INTEGER"); + + b.Property("WatermarkId") + .HasColumnType("INTEGER"); + + b.HasKey("ProgramScheduleItemId", "WatermarkId"); + + b.HasIndex("WatermarkId"); + + b.ToTable("ProgramScheduleItemWatermark"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.RemoteStreamMetadata", 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("Plot") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("RemoteStreamId") + .HasColumnType("INTEGER"); + + b.Property("SortTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RemoteStreamId"); + + b.ToTable("RemoteStreamMetadata", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.RerunCollection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CollectionId") + .HasColumnType("INTEGER"); + + b.Property("CollectionType") + .HasColumnType("INTEGER"); + + b.Property("FirstRunPlaybackOrder") + .HasColumnType("INTEGER"); + + b.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b.Property("MultiCollectionId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .UseCollation("NOCASE"); + + b.Property("RerunPlaybackOrder") + .HasColumnType("INTEGER"); + + b.Property("SmartCollectionId") + .HasColumnType("INTEGER"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CollectionId"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("MultiCollectionId"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("SmartCollectionId"); + + b.ToTable("RerunCollection", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Resolution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("IsCustom") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(false); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Resolution", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.Block", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BlockGroupId") + .HasColumnType("INTEGER"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("Minutes") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .UseCollation("NOCASE"); + + b.Property("StopScheduling") + .HasColumnType("INTEGER"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("BlockGroupId", "Name") + .IsUnique(); + + b.ToTable("Block", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.BlockGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .UseCollation("NOCASE"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("BlockGroup", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.BlockItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BlockId") + .HasColumnType("INTEGER"); + + b.Property("CollectionId") + .HasColumnType("INTEGER"); + + b.Property("CollectionType") + .HasColumnType("INTEGER"); + + b.Property("DisableWatermarks") + .HasColumnType("INTEGER"); + + b.Property("IncludeInProgramGuide") + .HasColumnType("INTEGER"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b.Property("MultiCollectionId") + .HasColumnType("INTEGER"); + + b.Property("PlaybackOrder") + .HasColumnType("INTEGER"); + + b.Property("SearchQuery") + .HasColumnType("TEXT"); + + b.Property("SearchTitle") + .HasColumnType("TEXT"); + + b.Property("SmartCollectionId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("BlockId"); + + b.HasIndex("CollectionId"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("MultiCollectionId"); + + b.HasIndex("SmartCollectionId"); + + b.ToTable("BlockItem", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.Deco", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BreakContentMode") + .HasColumnType("INTEGER"); + + b.Property("DeadAirFallbackCollectionId") + .HasColumnType("INTEGER"); + + b.Property("DeadAirFallbackCollectionType") + .HasColumnType("INTEGER"); + + b.Property("DeadAirFallbackMediaItemId") + .HasColumnType("INTEGER"); + + b.Property("DeadAirFallbackMode") + .HasColumnType("INTEGER"); + + b.Property("DeadAirFallbackMultiCollectionId") + .HasColumnType("INTEGER"); + + b.Property("DeadAirFallbackSmartCollectionId") + .HasColumnType("INTEGER"); + + b.Property("DecoGroupId") + .HasColumnType("INTEGER"); + + b.Property("DefaultFillerCollectionId") + .HasColumnType("INTEGER"); + + b.Property("DefaultFillerCollectionType") + .HasColumnType("INTEGER"); + + b.Property("DefaultFillerMediaItemId") + .HasColumnType("INTEGER"); + + b.Property("DefaultFillerMode") + .HasColumnType("INTEGER"); + + b.Property("DefaultFillerMultiCollectionId") + .HasColumnType("INTEGER"); + + b.Property("DefaultFillerSmartCollectionId") + .HasColumnType("INTEGER"); + + b.Property("DefaultFillerTrimToFit") + .HasColumnType("INTEGER"); + + b.Property("GraphicsElementsMode") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .UseCollation("NOCASE"); + + b.Property("UseGraphicsElementsDuringFiller") + .HasColumnType("INTEGER"); + + b.Property("UseWatermarkDuringFiller") + .HasColumnType("INTEGER"); + + b.Property("WatermarkMode") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DeadAirFallbackCollectionId"); + + b.HasIndex("DeadAirFallbackMediaItemId"); + + b.HasIndex("DeadAirFallbackMultiCollectionId"); + + b.HasIndex("DeadAirFallbackSmartCollectionId"); + + b.HasIndex("DefaultFillerCollectionId"); + + b.HasIndex("DefaultFillerMediaItemId"); + + b.HasIndex("DefaultFillerMultiCollectionId"); + + b.HasIndex("DefaultFillerSmartCollectionId"); + + b.HasIndex("DecoGroupId", "Name") + .IsUnique(); + + b.ToTable("Deco", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoBreakContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CollectionId") + .HasColumnType("INTEGER"); + + b.Property("CollectionType") + .HasColumnType("INTEGER"); + + b.Property("DecoId") + .HasColumnType("INTEGER"); + + b.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b.Property("MultiCollectionId") + .HasColumnType("INTEGER"); + + b.Property("Placement") + .HasColumnType("INTEGER"); + + b.Property("PlaylistId") + .HasColumnType("INTEGER"); + + b.Property("SmartCollectionId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CollectionId"); + + b.HasIndex("DecoId"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("MultiCollectionId"); + + b.HasIndex("PlaylistId"); + + b.HasIndex("SmartCollectionId"); + + b.ToTable("DecoBreakContent", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .UseCollation("NOCASE"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("DecoGroup", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("DecoTemplateGroupId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .UseCollation("NOCASE"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DecoTemplateGroupId", "Name") + .IsUnique(); + + b.ToTable("DecoTemplate", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoTemplateGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .UseCollation("NOCASE"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("DecoTemplateGroup", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoTemplateItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DecoId") + .HasColumnType("INTEGER"); + + b.Property("DecoTemplateId") + .HasColumnType("INTEGER"); + + b.Property("EndTime") + .HasColumnType("TEXT"); + + b.Property("StartTime") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DecoId"); + + b.HasIndex("DecoTemplateId"); + + b.ToTable("DecoTemplateItem", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.PlayoutHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BlockId") + .HasColumnType("INTEGER"); + + b.Property("ChildKey") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasColumnType("TEXT"); + + b.Property("Finish") + .HasColumnType("TEXT"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("IsCurrentChild") + .HasColumnType("INTEGER"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("PlaybackOrder") + .HasColumnType("INTEGER"); + + b.Property("PlayoutId") + .HasColumnType("INTEGER"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("BlockId"); + + b.HasIndex("PlayoutId"); + + b.ToTable("PlayoutHistory", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.PlayoutTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("DaysOfMonth") + .HasColumnType("TEXT"); + + b.Property("DaysOfWeek") + .HasColumnType("TEXT"); + + b.Property("DecoTemplateId") + .HasColumnType("INTEGER"); + + b.Property("EndDay") + .HasColumnType("INTEGER"); + + b.Property("EndMonth") + .HasColumnType("INTEGER"); + + b.Property("EndYear") + .HasColumnType("INTEGER"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("LimitToDateRange") + .HasColumnType("INTEGER"); + + b.Property("MonthsOfYear") + .HasColumnType("TEXT"); + + b.Property("PlayoutId") + .HasColumnType("INTEGER"); + + b.Property("StartDay") + .HasColumnType("INTEGER"); + + b.Property("StartMonth") + .HasColumnType("INTEGER"); + + b.Property("StartYear") + .HasColumnType("INTEGER"); + + b.Property("TemplateId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DecoTemplateId"); + + b.HasIndex("PlayoutId"); + + b.HasIndex("TemplateId"); + + b.ToTable("PlayoutTemplate", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.RerunHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b.Property("PlayoutId") + .HasColumnType("INTEGER"); + + b.Property("RerunCollectionId") + .HasColumnType("INTEGER"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("PlayoutId"); + + b.HasIndex("RerunCollectionId"); + + b.ToTable("RerunHistory", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.Template", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .UseCollation("NOCASE"); + + b.Property("TemplateGroupId") + .HasColumnType("INTEGER"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("TemplateGroupId", "Name") + .IsUnique(); + + b.ToTable("Template", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.TemplateGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .UseCollation("NOCASE"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("TemplateGroup", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.TemplateItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BlockId") + .HasColumnType("INTEGER"); + + b.Property("StartTime") + .HasColumnType("TEXT"); + + b.Property("TemplateId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("BlockId"); + + b.HasIndex("TemplateId"); + + b.ToTable("TemplateItem", (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") + .UseCollation("NOCASE"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ShowId"); + + b.HasIndex("Title"); + + b.ToTable("ShowMetadata", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.SmartCollection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .UseCollation("NOCASE"); + + b.Property("Query") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("SmartCollection", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.SongMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Album") + .HasColumnType("TEXT"); + + b.PrimitiveCollection("AlbumArtists") + .HasColumnType("TEXT"); + + b.PrimitiveCollection("Artists") + .HasColumnType("TEXT"); + + b.Property("Comment") + .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("ImageMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MovieMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MusicVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OtherVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("RemoteStreamMetadataId") + .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("ImageMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("OtherVideoMetadataId"); + + b.HasIndex("RemoteStreamMetadataId"); + + 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("ImageMetadataId") + .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("RemoteStreamMetadataId") + .HasColumnType("INTEGER"); + + 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("ImageMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("OtherVideoMetadataId"); + + b.HasIndex("RemoteStreamMetadataId"); + + 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("ExternalTypeId") + .HasColumnType("TEXT"); + + b.Property("ImageMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MovieMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MusicVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OtherVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("RemoteStreamMetadataId") + .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("ImageMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("OtherVideoMetadataId"); + + b.HasIndex("RemoteStreamMetadataId"); + + 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("AutoRefresh") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("GeneratePlaylist") + .HasColumnType("INTEGER"); + + b.Property("ItemCount") + .HasColumnType("INTEGER"); + + b.Property("LastMatch") + .HasColumnType("TEXT"); + + b.Property("LastUpdate") + .HasColumnType("TEXT"); + + b.Property("List") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.Property("PlaylistId") + .HasColumnType("INTEGER"); + + b.Property("TraktId") + .HasColumnType("INTEGER"); + + b.Property("User") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.HasIndex("PlaylistId"); + + 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") + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("TEXT"); + + b.Property("ShouldSyncItems") + .HasColumnType("INTEGER"); + + b.HasIndex("ItemId"); + + 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("LastNetworksScan") + .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.Image", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.ToTable("Image", (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.RemoteStream", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.Property("Duration") + .HasColumnType("TEXT"); + + b.Property("FallbackQuery") + .HasColumnType("TEXT"); + + b.Property("IsLive") + .HasColumnType("INTEGER"); + + b.Property("Script") + .HasColumnType("TEXT"); + + b.Property("Url") + .HasColumnType("TEXT"); + + b.ToTable("RemoteStream", (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("LastCollectionsScan") + .HasColumnType("TEXT"); + + 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("LastCollectionsScan") + .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("TEXT"); + + b.Property("MultipleMode") + .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") + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("TEXT"); + + b.HasIndex("ItemId"); + + 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") + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("TEXT"); + + b.HasIndex("ItemId"); + + 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.PlexOtherVideo", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.OtherVideo"); + + b.Property("Etag") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.ToTable("PlexOtherVideo", (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") + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("TEXT"); + + b.HasIndex("ItemId"); + + 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") + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasMaxLength(36) + .IsUnicode(false) + .HasColumnType("TEXT"); + + b.HasIndex("ItemId"); + + 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.ImageMetadata", null) + .WithMany("Actors") + .HasForeignKey("ImageMetadataId") + .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.RemoteStreamMetadata", null) + .WithMany("Actors") + .HasForeignKey("RemoteStreamMetadataId") + .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.ImageMetadata", null) + .WithMany("Artwork") + .HasForeignKey("ImageMetadataId") + .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.RemoteStreamMetadata", null) + .WithMany("Artwork") + .HasForeignKey("RemoteStreamMetadataId") + .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.BlockItemGraphicsElement", b => + { + b.HasOne("ErsatzTV.Core.Domain.Scheduling.BlockItem", "BlockItem") + .WithMany("BlockItemGraphicsElements") + .HasForeignKey("BlockItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.GraphicsElement", "GraphicsElement") + .WithMany("BlockItemGraphicsElements") + .HasForeignKey("GraphicsElementId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BlockItem"); + + b.Navigation("GraphicsElement"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.BlockItemWatermark", b => + { + b.HasOne("ErsatzTV.Core.Domain.Scheduling.BlockItem", "BlockItem") + .WithMany("BlockItemWatermarks") + .HasForeignKey("BlockItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.ChannelWatermark", "Watermark") + .WithMany("BlockItemWatermarks") + .HasForeignKey("WatermarkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BlockItem"); + + b.Navigation("Watermark"); + }); + + 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.Channel", "MirrorSourceChannel") + .WithMany() + .HasForeignKey("MirrorSourceChannelId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("ErsatzTV.Core.Domain.ChannelWatermark", "Watermark") + .WithMany() + .HasForeignKey("WatermarkId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("FFmpegProfile"); + + b.Navigation("FallbackFiller"); + + b.Navigation("MirrorSourceChannel"); + + b.Navigation("Watermark"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ChannelTemplate", b => + { + b.HasOne("ErsatzTV.Core.Domain.FFmpegProfile", "FFmpegProfile") + .WithMany() + .HasForeignKey("FFmpegProfileId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.Filler.FillerPreset", "FallbackFiller") + .WithMany() + .HasForeignKey("FallbackFillerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("ErsatzTV.Core.Domain.Filler.FillerPreset", "MidRollFiller") + .WithMany() + .HasForeignKey("MidRollFillerId") + .OnDelete(DeleteBehavior.SetNull); + + 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.ChannelWatermark", "Watermark") + .WithMany() + .HasForeignKey("WatermarkId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("FFmpegProfile"); + + b.Navigation("FallbackFiller"); + + b.Navigation("MidRollFiller"); + + b.Navigation("PostRollFiller"); + + b.Navigation("PreRollFiller"); + + 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.DecoGraphicsElement", b => + { + b.HasOne("ErsatzTV.Core.Domain.Scheduling.Deco", "Deco") + .WithMany("DecoGraphicsElements") + .HasForeignKey("DecoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.GraphicsElement", "GraphicsElement") + .WithMany("DecoGraphicsElements") + .HasForeignKey("GraphicsElementId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Deco"); + + b.Navigation("GraphicsElement"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.DecoWatermark", b => + { + b.HasOne("ErsatzTV.Core.Domain.Scheduling.Deco", "Deco") + .WithMany("DecoWatermarks") + .HasForeignKey("DecoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.ChannelWatermark", "Watermark") + .WithMany("DecoWatermarks") + .HasForeignKey("WatermarkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Deco"); + + b.Navigation("Watermark"); + }); + + 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.Playlist", "Playlist") + .WithMany() + .HasForeignKey("PlaylistId") + .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("Playlist"); + + 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.ImageMetadata", null) + .WithMany("Genres") + .HasForeignKey("ImageMetadataId") + .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.RemoteStreamMetadata", null) + .WithMany("Genres") + .HasForeignKey("RemoteStreamMetadataId") + .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.ImageFolderDuration", b => + { + b.HasOne("ErsatzTV.Core.Domain.LibraryFolder", "LibraryFolder") + .WithOne("ImageFolderDuration") + .HasForeignKey("ErsatzTV.Core.Domain.ImageFolderDuration", "LibraryFolderId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("LibraryFolder"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ImageMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.Image", "Image") + .WithMany("ImageMetadata") + .HasForeignKey("ImageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Image"); + }); + + 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.HasOne("ErsatzTV.Core.Domain.LibraryFolder", "Parent") + .WithMany("Children") + .HasForeignKey("ParentId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("LibraryPath"); + + b.Navigation("Parent"); + }); + + 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.LibraryFolder", "LibraryFolder") + .WithMany("MediaFiles") + .HasForeignKey("LibraryFolderId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ErsatzTV.Core.Domain.MediaVersion", "MediaVersion") + .WithMany("MediaFiles") + .HasForeignKey("MediaVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("LibraryFolder"); + + 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.Image", null) + .WithMany("MediaVersions") + .HasForeignKey("ImageId") + .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.RemoteStream", null) + .WithMany("MediaVersions") + .HasForeignKey("RemoteStreamId") + .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.ImageMetadata", null) + .WithMany("Guids") + .HasForeignKey("ImageMetadataId") + .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.RemoteStreamMetadata", null) + .WithMany("Guids") + .HasForeignKey("RemoteStreamMetadataId") + .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.Playlist", b => + { + b.HasOne("ErsatzTV.Core.Domain.PlaylistGroup", "PlaylistGroup") + .WithMany("Playlists") + .HasForeignKey("PlaylistGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PlaylistGroup"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlaylistItem", 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.Playlist", "Playlist") + .WithMany("Items") + .HasForeignKey("PlaylistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.SmartCollection", "SmartCollection") + .WithMany() + .HasForeignKey("SmartCollectionId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Collection"); + + b.Navigation("MediaItem"); + + b.Navigation("MultiCollection"); + + b.Navigation("Playlist"); + + b.Navigation("SmartCollection"); + }); + + 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.Scheduling.Deco", "Deco") + .WithMany("Playouts") + .HasForeignKey("DecoId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule") + .WithMany("Playouts") + .HasForeignKey("ProgramScheduleId") + .OnDelete(DeleteBehavior.Cascade); + + b.OwnsOne("ErsatzTV.Core.Domain.PlayoutAnchor", "Anchor", b1 => + { + b1.Property("PlayoutId") + .HasColumnType("INTEGER"); + + b1.Property("Context") + .HasColumnType("TEXT"); + + 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("NextInstructionIndex") + .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("Deco"); + + b.Navigation("ProgramSchedule"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutBuildStatus", b => + { + b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout") + .WithOne("BuildStatus") + .HasForeignKey("ErsatzTV.Core.Domain.PlayoutBuildStatus", "PlayoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Playout"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutGap", b => + { + b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout") + .WithMany("Gaps") + .HasForeignKey("PlayoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Playout"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem") + .WithMany() + .HasForeignKey("MediaItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout") + .WithMany("Items") + .HasForeignKey("PlayoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MediaItem"); + + b.Navigation("Playout"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItemGraphicsElement", b => + { + b.HasOne("ErsatzTV.Core.Domain.GraphicsElement", "GraphicsElement") + .WithMany("PlayoutItemGraphicsElements") + .HasForeignKey("GraphicsElementId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.PlayoutItem", "PlayoutItem") + .WithMany("PlayoutItemGraphicsElements") + .HasForeignKey("PlayoutItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("GraphicsElement"); + + b.Navigation("PlayoutItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItemWatermark", b => + { + b.HasOne("ErsatzTV.Core.Domain.PlayoutItem", "PlayoutItem") + .WithMany("PlayoutItemWatermarks") + .HasForeignKey("PlayoutItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.ChannelWatermark", "Watermark") + .WithMany("PlayoutItemWatermarks") + .HasForeignKey("WatermarkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PlayoutItem"); + + 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.Playlist", "Playlist") + .WithMany() + .HasForeignKey("PlaylistId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout") + .WithMany("ProgramScheduleAnchors") + .HasForeignKey("PlayoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.RerunCollection", "RerunCollection") + .WithMany() + .HasForeignKey("RerunCollectionId") + .OnDelete(DeleteBehavior.Cascade); + + 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("Playlist"); + + b.Navigation("Playout"); + + b.Navigation("RerunCollection"); + + b.Navigation("SmartCollection"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutScheduleItemFillGroupIndex", b => + { + b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout") + .WithMany("FillGroupIndices") + .HasForeignKey("PlayoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", "ProgramScheduleItem") + .WithMany() + .HasForeignKey("ProgramScheduleItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("ErsatzTV.Core.Domain.CollectionEnumeratorState", "EnumeratorState", b1 => + { + b1.Property("PlayoutScheduleItemFillGroupIndexId") + .HasColumnType("INTEGER"); + + b1.Property("Index") + .HasColumnType("INTEGER"); + + b1.Property("Seed") + .HasColumnType("INTEGER"); + + b1.HasKey("PlayoutScheduleItemFillGroupIndexId"); + + b1.ToTable("FillGroupEnumeratorState", (string)null); + + b1.WithOwner() + .HasForeignKey("PlayoutScheduleItemFillGroupIndexId"); + }); + + b.Navigation("EnumeratorState"); + + b.Navigation("Playout"); + + b.Navigation("ProgramScheduleItem"); + }); + + 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.Playlist", "Playlist") + .WithMany() + .HasForeignKey("PlaylistId") + .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); + + b.HasOne("ErsatzTV.Core.Domain.RerunCollection", "RerunCollection") + .WithMany() + .HasForeignKey("RerunCollectionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SmartCollection", "SmartCollection") + .WithMany() + .HasForeignKey("SmartCollectionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.Filler.FillerPreset", "TailFiller") + .WithMany() + .HasForeignKey("TailFillerId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Collection"); + + b.Navigation("FallbackFiller"); + + b.Navigation("MediaItem"); + + b.Navigation("MidRollFiller"); + + b.Navigation("MultiCollection"); + + b.Navigation("Playlist"); + + b.Navigation("PostRollFiller"); + + b.Navigation("PreRollFiller"); + + b.Navigation("ProgramSchedule"); + + b.Navigation("RerunCollection"); + + b.Navigation("SmartCollection"); + + b.Navigation("TailFiller"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemGraphicsElement", b => + { + b.HasOne("ErsatzTV.Core.Domain.GraphicsElement", "GraphicsElement") + .WithMany("ProgramScheduleItemGraphicsElements") + .HasForeignKey("GraphicsElementId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", "ProgramScheduleItem") + .WithMany("ProgramScheduleItemGraphicsElements") + .HasForeignKey("ProgramScheduleItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("GraphicsElement"); + + b.Navigation("ProgramScheduleItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemWatermark", b => + { + b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", "ProgramScheduleItem") + .WithMany("ProgramScheduleItemWatermarks") + .HasForeignKey("ProgramScheduleItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.ChannelWatermark", "Watermark") + .WithMany("ProgramScheduleItemWatermarks") + .HasForeignKey("WatermarkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ProgramScheduleItem"); + + b.Navigation("Watermark"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.RemoteStreamMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.RemoteStream", "RemoteStream") + .WithMany("RemoteStreamMetadata") + .HasForeignKey("RemoteStreamId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("RemoteStream"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.RerunCollection", 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.Scheduling.Block", b => + { + b.HasOne("ErsatzTV.Core.Domain.Scheduling.BlockGroup", "BlockGroup") + .WithMany("Blocks") + .HasForeignKey("BlockGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BlockGroup"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.BlockItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.Scheduling.Block", "Block") + .WithMany("Items") + .HasForeignKey("BlockId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + 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("Block"); + + b.Navigation("Collection"); + + b.Navigation("MediaItem"); + + b.Navigation("MultiCollection"); + + b.Navigation("SmartCollection"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.Deco", b => + { + b.HasOne("ErsatzTV.Core.Domain.Collection", "DeadAirFallbackCollection") + .WithMany() + .HasForeignKey("DeadAirFallbackCollectionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MediaItem", "DeadAirFallbackMediaItem") + .WithMany() + .HasForeignKey("DeadAirFallbackMediaItemId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MultiCollection", "DeadAirFallbackMultiCollection") + .WithMany() + .HasForeignKey("DeadAirFallbackMultiCollectionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SmartCollection", "DeadAirFallbackSmartCollection") + .WithMany() + .HasForeignKey("DeadAirFallbackSmartCollectionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.Scheduling.DecoGroup", "DecoGroup") + .WithMany("Decos") + .HasForeignKey("DecoGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.Collection", "DefaultFillerCollection") + .WithMany() + .HasForeignKey("DefaultFillerCollectionId"); + + b.HasOne("ErsatzTV.Core.Domain.MediaItem", "DefaultFillerMediaItem") + .WithMany() + .HasForeignKey("DefaultFillerMediaItemId"); + + b.HasOne("ErsatzTV.Core.Domain.MultiCollection", "DefaultFillerMultiCollection") + .WithMany() + .HasForeignKey("DefaultFillerMultiCollectionId"); + + b.HasOne("ErsatzTV.Core.Domain.SmartCollection", "DefaultFillerSmartCollection") + .WithMany() + .HasForeignKey("DefaultFillerSmartCollectionId"); + + b.Navigation("DeadAirFallbackCollection"); + + b.Navigation("DeadAirFallbackMediaItem"); + + b.Navigation("DeadAirFallbackMultiCollection"); + + b.Navigation("DeadAirFallbackSmartCollection"); + + b.Navigation("DecoGroup"); + + b.Navigation("DefaultFillerCollection"); + + b.Navigation("DefaultFillerMediaItem"); + + b.Navigation("DefaultFillerMultiCollection"); + + b.Navigation("DefaultFillerSmartCollection"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoBreakContent", b => + { + b.HasOne("ErsatzTV.Core.Domain.Collection", "Collection") + .WithMany() + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.Scheduling.Deco", "Deco") + .WithMany("BreakContent") + .HasForeignKey("DecoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + 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.Playlist", "Playlist") + .WithMany() + .HasForeignKey("PlaylistId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SmartCollection", "SmartCollection") + .WithMany() + .HasForeignKey("SmartCollectionId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Collection"); + + b.Navigation("Deco"); + + b.Navigation("MediaItem"); + + b.Navigation("MultiCollection"); + + b.Navigation("Playlist"); + + b.Navigation("SmartCollection"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoTemplate", b => + { + b.HasOne("ErsatzTV.Core.Domain.Scheduling.DecoTemplateGroup", "DecoTemplateGroup") + .WithMany("DecoTemplates") + .HasForeignKey("DecoTemplateGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DecoTemplateGroup"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoTemplateItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.Scheduling.Deco", "Deco") + .WithMany() + .HasForeignKey("DecoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.Scheduling.DecoTemplate", "DecoTemplate") + .WithMany("Items") + .HasForeignKey("DecoTemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Deco"); + + b.Navigation("DecoTemplate"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.PlayoutHistory", b => + { + b.HasOne("ErsatzTV.Core.Domain.Scheduling.Block", "Block") + .WithMany("PlayoutHistory") + .HasForeignKey("BlockId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout") + .WithMany("PlayoutHistory") + .HasForeignKey("PlayoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Block"); + + b.Navigation("Playout"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.PlayoutTemplate", b => + { + b.HasOne("ErsatzTV.Core.Domain.Scheduling.DecoTemplate", "DecoTemplate") + .WithMany("PlayoutTemplates") + .HasForeignKey("DecoTemplateId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout") + .WithMany("Templates") + .HasForeignKey("PlayoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.Scheduling.Template", "Template") + .WithMany("PlayoutTemplates") + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DecoTemplate"); + + b.Navigation("Playout"); + + b.Navigation("Template"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.RerunHistory", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem") + .WithMany() + .HasForeignKey("MediaItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout") + .WithMany() + .HasForeignKey("PlayoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.RerunCollection", "RerunCollection") + .WithMany() + .HasForeignKey("RerunCollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MediaItem"); + + b.Navigation("Playout"); + + b.Navigation("RerunCollection"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.Template", b => + { + b.HasOne("ErsatzTV.Core.Domain.Scheduling.TemplateGroup", "TemplateGroup") + .WithMany("Templates") + .HasForeignKey("TemplateGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TemplateGroup"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.TemplateItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.Scheduling.Block", "Block") + .WithMany("TemplateItems") + .HasForeignKey("BlockId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.Scheduling.Template", "Template") + .WithMany("Items") + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Block"); + + b.Navigation("Template"); + }); + + 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.ImageMetadata", null) + .WithMany("Studios") + .HasForeignKey("ImageMetadataId") + .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.RemoteStreamMetadata", null) + .WithMany("Studios") + .HasForeignKey("RemoteStreamMetadataId") + .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.ImageMetadata", null) + .WithMany("Subtitles") + .HasForeignKey("ImageMetadataId") + .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.RemoteStreamMetadata", null) + .WithMany("Subtitles") + .HasForeignKey("RemoteStreamMetadataId") + .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.ImageMetadata", null) + .WithMany("Tags") + .HasForeignKey("ImageMetadataId") + .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.RemoteStreamMetadata", null) + .WithMany("Tags") + .HasForeignKey("RemoteStreamMetadataId") + .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.TraktList", b => + { + b.HasOne("ErsatzTV.Core.Domain.Playlist", "Playlist") + .WithMany() + .HasForeignKey("PlaylistId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Playlist"); + }); + + 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.Image", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.Image", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Movie", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.Movie", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideo", b => + { + b.HasOne("ErsatzTV.Core.Domain.Artist", "Artist") + .WithMany("MusicVideos") + .HasForeignKey("ArtistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.MusicVideo", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Artist"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.OtherVideo", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.OtherVideo", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.RemoteStream", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.RemoteStream", "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.PlexOtherVideo", b => + { + b.HasOne("ErsatzTV.Core.Domain.OtherVideo", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.PlexOtherVideo", "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.ChannelWatermark", b => + { + b.Navigation("BlockItemWatermarks"); + + b.Navigation("DecoWatermarks"); + + b.Navigation("PlayoutItemWatermarks"); + + b.Navigation("ProgramScheduleItemWatermarks"); + }); + + 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.GraphicsElement", b => + { + b.Navigation("BlockItemGraphicsElements"); + + b.Navigation("DecoGraphicsElements"); + + b.Navigation("PlayoutItemGraphicsElements"); + + b.Navigation("ProgramScheduleItemGraphicsElements"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ImageMetadata", 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.Library", b => + { + b.Navigation("Paths"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LibraryFolder", b => + { + b.Navigation("Children"); + + b.Navigation("ImageFolderDuration"); + + b.Navigation("MediaFiles"); + }); + + 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.Playlist", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlaylistGroup", b => + { + b.Navigation("Playlists"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b => + { + b.Navigation("BuildStatus"); + + b.Navigation("FillGroupIndices"); + + b.Navigation("Gaps"); + + b.Navigation("Items"); + + b.Navigation("PlayoutHistory"); + + b.Navigation("ProgramScheduleAlternates"); + + b.Navigation("ProgramScheduleAnchors"); + + b.Navigation("Templates"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItem", b => + { + b.Navigation("PlayoutItemGraphicsElements"); + + b.Navigation("PlayoutItemWatermarks"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramSchedule", b => + { + b.Navigation("Items"); + + b.Navigation("Playouts"); + + b.Navigation("ProgramScheduleAlternates"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItem", b => + { + b.Navigation("ProgramScheduleItemGraphicsElements"); + + b.Navigation("ProgramScheduleItemWatermarks"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.RemoteStreamMetadata", 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.Scheduling.Block", b => + { + b.Navigation("Items"); + + b.Navigation("PlayoutHistory"); + + b.Navigation("TemplateItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.BlockGroup", b => + { + b.Navigation("Blocks"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.BlockItem", b => + { + b.Navigation("BlockItemGraphicsElements"); + + b.Navigation("BlockItemWatermarks"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.Deco", b => + { + b.Navigation("BreakContent"); + + b.Navigation("DecoGraphicsElements"); + + b.Navigation("DecoWatermarks"); + + b.Navigation("Playouts"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoGroup", b => + { + b.Navigation("Decos"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoTemplate", b => + { + b.Navigation("Items"); + + b.Navigation("PlayoutTemplates"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoTemplateGroup", b => + { + b.Navigation("DecoTemplates"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.Template", b => + { + b.Navigation("Items"); + + b.Navigation("PlayoutTemplates"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.TemplateGroup", b => + { + b.Navigation("Templates"); + }); + + 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.Image", b => + { + b.Navigation("ImageMetadata"); + + 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.RemoteStream", b => + { + b.Navigation("MediaVersions"); + + b.Navigation("RemoteStreamMetadata"); + }); + + 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.Sqlite/Migrations/20260717071946_AddMultiCollectionItemWeight.cs b/ErsatzTV.Infrastructure.Sqlite/Migrations/20260717071946_AddMultiCollectionItemWeight.cs new file mode 100644 index 000000000..cfa4938ce --- /dev/null +++ b/ErsatzTV.Infrastructure.Sqlite/Migrations/20260717071946_AddMultiCollectionItemWeight.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ErsatzTV.Infrastructure.Sqlite.Migrations +{ + /// + public partial class AddMultiCollectionItemWeight : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Weight", + table: "MultiCollectionSmartItem", + type: "INTEGER", + nullable: false, + defaultValue: 1); + + migrationBuilder.AddColumn( + name: "Weight", + table: "MultiCollectionItem", + type: "INTEGER", + nullable: false, + defaultValue: 1); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Weight", + table: "MultiCollectionSmartItem"); + + migrationBuilder.DropColumn( + name: "Weight", + table: "MultiCollectionItem"); + } + } +} diff --git a/ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs b/ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs index 830a6e74c..a04121309 100644 --- a/ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs +++ b/ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs @@ -1729,6 +1729,11 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations b.Property("ScheduleAsGroup") .HasColumnType("INTEGER"); + b.Property("Weight") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(1); + b.HasKey("MultiCollectionId", "CollectionId"); b.HasIndex("CollectionId"); @@ -1750,6 +1755,11 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations b.Property("ScheduleAsGroup") .HasColumnType("INTEGER"); + b.Property("Weight") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(1); + b.HasKey("MultiCollectionId", "SmartCollectionId"); b.HasIndex("SmartCollectionId"); diff --git a/ErsatzTV.Infrastructure/Data/Configurations/Collection/MultiCollectionConfiguration.cs b/ErsatzTV.Infrastructure/Data/Configurations/Collection/MultiCollectionConfiguration.cs index 87ec40622..5b1e7be3a 100644 --- a/ErsatzTV.Infrastructure/Data/Configurations/Collection/MultiCollectionConfiguration.cs +++ b/ErsatzTV.Infrastructure/Data/Configurations/Collection/MultiCollectionConfiguration.cs @@ -30,7 +30,14 @@ public class MultiCollectionConfiguration : IEntityTypeConfiguration mc.MultiCollectionItems) .HasForeignKey(mci => mci.MultiCollectionId) .OnDelete(DeleteBehavior.Cascade), - j => j.HasKey(mci => new { mci.MultiCollectionId, mci.CollectionId })); + j => + { + j.HasKey(mci => new { mci.MultiCollectionId, mci.CollectionId }); + + // default 1 = fair share (#70). Must be a DB default too: existing rows would + // otherwise migrate to 0, and a 0-weight source is dropped by the enumerator. + j.Property(mci => mci.Weight).HasDefaultValue(1); + }); builder.HasMany(m => m.SmartCollections) .WithMany(m => m.MultiCollections) @@ -43,6 +50,13 @@ public class MultiCollectionConfiguration : IEntityTypeConfiguration mc.MultiCollectionSmartItems) .HasForeignKey(mci => mci.MultiCollectionId) .OnDelete(DeleteBehavior.Cascade), - j => j.HasKey(mci => new { mci.MultiCollectionId, mci.SmartCollectionId })); + j => + { + j.HasKey(mci => new { mci.MultiCollectionId, mci.SmartCollectionId }); + + // mirrors MultiCollectionItem above -- omitting it would silently un-weight + // every smart-collection member of a multi collection + j.Property(mci => mci.Weight).HasDefaultValue(1); + }); } } diff --git a/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs index 95714eace..5a3f1a795 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs @@ -738,7 +738,8 @@ public class MediaCollectionRepository : IMediaCollectionRepository sortedItems, multiCollectionItem.ScheduleAsGroup, multiCollectionItem.PlaybackOrder, - multiCollectionItem.Collection.UseCustomPlaybackOrder)); + multiCollectionItem.Collection.UseCustomPlaybackOrder, + multiCollectionItem.Weight)); } } else @@ -751,7 +752,8 @@ public class MediaCollectionRepository : IMediaCollectionRepository items, multiCollectionItem.ScheduleAsGroup, multiCollectionItem.PlaybackOrder, - multiCollectionItem.Collection.UseCustomPlaybackOrder)); + multiCollectionItem.Collection.UseCustomPlaybackOrder, + multiCollectionItem.Weight)); } } @@ -769,7 +771,8 @@ public class MediaCollectionRepository : IMediaCollectionRepository items, multiCollectionSmartItem.ScheduleAsGroup, multiCollectionSmartItem.PlaybackOrder, - false)); + false, + multiCollectionSmartItem.Weight)); } } -- 2.47.3 From 2fda7d28f8528d8c6d3f30720bc2cdc204f57381 Mon Sep 17 00:00:00 2001 From: Timothy Date: Fri, 17 Jul 2026 09:33:46 +0200 Subject: [PATCH 02/12] test(70): pin WeightedShuffle semantics; fix unbounded reshuffle retry on wrap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ten tests pin the distribution contract exactly rather than statistically, because the sequence IS the product decision: 3:1 emits A A B A (spread, not drained); equal weights air a 2-item source as often as a 20-item one; ties break to the earliest source; a defaulted weight behaves as fair-share (guarding the migration default); restoring at an index equals advancing to it (the stateless contract that lets CollectionEnumeratorState carry this order). Fixes a hang found by the non-vacuity control. When a rotation wraps, MoveNext retried the rebuild to avoid an immediate repeat. ShuffleInOrder can do that unbounded because its reshuffle randomizes the lead item — but this order's lead is decided by weight, so the heaviest source always leads, and when it holds a single item the lead never changes and the retry never terminates. Two single-item collections with unequal weights would wedge the playout build. The retry is now bounded: avoiding a back-to-back repeat is a nicety, not terminating is not. Regression test walks several wraps under a timeout. Non-vacuity proven per repo lore: inverting the real WRR pick (max -> min, never if(true), which trips CS0219 under warnings-as-errors and silently serves a stale dll to --no-build) failed 5 of 10 tests on a clean build (0 errors, so not a stale-dll false pass). Reverted; 10/10 green. Refs #70 Co-Authored-By: Claude Opus 4.8 (1M context) --- ...eightedShuffleCollectionEnumeratorTests.cs | 281 ++++++++++++++++++ .../WeightedShuffleCollectionEnumerator.cs | 14 +- 2 files changed, 294 insertions(+), 1 deletion(-) create mode 100644 ErsatzTV.Core.Tests/Scheduling/WeightedShuffleCollectionEnumeratorTests.cs diff --git a/ErsatzTV.Core.Tests/Scheduling/WeightedShuffleCollectionEnumeratorTests.cs b/ErsatzTV.Core.Tests/Scheduling/WeightedShuffleCollectionEnumeratorTests.cs new file mode 100644 index 000000000..50cd73919 --- /dev/null +++ b/ErsatzTV.Core.Tests/Scheduling/WeightedShuffleCollectionEnumeratorTests.cs @@ -0,0 +1,281 @@ +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Scheduling; +using LanguageExt.UnsafeValueAccess; +using NUnit.Framework; +using Shouldly; + +namespace ErsatzTV.Core.Tests.Scheduling; + +/// +/// Pins the weighted / fair-share distribution contract (#70). The sequence semantics here are the +/// product decision, so they are asserted exactly rather than statistically. +/// +[TestFixture] +public class WeightedShuffleCollectionEnumeratorTests +{ + // ids are allocated per source so an emitted item's source is identifiable from its id + private const int SourceAFirstId = 100; + private const int SourceBFirstId = 200; + private const int SourceCFirstId = 300; + + private static CollectionWithItems Source(string key, int firstId, int itemCount, int weight) => + new( + 0, + 0, + key, + Enumerable.Range(firstId, itemCount) + .Select(i => new Movie { Id = i, MovieMetadata = [] }) + .Cast() + .ToList(), + true, + PlaybackOrder.WeightedShuffle, + false, + weight); + + private static string SourceOf(int id) => id switch + { + >= SourceCFirstId => "C", + >= SourceBFirstId => "B", + _ => "A" + }; + + private static List TakeSourceSequence(WeightedShuffleCollectionEnumerator enumerator, int count) + { + var result = new List(); + for (var i = 0; i < count; i++) + { + enumerator.Current.IsSome.ShouldBeTrue(); + result.Add(SourceOf(enumerator.Current.ValueUnsafe().Id)); + enumerator.MoveNext(Option.None); + } + + return result; + } + + [Test] + public void Weights_Three_To_One_Emit_A_A_B_A() + { + // the canonical smooth-WRR contract: 3:1 spreads B through the rotation (A A B A), + // rather than draining A first (A A A B) the way PlaylistItem.Count does + var collections = new List + { + Source("A", SourceAFirstId, 3, 3), + Source("B", SourceBFirstId, 1, 1) + }; + + var enumerator = new WeightedShuffleCollectionEnumerator( + collections, + new CollectionEnumeratorState { Seed = 1234, Index = 0 }, + CancellationToken.None); + + TakeSourceSequence(enumerator, 4).ShouldBe(["A", "A", "B", "A"]); + } + + [Test] + public void Equal_Weights_Are_Fair_Share_Regardless_Of_Collection_Size() + { + // the heart of goal (2): a 20-item source must air as often as a 2-item source. + // ShuffleInOrder cannot do this -- it plays every item once, so airtime tracks size. + var collections = new List + { + Source("A", SourceAFirstId, 20, 1), + Source("B", SourceBFirstId, 2, 1) + }; + + var enumerator = new WeightedShuffleCollectionEnumerator( + collections, + new CollectionEnumeratorState { Seed = 1234, Index = 0 }, + CancellationToken.None); + + List sequence = TakeSourceSequence(enumerator, 40); + + // equal weights => strict alternation, so the small source loops rather than falling silent + sequence.Count(s => s == "A").ShouldBe(20); + sequence.Count(s => s == "B").ShouldBe(20); + } + + [Test] + public void Ties_Break_To_The_Earliest_Source_In_List_Order() + { + var collections = new List + { + Source("A", SourceAFirstId, 4, 1), + Source("B", SourceBFirstId, 4, 1), + Source("C", SourceCFirstId, 4, 1) + }; + + var enumerator = new WeightedShuffleCollectionEnumerator( + collections, + new CollectionEnumeratorState { Seed = 99, Index = 0 }, + CancellationToken.None); + + // all accumulators tie every round, so list order decides + TakeSourceSequence(enumerator, 6).ShouldBe(["A", "B", "C", "A", "B", "C"]); + } + + [Test] + public void Weight_Defaults_To_One_So_An_Unweighted_Collection_Is_Fair_Share() + { + // guards the migration default: an existing row that arrives without a weight must + // behave as fair-share, never as a dropped source + var collections = new List + { + new(0, 0, "A", [new Movie { Id = SourceAFirstId, MovieMetadata = [] }], true, PlaybackOrder.WeightedShuffle, false), + new(0, 0, "B", [new Movie { Id = SourceBFirstId, MovieMetadata = [] }], true, PlaybackOrder.WeightedShuffle, false) + }; + + collections[0].Weight.ShouldBe(1); + collections[1].Weight.ShouldBe(1); + + var enumerator = new WeightedShuffleCollectionEnumerator( + collections, + new CollectionEnumeratorState { Seed = 7, Index = 0 }, + CancellationToken.None); + + TakeSourceSequence(enumerator, 4).ShouldBe(["A", "B", "A", "B"]); + } + + [Test] + public void Restoring_At_An_Index_Equals_Advancing_To_It() + { + // the stateless contract: (Seed, Index) fully determines position, which is what lets the + // existing CollectionEnumeratorState persistence carry this order with no per-source counters + List Collections() => + [ + Source("A", SourceAFirstId, 5, 3), + Source("B", SourceBFirstId, 3, 1) + ]; + + const int Seed = 4242; + const int Target = 7; + + var advanced = new WeightedShuffleCollectionEnumerator( + Collections(), + new CollectionEnumeratorState { Seed = Seed, Index = 0 }, + CancellationToken.None); + for (var i = 0; i < Target; i++) + { + advanced.MoveNext(Option.None); + } + + var restored = new WeightedShuffleCollectionEnumerator( + Collections(), + new CollectionEnumeratorState { Seed = Seed, Index = Target }, + CancellationToken.None); + + restored.State.Index.ShouldBe(advanced.State.Index); + restored.State.Seed.ShouldBe(advanced.State.Seed); + restored.Current.ValueUnsafe().Id.ShouldBe(advanced.Current.ValueUnsafe().Id); + } + + [Test] + public void A_Single_Source_Emits_All_Of_Its_Items() + { + var collections = new List { Source("A", SourceAFirstId, 5, 3) }; + + var enumerator = new WeightedShuffleCollectionEnumerator( + collections, + new CollectionEnumeratorState { Seed = 11, Index = 0 }, + CancellationToken.None); + + var seen = new System.Collections.Generic.HashSet(); + for (var i = 0; i < 5; i++) + { + seen.Add(enumerator.Current.ValueUnsafe().Id); + enumerator.MoveNext(Option.None); + } + + seen.Count.ShouldBe(5); + } + + [Test] + public void An_Empty_Source_Is_Ignored_Rather_Than_Emitting_Nothing() + { + var collections = new List + { + Source("A", SourceAFirstId, 2, 1), + new(0, 0, "empty", [], true, PlaybackOrder.WeightedShuffle, false, 5) + }; + + var enumerator = new WeightedShuffleCollectionEnumerator( + collections, + new CollectionEnumeratorState { Seed = 3, Index = 0 }, + CancellationToken.None); + + // a heavily-weighted empty source must not starve the rotation or emit None + TakeSourceSequence(enumerator, 4).ShouldBe(["A", "A", "A", "A"]); + } + + [Test] + [Timeout(10_000)] + public void Single_Item_Sources_Do_Not_Hang_On_Rotation_Wrap() + { + // regression: the wrap retries a rebuild to avoid an immediate repeat, but this order's lead item is + // decided by weight, so the heaviest source always leads. With one item in it the lead is invariant and + // an unbounded retry never terminates -- a hung playout build, not a wrong one. Found by the + // non-vacuity control, which hung instead of failing. + var collections = new List + { + Source("A", SourceAFirstId, 1, 3), + Source("B", SourceBFirstId, 1, 1) + }; + + var enumerator = new WeightedShuffleCollectionEnumerator( + collections, + new CollectionEnumeratorState { Seed = 1234, Index = 0 }, + CancellationToken.None); + + // walk several full rotations so the wrap path is exercised repeatedly + List sequence = TakeSourceSequence(enumerator, 24); + + sequence.ShouldContain("A"); + sequence.ShouldContain("B"); + } + + [Test] + public void No_Sources_Yields_No_Current() + { + var enumerator = new WeightedShuffleCollectionEnumerator( + [], + new CollectionEnumeratorState { Seed = 1, Index = 0 }, + CancellationToken.None); + + enumerator.Current.IsNone.ShouldBeTrue(); + enumerator.Count.ShouldBe(0); + Should.NotThrow(() => enumerator.MoveNext(Option.None)); + } + + [Test] + public void A_Custom_Ordered_Source_Keeps_Its_Order() + { + var collections = new List + { + new( + 0, + 0, + "A", + Enumerable.Range(SourceAFirstId, 4) + .Select(i => new Movie { Id = i, MovieMetadata = [] }) + .Cast() + .ToList(), + true, + PlaybackOrder.WeightedShuffle, + true, + 1) + }; + + var enumerator = new WeightedShuffleCollectionEnumerator( + collections, + new CollectionEnumeratorState { Seed = 555, Index = 0 }, + CancellationToken.None); + + var ids = new List(); + for (var i = 0; i < 4; i++) + { + ids.Add(enumerator.Current.ValueUnsafe().Id); + enumerator.MoveNext(Option.None); + } + + ids.ShouldBe([SourceAFirstId, SourceAFirstId + 1, SourceAFirstId + 2, SourceAFirstId + 3]); + } +} diff --git a/ErsatzTV.Core/Scheduling/WeightedShuffleCollectionEnumerator.cs b/ErsatzTV.Core/Scheduling/WeightedShuffleCollectionEnumerator.cs index 21672fdff..1de9992ac 100644 --- a/ErsatzTV.Core/Scheduling/WeightedShuffleCollectionEnumerator.cs +++ b/ErsatzTV.Core/Scheduling/WeightedShuffleCollectionEnumerator.cs @@ -32,6 +32,15 @@ public class WeightedShuffleCollectionEnumerator : IMediaCollectionEnumerator /// private const int MaxCycleLength = 100_000; + /// + /// Bounds the avoid-an-immediate-repeat retry when a rotation wraps. Unlike + /// , whose reshuffle randomizes which item leads the + /// next cycle, this order's lead item is decided by weight — the heaviest source always wins the first + /// pick. So when that source has a single item the lead never changes and an unbounded retry would spin + /// forever. Avoiding a back-to-back repeat is a nicety; never terminating is not. + /// + private const int MaxReshuffleAttempts = 10; + private readonly CancellationToken _cancellationToken; private readonly IList _collections; private readonly Lazy> _lazyMinimumDuration; @@ -101,13 +110,16 @@ public class WeightedShuffleCollectionEnumerator : IMediaCollectionEnumerator Option tail = Current; State.Index = 0; + var attempts = 0; do { State.Seed = _random.Next(); _random = new Random(State.Seed); _shuffled = Build(_collections, _random); + attempts++; } while (!_cancellationToken.IsCancellationRequested && _collections.Count > 1 && - _shuffled.Length > 0 && Current.Map(x => x.Id) == tail.Map(x => x.Id)); + _shuffled.Length > 0 && attempts < MaxReshuffleAttempts && + Current.Map(x => x.Id) == tail.Map(x => x.Id)); } else { -- 2.47.3 From 20b56905ec2bb7f9f941dbf7d085fe48892a239c Mon Sep 17 00:00:00 2001 From: Timothy Date: Fri, 17 Jul 2026 12:34:07 +0200 Subject: [PATCH 03/12] feat(70): refuse WeightedShuffle where it isn't implemented, report where it can't be MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WeightedShuffle is implemented for classic schedule items only, and every other engine mishandles an order it doesn't know *silently*: PlaylistEnumerator has no default arm so the item is dropped from the playlist; BlockPlayoutBuilder filters block items against an allow-list and `continue`s past the rest; YAML and Scripted return None, which their callers' foreach reads as "no content". A weighted order degrading to unweighted random or to nothing is the worst failure mode here, because the output is supposed to look arbitrary — nobody would notice. Rather than change those shipped fallbacks (a real defect class, but pre-existing and wider than this feature — filed separately, non-goal here), this closes the new exposure at the write path: ReplacePlaylistItems and ReplaceBlockItems reject WeightedShuffle with an error naming where it is available. If it can't be persisted where it isn't handled, the silent sites never see it. Defence in depth for the two engines that address orders by name: YAML and Scripted now log a warning when a parsed order falls through unhandled, so an empty schedule explains itself. Enum.Parse accepts "weightedShuffle" the moment the value exists, so the gate above can't cover them. EnumeratorForContent becomes an instance method to reach the logger. ProgramScheduleItemCommandBase lists WeightedShuffle explicitly as valid for multi collections — it already passed by falling through the switch, and implicit-by- omission is how this subsystem grew its silent paths. Core.Tests: 558 passed, 0 failed. Refs #70 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Commands/ReplacePlaylistItemsHandler.cs | 27 +++++++++++++++++-- .../ProgramScheduleItemCommandBase.cs | 3 +++ .../Commands/ReplaceBlockItemsHandler.cs | 27 +++++++++++++++++-- .../Scheduling/Engine/SchedulingEngine.cs | 11 ++++++-- .../YamlScheduling/EnumeratorCache.cs | 12 +++++++-- 5 files changed, 72 insertions(+), 8 deletions(-) diff --git a/ErsatzTV.Application/MediaCollections/Commands/ReplacePlaylistItemsHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/ReplacePlaylistItemsHandler.cs index 023c8ee6e..a8fadda04 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/ReplacePlaylistItemsHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/ReplacePlaylistItemsHandler.cs @@ -1,4 +1,4 @@ -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; @@ -73,7 +73,30 @@ public class ReplacePlaylistItemsHandler(IDbContextFactory dbContextF ReplacePlaylistItems request, CancellationToken cancellationToken) => PlaylistMustExist(dbContext, request.PlaylistId, cancellationToken) - .BindT(playlist => CollectionTypesMustBeValid(request, playlist)); + .BindT(playlist => CollectionTypesMustBeValid(request, playlist)) + .BindT(playlist => PlaybackOrdersMustBeSupported(request, playlist)); + + private static Validation PlaybackOrdersMustBeSupported( + ReplacePlaylistItems request, + Playlist playlist) => + request.Items + .Map(item => PlaybackOrderMustBeSupported(item.PlaybackOrder)) + .Sequence() + .Map(_ => playlist); + + private static Validation PlaybackOrderMustBeSupported(PlaybackOrder playbackOrder) + { + // WeightedShuffle (#70) is implemented for classic schedule items only. PlaylistEnumerator has no + // default arm, so an order it doesn't know leaves the enumerator null and the item is dropped from the + // playlist silently -- refuse it at the write path instead of scheduling nothing at build time. + if (playbackOrder is PlaybackOrder.WeightedShuffle) + { + return BaseError.New( + $"Playback order '{playbackOrder}' is not supported for playlist items; it is available on classic schedule items"); + } + + return Unit.Default; + } private static Task> PlaylistMustExist( TvContext dbContext, diff --git a/ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs b/ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs index 3bec3ff06..5677849f3 100644 --- a/ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs +++ b/ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs @@ -63,6 +63,9 @@ public abstract class ProgramScheduleItemCommandBase return BaseError.New($"Invalid playback order for multi collection: '{item.PlaybackOrder}'"); case PlaybackOrder.Shuffle: case PlaybackOrder.ShuffleInOrder: + // WeightedShuffle (#70) distributes across a multi collection's sources, so this is its + // intended home. Listed explicitly rather than falling through the switch by omission. + case PlaybackOrder.WeightedShuffle: break; } } diff --git a/ErsatzTV.Application/Scheduling/Commands/ReplaceBlockItemsHandler.cs b/ErsatzTV.Application/Scheduling/Commands/ReplaceBlockItemsHandler.cs index 3e1c8d8ba..b0965c9ae 100644 --- a/ErsatzTV.Application/Scheduling/Commands/ReplaceBlockItemsHandler.cs +++ b/ErsatzTV.Application/Scheduling/Commands/ReplaceBlockItemsHandler.cs @@ -1,4 +1,4 @@ -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Scheduling; using ErsatzTV.Infrastructure.Data; @@ -112,7 +112,30 @@ public class ReplaceBlockItemsHandler(IDbContextFactory dbContextFact BlockMustExist(dbContext, request.BlockId, cancellationToken) .BindT(block => MinutesMustBeValid(request, block)) .BindT(block => BlockNameMustBeValid(dbContext, block, request)) - .BindT(block => CollectionTypesMustBeValid(request, block)); + .BindT(block => CollectionTypesMustBeValid(request, block)) + .BindT(block => PlaybackOrdersMustBeSupported(request, block)); + + private static Validation PlaybackOrdersMustBeSupported( + ReplaceBlockItems request, + Block block) => + request.Items + .Map(item => PlaybackOrderMustBeSupported(item.PlaybackOrder)) + .Sequence() + .Map(_ => block); + + private static Validation PlaybackOrderMustBeSupported(PlaybackOrder playbackOrder) + { + // WeightedShuffle (#70) is implemented for classic schedule items only. BlockPlayoutBuilder filters + // block items against an allow-list of orders and silently `continue`s past anything else, so an + // unsupported order here means the block item never airs and nothing reports why. + if (playbackOrder is PlaybackOrder.WeightedShuffle) + { + return BaseError.New( + $"Playback order '{playbackOrder}' is not supported for block items; it is available on classic schedule items"); + } + + return Unit.Default; + } private static Task> BlockMustExist( TvContext dbContext, diff --git a/ErsatzTV.Core/Scheduling/Engine/SchedulingEngine.cs b/ErsatzTV.Core/Scheduling/Engine/SchedulingEngine.cs index 53837822e..508273b07 100644 --- a/ErsatzTV.Core/Scheduling/Engine/SchedulingEngine.cs +++ b/ErsatzTV.Core/Scheduling/Engine/SchedulingEngine.cs @@ -1,4 +1,4 @@ -using System.Collections.Immutable; +using System.Collections.Immutable; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Filler; using ErsatzTV.Core.Domain.Scheduling; @@ -1217,7 +1217,8 @@ public class SchedulingEngine( return _state; } - private static Option EnumeratorForContent( + // instance rather than static so an unsupported order can be reported (#70) + private Option EnumeratorForContent( List items, CollectionEnumeratorState state, PlaybackOrder playbackOrder, @@ -1235,6 +1236,12 @@ public class SchedulingEngine( return new BlockPlayoutShuffledMediaCollectionEnumerator(groupedMediaItems, state); } + // None means the caller's foreach never runs, so the content is simply absent from the playout with + // nothing said. Report it instead of leaving a silently empty schedule (#70). + logger.LogWarning( + "Playback order {PlaybackOrder} is not supported by scripted scheduling; no content will be scheduled for this entry", + playbackOrder); + return Option.None; } diff --git a/ErsatzTV.Core/Scheduling/YamlScheduling/EnumeratorCache.cs b/ErsatzTV.Core/Scheduling/YamlScheduling/EnumeratorCache.cs index 9c394253c..f325ea6f4 100644 --- a/ErsatzTV.Core/Scheduling/YamlScheduling/EnumeratorCache.cs +++ b/ErsatzTV.Core/Scheduling/YamlScheduling/EnumeratorCache.cs @@ -1,4 +1,4 @@ -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Scheduling; using ErsatzTV.Core.Scheduling.BlockScheduling; @@ -178,7 +178,8 @@ public class EnumeratorCache(IMediaCollectionRepository mediaCollectionRepositor cancellationToken); } - switch (Enum.Parse(content.Order, true)) + var parsedOrder = Enum.Parse(content.Order, true); + switch (parsedOrder) { case PlaybackOrder.Chronological: return new ChronologicalMediaCollectionEnumerator(items, state); @@ -190,6 +191,13 @@ public class EnumeratorCache(IMediaCollectionRepository mediaCollectionRepositor return new BlockPlayoutShuffledMediaCollectionEnumerator(groupedMediaItems, state); } + // this path schedules nothing for the content, which is indistinguishable from "no items" downstream. + // Orders are addressed by name here, so any order the enum knows parses fine and then lands here -- + // say so, rather than leaving an empty schedule to be explained (#70). + logger.LogWarning( + "Playback order {PlaybackOrder} is not supported by sequential (YAML) scheduling; no content will be scheduled for this entry", + parsedOrder); + return Option.None; } -- 2.47.3 From b808f8139e31fea4020870e56f01f1037cff4621 Mon Sep 17 00:00:00 2001 From: Timothy Date: Fri, 17 Jul 2026 12:36:20 +0200 Subject: [PATCH 04/12] test(70): prove per-source Weight reaches the scheduler for both member kinds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A multi collection holds plain collections and smart collections through two separate join entities that build CollectionWithItems at separate call sites. Only the plain one is obvious, and forgetting the smart mirror un-weights smart-collection members with nothing reporting it — so it gets a test rather than a comment. Three tests: both weights survive the round trip; an unweighted row reads back as 1, not 0 (a 0-weight source is filtered out by the enumerator, so a 0 default would silently drop every pre-existing member from a weighted rotation); and the real MediaCollectionRepository hands the weight to the scheduler — the mapping tests alone would pass even if the repo dropped it on the floor. ErsatzTV.Tests: 1641 passed, 0 failed. Refs #70 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../MediaCollectionRepositoryWeightTests.cs | 219 ++++++++++++++++++ 1 file changed, 219 insertions(+) create mode 100644 ErsatzTV.Tests/Infrastructure/MediaCollectionRepositoryWeightTests.cs diff --git a/ErsatzTV.Tests/Infrastructure/MediaCollectionRepositoryWeightTests.cs b/ErsatzTV.Tests/Infrastructure/MediaCollectionRepositoryWeightTests.cs new file mode 100644 index 000000000..4a8835b18 --- /dev/null +++ b/ErsatzTV.Tests/Infrastructure/MediaCollectionRepositoryWeightTests.cs @@ -0,0 +1,219 @@ +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Interfaces.Search; +using ErsatzTV.Core.Scheduling; +using ErsatzTV.Infrastructure.Data; +using ErsatzTV.Infrastructure.Data.Repositories; +using ErsatzTV.Tests.Support; +using NSubstitute; +using NUnit.Framework; +using Shouldly; + +namespace ErsatzTV.Tests.Infrastructure; + +/// +/// Pins that a multi collection's per-source Weight (#70) reaches the scheduler for BOTH kinds of +/// member. A multi collection holds plain collections and smart collections through two separate join +/// entities, and only the plain one is obvious — forget the smart mirror and smart-collection members +/// silently fall back to fair-share with nothing reporting it. +/// +[TestFixture] +public class MediaCollectionRepositoryWeightTests +{ + private InMemoryTvContext _db; + + [SetUp] + public async Task SetUp() => _db = await InMemoryTvContext.CreateAsync(); + + [TearDown] + public async Task TearDown() => await _db.DisposeAsync(); + + private static Movie Movie(int id) => new() + { + Id = id, + MovieMetadata = [], + MediaVersions = [] + }; + + [Test] + public async Task Weight_Is_Carried_For_Both_Collection_And_SmartCollection_Members() + { + const int MultiCollectionId = 1; + const int CollectionId = 10; + const int SmartCollectionId = 20; + + await using (TvContext context = _db.Factory.CreateDbContext()) + { + context.Movies.Add(Movie(100)); + + context.Collections.Add( + new Collection + { + Id = CollectionId, + Name = "plain", + UseCustomPlaybackOrder = false, + CollectionItems = [new CollectionItem { CollectionId = CollectionId, MediaItemId = 100 }] + }); + + context.SmartCollections.Add( + new SmartCollection + { + Id = SmartCollectionId, + Name = "smart", + Query = "type:movie" + }); + + context.MultiCollections.Add( + new MultiCollection + { + Id = MultiCollectionId, + Name = "multi", + MultiCollectionItems = + [ + new MultiCollectionItem + { + MultiCollectionId = MultiCollectionId, + CollectionId = CollectionId, + ScheduleAsGroup = true, + PlaybackOrder = PlaybackOrder.WeightedShuffle, + Weight = 5 + } + ], + MultiCollectionSmartItems = + [ + new MultiCollectionSmartItem + { + MultiCollectionId = MultiCollectionId, + SmartCollectionId = SmartCollectionId, + ScheduleAsGroup = true, + PlaybackOrder = PlaybackOrder.WeightedShuffle, + Weight = 7 + } + ] + }); + + await context.SaveChangesAsync(); + } + + await using (TvContext context = _db.Factory.CreateDbContext()) + { + MultiCollection multiCollection = context.MultiCollections + .Single(mc => mc.Id == MultiCollectionId); + + context.Entry(multiCollection).Collection(mc => mc.MultiCollectionItems).Load(); + context.Entry(multiCollection).Collection(mc => mc.MultiCollectionSmartItems).Load(); + + // the persisted weights must survive the round trip on BOTH join entities + multiCollection.MultiCollectionItems.Single().Weight.ShouldBe(5); + multiCollection.MultiCollectionSmartItems.Single().Weight.ShouldBe(7); + } + } + + [Test] + public async Task Weight_Defaults_To_One_So_An_Existing_Row_Is_Fair_Share_Not_Dropped() + { + // the migration backfills 1 rather than 0. A 0-weight source is filtered out by the enumerator, so a + // 0 default would silently remove every pre-existing member from a weighted rotation. + const int MultiCollectionId = 2; + const int CollectionId = 30; + const int SmartCollectionId = 40; + + await using (TvContext context = _db.Factory.CreateDbContext()) + { + context.Collections.Add(new Collection { Id = CollectionId, Name = "plain2", CollectionItems = [] }); + context.SmartCollections.Add( + new SmartCollection { Id = SmartCollectionId, Name = "smart2", Query = "type:movie" }); + + context.MultiCollections.Add( + new MultiCollection + { + Id = MultiCollectionId, + Name = "multi2", + MultiCollectionItems = + [ + new MultiCollectionItem + { + MultiCollectionId = MultiCollectionId, + CollectionId = CollectionId, + ScheduleAsGroup = true, + PlaybackOrder = PlaybackOrder.Shuffle + } + ], + MultiCollectionSmartItems = + [ + new MultiCollectionSmartItem + { + MultiCollectionId = MultiCollectionId, + SmartCollectionId = SmartCollectionId, + ScheduleAsGroup = true, + PlaybackOrder = PlaybackOrder.Shuffle + } + ] + }); + + await context.SaveChangesAsync(); + } + + await using (TvContext context = _db.Factory.CreateDbContext()) + { + MultiCollection multiCollection = context.MultiCollections.Single(mc => mc.Id == MultiCollectionId); + context.Entry(multiCollection).Collection(mc => mc.MultiCollectionItems).Load(); + context.Entry(multiCollection).Collection(mc => mc.MultiCollectionSmartItems).Load(); + + multiCollection.MultiCollectionItems.Single().Weight.ShouldBe(1); + multiCollection.MultiCollectionSmartItems.Single().Weight.ShouldBe(1); + } + } + + [Test] + public async Task GetMultiCollectionCollections_Carries_Weight_Into_CollectionWithItems() + { + // the mapping tests above prove the column round-trips; this proves the repository actually hands the + // weight to the scheduler. The plain-collection branch and the smart-collection branch build + // CollectionWithItems at separate call sites, so each has to be checked on its own. + const int MultiCollectionId = 3; + const int CollectionId = 50; + + await using (TvContext context = _db.Factory.CreateDbContext()) + { + context.Movies.Add(Movie(500)); + + context.Collections.Add( + new Collection + { + Id = CollectionId, + Name = "weighted-plain", + UseCustomPlaybackOrder = false, + CollectionItems = [new CollectionItem { CollectionId = CollectionId, MediaItemId = 500 }] + }); + + context.MultiCollections.Add( + new MultiCollection + { + Id = MultiCollectionId, + Name = "weighted-multi", + MultiCollectionItems = + [ + new MultiCollectionItem + { + MultiCollectionId = MultiCollectionId, + CollectionId = CollectionId, + ScheduleAsGroup = true, + PlaybackOrder = PlaybackOrder.WeightedShuffle, + Weight = 4 + } + ], + MultiCollectionSmartItems = [] + }); + + await context.SaveChangesAsync(); + } + + var repository = new MediaCollectionRepository(Substitute.For(), _db.Factory); + + List result = + await repository.GetMultiCollectionCollections(MultiCollectionId, CancellationToken.None); + + CollectionWithItems collection = result.ShouldHaveSingleItem(); + collection.Weight.ShouldBe(4); + } +} -- 2.47.3 From 0e41503921d965d0ae995c81206f366e9009b892 Mon Sep 17 00:00:00 2001 From: Timothy Date: Fri, 17 Jul 2026 12:40:48 +0200 Subject: [PATCH 05/12] feat(70): accept per-source weight on the multi-collection API Without this the weight is only reachable by editing the database, so the enumerator has nothing to distribute by. Weight is threaded through create and update (all four handler branches: add and update, plain and smart) and defaults to 1, so it is optional on the wire and /api/v1 stays additive under the freeze. It is returned on the read path too, which is load-bearing rather than symmetry: the update replaces the item list, so a client that GETs, edits a name, and PUTs back would silently reset every weight to the default if the GET didn't carry it. Weight edits ride the existing MultiCollection Version token, so If-Match/412 concurrency needs no new design. Regenerated v1.json + v1.d.ts + endpoint-index via update-openapi.sh and generate:api (never hand-edited). The spec picks up weight on both request and response models and WeightedShuffle in the PlaybackOrder enum; weight is emitted optional. Refs #70 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Commands/CreateMultiCollection.cs | 3 ++- .../Commands/CreateMultiCollectionHandler.cs | 6 ++++-- .../Commands/UpdateMultiCollection.cs | 3 ++- .../Commands/UpdateMultiCollectionHandler.cs | 10 +++++++--- ErsatzTV.Application/MediaCollections/Mapper.cs | 6 ++++-- .../MultiCollectionItemViewModel.cs | 3 ++- .../MultiCollectionSmartItemViewModel.cs | 3 ++- .../MultiCollectionResponseModel.cs | 3 ++- .../Controllers/Api/MultiCollectionController.cs | 6 ++++-- .../Api/Requests/CreateMultiCollectionRequest.cs | 6 ++++-- .../Api/Requests/UpdateMultiCollectionRequest.cs | 3 ++- ErsatzTV/wwwroot/openapi/v1.json | 13 ++++++++++++- web/src/api/generated/v1.d.ts | 4 +++- 13 files changed, 50 insertions(+), 19 deletions(-) diff --git a/ErsatzTV.Application/MediaCollections/Commands/CreateMultiCollection.cs b/ErsatzTV.Application/MediaCollections/Commands/CreateMultiCollection.cs index a3830c13e..e49df679b 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/CreateMultiCollection.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/CreateMultiCollection.cs @@ -7,7 +7,8 @@ public record CreateMultiCollectionItem( int? CollectionId, int? SmartCollectionId, bool ScheduleAsGroup, - PlaybackOrder PlaybackOrder); + PlaybackOrder PlaybackOrder, + int Weight = 1); public record CreateMultiCollection(string Name, List Items) : IRequest>; diff --git a/ErsatzTV.Application/MediaCollections/Commands/CreateMultiCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/CreateMultiCollectionHandler.cs index 9410ca8c2..cf0bd7ded 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/CreateMultiCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/CreateMultiCollectionHandler.cs @@ -63,7 +63,8 @@ public class CreateMultiCollectionHandler : { CollectionId = i.CollectionId.Value, ScheduleAsGroup = i.ScheduleAsGroup, - PlaybackOrder = i.PlaybackOrder + PlaybackOrder = i.PlaybackOrder, + Weight = i.Weight }); } @@ -79,7 +80,8 @@ public class CreateMultiCollectionHandler : { SmartCollectionId = i.SmartCollectionId.Value, ScheduleAsGroup = i.ScheduleAsGroup, - PlaybackOrder = i.PlaybackOrder + PlaybackOrder = i.PlaybackOrder, + Weight = i.Weight }); } diff --git a/ErsatzTV.Application/MediaCollections/Commands/UpdateMultiCollection.cs b/ErsatzTV.Application/MediaCollections/Commands/UpdateMultiCollection.cs index 911347c6f..9571ee345 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/UpdateMultiCollection.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/UpdateMultiCollection.cs @@ -7,7 +7,8 @@ public record UpdateMultiCollectionItem( int? CollectionId, int? SmartCollectionId, bool ScheduleAsGroup, - PlaybackOrder PlaybackOrder); + PlaybackOrder PlaybackOrder, + int Weight = 1); public record UpdateMultiCollection( int MultiCollectionId, diff --git a/ErsatzTV.Application/MediaCollections/Commands/UpdateMultiCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/UpdateMultiCollectionHandler.cs index 0d4abcb53..dde2c7fd1 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/UpdateMultiCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/UpdateMultiCollectionHandler.cs @@ -1,4 +1,4 @@ -using System.Threading.Channels; +using System.Threading.Channels; using ErsatzTV.Application.Playouts; using ErsatzTV.Core; using ErsatzTV.Core.Domain; @@ -76,7 +76,8 @@ public class UpdateMultiCollectionHandler : IRequestHandler @@ -78,7 +79,8 @@ internal static class Mapper multiCollectionSmartItem.MultiCollectionId, ProjectToViewModel(multiCollectionSmartItem.SmartCollection), multiCollectionSmartItem.ScheduleAsGroup, - multiCollectionSmartItem.PlaybackOrder); + multiCollectionSmartItem.PlaybackOrder, + multiCollectionSmartItem.Weight); internal static TreeViewModel ProjectToViewModel(List playlistGroups) => new( diff --git a/ErsatzTV.Application/MediaCollections/MultiCollectionItemViewModel.cs b/ErsatzTV.Application/MediaCollections/MultiCollectionItemViewModel.cs index 146c581da..41367cada 100644 --- a/ErsatzTV.Application/MediaCollections/MultiCollectionItemViewModel.cs +++ b/ErsatzTV.Application/MediaCollections/MultiCollectionItemViewModel.cs @@ -6,4 +6,5 @@ public record MultiCollectionItemViewModel( int MultiCollectionId, MediaCollectionViewModel Collection, bool ScheduleAsGroup, - PlaybackOrder PlaybackOrder); + PlaybackOrder PlaybackOrder, + int Weight = 1); diff --git a/ErsatzTV.Application/MediaCollections/MultiCollectionSmartItemViewModel.cs b/ErsatzTV.Application/MediaCollections/MultiCollectionSmartItemViewModel.cs index 4f733de0b..047504bc2 100644 --- a/ErsatzTV.Application/MediaCollections/MultiCollectionSmartItemViewModel.cs +++ b/ErsatzTV.Application/MediaCollections/MultiCollectionSmartItemViewModel.cs @@ -6,4 +6,5 @@ public record MultiCollectionSmartItemViewModel( int MultiCollectionId, SmartCollectionViewModel SmartCollection, bool ScheduleAsGroup, - PlaybackOrder PlaybackOrder); + PlaybackOrder PlaybackOrder, + int Weight = 1); diff --git a/ErsatzTV.Core/Api/MediaCollections/MultiCollectionResponseModel.cs b/ErsatzTV.Core/Api/MediaCollections/MultiCollectionResponseModel.cs index 4117e95cc..b4c5dbe3a 100644 --- a/ErsatzTV.Core/Api/MediaCollections/MultiCollectionResponseModel.cs +++ b/ErsatzTV.Core/Api/MediaCollections/MultiCollectionResponseModel.cs @@ -13,7 +13,8 @@ public record MultiCollectionItemResponseModel( int? SmartCollectionId, string Name, bool ScheduleAsGroup, - PlaybackOrder PlaybackOrder); + PlaybackOrder PlaybackOrder, + int Weight = 1); public record PagedMultiCollectionsResponseModel( int TotalCount, diff --git a/ErsatzTV/Controllers/Api/MultiCollectionController.cs b/ErsatzTV/Controllers/Api/MultiCollectionController.cs index 2422334c5..21796e795 100644 --- a/ErsatzTV/Controllers/Api/MultiCollectionController.cs +++ b/ErsatzTV/Controllers/Api/MultiCollectionController.cs @@ -136,13 +136,15 @@ public class MultiCollectionController(IMediator mediator) : ControllerBase null, i.Collection.Name, i.ScheduleAsGroup, - i.PlaybackOrder)) + i.PlaybackOrder, + i.Weight)) .Concat(vm.SmartItems.Map(i => new MultiCollectionItemResponseModel( null, i.SmartCollection.Id, i.SmartCollection.Name, i.ScheduleAsGroup, - i.PlaybackOrder))) + i.PlaybackOrder, + i.Weight))) .ToList(); return new MultiCollectionResponseModel(vm.Id, vm.Name, items); diff --git a/ErsatzTV/Controllers/Api/Requests/CreateMultiCollectionRequest.cs b/ErsatzTV/Controllers/Api/Requests/CreateMultiCollectionRequest.cs index 5d655fd2d..783d08fec 100644 --- a/ErsatzTV/Controllers/Api/Requests/CreateMultiCollectionRequest.cs +++ b/ErsatzTV/Controllers/Api/Requests/CreateMultiCollectionRequest.cs @@ -9,7 +9,8 @@ public record MultiCollectionItemRequest( int? CollectionId, int? SmartCollectionId, bool ScheduleAsGroup, - PlaybackOrder PlaybackOrder); + PlaybackOrder PlaybackOrder, + int Weight = 1); public record CreateMultiCollectionRequest(string Name, List Items) { @@ -21,6 +22,7 @@ public record CreateMultiCollectionRequest(string Name, List Date: Fri, 17 Jul 2026 12:42:02 +0200 Subject: [PATCH 06/12] docs(70): record the WeightedShuffle design and correct the ShuffleInOrder misreading MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit decisions.md entry covers why one enum value rather than two or a separate setting, why the weight lives on both multi-collection join entities with a DB default of 1, why it can only be applied on the ShuffleInOrder-shaped path (source identity is destroyed on the Shuffle path), why it is stateless, and why cross-engine exposure is closed by write-path validation instead of by changing five shipped fallbacks. The load-bearing part is the ShuffleInOrder clarification. Its balanced-shuffle padding reads as equalization, but the spacers emit nothing — one cycle plays every item once, so airtime stays proportional to collection size. It is anti-clumping, not fair-share. That was misread during this issue's own design pass and would have collapsed #70 to "already shipped"; the distinction is the whole justification for the feature, so it is now written down in both decisions.md and the domain-model glossary rather than left to be rediscovered. domain-model.md gains a Playback order row contrasting Shuffle / ShuffleInOrder / WeightedShuffle, and notes on the multi-collection row that the two join entities are mirrors. Refs #70 Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/decisions.md | 68 ++++++++++++++++++++++++++++++++++++++++++++ docs/domain-model.md | 3 +- 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/docs/decisions.md b/docs/decisions.md index 7d135deec..28104a56d 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -89,6 +89,7 @@ in-file entries. - [2026-07-17 — Auto-Tune DetailPanel member list = live search-index roll-up, not EF enumeration (#384)](#2026-07-17--auto-tune-detailpanel-member-list--live-search-index-roll-up-not-ef-enumeration-384) - [2026-07-17 — No persistent compiler servers in CI; every `services:` container gets an explicit cap; #390's small-lane move reversed (#406)](#2026-07-17--no-persistent-compiler-servers-in-ci-every-services-container-gets-an-explicit-cap-390s-small-lane-move-reversed-406) - [2026-07-17 — Channel health on the API = the raw `PlayoutCount` fact on the list DTO, not a derived status enum (#72)](#2026-07-17--channel-health-on-the-api--the-raw-playoutcount-fact-on-the-list-dto-not-a-derived-status-enum-72) +- [2026-07-17 — Weighted / fair-share distribution is a new `WeightedShuffle` order; `ShuffleInOrder` is anti-clumping, not fair-share (#70)](#2026-07-17--weighted--fair-share-distribution-is-a-new-weightedshuffle-order-shuffleinorder-is-anti-clumping-not-fair-share-70) --- @@ -1387,3 +1388,70 @@ Keeping all three out held #72 to a **read-path-only** change: no migration, no channel editor's playout-source guard. Both call sites now share `Mapper.GetPlayoutsCount` (Mirror-aware). The related *silent* server-side coercion of Mirror→Generated (a 200 that discards the caller's intent, against the §3 "surface it, don't silently filter" rule) is filed as **#401**, not fixed here. +## 2026-07-17 — Weighted / fair-share distribution is a new `WeightedShuffle` order; `ShuffleInOrder` is anti-clumping, not fair-share (#70) + +`PlaybackOrder.WeightedShuffle = 9` ships the two behaviors #70 asked for — "air Show A 70% / Show B 30%" +and "no single show dominates" — as **one** order, because fair-share is the equal-weights case of weighted. + +- **`ShuffleInOrder` does NOT already do fair-share, despite looking like it.** Its balanced shuffle + (keyj) pads every source to the longest with `Option.None` spacers — but **spacers emit + nothing** (`ShuffleInOrderCollectionEnumerator.cs`, `result.AddRange(maybeItem)` over an `Option`). One + cycle therefore plays **every item exactly once**, so a 200-episode show still takes 10× the airtime of a + 20-episode one. What it buys is **anti-clumping**: the small show is spread evenly instead of arriving in + bursts, and each show plays chronologically. That is a different product from "airs as often". Recorded + because the padding reads as equalization and this was misread once during #70's own design pass — the + distinction is the entire justification for the issue. +- **Plain `Shuffle` is already implicitly weighted by collection size** (Fisher–Yates over flattened items), + which is why "just use Shuffle" isn't fair-share either. +- **One new enum value, not two, and not a separate setting.** Fair-share = `WeightedShuffle` with weights + left at their default of 1, so the UI's "fair-share toggle" needs no second mechanism. A separate + non-enum "distribution" setting was rejected: it would add an orthogonal axis every `PlaybackOrder` + dispatch site must *also* consult, multiplying the blast radius below. Retrofitting weights onto + `ShuffleInOrder` was rejected: it would silently change shipped users' output. +- **Weights live on `MultiCollectionItem` AND `MultiCollectionSmartItem`** (`int Weight`, DB default 1, + dual-provider migration `AddMultiCollectionItemWeight`). The mirror is mandatory — omitting the smart + item un-weights smart-collection members silently. The DB default must be **1, not 0**: a 0-weight source + is filtered out by the enumerator, so a 0 backfill would drop every pre-existing member from a rotation. + Rejected carriers: `PlaylistItem` (sequential engine; its `Count`/`PlayAll` already mean "how much of this + source", and drain-N-consecutively `A A A B` is a different product from smooth interleave `A A B A`); + `CollectionItem` (wrong grain — per media item, and the largest table). +- **Applied on the `ShuffleInOrder`-shaped path only, because that is where source identity survives.** + `MultiCollectionGrouper` collapses sources into `GroupedMediaItem` + `.Distinct()`, so by the time the + `Shuffle` path has its list, *which source an item came from* is gone. `CollectionWithItems` gains + `int Weight = 1`; `WeightedShuffleCollectionEnumerator` consumes + `ShuffleSourceBuilder.GetCollectionItemsForShuffleInOrder` **unchanged** — the schedule-entity-free entry + point #380 reserved for this issue (no signature change, no god-factory, engines stay separate). +- **Stateless.** Smooth weighted round-robin (`acc += weight`; richest wins; pays the total) is a pure + function of (`Seed`, `Index`), so it restores by replay like its siblings and needs no per-source counters + (`CollectionEnumeratorState` has nowhere to put them). A rotation is sized so the source needing the most + picks works through all its items at its share; smaller sources loop inside it — that looping is what makes + equal weights mean equal airtime. **Weight ratios are data, not state**: a `Reset` build re-randomizes + `Playout.Seed` (fresh within-source shuffle and start point) but the ratios hold identically. +- **Ties break to the earliest source in list order** (strict `>`), keeping the sequence deterministic. +- **Within-source order is shuffled, not chronological** (custom-ordered collections are still honored). + Chronological inner order would make every rotation byte-identical, since source selection is + deterministic — the reseed on wrap would change nothing and the avoid-an-immediate-repeat retry could + never succeed. "Weighted **in order**" would be a separate order if wanted. +- **Cross-engine exposure is closed by write-path validation, not by touching the silent fallbacks.** An + unhandled `PlaybackOrder` fails **silently at five of six dispatch sites**: Classic substitutes + `RandomizedMediaCollectionEnumerator` (`PlayoutBuilder`'s `default:`, `// TODO: handle this error case + differently?`); `PlaylistEnumerator` has **no `default:` arm**, so the enumerator stays null and the item is + dropped from the playlist (and null is *legitimate* there for `SeasonEpisode` with `Count == 0`, so nothing + flags it); `BlockPlayoutBuilder` filters block items against an allow-list and `continue`s past the rest; + YAML/Scripted return `Option.None`, which their callers read as "no content". Only `MultiCollectionGroup` + throws. **A weighted order degrading to unweighted random is the worst case, because the output is supposed + to look arbitrary.** So `ReplacePlaylistItems`/`ReplaceBlockItems` **reject** `WeightedShuffle` at the write + path — if it can't be persisted where it isn't handled, the silent sites never see it — and YAML/Scripted + (which address orders by *name*, so `Enum.Parse` accepts it regardless) log a warning when an order falls + through unhandled. Making those pre-existing fallbacks loud is a real but separate defect class, filed as a + follow-up and an explicit non-goal here. +- **Weight is returned on the multi-collection GET, not just accepted on write.** The update replaces the + item list, so a client that reads, edits a name, and writes back would silently reset every weight to the + default if the read didn't carry it. Edits ride the existing `Version` token (If-Match/412, §7a). +- **Scope:** Classic engine only; no SPA (the schedule-editor weight UI is gated on #388). Plain + single/smart collections (the `GroupIntoFakeCollections` path) carry no persisted weights — fair-share + applies there by construction, and per-source weighting requires a multi collection. Auto-tune's + per-member weights (#385/#386) need their own carrier: an auto-tuned channel is backed by one live + SmartCollection, so its "sources" are members of a single collection, and a `PlaylistItem` cannot even + reference a search query. Nothing here forecloses it — the enumerator reads weights off + `CollectionWithItems` source-agnostically. diff --git a/docs/domain-model.md b/docs/domain-model.md index fbeb70f68..7ad86c651 100644 --- a/docs/domain-model.md +++ b/docs/domain-model.md @@ -66,10 +66,11 @@ Channel (1) ──< Playout (0..N per channel; ChannelPlayoutSource distinguishe | **FillerPreset** | A reusable filler definition: `FillerKind` (PreRoll/MidRoll/PostRoll/Tail/Fallback; also `GuideMode=99`, `DecoDefault=100`) × `FillerMode` (None/Duration/Count/Pad/RandomCount) over a collection/media-item/multi-collection/smart-collection/playlist source, with an optional `Expression` (NCalc). Referenced from `ProgramScheduleItem` (Pre/Mid/Post/Tail/FallbackFillerId) and `Channel.FallbackFillerId`. | `FillerPreset`, `FillerKind`, `FillerMode` | `/app/filler-presets` | | **Clock-boundary padding** (#77) | Snapping a schedule to clean `:00/:15/:30`-style guide times is **not a separate feature** — it is `FillerMode.Pad` + `PadToNearestMinute` on a `FillerPreset` (Classic), or the `pad_to_next`/`pad_until` YAML instructions (Sequential). Block playouts are inherently clock-anchored via `TemplateItem.StartTime`. The EPG reflects the padded boundary automatically (`ChannelGuideProjector` coalesces trailing filler into the programme window). No one-click per-channel toggle yet — deferred to a UI follow-up blocked on #388. See `decisions.md` 2026-07-17. | `FillerPreset` (`Pad`) | `/app/filler-presets` | | **Seasonal / date-conditional scheduling** (#73) | Holiday/seasonal channels are **not a separate feature** — they are the existing date predicate on `IAlternateScheduleItem`, implemented by `ProgramScheduleAlternate` (Classic) and `PlayoutTemplate` (Block), evaluated by `AlternateScheduleSelector.GetScheduleForDate` (first match by `Index`, catch-all last). **Leaving `StartYear`/`EndYear` empty makes the range repeat every year** — the "set once, works every December" switch; explicit years (required in pairs) mean a one-off window and disable wrap-around detection. Wrap-around (Nov→Feb) and invalid/leap dates (Feb 31) are handled. No *soft* prioritization primitive exists (binary first-match-wins); that ask belongs to #70's weighting work. See `channels.md` → "Recipe: seasonal / holiday programming" and `decisions.md` 2026-07-17. | `IAlternateScheduleItem`, `ProgramScheduleAlternate`, `PlayoutTemplate` | `/app/playouts/{id}/alternate-schedules`, `/app/playouts/{id}/templates` | +| **Playback order** | How a schedule item's source(s) are sequenced (`PlaybackOrder`). Note three that are easily confused: **`Shuffle`** is Fisher–Yates over the flattened items, so airtime is implicitly proportional to collection size (a 200-episode show swamps a 20-episode one). **`ShuffleInOrder`** is a balanced shuffle (keyj) that pads sources to equal length with non-emitting spacers — it plays every item exactly once per cycle, so it prevents *clumping* but leaves airtime proportional to size; it is **not** fair-share. **`WeightedShuffle`** (#70) picks a *source* by smooth weighted round-robin then takes its next item, so each source's `Weight` is its share of airtime — equal weights (the default) mean equal airtime regardless of library size, with small sources looping. Classic engine only; rejected at the write path for playlist/block items. See `decisions.md` 2026-07-17. | `PlaybackOrder`, `MultiCollectionItem.Weight`, `MultiCollectionSmartItem.Weight` | `/app/schedules` (weight UI pending, gated on #388) | | **Watermark** | `ChannelWatermark` image overlay; attached at channel, schedule-item, block-item, deco, or playout-item level with position/size/opacity. | `ChannelWatermark`, `DecoWatermark`, `BlockItemWatermark`, `ProgramScheduleItemWatermark` | `/app/watermarks` | | **Collection** | Manual list of media items (`CollectionItem`). | `Collection` | `/app/collections` | | **SmartCollection** | Saved search — a `Query` string, no static item list. | `SmartCollection` | `/app/collections` | -| **MultiCollection** | Combines multiple `Collection`s and/or `SmartCollection`s (with grouping via `MultiCollectionItem`/`MultiCollectionSmartItem`). | `MultiCollection` | `/app/multi-collections` (#151) | +| **MultiCollection** | Combines multiple `Collection`s and/or `SmartCollection`s (with grouping via `MultiCollectionItem`/`MultiCollectionSmartItem`). Both join entities carry a per-source `Weight` (default 1) used by `PlaybackOrder.WeightedShuffle` (#70) and ignored by every other order — the two are mirrors, so a change to one belongs on the other. | `MultiCollection` | `/app/multi-collections` (#151) | | **RerunCollection** | One source (collection/media item/multi/smart) with separate `FirstRunPlaybackOrder` vs `RerunPlaybackOrder`. | `RerunCollection` | `/app/rerun-collections` (#152) | | **Playlist** / **PlaylistGroup** | Ordered `PlaylistItem`s; `IsSystem` flag marks built-in/non-deletable playlists and groups. | `Playlist`, `PlaylistGroup`, `PlaylistItem` | `/app/playlists` | | **Media kinds** | `CollectionType` enum distinguishes container kinds (Collection/TelevisionShow/TelevisionSeason/Artist/MultiCollection/SmartCollection/Playlist/RerunFirstRun/RerunRerun/SearchQuery) from leaf media kinds (Movie/Episode/MusicVideo/OtherVideo/Song/Image/RemoteStream) plus synthetic `FakeCollection`/`FakePlaylistItem`. Concrete media entities: `Movie`, `Show`/`Season`/`Episode`, `Artist`/`MusicVideo`/`Song`, `OtherVideo`, `Image`, `RemoteStream` (`ErsatzTV.Core/Domain/MediaItem/`). | `MediaItem` subclasses | `/app/media?kind=...` | -- 2.47.3 From 2ef7cb3fdfe5a7b144e7a78a6ad9e512bf011dd1 Mon Sep 17 00:00:00 2001 From: Timothy Date: Fri, 17 Jul 2026 13:05:24 +0200 Subject: [PATCH 07/12] =?UTF-8?q?fix(70):=20close=20the=20review=20blocker?= =?UTF-8?q?s=20=E2=80=94=20third=20playlist=20writer,=20weight=20bounds,?= =?UTF-8?q?=20overflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adversarial review of PR #402 returned BLOCKED. It could not break the WRR math or the stateless-restore claim (it probed restore across wraps at indices 12/13/20/37 — all held, and the clamp preserves a 1000:1 ratio exactly). What it broke was the perimeter. B1 — the validation gate had a hole, so the silent-drop bug shipped. CreateChannelFromLineup is a THIRD writer of PlaylistItem.PlaybackOrder; its own guard only covered MultiCollection entries, so a 2+ entry lineup of plain collections persisted WeightedShuffle straight through to PlaylistEnumerator's null-drop. My decisions.md claim that "the silent sites never see it" was false as written — corrected in place, with the lesson recorded: grep every writer of the field, the non-obvious composite handler is the one that gets missed. The Add*ToPlaylist handlers are safe only because they hardcode their order. B2 — Weight had no validation at all, and create/update disagreed on the same input. EF's HasDefaultValue(1) substitutes 1 for a 0 on INSERT (0 reads as "not set") but an UPDATE writes the 0 through — and a 0-weight source was filtered out of the rotation, deleting it from the channel silently. Exactly the failure this order is careful to avoid everywhere else. Now bounded 1..1000 by a shared MultiCollectionItemWeight used by both paths so they cannot drift, and clamped again in the enumerator for rows that predate the gate. B3 — Sum(weights) is checked arithmetic, so two int.MaxValue weights threw OverflowException from inside a playout build. Reachable through the API precisely because of B2. The ceiling fixes both; the sum also widens to long. M1 the lineup mirror now allows WeightedShuffle for multi collections, matching the PlayoutModeMustBeValid change it claims to mirror. M3 ScheduleAsGroup is documented as deliberately unread by this order. L1 MinimumDuration is computed over every source instead of the current rotation — under the clamp a rotation is a strict subset and is rebuilt each wrap, so caching over it went stale. L2 the retry guard keys off the rotation, not the raw collection count. N1 the tautological default test is gone: it built entities in C#, so it asserted the property initializer, not the migration — it could not have failed. Replaced with clamp, overflow, and cross-wrap restore cases (the property the review proved but found unpinned). H1 the two follow-ups the PR body claimed were "filed" did not exist. Now filed: #403 (silent dispatch-fallback hardening) and #404 (SPA weight UI, blocked-by #388). Core.Tests 565 passed, ErsatzTV.Tests 1643 passed, 0 failed. Refs #70 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../CreateChannelFromLineupHandler.cs | 18 +++- .../Commands/CreateMultiCollectionHandler.cs | 11 ++- .../Commands/UpdateMultiCollectionHandler.cs | 15 +++- ...eightedShuffleCollectionEnumeratorTests.cs | 86 +++++++++++++++++-- .../Collection/MultiCollectionItemWeight.cs | 27 ++++++ .../WeightedShuffleCollectionEnumerator.cs | 31 +++++-- .../CreateChannelFromLineupHandlerTests.cs | 41 +++++++++ docs/decisions.md | 27 ++++-- 8 files changed, 232 insertions(+), 24 deletions(-) create mode 100644 ErsatzTV.Core/Domain/Collection/MultiCollectionItemWeight.cs diff --git a/ErsatzTV.Application/Channels/Commands/CreateChannelFromLineupHandler.cs b/ErsatzTV.Application/Channels/Commands/CreateChannelFromLineupHandler.cs index 4127dc10f..39fede040 100644 --- a/ErsatzTV.Application/Channels/Commands/CreateChannelFromLineupHandler.cs +++ b/ErsatzTV.Application/Channels/Commands/CreateChannelFromLineupHandler.cs @@ -1,4 +1,4 @@ -using System.Globalization; +using System.Globalization; using System.Text.RegularExpressions; using System.Threading.Channels; using ErsatzTV.Application.Playouts; @@ -197,13 +197,25 @@ public class CreateChannelFromLineupHandler( bool multiItem = normalized.Count >= 2; - // MultiCollection entries only support Shuffle / ShuffleInOrder (mirrors PlayoutModeMustBeValid). + // MultiCollection entries only support Shuffle / ShuffleInOrder / WeightedShuffle + // (mirrors PlayoutModeMustBeValid -- keep the two lists in step). if (normalized.Any(i => i.CollectionType is CollectionType.MultiCollection) && - playbackOrder is not (PlaybackOrder.Shuffle or PlaybackOrder.ShuffleInOrder)) + playbackOrder is not (PlaybackOrder.Shuffle or PlaybackOrder.ShuffleInOrder + or PlaybackOrder.WeightedShuffle)) { return BaseError.New($"Invalid playback order for multi collection: '{playbackOrder}'"); } + // A lineup of 2+ entries is persisted as a Playlist, and PlaylistEnumerator has no default arm: an + // order it doesn't know leaves the enumerator null and the items are dropped from the playlist with + // nothing reported. This is the third writer of PlaylistItem.PlaybackOrder, alongside + // ReplacePlaylistItems and ReplaceBlockItems (#70; the silent fallbacks themselves are #403). + if (multiItem && playbackOrder is PlaybackOrder.WeightedShuffle) + { + return BaseError.New( + $"Playback order '{playbackOrder}' is not supported for a multi-item lineup; it is available on classic schedule items"); + } + if (multiItem) { // The generated playlist cannot express rerun collections or nested playlists diff --git a/ErsatzTV.Application/MediaCollections/Commands/CreateMultiCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/CreateMultiCollectionHandler.cs index cf0bd7ded..c4ad1ba42 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/CreateMultiCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/CreateMultiCollectionHandler.cs @@ -51,7 +51,9 @@ public class CreateMultiCollectionHandler : private static Task> Validate( TvContext dbContext, CreateMultiCollection request) => - ValidateName(dbContext, request).MapT(name => new MultiCollection + ValidateName(dbContext, request) + .BindT(name => ValidateWeights(request).Map(_ => name)) + .MapT(name => new MultiCollection { Name = name, MultiCollectionItems = request.Items.Bind(i => @@ -90,6 +92,13 @@ public class CreateMultiCollectionHandler : .ToList() }); + // Bounds are shared with the update path so the two cannot drift: an out-of-range weight silently + // removes the source from the rotation (0) or overflows the rotation arithmetic (huge). See #70. + private static Validation ValidateWeights(CreateMultiCollection request) => + request.Items.All(i => MultiCollectionItemWeight.IsValid(i.Weight)) + ? Unit.Default + : BaseError.New(MultiCollectionItemWeight.ValidationMessage); + private static async Task> ValidateName( TvContext dbContext, CreateMultiCollection createMultiCollection) diff --git a/ErsatzTV.Application/MediaCollections/Commands/UpdateMultiCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/UpdateMultiCollectionHandler.cs index dde2c7fd1..7149c7e86 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/UpdateMultiCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/UpdateMultiCollectionHandler.cs @@ -160,8 +160,19 @@ public class UpdateMultiCollectionHandler : IRequestHandler - (await MultiCollectionMustExist(dbContext, request, cancellationToken), await ValidateName(dbContext, request)) - .Apply((collectionToUpdate, _) => collectionToUpdate); + (await MultiCollectionMustExist(dbContext, request, cancellationToken), + await ValidateName(dbContext, request), + ValidateWeights(request)) + .Apply((collectionToUpdate, _, _) => collectionToUpdate); + + // Bounds are shared with the create path so the two cannot drift. This gate matters more here than on + // create: EF's HasDefaultValue substitutes 1 for a 0 on INSERT (0 reads as "not set"), but an UPDATE + // writes the 0 through -- and a 0-weight source is filtered out of the rotation, deleting it from the + // channel with nothing reported. See #70. + private static Validation ValidateWeights(UpdateMultiCollection request) => + request.Items.All(i => MultiCollectionItemWeight.IsValid(i.Weight)) + ? Unit.Default + : BaseError.New(MultiCollectionItemWeight.ValidationMessage); private static Task> MultiCollectionMustExist( TvContext dbContext, diff --git a/ErsatzTV.Core.Tests/Scheduling/WeightedShuffleCollectionEnumeratorTests.cs b/ErsatzTV.Core.Tests/Scheduling/WeightedShuffleCollectionEnumeratorTests.cs index 50cd73919..25e2a263e 100644 --- a/ErsatzTV.Core.Tests/Scheduling/WeightedShuffleCollectionEnumeratorTests.cs +++ b/ErsatzTV.Core.Tests/Scheduling/WeightedShuffleCollectionEnumeratorTests.cs @@ -114,19 +114,15 @@ public class WeightedShuffleCollectionEnumeratorTests } [Test] - public void Weight_Defaults_To_One_So_An_Unweighted_Collection_Is_Fair_Share() + public void An_Unweighted_Collection_Is_Fair_Share() { - // guards the migration default: an existing row that arrives without a weight must - // behave as fair-share, never as a dropped source + // a source arriving without an explicit weight must rotate as fair-share, never fall out var collections = new List { new(0, 0, "A", [new Movie { Id = SourceAFirstId, MovieMetadata = [] }], true, PlaybackOrder.WeightedShuffle, false), new(0, 0, "B", [new Movie { Id = SourceBFirstId, MovieMetadata = [] }], true, PlaybackOrder.WeightedShuffle, false) }; - collections[0].Weight.ShouldBe(1); - collections[1].Weight.ShouldBe(1); - var enumerator = new WeightedShuffleCollectionEnumerator( collections, new CollectionEnumeratorState { Seed = 7, Index = 0 }, @@ -135,6 +131,84 @@ public class WeightedShuffleCollectionEnumeratorTests TakeSourceSequence(enumerator, 4).ShouldBe(["A", "B", "A", "B"]); } + [Test] + [TestCase(0)] + [TestCase(-5)] + public void A_Non_Positive_Weight_Does_Not_Delete_The_Source(int weight) + { + // the write path bounds weight, but a row can predate that gate. Treating a 0/negative weight as a + // filter would remove the source from the channel silently -- the exact failure this order avoids + // everywhere else. It is clamped to the floor instead. + var collections = new List + { + Source("A", SourceAFirstId, 2, 1), + Source("B", SourceBFirstId, 2, weight) + }; + + var enumerator = new WeightedShuffleCollectionEnumerator( + collections, + new CollectionEnumeratorState { Seed = 21, Index = 0 }, + CancellationToken.None); + + TakeSourceSequence(enumerator, 8).ShouldContain("B"); + } + + [Test] + public void An_Enormous_Weight_Does_Not_Overflow_The_Rotation() + { + // summing unclamped weights is checked arithmetic, so an out-of-range row would throw from inside a + // playout build rather than merely schedule oddly + var collections = new List + { + Source("A", SourceAFirstId, 2, int.MaxValue), + Source("B", SourceBFirstId, 2, int.MaxValue) + }; + + WeightedShuffleCollectionEnumerator enumerator = null; + Should.NotThrow(() => enumerator = new WeightedShuffleCollectionEnumerator( + collections, + new CollectionEnumeratorState { Seed = 5, Index = 0 }, + CancellationToken.None)); + + // both clamp to the ceiling, so they tie and alternate by list order + TakeSourceSequence(enumerator, 4).ShouldBe(["A", "B", "A", "B"]); + } + + [Test] + [TestCase(12)] + [TestCase(13)] + [TestCase(20)] + [TestCase(37)] + public void Restoring_Past_A_Rotation_Wrap_Equals_Advancing_To_It(int target) + { + // the stateless claim has to hold ACROSS a wrap, not just within the first rotation: each wrap + // re-derives the rotation from the new seed alone, so (Seed, Index) still determines position. + // Cycle length here is 12, so every case but the first crosses at least one wrap. + List Collections() => + [ + Source("A", SourceAFirstId, 5, 3), + Source("B", SourceBFirstId, 3, 1) + ]; + + const int Seed = 909; + + var advanced = new WeightedShuffleCollectionEnumerator( + Collections(), + new CollectionEnumeratorState { Seed = Seed, Index = 0 }, + CancellationToken.None); + for (var i = 0; i < target; i++) + { + advanced.MoveNext(Option.None); + } + + var restored = new WeightedShuffleCollectionEnumerator( + Collections(), + new CollectionEnumeratorState { Seed = advanced.State.Seed, Index = advanced.State.Index }, + CancellationToken.None); + + restored.Current.ValueUnsafe().Id.ShouldBe(advanced.Current.ValueUnsafe().Id); + } + [Test] public void Restoring_At_An_Index_Equals_Advancing_To_It() { diff --git a/ErsatzTV.Core/Domain/Collection/MultiCollectionItemWeight.cs b/ErsatzTV.Core/Domain/Collection/MultiCollectionItemWeight.cs new file mode 100644 index 000000000..b1b91d5fc --- /dev/null +++ b/ErsatzTV.Core/Domain/Collection/MultiCollectionItemWeight.cs @@ -0,0 +1,27 @@ +namespace ErsatzTV.Core.Domain; + +/// +/// Bounds for a multi-collection member's per-source Weight (#70). +/// +/// The floor is 1, not 0. A source with a weight of 0 is filtered out of the rotation by +/// WeightedShuffleCollectionEnumerator, so accepting one would let a caller delete a source from a +/// channel with nothing reported — the silent failure this order is otherwise careful to avoid. "Don't play +/// this source" is expressed by removing it from the multi collection, not by weighting it to nothing. +/// +/// +/// The ceiling keeps the rotation arithmetic in range: the enumerator sums weights and scales the rotation +/// by items * totalWeight / weight, and unbounded values overflow that sum (checked arithmetic, so it +/// throws) inside a playout build. +/// +/// +public static class MultiCollectionItemWeight +{ + public const int Minimum = 1; + public const int Maximum = 1000; + public const int Default = 1; + + public static bool IsValid(int weight) => weight is >= Minimum and <= Maximum; + + public static string ValidationMessage => + $"Weight must be between {Minimum} and {Maximum}"; +} diff --git a/ErsatzTV.Core/Scheduling/WeightedShuffleCollectionEnumerator.cs b/ErsatzTV.Core/Scheduling/WeightedShuffleCollectionEnumerator.cs index 1de9992ac..c32f4d9e3 100644 --- a/ErsatzTV.Core/Scheduling/WeightedShuffleCollectionEnumerator.cs +++ b/ErsatzTV.Core/Scheduling/WeightedShuffleCollectionEnumerator.cs @@ -66,9 +66,15 @@ public class WeightedShuffleCollectionEnumerator : IMediaCollectionEnumerator _random = new Random(state.Seed); _shuffled = Build(_collections, _random); + // computed over every source's items, not the current rotation: a rotation can be a strict subset + // (see MaxCycleLength) and is rebuilt on every wrap, so caching over it would go stale _lazyMinimumDuration = new Lazy>(() => - _shuffled.Bind(i => i.GetNonZeroDuration()).OrderBy(identity).HeadOrNone()); + _collections + .Bind(c => c.MediaItems) + .Bind(i => i.GetNonZeroDuration()) + .OrderBy(identity) + .HeadOrNone()); State = new CollectionEnumeratorState { Seed = state.Seed }; while (State.Index < state.Index) @@ -117,8 +123,11 @@ public class WeightedShuffleCollectionEnumerator : IMediaCollectionEnumerator _random = new Random(State.Seed); _shuffled = Build(_collections, _random); attempts++; - } while (!_cancellationToken.IsCancellationRequested && _collections.Count > 1 && - _shuffled.Length > 0 && attempts < MaxReshuffleAttempts && + // guard on the rotation, not the raw collection count: an empty source contributes nothing, + // so a 2-collection/1-item rotation would otherwise burn every attempt chasing an + // impossible non-repeat on every wrap + } while (!_cancellationToken.IsCancellationRequested && _shuffled.Length > 1 && + attempts < MaxReshuffleAttempts && Current.Map(x => x.Id) == tail.Map(x => x.Id)); } else @@ -149,7 +158,7 @@ public class WeightedShuffleCollectionEnumerator : IMediaCollectionEnumerator return 0; } - int totalWeight = active.Sum(c => c.Weight); + long totalWeight = active.Sum(c => (long)EffectiveWeight(c)); var length = 0; foreach (CollectionWithItems collection in active) @@ -157,7 +166,7 @@ public class WeightedShuffleCollectionEnumerator : IMediaCollectionEnumerator // picks needed for this source to emit every item once, given it wins Weight of every totalWeight picks var needed = (int)Math.Min( MaxCycleLength, - Math.Ceiling(collection.MediaItems.Count * (double)totalWeight / collection.Weight)); + Math.Ceiling(collection.MediaItems.Count * (double)totalWeight / EffectiveWeight(collection))); length = Math.Max(length, needed); } @@ -165,7 +174,15 @@ public class WeightedShuffleCollectionEnumerator : IMediaCollectionEnumerator } private static List ActiveSources(IList collections) => - collections.Filter(c => c.MediaItems.Count > 0 && c.Weight > 0).ToList(); + collections.Filter(c => c.MediaItems.Count > 0).ToList(); + + /// + /// Weight as the rotation should treat it. The write path bounds this (), + /// but a row can predate that gate, so it is clamped rather than trusted: an out-of-range value must not + /// drop a source from the channel (0 or negative) or overflow the weight sum inside a playout build. + /// + private static int EffectiveWeight(CollectionWithItems collection) => + Math.Clamp(collection.Weight, MultiCollectionItemWeight.Minimum, MultiCollectionItemWeight.Maximum); private static MediaItem[] Build(IList collections, Random random) { @@ -176,7 +193,7 @@ public class WeightedShuffleCollectionEnumerator : IMediaCollectionEnumerator } var sources = active - .Map(c => new WeightedSource { Weight = c.Weight, Items = OrderItems(c, random) }) + .Map(c => new WeightedSource { Weight = EffectiveWeight(c), Items = OrderItems(c, random) }) .ToList(); int totalWeight = sources.Sum(s => s.Weight); diff --git a/ErsatzTV.Tests/Application/Channels/CreateChannelFromLineupHandlerTests.cs b/ErsatzTV.Tests/Application/Channels/CreateChannelFromLineupHandlerTests.cs index a4e46ce90..0574d17c3 100644 --- a/ErsatzTV.Tests/Application/Channels/CreateChannelFromLineupHandlerTests.cs +++ b/ErsatzTV.Tests/Application/Channels/CreateChannelFromLineupHandlerTests.cs @@ -350,6 +350,47 @@ public class CreateChannelFromLineupHandlerTests LeftOf(result).Value.ShouldContain("Invalid playback order for multi collection"); } + [Test] + public async Task Should_Reject_WeightedShuffle_For_A_Multi_Item_Lineup() + { + // regression (#70, found by adversarial review of PR #402): a 2+ entry lineup is persisted as a + // Playlist, and PlaylistEnumerator has no default arm -- an order it doesn't know leaves the + // enumerator null and the items vanish from the playlist with nothing reported. This handler is the + // THIRD writer of PlaylistItem.PlaybackOrder and was missed when the other two were gated. + await SeedTemplateDependencies(); + await SeedTemplate(); + await SeedCollection(21); + await SeedCollection(22); + + var advanced = new CreateChannelFromLineupAdvancedOptions(PlaybackOrder: PlaybackOrder.WeightedShuffle); + + Either result = + await MakeHandler().Handle( + MakeRequest(advanced: advanced, lineup: [CollectionItem(21), CollectionItem(22)]), + CancellationToken.None); + + LeftOf(result).Value.ShouldContain("not supported for a multi-item lineup"); + } + + [Test] + public async Task Should_Allow_WeightedShuffle_For_A_MultiCollection_Lineup() + { + // the mirror of PlayoutModeMustBeValid: a multi collection is WeightedShuffle's intended home, and a + // single-entry lineup goes to a flood item on the Classic engine, which handles the order + await SeedTemplateDependencies(); + await SeedTemplate(); + await SeedMultiCollection(16); + + var advanced = new CreateChannelFromLineupAdvancedOptions(PlaybackOrder: PlaybackOrder.WeightedShuffle); + + Either result = + await MakeHandler().Handle( + MakeRequest(advanced: advanced, lineup: [MultiCollectionItem(16)]), + CancellationToken.None); + + result.IsRight.ShouldBeTrue(); + } + [Test] public async Task Should_Reject_Mirror_Playout_Source() { diff --git a/docs/decisions.md b/docs/decisions.md index 28104a56d..0aff22e30 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -1428,6 +1428,10 @@ and "no single show dominates" — as **one** order, because fair-share is the e equal weights mean equal airtime. **Weight ratios are data, not state**: a `Reset` build re-randomizes `Playout.Seed` (fresh within-source shuffle and start point) but the ratios hold identically. - **Ties break to the earliest source in list order** (strict `>`), keeping the sequence deterministic. +- **`ScheduleAsGroup` is not read by this order**, unlike `ShuffleInOrder`, which merges every + non-`ScheduleAsGroup` collection into ONE source. Here each `CollectionWithItems` is its own weighted + source, which is the entire point — per-source weights are meaningless if sources are merged. Recorded + because it means a persisted per-item flag quietly has no effect under this order. - **Within-source order is shuffled, not chronological** (custom-ordered collections are still honored). Chronological inner order would make every rotation byte-identical, since source selection is deterministic — the reseed on wrap would change nothing and the avoid-an-immediate-repeat retry could @@ -1440,11 +1444,24 @@ and "no single show dominates" — as **one** order, because fair-share is the e flags it); `BlockPlayoutBuilder` filters block items against an allow-list and `continue`s past the rest; YAML/Scripted return `Option.None`, which their callers read as "no content". Only `MultiCollectionGroup` throws. **A weighted order degrading to unweighted random is the worst case, because the output is supposed - to look arbitrary.** So `ReplacePlaylistItems`/`ReplaceBlockItems` **reject** `WeightedShuffle` at the write - path — if it can't be persisted where it isn't handled, the silent sites never see it — and YAML/Scripted - (which address orders by *name*, so `Enum.Parse` accepts it regardless) log a warning when an order falls - through unhandled. Making those pre-existing fallbacks loud is a real but separate defect class, filed as a - follow-up and an explicit non-goal here. + to look arbitrary.** So the write path **rejects** `WeightedShuffle` everywhere it isn't handled — if it + can't be persisted where it isn't handled, the silent sites never see it — and YAML/Scripted (which address + orders by *name*, so `Enum.Parse` accepts it regardless) log a warning when an order falls through + unhandled. Making those pre-existing fallbacks loud is a real but separate defect class: **#403**, an + explicit non-goal here. + - **There are THREE writers of `PlaylistItem.PlaybackOrder`, not two.** Besides `ReplacePlaylistItems` and + `ReplaceBlockItems`, **`CreateChannelFromLineup`** persists one: a lineup of 2+ entries is stored as a + `Playlist`, and its own guard only covered *MultiCollection* entries, so a multi-entry lineup of plain + collections slipped `WeightedShuffle` straight through to `PlaylistEnumerator`'s null-drop. Caught by + adversarial review of PR #402 **after** the first two gates were written and this very claim had been + recorded as complete. The `Add*ToPlaylist` handlers are safe only because they hardcode their order. + Before trusting any "this order can't reach that engine" claim, grep every writer of the field — the + non-obvious composite handler is the one that gets missed. + - **Weight is bounded at the write path** (`MultiCollectionItemWeight`, 1..1000) and clamped again in the + enumerator. Both are needed: **EF's `HasDefaultValue(1)` substitutes 1 for a `0` on INSERT** (0 reads as + "not set") **but an UPDATE writes the 0 through**, so create and update disagreed on the same input, and + a 0-weight source was filtered out of the rotation — deleting it from the channel silently. The ceiling + keeps `Sum(weights)` (checked arithmetic) from overflowing inside a playout build. - **Weight is returned on the multi-collection GET, not just accepted on write.** The update replaces the item list, so a client that reads, edits a name, and writes back would silently reset every weight to the default if the read didn't carry it. Edits ride the existing `Version` token (If-Match/412, §7a). -- 2.47.3 From e2189f0dee716cb60468c58b608ee48c0faad2f7 Mon Sep 17 00:00:00 2001 From: Timothy Date: Fri, 17 Jul 2026 13:23:13 +0200 Subject: [PATCH 08/12] fix(70): test the weight gate; correct rationale my own clamp made false MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Re-review of the fix commit returned MERGEABLE-WITH-NITS. It verified the gate is now complete by enumerating the writers itself (no fourth persisting writer) and proved B2's fix works by writing throwaway handler tests — which was also its point: the fix shipped with none. B2 was create and update silently DISAGREEING on the same input, and the fix re-established agreement with nothing pinning it. Both paths are now driven from one shared case list, plus an explicit test that create and update agree on every case — the per-path tests would both have passed while the two diverged, which is how the bug existed in the first place. Non-vacuity proven: inverting only the update path's validation fails 10 of 20 on a clean build (0 errors, so not a stale-dll pass), and the agreement test is among the failures. The rest is my own prose contradicting my own code. The commit that added EffectiveWeight removed the weight filter, then left four statements asserting a 0-weight source "is filtered out" — two of them authored by that same commit, including the stated justification for Minimum=1 in MultiCollectionItemWeight. A future agent could have read that and deleted the clamp or the floor as redundant; they are belt-and-braces and neither is. Corrected to describe what the code now does: the gate refuses input that means nothing on a share-of-airtime scale, the clamp protects rows predating the gate. Also corrected the writer count in the very bullet whose lesson is "grep every writer of the field": ReplaceBlockItems writes BlockItem.PlaybackOrder, not PlaylistItem.PlaybackOrder. There are TWO persisting writers of PlaylistItem's, and the correction itself had miscounted by conflating the two fields — so the lesson now says to grep each field separately. Core.Tests 565 passed, ErsatzTV.Tests 1673 passed, 0 failed. Refs #70 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../CreateChannelFromLineupHandler.cs | 4 +- .../Commands/UpdateMultiCollectionHandler.cs | 9 +- .../Collection/MultiCollectionItemWeight.cs | 10 +- .../MultiCollectionWeightValidationTests.cs | 153 ++++++++++++++++++ .../MediaCollectionRepositoryWeightTests.cs | 5 +- docs/decisions.md | 25 +-- 6 files changed, 184 insertions(+), 22 deletions(-) create mode 100644 ErsatzTV.Tests/Application/MediaCollections/MultiCollectionWeightValidationTests.cs diff --git a/ErsatzTV.Application/Channels/Commands/CreateChannelFromLineupHandler.cs b/ErsatzTV.Application/Channels/Commands/CreateChannelFromLineupHandler.cs index 39fede040..e52013848 100644 --- a/ErsatzTV.Application/Channels/Commands/CreateChannelFromLineupHandler.cs +++ b/ErsatzTV.Application/Channels/Commands/CreateChannelFromLineupHandler.cs @@ -208,8 +208,8 @@ public class CreateChannelFromLineupHandler( // A lineup of 2+ entries is persisted as a Playlist, and PlaylistEnumerator has no default arm: an // order it doesn't know leaves the enumerator null and the items are dropped from the playlist with - // nothing reported. This is the third writer of PlaylistItem.PlaybackOrder, alongside - // ReplacePlaylistItems and ReplaceBlockItems (#70; the silent fallbacks themselves are #403). + // nothing reported. This is the second (and less obvious) persisting writer of + // PlaylistItem.PlaybackOrder, alongside ReplacePlaylistItems (#70; the silent fallbacks are #403). if (multiItem && playbackOrder is PlaybackOrder.WeightedShuffle) { return BaseError.New( diff --git a/ErsatzTV.Application/MediaCollections/Commands/UpdateMultiCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/UpdateMultiCollectionHandler.cs index 7149c7e86..9fab40dc5 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/UpdateMultiCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/UpdateMultiCollectionHandler.cs @@ -165,10 +165,11 @@ public class UpdateMultiCollectionHandler : IRequestHandler collectionToUpdate); - // Bounds are shared with the create path so the two cannot drift. This gate matters more here than on - // create: EF's HasDefaultValue substitutes 1 for a 0 on INSERT (0 reads as "not set"), but an UPDATE - // writes the 0 through -- and a 0-weight source is filtered out of the rotation, deleting it from the - // channel with nothing reported. See #70. + // Bounds are shared with the create path so the two cannot drift -- they silently disagreed before #402: + // EF's HasDefaultValue substitutes 1 for a 0 on INSERT (0 reads as "not set"), but an UPDATE writes the 0 + // through, so the same input landed differently depending on the verb. The enumerator clamps out-of-range + // weights, so a 0 no longer removes the source; this gate is about refusing input that has no meaning on a + // share-of-airtime scale, and about keeping create and update honest with each other. See #70. private static Validation ValidateWeights(UpdateMultiCollection request) => request.Items.All(i => MultiCollectionItemWeight.IsValid(i.Weight)) ? Unit.Default diff --git a/ErsatzTV.Core/Domain/Collection/MultiCollectionItemWeight.cs b/ErsatzTV.Core/Domain/Collection/MultiCollectionItemWeight.cs index b1b91d5fc..1b435b9fa 100644 --- a/ErsatzTV.Core/Domain/Collection/MultiCollectionItemWeight.cs +++ b/ErsatzTV.Core/Domain/Collection/MultiCollectionItemWeight.cs @@ -3,10 +3,12 @@ namespace ErsatzTV.Core.Domain; /// /// Bounds for a multi-collection member's per-source Weight (#70). /// -/// The floor is 1, not 0. A source with a weight of 0 is filtered out of the rotation by -/// WeightedShuffleCollectionEnumerator, so accepting one would let a caller delete a source from a -/// channel with nothing reported — the silent failure this order is otherwise careful to avoid. "Don't play -/// this source" is expressed by removing it from the multi collection, not by weighting it to nothing. +/// The floor is 1, not 0. A weight is a share of airtime, and 0 has no meaning on that scale — +/// "don't play this source" is expressed by removing it from the multi collection, not by weighting it to +/// nothing. Rejecting it here keeps that intent from being expressed in a way the rotation would have to +/// interpret. WeightedShuffleCollectionEnumerator independently clamps to this range rather than +/// trusting it, so a row that predates this gate rotates at the floor instead of vanishing; the two are +/// belt-and-braces, not duplicates — neither is redundant. /// /// /// The ceiling keeps the rotation arithmetic in range: the enumerator sums weights and scales the rotation diff --git a/ErsatzTV.Tests/Application/MediaCollections/MultiCollectionWeightValidationTests.cs b/ErsatzTV.Tests/Application/MediaCollections/MultiCollectionWeightValidationTests.cs new file mode 100644 index 000000000..1520b5540 --- /dev/null +++ b/ErsatzTV.Tests/Application/MediaCollections/MultiCollectionWeightValidationTests.cs @@ -0,0 +1,153 @@ +using ErsatzTV.Application.MediaCollections; +using ErsatzTV.Core; +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Interfaces.Repositories; +using ErsatzTV.Tests.Support; +using LanguageExt; +using NSubstitute; +using NUnit.Framework; +using Shouldly; + +namespace ErsatzTV.Tests.Application.MediaCollections; + +/// +/// Pins the per-source Weight bounds (#70) on BOTH write paths, driven from one shared case list. +/// +/// The bug this guards against was create and update disagreeing, not either one being wrong on its +/// own: EF's HasDefaultValue(1) substitutes 1 for a 0 on INSERT (0 reads as "not set") while an +/// UPDATE writes the 0 through, so the same request body landed differently depending on the verb. Testing the +/// two paths against the same inputs is the point — a per-path test would have passed while they diverged. +/// +/// +[TestFixture] +public class MultiCollectionWeightValidationTests : MediaCollectionHandlerTestBase +{ + private const int CollectionId = 1; + private const int SmartCollectionId = 2; + + private static readonly int[] RejectedWeights = [0, -1, -5, MultiCollectionItemWeight.Maximum + 1, int.MaxValue]; + private static readonly int[] AcceptedWeights = + [MultiCollectionItemWeight.Minimum, 2, 500, MultiCollectionItemWeight.Maximum]; + + [SetUp] + public async Task SetUp() + { + await SeedCollection(CollectionId); + await SeedSmartCollection(SmartCollectionId); + } + + private CreateMultiCollectionHandler CreateHandler() => new(Db.Factory, SearchTargets); + + private UpdateMultiCollectionHandler UpdateHandler() + { + var repo = Substitute.For(); + + // the handler enqueues a playout rebuild for every playout using the collection once a real change + // lands; an unstubbed mock returns null there and the NRE looks like a product bug + repo.PlayoutIdsUsingMultiCollection(Arg.Any()).Returns([]); + + return new UpdateMultiCollectionHandler(Db.Factory, repo, Worker, SearchTargets); + } + + private static CreateMultiCollection CreateCommand(string name, int weight) => + new(name, [new CreateMultiCollectionItem(CollectionId, null, true, PlaybackOrder.WeightedShuffle, weight)]); + + private static CreateMultiCollection CreateSmartCommand(string name, int weight) => + new( + name, + [new CreateMultiCollectionItem(null, SmartCollectionId, true, PlaybackOrder.WeightedShuffle, weight)]); + + private async Task SeedMultiCollection(string name) + { + Either created = + await CreateHandler().Handle(CreateCommand(name, MultiCollectionItemWeight.Minimum), CancellationToken.None); + created.IsRight.ShouldBeTrue(); + return created.RightToSeq().Head().Id; + } + + [Test] + public async Task Create_Rejects_Out_Of_Range_Weight([ValueSource(nameof(RejectedWeights))] int weight) + { + Either result = + await CreateHandler().Handle(CreateCommand($"create-{weight}", weight), CancellationToken.None); + + result.IsLeft.ShouldBeTrue($"weight {weight} must be rejected"); + result.LeftToSeq().Head().Value.ShouldContain("Weight must be between"); + } + + [Test] + public async Task Update_Rejects_Out_Of_Range_Weight([ValueSource(nameof(RejectedWeights))] int weight) + { + // the path that actually persisted a 0 before #402 -- EF's INSERT default masked it on create + int id = await SeedMultiCollection($"update-{weight}"); + + Either result = await UpdateHandler().Handle( + new UpdateMultiCollection( + id, + $"update-{weight}", + [new UpdateMultiCollectionItem(CollectionId, null, true, PlaybackOrder.WeightedShuffle, weight)]), + CancellationToken.None); + + result.IsLeft.ShouldBeTrue($"weight {weight} must be rejected"); + result.LeftToSeq().Head().Value.ShouldContain("Weight must be between"); + } + + [Test] + public async Task Create_Accepts_In_Range_Weight([ValueSource(nameof(AcceptedWeights))] int weight) + { + Either result = + await CreateHandler().Handle(CreateCommand($"ok-create-{weight}", weight), CancellationToken.None); + + result.IsRight.ShouldBeTrue($"weight {weight} must be accepted"); + } + + [Test] + public async Task Update_Accepts_In_Range_Weight([ValueSource(nameof(AcceptedWeights))] int weight) + { + int id = await SeedMultiCollection($"ok-update-{weight}"); + + Either result = await UpdateHandler().Handle( + new UpdateMultiCollection( + id, + $"ok-update-{weight}", + [new UpdateMultiCollectionItem(CollectionId, null, true, PlaybackOrder.WeightedShuffle, weight)]), + CancellationToken.None); + + result.IsRight.ShouldBeTrue($"weight {weight} must be accepted"); + } + + [Test] + public async Task Create_Rejects_Out_Of_Range_Weight_On_A_Smart_Item() + { + // the smart mirror is the half that gets forgotten; the gate must cover it too + Either result = + await CreateHandler().Handle(CreateSmartCommand("smart-bad", 0), CancellationToken.None); + + result.IsLeft.ShouldBeTrue(); + result.LeftToSeq().Head().Value.ShouldContain("Weight must be between"); + } + + [Test] + public async Task Update_And_Create_Agree_On_Every_Case() + { + // the regression that started B2: same input, different verb, different outcome. Asserted directly + // rather than inferred from the two suites above passing. + foreach (int weight in RejectedWeights.Concat(AcceptedWeights)) + { + Either created = await CreateHandler() + .Handle(CreateCommand($"agree-{weight}", weight), CancellationToken.None); + + int id = await SeedMultiCollection($"agree-base-{weight}"); + Either updated = await UpdateHandler().Handle( + new UpdateMultiCollection( + id, + $"agree-base-{weight}", + [new UpdateMultiCollectionItem(CollectionId, null, true, PlaybackOrder.WeightedShuffle, weight)]), + CancellationToken.None); + + updated.IsRight.ShouldBe( + created.IsRight, + $"create and update disagreed on weight {weight}"); + } + } +} diff --git a/ErsatzTV.Tests/Infrastructure/MediaCollectionRepositoryWeightTests.cs b/ErsatzTV.Tests/Infrastructure/MediaCollectionRepositoryWeightTests.cs index 4a8835b18..67c84de00 100644 --- a/ErsatzTV.Tests/Infrastructure/MediaCollectionRepositoryWeightTests.cs +++ b/ErsatzTV.Tests/Infrastructure/MediaCollectionRepositoryWeightTests.cs @@ -111,8 +111,9 @@ public class MediaCollectionRepositoryWeightTests [Test] public async Task Weight_Defaults_To_One_So_An_Existing_Row_Is_Fair_Share_Not_Dropped() { - // the migration backfills 1 rather than 0. A 0-weight source is filtered out by the enumerator, so a - // 0 default would silently remove every pre-existing member from a weighted rotation. + // the migration backfills 1 rather than 0, so a pre-existing member arrives as fair-share rather than + // carrying a weight that means nothing on a share-of-airtime scale. (The enumerator also clamps, so a + // 0 would rotate at the floor rather than vanish — but the backfill should be right at the source.) const int MultiCollectionId = 2; const int CollectionId = 30; const int SmartCollectionId = 40; diff --git a/docs/decisions.md b/docs/decisions.md index 0aff22e30..a0119733d 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -1410,8 +1410,10 @@ and "no single show dominates" — as **one** order, because fair-share is the e `ShuffleInOrder` was rejected: it would silently change shipped users' output. - **Weights live on `MultiCollectionItem` AND `MultiCollectionSmartItem`** (`int Weight`, DB default 1, dual-provider migration `AddMultiCollectionItemWeight`). The mirror is mandatory — omitting the smart - item un-weights smart-collection members silently. The DB default must be **1, not 0**: a 0-weight source - is filtered out by the enumerator, so a 0 backfill would drop every pre-existing member from a rotation. + item un-weights smart-collection members silently. The DB default must be **1, not 0**: a 0 backfill would + hand every pre-existing member to the rotation carrying a weight that means nothing on a share-of-airtime + scale. (The enumerator clamps such rows to the floor so they rotate fair-share rather than vanish — but the + backfill should be right at the source.) Rejected carriers: `PlaylistItem` (sequential engine; its `Count`/`PlayAll` already mean "how much of this source", and drain-N-consecutively `A A A B` is a different product from smooth interleave `A A B A`); `CollectionItem` (wrong grain — per media item, and the largest table). @@ -1449,14 +1451,17 @@ and "no single show dominates" — as **one** order, because fair-share is the e orders by *name*, so `Enum.Parse` accepts it regardless) log a warning when an order falls through unhandled. Making those pre-existing fallbacks loud is a real but separate defect class: **#403**, an explicit non-goal here. - - **There are THREE writers of `PlaylistItem.PlaybackOrder`, not two.** Besides `ReplacePlaylistItems` and - `ReplaceBlockItems`, **`CreateChannelFromLineup`** persists one: a lineup of 2+ entries is stored as a - `Playlist`, and its own guard only covered *MultiCollection* entries, so a multi-entry lineup of plain - collections slipped `WeightedShuffle` straight through to `PlaylistEnumerator`'s null-drop. Caught by - adversarial review of PR #402 **after** the first two gates were written and this very claim had been - recorded as complete. The `Add*ToPlaylist` handlers are safe only because they hardcode their order. - Before trusting any "this order can't reach that engine" claim, grep every writer of the field — the - non-obvious composite handler is the one that gets missed. + - **There are TWO persisting writers of `PlaylistItem.PlaybackOrder`, and the second is not obvious.** + `ReplacePlaylistItems` is the expected one; **`CreateChannelFromLineup`** is the other — a lineup of 2+ + entries is stored as a `Playlist`, and its own guard only covered *MultiCollection* entries, so a + multi-entry lineup of plain collections slipped `WeightedShuffle` straight through to + `PlaylistEnumerator`'s null-drop. (`ReplaceBlockItems` is the separate writer of + `BlockItem.PlaybackOrder`; `Add*ToPlaylist` and the Trakt path are safe only because they hardcode their + order, and the two Preview handlers build in memory without persisting.) Caught by adversarial review of + PR #402 **after** the first gates were written and this very bullet had recorded the perimeter as + complete — twice, since the correction itself first miscounted the writers by conflating the two fields. + Before trusting any "this order can't reach that engine" claim, grep every writer of **each field + separately**; the non-obvious composite handler is the one that gets missed. - **Weight is bounded at the write path** (`MultiCollectionItemWeight`, 1..1000) and clamped again in the enumerator. Both are needed: **EF's `HasDefaultValue(1)` substitutes 1 for a `0` on INSERT** (0 reads as "not set") **but an UPDATE writes the 0 through**, so create and update disagreed on the same input, and -- 2.47.3 From 9e436212431e9576d05050ec5543ec4e656ada5f Mon Sep 17 00:00:00 2001 From: Timothy Date: Fri, 17 Jul 2026 14:39:51 +0200 Subject: [PATCH 09/12] style(70): strip UTF-8 BOM from the .cs files this PR touches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI's Formatting job failed: 19 touched files carried a BOM, which .editorconfig forbids (charset=utf-8). Pure encoding change — one byte per file, no semantic diff (verified: every hunk is `-namespace` -> `+namespace`). Self-inflicted. The patches that edited these legacy files wrote them back as utf-8-sig to "preserve the existing style", but the #311 fix-as-you-touch gate requires a file to be normalized when you touch it — that is the whole point of scoping the gate to changed files instead of reformatting the ~2500 legacy BOM files at once. dotnet format leaves the EF-generated Designer/snapshot files alone as generated code, and its verify skips them the same way, so they stay as ef emitted them. Two corrections to what I believed going in: - `dotnet format --include` does NOT no-op here. It reported `error CHARSET` for each file and exit 2, reproducing CI exactly, and fixed them in place. The note claiming otherwise is wrong for this invocation. - My first BOM check reported all files clean. The od pattern was wrong; reading the first three bytes directly found 19. A detector that can only say "ok" is worse than no detector. Core.Tests 565, ErsatzTV.Tests 1673, Architecture.Tests 5 — all passed. API artifacts still in sync. Refs #70 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../CreateChannelFromLineupHandler.cs | 2 +- .../Commands/CreateMultiCollection.cs | 2 +- .../Commands/CreateMultiCollectionHandler.cs | 64 +++++++++---------- .../Commands/ReplacePlaylistItemsHandler.cs | 2 +- .../Commands/UpdateMultiCollectionHandler.cs | 2 +- .../MediaCollections/Mapper.cs | 2 +- .../MultiCollectionItemViewModel.cs | 2 +- .../MultiCollectionSmartItemViewModel.cs | 2 +- .../ProgramScheduleItemCommandBase.cs | 2 +- .../Commands/ReplaceBlockItemsHandler.cs | 2 +- .../Domain/Collection/MultiCollectionItem.cs | 2 +- .../Collection/MultiCollectionSmartItem.cs | 2 +- ErsatzTV.Core/Domain/PlaybackOrder.cs | 2 +- .../Scheduling/CollectionWithItems.cs | 2 +- .../Scheduling/Engine/SchedulingEngine.cs | 2 +- ErsatzTV.Core/Scheduling/PlayoutBuilder.cs | 2 +- .../YamlScheduling/EnumeratorCache.cs | 4 +- ...0717072111_AddMultiCollectionItemWeight.cs | 2 +- ...0717071946_AddMultiCollectionItemWeight.cs | 2 +- .../MultiCollectionConfiguration.cs | 2 +- .../Repositories/MediaCollectionRepository.cs | 2 +- 21 files changed, 53 insertions(+), 53 deletions(-) diff --git a/ErsatzTV.Application/Channels/Commands/CreateChannelFromLineupHandler.cs b/ErsatzTV.Application/Channels/Commands/CreateChannelFromLineupHandler.cs index e52013848..050b4038b 100644 --- a/ErsatzTV.Application/Channels/Commands/CreateChannelFromLineupHandler.cs +++ b/ErsatzTV.Application/Channels/Commands/CreateChannelFromLineupHandler.cs @@ -1,4 +1,4 @@ -using System.Globalization; +using System.Globalization; using System.Text.RegularExpressions; using System.Threading.Channels; using ErsatzTV.Application.Playouts; diff --git a/ErsatzTV.Application/MediaCollections/Commands/CreateMultiCollection.cs b/ErsatzTV.Application/MediaCollections/Commands/CreateMultiCollection.cs index e49df679b..0822a5f45 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/CreateMultiCollection.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/CreateMultiCollection.cs @@ -1,4 +1,4 @@ -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; namespace ErsatzTV.Application.MediaCollections; diff --git a/ErsatzTV.Application/MediaCollections/Commands/CreateMultiCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/CreateMultiCollectionHandler.cs index c4ad1ba42..86d1384f3 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/CreateMultiCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/CreateMultiCollectionHandler.cs @@ -1,4 +1,4 @@ -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Infrastructure.Data; @@ -54,43 +54,43 @@ public class CreateMultiCollectionHandler : ValidateName(dbContext, request) .BindT(name => ValidateWeights(request).Map(_ => name)) .MapT(name => new MultiCollection - { - Name = name, - MultiCollectionItems = request.Items.Bind(i => - { - if (i.CollectionId.HasValue) + { + Name = name, + MultiCollectionItems = request.Items.Bind(i => { - return Some( - new MultiCollectionItem - { - CollectionId = i.CollectionId.Value, - ScheduleAsGroup = i.ScheduleAsGroup, - PlaybackOrder = i.PlaybackOrder, - Weight = i.Weight - }); - } + if (i.CollectionId.HasValue) + { + return Some( + new MultiCollectionItem + { + CollectionId = i.CollectionId.Value, + ScheduleAsGroup = i.ScheduleAsGroup, + PlaybackOrder = i.PlaybackOrder, + Weight = i.Weight + }); + } - return Option.None; - }) + return Option.None; + }) .ToList(), - MultiCollectionSmartItems = request.Items.Bind(i => - { - if (i.SmartCollectionId.HasValue) + MultiCollectionSmartItems = request.Items.Bind(i => { - return Some( - new MultiCollectionSmartItem - { - SmartCollectionId = i.SmartCollectionId.Value, - ScheduleAsGroup = i.ScheduleAsGroup, - PlaybackOrder = i.PlaybackOrder, - Weight = i.Weight - }); - } + if (i.SmartCollectionId.HasValue) + { + return Some( + new MultiCollectionSmartItem + { + SmartCollectionId = i.SmartCollectionId.Value, + ScheduleAsGroup = i.ScheduleAsGroup, + PlaybackOrder = i.PlaybackOrder, + Weight = i.Weight + }); + } - return Option.None; - }) + return Option.None; + }) .ToList() - }); + }); // Bounds are shared with the update path so the two cannot drift: an out-of-range weight silently // removes the source from the rotation (0) or overflows the rotation arithmetic (huge). See #70. diff --git a/ErsatzTV.Application/MediaCollections/Commands/ReplacePlaylistItemsHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/ReplacePlaylistItemsHandler.cs index a8fadda04..cd98f5595 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/ReplacePlaylistItemsHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/ReplacePlaylistItemsHandler.cs @@ -1,4 +1,4 @@ -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; diff --git a/ErsatzTV.Application/MediaCollections/Commands/UpdateMultiCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/UpdateMultiCollectionHandler.cs index 9fab40dc5..ecde9ffa1 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/UpdateMultiCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/UpdateMultiCollectionHandler.cs @@ -1,4 +1,4 @@ -using System.Threading.Channels; +using System.Threading.Channels; using ErsatzTV.Application.Playouts; using ErsatzTV.Core; using ErsatzTV.Core.Domain; diff --git a/ErsatzTV.Application/MediaCollections/Mapper.cs b/ErsatzTV.Application/MediaCollections/Mapper.cs index c90136932..b59029c2a 100644 --- a/ErsatzTV.Application/MediaCollections/Mapper.cs +++ b/ErsatzTV.Application/MediaCollections/Mapper.cs @@ -1,4 +1,4 @@ -using ErsatzTV.Application.Tree; +using ErsatzTV.Application.Tree; using ErsatzTV.Core.Api.SmartCollections; using ErsatzTV.Core.Domain; diff --git a/ErsatzTV.Application/MediaCollections/MultiCollectionItemViewModel.cs b/ErsatzTV.Application/MediaCollections/MultiCollectionItemViewModel.cs index 41367cada..ac5ebd239 100644 --- a/ErsatzTV.Application/MediaCollections/MultiCollectionItemViewModel.cs +++ b/ErsatzTV.Application/MediaCollections/MultiCollectionItemViewModel.cs @@ -1,4 +1,4 @@ -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; namespace ErsatzTV.Application.MediaCollections; diff --git a/ErsatzTV.Application/MediaCollections/MultiCollectionSmartItemViewModel.cs b/ErsatzTV.Application/MediaCollections/MultiCollectionSmartItemViewModel.cs index 047504bc2..4b1eb5c01 100644 --- a/ErsatzTV.Application/MediaCollections/MultiCollectionSmartItemViewModel.cs +++ b/ErsatzTV.Application/MediaCollections/MultiCollectionSmartItemViewModel.cs @@ -1,4 +1,4 @@ -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; namespace ErsatzTV.Application.MediaCollections; diff --git a/ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs b/ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs index 5677849f3..8471c6ab9 100644 --- a/ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs +++ b/ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs @@ -1,4 +1,4 @@ -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Filler; using ErsatzTV.Core.Errors; diff --git a/ErsatzTV.Application/Scheduling/Commands/ReplaceBlockItemsHandler.cs b/ErsatzTV.Application/Scheduling/Commands/ReplaceBlockItemsHandler.cs index b0965c9ae..f0fb81345 100644 --- a/ErsatzTV.Application/Scheduling/Commands/ReplaceBlockItemsHandler.cs +++ b/ErsatzTV.Application/Scheduling/Commands/ReplaceBlockItemsHandler.cs @@ -1,4 +1,4 @@ -using ErsatzTV.Core; +using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Scheduling; using ErsatzTV.Infrastructure.Data; diff --git a/ErsatzTV.Core/Domain/Collection/MultiCollectionItem.cs b/ErsatzTV.Core/Domain/Collection/MultiCollectionItem.cs index 1b6019e63..d55fa298e 100644 --- a/ErsatzTV.Core/Domain/Collection/MultiCollectionItem.cs +++ b/ErsatzTV.Core/Domain/Collection/MultiCollectionItem.cs @@ -1,4 +1,4 @@ -namespace ErsatzTV.Core.Domain; +namespace ErsatzTV.Core.Domain; public class MultiCollectionItem { diff --git a/ErsatzTV.Core/Domain/Collection/MultiCollectionSmartItem.cs b/ErsatzTV.Core/Domain/Collection/MultiCollectionSmartItem.cs index 447554a64..f9f32bd5b 100644 --- a/ErsatzTV.Core/Domain/Collection/MultiCollectionSmartItem.cs +++ b/ErsatzTV.Core/Domain/Collection/MultiCollectionSmartItem.cs @@ -1,4 +1,4 @@ -namespace ErsatzTV.Core.Domain; +namespace ErsatzTV.Core.Domain; public class MultiCollectionSmartItem { diff --git a/ErsatzTV.Core/Domain/PlaybackOrder.cs b/ErsatzTV.Core/Domain/PlaybackOrder.cs index 2c2b249f9..47964ee28 100644 --- a/ErsatzTV.Core/Domain/PlaybackOrder.cs +++ b/ErsatzTV.Core/Domain/PlaybackOrder.cs @@ -1,4 +1,4 @@ -namespace ErsatzTV.Core.Domain; +namespace ErsatzTV.Core.Domain; public enum PlaybackOrder { diff --git a/ErsatzTV.Core/Scheduling/CollectionWithItems.cs b/ErsatzTV.Core/Scheduling/CollectionWithItems.cs index 950d0eb37..a341823a4 100644 --- a/ErsatzTV.Core/Scheduling/CollectionWithItems.cs +++ b/ErsatzTV.Core/Scheduling/CollectionWithItems.cs @@ -1,4 +1,4 @@ -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; namespace ErsatzTV.Core.Scheduling; diff --git a/ErsatzTV.Core/Scheduling/Engine/SchedulingEngine.cs b/ErsatzTV.Core/Scheduling/Engine/SchedulingEngine.cs index 508273b07..359ffdf24 100644 --- a/ErsatzTV.Core/Scheduling/Engine/SchedulingEngine.cs +++ b/ErsatzTV.Core/Scheduling/Engine/SchedulingEngine.cs @@ -1,4 +1,4 @@ -using System.Collections.Immutable; +using System.Collections.Immutable; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Filler; using ErsatzTV.Core.Domain.Scheduling; diff --git a/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs b/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs index 13f0c8bfe..609c29e35 100644 --- a/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs +++ b/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs @@ -1,4 +1,4 @@ -using System.IO.Abstractions; +using System.IO.Abstractions; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Filler; using ErsatzTV.Core.Errors; diff --git a/ErsatzTV.Core/Scheduling/YamlScheduling/EnumeratorCache.cs b/ErsatzTV.Core/Scheduling/YamlScheduling/EnumeratorCache.cs index f325ea6f4..1f1f575bf 100644 --- a/ErsatzTV.Core/Scheduling/YamlScheduling/EnumeratorCache.cs +++ b/ErsatzTV.Core/Scheduling/YamlScheduling/EnumeratorCache.cs @@ -1,4 +1,4 @@ -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Scheduling; using ErsatzTV.Core.Scheduling.BlockScheduling; @@ -93,7 +93,7 @@ public class EnumeratorCache(IMediaCollectionRepository mediaCollectionRepositor cancellationToken); break; - // playlist is handled later + // playlist is handled later } _mediaItems[content.Key] = items; diff --git a/ErsatzTV.Infrastructure.MySql/Migrations/20260717072111_AddMultiCollectionItemWeight.cs b/ErsatzTV.Infrastructure.MySql/Migrations/20260717072111_AddMultiCollectionItemWeight.cs index 5e835e18f..d8d4e0507 100644 --- a/ErsatzTV.Infrastructure.MySql/Migrations/20260717072111_AddMultiCollectionItemWeight.cs +++ b/ErsatzTV.Infrastructure.MySql/Migrations/20260717072111_AddMultiCollectionItemWeight.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/ErsatzTV.Infrastructure.Sqlite/Migrations/20260717071946_AddMultiCollectionItemWeight.cs b/ErsatzTV.Infrastructure.Sqlite/Migrations/20260717071946_AddMultiCollectionItemWeight.cs index cfa4938ce..c6e7f2367 100644 --- a/ErsatzTV.Infrastructure.Sqlite/Migrations/20260717071946_AddMultiCollectionItemWeight.cs +++ b/ErsatzTV.Infrastructure.Sqlite/Migrations/20260717071946_AddMultiCollectionItemWeight.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/ErsatzTV.Infrastructure/Data/Configurations/Collection/MultiCollectionConfiguration.cs b/ErsatzTV.Infrastructure/Data/Configurations/Collection/MultiCollectionConfiguration.cs index 5b1e7be3a..888fa1dd7 100644 --- a/ErsatzTV.Infrastructure/Data/Configurations/Collection/MultiCollectionConfiguration.cs +++ b/ErsatzTV.Infrastructure/Data/Configurations/Collection/MultiCollectionConfiguration.cs @@ -1,4 +1,4 @@ -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; diff --git a/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs index 5a3f1a795..47b2b9d16 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs @@ -1,4 +1,4 @@ -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; using Dapper; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; -- 2.47.3 From dff6aa510280e12ab451c11b0776ab7577819f85 Mon Sep 17 00:00:00 2001 From: Timothy Date: Fri, 17 Jul 2026 14:53:37 +0200 Subject: [PATCH 10/12] =?UTF-8?q?test(70):=20classic-weighted=20golden=20?= =?UTF-8?q?=E2=80=94=20weight=20beats=20collection=20size=20end-to-end?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The unit tests pin the enumerator's sequence in isolation; this pins that the real PlayoutBuilder actually distributes by weight, through the whole chain: MultiCollection -> MediaCollectionRepository -> CollectionWithItems.Weight -> ShuffleSourceBuilder -> WeightedShuffleCollectionEnumerator -> PlayoutItems. The fixture is deliberately lopsided: the HEAVY source (weight 3) is the SMALL collection (2 items) and the LIGHT source (weight 1) is the LARGE one (4). Over the pinned 2-day window that yields 81 : 27 = exactly 3.00 : 1 — the smaller collection taking the larger share, which is the entire point of the feature and is not reachable by any existing order: Shuffle is size-proportional (~2:4 here) and ShuffleInOrder plays every item exactly once per cycle. Longest consecutive same-source run is 3, so it interleaves smoothly rather than draining blocks. Same determinism contract as Classic_shuffle: pinned Playout.Seed + Continue mode (Reset randomizes the seed). Channel number/GUID 5 — every golden fixture shares one in-memory DB, so those must be globally unique. The net is armed, not decorative: with the golden absent the test fails (missing golden is a hard failure by design), and regenerating added only classic-weighted — the three existing goldens are byte-identical, so nothing was silently re-baselined. Core.Tests 566 passed, 0 failed. Refs #70 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Goldens/Goldens/classic-weighted.txt | 108 ++++++++++ .../Goldens/PlayoutBuildGoldenTests.cs | 187 ++++++++++++++++++ 2 files changed, 295 insertions(+) create mode 100644 ErsatzTV.Core.Tests/Scheduling/Goldens/Goldens/classic-weighted.txt diff --git a/ErsatzTV.Core.Tests/Scheduling/Goldens/Goldens/classic-weighted.txt b/ErsatzTV.Core.Tests/Scheduling/Goldens/Goldens/classic-weighted.txt new file mode 100644 index 000000000..c515db998 --- /dev/null +++ b/ErsatzTV.Core.Tests/Scheduling/Goldens/Goldens/classic-weighted.txt @@ -0,0 +1,108 @@ +000 | 2026-01-15 00:00:00 - 2026-01-15 00:30:00 | None | Heavy Movie 01 +001 | 2026-01-15 00:30:00 - 2026-01-15 01:00:00 | None | Heavy Movie 02 +002 | 2026-01-15 01:00:00 - 2026-01-15 01:30:00 | None | Light Movie 04 +003 | 2026-01-15 01:30:00 - 2026-01-15 02:00:00 | None | Heavy Movie 01 +004 | 2026-01-15 02:00:00 - 2026-01-15 02:30:00 | None | Heavy Movie 02 +005 | 2026-01-15 02:30:00 - 2026-01-15 03:00:00 | None | Heavy Movie 01 +006 | 2026-01-15 03:00:00 - 2026-01-15 03:30:00 | None | Light Movie 03 +007 | 2026-01-15 03:30:00 - 2026-01-15 04:00:00 | None | Heavy Movie 02 +008 | 2026-01-15 04:00:00 - 2026-01-15 04:30:00 | None | Heavy Movie 01 +009 | 2026-01-15 04:30:00 - 2026-01-15 05:00:00 | None | Heavy Movie 02 +010 | 2026-01-15 05:00:00 - 2026-01-15 05:30:00 | None | Light Movie 01 +011 | 2026-01-15 05:30:00 - 2026-01-15 06:00:00 | None | Heavy Movie 01 +012 | 2026-01-15 06:00:00 - 2026-01-15 06:30:00 | None | Heavy Movie 02 +013 | 2026-01-15 06:30:00 - 2026-01-15 07:00:00 | None | Heavy Movie 01 +014 | 2026-01-15 07:00:00 - 2026-01-15 07:30:00 | None | Light Movie 02 +015 | 2026-01-15 07:30:00 - 2026-01-15 08:00:00 | None | Heavy Movie 02 +016 | 2026-01-15 08:00:00 - 2026-01-15 08:30:00 | None | Heavy Movie 01 +017 | 2026-01-15 08:30:00 - 2026-01-15 09:00:00 | None | Heavy Movie 02 +018 | 2026-01-15 09:00:00 - 2026-01-15 09:30:00 | None | Light Movie 02 +019 | 2026-01-15 09:30:00 - 2026-01-15 10:00:00 | None | Heavy Movie 01 +020 | 2026-01-15 10:00:00 - 2026-01-15 10:30:00 | None | Heavy Movie 02 +021 | 2026-01-15 10:30:00 - 2026-01-15 11:00:00 | None | Heavy Movie 01 +022 | 2026-01-15 11:00:00 - 2026-01-15 11:30:00 | None | Light Movie 04 +023 | 2026-01-15 11:30:00 - 2026-01-15 12:00:00 | None | Heavy Movie 02 +024 | 2026-01-15 12:00:00 - 2026-01-15 12:30:00 | None | Heavy Movie 01 +025 | 2026-01-15 12:30:00 - 2026-01-15 13:00:00 | None | Heavy Movie 02 +026 | 2026-01-15 13:00:00 - 2026-01-15 13:30:00 | None | Light Movie 01 +027 | 2026-01-15 13:30:00 - 2026-01-15 14:00:00 | None | Heavy Movie 01 +028 | 2026-01-15 14:00:00 - 2026-01-15 14:30:00 | None | Heavy Movie 02 +029 | 2026-01-15 14:30:00 - 2026-01-15 15:00:00 | None | Heavy Movie 01 +030 | 2026-01-15 15:00:00 - 2026-01-15 15:30:00 | None | Light Movie 03 +031 | 2026-01-15 15:30:00 - 2026-01-15 16:00:00 | None | Heavy Movie 02 +032 | 2026-01-15 16:00:00 - 2026-01-15 16:30:00 | None | Heavy Movie 01 +033 | 2026-01-15 16:30:00 - 2026-01-15 17:00:00 | None | Heavy Movie 02 +034 | 2026-01-15 17:00:00 - 2026-01-15 17:30:00 | None | Light Movie 02 +035 | 2026-01-15 17:30:00 - 2026-01-15 18:00:00 | None | Heavy Movie 01 +036 | 2026-01-15 18:00:00 - 2026-01-15 18:30:00 | None | Heavy Movie 02 +037 | 2026-01-15 18:30:00 - 2026-01-15 19:00:00 | None | Heavy Movie 01 +038 | 2026-01-15 19:00:00 - 2026-01-15 19:30:00 | None | Light Movie 03 +039 | 2026-01-15 19:30:00 - 2026-01-15 20:00:00 | None | Heavy Movie 02 +040 | 2026-01-15 20:00:00 - 2026-01-15 20:30:00 | None | Heavy Movie 01 +041 | 2026-01-15 20:30:00 - 2026-01-15 21:00:00 | None | Heavy Movie 02 +042 | 2026-01-15 21:00:00 - 2026-01-15 21:30:00 | None | Light Movie 01 +043 | 2026-01-15 21:30:00 - 2026-01-15 22:00:00 | None | Heavy Movie 01 +044 | 2026-01-15 22:00:00 - 2026-01-15 22:30:00 | None | Heavy Movie 02 +045 | 2026-01-15 22:30:00 - 2026-01-15 23:00:00 | None | Heavy Movie 01 +046 | 2026-01-15 23:00:00 - 2026-01-15 23:30:00 | None | Light Movie 04 +047 | 2026-01-15 23:30:00 - 2026-01-16 00:00:00 | None | Heavy Movie 02 +048 | 2026-01-16 00:00:00 - 2026-01-16 00:30:00 | None | Heavy Movie 01 +049 | 2026-01-16 00:30:00 - 2026-01-16 01:00:00 | None | Heavy Movie 02 +050 | 2026-01-16 01:00:00 - 2026-01-16 01:30:00 | None | Light Movie 02 +051 | 2026-01-16 01:30:00 - 2026-01-16 02:00:00 | None | Heavy Movie 01 +052 | 2026-01-16 02:00:00 - 2026-01-16 02:30:00 | None | Heavy Movie 02 +053 | 2026-01-16 02:30:00 - 2026-01-16 03:00:00 | None | Heavy Movie 01 +054 | 2026-01-16 03:00:00 - 2026-01-16 03:30:00 | None | Light Movie 04 +055 | 2026-01-16 03:30:00 - 2026-01-16 04:00:00 | None | Heavy Movie 02 +056 | 2026-01-16 04:00:00 - 2026-01-16 04:30:00 | None | Heavy Movie 01 +057 | 2026-01-16 04:30:00 - 2026-01-16 05:00:00 | None | Heavy Movie 02 +058 | 2026-01-16 05:00:00 - 2026-01-16 05:30:00 | None | Light Movie 01 +059 | 2026-01-16 05:30:00 - 2026-01-16 06:00:00 | None | Heavy Movie 01 +060 | 2026-01-16 06:00:00 - 2026-01-16 06:30:00 | None | Heavy Movie 02 +061 | 2026-01-16 06:30:00 - 2026-01-16 07:00:00 | None | Heavy Movie 01 +062 | 2026-01-16 07:00:00 - 2026-01-16 07:30:00 | None | Light Movie 03 +063 | 2026-01-16 07:30:00 - 2026-01-16 08:00:00 | None | Heavy Movie 02 +064 | 2026-01-16 08:00:00 - 2026-01-16 08:30:00 | None | Heavy Movie 01 +065 | 2026-01-16 08:30:00 - 2026-01-16 09:00:00 | None | Heavy Movie 02 +066 | 2026-01-16 09:00:00 - 2026-01-16 09:30:00 | None | Light Movie 01 +067 | 2026-01-16 09:30:00 - 2026-01-16 10:00:00 | None | Heavy Movie 01 +068 | 2026-01-16 10:00:00 - 2026-01-16 10:30:00 | None | Heavy Movie 02 +069 | 2026-01-16 10:30:00 - 2026-01-16 11:00:00 | None | Heavy Movie 01 +070 | 2026-01-16 11:00:00 - 2026-01-16 11:30:00 | None | Light Movie 02 +071 | 2026-01-16 11:30:00 - 2026-01-16 12:00:00 | None | Heavy Movie 02 +072 | 2026-01-16 12:00:00 - 2026-01-16 12:30:00 | None | Heavy Movie 01 +073 | 2026-01-16 12:30:00 - 2026-01-16 13:00:00 | None | Heavy Movie 02 +074 | 2026-01-16 13:00:00 - 2026-01-16 13:30:00 | None | Light Movie 03 +075 | 2026-01-16 13:30:00 - 2026-01-16 14:00:00 | None | Heavy Movie 01 +076 | 2026-01-16 14:00:00 - 2026-01-16 14:30:00 | None | Heavy Movie 02 +077 | 2026-01-16 14:30:00 - 2026-01-16 15:00:00 | None | Heavy Movie 01 +078 | 2026-01-16 15:00:00 - 2026-01-16 15:30:00 | None | Light Movie 04 +079 | 2026-01-16 15:30:00 - 2026-01-16 16:00:00 | None | Heavy Movie 02 +080 | 2026-01-16 16:00:00 - 2026-01-16 16:30:00 | None | Heavy Movie 01 +081 | 2026-01-16 16:30:00 - 2026-01-16 17:00:00 | None | Heavy Movie 02 +082 | 2026-01-16 17:00:00 - 2026-01-16 17:30:00 | None | Light Movie 03 +083 | 2026-01-16 17:30:00 - 2026-01-16 18:00:00 | None | Heavy Movie 01 +084 | 2026-01-16 18:00:00 - 2026-01-16 18:30:00 | None | Heavy Movie 02 +085 | 2026-01-16 18:30:00 - 2026-01-16 19:00:00 | None | Heavy Movie 01 +086 | 2026-01-16 19:00:00 - 2026-01-16 19:30:00 | None | Light Movie 02 +087 | 2026-01-16 19:30:00 - 2026-01-16 20:00:00 | None | Heavy Movie 02 +088 | 2026-01-16 20:00:00 - 2026-01-16 20:30:00 | None | Heavy Movie 01 +089 | 2026-01-16 20:30:00 - 2026-01-16 21:00:00 | None | Heavy Movie 02 +090 | 2026-01-16 21:00:00 - 2026-01-16 21:30:00 | None | Light Movie 04 +091 | 2026-01-16 21:30:00 - 2026-01-16 22:00:00 | None | Heavy Movie 01 +092 | 2026-01-16 22:00:00 - 2026-01-16 22:30:00 | None | Heavy Movie 02 +093 | 2026-01-16 22:30:00 - 2026-01-16 23:00:00 | None | Heavy Movie 01 +094 | 2026-01-16 23:00:00 - 2026-01-16 23:30:00 | None | Light Movie 01 +095 | 2026-01-16 23:30:00 - 2026-01-17 00:00:00 | None | Heavy Movie 02 +096 | 2026-01-17 00:00:00 - 2026-01-17 00:30:00 | None | Heavy Movie 01 +097 | 2026-01-17 00:30:00 - 2026-01-17 01:00:00 | None | Heavy Movie 02 +098 | 2026-01-17 01:00:00 - 2026-01-17 01:30:00 | None | Light Movie 04 +099 | 2026-01-17 01:30:00 - 2026-01-17 02:00:00 | None | Heavy Movie 01 +100 | 2026-01-17 02:00:00 - 2026-01-17 02:30:00 | None | Heavy Movie 02 +101 | 2026-01-17 02:30:00 - 2026-01-17 03:00:00 | None | Heavy Movie 01 +102 | 2026-01-17 03:00:00 - 2026-01-17 03:30:00 | None | Light Movie 02 +103 | 2026-01-17 03:30:00 - 2026-01-17 04:00:00 | None | Heavy Movie 02 +104 | 2026-01-17 04:00:00 - 2026-01-17 04:30:00 | None | Heavy Movie 01 +105 | 2026-01-17 04:30:00 - 2026-01-17 05:00:00 | None | Heavy Movie 02 +106 | 2026-01-17 05:00:00 - 2026-01-17 05:30:00 | None | Light Movie 01 +107 | 2026-01-17 05:30:00 - 2026-01-17 06:00:00 | None | Heavy Movie 01 diff --git a/ErsatzTV.Core.Tests/Scheduling/Goldens/PlayoutBuildGoldenTests.cs b/ErsatzTV.Core.Tests/Scheduling/Goldens/PlayoutBuildGoldenTests.cs index 5348f78fc..5d6716488 100644 --- a/ErsatzTV.Core.Tests/Scheduling/Goldens/PlayoutBuildGoldenTests.cs +++ b/ErsatzTV.Core.Tests/Scheduling/Goldens/PlayoutBuildGoldenTests.cs @@ -124,6 +124,16 @@ public class PlayoutBuildGoldenTests [Test] public Task Classic_shuffle() => Verify("classic-shuffle.txt", BuildShufflePlayout); + // Classic + PlaybackOrder.WeightedShuffle (#70): the weight only means anything end-to-end if it survives + // MultiCollection -> MediaCollectionRepository -> CollectionWithItems.Weight -> ShuffleSourceBuilder -> + // WeightedShuffleCollectionEnumerator. The unit tests pin the enumerator's sequence in isolation; this pins + // that the real builder actually distributes by weight. The fixture is deliberately lopsided -- the LIGHTER + // source (weight 1) is the LARGER collection (4 items vs 2) -- so a golden that merely tracked collection + // size, or that ignored weight, would look obviously different from one that honors a 3:1 ratio. + // Same determinism contract as Classic_shuffle: pinned Seed + Continue (Reset would randomize the seed). + [Test] + public Task Classic_weighted() => Verify("classic-weighted.txt", BuildWeightedPlayout); + [Test] [Explicit("Regenerates all playout goldens from current output; review the diff before committing.")] public async Task Regenerate_goldens() @@ -376,6 +386,183 @@ public class PlayoutBuildGoldenTests return (playout.Id, titles); } + private async Task<(List Items, Dictionary Titles)> BuildWeightedPlayout() + { + var cancellationToken = CancellationToken.None; + + var (playoutId, titles) = await SeedWeightedData(cancellationToken); + + var builder = new PlayoutBuilder( + new ConfigElementRepository(_dbContextFactory), + new MediaCollectionRepository(Substitute.For(), _dbContextFactory), + new TelevisionRepository(_dbContextFactory, NullLogger.Instance), + new ArtistRepository(_dbContextFactory), + Substitute.For(), + new MockFileSystem(), + Substitute.For(), + NullLogger.Instance); + + await using TvContext context = _dbContextFactory.CreateDbContext(); + + Playout playout = await context.Playouts + .Include(p => p.ProgramScheduleAnchors) + .ThenInclude(a => a.EnumeratorState) + .Include(p => p.FillGroupIndices) + .ThenInclude(fgi => fgi.EnumeratorState) + .SingleAsync(p => p.Id == playoutId, cancellationToken); + + PlayoutReferenceData referenceData = await GetReferenceData(context, playoutId); + + Either result = await builder.Build( + playout, + referenceData, + PlayoutBuildResult.Empty, + PlayoutBuildMode.Continue, + Start, + Start.AddDays(2), + cancellationToken); + + PlayoutBuildResult buildResult = result.Match( + r => r, + error => throw new AssertionException($"Build returned error: {error.Value}")); + + return (buildResult.AddedItems, titles); + } + + private async Task<(int PlayoutId, Dictionary Titles)> SeedWeightedData( + CancellationToken cancellationToken) + { + await using TvContext context = _dbContextFactory.CreateDbContext(); + + var path = new LibraryPath { Path = "Weighted LibraryPath" }; + var library = new LocalLibrary + { + MediaKind = LibraryMediaKind.Movies, + Paths = new List { path }, + MediaSource = new LocalMediaSource() + }; + await context.Libraries.AddAsync(library, cancellationToken); + await context.SaveChangesAsync(cancellationToken); + + // Titles carry their source so the golden reads as a rotation rather than a list of ids. + // "Heavy" is the SMALL collection with weight 3; "Light" is the LARGE one with weight 1. + async Task> SeedMovies(string prefix, int count) + { + var movies = new List(); + for (var i = 1; i <= count; i++) + { + movies.Add( + new Movie + { + MediaVersions = new List { new() { Duration = TimeSpan.FromMinutes(30) } }, + MovieMetadata = new List + { + new() + { + Title = $"{prefix} Movie {i:D2}", + ReleaseDate = new DateTime(2010, 1, 1).AddDays(i) + } + }, + LibraryPath = path, + LibraryPathId = path.Id + }); + } + + await context.Movies.AddRangeAsync(movies, cancellationToken); + await context.SaveChangesAsync(cancellationToken); + return movies; + } + + List heavy = await SeedMovies("Heavy", 2); + List light = await SeedMovies("Light", 4); + + var titles = heavy.Concat(light).ToDictionary(m => m.Id, m => m.MovieMetadata[0].Title); + + var heavyCollection = new Collection + { + Name = "Weighted Heavy Collection", + MediaItems = heavy.Cast().ToList() + }; + var lightCollection = new Collection + { + Name = "Weighted Light Collection", + MediaItems = light.Cast().ToList() + }; + await context.Collections.AddRangeAsync([heavyCollection, lightCollection], cancellationToken); + await context.SaveChangesAsync(cancellationToken); + + var multiCollection = new MultiCollection + { + Name = "Weighted Multi Collection", + MultiCollectionItems = + [ + new MultiCollectionItem + { + CollectionId = heavyCollection.Id, + ScheduleAsGroup = true, + PlaybackOrder = PlaybackOrder.WeightedShuffle, + Weight = 3 + }, + new MultiCollectionItem + { + CollectionId = lightCollection.Id, + ScheduleAsGroup = true, + PlaybackOrder = PlaybackOrder.WeightedShuffle, + Weight = 1 + } + ] + }; + await context.MultiCollections.AddAsync(multiCollection, cancellationToken); + await context.SaveChangesAsync(cancellationToken); + + var scheduleItems = new List + { + new ProgramScheduleItemDuration + { + MultiCollection = multiCollection, + MultiCollectionId = multiCollection.Id, + CollectionType = CollectionType.MultiCollection, + PlayoutDuration = TimeSpan.FromHours(1), + TailMode = TailMode.None, + PlaybackOrder = PlaybackOrder.WeightedShuffle + } + }; + + var ffmpegProfile = new FFmpegProfile { Name = "Weighted FFmpeg Profile" }; + await context.FFmpegProfiles.AddAsync(ffmpegProfile, cancellationToken); + await context.SaveChangesAsync(cancellationToken); + + // Number/GUID must be globally unique: every golden fixture shares one in-memory DB. + var channel = new Channel(Guid.Parse("00000000-0000-0000-0000-000000000005")) + { + Name = "Weighted Test Channel", + Number = "5", + FFmpegProfile = ffmpegProfile, + FFmpegProfileId = ffmpegProfile.Id + }; + await context.Channels.AddAsync(channel, cancellationToken); + await context.SaveChangesAsync(cancellationToken); + + var schedule = new ProgramSchedule { Name = "Weighted Test Schedule", Items = scheduleItems }; + await context.ProgramSchedules.AddAsync(schedule, cancellationToken); + await context.SaveChangesAsync(cancellationToken); + + var playout = new Playout + { + Channel = channel, + ChannelId = channel.Id, + ProgramSchedule = schedule, + ProgramScheduleId = schedule.Id, + ScheduleKind = PlayoutScheduleKind.Classic, + // Pinned so the weighted rotation is deterministic; Continue preserves it (Reset would overwrite). + Seed = 7654321 + }; + await context.Playouts.AddAsync(playout, cancellationToken); + await context.SaveChangesAsync(cancellationToken); + + return (playout.Id, titles); + } + private async Task<(int PlayoutId, Dictionary Titles)> SeedData(CancellationToken cancellationToken) { await using TvContext context = _dbContextFactory.CreateDbContext(); -- 2.47.3 From 17b39c8cff34ae084393596db2d26b256cc5b4de Mon Sep 17 00:00:00 2001 From: Timothy Date: Fri, 17 Jul 2026 15:57:59 +0200 Subject: [PATCH 11/12] docs(70): re-derive the stale-claim fix by grep instead of working the review's list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round-3 review returned BLOCKED: must-fix (b) was not closed. It was right, and the root cause it named is the point of this commit — the previous correction "was scoped to the four sites the reviewer listed rather than re-derived by grep". Fixing the list is not fixing the class. That is the same failure as B1, where the gate covered the two writers already in hand and missed CreateChannelFromLineup. Re-grepped the behavior class instead. Three survivors, two of them missed and one freshly introduced by the correction itself: - CreateMultiCollectionHandler.cs — the create twin of a comment whose UPDATE twin I corrected and whose create twin I never opened. Present tense, and contradicted by two tests in this same PR. - decisions.md — corrected one line in that file and left its sibling. - MultiCollectionItemWeight.cs (and its decisions.md mirror) — the ceiling rationale still claimed unbounded weights overflow the sum. They cannot: EffectiveWeight clamps before every sum and CycleLength widens to long. The earlier pass pattern-matched on the word "filtered" and left the identical defect on the ceiling. The ceiling's real job is the floor's argument — a billion is not a share of airtime any more than 0 is — so it now says that, and credits the clamp with the arithmetic safety it actually provides. Also corrected the writer claim to the right predicate: not "two persisting writers" (Add*ToPlaylist and Trakt persist it too, hardcoded) but two writers that persist a CALLER-SUPPLIED order. The full set is now classified persists-caller-value / persists-hardcoded / in-memory, including Engine/PlaylistHelper, which the previous "two Preview handlers" phrasing missed. That bullet has been wrong three times in the same shape; it now says so, since a lesson that keeps being re-learned is worth recording as a pattern rather than a fact. The BOM check caught this commit re-adding a BOM to the one file patched with utf-8-sig — the same trap, an hour after writing it down. Stripped; the mechanical pre-push check is what makes that survivable. Core.Tests 566, ErsatzTV.Tests 1673, 0 failed. Format verify exit 0. decisions.md +90/-0 (append-only guard green). Refs #70 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Commands/CreateMultiCollectionHandler.cs | 7 +++- .../Collection/MultiCollectionItemWeight.cs | 9 ++-- docs/decisions.md | 41 ++++++++++++------- 3 files changed, 37 insertions(+), 20 deletions(-) diff --git a/ErsatzTV.Application/MediaCollections/Commands/CreateMultiCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/CreateMultiCollectionHandler.cs index 86d1384f3..1bbf53392 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/CreateMultiCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/CreateMultiCollectionHandler.cs @@ -92,8 +92,11 @@ public class CreateMultiCollectionHandler : .ToList() }); - // Bounds are shared with the update path so the two cannot drift: an out-of-range weight silently - // removes the source from the rotation (0) or overflows the rotation arithmetic (huge). See #70. + // Bounds are shared with the update path so the two cannot drift -- they silently disagreed before #402: + // EF's HasDefaultValue substitutes 1 for a 0 on INSERT (0 reads as "not set") while an UPDATE writes the 0 + // through, so the same input landed differently depending on the verb. The enumerator clamps out-of-range + // weights, so neither a 0 nor a huge value can reach the rotation; this gate refuses input that has no + // meaning on a share-of-airtime scale, and keeps create and update honest with each other. See #70. private static Validation ValidateWeights(CreateMultiCollection request) => request.Items.All(i => MultiCollectionItemWeight.IsValid(i.Weight)) ? Unit.Default diff --git a/ErsatzTV.Core/Domain/Collection/MultiCollectionItemWeight.cs b/ErsatzTV.Core/Domain/Collection/MultiCollectionItemWeight.cs index 1b435b9fa..e5b701646 100644 --- a/ErsatzTV.Core/Domain/Collection/MultiCollectionItemWeight.cs +++ b/ErsatzTV.Core/Domain/Collection/MultiCollectionItemWeight.cs @@ -11,9 +11,12 @@ namespace ErsatzTV.Core.Domain; /// belt-and-braces, not duplicates — neither is redundant. /// /// -/// The ceiling keeps the rotation arithmetic in range: the enumerator sums weights and scales the rotation -/// by items * totalWeight / weight, and unbounded values overflow that sum (checked arithmetic, so it -/// throws) inside a playout build. +/// The ceiling is the same argument as the floor, not an arithmetic guard. A weight of a billion is not a +/// share of airtime any more than 0 is, so it is refused at the boundary rather than silently reinterpreted. +/// The rotation arithmetic is made safe by EffectiveWeight's clamp (and by CycleLength summing +/// to long) — not by this constant: with the clamp in place a sum cannot overflow whatever the +/// stored value is, which is exactly why a row that predates this gate is still safe. +/// An_Enormous_Weight_Does_Not_Overflow_The_Rotation pins that. /// /// public static class MultiCollectionItemWeight diff --git a/docs/decisions.md b/docs/decisions.md index a0119733d..a5db51d23 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -1451,22 +1451,33 @@ and "no single show dominates" — as **one** order, because fair-share is the e orders by *name*, so `Enum.Parse` accepts it regardless) log a warning when an order falls through unhandled. Making those pre-existing fallbacks loud is a real but separate defect class: **#403**, an explicit non-goal here. - - **There are TWO persisting writers of `PlaylistItem.PlaybackOrder`, and the second is not obvious.** - `ReplacePlaylistItems` is the expected one; **`CreateChannelFromLineup`** is the other — a lineup of 2+ - entries is stored as a `Playlist`, and its own guard only covered *MultiCollection* entries, so a - multi-entry lineup of plain collections slipped `WeightedShuffle` straight through to - `PlaylistEnumerator`'s null-drop. (`ReplaceBlockItems` is the separate writer of - `BlockItem.PlaybackOrder`; `Add*ToPlaylist` and the Trakt path are safe only because they hardcode their - order, and the two Preview handlers build in memory without persisting.) Caught by adversarial review of - PR #402 **after** the first gates were written and this very bullet had recorded the perimeter as - complete — twice, since the correction itself first miscounted the writers by conflating the two fields. - Before trusting any "this order can't reach that engine" claim, grep every writer of **each field - separately**; the non-obvious composite handler is the one that gets missed. + - **Exactly two writers persist a CALLER-SUPPLIED `PlaylistItem.PlaybackOrder`, and the second is not + obvious.** `ReplacePlaylistItems` is the expected one; **`CreateChannelFromLineup`** is the other — a + lineup of 2+ entries is stored as a `Playlist`, and its own guard only covered *MultiCollection* entries, + so a multi-entry lineup of plain collections slipped `WeightedShuffle` straight through to + `PlaylistEnumerator`'s null-drop. Both are gated. The full writer set, since "persisting writer" alone is + the wrong axis: `Add*ToPlaylist` and `TraktCommandBase` **do** persist the field but hardcode it + (`Shuffle`/`Chronological`), so no caller value reaches them; `ReplaceBlockItems` writes the *different* + field `BlockItem.PlaybackOrder` (also gated); and `PreviewPlaylistPlayout`, `PreviewBlockPlayout` and + `Engine/PlaylistHelper` build in memory without persisting. + **This bullet has now been wrong three times, each time in the same shape**: it recorded the perimeter as + complete when the gate covered only the writers already in hand (missing `CreateChannelFromLineup`); the + first correction miscounted by conflating `PlaylistItem` with `BlockItem`; the second said "two persisting + writers" when the true predicate is *two writers that persist a caller-supplied order*. The failure is + always **enumerating from a list instead of re-deriving from a grep** — which is also how the + "0-weight is filtered out" claims survived their own correction. Before trusting any "this order can't + reach that engine" claim, grep every writer of **each field separately** and classify each as + persists-caller-value / persists-hardcoded / in-memory. The non-obvious composite handler is the one that + gets missed. - **Weight is bounded at the write path** (`MultiCollectionItemWeight`, 1..1000) and clamped again in the - enumerator. Both are needed: **EF's `HasDefaultValue(1)` substitutes 1 for a `0` on INSERT** (0 reads as - "not set") **but an UPDATE writes the 0 through**, so create and update disagreed on the same input, and - a 0-weight source was filtered out of the rotation — deleting it from the channel silently. The ceiling - keeps `Sum(weights)` (checked arithmetic) from overflowing inside a playout build. + enumerator (`EffectiveWeight`). They do **different** jobs, which is why both stay: **EF's + `HasDefaultValue(1)` substitutes 1 for a `0` on INSERT** (0 reads as "not set") **but an UPDATE writes the + 0 through**, so create and update disagreed on the same input — the gate makes them agree and refuses + values that mean nothing on a share-of-airtime scale (0, negative, or absurdly large). The **clamp** is + what makes the rotation arithmetic safe for rows that predate the gate: it bounds every weight before the + sum, so `Sum(weights)` cannot overflow regardless of what is stored. Historical note: an earlier revision + of this PR *filtered* `Weight > 0` instead of clamping, which silently deleted a 0-weight source from the + channel; the clamp replaced it, and any comment still describing that filter is stale. - **Weight is returned on the multi-collection GET, not just accepted on write.** The update replaces the item list, so a client that reads, edits a name, and writes back would silently reset every weight to the default if the read didn't carry it. Edits ride the existing `Version` token (If-Match/412, §7a). -- 2.47.3 From 4693aa14c3e574248eba564b3f3413393d96fefd Mon Sep 17 00:00:00 2001 From: Timothy Date: Fri, 17 Jul 2026 16:10:36 +0200 Subject: [PATCH 12/12] =?UTF-8?q?docs(70):=20fix=20the=20last=20stale=20cl?= =?UTF-8?q?aim=20=E2=80=94=20found=20by=20reading=20every=20weight=20comme?= =?UTF-8?q?nt,=20not=20by=20grepping=20verbs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round-4 review returned BLOCKED, and its diagnosis is the sharpest of the four: my "re-derived by grep" claim was false. The reviewer's grep included `dropped`; mine had `drop(ped)? every` and missed the bare word — so MultiCollectionConfiguration.cs:38 ("a 0-weight source is dropped by the enumerator") never matched. That is the same failure as the previous round, one level up: I replaced a list of SITES with a list of VERBS I guessed and called it a class. Fourth consecutive incomplete completeness claim, and again the code twin of a doc line I had already corrected. The site is now accurate. The DB default of 1 is still right, but for the reason decisions.md already gives: a 0 backfill means nothing on a share-of-airtime scale. The enumerator clamps such a row to the floor, so it rotates rather than vanishing -- the drop it cited cannot happen since EffectiveWeight replaced the Weight > 0 filter, and A_Non_Positive_Weight_Does_Not_Delete_The_Source pins that. Method changed, not just the text: instead of guessing which verb a stale claim might use, enumerate EVERY comment/doc line mentioning weight across the diff and read them. 41 lines, one false. Both automated passes then produced false positives on different subjects -- a line-based check flagged EffectiveWeight's "must not drop..." (the framing verb wraps to the previous line), and a sentence-aware one flagged ReplacePlaylistItemsHandler's "the item is dropped from the playlist" (true, and about unknown ORDERS, not weights: PlaylistEnumerator really has 0 default arms and gates on `enumerator is not null` at :210). Both were read and cleared rather than counted, which is the only reason this message can say what it says. Verification: Build 0 errors; Core.Tests 566; ErsatzTV.Tests 1673; 0 failed; no BOM on any touched non-generated .cs; decisions-guard exit 0. Refs #70 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Collection/MultiCollectionConfiguration.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ErsatzTV.Infrastructure/Data/Configurations/Collection/MultiCollectionConfiguration.cs b/ErsatzTV.Infrastructure/Data/Configurations/Collection/MultiCollectionConfiguration.cs index 888fa1dd7..264fdccdb 100644 --- a/ErsatzTV.Infrastructure/Data/Configurations/Collection/MultiCollectionConfiguration.cs +++ b/ErsatzTV.Infrastructure/Data/Configurations/Collection/MultiCollectionConfiguration.cs @@ -34,8 +34,10 @@ public class MultiCollectionConfiguration : IEntityTypeConfiguration new { mci.MultiCollectionId, mci.CollectionId }); - // default 1 = fair share (#70). Must be a DB default too: existing rows would - // otherwise migrate to 0, and a 0-weight source is dropped by the enumerator. + // default 1 = fair share (#70). Must be a DB default too, or existing rows migrate to 0 -- + // a value that means nothing on a share-of-airtime scale. The enumerator clamps such a + // row to the floor, so it rotates rather than vanishing; the backfill should still be + // right at the source. j.Property(mci => mci.Weight).HasDefaultValue(1); }); -- 2.47.3