From 000fc78fd3b012e2a78c3e4be5919947d683062f Mon Sep 17 00:00:00 2001 From: Jason Dove Date: Sun, 8 Jan 2023 23:22:17 -0600 Subject: [PATCH] add alternate schedule system (#1105) * start to add program schedule alternates * edit days of the week * editor improvements * save changes * build playouts using alternate schedules * reset playout as needed * add priority message --- .../Queries/GetChannelNameByPlayoutId.cs | 3 + .../GetChannelNameByPlayoutIdHandler.cs | 24 + .../Playouts/Commands/BuildPlayoutHandler.cs | 31 + .../ReplacePlayoutAlternateSchedule.cs | 9 + .../ReplacePlayoutAlternateScheduleItems.cs | 6 + ...acePlayoutAlternateScheduleItemsHandler.cs | 147 + ErsatzTV.Application/Playouts/Mapper.cs | 10 + .../PlayoutAlternateScheduleViewModel.cs | 9 + .../Queries/GetPlayoutAlternateSchedules.cs | 3 + .../GetPlayoutAlternateSchedulesHandler.cs | 53 + .../Scheduling/PlayoutBuilderTests.cs | 54 +- ErsatzTV.Core/Domain/Playout.cs | 1 + ErsatzTV.Core/Domain/ProgramSchedule.cs | 1 + .../Domain/ProgramScheduleAlternate.cs | 28 + ErsatzTV.Core/Scheduling/PlayoutBuilder.cs | 60 +- .../Scheduling/PlayoutScheduleSelector.cs | 40 + .../Configurations/CollectionValueComparer.cs | 13 + .../EnumCollectionJsonValueConverter.cs | 14 + .../Configurations/PlayoutConfiguration.cs | 5 + .../ProgramScheduleAlternateConfiguration.cs | 34 + .../ProgramScheduleConfiguration.cs | 5 + ErsatzTV.Infrastructure/Data/TvContext.cs | 1 + ..._Add_ProgramScheduleAlternates.Designer.cs | 4394 +++++++++++++++++ ...108192830_Add_ProgramScheduleAlternates.cs | 61 + .../Migrations/TvContextModelSnapshot.cs | 752 +-- .../PlayoutAlternateSchedulesEditor.razor | 486 ++ ErsatzTV/Pages/Playouts.razor | 9 +- ...AlternateScheduleEditViewModelValidator.cs | 12 + .../PlayoutAlternateScheduleEditViewModel.cs | 13 + 29 files changed, 5893 insertions(+), 385 deletions(-) create mode 100644 ErsatzTV.Application/Channels/Queries/GetChannelNameByPlayoutId.cs create mode 100644 ErsatzTV.Application/Channels/Queries/GetChannelNameByPlayoutIdHandler.cs create mode 100644 ErsatzTV.Application/Playouts/Commands/ReplacePlayoutAlternateSchedule.cs create mode 100644 ErsatzTV.Application/Playouts/Commands/ReplacePlayoutAlternateScheduleItems.cs create mode 100644 ErsatzTV.Application/Playouts/Commands/ReplacePlayoutAlternateScheduleItemsHandler.cs create mode 100644 ErsatzTV.Application/Playouts/PlayoutAlternateScheduleViewModel.cs create mode 100644 ErsatzTV.Application/Playouts/Queries/GetPlayoutAlternateSchedules.cs create mode 100644 ErsatzTV.Application/Playouts/Queries/GetPlayoutAlternateSchedulesHandler.cs create mode 100644 ErsatzTV.Core/Domain/ProgramScheduleAlternate.cs create mode 100644 ErsatzTV.Core/Scheduling/PlayoutScheduleSelector.cs create mode 100644 ErsatzTV.Infrastructure/Data/Configurations/CollectionValueComparer.cs create mode 100644 ErsatzTV.Infrastructure/Data/Configurations/EnumCollectionJsonValueConverter.cs create mode 100644 ErsatzTV.Infrastructure/Data/Configurations/ProgramScheduleAlternateConfiguration.cs create mode 100644 ErsatzTV.Infrastructure/Migrations/20230108192830_Add_ProgramScheduleAlternates.Designer.cs create mode 100644 ErsatzTV.Infrastructure/Migrations/20230108192830_Add_ProgramScheduleAlternates.cs create mode 100644 ErsatzTV/Pages/PlayoutAlternateSchedulesEditor.razor create mode 100644 ErsatzTV/Validators/PlayoutAlternateScheduleEditViewModelValidator.cs create mode 100644 ErsatzTV/ViewModels/PlayoutAlternateScheduleEditViewModel.cs diff --git a/ErsatzTV.Application/Channels/Queries/GetChannelNameByPlayoutId.cs b/ErsatzTV.Application/Channels/Queries/GetChannelNameByPlayoutId.cs new file mode 100644 index 000000000..58bb914a9 --- /dev/null +++ b/ErsatzTV.Application/Channels/Queries/GetChannelNameByPlayoutId.cs @@ -0,0 +1,3 @@ +namespace ErsatzTV.Application.Channels; + +public record GetChannelNameByPlayoutId(int PlayoutId) : IRequest>; diff --git a/ErsatzTV.Application/Channels/Queries/GetChannelNameByPlayoutIdHandler.cs b/ErsatzTV.Application/Channels/Queries/GetChannelNameByPlayoutIdHandler.cs new file mode 100644 index 000000000..e7cd9aaf7 --- /dev/null +++ b/ErsatzTV.Application/Channels/Queries/GetChannelNameByPlayoutIdHandler.cs @@ -0,0 +1,24 @@ +using ErsatzTV.Infrastructure.Data; +using ErsatzTV.Infrastructure.Extensions; +using Microsoft.EntityFrameworkCore; + +namespace ErsatzTV.Application.Channels; + +public class GetChannelNameByPlayoutIdHandler : IRequestHandler> +{ + private readonly IDbContextFactory _dbContextFactory; + + public GetChannelNameByPlayoutIdHandler(IDbContextFactory dbContextFactory) + { + _dbContextFactory = dbContextFactory; + } + + public async Task> Handle(GetChannelNameByPlayoutId request, CancellationToken cancellationToken) + { + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + return await dbContext.Playouts + .Include(p => p.Channel) + .SelectOneAsync(p => p.Id, p => p.Id == request.PlayoutId) + .MapT(p => p.Channel.Name); + } +} diff --git a/ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs b/ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs index b7f437d9a..e2576cfe3 100644 --- a/ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs +++ b/ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs @@ -75,6 +75,36 @@ public class BuildPlayoutHandler : IRequestHandler p.Channel) .Include(p => p.Items) + + .Include(p => p.ProgramScheduleAlternates) + .ThenInclude(a => a.ProgramSchedule) + .ThenInclude(ps => ps.Items) + .ThenInclude(psi => psi.Collection) + .Include(p => p.ProgramScheduleAlternates) + .ThenInclude(a => a.ProgramSchedule) + .ThenInclude(ps => ps.Items) + .ThenInclude(psi => psi.MediaItem) + .Include(p => p.ProgramScheduleAlternates) + .ThenInclude(a => a.ProgramSchedule) + .ThenInclude(ps => ps.Items) + .ThenInclude(psi => psi.PreRollFiller) + .Include(p => p.ProgramScheduleAlternates) + .ThenInclude(a => a.ProgramSchedule) + .ThenInclude(ps => ps.Items) + .ThenInclude(psi => psi.MidRollFiller) + .Include(p => p.ProgramScheduleAlternates) + .ThenInclude(a => a.ProgramSchedule) + .ThenInclude(ps => ps.Items) + .ThenInclude(psi => psi.PostRollFiller) + .Include(p => p.ProgramScheduleAlternates) + .ThenInclude(a => a.ProgramSchedule) + .ThenInclude(ps => ps.Items) + .ThenInclude(psi => psi.TailFiller) + .Include(p => p.ProgramScheduleAlternates) + .ThenInclude(a => a.ProgramSchedule) + .ThenInclude(ps => ps.Items) + .ThenInclude(psi => psi.FallbackFiller) + .Include(p => p.ProgramScheduleAnchors) .ThenInclude(a => a.MediaItem) .Include(p => p.ProgramSchedule) @@ -98,6 +128,7 @@ public class BuildPlayoutHandler : IRequestHandler p.ProgramSchedule) .ThenInclude(ps => ps.Items) .ThenInclude(psi => psi.FallbackFiller) + .SelectOneAsync(p => p.Id, p => p.Id == buildPlayout.PlayoutId) .Map(o => o.ToValidation("Playout does not exist.")); } diff --git a/ErsatzTV.Application/Playouts/Commands/ReplacePlayoutAlternateSchedule.cs b/ErsatzTV.Application/Playouts/Commands/ReplacePlayoutAlternateSchedule.cs new file mode 100644 index 000000000..f85db7545 --- /dev/null +++ b/ErsatzTV.Application/Playouts/Commands/ReplacePlayoutAlternateSchedule.cs @@ -0,0 +1,9 @@ +namespace ErsatzTV.Application.Playouts; + +public record ReplacePlayoutAlternateSchedule( + int Id, + int Index, + int ProgramScheduleId, + List DaysOfWeek, + List DaysOfMonth, + List MonthsOfYear); diff --git a/ErsatzTV.Application/Playouts/Commands/ReplacePlayoutAlternateScheduleItems.cs b/ErsatzTV.Application/Playouts/Commands/ReplacePlayoutAlternateScheduleItems.cs new file mode 100644 index 000000000..5bb736c4a --- /dev/null +++ b/ErsatzTV.Application/Playouts/Commands/ReplacePlayoutAlternateScheduleItems.cs @@ -0,0 +1,6 @@ +using ErsatzTV.Core; + +namespace ErsatzTV.Application.Playouts; + +public record ReplacePlayoutAlternateScheduleItems + (int PlayoutId, List Items) : IRequest>; diff --git a/ErsatzTV.Application/Playouts/Commands/ReplacePlayoutAlternateScheduleItemsHandler.cs b/ErsatzTV.Application/Playouts/Commands/ReplacePlayoutAlternateScheduleItemsHandler.cs new file mode 100644 index 000000000..cdfbe1d30 --- /dev/null +++ b/ErsatzTV.Application/Playouts/Commands/ReplacePlayoutAlternateScheduleItemsHandler.cs @@ -0,0 +1,147 @@ +using System.Threading.Channels; +using ErsatzTV.Core; +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Scheduling; +using ErsatzTV.Infrastructure.Data; +using ErsatzTV.Infrastructure.Extensions; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace ErsatzTV.Application.Playouts; + +public class ReplacePlayoutAlternateScheduleItemsHandler : + IRequestHandler> +{ + private readonly IDbContextFactory _dbContextFactory; + private readonly ChannelWriter _channel; + private readonly ILogger _logger; + + public ReplacePlayoutAlternateScheduleItemsHandler( + IDbContextFactory dbContextFactory, + ChannelWriter channel, + ILogger logger) + { + _dbContextFactory = dbContextFactory; + _channel = channel; + _logger = logger; + } + + public async Task> Handle( + ReplacePlayoutAlternateScheduleItems request, + CancellationToken cancellationToken) + { + // TODO: validate that items is not empty + + try + { + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + + Option maybePlayout = await dbContext.Playouts + .Include(p => p.ProgramSchedule) + .Include(p => p.ProgramScheduleAlternates) + .ThenInclude(p => p.ProgramSchedule) + .SelectOneAsync(p => p.Id, p => p.Id == request.PlayoutId); + + foreach (Playout playout in maybePlayout) + { + ProgramSchedule existingDefault = playout.ProgramSchedule; + + // exclude highest index + int maxIndex = request.Items.Map(x => x.Index).Max(); + ReplacePlayoutAlternateSchedule highest = request.Items.First(x => x.Index == maxIndex); + + ProgramScheduleAlternate[] existing = playout.ProgramScheduleAlternates.ToArray(); + + var incoming = request.Items.Except(new[] { highest }).ToList(); + + var toAdd = incoming.Filter(x => existing.All(e => e.Id != x.Id)).ToList(); + var toRemove = existing.Filter(e => incoming.All(m => m.Id != e.Id)).ToList(); + var toUpdate = incoming.Except(toAdd).ToList(); + + playout.ProgramScheduleAlternates.RemoveAll(toRemove.Contains); + + foreach (ReplacePlayoutAlternateSchedule add in toAdd) + { + playout.ProgramScheduleAlternates.Add( + new ProgramScheduleAlternate + { + PlayoutId = playout.Id, + Index = add.Index, + ProgramScheduleId = add.ProgramScheduleId, + DaysOfWeek = add.DaysOfWeek, + DaysOfMonth = add.DaysOfMonth, + MonthsOfYear = add.MonthsOfYear + }); + } + + foreach (ReplacePlayoutAlternateSchedule update in toUpdate) + { + foreach (ProgramScheduleAlternate ex in existing.Filter(x => x.Id == update.Id)) + { + ex.Index = update.Index; + ex.ProgramScheduleId = update.ProgramScheduleId; + ex.DaysOfWeek = update.DaysOfWeek; + ex.DaysOfMonth = update.DaysOfMonth; + ex.MonthsOfYear = update.MonthsOfYear; + } + } + + // save highest index directly to playout + if (playout.ProgramScheduleId != highest.ProgramScheduleId) + { + playout.ProgramScheduleId = highest.ProgramScheduleId; + } + + await dbContext.SaveChangesAsync(cancellationToken); + + Option maybePlayoutItem = await dbContext.PlayoutItems + .Filter(pi => pi.PlayoutId == request.PlayoutId) + .OrderByDescending(pi => pi.Start) + .FirstOrDefaultAsync(cancellationToken) + .Map(Optional); + + foreach (PlayoutItem playoutItem in maybePlayoutItem) + { + DateTimeOffset start = DateTimeOffset.Now; + var daysToCheck = Enumerable.Range(0, (playoutItem.StartOffset - start).Days + 1) + .Select(d => start.AddDays(d)) + .ToList(); + + foreach (DateTimeOffset dayToCheck in daysToCheck) + { + ProgramSchedule oldSchedule = PlayoutScheduleSelector.GetProgramScheduleFor( + existingDefault, + existing, + dayToCheck); + + ProgramSchedule newSchedule = PlayoutScheduleSelector.GetProgramScheduleFor( + playout.ProgramSchedule, + playout.ProgramScheduleAlternates, + dayToCheck); + + if (oldSchedule.Id != newSchedule.Id) + { + _logger.LogInformation( + "Alternate schedule change detected for day {Day}, schedule {One} => {Two}; will refresh playout", + dayToCheck, + oldSchedule.Name, + newSchedule.Name); + + await _channel.WriteAsync( + new BuildPlayout(request.PlayoutId, PlayoutBuildMode.Refresh), + cancellationToken); + + break; + } + } + } + } + + return Unit.Default; + } + catch (Exception ex) + { + return BaseError.New(ex.Message); + } + } +} diff --git a/ErsatzTV.Application/Playouts/Mapper.cs b/ErsatzTV.Application/Playouts/Mapper.cs index 27917bb4d..ad82fd92b 100644 --- a/ErsatzTV.Application/Playouts/Mapper.cs +++ b/ErsatzTV.Application/Playouts/Mapper.cs @@ -10,6 +10,16 @@ internal static class Mapper playoutItem.StartOffset, GetDisplayDuration(playoutItem.FinishOffset - playoutItem.StartOffset)); + internal static PlayoutAlternateScheduleViewModel ProjectToViewModel( + ProgramScheduleAlternate programScheduleAlternate) => + new( + programScheduleAlternate.Id, + programScheduleAlternate.Index, + programScheduleAlternate.ProgramScheduleId, + programScheduleAlternate.DaysOfWeek, + programScheduleAlternate.DaysOfMonth, + programScheduleAlternate.MonthsOfYear); + private static string GetDisplayTitle(PlayoutItem playoutItem) { switch (playoutItem.MediaItem) diff --git a/ErsatzTV.Application/Playouts/PlayoutAlternateScheduleViewModel.cs b/ErsatzTV.Application/Playouts/PlayoutAlternateScheduleViewModel.cs new file mode 100644 index 000000000..e34db130a --- /dev/null +++ b/ErsatzTV.Application/Playouts/PlayoutAlternateScheduleViewModel.cs @@ -0,0 +1,9 @@ +namespace ErsatzTV.Application.Playouts; + +public record PlayoutAlternateScheduleViewModel( + int Id, + int Index, + int ProgramScheduleId, + ICollection DaysOfWeek, + ICollection DaysOfMonth, + ICollection MonthsOfYear); diff --git a/ErsatzTV.Application/Playouts/Queries/GetPlayoutAlternateSchedules.cs b/ErsatzTV.Application/Playouts/Queries/GetPlayoutAlternateSchedules.cs new file mode 100644 index 000000000..985877b0b --- /dev/null +++ b/ErsatzTV.Application/Playouts/Queries/GetPlayoutAlternateSchedules.cs @@ -0,0 +1,3 @@ +namespace ErsatzTV.Application.Playouts; + +public record GetPlayoutAlternateSchedules(int PlayoutId) : IRequest>; diff --git a/ErsatzTV.Application/Playouts/Queries/GetPlayoutAlternateSchedulesHandler.cs b/ErsatzTV.Application/Playouts/Queries/GetPlayoutAlternateSchedulesHandler.cs new file mode 100644 index 000000000..bb2636b00 --- /dev/null +++ b/ErsatzTV.Application/Playouts/Queries/GetPlayoutAlternateSchedulesHandler.cs @@ -0,0 +1,53 @@ +using ErsatzTV.Core.Domain; +using ErsatzTV.Infrastructure.Data; +using ErsatzTV.Infrastructure.Extensions; +using Microsoft.EntityFrameworkCore; +using static ErsatzTV.Application.Playouts.Mapper; + +namespace ErsatzTV.Application.Playouts; + +public class GetPlayoutAlternateSchedulesHandler : + IRequestHandler> +{ + private readonly IDbContextFactory _dbContextFactory; + + public GetPlayoutAlternateSchedulesHandler(IDbContextFactory dbContextFactory) => + _dbContextFactory = dbContextFactory; + + public async Task> Handle( + GetPlayoutAlternateSchedules request, + CancellationToken cancellationToken) + { + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + + List result = await dbContext.ProgramScheduleAlternates + .Filter(psa => psa.PlayoutId == request.PlayoutId) + .Include(psa => psa.ProgramSchedule) + .Map(psa => ProjectToViewModel(psa)) + .ToListAsync(cancellationToken); + + Option maybeDefaultSchedule = await dbContext.Playouts + .Include(p => p.ProgramSchedule) + .SelectOneAsync(p => p.Id, p => p.Id == request.PlayoutId) + .MapT(p => p.ProgramSchedule); + + foreach (ProgramSchedule defaultSchedule in maybeDefaultSchedule) + { + var psa = new ProgramScheduleAlternate + { + Id = -1, + PlayoutId = request.PlayoutId, + ProgramScheduleId = defaultSchedule.Id, + ProgramSchedule = defaultSchedule, + Index = result.Map(i => i.Index).DefaultIfEmpty().Max() + 1, + DaysOfMonth = ProgramScheduleAlternate.AllDaysOfMonth(), + DaysOfWeek = ProgramScheduleAlternate.AllDaysOfWeek(), + MonthsOfYear = ProgramScheduleAlternate.AllMonthsOfYear() + }; + + result.Add(ProjectToViewModel(psa)); + } + + return result; + } +} diff --git a/ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs b/ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs index 20cf9bd2f..afa42c345 100644 --- a/ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs +++ b/ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs @@ -543,7 +543,8 @@ public class PlayoutBuilderTests }, Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, ProgramScheduleAnchors = new List(), - Items = new List() + Items = new List(), + ProgramScheduleAlternates = new List() }; var configRepo = new Mock(); @@ -639,7 +640,8 @@ public class PlayoutBuilderTests }, Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, ProgramScheduleAnchors = new List(), - Items = new List() + Items = new List(), + ProgramScheduleAlternates = new List() }; var configRepo = new Mock(); @@ -783,7 +785,8 @@ public class PlayoutBuilderTests }, Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, ProgramScheduleAnchors = new List(), - Items = new List() + Items = new List(), + ProgramScheduleAlternates = new List() }; var configRepo = new Mock(); @@ -885,7 +888,8 @@ public class PlayoutBuilderTests }, Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, ProgramScheduleAnchors = new List(), - Items = new List() + Items = new List(), + ProgramScheduleAlternates = new List() }; var configRepo = new Mock(); @@ -996,7 +1000,8 @@ public class PlayoutBuilderTests InFlood = true }, ProgramScheduleAnchors = new List(), - Items = new List() + Items = new List(), + ProgramScheduleAlternates = new List() }; var configRepo = new Mock(); @@ -1100,7 +1105,8 @@ public class PlayoutBuilderTests }, Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, ProgramScheduleAnchors = new List(), - Items = new List() + Items = new List(), + ProgramScheduleAlternates = new List() }; var configRepo = new Mock(); @@ -1208,7 +1214,8 @@ public class PlayoutBuilderTests }, Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, ProgramScheduleAnchors = new List(), - Items = new List() + Items = new List(), + ProgramScheduleAlternates = new List() }; var configRepo = new Mock(); @@ -1321,7 +1328,8 @@ public class PlayoutBuilderTests MultipleRemaining = 2 }, ProgramScheduleAnchors = new List(), - Items = new List() + Items = new List(), + ProgramScheduleAlternates = new List() }; var configRepo = new Mock(); @@ -1423,7 +1431,8 @@ public class PlayoutBuilderTests }, Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, ProgramScheduleAnchors = new List(), - Items = new List() + Items = new List(), + ProgramScheduleAlternates = new List() }; var configRepo = new Mock(); @@ -1536,7 +1545,8 @@ public class PlayoutBuilderTests DurationFinish = HoursAfterMidnight(3).UtcDateTime }, ProgramScheduleAnchors = new List(), - Items = new List() + Items = new List(), + ProgramScheduleAlternates = new List() }; var configRepo = new Mock(); @@ -1660,7 +1670,8 @@ public class PlayoutBuilderTests }, Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, ProgramScheduleAnchors = new List(), - Items = new List() + Items = new List(), + ProgramScheduleAlternates = new List() }; var configRepo = new Mock(); @@ -1776,7 +1787,8 @@ public class PlayoutBuilderTests }, Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, ProgramScheduleAnchors = new List(), - Items = new List() + Items = new List(), + ProgramScheduleAlternates = new List() }; var configRepo = new Mock(); @@ -1852,7 +1864,8 @@ public class PlayoutBuilderTests }, Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, ProgramScheduleAnchors = new List(), - Items = new List() + Items = new List(), + ProgramScheduleAlternates = new List() }; var configRepo = new Mock(); @@ -2049,7 +2062,8 @@ public class PlayoutBuilderTests }, ProgramScheduleAnchors = new List(), - Items = new List() + Items = new List(), + ProgramScheduleAlternates = new List() }; playout.ProgramScheduleAnchors.Add( @@ -2437,7 +2451,8 @@ public class PlayoutBuilderTests InFlood = true }, ProgramScheduleAnchors = new List(), - Items = new List() + Items = new List(), + ProgramScheduleAlternates = new List() }; var configRepo = new Mock(); @@ -2548,7 +2563,8 @@ public class PlayoutBuilderTests MultipleRemaining = 2 }, ProgramScheduleAnchors = new List(), - Items = new List() + Items = new List(), + ProgramScheduleAlternates = new List() }; var configRepo = new Mock(); @@ -2659,7 +2675,8 @@ public class PlayoutBuilderTests DurationFinish = HoursAfterMidnight(3).UtcDateTime }, ProgramScheduleAnchors = new List(), - Items = new List() + Items = new List(), + ProgramScheduleAlternates = new List() }; var configRepo = new Mock(); @@ -2768,7 +2785,8 @@ public class PlayoutBuilderTests ProgramSchedule = new ProgramSchedule { Items = items }, Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, Items = new List(), - ProgramScheduleAnchors = new List() + ProgramScheduleAnchors = new List(), + ProgramScheduleAlternates = new List() }; return new TestData(builder, playout); diff --git a/ErsatzTV.Core/Domain/Playout.cs b/ErsatzTV.Core/Domain/Playout.cs index 3adf88765..9eb70acbe 100644 --- a/ErsatzTV.Core/Domain/Playout.cs +++ b/ErsatzTV.Core/Domain/Playout.cs @@ -7,6 +7,7 @@ public class Playout public Channel Channel { get; set; } public int ProgramScheduleId { get; set; } public ProgramSchedule ProgramSchedule { get; set; } + public List ProgramScheduleAlternates { get; set; } public ProgramSchedulePlayoutType ProgramSchedulePlayoutType { get; set; } public List Items { get; set; } public PlayoutAnchor Anchor { get; set; } diff --git a/ErsatzTV.Core/Domain/ProgramSchedule.cs b/ErsatzTV.Core/Domain/ProgramSchedule.cs index 034fe56d2..09c0ef10e 100644 --- a/ErsatzTV.Core/Domain/ProgramSchedule.cs +++ b/ErsatzTV.Core/Domain/ProgramSchedule.cs @@ -10,4 +10,5 @@ public class ProgramSchedule public bool RandomStartPoint { get; set; } public List Items { get; set; } public List Playouts { get; set; } + public List ProgramScheduleAlternates { get; set; } } diff --git a/ErsatzTV.Core/Domain/ProgramScheduleAlternate.cs b/ErsatzTV.Core/Domain/ProgramScheduleAlternate.cs new file mode 100644 index 000000000..8d9341449 --- /dev/null +++ b/ErsatzTV.Core/Domain/ProgramScheduleAlternate.cs @@ -0,0 +1,28 @@ +namespace ErsatzTV.Core.Domain; + +public class ProgramScheduleAlternate +{ + public static List AllDaysOfWeek() => new() + { + DayOfWeek.Monday, + DayOfWeek.Tuesday, + DayOfWeek.Wednesday, + DayOfWeek.Thursday, + DayOfWeek.Friday, + DayOfWeek.Saturday, + DayOfWeek.Sunday + }; + + public static List AllDaysOfMonth() => Enumerable.Range(1, 31).ToList(); + public static List AllMonthsOfYear() => Enumerable.Range(1, 12).ToList(); + + public int Id { get; set; } + public int PlayoutId { get; set; } + public Playout Playout { get; set; } + public int ProgramScheduleId { get; set; } + public ProgramSchedule ProgramSchedule { get; set; } + public int Index { get; set; } + public ICollection DaysOfWeek { get; set; } + public ICollection DaysOfMonth { get; set; } + public ICollection MonthsOfYear { get; set; } +} diff --git a/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs b/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs index 0519fdb0f..66b8a812a 100644 --- a/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs +++ b/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs @@ -203,7 +203,7 @@ public class PlayoutBuilder : IPlayoutBuilder parameters.Start, parameters.Finish, parameters.CollectionMediaItems, - playout.ProgramSchedule.RandomStartPoint); + true); return playout; } @@ -237,11 +237,7 @@ public class PlayoutBuilder : IPlayoutBuilder Map> collectionMediaItems = await GetCollectionMediaItems(playout); if (!collectionMediaItems.Any()) { - _logger.LogWarning( - "Playout {Playout} schedule {Schedule} has no items", - playout.Channel.Name, - playout.ProgramSchedule.Name); - + _logger.LogWarning("Playout {Playout} has no items", playout.Channel.Name); return None; } @@ -365,13 +361,21 @@ public class PlayoutBuilder : IPlayoutBuilder bool saveAnchorDate, bool randomStartPoint) { - var sortedScheduleItems = playout.ProgramSchedule.Items.OrderBy(i => i.Index).ToList(); + ProgramSchedule activeSchedule = PlayoutScheduleSelector.GetProgramScheduleFor( + playout.ProgramSchedule, + playout.ProgramScheduleAlternates, + playoutStart); + + // random start points are disabled in some scenarios, so ensure it's enabled and active + randomStartPoint = randomStartPoint && activeSchedule.RandomStartPoint; + + var sortedScheduleItems = activeSchedule.Items.OrderBy(i => i.Index).ToList(); CollectionEnumeratorState scheduleItemsEnumeratorState = playout.Anchor?.ScheduleItemsEnumeratorState ?? new CollectionEnumeratorState { Seed = Random.Next(), Index = 0 }; - IScheduleItemsEnumerator scheduleItemsEnumerator = playout.ProgramSchedule.ShuffleScheduleItems - ? new ShuffledScheduleItemsEnumerator(playout.ProgramSchedule.Items, scheduleItemsEnumeratorState) - : new OrderedScheduleItemsEnumerator(playout.ProgramSchedule.Items, scheduleItemsEnumeratorState); + IScheduleItemsEnumerator scheduleItemsEnumerator = activeSchedule.ShuffleScheduleItems + ? new ShuffledScheduleItemsEnumerator(activeSchedule.Items, scheduleItemsEnumeratorState) + : new OrderedScheduleItemsEnumerator(activeSchedule.Items, scheduleItemsEnumeratorState); var collectionEnumerators = new Dictionary(); foreach ((CollectionKey collectionKey, List mediaItems) in collectionMediaItems) { @@ -381,7 +385,13 @@ public class PlayoutBuilder : IPlayoutBuilder PlaybackOrder playbackOrder = maybeScheduleItem .Match(item => item.PlaybackOrder, () => PlaybackOrder.Shuffle); IMediaCollectionEnumerator enumerator = - await GetMediaCollectionEnumerator(playout, collectionKey, mediaItems, playbackOrder, randomStartPoint); + await GetMediaCollectionEnumerator( + playout, + activeSchedule, + collectionKey, + mediaItems, + playbackOrder, + randomStartPoint); collectionEnumerators.Add(collectionKey, enumerator); } @@ -533,7 +543,11 @@ public class PlayoutBuilder : IPlayoutBuilder } // build program schedule anchors - playout.ProgramScheduleAnchors = BuildProgramScheduleAnchors(playout, collectionEnumerators, saveAnchorDate); + playout.ProgramScheduleAnchors = BuildProgramScheduleAnchors( + playout, + activeSchedule, + collectionEnumerators, + saveAnchorDate); return playout; } @@ -541,6 +555,8 @@ public class PlayoutBuilder : IPlayoutBuilder private async Task>> GetCollectionMediaItems(Playout playout) { var collectionKeys = playout.ProgramSchedule.Items + .Append(playout.ProgramScheduleAlternates.Bind(psa => psa.ProgramSchedule.Items)) + .DistinctBy(i => i.Id) .SelectMany(CollectionKeysForItem) .Distinct() .ToList(); @@ -640,6 +656,7 @@ public class PlayoutBuilder : IPlayoutBuilder private static List BuildProgramScheduleAnchors( Playout playout, + ProgramSchedule activeSchedule, Dictionary collectionEnumerators, bool saveAnchorDate) { @@ -665,8 +682,8 @@ public class PlayoutBuilder : IPlayoutBuilder { Playout = playout, PlayoutId = playout.Id, - ProgramSchedule = playout.ProgramSchedule, - ProgramScheduleId = playout.ProgramScheduleId, + ProgramSchedule = activeSchedule, + ProgramScheduleId = activeSchedule.Id, CollectionType = collectionKey.CollectionType, CollectionId = collectionKey.CollectionId, MultiCollectionId = collectionKey.MultiCollectionId, @@ -694,6 +711,7 @@ public class PlayoutBuilder : IPlayoutBuilder private async Task GetMediaCollectionEnumerator( Playout playout, + ProgramSchedule activeSchedule, CollectionKey collectionKey, List mediaItems, PlaybackOrder playbackOrder, @@ -702,7 +720,7 @@ public class PlayoutBuilder : IPlayoutBuilder Option maybeAnchor = playout.ProgramScheduleAnchors .OrderByDescending(a => a.AnchorDate ?? DateTime.MaxValue) .FirstOrDefault( - a => a.ProgramScheduleId == playout.ProgramScheduleId + a => a.ProgramScheduleId == activeSchedule.Id && a.CollectionType == collectionKey.CollectionType && a.CollectionId == collectionKey.CollectionId && a.MultiCollectionId == collectionKey.MultiCollectionId @@ -757,7 +775,7 @@ public class PlayoutBuilder : IPlayoutBuilder return new ShuffleInOrderCollectionEnumerator( await GetCollectionItemsForShuffleInOrder(collectionKey), state, - playout.ProgramSchedule.RandomStartPoint); + activeSchedule.RandomStartPoint); case PlaybackOrder.MultiEpisodeShuffle when collectionKey.CollectionType == ProgramScheduleItemCollectionType.TelevisionShow && collectionKey.MediaItemId.HasValue: @@ -795,7 +813,7 @@ public class PlayoutBuilder : IPlayoutBuilder case PlaybackOrder.MultiEpisodeShuffle: case PlaybackOrder.Shuffle: return new ShuffledMediaCollectionEnumerator( - await GetGroupedMediaItemsForShuffle(playout, mediaItems, collectionKey), + await GetGroupedMediaItemsForShuffle(activeSchedule, mediaItems, collectionKey), state); default: // TODO: handle this error case differently? @@ -804,7 +822,7 @@ public class PlayoutBuilder : IPlayoutBuilder } private async Task> GetGroupedMediaItemsForShuffle( - Playout playout, + ProgramSchedule activeSchedule, List mediaItems, CollectionKey collectionKey) { @@ -816,10 +834,8 @@ public class PlayoutBuilder : IPlayoutBuilder return MultiCollectionGrouper.GroupMediaItems(collections); } - return playout.ProgramSchedule.KeepMultiPartEpisodesTogether - ? MultiPartEpisodeGrouper.GroupMediaItems( - mediaItems, - playout.ProgramSchedule.TreatCollectionsAsShows) + return activeSchedule.KeepMultiPartEpisodesTogether + ? MultiPartEpisodeGrouper.GroupMediaItems(mediaItems, activeSchedule.TreatCollectionsAsShows) : mediaItems.Map(mi => new GroupedMediaItem(mi, null)).ToList(); } diff --git a/ErsatzTV.Core/Scheduling/PlayoutScheduleSelector.cs b/ErsatzTV.Core/Scheduling/PlayoutScheduleSelector.cs new file mode 100644 index 000000000..eb59a4eb5 --- /dev/null +++ b/ErsatzTV.Core/Scheduling/PlayoutScheduleSelector.cs @@ -0,0 +1,40 @@ +using ErsatzTV.Core.Domain; + +namespace ErsatzTV.Core.Scheduling; + +public static class PlayoutScheduleSelector +{ + public static ProgramSchedule GetProgramScheduleFor( + ProgramSchedule defaultSchedule, + IEnumerable alternates, + DateTimeOffset date) + { + foreach (ProgramScheduleAlternate alternate in alternates.OrderBy(x => x.Index)) + { + bool daysOfWeek = alternate.DaysOfWeek.Count is 0 or 7 || + alternate.DaysOfWeek.Contains(date.DayOfWeek); + + if (!daysOfWeek) + { + continue; + } + + bool daysOfMonth = alternate.DaysOfMonth.Count is 0 or 31 || + alternate.DaysOfMonth.Contains(date.Day); + if (!daysOfMonth) + { + continue; + } + + bool monthOfYear = alternate.MonthsOfYear.Count is 0 or 12 || + alternate.MonthsOfYear.Contains(date.Month); + + if (monthOfYear) + { + return alternate.ProgramSchedule; + } + } + + return defaultSchedule; + } +} diff --git a/ErsatzTV.Infrastructure/Data/Configurations/CollectionValueComparer.cs b/ErsatzTV.Infrastructure/Data/Configurations/CollectionValueComparer.cs new file mode 100644 index 000000000..e42f26dee --- /dev/null +++ b/ErsatzTV.Infrastructure/Data/Configurations/CollectionValueComparer.cs @@ -0,0 +1,13 @@ +using Microsoft.EntityFrameworkCore.ChangeTracking; + +namespace ErsatzTV.Infrastructure.Data.Configurations; + +public class CollectionValueComparer : ValueComparer> +{ + public CollectionValueComparer() : base( + (c1, c2) => c1.SequenceEqual(c2), + c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())), + c => c.ToHashSet()) + { + } +} diff --git a/ErsatzTV.Infrastructure/Data/Configurations/EnumCollectionJsonValueConverter.cs b/ErsatzTV.Infrastructure/Data/Configurations/EnumCollectionJsonValueConverter.cs new file mode 100644 index 000000000..6fa56ce41 --- /dev/null +++ b/ErsatzTV.Infrastructure/Data/Configurations/EnumCollectionJsonValueConverter.cs @@ -0,0 +1,14 @@ +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Newtonsoft.Json; + +namespace ErsatzTV.Infrastructure.Data.Configurations; + +public class EnumCollectionJsonValueConverter : ValueConverter, string> where T : Enum +{ + public EnumCollectionJsonValueConverter() : base( + v => JsonConvert.SerializeObject(v.Select(e => e.ToString()).ToList()), + v => JsonConvert.DeserializeObject>(v) + .Select(e => (T)Enum.Parse(typeof(T), e)).ToList()) + { + } +} diff --git a/ErsatzTV.Infrastructure/Data/Configurations/PlayoutConfiguration.cs b/ErsatzTV.Infrastructure/Data/Configurations/PlayoutConfiguration.cs index 3e350dbb6..3a9750346 100644 --- a/ErsatzTV.Infrastructure/Data/Configurations/PlayoutConfiguration.cs +++ b/ErsatzTV.Infrastructure/Data/Configurations/PlayoutConfiguration.cs @@ -10,6 +10,11 @@ public class PlayoutConfiguration : IEntityTypeConfiguration { builder.ToTable("Playout"); + builder.HasMany(p => p.ProgramScheduleAlternates) + .WithOne(a => a.Playout) + .HasForeignKey(a => a.PlayoutId) + .OnDelete(DeleteBehavior.Cascade); + builder.HasMany(p => p.Items) .WithOne(pi => pi.Playout) .HasForeignKey(pi => pi.PlayoutId) diff --git a/ErsatzTV.Infrastructure/Data/Configurations/ProgramScheduleAlternateConfiguration.cs b/ErsatzTV.Infrastructure/Data/Configurations/ProgramScheduleAlternateConfiguration.cs new file mode 100644 index 000000000..e3363ba29 --- /dev/null +++ b/ErsatzTV.Infrastructure/Data/Configurations/ProgramScheduleAlternateConfiguration.cs @@ -0,0 +1,34 @@ +using ErsatzTV.Core.Domain; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace ErsatzTV.Infrastructure.Data.Configurations; + +public class ProgramScheduleAlternateConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("ProgramScheduleAlternate"); + + var intCollectionValueConverter = new ValueConverter, string>( + i => string.Join(",", i), + s => string.IsNullOrWhiteSpace(s) + ? Array.Empty() + : s.Split(new[] { ',' }).Select(int.Parse).ToArray()); + + var intCollectionValueComparer = new CollectionValueComparer(); + + builder.Property(t => t.DaysOfMonth) + .HasConversion(intCollectionValueConverter) + .Metadata.SetValueComparer(intCollectionValueComparer); + + builder.Property(t => t.MonthsOfYear) + .HasConversion(intCollectionValueConverter) + .Metadata.SetValueComparer(intCollectionValueComparer); + + builder.Property(t => t.DaysOfWeek) + .HasConversion(new EnumCollectionJsonValueConverter()) + .Metadata.SetValueComparer(new CollectionValueComparer()); + } +} diff --git a/ErsatzTV.Infrastructure/Data/Configurations/ProgramScheduleConfiguration.cs b/ErsatzTV.Infrastructure/Data/Configurations/ProgramScheduleConfiguration.cs index f59409171..67b39c011 100644 --- a/ErsatzTV.Infrastructure/Data/Configurations/ProgramScheduleConfiguration.cs +++ b/ErsatzTV.Infrastructure/Data/Configurations/ProgramScheduleConfiguration.cs @@ -22,5 +22,10 @@ public class ProgramScheduleConfiguration : IEntityTypeConfiguration p.ProgramSchedule) .HasForeignKey(p => p.ProgramScheduleId) .OnDelete(DeleteBehavior.Cascade); + + builder.HasMany(p => p.ProgramScheduleAlternates) + .WithOne(a => a.ProgramSchedule) + .HasForeignKey(a => a.ProgramScheduleId) + .OnDelete(DeleteBehavior.Cascade); } } diff --git a/ErsatzTV.Infrastructure/Data/TvContext.cs b/ErsatzTV.Infrastructure/Data/TvContext.cs index 9e758e193..041a96050 100644 --- a/ErsatzTV.Infrastructure/Data/TvContext.cs +++ b/ErsatzTV.Infrastructure/Data/TvContext.cs @@ -76,6 +76,7 @@ public class TvContext : DbContext public DbSet ProgramSchedules { get; set; } public DbSet ProgramScheduleItems { get; set; } public DbSet Playouts { get; set; } + public DbSet ProgramScheduleAlternates { get; set; } public DbSet PlayoutItems { get; set; } public DbSet PlayoutProgramScheduleItemAnchors { get; set; } public DbSet FFmpegProfiles { get; set; } diff --git a/ErsatzTV.Infrastructure/Migrations/20230108192830_Add_ProgramScheduleAlternates.Designer.cs b/ErsatzTV.Infrastructure/Migrations/20230108192830_Add_ProgramScheduleAlternates.Designer.cs new file mode 100644 index 000000000..a6a793abc --- /dev/null +++ b/ErsatzTV.Infrastructure/Migrations/20230108192830_Add_ProgramScheduleAlternates.Designer.cs @@ -0,0 +1,4394 @@ +// +using System; +using ErsatzTV.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace ErsatzTV.Infrastructure.Migrations +{ + [DbContext(typeof(TvContext))] + [Migration("20230108192830_Add_ProgramScheduleAlternates")] + partial class AddProgramScheduleAlternates + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.1"); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Actor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArtistMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ArtworkId") + .HasColumnType("INTEGER"); + + b.Property("EpisodeMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MovieMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MusicVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("OtherVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Role") + .HasColumnType("TEXT"); + + b.Property("SeasonMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ShowMetadataId") + .HasColumnType("INTEGER"); + + b.Property("SongMetadataId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ArtistMetadataId"); + + b.HasIndex("ArtworkId") + .IsUnique(); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("OtherVideoMetadataId"); + + b.HasIndex("SeasonMetadataId"); + + b.HasIndex("ShowMetadataId"); + + b.HasIndex("SongMetadataId"); + + b.ToTable("Actor", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ArtistMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArtistId") + .HasColumnType("INTEGER"); + + b.Property("Biography") + .HasColumnType("TEXT"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("Disambiguation") + .HasColumnType("TEXT"); + + b.Property("Formed") + .HasColumnType("TEXT"); + + b.Property("MetadataKind") + .HasColumnType("INTEGER"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("SortTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ArtistId"); + + b.ToTable("ArtistMetadata", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Artwork", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArtistMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ArtworkKind") + .HasColumnType("INTEGER"); + + b.Property("BlurHash43") + .HasColumnType("TEXT"); + + b.Property("BlurHash54") + .HasColumnType("TEXT"); + + b.Property("BlurHash64") + .HasColumnType("TEXT"); + + b.Property("ChannelId") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("EpisodeMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MovieMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MusicVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("OtherVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("SeasonMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ShowMetadataId") + .HasColumnType("INTEGER"); + + b.Property("SongMetadataId") + .HasColumnType("INTEGER"); + + b.Property("SourcePath") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ArtistMetadataId"); + + b.HasIndex("ChannelId"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("OtherVideoMetadataId"); + + b.HasIndex("SeasonMetadataId"); + + b.HasIndex("ShowMetadataId"); + + b.HasIndex("SongMetadataId"); + + b.ToTable("Artwork", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Channel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Categories") + .HasColumnType("TEXT"); + + b.Property("FFmpegProfileId") + .HasColumnType("INTEGER"); + + b.Property("FallbackFillerId") + .HasColumnType("INTEGER"); + + b.Property("Group") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("ErsatzTV"); + + b.Property("MusicVideoCreditsMode") + .HasColumnType("INTEGER"); + + b.Property("MusicVideoCreditsTemplate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("TEXT"); + + b.Property("PreferredAudioLanguageCode") + .HasColumnType("TEXT"); + + b.Property("PreferredAudioTitle") + .HasColumnType("TEXT"); + + b.Property("PreferredSubtitleLanguageCode") + .HasColumnType("TEXT"); + + b.Property("StreamingMode") + .HasColumnType("INTEGER"); + + b.Property("SubtitleMode") + .HasColumnType("INTEGER"); + + b.Property("UniqueId") + .HasColumnType("TEXT"); + + b.Property("WatermarkId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("FFmpegProfileId"); + + b.HasIndex("FallbackFillerId"); + + b.HasIndex("Number") + .IsUnique(); + + b.HasIndex("WatermarkId"); + + b.ToTable("Channel", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ChannelWatermark", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DurationSeconds") + .HasColumnType("INTEGER"); + + b.Property("FrequencyMinutes") + .HasColumnType("INTEGER"); + + b.Property("HorizontalMarginPercent") + .HasColumnType("INTEGER"); + + b.Property("Image") + .HasColumnType("TEXT"); + + b.Property("ImageSource") + .HasColumnType("INTEGER"); + + b.Property("Location") + .HasColumnType("INTEGER"); + + b.Property("Mode") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Opacity") + .HasColumnType("INTEGER"); + + b.Property("PlaceWithinSourceContent") + .HasColumnType("INTEGER"); + + b.Property("Size") + .HasColumnType("INTEGER"); + + b.Property("VerticalMarginPercent") + .HasColumnType("INTEGER"); + + b.Property("WidthPercent") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("ChannelWatermark", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Collection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("UseCustomPlaybackOrder") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.CollectionItem", b => + { + b.Property("CollectionId") + .HasColumnType("INTEGER"); + + b.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b.Property("CustomIndex") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionId", "MediaItemId"); + + b.HasIndex("MediaItemId"); + + b.ToTable("CollectionItem", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ConfigElement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("ConfigElement", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Director", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("EpisodeMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MovieMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OtherVideoMetadataId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("OtherVideoMetadataId"); + + b.ToTable("Director", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyCollection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Etag") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("EmbyCollection", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Address") + .HasColumnType("TEXT"); + + b.Property("EmbyMediaSourceId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("EmbyMediaSourceId"); + + b.ToTable("EmbyConnection", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyPathReplacement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("EmbyMediaSourceId") + .HasColumnType("INTEGER"); + + b.Property("EmbyPath") + .HasColumnType("TEXT"); + + b.Property("LocalPath") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("EmbyMediaSourceId"); + + b.ToTable("EmbyPathReplacement", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EpisodeMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("EpisodeId") + .HasColumnType("INTEGER"); + + b.Property("EpisodeNumber") + .HasColumnType("INTEGER"); + + b.Property("MetadataKind") + .HasColumnType("INTEGER"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("Outline") + .HasColumnType("TEXT"); + + b.Property("Plot") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("SortTitle") + .HasColumnType("TEXT"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("EpisodeId"); + + b.ToTable("EpisodeMetadata", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.FFmpegProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AudioBitrate") + .HasColumnType("INTEGER"); + + b.Property("AudioBufferSize") + .HasColumnType("INTEGER"); + + b.Property("AudioChannels") + .HasColumnType("INTEGER"); + + b.Property("AudioFormat") + .HasColumnType("INTEGER"); + + b.Property("AudioSampleRate") + .HasColumnType("INTEGER"); + + b.Property("BitDepth") + .HasColumnType("INTEGER"); + + b.Property("DeinterlaceVideo") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("HardwareAcceleration") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizeFramerate") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(false); + + b.Property("NormalizeLoudness") + .HasColumnType("INTEGER"); + + b.Property("QsvExtraHardwareFrames") + .HasColumnType("INTEGER"); + + b.Property("ResolutionId") + .HasColumnType("INTEGER"); + + b.Property("ThreadCount") + .HasColumnType("INTEGER"); + + b.Property("VaapiDevice") + .HasColumnType("TEXT"); + + b.Property("VaapiDriver") + .HasColumnType("INTEGER"); + + b.Property("VideoBitrate") + .HasColumnType("INTEGER"); + + b.Property("VideoBufferSize") + .HasColumnType("INTEGER"); + + b.Property("VideoFormat") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ResolutionId"); + + b.ToTable("FFmpegProfile", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Filler.FillerPreset", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AllowWatermarks") + .HasColumnType("INTEGER"); + + b.Property("CollectionId") + .HasColumnType("INTEGER"); + + b.Property("CollectionType") + .HasColumnType("INTEGER"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.Property("Duration") + .HasColumnType("TEXT"); + + b.Property("FillerKind") + .HasColumnType("INTEGER"); + + b.Property("FillerMode") + .HasColumnType("INTEGER"); + + b.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b.Property("MultiCollectionId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("PadToNearestMinute") + .HasColumnType("INTEGER"); + + b.Property("SmartCollectionId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CollectionId"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("MultiCollectionId"); + + b.HasIndex("SmartCollectionId"); + + b.ToTable("FillerPreset", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Genre", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArtistMetadataId") + .HasColumnType("INTEGER"); + + b.Property("EpisodeMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MovieMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MusicVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OtherVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("SeasonMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ShowMetadataId") + .HasColumnType("INTEGER"); + + b.Property("SongMetadataId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ArtistMetadataId"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("OtherVideoMetadataId"); + + b.HasIndex("SeasonMetadataId"); + + b.HasIndex("ShowMetadataId"); + + b.HasIndex("SongMetadataId"); + + b.ToTable("Genre"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinCollection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Etag") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("JellyfinCollection", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Address") + .HasColumnType("TEXT"); + + b.Property("JellyfinMediaSourceId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("JellyfinMediaSourceId"); + + b.ToTable("JellyfinConnection", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinPathReplacement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("JellyfinMediaSourceId") + .HasColumnType("INTEGER"); + + b.Property("JellyfinPath") + .HasColumnType("TEXT"); + + b.Property("LocalPath") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("JellyfinMediaSourceId"); + + b.ToTable("JellyfinPathReplacement", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LanguageCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("EnglishName") + .HasColumnType("TEXT"); + + b.Property("FrenchName") + .HasColumnType("TEXT"); + + b.Property("ThreeCode1") + .HasColumnType("TEXT"); + + b.Property("ThreeCode2") + .HasColumnType("TEXT"); + + b.Property("TwoCode") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("LanguageCode", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Library", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastScan") + .HasColumnType("TEXT"); + + b.Property("MediaKind") + .HasColumnType("INTEGER"); + + b.Property("MediaSourceId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MediaSourceId"); + + b.ToTable("Library", (string)null); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LibraryFolder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Etag") + .HasColumnType("TEXT"); + + b.Property("LibraryPathId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryPathId"); + + b.ToTable("LibraryFolder", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LibraryPath", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastScan") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("LibraryPath", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaChapter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("EndTime") + .HasColumnType("TEXT"); + + b.Property("MediaVersionId") + .HasColumnType("INTEGER"); + + b.Property("StartTime") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MediaVersionId"); + + b.ToTable("MediaChapter", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("MediaVersionId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MediaVersionId"); + + b.HasIndex("Path") + .IsUnique(); + + b.ToTable("MediaFile", (string)null); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LibraryPathId") + .HasColumnType("INTEGER"); + + b.Property("State") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("LibraryPathId"); + + b.ToTable("MediaItem", (string)null); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("MediaSource", (string)null); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AttachedPic") + .HasColumnType("INTEGER"); + + b.Property("BitsPerRawSample") + .HasColumnType("INTEGER"); + + b.Property("Channels") + .HasColumnType("INTEGER"); + + b.Property("Codec") + .HasColumnType("TEXT"); + + b.Property("ColorPrimaries") + .HasColumnType("TEXT"); + + b.Property("ColorRange") + .HasColumnType("TEXT"); + + b.Property("ColorSpace") + .HasColumnType("TEXT"); + + b.Property("ColorTransfer") + .HasColumnType("TEXT"); + + b.Property("Default") + .HasColumnType("INTEGER"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("Forced") + .HasColumnType("INTEGER"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("MediaStreamKind") + .HasColumnType("INTEGER"); + + b.Property("MediaVersionId") + .HasColumnType("INTEGER"); + + b.Property("MimeType") + .HasColumnType("TEXT"); + + b.Property("PixelFormat") + .HasColumnType("TEXT"); + + b.Property("Profile") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MediaVersionId"); + + b.ToTable("MediaStream", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("DisplayAspectRatio") + .HasColumnType("TEXT"); + + b.Property("Duration") + .HasColumnType("TEXT"); + + b.Property("EpisodeId") + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("MovieId") + .HasColumnType("INTEGER"); + + b.Property("MusicVideoId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OtherVideoId") + .HasColumnType("INTEGER"); + + b.Property("RFrameRate") + .HasColumnType("TEXT"); + + b.Property("SampleAspectRatio") + .HasColumnType("TEXT"); + + b.Property("SongId") + .HasColumnType("INTEGER"); + + b.Property("VideoScanKind") + .HasColumnType("INTEGER"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("EpisodeId"); + + b.HasIndex("MovieId"); + + b.HasIndex("MusicVideoId"); + + b.HasIndex("OtherVideoId"); + + b.HasIndex("SongId"); + + b.ToTable("MediaVersion", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MetadataGuid", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArtistMetadataId") + .HasColumnType("INTEGER"); + + b.Property("EpisodeMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Guid") + .HasColumnType("TEXT"); + + b.Property("MovieMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MusicVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("OtherVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("SeasonMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ShowMetadataId") + .HasColumnType("INTEGER"); + + b.Property("SongMetadataId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ArtistMetadataId"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("OtherVideoMetadataId"); + + b.HasIndex("SeasonMetadataId"); + + b.HasIndex("ShowMetadataId"); + + b.HasIndex("SongMetadataId"); + + b.ToTable("MetadataGuid", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Mood", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArtistMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ArtistMetadataId"); + + b.ToTable("Mood"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MovieMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ContentRating") + .HasColumnType("TEXT"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("MetadataKind") + .HasColumnType("INTEGER"); + + b.Property("MovieId") + .HasColumnType("INTEGER"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("Outline") + .HasColumnType("TEXT"); + + b.Property("Plot") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("SortTitle") + .HasColumnType("TEXT"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("MovieId"); + + b.ToTable("MovieMetadata", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MultiCollection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("MultiCollection", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MultiCollectionItem", b => + { + b.Property("MultiCollectionId") + .HasColumnType("INTEGER"); + + b.Property("CollectionId") + .HasColumnType("INTEGER"); + + b.Property("PlaybackOrder") + .HasColumnType("INTEGER"); + + b.Property("ScheduleAsGroup") + .HasColumnType("INTEGER"); + + b.HasKey("MultiCollectionId", "CollectionId"); + + b.HasIndex("CollectionId"); + + b.ToTable("MultiCollectionItem", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MultiCollectionSmartItem", b => + { + b.Property("MultiCollectionId") + .HasColumnType("INTEGER"); + + b.Property("SmartCollectionId") + .HasColumnType("INTEGER"); + + b.Property("PlaybackOrder") + .HasColumnType("INTEGER"); + + b.Property("ScheduleAsGroup") + .HasColumnType("INTEGER"); + + b.HasKey("MultiCollectionId", "SmartCollectionId"); + + b.HasIndex("SmartCollectionId"); + + b.ToTable("MultiCollectionSmartItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideoArtist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("MusicVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MusicVideoMetadataId"); + + b.ToTable("MusicVideoArtist"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideoMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Album") + .HasColumnType("TEXT"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("MetadataKind") + .HasColumnType("INTEGER"); + + b.Property("MusicVideoId") + .HasColumnType("INTEGER"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("Plot") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("SortTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Track") + .HasColumnType("INTEGER"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("MusicVideoId"); + + b.ToTable("MusicVideoMetadata", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.OtherVideoMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ContentRating") + .HasColumnType("TEXT"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("MetadataKind") + .HasColumnType("INTEGER"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("OtherVideoId") + .HasColumnType("INTEGER"); + + b.Property("Outline") + .HasColumnType("TEXT"); + + b.Property("Plot") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("SortTitle") + .HasColumnType("TEXT"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OtherVideoId"); + + b.ToTable("OtherVideoMetadata", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("INTEGER"); + + b.Property("DailyRebuildTime") + .HasColumnType("TEXT"); + + b.Property("ProgramScheduleId") + .HasColumnType("INTEGER"); + + b.Property("ProgramSchedulePlayoutType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChannelId"); + + b.HasIndex("ProgramScheduleId"); + + b.ToTable("Playout", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterTitle") + .HasColumnType("TEXT"); + + b.Property("CustomTitle") + .HasColumnType("TEXT"); + + b.Property("DisableWatermarks") + .HasColumnType("INTEGER"); + + b.Property("FillerKind") + .HasColumnType("INTEGER"); + + b.Property("Finish") + .HasColumnType("TEXT"); + + b.Property("GuideFinish") + .HasColumnType("TEXT"); + + b.Property("GuideGroup") + .HasColumnType("INTEGER"); + + b.Property("InPoint") + .HasColumnType("TEXT"); + + b.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b.Property("OutPoint") + .HasColumnType("TEXT"); + + b.Property("PlayoutId") + .HasColumnType("INTEGER"); + + b.Property("PreferredAudioLanguageCode") + .HasColumnType("TEXT"); + + b.Property("PreferredAudioTitle") + .HasColumnType("TEXT"); + + b.Property("PreferredSubtitleLanguageCode") + .HasColumnType("TEXT"); + + b.Property("Start") + .HasColumnType("TEXT"); + + b.Property("SubtitleMode") + .HasColumnType("INTEGER"); + + b.Property("WatermarkId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("PlayoutId"); + + b.HasIndex("WatermarkId"); + + b.ToTable("PlayoutItem", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutProgramScheduleAnchor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AnchorDate") + .HasColumnType("TEXT"); + + b.Property("CollectionId") + .HasColumnType("INTEGER"); + + b.Property("CollectionType") + .HasColumnType("INTEGER"); + + b.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b.Property("MultiCollectionId") + .HasColumnType("INTEGER"); + + b.Property("PlayoutId") + .HasColumnType("INTEGER"); + + b.Property("ProgramScheduleId") + .HasColumnType("INTEGER"); + + b.Property("SmartCollectionId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CollectionId"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("MultiCollectionId"); + + b.HasIndex("PlayoutId"); + + b.HasIndex("ProgramScheduleId"); + + b.HasIndex("SmartCollectionId"); + + b.ToTable("PlayoutProgramScheduleAnchor", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("PlexMediaSourceId") + .HasColumnType("INTEGER"); + + b.Property("Uri") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PlexMediaSourceId"); + + b.ToTable("PlexConnection", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexPathReplacement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LocalPath") + .HasColumnType("TEXT"); + + b.Property("PlexMediaSourceId") + .HasColumnType("INTEGER"); + + b.Property("PlexPath") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PlexMediaSourceId"); + + b.ToTable("PlexPathReplacement", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("KeepMultiPartEpisodesTogether") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("RandomStartPoint") + .HasColumnType("INTEGER"); + + b.Property("ShuffleScheduleItems") + .HasColumnType("INTEGER"); + + b.Property("TreatCollectionsAsShows") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("ProgramSchedule", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleAlternate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DaysOfMonth") + .HasColumnType("TEXT"); + + b.Property("DaysOfWeek") + .HasColumnType("TEXT"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("MonthsOfYear") + .HasColumnType("TEXT"); + + b.Property("PlayoutId") + .HasColumnType("INTEGER"); + + b.Property("ProgramScheduleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PlayoutId"); + + b.HasIndex("ProgramScheduleId"); + + b.ToTable("ProgramScheduleAlternate", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CollectionId") + .HasColumnType("INTEGER"); + + b.Property("CollectionType") + .HasColumnType("INTEGER"); + + b.Property("CustomTitle") + .HasColumnType("TEXT"); + + b.Property("FallbackFillerId") + .HasColumnType("INTEGER"); + + b.Property("GuideMode") + .HasColumnType("INTEGER"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b.Property("MidRollFillerId") + .HasColumnType("INTEGER"); + + b.Property("MultiCollectionId") + .HasColumnType("INTEGER"); + + b.Property("PlaybackOrder") + .HasColumnType("INTEGER"); + + b.Property("PostRollFillerId") + .HasColumnType("INTEGER"); + + b.Property("PreRollFillerId") + .HasColumnType("INTEGER"); + + b.Property("PreferredAudioLanguageCode") + .HasColumnType("TEXT"); + + b.Property("PreferredAudioTitle") + .HasColumnType("TEXT"); + + b.Property("PreferredSubtitleLanguageCode") + .HasColumnType("TEXT"); + + b.Property("ProgramScheduleId") + .HasColumnType("INTEGER"); + + b.Property("SmartCollectionId") + .HasColumnType("INTEGER"); + + b.Property("StartTime") + .HasColumnType("TEXT"); + + b.Property("SubtitleMode") + .HasColumnType("INTEGER"); + + b.Property("TailFillerId") + .HasColumnType("INTEGER"); + + b.Property("WatermarkId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CollectionId"); + + b.HasIndex("FallbackFillerId"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("MidRollFillerId"); + + b.HasIndex("MultiCollectionId"); + + b.HasIndex("PostRollFillerId"); + + b.HasIndex("PreRollFillerId"); + + b.HasIndex("ProgramScheduleId"); + + b.HasIndex("SmartCollectionId"); + + b.HasIndex("TailFillerId"); + + b.HasIndex("WatermarkId"); + + b.ToTable("ProgramScheduleItem", (string)null); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Resolution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Resolution", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.SeasonMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("MetadataKind") + .HasColumnType("INTEGER"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("Outline") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("SeasonId") + .HasColumnType("INTEGER"); + + b.Property("SortTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId"); + + b.ToTable("SeasonMetadata", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ShowMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ContentRating") + .HasColumnType("TEXT"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("MetadataKind") + .HasColumnType("INTEGER"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("Outline") + .HasColumnType("TEXT"); + + b.Property("Plot") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("ShowId") + .HasColumnType("INTEGER"); + + b.Property("SortTitle") + .HasColumnType("TEXT"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ShowId"); + + b.ToTable("ShowMetadata", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.SmartCollection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Query") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("SmartCollection", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.SongMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Album") + .HasColumnType("TEXT"); + + b.Property("AlbumArtist") + .HasColumnType("TEXT"); + + b.Property("Artist") + .HasColumnType("TEXT"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("MetadataKind") + .HasColumnType("INTEGER"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("SongId") + .HasColumnType("INTEGER"); + + b.Property("SortTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Track") + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SongId"); + + b.ToTable("SongMetadata", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Studio", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArtistMetadataId") + .HasColumnType("INTEGER"); + + b.Property("EpisodeMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MovieMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MusicVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OtherVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("SeasonMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ShowMetadataId") + .HasColumnType("INTEGER"); + + b.Property("SongMetadataId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ArtistMetadataId"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("OtherVideoMetadataId"); + + b.HasIndex("SeasonMetadataId"); + + b.HasIndex("ShowMetadataId"); + + b.HasIndex("SongMetadataId"); + + b.ToTable("Studio", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Style", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArtistMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ArtistMetadataId"); + + b.ToTable("Style"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Subtitle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArtistMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Codec") + .HasColumnType("TEXT"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("Default") + .HasColumnType("INTEGER"); + + b.Property("EpisodeMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Forced") + .HasColumnType("INTEGER"); + + b.Property("IsExtracted") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(false); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("MovieMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MusicVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("OtherVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("SDH") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(false); + + b.Property("SeasonMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ShowMetadataId") + .HasColumnType("INTEGER"); + + b.Property("SongMetadataId") + .HasColumnType("INTEGER"); + + b.Property("StreamIndex") + .HasColumnType("INTEGER"); + + b.Property("SubtitleKind") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ArtistMetadataId"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("OtherVideoMetadataId"); + + b.HasIndex("SeasonMetadataId"); + + b.HasIndex("ShowMetadataId"); + + b.HasIndex("SongMetadataId"); + + b.ToTable("Subtitle", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArtistMetadataId") + .HasColumnType("INTEGER"); + + b.Property("EpisodeMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ExternalCollectionId") + .HasColumnType("TEXT"); + + b.Property("MovieMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MusicVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OtherVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("SeasonMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ShowMetadataId") + .HasColumnType("INTEGER"); + + b.Property("SongMetadataId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ArtistMetadataId"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("OtherVideoMetadataId"); + + b.HasIndex("SeasonMetadataId"); + + b.HasIndex("ShowMetadataId"); + + b.HasIndex("SongMetadataId"); + + b.ToTable("Tag"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TraktList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("ItemCount") + .HasColumnType("INTEGER"); + + b.Property("List") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("TraktId") + .HasColumnType("INTEGER"); + + b.Property("User") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("TraktList", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TraktListItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Episode") + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b.Property("Rank") + .HasColumnType("INTEGER"); + + b.Property("Season") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TraktId") + .HasColumnType("INTEGER"); + + b.Property("TraktListId") + .HasColumnType("INTEGER"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("TraktListId"); + + b.ToTable("TraktListItem", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TraktListItemGuid", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Guid") + .HasColumnType("TEXT"); + + b.Property("TraktListItemId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("TraktListItemId"); + + b.ToTable("TraktListItemGuid", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Writer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("EpisodeMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MovieMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OtherVideoMetadataId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("OtherVideoMetadataId"); + + b.ToTable("Writer", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Emby.EmbyPathInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("EmbyLibraryId") + .HasColumnType("INTEGER"); + + b.Property("NetworkPath") + .HasColumnType("TEXT"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("EmbyLibraryId"); + + b.ToTable("EmbyPathInfo"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Jellyfin.JellyfinPathInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("JellyfinLibraryId") + .HasColumnType("INTEGER"); + + b.Property("NetworkPath") + .HasColumnType("TEXT"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("JellyfinLibraryId"); + + b.ToTable("JellyfinPathInfo"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyLibrary", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Library"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ShouldSyncItems") + .HasColumnType("INTEGER"); + + b.ToTable("EmbyLibrary", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinLibrary", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Library"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ShouldSyncItems") + .HasColumnType("INTEGER"); + + b.ToTable("JellyfinLibrary", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LocalLibrary", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Library"); + + b.ToTable("LocalLibrary", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexLibrary", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Library"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("ShouldSyncItems") + .HasColumnType("INTEGER"); + + b.ToTable("PlexLibrary", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaFile", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaFile"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("PlexId") + .HasColumnType("INTEGER"); + + b.ToTable("PlexMediaFile", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Artist", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.ToTable("Artist", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Episode", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.Property("SeasonId") + .HasColumnType("INTEGER"); + + b.HasIndex("SeasonId"); + + b.ToTable("Episode", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Movie", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.ToTable("Movie", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideo", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.Property("ArtistId") + .HasColumnType("INTEGER"); + + b.HasIndex("ArtistId"); + + b.ToTable("MusicVideo", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.OtherVideo", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.ToTable("OtherVideo", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Season", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.Property("ShowId") + .HasColumnType("INTEGER"); + + b.HasIndex("ShowId"); + + b.ToTable("Season", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Show", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.ToTable("Show", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Song", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.ToTable("Song", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyMediaSource", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaSource"); + + b.Property("OperatingSystem") + .HasColumnType("TEXT"); + + b.Property("ServerName") + .HasColumnType("TEXT"); + + b.ToTable("EmbyMediaSource", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinMediaSource", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaSource"); + + b.Property("OperatingSystem") + .HasColumnType("TEXT"); + + b.Property("ServerName") + .HasColumnType("TEXT"); + + b.ToTable("JellyfinMediaSource", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LocalMediaSource", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaSource"); + + b.ToTable("LocalMediaSource", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSource", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaSource"); + + b.Property("ClientIdentifier") + .HasColumnType("TEXT"); + + b.Property("Platform") + .HasColumnType("TEXT"); + + b.Property("PlatformVersion") + .HasColumnType("TEXT"); + + b.Property("ProductVersion") + .HasColumnType("TEXT"); + + b.Property("ServerName") + .HasColumnType("TEXT"); + + b.ToTable("PlexMediaSource", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemDuration", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); + + b.Property("PlayoutDuration") + .HasColumnType("TEXT"); + + b.Property("TailMode") + .HasColumnType("INTEGER"); + + b.ToTable("ProgramScheduleDurationItem", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemFlood", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); + + b.ToTable("ProgramScheduleFloodItem", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemMultiple", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.ToTable("ProgramScheduleMultipleItem", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemOne", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); + + b.ToTable("ProgramScheduleOneItem", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyEpisode", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Episode"); + + b.Property("Etag") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.ToTable("EmbyEpisode", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinEpisode", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Episode"); + + b.Property("Etag") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.ToTable("JellyfinEpisode", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexEpisode", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Episode"); + + b.Property("Etag") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.ToTable("PlexEpisode", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyMovie", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Movie"); + + b.Property("Etag") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.ToTable("EmbyMovie", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinMovie", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Movie"); + + b.Property("Etag") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.ToTable("JellyfinMovie", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMovie", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Movie"); + + b.Property("Etag") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.ToTable("PlexMovie", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbySeason", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Season"); + + b.Property("Etag") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.ToTable("EmbySeason", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinSeason", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Season"); + + b.Property("Etag") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.ToTable("JellyfinSeason", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexSeason", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Season"); + + b.Property("Etag") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.ToTable("PlexSeason", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyShow", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Show"); + + b.Property("Etag") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.ToTable("EmbyShow", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinShow", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Show"); + + b.Property("Etag") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.ToTable("JellyfinShow", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexShow", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Show"); + + b.Property("Etag") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.ToTable("PlexShow", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Actor", b => + { + b.HasOne("ErsatzTV.Core.Domain.ArtistMetadata", null) + .WithMany("Actors") + .HasForeignKey("ArtistMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.Artwork", "Artwork") + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.Actor", "ArtworkId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null) + .WithMany("Actors") + .HasForeignKey("EpisodeMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MovieMetadata", null) + .WithMany("Actors") + .HasForeignKey("MovieMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MusicVideoMetadata", null) + .WithMany("Actors") + .HasForeignKey("MusicVideoMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.OtherVideoMetadata", null) + .WithMany("Actors") + .HasForeignKey("OtherVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null) + .WithMany("Actors") + .HasForeignKey("SeasonMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.ShowMetadata", null) + .WithMany("Actors") + .HasForeignKey("ShowMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SongMetadata", null) + .WithMany("Actors") + .HasForeignKey("SongMetadataId"); + + b.Navigation("Artwork"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ArtistMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.Artist", "Artist") + .WithMany("ArtistMetadata") + .HasForeignKey("ArtistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Artist"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Artwork", b => + { + b.HasOne("ErsatzTV.Core.Domain.ArtistMetadata", null) + .WithMany("Artwork") + .HasForeignKey("ArtistMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.Channel", null) + .WithMany("Artwork") + .HasForeignKey("ChannelId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null) + .WithMany("Artwork") + .HasForeignKey("EpisodeMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MovieMetadata", null) + .WithMany("Artwork") + .HasForeignKey("MovieMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MusicVideoMetadata", null) + .WithMany("Artwork") + .HasForeignKey("MusicVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.OtherVideoMetadata", null) + .WithMany("Artwork") + .HasForeignKey("OtherVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null) + .WithMany("Artwork") + .HasForeignKey("SeasonMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.ShowMetadata", null) + .WithMany("Artwork") + .HasForeignKey("ShowMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SongMetadata", null) + .WithMany("Artwork") + .HasForeignKey("SongMetadataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Channel", b => + { + b.HasOne("ErsatzTV.Core.Domain.FFmpegProfile", "FFmpegProfile") + .WithMany() + .HasForeignKey("FFmpegProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.Filler.FillerPreset", "FallbackFiller") + .WithMany() + .HasForeignKey("FallbackFillerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("ErsatzTV.Core.Domain.ChannelWatermark", "Watermark") + .WithMany() + .HasForeignKey("WatermarkId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("FFmpegProfile"); + + b.Navigation("FallbackFiller"); + + b.Navigation("Watermark"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.CollectionItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.Collection", "Collection") + .WithMany("CollectionItems") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem") + .WithMany("CollectionItems") + .HasForeignKey("MediaItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("MediaItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Director", b => + { + b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null) + .WithMany("Directors") + .HasForeignKey("EpisodeMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MovieMetadata", null) + .WithMany("Directors") + .HasForeignKey("MovieMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.OtherVideoMetadata", null) + .WithMany("Directors") + .HasForeignKey("OtherVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyConnection", b => + { + b.HasOne("ErsatzTV.Core.Domain.EmbyMediaSource", "EmbyMediaSource") + .WithMany("Connections") + .HasForeignKey("EmbyMediaSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EmbyMediaSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyPathReplacement", b => + { + b.HasOne("ErsatzTV.Core.Domain.EmbyMediaSource", "EmbyMediaSource") + .WithMany("PathReplacements") + .HasForeignKey("EmbyMediaSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EmbyMediaSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EpisodeMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.Episode", "Episode") + .WithMany("EpisodeMetadata") + .HasForeignKey("EpisodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Episode"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.FFmpegProfile", b => + { + b.HasOne("ErsatzTV.Core.Domain.Resolution", "Resolution") + .WithMany() + .HasForeignKey("ResolutionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Resolution"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Filler.FillerPreset", b => + { + b.HasOne("ErsatzTV.Core.Domain.Collection", "Collection") + .WithMany() + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem") + .WithMany() + .HasForeignKey("MediaItemId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MultiCollection", "MultiCollection") + .WithMany() + .HasForeignKey("MultiCollectionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SmartCollection", "SmartCollection") + .WithMany() + .HasForeignKey("SmartCollectionId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Collection"); + + b.Navigation("MediaItem"); + + b.Navigation("MultiCollection"); + + b.Navigation("SmartCollection"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Genre", b => + { + b.HasOne("ErsatzTV.Core.Domain.ArtistMetadata", null) + .WithMany("Genres") + .HasForeignKey("ArtistMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null) + .WithMany("Genres") + .HasForeignKey("EpisodeMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.MovieMetadata", null) + .WithMany("Genres") + .HasForeignKey("MovieMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MusicVideoMetadata", null) + .WithMany("Genres") + .HasForeignKey("MusicVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.OtherVideoMetadata", null) + .WithMany("Genres") + .HasForeignKey("OtherVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null) + .WithMany("Genres") + .HasForeignKey("SeasonMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.ShowMetadata", null) + .WithMany("Genres") + .HasForeignKey("ShowMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SongMetadata", null) + .WithMany("Genres") + .HasForeignKey("SongMetadataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinConnection", b => + { + b.HasOne("ErsatzTV.Core.Domain.JellyfinMediaSource", "JellyfinMediaSource") + .WithMany("Connections") + .HasForeignKey("JellyfinMediaSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JellyfinMediaSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinPathReplacement", b => + { + b.HasOne("ErsatzTV.Core.Domain.JellyfinMediaSource", "JellyfinMediaSource") + .WithMany("PathReplacements") + .HasForeignKey("JellyfinMediaSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JellyfinMediaSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Library", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaSource", "MediaSource") + .WithMany("Libraries") + .HasForeignKey("MediaSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MediaSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LibraryFolder", b => + { + b.HasOne("ErsatzTV.Core.Domain.LibraryPath", "LibraryPath") + .WithMany("LibraryFolders") + .HasForeignKey("LibraryPathId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("LibraryPath"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LibraryPath", b => + { + b.HasOne("ErsatzTV.Core.Domain.Library", "Library") + .WithMany("Paths") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaChapter", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaVersion", "MediaVersion") + .WithMany("Chapters") + .HasForeignKey("MediaVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MediaVersion"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaFile", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaVersion", "MediaVersion") + .WithMany("MediaFiles") + .HasForeignKey("MediaVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MediaVersion"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.LibraryPath", "LibraryPath") + .WithMany("MediaItems") + .HasForeignKey("LibraryPathId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("LibraryPath"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaStream", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaVersion", "MediaVersion") + .WithMany("Streams") + .HasForeignKey("MediaVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MediaVersion"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaVersion", b => + { + b.HasOne("ErsatzTV.Core.Domain.Episode", null) + .WithMany("MediaVersions") + .HasForeignKey("EpisodeId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.Movie", null) + .WithMany("MediaVersions") + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MusicVideo", null) + .WithMany("MediaVersions") + .HasForeignKey("MusicVideoId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.OtherVideo", null) + .WithMany("MediaVersions") + .HasForeignKey("OtherVideoId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.Song", null) + .WithMany("MediaVersions") + .HasForeignKey("SongId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MetadataGuid", b => + { + b.HasOne("ErsatzTV.Core.Domain.ArtistMetadata", null) + .WithMany("Guids") + .HasForeignKey("ArtistMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null) + .WithMany("Guids") + .HasForeignKey("EpisodeMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MovieMetadata", null) + .WithMany("Guids") + .HasForeignKey("MovieMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MusicVideoMetadata", null) + .WithMany("Guids") + .HasForeignKey("MusicVideoMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.OtherVideoMetadata", null) + .WithMany("Guids") + .HasForeignKey("OtherVideoMetadataId") + .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"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Mood", b => + { + b.HasOne("ErsatzTV.Core.Domain.ArtistMetadata", null) + .WithMany("Moods") + .HasForeignKey("ArtistMetadataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MovieMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.Movie", "Movie") + .WithMany("MovieMetadata") + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Movie"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MultiCollectionItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.Collection", "Collection") + .WithMany("MultiCollectionItems") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.MultiCollection", "MultiCollection") + .WithMany("MultiCollectionItems") + .HasForeignKey("MultiCollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("MultiCollection"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MultiCollectionSmartItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.MultiCollection", "MultiCollection") + .WithMany("MultiCollectionSmartItems") + .HasForeignKey("MultiCollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.SmartCollection", "SmartCollection") + .WithMany("MultiCollectionSmartItems") + .HasForeignKey("SmartCollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MultiCollection"); + + b.Navigation("SmartCollection"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideoArtist", b => + { + b.HasOne("ErsatzTV.Core.Domain.MusicVideoMetadata", null) + .WithMany("Artists") + .HasForeignKey("MusicVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideoMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.MusicVideo", "MusicVideo") + .WithMany("MusicVideoMetadata") + .HasForeignKey("MusicVideoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MusicVideo"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.OtherVideoMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.OtherVideo", "OtherVideo") + .WithMany("OtherVideoMetadata") + .HasForeignKey("OtherVideoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OtherVideo"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b => + { + b.HasOne("ErsatzTV.Core.Domain.Channel", "Channel") + .WithMany("Playouts") + .HasForeignKey("ChannelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule") + .WithMany("Playouts") + .HasForeignKey("ProgramScheduleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("ErsatzTV.Core.Domain.PlayoutAnchor", "Anchor", b1 => + { + b1.Property("PlayoutId") + .HasColumnType("INTEGER"); + + b1.Property("DurationFinish") + .HasColumnType("TEXT"); + + b1.Property("InDurationFiller") + .HasColumnType("INTEGER"); + + b1.Property("InFlood") + .HasColumnType("INTEGER"); + + b1.Property("MultipleRemaining") + .HasColumnType("INTEGER"); + + b1.Property("NextGuideGroup") + .HasColumnType("INTEGER"); + + b1.Property("NextStart") + .HasColumnType("TEXT"); + + b1.HasKey("PlayoutId"); + + b1.ToTable("PlayoutAnchor", (string)null); + + b1.WithOwner() + .HasForeignKey("PlayoutId"); + + b1.OwnsOne("ErsatzTV.Core.Domain.CollectionEnumeratorState", "ScheduleItemsEnumeratorState", b2 => + { + b2.Property("PlayoutAnchorPlayoutId") + .HasColumnType("INTEGER"); + + b2.Property("Index") + .HasColumnType("INTEGER"); + + b2.Property("Seed") + .HasColumnType("INTEGER"); + + b2.HasKey("PlayoutAnchorPlayoutId"); + + b2.ToTable("ScheduleItemsEnumeratorState", (string)null); + + b2.WithOwner() + .HasForeignKey("PlayoutAnchorPlayoutId"); + }); + + b1.Navigation("ScheduleItemsEnumeratorState"); + }); + + b.Navigation("Anchor"); + + b.Navigation("Channel"); + + b.Navigation("ProgramSchedule"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem") + .WithMany() + .HasForeignKey("MediaItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout") + .WithMany("Items") + .HasForeignKey("PlayoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.ChannelWatermark", "Watermark") + .WithMany() + .HasForeignKey("WatermarkId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("MediaItem"); + + b.Navigation("Playout"); + + b.Navigation("Watermark"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutProgramScheduleAnchor", b => + { + b.HasOne("ErsatzTV.Core.Domain.Collection", "Collection") + .WithMany() + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem") + .WithMany() + .HasForeignKey("MediaItemId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MultiCollection", "MultiCollection") + .WithMany() + .HasForeignKey("MultiCollectionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout") + .WithMany("ProgramScheduleAnchors") + .HasForeignKey("PlayoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule") + .WithMany() + .HasForeignKey("ProgramScheduleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.SmartCollection", "SmartCollection") + .WithMany() + .HasForeignKey("SmartCollectionId"); + + b.OwnsOne("ErsatzTV.Core.Domain.CollectionEnumeratorState", "EnumeratorState", b1 => + { + b1.Property("PlayoutProgramScheduleAnchorId") + .HasColumnType("INTEGER"); + + b1.Property("Index") + .HasColumnType("INTEGER"); + + b1.Property("Seed") + .HasColumnType("INTEGER"); + + b1.HasKey("PlayoutProgramScheduleAnchorId"); + + b1.ToTable("CollectionEnumeratorState", (string)null); + + b1.WithOwner() + .HasForeignKey("PlayoutProgramScheduleAnchorId"); + }); + + b.Navigation("Collection"); + + b.Navigation("EnumeratorState"); + + b.Navigation("MediaItem"); + + b.Navigation("MultiCollection"); + + b.Navigation("Playout"); + + b.Navigation("ProgramSchedule"); + + b.Navigation("SmartCollection"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexConnection", b => + { + b.HasOne("ErsatzTV.Core.Domain.PlexMediaSource", "PlexMediaSource") + .WithMany("Connections") + .HasForeignKey("PlexMediaSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PlexMediaSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexPathReplacement", b => + { + b.HasOne("ErsatzTV.Core.Domain.PlexMediaSource", "PlexMediaSource") + .WithMany("PathReplacements") + .HasForeignKey("PlexMediaSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PlexMediaSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleAlternate", b => + { + b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout") + .WithMany("ProgramScheduleAlternates") + .HasForeignKey("PlayoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule") + .WithMany("ProgramScheduleAlternates") + .HasForeignKey("ProgramScheduleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Playout"); + + b.Navigation("ProgramSchedule"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.Collection", "Collection") + .WithMany() + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.Filler.FillerPreset", "FallbackFiller") + .WithMany() + .HasForeignKey("FallbackFillerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem") + .WithMany() + .HasForeignKey("MediaItemId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.Filler.FillerPreset", "MidRollFiller") + .WithMany() + .HasForeignKey("MidRollFillerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("ErsatzTV.Core.Domain.MultiCollection", "MultiCollection") + .WithMany() + .HasForeignKey("MultiCollectionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.Filler.FillerPreset", "PostRollFiller") + .WithMany() + .HasForeignKey("PostRollFillerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("ErsatzTV.Core.Domain.Filler.FillerPreset", "PreRollFiller") + .WithMany() + .HasForeignKey("PreRollFillerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule") + .WithMany("Items") + .HasForeignKey("ProgramScheduleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.SmartCollection", "SmartCollection") + .WithMany() + .HasForeignKey("SmartCollectionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.Filler.FillerPreset", "TailFiller") + .WithMany() + .HasForeignKey("TailFillerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("ErsatzTV.Core.Domain.ChannelWatermark", "Watermark") + .WithMany() + .HasForeignKey("WatermarkId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Collection"); + + b.Navigation("FallbackFiller"); + + b.Navigation("MediaItem"); + + b.Navigation("MidRollFiller"); + + b.Navigation("MultiCollection"); + + b.Navigation("PostRollFiller"); + + b.Navigation("PreRollFiller"); + + b.Navigation("ProgramSchedule"); + + b.Navigation("SmartCollection"); + + b.Navigation("TailFiller"); + + b.Navigation("Watermark"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.SeasonMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.Season", "Season") + .WithMany("SeasonMetadata") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ShowMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.Show", "Show") + .WithMany("ShowMetadata") + .HasForeignKey("ShowId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Show"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.SongMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.Song", "Song") + .WithMany("SongMetadata") + .HasForeignKey("SongId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Song"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Studio", b => + { + b.HasOne("ErsatzTV.Core.Domain.ArtistMetadata", null) + .WithMany("Studios") + .HasForeignKey("ArtistMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null) + .WithMany("Studios") + .HasForeignKey("EpisodeMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.MovieMetadata", null) + .WithMany("Studios") + .HasForeignKey("MovieMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MusicVideoMetadata", null) + .WithMany("Studios") + .HasForeignKey("MusicVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.OtherVideoMetadata", null) + .WithMany("Studios") + .HasForeignKey("OtherVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null) + .WithMany("Studios") + .HasForeignKey("SeasonMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.ShowMetadata", null) + .WithMany("Studios") + .HasForeignKey("ShowMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SongMetadata", null) + .WithMany("Studios") + .HasForeignKey("SongMetadataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Style", b => + { + b.HasOne("ErsatzTV.Core.Domain.ArtistMetadata", null) + .WithMany("Styles") + .HasForeignKey("ArtistMetadataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Subtitle", b => + { + b.HasOne("ErsatzTV.Core.Domain.ArtistMetadata", null) + .WithMany("Subtitles") + .HasForeignKey("ArtistMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null) + .WithMany("Subtitles") + .HasForeignKey("EpisodeMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MovieMetadata", null) + .WithMany("Subtitles") + .HasForeignKey("MovieMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MusicVideoMetadata", null) + .WithMany("Subtitles") + .HasForeignKey("MusicVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.OtherVideoMetadata", null) + .WithMany("Subtitles") + .HasForeignKey("OtherVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null) + .WithMany("Subtitles") + .HasForeignKey("SeasonMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.ShowMetadata", null) + .WithMany("Subtitles") + .HasForeignKey("ShowMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.SongMetadata", null) + .WithMany("Subtitles") + .HasForeignKey("SongMetadataId"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Tag", b => + { + b.HasOne("ErsatzTV.Core.Domain.ArtistMetadata", null) + .WithMany("Tags") + .HasForeignKey("ArtistMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null) + .WithMany("Tags") + .HasForeignKey("EpisodeMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.MovieMetadata", null) + .WithMany("Tags") + .HasForeignKey("MovieMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MusicVideoMetadata", null) + .WithMany("Tags") + .HasForeignKey("MusicVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.OtherVideoMetadata", null) + .WithMany("Tags") + .HasForeignKey("OtherVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null) + .WithMany("Tags") + .HasForeignKey("SeasonMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.ShowMetadata", null) + .WithMany("Tags") + .HasForeignKey("ShowMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SongMetadata", null) + .WithMany("Tags") + .HasForeignKey("SongMetadataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TraktListItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem") + .WithMany("TraktListItems") + .HasForeignKey("MediaItemId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("ErsatzTV.Core.Domain.TraktList", "TraktList") + .WithMany("Items") + .HasForeignKey("TraktListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MediaItem"); + + b.Navigation("TraktList"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TraktListItemGuid", b => + { + b.HasOne("ErsatzTV.Core.Domain.TraktListItem", "TraktListItem") + .WithMany("Guids") + .HasForeignKey("TraktListItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TraktListItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Writer", b => + { + b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null) + .WithMany("Writers") + .HasForeignKey("EpisodeMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MovieMetadata", null) + .WithMany("Writers") + .HasForeignKey("MovieMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.OtherVideoMetadata", null) + .WithMany("Writers") + .HasForeignKey("OtherVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Emby.EmbyPathInfo", b => + { + b.HasOne("ErsatzTV.Core.Domain.EmbyLibrary", null) + .WithMany("PathInfos") + .HasForeignKey("EmbyLibraryId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Jellyfin.JellyfinPathInfo", b => + { + b.HasOne("ErsatzTV.Core.Domain.JellyfinLibrary", null) + .WithMany("PathInfos") + .HasForeignKey("JellyfinLibraryId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyLibrary", b => + { + b.HasOne("ErsatzTV.Core.Domain.Library", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.EmbyLibrary", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinLibrary", b => + { + b.HasOne("ErsatzTV.Core.Domain.Library", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.JellyfinLibrary", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LocalLibrary", b => + { + b.HasOne("ErsatzTV.Core.Domain.Library", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.LocalLibrary", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexLibrary", b => + { + b.HasOne("ErsatzTV.Core.Domain.Library", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.PlexLibrary", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaFile", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaFile", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.PlexMediaFile", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Artist", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.Artist", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Episode", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.Episode", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.Season", "Season") + .WithMany("Episodes") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Movie", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.Movie", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideo", b => + { + b.HasOne("ErsatzTV.Core.Domain.Artist", "Artist") + .WithMany("MusicVideos") + .HasForeignKey("ArtistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.MusicVideo", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Artist"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.OtherVideo", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.OtherVideo", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Season", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.Season", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.Show", "Show") + .WithMany("Seasons") + .HasForeignKey("ShowId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Show"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Show", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.Show", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Song", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.Song", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyMediaSource", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaSource", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.EmbyMediaSource", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinMediaSource", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaSource", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.JellyfinMediaSource", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LocalMediaSource", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaSource", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.LocalMediaSource", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSource", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaSource", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.PlexMediaSource", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemDuration", b => + { + b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemDuration", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemFlood", b => + { + b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemFlood", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemMultiple", b => + { + b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemMultiple", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemOne", b => + { + b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemOne", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyEpisode", b => + { + b.HasOne("ErsatzTV.Core.Domain.Episode", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.EmbyEpisode", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinEpisode", b => + { + b.HasOne("ErsatzTV.Core.Domain.Episode", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.JellyfinEpisode", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexEpisode", b => + { + b.HasOne("ErsatzTV.Core.Domain.Episode", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.PlexEpisode", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyMovie", b => + { + b.HasOne("ErsatzTV.Core.Domain.Movie", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.EmbyMovie", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinMovie", b => + { + b.HasOne("ErsatzTV.Core.Domain.Movie", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.JellyfinMovie", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMovie", b => + { + b.HasOne("ErsatzTV.Core.Domain.Movie", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.PlexMovie", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbySeason", b => + { + b.HasOne("ErsatzTV.Core.Domain.Season", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.EmbySeason", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinSeason", b => + { + b.HasOne("ErsatzTV.Core.Domain.Season", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.JellyfinSeason", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexSeason", b => + { + b.HasOne("ErsatzTV.Core.Domain.Season", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.PlexSeason", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyShow", b => + { + b.HasOne("ErsatzTV.Core.Domain.Show", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.EmbyShow", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinShow", b => + { + b.HasOne("ErsatzTV.Core.Domain.Show", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.JellyfinShow", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexShow", b => + { + b.HasOne("ErsatzTV.Core.Domain.Show", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.PlexShow", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ArtistMetadata", b => + { + b.Navigation("Actors"); + + b.Navigation("Artwork"); + + b.Navigation("Genres"); + + b.Navigation("Guids"); + + b.Navigation("Moods"); + + b.Navigation("Studios"); + + b.Navigation("Styles"); + + b.Navigation("Subtitles"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Channel", b => + { + b.Navigation("Artwork"); + + b.Navigation("Playouts"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Collection", b => + { + b.Navigation("CollectionItems"); + + b.Navigation("MultiCollectionItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EpisodeMetadata", b => + { + b.Navigation("Actors"); + + b.Navigation("Artwork"); + + b.Navigation("Directors"); + + b.Navigation("Genres"); + + b.Navigation("Guids"); + + b.Navigation("Studios"); + + b.Navigation("Subtitles"); + + b.Navigation("Tags"); + + b.Navigation("Writers"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Library", b => + { + b.Navigation("Paths"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LibraryPath", b => + { + b.Navigation("LibraryFolders"); + + b.Navigation("MediaItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaItem", b => + { + b.Navigation("CollectionItems"); + + b.Navigation("TraktListItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaSource", b => + { + b.Navigation("Libraries"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaVersion", b => + { + b.Navigation("Chapters"); + + b.Navigation("MediaFiles"); + + b.Navigation("Streams"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MovieMetadata", b => + { + b.Navigation("Actors"); + + b.Navigation("Artwork"); + + b.Navigation("Directors"); + + b.Navigation("Genres"); + + b.Navigation("Guids"); + + b.Navigation("Studios"); + + b.Navigation("Subtitles"); + + b.Navigation("Tags"); + + b.Navigation("Writers"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MultiCollection", b => + { + b.Navigation("MultiCollectionItems"); + + b.Navigation("MultiCollectionSmartItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideoMetadata", b => + { + b.Navigation("Actors"); + + b.Navigation("Artists"); + + b.Navigation("Artwork"); + + b.Navigation("Genres"); + + b.Navigation("Guids"); + + b.Navigation("Studios"); + + b.Navigation("Subtitles"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.OtherVideoMetadata", b => + { + b.Navigation("Actors"); + + b.Navigation("Artwork"); + + b.Navigation("Directors"); + + b.Navigation("Genres"); + + b.Navigation("Guids"); + + b.Navigation("Studios"); + + b.Navigation("Subtitles"); + + b.Navigation("Tags"); + + b.Navigation("Writers"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b => + { + b.Navigation("Items"); + + b.Navigation("ProgramScheduleAlternates"); + + b.Navigation("ProgramScheduleAnchors"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramSchedule", b => + { + b.Navigation("Items"); + + b.Navigation("Playouts"); + + b.Navigation("ProgramScheduleAlternates"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.SeasonMetadata", b => + { + b.Navigation("Actors"); + + b.Navigation("Artwork"); + + b.Navigation("Genres"); + + b.Navigation("Guids"); + + b.Navigation("Studios"); + + b.Navigation("Subtitles"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ShowMetadata", b => + { + b.Navigation("Actors"); + + b.Navigation("Artwork"); + + b.Navigation("Genres"); + + b.Navigation("Guids"); + + b.Navigation("Studios"); + + b.Navigation("Subtitles"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.SmartCollection", b => + { + b.Navigation("MultiCollectionSmartItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.SongMetadata", b => + { + b.Navigation("Actors"); + + b.Navigation("Artwork"); + + b.Navigation("Genres"); + + b.Navigation("Guids"); + + b.Navigation("Studios"); + + b.Navigation("Subtitles"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TraktList", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TraktListItem", b => + { + b.Navigation("Guids"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyLibrary", b => + { + b.Navigation("PathInfos"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinLibrary", b => + { + b.Navigation("PathInfos"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Artist", b => + { + b.Navigation("ArtistMetadata"); + + b.Navigation("MusicVideos"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Episode", b => + { + b.Navigation("EpisodeMetadata"); + + b.Navigation("MediaVersions"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Movie", b => + { + b.Navigation("MediaVersions"); + + b.Navigation("MovieMetadata"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideo", b => + { + b.Navigation("MediaVersions"); + + b.Navigation("MusicVideoMetadata"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.OtherVideo", b => + { + b.Navigation("MediaVersions"); + + b.Navigation("OtherVideoMetadata"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Season", b => + { + b.Navigation("Episodes"); + + b.Navigation("SeasonMetadata"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Show", b => + { + b.Navigation("Seasons"); + + b.Navigation("ShowMetadata"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Song", b => + { + b.Navigation("MediaVersions"); + + b.Navigation("SongMetadata"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyMediaSource", b => + { + b.Navigation("Connections"); + + b.Navigation("PathReplacements"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinMediaSource", b => + { + b.Navigation("Connections"); + + b.Navigation("PathReplacements"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSource", b => + { + b.Navigation("Connections"); + + b.Navigation("PathReplacements"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ErsatzTV.Infrastructure/Migrations/20230108192830_Add_ProgramScheduleAlternates.cs b/ErsatzTV.Infrastructure/Migrations/20230108192830_Add_ProgramScheduleAlternates.cs new file mode 100644 index 000000000..a82e871b8 --- /dev/null +++ b/ErsatzTV.Infrastructure/Migrations/20230108192830_Add_ProgramScheduleAlternates.cs @@ -0,0 +1,61 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ErsatzTV.Infrastructure.Migrations +{ + /// + public partial class AddProgramScheduleAlternates : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ProgramScheduleAlternate", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + PlayoutId = table.Column(type: "INTEGER", nullable: false), + ProgramScheduleId = table.Column(type: "INTEGER", nullable: false), + Index = table.Column(type: "INTEGER", nullable: false), + DaysOfWeek = table.Column(type: "TEXT", nullable: true), + DaysOfMonth = table.Column(type: "TEXT", nullable: true), + MonthsOfYear = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ProgramScheduleAlternate", x => x.Id); + table.ForeignKey( + name: "FK_ProgramScheduleAlternate_Playout_PlayoutId", + column: x => x.PlayoutId, + principalTable: "Playout", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ProgramScheduleAlternate_ProgramSchedule_ProgramScheduleId", + column: x => x.ProgramScheduleId, + principalTable: "ProgramSchedule", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_ProgramScheduleAlternate_PlayoutId", + table: "ProgramScheduleAlternate", + column: "PlayoutId"); + + migrationBuilder.CreateIndex( + name: "IX_ProgramScheduleAlternate_ProgramScheduleId", + table: "ProgramScheduleAlternate", + column: "ProgramScheduleId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ProgramScheduleAlternate"); + } + } +} diff --git a/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs b/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs index c7af12d93..9d2af339a 100644 --- a/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs +++ b/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs @@ -15,7 +15,7 @@ namespace ErsatzTV.Infrastructure.Migrations protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "6.0.10"); + modelBuilder.HasAnnotation("ProductVersion", "7.0.1"); modelBuilder.Entity("ErsatzTV.Core.Domain.Actor", b => { @@ -820,6 +820,8 @@ namespace ErsatzTV.Infrastructure.Migrations b.HasIndex("MediaSourceId"); b.ToTable("Library", (string)null); + + b.UseTptMappingStrategy(); }); modelBuilder.Entity("ErsatzTV.Core.Domain.LibraryFolder", b => @@ -914,6 +916,8 @@ namespace ErsatzTV.Infrastructure.Migrations .IsUnique(); b.ToTable("MediaFile", (string)null); + + b.UseTptMappingStrategy(); }); modelBuilder.Entity("ErsatzTV.Core.Domain.MediaItem", b => @@ -933,6 +937,8 @@ namespace ErsatzTV.Infrastructure.Migrations b.HasIndex("LibraryPathId"); b.ToTable("MediaItem", (string)null); + + b.UseTptMappingStrategy(); }); modelBuilder.Entity("ErsatzTV.Core.Domain.MediaSource", b => @@ -944,6 +950,8 @@ namespace ErsatzTV.Infrastructure.Migrations b.HasKey("Id"); b.ToTable("MediaSource", (string)null); + + b.UseTptMappingStrategy(); }); modelBuilder.Entity("ErsatzTV.Core.Domain.MediaStream", b => @@ -1598,6 +1606,39 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("ProgramSchedule", (string)null); }); + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleAlternate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DaysOfMonth") + .HasColumnType("TEXT"); + + b.Property("DaysOfWeek") + .HasColumnType("TEXT"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("MonthsOfYear") + .HasColumnType("TEXT"); + + b.Property("PlayoutId") + .HasColumnType("INTEGER"); + + b.Property("ProgramScheduleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PlayoutId"); + + b.HasIndex("ProgramScheduleId"); + + b.ToTable("ProgramScheduleAlternate", (string)null); + }); + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItem", b => { b.Property("Id") @@ -1692,6 +1733,8 @@ namespace ErsatzTV.Infrastructure.Migrations b.HasIndex("WatermarkId"); b.ToTable("ProgramScheduleItem", (string)null); + + b.UseTptMappingStrategy(); }); modelBuilder.Entity("ErsatzTV.Core.Domain.Resolution", b => @@ -2265,13 +2308,6 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("JellyfinPathInfo"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.Artist", b => - { - b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); - - b.ToTable("Artist", (string)null); - }); - modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyLibrary", b => { b.HasBaseType("ErsatzTV.Core.Domain.Library"); @@ -2285,31 +2321,6 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("EmbyLibrary", (string)null); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyMediaSource", b => - { - b.HasBaseType("ErsatzTV.Core.Domain.MediaSource"); - - b.Property("OperatingSystem") - .HasColumnType("TEXT"); - - b.Property("ServerName") - .HasColumnType("TEXT"); - - b.ToTable("EmbyMediaSource", (string)null); - }); - - modelBuilder.Entity("ErsatzTV.Core.Domain.Episode", b => - { - b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); - - b.Property("SeasonId") - .HasColumnType("INTEGER"); - - b.HasIndex("SeasonId"); - - b.ToTable("Episode", (string)null); - }); - modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinLibrary", b => { b.HasBaseType("ErsatzTV.Core.Domain.Library"); @@ -2323,19 +2334,6 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("JellyfinLibrary", (string)null); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinMediaSource", b => - { - b.HasBaseType("ErsatzTV.Core.Domain.MediaSource"); - - b.Property("OperatingSystem") - .HasColumnType("TEXT"); - - b.Property("ServerName") - .HasColumnType("TEXT"); - - b.ToTable("JellyfinMediaSource", (string)null); - }); - modelBuilder.Entity("ErsatzTV.Core.Domain.LocalLibrary", b => { b.HasBaseType("ErsatzTV.Core.Domain.Library"); @@ -2343,11 +2341,49 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("LocalLibrary", (string)null); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.LocalMediaSource", b => + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexLibrary", b => { - b.HasBaseType("ErsatzTV.Core.Domain.MediaSource"); + b.HasBaseType("ErsatzTV.Core.Domain.Library"); - b.ToTable("LocalMediaSource", (string)null); + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("ShouldSyncItems") + .HasColumnType("INTEGER"); + + b.ToTable("PlexLibrary", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaFile", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaFile"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("PlexId") + .HasColumnType("INTEGER"); + + b.ToTable("PlexMediaFile", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Artist", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.ToTable("Artist", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Episode", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.Property("SeasonId") + .HasColumnType("INTEGER"); + + b.HasIndex("SeasonId"); + + b.ToTable("Episode", (string)null); }); modelBuilder.Entity("ErsatzTV.Core.Domain.Movie", b => @@ -2376,30 +2412,66 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("OtherVideo", (string)null); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.PlexLibrary", b => + modelBuilder.Entity("ErsatzTV.Core.Domain.Season", b => { - b.HasBaseType("ErsatzTV.Core.Domain.Library"); + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); - b.Property("Key") - .HasColumnType("TEXT"); - - b.Property("ShouldSyncItems") + b.Property("SeasonNumber") .HasColumnType("INTEGER"); - b.ToTable("PlexLibrary", (string)null); + b.Property("ShowId") + .HasColumnType("INTEGER"); + + b.HasIndex("ShowId"); + + b.ToTable("Season", (string)null); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaFile", b => + modelBuilder.Entity("ErsatzTV.Core.Domain.Show", b => { - b.HasBaseType("ErsatzTV.Core.Domain.MediaFile"); + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); - b.Property("Key") + 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("OperatingSystem") .HasColumnType("TEXT"); - b.Property("PlexId") - .HasColumnType("INTEGER"); + b.Property("ServerName") + .HasColumnType("TEXT"); - b.ToTable("PlexMediaFile", (string)null); + b.ToTable("EmbyMediaSource", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinMediaSource", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaSource"); + + b.Property("OperatingSystem") + .HasColumnType("TEXT"); + + b.Property("ServerName") + .HasColumnType("TEXT"); + + b.ToTable("JellyfinMediaSource", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LocalMediaSource", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaSource"); + + b.ToTable("LocalMediaSource", (string)null); }); modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSource", b => @@ -2461,35 +2533,6 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("ProgramScheduleOneItem", (string)null); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.Season", b => - { - b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); - - b.Property("SeasonNumber") - .HasColumnType("INTEGER"); - - b.Property("ShowId") - .HasColumnType("INTEGER"); - - b.HasIndex("ShowId"); - - b.ToTable("Season", (string)null); - }); - - modelBuilder.Entity("ErsatzTV.Core.Domain.Show", b => - { - b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); - - b.ToTable("Show", (string)null); - }); - - modelBuilder.Entity("ErsatzTV.Core.Domain.Song", b => - { - b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); - - b.ToTable("Song", (string)null); - }); - modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyEpisode", b => { b.HasBaseType("ErsatzTV.Core.Domain.Episode"); @@ -2503,45 +2546,6 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("EmbyEpisode", (string)null); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyMovie", b => - { - b.HasBaseType("ErsatzTV.Core.Domain.Movie"); - - b.Property("Etag") - .HasColumnType("TEXT"); - - b.Property("ItemId") - .HasColumnType("TEXT"); - - b.ToTable("EmbyMovie", (string)null); - }); - - modelBuilder.Entity("ErsatzTV.Core.Domain.EmbySeason", b => - { - b.HasBaseType("ErsatzTV.Core.Domain.Season"); - - b.Property("Etag") - .HasColumnType("TEXT"); - - b.Property("ItemId") - .HasColumnType("TEXT"); - - b.ToTable("EmbySeason", (string)null); - }); - - modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyShow", b => - { - b.HasBaseType("ErsatzTV.Core.Domain.Show"); - - b.Property("Etag") - .HasColumnType("TEXT"); - - b.Property("ItemId") - .HasColumnType("TEXT"); - - b.ToTable("EmbyShow", (string)null); - }); - modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinEpisode", b => { b.HasBaseType("ErsatzTV.Core.Domain.Episode"); @@ -2555,45 +2559,6 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("JellyfinEpisode", (string)null); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinMovie", b => - { - b.HasBaseType("ErsatzTV.Core.Domain.Movie"); - - b.Property("Etag") - .HasColumnType("TEXT"); - - b.Property("ItemId") - .HasColumnType("TEXT"); - - b.ToTable("JellyfinMovie", (string)null); - }); - - modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinSeason", b => - { - b.HasBaseType("ErsatzTV.Core.Domain.Season"); - - b.Property("Etag") - .HasColumnType("TEXT"); - - b.Property("ItemId") - .HasColumnType("TEXT"); - - b.ToTable("JellyfinSeason", (string)null); - }); - - modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinShow", b => - { - b.HasBaseType("ErsatzTV.Core.Domain.Show"); - - b.Property("Etag") - .HasColumnType("TEXT"); - - b.Property("ItemId") - .HasColumnType("TEXT"); - - b.ToTable("JellyfinShow", (string)null); - }); - modelBuilder.Entity("ErsatzTV.Core.Domain.PlexEpisode", b => { b.HasBaseType("ErsatzTV.Core.Domain.Episode"); @@ -2607,6 +2572,32 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("PlexEpisode", (string)null); }); + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyMovie", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Movie"); + + b.Property("Etag") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.ToTable("EmbyMovie", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinMovie", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Movie"); + + b.Property("Etag") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.ToTable("JellyfinMovie", (string)null); + }); + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMovie", b => { b.HasBaseType("ErsatzTV.Core.Domain.Movie"); @@ -2620,6 +2611,32 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("PlexMovie", (string)null); }); + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbySeason", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Season"); + + b.Property("Etag") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.ToTable("EmbySeason", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinSeason", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Season"); + + b.Property("Etag") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.ToTable("JellyfinSeason", (string)null); + }); + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexSeason", b => { b.HasBaseType("ErsatzTV.Core.Domain.Season"); @@ -2633,6 +2650,32 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("PlexSeason", (string)null); }); + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyShow", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Show"); + + b.Property("Etag") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.ToTable("EmbyShow", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinShow", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Show"); + + b.Property("Etag") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.ToTable("JellyfinShow", (string)null); + }); + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexShow", b => { b.HasBaseType("ErsatzTV.Core.Domain.Show"); @@ -3371,6 +3414,25 @@ namespace ErsatzTV.Infrastructure.Migrations 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") @@ -3675,15 +3737,6 @@ namespace ErsatzTV.Infrastructure.Migrations .OnDelete(DeleteBehavior.Cascade); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.Artist", b => - { - b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) - .WithOne() - .HasForeignKey("ErsatzTV.Core.Domain.Artist", "Id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyLibrary", b => { b.HasOne("ErsatzTV.Core.Domain.Library", null) @@ -3693,11 +3746,47 @@ namespace ErsatzTV.Infrastructure.Migrations .IsRequired(); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyMediaSource", b => + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinLibrary", b => { - b.HasOne("ErsatzTV.Core.Domain.MediaSource", null) + b.HasOne("ErsatzTV.Core.Domain.Library", null) .WithOne() - .HasForeignKey("ErsatzTV.Core.Domain.EmbyMediaSource", "Id") + .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(); }); @@ -3719,42 +3808,6 @@ namespace ErsatzTV.Infrastructure.Migrations b.Navigation("Season"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinLibrary", b => - { - b.HasOne("ErsatzTV.Core.Domain.Library", null) - .WithOne() - .HasForeignKey("ErsatzTV.Core.Domain.JellyfinLibrary", "Id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinMediaSource", b => - { - b.HasOne("ErsatzTV.Core.Domain.MediaSource", null) - .WithOne() - .HasForeignKey("ErsatzTV.Core.Domain.JellyfinMediaSource", "Id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ErsatzTV.Core.Domain.LocalLibrary", b => - { - b.HasOne("ErsatzTV.Core.Domain.Library", null) - .WithOne() - .HasForeignKey("ErsatzTV.Core.Domain.LocalLibrary", "Id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ErsatzTV.Core.Domain.LocalMediaSource", b => - { - b.HasOne("ErsatzTV.Core.Domain.MediaSource", null) - .WithOne() - .HasForeignKey("ErsatzTV.Core.Domain.LocalMediaSource", "Id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - modelBuilder.Entity("ErsatzTV.Core.Domain.Movie", b => { b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) @@ -3790,20 +3843,64 @@ namespace ErsatzTV.Infrastructure.Migrations .IsRequired(); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.PlexLibrary", b => + modelBuilder.Entity("ErsatzTV.Core.Domain.Season", b => { - b.HasOne("ErsatzTV.Core.Domain.Library", null) + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) .WithOne() - .HasForeignKey("ErsatzTV.Core.Domain.PlexLibrary", "Id") + .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.PlexMediaFile", b => + modelBuilder.Entity("ErsatzTV.Core.Domain.Song", b => { - b.HasOne("ErsatzTV.Core.Domain.MediaFile", null) + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) .WithOne() - .HasForeignKey("ErsatzTV.Core.Domain.PlexMediaFile", "Id") + .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(); }); @@ -3853,41 +3950,6 @@ namespace ErsatzTV.Infrastructure.Migrations .IsRequired(); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.Season", b => - { - b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) - .WithOne() - .HasForeignKey("ErsatzTV.Core.Domain.Season", "Id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("ErsatzTV.Core.Domain.Show", "Show") - .WithMany("Seasons") - .HasForeignKey("ShowId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Show"); - }); - - modelBuilder.Entity("ErsatzTV.Core.Domain.Show", b => - { - b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) - .WithOne() - .HasForeignKey("ErsatzTV.Core.Domain.Show", "Id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ErsatzTV.Core.Domain.Song", b => - { - b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) - .WithOne() - .HasForeignKey("ErsatzTV.Core.Domain.Song", "Id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyEpisode", b => { b.HasOne("ErsatzTV.Core.Domain.Episode", null) @@ -3897,33 +3959,6 @@ namespace ErsatzTV.Infrastructure.Migrations .IsRequired(); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyMovie", b => - { - b.HasOne("ErsatzTV.Core.Domain.Movie", null) - .WithOne() - .HasForeignKey("ErsatzTV.Core.Domain.EmbyMovie", "Id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ErsatzTV.Core.Domain.EmbySeason", b => - { - b.HasOne("ErsatzTV.Core.Domain.Season", null) - .WithOne() - .HasForeignKey("ErsatzTV.Core.Domain.EmbySeason", "Id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyShow", b => - { - b.HasOne("ErsatzTV.Core.Domain.Show", null) - .WithOne() - .HasForeignKey("ErsatzTV.Core.Domain.EmbyShow", "Id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinEpisode", b => { b.HasOne("ErsatzTV.Core.Domain.Episode", null) @@ -3933,33 +3968,6 @@ namespace ErsatzTV.Infrastructure.Migrations .IsRequired(); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinMovie", b => - { - b.HasOne("ErsatzTV.Core.Domain.Movie", null) - .WithOne() - .HasForeignKey("ErsatzTV.Core.Domain.JellyfinMovie", "Id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinSeason", b => - { - b.HasOne("ErsatzTV.Core.Domain.Season", null) - .WithOne() - .HasForeignKey("ErsatzTV.Core.Domain.JellyfinSeason", "Id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinShow", b => - { - b.HasOne("ErsatzTV.Core.Domain.Show", null) - .WithOne() - .HasForeignKey("ErsatzTV.Core.Domain.JellyfinShow", "Id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - modelBuilder.Entity("ErsatzTV.Core.Domain.PlexEpisode", b => { b.HasOne("ErsatzTV.Core.Domain.Episode", null) @@ -3969,6 +3977,24 @@ namespace ErsatzTV.Infrastructure.Migrations .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) @@ -3978,6 +4004,24 @@ namespace ErsatzTV.Infrastructure.Migrations .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) @@ -3987,6 +4031,24 @@ namespace ErsatzTV.Infrastructure.Migrations .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) @@ -4157,6 +4219,8 @@ namespace ErsatzTV.Infrastructure.Migrations { b.Navigation("Items"); + b.Navigation("ProgramScheduleAlternates"); + b.Navigation("ProgramScheduleAnchors"); }); @@ -4165,6 +4229,8 @@ namespace ErsatzTV.Infrastructure.Migrations b.Navigation("Items"); b.Navigation("Playouts"); + + b.Navigation("ProgramScheduleAlternates"); }); modelBuilder.Entity("ErsatzTV.Core.Domain.SeasonMetadata", b => @@ -4233,42 +4299,28 @@ namespace ErsatzTV.Infrastructure.Migrations b.Navigation("Guids"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.Artist", b => - { - b.Navigation("ArtistMetadata"); - - b.Navigation("MusicVideos"); - }); - modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyLibrary", b => { b.Navigation("PathInfos"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyMediaSource", b => - { - b.Navigation("Connections"); - - b.Navigation("PathReplacements"); - }); - - modelBuilder.Entity("ErsatzTV.Core.Domain.Episode", b => - { - b.Navigation("EpisodeMetadata"); - - b.Navigation("MediaVersions"); - }); - modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinLibrary", b => { b.Navigation("PathInfos"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinMediaSource", b => + modelBuilder.Entity("ErsatzTV.Core.Domain.Artist", b => { - b.Navigation("Connections"); + b.Navigation("ArtistMetadata"); - b.Navigation("PathReplacements"); + b.Navigation("MusicVideos"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Episode", b => + { + b.Navigation("EpisodeMetadata"); + + b.Navigation("MediaVersions"); }); modelBuilder.Entity("ErsatzTV.Core.Domain.Movie", b => @@ -4292,13 +4344,6 @@ namespace ErsatzTV.Infrastructure.Migrations b.Navigation("OtherVideoMetadata"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSource", b => - { - b.Navigation("Connections"); - - b.Navigation("PathReplacements"); - }); - modelBuilder.Entity("ErsatzTV.Core.Domain.Season", b => { b.Navigation("Episodes"); @@ -4319,6 +4364,27 @@ namespace ErsatzTV.Infrastructure.Migrations 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/Pages/PlayoutAlternateSchedulesEditor.razor b/ErsatzTV/Pages/PlayoutAlternateSchedulesEditor.razor new file mode 100644 index 000000000..78943e652 --- /dev/null +++ b/ErsatzTV/Pages/PlayoutAlternateSchedulesEditor.razor @@ -0,0 +1,486 @@ +@page "/playouts/{Id:int}/alternate-schedules" +@using ErsatzTV.Application.ProgramSchedules +@using ErsatzTV.Application.Playouts +@using Microsoft.AspNetCore.Components +@using ErsatzTV.Application.Channels +@using System.Text +@using System.Globalization +@implements IDisposable +@inject NavigationManager NavigationManager +@inject ILogger Logger +@inject ISnackbar Snackbar +@inject IMediator Mediator + + + + + @_channelName Alternate Schedules + + In priority order from top to bottom + + + + + + + + + + + + Schedule + Days of the Week + Days of the Month + Months + + + + + + + + @context.ProgramSchedule.Name + + + + + @ToDaysOfWeekString(context.DaysOfWeek) + + + + + @ToDaysOfMonthString(context.DaysOfMonth) + + + + + @ToMonthsOfYearString(context.MonthsOfYear) + + + + + + + + + + + + + + + + + + Add Alternate Schedule + + + Save Changes + + +@if (_selectedItem is not null) +{ + + +
+
+ + + + @foreach (ProgramScheduleViewModel schedule in _schedules) + { + @schedule.Name + } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Weekdays + + + Weekends + + + All + + + None + + + +
+
+ + + + @foreach (int day in Enumerable.Range(1, 31)) + { + + + + } + + + + + All + + + None + + + +
+
+ + + + + + @foreach (int month in Enumerable.Range(2, 11)) + { + + + + } + + + + All + + + None + + + +
+
+
+} +
+ +@code { + private readonly CancellationTokenSource _cts = new(); + + private readonly DateTimeFormatInfo _dtf = CultureInfo.CurrentUICulture.DateTimeFormat; + + [Parameter] + public int Id { get; set; } + + private string _channelName; + private List _items; + private List _schedules; + + private PlayoutAlternateScheduleEditViewModel _selectedItem; + + public void Dispose() + { + _cts.Cancel(); + _cts.Dispose(); + } + + protected override async Task OnParametersSetAsync() => await LoadScheduleItems(); + + private async Task LoadScheduleItems() + { + _schedules = await Mediator.Send(new GetAllProgramSchedules(), _cts.Token) + .Map(list => list.OrderBy(vm => vm.Name, StringComparer.CurrentCultureIgnoreCase).ToList()); + + _channelName = (await Mediator.Send(new GetChannelNameByPlayoutId(Id), _cts.Token)).IfNone(string.Empty); + + List results = await Mediator.Send(new GetPlayoutAlternateSchedules(Id), _cts.Token); + _items = results.Map(ProjectToEditViewModel).ToList(); + if (_items.Count == 1) + { + _selectedItem = _items.Head(); + } + } + + private PlayoutAlternateScheduleEditViewModel ProjectToEditViewModel(PlayoutAlternateScheduleViewModel item) => + new() + { + Id = item.Id, + Index = item.Index, + ProgramSchedule = _schedules.Find(vm => vm.Id == item.ProgramScheduleId), + DaysOfWeek = item.DaysOfWeek.OrderBy(x => ((int)x + 6) % 7).ToList(), + DaysOfMonth = item.DaysOfMonth.ToList(), + MonthsOfYear = item.MonthsOfYear.ToList() + }; + + private void DayOfWeekChanged(DayOfWeek dayOfWeek, bool isChecked) + { + if (isChecked && !_selectedItem.DaysOfWeek.Contains(dayOfWeek)) + { + _selectedItem.DaysOfWeek.Add(dayOfWeek); + _selectedItem.DaysOfWeek = _selectedItem.DaysOfWeek.OrderBy(x => ((int)x + 6) % 7).ToList(); + } + + if (!isChecked) + { + _selectedItem.DaysOfWeek.Remove(dayOfWeek); + } + } + + private void SelectWeekdays() + { + _selectedItem.DaysOfWeek.Clear(); + _selectedItem.DaysOfWeek.AddRange(new[] + { + DayOfWeek.Monday, + DayOfWeek.Tuesday, + DayOfWeek.Wednesday, + DayOfWeek.Thursday, + DayOfWeek.Friday + }); + } + + private void SelectWeekends() + { + _selectedItem.DaysOfWeek.Clear(); + _selectedItem.DaysOfWeek.AddRange(new[] + { + DayOfWeek.Saturday, + DayOfWeek.Sunday + }); + } + + private void SelectAllDaysOfWeek() + { + _selectedItem.DaysOfWeek.Clear(); + _selectedItem.DaysOfWeek.AddRange(ProgramScheduleAlternate.AllDaysOfWeek()); + } + + private void SelectNoDaysOfWeek() + { + _selectedItem.DaysOfWeek.Clear(); + } + + private void DayOfMonthChanged(int dayOfMonth, bool isChecked) + { + if (isChecked && !_selectedItem.DaysOfMonth.Contains(dayOfMonth)) + { + _selectedItem.DaysOfMonth.Add(dayOfMonth); + _selectedItem.DaysOfMonth.Sort(); + } + + if (!isChecked) + { + _selectedItem.DaysOfMonth.Remove(dayOfMonth); + } + } + + private void SelectAllDaysOfMonth() + { + _selectedItem.DaysOfMonth.Clear(); + _selectedItem.DaysOfMonth.AddRange(ProgramScheduleAlternate.AllDaysOfMonth()); + } + + private void SelectNoDaysOfMonth() + { + _selectedItem.DaysOfMonth.Clear(); + } + + private void MonthOfYearChanged(int monthOfYear, bool isChecked) + { + if (isChecked && !_selectedItem.MonthsOfYear.Contains(monthOfYear)) + { + _selectedItem.MonthsOfYear.Add(monthOfYear); + _selectedItem.MonthsOfYear.Sort(); + } + + if (!isChecked) + { + _selectedItem.MonthsOfYear.Remove(monthOfYear); + } + } + + private void SelectAllMonthsOfYear() + { + _selectedItem.MonthsOfYear.Clear(); + _selectedItem.MonthsOfYear.AddRange(ProgramScheduleAlternate.AllMonthsOfYear()); + } + + private void SelectNoMonthsOfYear() + { + _selectedItem.MonthsOfYear.Clear(); + } + + private void AddAlternateSchedule() + { + var item = new PlayoutAlternateScheduleEditViewModel + { + Index = _items.Map(i => i.Index).DefaultIfEmpty().Max() + 1, + ProgramSchedule = _schedules.Head(), + DaysOfWeek = ProgramScheduleAlternate.AllDaysOfWeek(), + DaysOfMonth = ProgramScheduleAlternate.AllDaysOfMonth(), + MonthsOfYear = ProgramScheduleAlternate.AllMonthsOfYear() + }; + + _items.Add(item); + _selectedItem = item; + } + + private void RemoveAlternateSchedule(PlayoutAlternateScheduleEditViewModel item) + { + _selectedItem = null; + _items.Remove(item); + } + + private void MoveItemUp(PlayoutAlternateScheduleEditViewModel item) + { + // swap with lower index + PlayoutAlternateScheduleEditViewModel toSwap = _items.OrderByDescending(x => x.Index).First(x => x.Index < item.Index); + (toSwap.Index, item.Index) = (item.Index, toSwap.Index); + } + + private void MoveItemDown(PlayoutAlternateScheduleEditViewModel item) + { + // swap with higher index + PlayoutAlternateScheduleEditViewModel toSwap = _items.OrderBy(x => x.Index).First(x => x.Index > item.Index); + (toSwap.Index, item.Index) = (item.Index, toSwap.Index); + } + + private async Task SaveChanges() + { + var items = _items.Map(item => new ReplacePlayoutAlternateSchedule( + item.Id, + item.Index, + item.ProgramSchedule.Id, + item.DaysOfWeek, + item.DaysOfMonth, + item.MonthsOfYear)).ToList(); + + Seq errorMessages = await Mediator.Send(new ReplacePlayoutAlternateScheduleItems(Id, items), _cts.Token) + .Map(e => e.LeftToSeq()); + + errorMessages.HeadOrNone().Match( + error => + { + Snackbar.Add($"Unexpected error saving alternate schedules: {error.Value}", Severity.Error); + Logger.LogError("Unexpected error saving alternate schedules: {Error}", error.Value); + }, + () => NavigationManager.NavigateTo("/playouts")); + } + + private string ToDaysOfWeekString(List daysOfWeek) + { + if (daysOfWeek.Count is 0 or 7) + { + return "*any*"; + } + + daysOfWeek.Sort(); + + return string.Join(", ", daysOfWeek.Map(_dtf.GetAbbreviatedDayName)); + } + + private string ToDaysOfMonthString(List daysOfMonth) + { + if (daysOfMonth.Count is 0 or 31) + { + return "*any*"; + } + + return ToRangeString(daysOfMonth); + } + + private string ToMonthsOfYearString(List monthsOfYear) + { + if (monthsOfYear.Count is 0 or 12) + { + return "*any*"; + } + + monthsOfYear.Sort(); + + return string.Join(", ", monthsOfYear.Map(_dtf.GetAbbreviatedMonthName)); + } + + private static string ToRangeString(List list) + { + list = list.Distinct().ToList(); + list.Sort(); + + var result = new StringBuilder(); + for (var i = 0; i < list.Count; i++) + { + int temp = list[i]; + + //add a number + result.Append(list[i]); + + //skip number(s) between a range + while (i < list.Count - 1 && list[i + 1] == list[i] + 1) + { + i++; + } + + //add the range + if (temp != list[i]) + { + result.Append("-").Append(list[i]); + } + + //add comma + if (i != list.Count - 1) + { + result.Append(", "); + } + + } + + return result.ToString(); + } + +} \ No newline at end of file diff --git a/ErsatzTV/Pages/Playouts.razor b/ErsatzTV/Pages/Playouts.razor index ef06305bb..56ceec0b3 100644 --- a/ErsatzTV/Pages/Playouts.razor +++ b/ErsatzTV/Pages/Playouts.razor @@ -33,7 +33,7 @@ - Schedule + Default Schedule @* Playout Type *@ @@ -45,6 +45,11 @@ @* @context.ProgramSchedulePlayoutType *@
+ + + + @@ -168,7 +173,7 @@ } } } - + private async Task ResetPlayout(PlayoutNameViewModel playout) { await _mediator.Send(new BuildPlayout(playout.PlayoutId, PlayoutBuildMode.Reset), _cts.Token); diff --git a/ErsatzTV/Validators/PlayoutAlternateScheduleEditViewModelValidator.cs b/ErsatzTV/Validators/PlayoutAlternateScheduleEditViewModelValidator.cs new file mode 100644 index 000000000..17f9e7b70 --- /dev/null +++ b/ErsatzTV/Validators/PlayoutAlternateScheduleEditViewModelValidator.cs @@ -0,0 +1,12 @@ +using ErsatzTV.ViewModels; +using FluentValidation; + +namespace ErsatzTV.Validators; + +public class PlayoutAlternateScheduleEditViewModelValidator : AbstractValidator +{ + public PlayoutAlternateScheduleEditViewModelValidator() + { + RuleFor(p => p.ProgramSchedule).NotNull(); + } +} diff --git a/ErsatzTV/ViewModels/PlayoutAlternateScheduleEditViewModel.cs b/ErsatzTV/ViewModels/PlayoutAlternateScheduleEditViewModel.cs new file mode 100644 index 000000000..590de5f25 --- /dev/null +++ b/ErsatzTV/ViewModels/PlayoutAlternateScheduleEditViewModel.cs @@ -0,0 +1,13 @@ +using ErsatzTV.Application.ProgramSchedules; + +namespace ErsatzTV.ViewModels; + +public class PlayoutAlternateScheduleEditViewModel +{ + public int Id { get; set; } + public int Index { get; set; } + public ProgramScheduleViewModel ProgramSchedule { get; set; } + public List DaysOfWeek { get; set; } + public List DaysOfMonth { get; set; } + public List MonthsOfYear { get; set; } +}