diff --git a/ErsatzTV.Application/MediaCards/Mapper.cs b/ErsatzTV.Application/MediaCards/Mapper.cs index b26f4e419..04a18e760 100644 --- a/ErsatzTV.Application/MediaCards/Mapper.cs +++ b/ErsatzTV.Application/MediaCards/Mapper.cs @@ -39,7 +39,8 @@ namespace ErsatzTV.Application.MediaCards internal static TelevisionEpisodeCardViewModel ProjectToViewModel( EpisodeMetadata episodeMetadata, Option maybeJellyfin, - Option maybeEmby) => + Option maybeEmby, + bool isSearchResult) => new( episodeMetadata.EpisodeId, episodeMetadata.ReleaseDate ?? DateTime.MinValue, @@ -48,12 +49,21 @@ namespace ErsatzTV.Application.MediaCards () => string.Empty), episodeMetadata.Episode.Season.ShowId, episodeMetadata.Episode.SeasonId, + episodeMetadata.Episode.Season.SeasonNumber, episodeMetadata.Episode.EpisodeNumber, episodeMetadata.Title, + episodeMetadata.SortTitle, episodeMetadata.Episode.EpisodeMetadata.HeadOrNone().Match( em => em.Plot ?? string.Empty, () => string.Empty), - GetThumbnail(episodeMetadata, maybeJellyfin, maybeEmby)); + isSearchResult + ? GetPoster( + episodeMetadata.Episode.Season.SeasonMetadata.Head(), + maybeJellyfin, + maybeEmby) + : GetThumbnail(episodeMetadata, maybeJellyfin, maybeEmby), + episodeMetadata.Directors.Map(d => d.Name).ToList(), + episodeMetadata.Writers.Map(w => w.Name).ToList()); internal static MovieCardViewModel ProjectToViewModel( MovieMetadata movieMetadata, @@ -101,7 +111,7 @@ namespace ErsatzTV.Application.MediaCards collection.MediaItems.OfType().Map(s => ProjectToViewModel(s, maybeJellyfin, maybeEmby)) .ToList(), collection.MediaItems.OfType() - .Map(e => ProjectToViewModel(e.EpisodeMetadata.Head(), maybeJellyfin, maybeEmby)) + .Map(e => ProjectToViewModel(e.EpisodeMetadata.Head(), maybeJellyfin, maybeEmby, false)) .ToList(), collection.MediaItems.OfType().Map(a => ProjectToViewModel(a.ArtistMetadata.Head())).ToList(), collection.MediaItems.OfType().Map(mv => ProjectToViewModel(mv.MusicVideoMetadata.Head())) diff --git a/ErsatzTV.Application/MediaCards/Queries/GetTelevisionEpisodeCardsHandler.cs b/ErsatzTV.Application/MediaCards/Queries/GetTelevisionEpisodeCardsHandler.cs index ade1f9a6d..97607c779 100644 --- a/ErsatzTV.Application/MediaCards/Queries/GetTelevisionEpisodeCardsHandler.cs +++ b/ErsatzTV.Application/MediaCards/Queries/GetTelevisionEpisodeCardsHandler.cs @@ -4,6 +4,7 @@ using System.Threading; using System.Threading.Tasks; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; +using ErsatzTV.Core.Search; using LanguageExt; using MediatR; using static ErsatzTV.Application.MediaCards.Mapper; @@ -39,9 +40,9 @@ namespace ErsatzTV.Application.MediaCards.Queries List results = await _televisionRepository .GetPagedEpisodes(request.TelevisionSeasonId, request.PageNumber, request.PageSize) - .Map(list => list.Map(e => ProjectToViewModel(e, maybeJellyfin, maybeEmby)).ToList()); + .Map(list => list.Map(e => ProjectToViewModel(e, maybeJellyfin, maybeEmby, false)).ToList()); - return new TelevisionEpisodeCardResultsViewModel(count, results); + return new TelevisionEpisodeCardResultsViewModel(count, results, Option.None); } } } diff --git a/ErsatzTV.Application/MediaCards/TelevisionEpisodeCardResultsViewModel.cs b/ErsatzTV.Application/MediaCards/TelevisionEpisodeCardResultsViewModel.cs index 348b7f9e4..bec3054a2 100644 --- a/ErsatzTV.Application/MediaCards/TelevisionEpisodeCardResultsViewModel.cs +++ b/ErsatzTV.Application/MediaCards/TelevisionEpisodeCardResultsViewModel.cs @@ -1,6 +1,11 @@ using System.Collections.Generic; +using ErsatzTV.Core.Search; +using LanguageExt; namespace ErsatzTV.Application.MediaCards { - public record TelevisionEpisodeCardResultsViewModel(int Count, List Cards); + public record TelevisionEpisodeCardResultsViewModel( + int Count, + List Cards, + Option PageMap); } diff --git a/ErsatzTV.Application/MediaCards/TelevisionEpisodeCardViewModel.cs b/ErsatzTV.Application/MediaCards/TelevisionEpisodeCardViewModel.cs index 2693c3c45..9e91bb3cb 100644 --- a/ErsatzTV.Application/MediaCards/TelevisionEpisodeCardViewModel.cs +++ b/ErsatzTV.Application/MediaCards/TelevisionEpisodeCardViewModel.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace ErsatzTV.Application.MediaCards { @@ -9,13 +10,17 @@ namespace ErsatzTV.Application.MediaCards string ShowTitle, int ShowId, int SeasonId, + int Season, int Episode, string Title, + string SortTitle, string Plot, - string Poster) : MediaCardViewModel( + string Poster, + List Directors, + List Writers) : MediaCardViewModel( EpisodeId, Title, $"Episode {Episode}", - $"Episode {Episode}", + SortTitle, Poster); } diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollection.cs b/ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollection.cs index 94810428a..d27841210 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollection.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollection.cs @@ -9,6 +9,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands int CollectionId, List MovieIds, List ShowIds, + List EpisodeIds, List ArtistIds, List MusicVideoIds) : MediatR.IRequest>; } diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollectionHandler.cs index 31c40dbdc..5c214de71 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollectionHandler.cs @@ -41,6 +41,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands { var allItems = request.MovieIds .Append(request.ShowIds) + .Append(request.EpisodeIds) .Append(request.ArtistIds) .Append(request.MusicVideoIds) .ToList(); @@ -59,8 +60,9 @@ namespace ErsatzTV.Application.MediaCollections.Commands } private async Task> Validate(AddItemsToCollection request) => - (await CollectionMustExist(request), await ValidateMovies(request), await ValidateShows(request)) - .Apply((_, _, _) => Unit.Default); + (await CollectionMustExist(request), await ValidateMovies(request), await ValidateShows(request), + await ValidateEpisodes(request)) + .Apply((_, _, _, _) => Unit.Default); private Task> CollectionMustExist(AddItemsToCollection request) => _mediaCollectionRepository.GetCollectionWithItems(request.CollectionId) @@ -80,5 +82,12 @@ namespace ErsatzTV.Application.MediaCollections.Commands .Filter(v => v == true) .MapT(_ => Unit.Default) .Map(v => v.ToValidation("Show does not exist")); + + private Task> ValidateEpisodes(AddItemsToCollection request) => + _televisionRepository.AllEpisodesExist(request.EpisodeIds) + .Map(Optional) + .Filter(v => v == true) + .MapT(_ => Unit.Default) + .Map(v => v.ToValidation("Show does not exist")); } } diff --git a/ErsatzTV.Application/Search/Queries/QuerySearchIndexAllItemsHandler.cs b/ErsatzTV.Application/Search/Queries/QuerySearchIndexAllItemsHandler.cs index d6c2f138a..ea1e52d9a 100644 --- a/ErsatzTV.Application/Search/Queries/QuerySearchIndexAllItemsHandler.cs +++ b/ErsatzTV.Application/Search/Queries/QuerySearchIndexAllItemsHandler.cs @@ -17,18 +17,16 @@ namespace ErsatzTV.Application.Search.Queries public async Task Handle( QuerySearchIndexAllItems request, - CancellationToken cancellationToken) - { - List movieIds = await _searchIndex.Search($"type:movie AND ({request.Query})", 0, 0) - .Map(result => result.Items.Map(i => i.Id).ToList()); - List showIds = await _searchIndex.Search($"type:show AND ({request.Query})", 0, 0) - .Map(result => result.Items.Map(i => i.Id).ToList()); - List artistIds = await _searchIndex.Search($"type:artist AND ({request.Query})", 0, 0) - .Map(result => result.Items.Map(i => i.Id).ToList()); - List musicVideoIds = await _searchIndex.Search($"type:music_video AND ({request.Query})", 0, 0) - .Map(result => result.Items.Map(i => i.Id).ToList()); + CancellationToken cancellationToken) => + new( + await GetIds("movie", request.Query), + await GetIds("show", request.Query), + await GetIds("episode", request.Query), + await GetIds("artist", request.Query), + await GetIds("music_video", request.Query)); - return new SearchResultAllItemsViewModel(movieIds, showIds, artistIds, musicVideoIds); - } + private Task> GetIds(string type, string query) => + _searchIndex.Search($"type:{type} AND ({query})", 0, 0) + .Map(result => result.Items.Map(i => i.Id).ToList()); } } diff --git a/ErsatzTV.Application/Search/Queries/QuerySearchIndexEpisodes.cs b/ErsatzTV.Application/Search/Queries/QuerySearchIndexEpisodes.cs new file mode 100644 index 000000000..5d0622865 --- /dev/null +++ b/ErsatzTV.Application/Search/Queries/QuerySearchIndexEpisodes.cs @@ -0,0 +1,8 @@ +using ErsatzTV.Application.MediaCards; +using MediatR; + +namespace ErsatzTV.Application.Search.Queries +{ + public record QuerySearchIndexEpisodes + (string Query, int PageNumber, int PageSize) : IRequest; +} diff --git a/ErsatzTV.Application/Search/Queries/QuerySearchIndexEpisodesHandler.cs b/ErsatzTV.Application/Search/Queries/QuerySearchIndexEpisodesHandler.cs new file mode 100644 index 000000000..dea6db740 --- /dev/null +++ b/ErsatzTV.Application/Search/Queries/QuerySearchIndexEpisodesHandler.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using ErsatzTV.Application.MediaCards; +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Interfaces.Repositories; +using ErsatzTV.Core.Interfaces.Search; +using ErsatzTV.Core.Search; +using LanguageExt; +using MediatR; +using static ErsatzTV.Application.MediaCards.Mapper; + +namespace ErsatzTV.Application.Search.Queries +{ + public class + QuerySearchIndexEpisodesHandler : IRequestHandler + { + private readonly IMediaSourceRepository _mediaSourceRepository; + private readonly ISearchIndex _searchIndex; + private readonly ITelevisionRepository _televisionRepository; + + public QuerySearchIndexEpisodesHandler( + ISearchIndex searchIndex, + ITelevisionRepository televisionRepository, + IMediaSourceRepository mediaSourceRepository) + { + _searchIndex = searchIndex; + _televisionRepository = televisionRepository; + _mediaSourceRepository = mediaSourceRepository; + } + + public async Task Handle( + QuerySearchIndexEpisodes request, + CancellationToken cancellationToken) + { + SearchResult searchResult = await _searchIndex.Search( + request.Query, + (request.PageNumber - 1) * request.PageSize, + request.PageSize); + + Option maybeJellyfin = await _mediaSourceRepository.GetAllJellyfin() + .Map(list => list.HeadOrNone()); + + Option maybeEmby = await _mediaSourceRepository.GetAllEmby() + .Map(list => list.HeadOrNone()); + + List items = await _televisionRepository + .GetEpisodesForCards(searchResult.Items.Map(i => i.Id).ToList()) + .Map(list => list.Map(s => ProjectToViewModel(s, maybeJellyfin, maybeEmby, true)).ToList()); + + return new TelevisionEpisodeCardResultsViewModel(searchResult.TotalCount, items, searchResult.PageMap); + } + } +} diff --git a/ErsatzTV.Application/Search/SearchResultAllItemsViewModel.cs b/ErsatzTV.Application/Search/SearchResultAllItemsViewModel.cs index 07ddbef95..7b0d1866d 100644 --- a/ErsatzTV.Application/Search/SearchResultAllItemsViewModel.cs +++ b/ErsatzTV.Application/Search/SearchResultAllItemsViewModel.cs @@ -5,6 +5,7 @@ namespace ErsatzTV.Application.Search public record SearchResultAllItemsViewModel( List MovieIds, List ShowIds, + List EpisodeIds, List ArtistIds, List MusicVideoIds); } diff --git a/ErsatzTV.Core.Tests/Fakes/FakeTelevisionRepository.cs b/ErsatzTV.Core.Tests/Fakes/FakeTelevisionRepository.cs index 408251467..ac21aafbc 100644 --- a/ErsatzTV.Core.Tests/Fakes/FakeTelevisionRepository.cs +++ b/ErsatzTV.Core.Tests/Fakes/FakeTelevisionRepository.cs @@ -11,17 +11,14 @@ namespace ErsatzTV.Core.Tests.Fakes public class FakeTelevisionRepository : ITelevisionRepository { public Task AllShowsExist(List showIds) => throw new NotSupportedException(); + public Task AllEpisodesExist(List episodeIds) => throw new NotSupportedException(); public Task> GetAllShows() => throw new NotSupportedException(); public Task> GetShow(int showId) => throw new NotSupportedException(); - public Task GetShowCount() => throw new NotSupportedException(); - - public Task> GetPagedShows(int pageNumber, int pageSize) => - throw new NotSupportedException(); - public Task> GetShowsForCards(List ids) => throw new NotSupportedException(); + public Task> GetEpisodesForCards(List ids) => throw new NotSupportedException(); public Task> GetShowItems(int showId) => throw new NotSupportedException(); @@ -89,10 +86,18 @@ namespace ErsatzTV.Core.Tests.Fakes public Task RemoveMissingPlexSeasons(string showKey, List seasonKeys) => throw new NotSupportedException(); - public Task RemoveMissingPlexEpisodes(string seasonKey, List episodeKeys) => + public Task> RemoveMissingPlexEpisodes(string seasonKey, List episodeKeys) => throw new NotSupportedException(); public Task SetEpisodeNumber(Episode episode, int episodeNumber) => throw new NotSupportedException(); + public Task AddDirector(EpisodeMetadata metadata, Director director) => throw new NotSupportedException(); + + public Task AddWriter(EpisodeMetadata metadata, Writer writer) => throw new NotSupportedException(); + + public Task GetShowCount() => throw new NotSupportedException(); + + public Task> GetPagedShows(int pageNumber, int pageSize) => + throw new NotSupportedException(); public Task Update(Show show) => throw new NotSupportedException(); diff --git a/ErsatzTV.Core.Tests/Scheduling/MultiPartEpisodeGrouperTests.cs b/ErsatzTV.Core.Tests/Scheduling/MultiPartEpisodeGrouperTests.cs index 4ba885bc5..fe93d34d6 100644 --- a/ErsatzTV.Core.Tests/Scheduling/MultiPartEpisodeGrouperTests.cs +++ b/ErsatzTV.Core.Tests/Scheduling/MultiPartEpisodeGrouperTests.cs @@ -9,14 +9,17 @@ namespace ErsatzTV.Core.Tests.Scheduling public class MultiPartEpisodeGrouperTests { [Test] - public void NotGrouped_Grouped_NotGrouped() + [TestCase("Episode 1", "Episode 2 (1)", "Episode 3 (2)", "Episode 4")] + [TestCase("Episode 1 - More", "Episode 2 (1) - Title", "Episode 3 (2) - After", "Episode 4 - Dash")] + [TestCase("Episode 1", "Episode 2 Part 1", "Episode 3 Part 2", "Episode 4")] + public void NotGrouped_Grouped_NotGrouped(string one, string two, string three, string four) { var mediaItems = new List { - NamedEpisode("Episode 1"), - NamedEpisode("Episode 2 (1)"), - NamedEpisode("Episode 3 (2)"), - NamedEpisode("Episode 4") + NamedEpisode(one), + NamedEpisode(two), + NamedEpisode(three), + NamedEpisode(four) }; List result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems); @@ -29,13 +32,16 @@ namespace ErsatzTV.Core.Tests.Scheduling } [Test] - public void Grouped_NotGrouped() + [TestCase("Episode 1 (1)", "Episode 2 (2)", "Episode 3")] + [TestCase("Episode 1 (1) - More", "Episode 2 (2) - Title", "Episode 3 - After")] + [TestCase("Episode 1 Part 1", "Episode 2 Part 2", "Episode 3")] + public void Grouped_NotGrouped(string one, string two, string three) { var mediaItems = new List { - NamedEpisode("Episode 1 (1)"), - NamedEpisode("Episode 2 (2)"), - NamedEpisode("Episode 3") + NamedEpisode(one), + NamedEpisode(two), + NamedEpisode(three) }; List result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems); @@ -47,15 +53,23 @@ namespace ErsatzTV.Core.Tests.Scheduling } [Test] - public void Grouped_NotGrouped_Grouped() + [TestCase("Episode 1 (1)", "Episode 2 (2)", "Episode 3", "Episode 4 (1)", "Episode 5 (2)")] + [TestCase( + "Episode 1 (1) - More", + "Episode 2 (2) - Title", + "Episode 3 - After", + "Episode 4 (1) - Dash", + "Episode 5 (2) - Again")] + [TestCase("Episode 1 Part 1", "Episode 2 Part 2", "Episode 3", "Episode 4 Part 1", "Episode 5 Part 2")] + public void Grouped_NotGrouped_Grouped(string one, string two, string three, string four, string five) { var mediaItems = new List { - NamedEpisode("Episode 1 (1)"), - NamedEpisode("Episode 2 (2)"), - NamedEpisode("Episode 3"), - NamedEpisode("Episode 4 (1)"), - NamedEpisode("Episode 5 (2)") + NamedEpisode(one), + NamedEpisode(two), + NamedEpisode(three), + NamedEpisode(four), + NamedEpisode(five) }; List result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems); @@ -69,14 +83,17 @@ namespace ErsatzTV.Core.Tests.Scheduling } [Test] - public void Grouped_Grouped() + [TestCase("Episode 1 (1)", "Episode 2 (2)", "Episode 3 (1)", "Episode 4 (2)")] + [TestCase("Episode 1 (1) - More", "Episode 2 (2) - Title", "Episode 3 (1) - After", "Episode 4 (2) - Dash")] + [TestCase("Episode 1 Part 1", "Episode 2 Part 2", "Episode 3 Part 1", "Episode 4 Part 2")] + public void Grouped_Grouped(string one, string two, string three, string four) { var mediaItems = new List { - NamedEpisode("Episode 1 (1)"), - NamedEpisode("Episode 2 (2)"), - NamedEpisode("Episode 3 (1)"), - NamedEpisode("Episode 4 (2)") + NamedEpisode(one), + NamedEpisode(two), + NamedEpisode(three), + NamedEpisode(four) }; List result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems); @@ -88,6 +105,75 @@ namespace ErsatzTV.Core.Tests.Scheduling result[1].Additional[0].Should().Be(mediaItems[3]); } + [Test] + [TestCase("Episode 1", "Episode 2 (2)", "Episode 3 (1)", "Episode 4 (2)")] + [TestCase("Episode 1 - More", "Episode 2 (2) - Title", "Episode 3 (1) - After", "Episode 4 (2) - Dash")] + [TestCase("Episode 1", "Episode 2 Part 2", "Episode 3 Part 1", "Episode 4 Part 2")] + public void Part2_Without_Part1(string one, string two, string three, string four) + { + var mediaItems = new List + { + NamedEpisode(one), + NamedEpisode(two), + NamedEpisode(three), + NamedEpisode(four) + }; + + List result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems); + + result.Count.Should().Be(3); + result[0].First.Should().Be(mediaItems[0]); + result[1].First.Should().Be(mediaItems[1]); + result[2].First.Should().Be(mediaItems[2]); + result[2].Additional[0].Should().Be(mediaItems[3]); + } + + [Test] + [TestCase("Episode 1 (1)", "Episode 3 (3)", "Episode 4", "Episode 5")] + [TestCase("Episode 1 (1) - More", "Episode 3 (3) - Title", "Episode 4 - After", "Episode 5 - Dash")] + [TestCase("Episode 1 Part 1", "Episode 3 Part 3", "Episode 4", "Episode 5")] + public void Skip_Part(string one, string two, string three, string four) + { + var mediaItems = new List + { + NamedEpisode(one), + NamedEpisode(two), + NamedEpisode(three), + NamedEpisode(four) + }; + + List result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems); + + result.Count.Should().Be(4); + result[0].First.Should().Be(mediaItems[0]); + result[1].First.Should().Be(mediaItems[1]); + result[2].First.Should().Be(mediaItems[2]); + result[3].First.Should().Be(mediaItems[3]); + } + + [Test] + [TestCase("Episode 1 (1)", "Episode 3 (1)", "Episode 4 (2)", "Episode 5")] + [TestCase("Episode 1 (1) - More", "Episode 3 (1) - Title", "Episode 4 (2) - After", "Episode 5 - Dash")] + [TestCase("Episode 1 Part 1", "Episode 3 Part 1", "Episode 4 Part 2", "Episode 5")] + public void Repeat_Part(string one, string two, string three, string four) + { + var mediaItems = new List + { + NamedEpisode(one), + NamedEpisode(two), + NamedEpisode(three), + NamedEpisode(four) + }; + + List result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems); + + result.Count.Should().Be(3); + result[0].First.Should().Be(mediaItems[0]); + result[1].First.Should().Be(mediaItems[1]); + result[1].Additional[0].Should().Be(mediaItems[2]); + result[2].First.Should().Be(mediaItems[3]); + } + private static Episode NamedEpisode(string title) => new() { diff --git a/ErsatzTV.Core/Domain/Metadata/EpisodeMetadata.cs b/ErsatzTV.Core/Domain/Metadata/EpisodeMetadata.cs index 61fe29c3f..fb710962f 100644 --- a/ErsatzTV.Core/Domain/Metadata/EpisodeMetadata.cs +++ b/ErsatzTV.Core/Domain/Metadata/EpisodeMetadata.cs @@ -1,4 +1,6 @@ -namespace ErsatzTV.Core.Domain +using System.Collections.Generic; + +namespace ErsatzTV.Core.Domain { public class EpisodeMetadata : Metadata { @@ -7,5 +9,7 @@ public string Tagline { get; set; } public int EpisodeId { get; set; } public Episode Episode { get; set; } + public List Directors { get; set; } + public List Writers { get; set; } } } diff --git a/ErsatzTV.Core/Domain/Metadata/Metadata.cs b/ErsatzTV.Core/Domain/Metadata/Metadata.cs index 89536b30b..9cda68ed1 100644 --- a/ErsatzTV.Core/Domain/Metadata/Metadata.cs +++ b/ErsatzTV.Core/Domain/Metadata/Metadata.cs @@ -19,5 +19,6 @@ namespace ErsatzTV.Core.Domain public List Tags { get; set; } public List Studios { get; set; } public List Actors { get; set; } + public List Guids { get; set; } } } diff --git a/ErsatzTV.Core/Domain/Metadata/MetadataGuid.cs b/ErsatzTV.Core/Domain/Metadata/MetadataGuid.cs new file mode 100644 index 000000000..c03389a08 --- /dev/null +++ b/ErsatzTV.Core/Domain/Metadata/MetadataGuid.cs @@ -0,0 +1,8 @@ +namespace ErsatzTV.Core.Domain +{ + public class MetadataGuid + { + public int Id { get; set; } + public string Guid { get; set; } + } +} diff --git a/ErsatzTV.Core/Emby/EmbyTelevisionLibraryScanner.cs b/ErsatzTV.Core/Emby/EmbyTelevisionLibraryScanner.cs index 4d7bcd2ac..9dca70366 100644 --- a/ErsatzTV.Core/Emby/EmbyTelevisionLibraryScanner.cs +++ b/ErsatzTV.Core/Emby/EmbyTelevisionLibraryScanner.cs @@ -120,8 +120,6 @@ namespace ErsatzTV.Core.Emby decimal percentCompletion = (decimal) shows.IndexOf(incoming) / shows.Count; await _mediator.Publish(new LibraryScanProgress(library.Id, percentCompletion)); - var changed = false; - Option maybeExisting = existingShows.Find(ie => ie.ItemId == incoming.ItemId); await maybeExisting.Match( async existing => @@ -135,7 +133,6 @@ namespace ErsatzTV.Core.Emby "UPDATE: Etag has changed for show {Show}", incoming.ShowMetadata.Head().Title); - changed = true; incoming.LibraryPathId = library.Paths.Head().Id; Option updated = await _televisionRepository.Update(incoming); @@ -148,7 +145,6 @@ namespace ErsatzTV.Core.Emby }, async () => { - changed = true; incoming.LibraryPathId = library.Paths.Head().Id; // _logger.LogDebug("INSERT: Item id is new for show {Show}", incoming.ShowMetadata.Head().Title); @@ -159,50 +155,45 @@ namespace ErsatzTV.Core.Emby } }); - if (changed) - { - List existingSeasons = - await _televisionRepository.GetExistingSeasons(library, incoming.ItemId); + List existingSeasons = + await _televisionRepository.GetExistingSeasons(library, incoming.ItemId); - Either> maybeSeasons = - await _embyApiClient.GetSeasonLibraryItems( + Either> maybeSeasons = + await _embyApiClient.GetSeasonLibraryItems( + address, + apiKey, + library.MediaSourceId, + incoming.ItemId); + + await maybeSeasons.Match( + async seasons => + { + await ProcessSeasons( address, apiKey, - library.MediaSourceId, - incoming.ItemId); + library, + ffprobePath, + pathReplacements, + incoming, + existingSeasons, + seasons); - await maybeSeasons.Match( - async seasons => - { - await ProcessSeasons( - address, - apiKey, - library, - ffprobePath, - pathReplacements, - incoming, - existingSeasons, - seasons); + var incomingSeasonIds = seasons.Map(s => s.ItemId).ToList(); + var seasonIds = existingSeasons + .Filter(i => !incomingSeasonIds.Contains(i.ItemId)) + .Map(m => m.ItemId) + .ToList(); + await _televisionRepository.RemoveMissingSeasons(library, seasonIds); + }, + error => + { + _logger.LogWarning( + "Error synchronizing emby library {Path}: {Error}", + library.Name, + error.Value); - await _searchIndex.UpdateItems(_searchRepository, new List { incoming }); - - var incomingSeasonIds = seasons.Map(s => s.ItemId).ToList(); - var seasonIds = existingSeasons - .Filter(i => !incomingSeasonIds.Contains(i.ItemId)) - .Map(m => m.ItemId) - .ToList(); - await _televisionRepository.RemoveMissingSeasons(library, seasonIds); - }, - error => - { - _logger.LogWarning( - "Error synchronizing emby library {Path}: {Error}", - library.Name, - error.Value); - - return Task.CompletedTask; - }); - } + return Task.CompletedTask; + }); } } @@ -218,8 +209,6 @@ namespace ErsatzTV.Core.Emby { foreach (EmbySeason incoming in seasons) { - var changed = false; - Option maybeExisting = existingSeasons.Find(ie => ie.ItemId == incoming.ItemId); await maybeExisting.Match( async existing => @@ -234,7 +223,6 @@ namespace ErsatzTV.Core.Emby show.ShowMetadata.Head().Title, incoming.SeasonMetadata.Head().Title); - changed = true; incoming.ShowId = show.Id; incoming.LibraryPathId = library.Paths.Head().Id; @@ -242,7 +230,6 @@ namespace ErsatzTV.Core.Emby }, async () => { - changed = true; incoming.ShowId = show.Id; incoming.LibraryPathId = library.Paths.Head().Id; @@ -254,68 +241,68 @@ namespace ErsatzTV.Core.Emby await _televisionRepository.AddSeason(incoming); }); - if (changed) - { - List existingEpisodes = - await _televisionRepository.GetExistingEpisodes(library, incoming.ItemId); + List existingEpisodes = + await _televisionRepository.GetExistingEpisodes(library, incoming.ItemId); - Either> maybeEpisodes = - await _embyApiClient.GetEpisodeLibraryItems( - address, - apiKey, - library.MediaSourceId, - incoming.ItemId); + Either> maybeEpisodes = + await _embyApiClient.GetEpisodeLibraryItems( + address, + apiKey, + library.MediaSourceId, + incoming.ItemId); - await maybeEpisodes.Match( - async episodes => + await maybeEpisodes.Match( + async episodes => + { + var validEpisodes = new List(); + foreach (EmbyEpisode episode in episodes) { - var validEpisodes = new List(); - foreach (EmbyEpisode episode in episodes) - { - string localPath = _pathReplacementService.GetReplacementEmbyPath( - pathReplacements, - episode.MediaVersions.Head().MediaFiles.Head().Path, - false); - - if (!_localFileSystem.FileExists(localPath)) - { - _logger.LogWarning( - "Skipping emby episode that does not exist at {Path}", - localPath); - } - else - { - validEpisodes.Add(episode); - } - } - - await ProcessEpisodes( - show.ShowMetadata.Head().Title, - incoming.SeasonMetadata.Head().Title, - library, - ffprobePath, + string localPath = _pathReplacementService.GetReplacementEmbyPath( pathReplacements, - incoming, - existingEpisodes, - validEpisodes); + episode.MediaVersions.Head().MediaFiles.Head().Path, + false); - var incomingEpisodeIds = episodes.Map(s => s.ItemId).ToList(); - var episodeIds = existingEpisodes - .Filter(i => !incomingEpisodeIds.Contains(i.ItemId)) - .Map(m => m.ItemId) - .ToList(); + if (!_localFileSystem.FileExists(localPath)) + { + _logger.LogWarning( + "Skipping emby episode that does not exist at {Path}", + localPath); + } + else + { + validEpisodes.Add(episode); + } + } + + await ProcessEpisodes( + show.ShowMetadata.Head().Title, + incoming.SeasonMetadata.Head().Title, + library, + ffprobePath, + pathReplacements, + incoming, + existingEpisodes, + validEpisodes); + + var incomingEpisodeIds = episodes.Map(s => s.ItemId).ToList(); + var episodeIds = existingEpisodes + .Filter(i => !incomingEpisodeIds.Contains(i.ItemId)) + .Map(m => m.ItemId) + .ToList(); + List missingEpisodeIds = await _televisionRepository.RemoveMissingEpisodes(library, episodeIds); - }, - error => - { - _logger.LogWarning( - "Error synchronizing emby library {Path}: {Error}", - library.Name, - error.Value); + await _searchIndex.RemoveItems(missingEpisodeIds); + _searchIndex.Commit(); + }, + error => + { + _logger.LogWarning( + "Error synchronizing emby library {Path}: {Error}", + library.Name, + error.Value); - return Task.CompletedTask; - }); - } + return Task.CompletedTask; + }); } } @@ -354,7 +341,13 @@ namespace ErsatzTV.Core.Emby incoming.SeasonId = season.Id; incoming.LibraryPathId = library.Paths.Head().Id; - await _televisionRepository.Update(incoming); + Option updated = await _televisionRepository.Update(incoming); + if (updated.IsSome) + { + await _searchIndex.UpdateItems( + _searchRepository, + new List { updated.ValueUnsafe() }); + } } catch (Exception ex) { @@ -379,7 +372,10 @@ namespace ErsatzTV.Core.Emby seasonName, incoming.EpisodeNumber); - await _televisionRepository.AddEpisode(incoming); + if (await _televisionRepository.AddEpisode(incoming)) + { + await _searchIndex.AddItems(_searchRepository, new List { incoming }); + } } catch (Exception ex) { diff --git a/ErsatzTV.Core/Interfaces/Plex/IPlexServerApiClient.cs b/ErsatzTV.Core/Interfaces/Plex/IPlexServerApiClient.cs index 881a91c2b..4a6763eb2 100644 --- a/ErsatzTV.Core/Interfaces/Plex/IPlexServerApiClient.cs +++ b/ErsatzTV.Core/Interfaces/Plex/IPlexServerApiClient.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Threading.Tasks; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Plex; @@ -46,7 +47,14 @@ namespace ErsatzTV.Core.Interfaces.Plex PlexConnection connection, PlexServerAuthToken token); - Task> GetStatistics( + Task>> GetMovieMetadataAndStatistics( + PlexLibrary library, + string key, + PlexConnection connection, + PlexServerAuthToken token); + + Task>> GetEpisodeMetadataAndStatistics( + PlexLibrary library, string key, PlexConnection connection, PlexServerAuthToken token); diff --git a/ErsatzTV.Core/Interfaces/Repositories/IEmbyTelevisionRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/IEmbyTelevisionRepository.cs index 8a2a8a645..8f427f5d8 100644 --- a/ErsatzTV.Core/Interfaces/Repositories/IEmbyTelevisionRepository.cs +++ b/ErsatzTV.Core/Interfaces/Repositories/IEmbyTelevisionRepository.cs @@ -16,10 +16,10 @@ namespace ErsatzTV.Core.Interfaces.Repositories Task AddSeason(EmbySeason season); Task Update(EmbySeason season); Task AddEpisode(EmbyEpisode episode); - Task Update(EmbyEpisode episode); + Task> Update(EmbyEpisode episode); Task> RemoveMissingShows(EmbyLibrary library, List showIds); Task RemoveMissingSeasons(EmbyLibrary library, List seasonIds); - Task RemoveMissingEpisodes(EmbyLibrary library, List episodeIds); + Task> RemoveMissingEpisodes(EmbyLibrary library, List episodeIds); Task DeleteEmptySeasons(EmbyLibrary library); Task> DeleteEmptyShows(EmbyLibrary library); } diff --git a/ErsatzTV.Core/Interfaces/Repositories/IJellyfinTelevisionRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/IJellyfinTelevisionRepository.cs index 6f8651b0f..7e7accdf8 100644 --- a/ErsatzTV.Core/Interfaces/Repositories/IJellyfinTelevisionRepository.cs +++ b/ErsatzTV.Core/Interfaces/Repositories/IJellyfinTelevisionRepository.cs @@ -16,10 +16,10 @@ namespace ErsatzTV.Core.Interfaces.Repositories Task AddSeason(JellyfinSeason season); Task Update(JellyfinSeason season); Task AddEpisode(JellyfinEpisode episode); - Task Update(JellyfinEpisode episode); + Task> Update(JellyfinEpisode episode); Task> RemoveMissingShows(JellyfinLibrary library, List showIds); Task RemoveMissingSeasons(JellyfinLibrary library, List seasonIds); - Task RemoveMissingEpisodes(JellyfinLibrary library, List episodeIds); + Task> RemoveMissingEpisodes(JellyfinLibrary library, List episodeIds); Task DeleteEmptySeasons(JellyfinLibrary library); Task> DeleteEmptyShows(JellyfinLibrary library); } diff --git a/ErsatzTV.Core/Interfaces/Repositories/IMetadataRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/IMetadataRepository.cs index 060385574..2529d854e 100644 --- a/ErsatzTV.Core/Interfaces/Repositories/IMetadataRepository.cs +++ b/ErsatzTV.Core/Interfaces/Repositories/IMetadataRepository.cs @@ -28,5 +28,9 @@ namespace ErsatzTV.Core.Interfaces.Repositories Task SetContentRating(ShowMetadata metadata, string contentRating); Task MarkAsExternal(MovieMetadata metadata); Task SetContentRating(MovieMetadata metadata, string contentRating); + Task RemoveGuid(MetadataGuid guid); + Task AddGuid(Domain.Metadata metadata, MetadataGuid guid); + Task RemoveDirector(Director director); + Task RemoveWriter(Writer writer); } } diff --git a/ErsatzTV.Core/Interfaces/Repositories/IMovieRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/IMovieRepository.cs index 5d47dd35f..2597de5e8 100644 --- a/ErsatzTV.Core/Interfaces/Repositories/IMovieRepository.cs +++ b/ErsatzTV.Core/Interfaces/Repositories/IMovieRepository.cs @@ -33,9 +33,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories Task> RemoveMissingEmbyMovies(EmbyLibrary library, List movieIds); Task AddEmby(EmbyMovie movie); Task> UpdateEmby(EmbyMovie movie); - Task RemoveDirector(Director director); Task AddDirector(MovieMetadata metadata, Director director); - Task RemoveWriter(Writer writer); Task AddWriter(MovieMetadata metadata, Writer writer); } } diff --git a/ErsatzTV.Core/Interfaces/Repositories/ITelevisionRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/ITelevisionRepository.cs index 936373753..5240fbb61 100644 --- a/ErsatzTV.Core/Interfaces/Repositories/ITelevisionRepository.cs +++ b/ErsatzTV.Core/Interfaces/Repositories/ITelevisionRepository.cs @@ -9,11 +9,11 @@ namespace ErsatzTV.Core.Interfaces.Repositories public interface ITelevisionRepository { Task AllShowsExist(List showIds); + Task AllEpisodesExist(List episodeIds); Task> GetAllShows(); Task> GetShow(int showId); - Task GetShowCount(); - Task> GetPagedShows(int pageNumber, int pageSize); Task> GetShowsForCards(List ids); + Task> GetEpisodesForCards(List ids); Task> GetShowItems(int showId); Task> GetAllSeasons(); Task> GetSeason(int seasonId); @@ -46,7 +46,9 @@ namespace ErsatzTV.Core.Interfaces.Repositories Task AddActor(EpisodeMetadata metadata, Actor actor); Task> RemoveMissingPlexShows(PlexLibrary library, List showKeys); Task RemoveMissingPlexSeasons(string showKey, List seasonKeys); - Task RemoveMissingPlexEpisodes(string seasonKey, List episodeKeys); + Task> RemoveMissingPlexEpisodes(string seasonKey, List episodeKeys); Task SetEpisodeNumber(Episode episode, int episodeNumber); + Task AddDirector(EpisodeMetadata metadata, Director director); + Task AddWriter(EpisodeMetadata metadata, Writer writer); } } diff --git a/ErsatzTV.Core/Jellyfin/JellyfinTelevisionLibraryScanner.cs b/ErsatzTV.Core/Jellyfin/JellyfinTelevisionLibraryScanner.cs index 27ae1adfb..b9ad2ef12 100644 --- a/ErsatzTV.Core/Jellyfin/JellyfinTelevisionLibraryScanner.cs +++ b/ErsatzTV.Core/Jellyfin/JellyfinTelevisionLibraryScanner.cs @@ -120,8 +120,6 @@ namespace ErsatzTV.Core.Jellyfin decimal percentCompletion = (decimal) shows.IndexOf(incoming) / shows.Count; await _mediator.Publish(new LibraryScanProgress(library.Id, percentCompletion)); - var changed = false; - Option maybeExisting = existingShows.Find(ie => ie.ItemId == incoming.ItemId); await maybeExisting.Match( async existing => @@ -135,7 +133,6 @@ namespace ErsatzTV.Core.Jellyfin "UPDATE: Etag has changed for show {Show}", incoming.ShowMetadata.Head().Title); - changed = true; incoming.LibraryPathId = library.Paths.Head().Id; Option updated = await _televisionRepository.Update(incoming); @@ -148,7 +145,6 @@ namespace ErsatzTV.Core.Jellyfin }, async () => { - changed = true; incoming.LibraryPathId = library.Paths.Head().Id; // _logger.LogDebug("INSERT: Item id is new for show {Show}", incoming.ShowMetadata.Head().Title); @@ -159,50 +155,45 @@ namespace ErsatzTV.Core.Jellyfin } }); - if (changed) - { - List existingSeasons = - await _televisionRepository.GetExistingSeasons(library, incoming.ItemId); + List existingSeasons = + await _televisionRepository.GetExistingSeasons(library, incoming.ItemId); - Either> maybeSeasons = - await _jellyfinApiClient.GetSeasonLibraryItems( + Either> maybeSeasons = + await _jellyfinApiClient.GetSeasonLibraryItems( + address, + apiKey, + library.MediaSourceId, + incoming.ItemId); + + await maybeSeasons.Match( + async seasons => + { + await ProcessSeasons( address, apiKey, - library.MediaSourceId, - incoming.ItemId); + library, + ffprobePath, + pathReplacements, + incoming, + existingSeasons, + seasons); - await maybeSeasons.Match( - async seasons => - { - await ProcessSeasons( - address, - apiKey, - library, - ffprobePath, - pathReplacements, - incoming, - existingSeasons, - seasons); + var incomingSeasonIds = seasons.Map(s => s.ItemId).ToList(); + var seasonIds = existingSeasons + .Filter(i => !incomingSeasonIds.Contains(i.ItemId)) + .Map(m => m.ItemId) + .ToList(); + await _televisionRepository.RemoveMissingSeasons(library, seasonIds); + }, + error => + { + _logger.LogWarning( + "Error synchronizing jellyfin library {Path}: {Error}", + library.Name, + error.Value); - await _searchIndex.UpdateItems(_searchRepository, new List { incoming }); - - var incomingSeasonIds = seasons.Map(s => s.ItemId).ToList(); - var seasonIds = existingSeasons - .Filter(i => !incomingSeasonIds.Contains(i.ItemId)) - .Map(m => m.ItemId) - .ToList(); - await _televisionRepository.RemoveMissingSeasons(library, seasonIds); - }, - error => - { - _logger.LogWarning( - "Error synchronizing jellyfin library {Path}: {Error}", - library.Name, - error.Value); - - return Task.CompletedTask; - }); - } + return Task.CompletedTask; + }); } } @@ -218,8 +209,6 @@ namespace ErsatzTV.Core.Jellyfin { foreach (JellyfinSeason incoming in seasons) { - var changed = false; - Option maybeExisting = existingSeasons.Find(ie => ie.ItemId == incoming.ItemId); await maybeExisting.Match( async existing => @@ -234,7 +223,6 @@ namespace ErsatzTV.Core.Jellyfin show.ShowMetadata.Head().Title, incoming.SeasonMetadata.Head().Title); - changed = true; incoming.ShowId = show.Id; incoming.LibraryPathId = library.Paths.Head().Id; @@ -242,7 +230,6 @@ namespace ErsatzTV.Core.Jellyfin }, async () => { - changed = true; incoming.ShowId = show.Id; incoming.LibraryPathId = library.Paths.Head().Id; @@ -254,68 +241,69 @@ namespace ErsatzTV.Core.Jellyfin await _televisionRepository.AddSeason(incoming); }); - if (changed) - { - List existingEpisodes = - await _televisionRepository.GetExistingEpisodes(library, incoming.ItemId); + List existingEpisodes = + await _televisionRepository.GetExistingEpisodes(library, incoming.ItemId); - Either> maybeEpisodes = - await _jellyfinApiClient.GetEpisodeLibraryItems( - address, - apiKey, - library.MediaSourceId, - incoming.ItemId); + Either> maybeEpisodes = + await _jellyfinApiClient.GetEpisodeLibraryItems( + address, + apiKey, + library.MediaSourceId, + incoming.ItemId); - await maybeEpisodes.Match( - async episodes => + await maybeEpisodes.Match( + async episodes => + { + var validEpisodes = new List(); + foreach (JellyfinEpisode episode in episodes) { - var validEpisodes = new List(); - foreach (JellyfinEpisode episode in episodes) - { - string localPath = _pathReplacementService.GetReplacementJellyfinPath( - pathReplacements, - episode.MediaVersions.Head().MediaFiles.Head().Path, - false); - - if (!_localFileSystem.FileExists(localPath)) - { - _logger.LogWarning( - "Skipping jellyfin episode that does not exist at {Path}", - localPath); - } - else - { - validEpisodes.Add(episode); - } - } - - await ProcessEpisodes( - show.ShowMetadata.Head().Title, - incoming.SeasonMetadata.Head().Title, - library, - ffprobePath, + string localPath = _pathReplacementService.GetReplacementJellyfinPath( pathReplacements, - incoming, - existingEpisodes, - validEpisodes); + episode.MediaVersions.Head().MediaFiles.Head().Path, + false); - var incomingEpisodeIds = episodes.Map(s => s.ItemId).ToList(); - var episodeIds = existingEpisodes - .Filter(i => !incomingEpisodeIds.Contains(i.ItemId)) - .Map(m => m.ItemId) - .ToList(); + if (!_localFileSystem.FileExists(localPath)) + { + _logger.LogWarning( + "Skipping jellyfin episode that does not exist at {Path}", + localPath); + } + else + { + validEpisodes.Add(episode); + } + } + + await ProcessEpisodes( + show.ShowMetadata.Head().Title, + incoming.SeasonMetadata.Head().Title, + library, + ffprobePath, + pathReplacements, + incoming, + existingEpisodes, + validEpisodes); + + var incomingEpisodeIds = episodes.Map(s => s.ItemId).ToList(); + var episodeIds = existingEpisodes + .Filter(i => !incomingEpisodeIds.Contains(i.ItemId)) + .Map(m => m.ItemId) + .ToList(); + + List missingEpisodeIds = await _televisionRepository.RemoveMissingEpisodes(library, episodeIds); - }, - error => - { - _logger.LogWarning( - "Error synchronizing jellyfin library {Path}: {Error}", - library.Name, - error.Value); + await _searchIndex.RemoveItems(missingEpisodeIds); + _searchIndex.Commit(); + }, + error => + { + _logger.LogWarning( + "Error synchronizing jellyfin library {Path}: {Error}", + library.Name, + error.Value); - return Task.CompletedTask; - }); - } + return Task.CompletedTask; + }); } } @@ -354,7 +342,13 @@ namespace ErsatzTV.Core.Jellyfin incoming.SeasonId = season.Id; incoming.LibraryPathId = library.Paths.Head().Id; - await _televisionRepository.Update(incoming); + Option updated = await _televisionRepository.Update(incoming); + if (updated.IsSome) + { + await _searchIndex.UpdateItems( + _searchRepository, + new List { updated.ValueUnsafe() }); + } } catch (Exception ex) { @@ -379,7 +373,10 @@ namespace ErsatzTV.Core.Jellyfin seasonName, incoming.EpisodeNumber); - await _televisionRepository.AddEpisode(incoming); + if (await _televisionRepository.AddEpisode(incoming)) + { + await _searchIndex.AddItems(_searchRepository, new List { incoming }); + } } catch (Exception ex) { diff --git a/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs b/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs index ca47d074f..3537555bb 100644 --- a/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs +++ b/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs @@ -212,6 +212,66 @@ namespace ErsatzTV.Core.Metadata (_, _) => Task.FromResult(false), _televisionRepository.AddActor); + foreach (Director director in existing.Directors + .Filter(d => metadata.Directors.All(d2 => d2.Name != d.Name)).ToList()) + { + existing.Directors.Remove(director); + if (await _metadataRepository.RemoveDirector(director)) + { + updated = true; + } + } + + foreach (Director director in metadata.Directors + .Filter(d => existing.Directors.All(d2 => d2.Name != d.Name)).ToList()) + { + existing.Directors.Add(director); + if (await _televisionRepository.AddDirector(existing, director)) + { + updated = true; + } + } + + foreach (Writer writer in existing.Writers + .Filter(w => metadata.Writers.All(w2 => w2.Name != w.Name)).ToList()) + { + existing.Writers.Remove(writer); + if (await _metadataRepository.RemoveWriter(writer)) + { + updated = true; + } + } + + foreach (Writer writer in metadata.Writers + .Filter(w => existing.Writers.All(w2 => w2.Name != w.Name)).ToList()) + { + existing.Writers.Add(writer); + if (await _televisionRepository.AddWriter(existing, writer)) + { + updated = true; + } + } + + foreach (MetadataGuid guid in existing.Guids + .Filter(g => metadata.Guids.All(g2 => g2.Guid != g.Guid)).ToList()) + { + existing.Guids.Remove(guid); + if (await _metadataRepository.RemoveGuid(guid)) + { + updated = true; + } + } + + foreach (MetadataGuid guid in metadata.Guids + .Filter(g => existing.Guids.All(g2 => g2.Guid != g.Guid)).ToList()) + { + existing.Guids.Add(guid); + if (await _metadataRepository.AddGuid(existing, guid)) + { + updated = true; + } + } + return await _metadataRepository.Update(existing) || updated; }, async () => @@ -261,17 +321,17 @@ namespace ErsatzTV.Core.Metadata _movieRepository.AddActor); foreach (Director director in existing.Directors - .Filter(g => metadata.Directors.All(g2 => g2.Name != g.Name)).ToList()) + .Filter(d => metadata.Directors.All(d2 => d2.Name != d.Name)).ToList()) { existing.Directors.Remove(director); - if (await _movieRepository.RemoveDirector(director)) + if (await _metadataRepository.RemoveDirector(director)) { updated = true; } } foreach (Director director in metadata.Directors - .Filter(g => existing.Directors.All(g2 => g2.Name != g.Name)).ToList()) + .Filter(d => existing.Directors.All(d2 => d2.Name != d.Name)).ToList()) { existing.Directors.Add(director); if (await _movieRepository.AddDirector(existing, director)) @@ -281,17 +341,17 @@ namespace ErsatzTV.Core.Metadata } foreach (Writer writer in existing.Writers - .Filter(g => metadata.Writers.All(g2 => g2.Name != g.Name)).ToList()) + .Filter(w => metadata.Writers.All(w2 => w2.Name != w.Name)).ToList()) { existing.Writers.Remove(writer); - if (await _movieRepository.RemoveWriter(writer)) + if (await _metadataRepository.RemoveWriter(writer)) { updated = true; } } foreach (Writer writer in metadata.Writers - .Filter(g => existing.Writers.All(g2 => g2.Name != g.Name)).ToList()) + .Filter(w => existing.Writers.All(w2 => w2.Name != w.Name)).ToList()) { existing.Writers.Add(writer); if (await _movieRepository.AddWriter(existing, writer)) @@ -300,6 +360,26 @@ namespace ErsatzTV.Core.Metadata } } + foreach (MetadataGuid guid in existing.Guids + .Filter(g => metadata.Guids.All(g2 => g2.Guid != g.Guid)).ToList()) + { + existing.Guids.Remove(guid); + if (await _metadataRepository.RemoveGuid(guid)) + { + updated = true; + } + } + + foreach (MetadataGuid guid in metadata.Guids + .Filter(g => existing.Guids.All(g2 => g2.Guid != g.Guid)).ToList()) + { + existing.Guids.Add(guid); + if (await _metadataRepository.AddGuid(existing, guid)) + { + updated = true; + } + } + return await _metadataRepository.Update(existing) || updated; }, async () => @@ -345,6 +425,26 @@ namespace ErsatzTV.Core.Metadata _televisionRepository.AddStudio, _televisionRepository.AddActor); + foreach (MetadataGuid guid in existing.Guids + .Filter(g => metadata.Guids.All(g2 => g2.Guid != g.Guid)).ToList()) + { + existing.Guids.Remove(guid); + if (await _metadataRepository.RemoveGuid(guid)) + { + updated = true; + } + } + + foreach (MetadataGuid guid in metadata.Guids + .Filter(g => existing.Guids.All(g2 => g2.Guid != g.Guid)).ToList()) + { + existing.Guids.Add(guid); + if (await _metadataRepository.AddGuid(existing, guid)) + { + updated = true; + } + } + return await _metadataRepository.Update(existing) || updated; }, async () => @@ -522,7 +622,10 @@ namespace ErsatzTV.Core.Metadata Genres = nfo.Genres.Map(g => new Genre { Name = g }).ToList(), Tags = nfo.Tags.Map(t => new Tag { Name = t }).ToList(), Studios = nfo.Studios.Map(s => new Studio { Name = s }).ToList(), - Actors = Actors(nfo.Actors, dateAdded, dateUpdated) + Actors = Actors(nfo.Actors, dateAdded, dateUpdated), + Guids = nfo.UniqueIds + .Map(id => new MetadataGuid { Guid = $"{id.Type}://{id.Guid}" }) + .ToList() }; }, None); @@ -582,7 +685,12 @@ namespace ErsatzTV.Core.Metadata Title = nfo.Title, ReleaseDate = GetAired(0, nfo.Aired), Plot = nfo.Plot, - Actors = Actors(nfo.Actors, dateAdded, dateUpdated) + Actors = Actors(nfo.Actors, dateAdded, dateUpdated), + Guids = nfo.UniqueIds + .Map(id => new MetadataGuid { Guid = $"{id.Type}://{id.Guid}" }) + .ToList(), + Directors = nfo.Directors.Map(d => new Director { Name = d }).ToList(), + Writers = nfo.Writers.Map(w => new Writer { Name = w }).ToList() }; return Tuple(metadata, nfo.Episode); }, @@ -624,7 +732,10 @@ namespace ErsatzTV.Core.Metadata Studios = nfo.Studios.Map(s => new Studio { Name = s }).ToList(), Actors = Actors(nfo.Actors, dateAdded, dateUpdated), Directors = nfo.Directors.Map(d => new Director { Name = d }).ToList(), - Writers = nfo.Writers.Map(w => new Writer { Name = w }).ToList() + Writers = nfo.Writers.Map(w => new Writer { Name = w }).ToList(), + Guids = nfo.UniqueIds + .Map(id => new MetadataGuid { Guid = $"{id.Type}://{id.Guid}" }) + .ToList() }; }, None); diff --git a/ErsatzTV.Core/Metadata/Nfo/MovieNfo.cs b/ErsatzTV.Core/Metadata/Nfo/MovieNfo.cs index 9d2ff4f27..4bbcdf5b0 100644 --- a/ErsatzTV.Core/Metadata/Nfo/MovieNfo.cs +++ b/ErsatzTV.Core/Metadata/Nfo/MovieNfo.cs @@ -45,5 +45,8 @@ namespace ErsatzTV.Core.Metadata.Nfo [XmlElement("director")] public List Directors { get; set; } + + [XmlElement("uniqueid")] + public List UniqueIds { get; set; } } } diff --git a/ErsatzTV.Core/Metadata/Nfo/TvShowEpisodeNfo.cs b/ErsatzTV.Core/Metadata/Nfo/TvShowEpisodeNfo.cs index 871fd074e..0009346c2 100644 --- a/ErsatzTV.Core/Metadata/Nfo/TvShowEpisodeNfo.cs +++ b/ErsatzTV.Core/Metadata/Nfo/TvShowEpisodeNfo.cs @@ -29,5 +29,14 @@ namespace ErsatzTV.Core.Metadata.Nfo [XmlElement("actor")] public List Actors { get; set; } + + [XmlElement("credits")] + public List Writers { get; set; } + + [XmlElement("director")] + public List Directors { get; set; } + + [XmlElement("uniqueid")] + public List UniqueIds { get; set; } } } diff --git a/ErsatzTV.Core/Metadata/Nfo/TvShowNfo.cs b/ErsatzTV.Core/Metadata/Nfo/TvShowNfo.cs index 5577bcee0..8224232c4 100644 --- a/ErsatzTV.Core/Metadata/Nfo/TvShowNfo.cs +++ b/ErsatzTV.Core/Metadata/Nfo/TvShowNfo.cs @@ -38,5 +38,8 @@ namespace ErsatzTV.Core.Metadata.Nfo [XmlElement("actor")] public List Actors { get; set; } + + [XmlElement("uniqueid")] + public List UniqueIds { get; set; } } } diff --git a/ErsatzTV.Core/Metadata/Nfo/UniqueIdNfo.cs b/ErsatzTV.Core/Metadata/Nfo/UniqueIdNfo.cs new file mode 100644 index 000000000..04ec995da --- /dev/null +++ b/ErsatzTV.Core/Metadata/Nfo/UniqueIdNfo.cs @@ -0,0 +1,16 @@ +using System.Xml.Serialization; + +namespace ErsatzTV.Core.Metadata.Nfo +{ + public class UniqueIdNfo + { + [XmlAttribute("default")] + public bool Default { get; set; } + + [XmlAttribute("type")] + public string Type { get; set; } + + [XmlText] + public string Guid { get; set; } + } +} diff --git a/ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs b/ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs index 6b702f938..c07ecdd0d 100644 --- a/ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs +++ b/ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs @@ -209,10 +209,20 @@ namespace ErsatzTV.Core.Metadata .BindT(UpdateMetadata) .BindT(UpdateThumbnail); - maybeEpisode.IfLeft( - error => _logger.LogWarning("Error processing episode at {Path}: {Error}", file, error.Value)); + await maybeEpisode.Match( + async episode => + { + await _searchIndex.UpdateItems(_searchRepository, new List { episode }); + }, + error => + { + _logger.LogWarning("Error processing episode at {Path}: {Error}", file, error.Value); + return Task.CompletedTask; + }); } + // TODO: remove missing episodes? + return Unit.Default; } diff --git a/ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs b/ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs index 331859295..54f36b142 100644 --- a/ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs +++ b/ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using ErsatzTV.Core.Domain; @@ -63,7 +64,7 @@ namespace ErsatzTV.Core.Plex // TODO: figure out how to rebuild playlists Either> maybeMovie = await _movieRepository .GetOrAdd(library, incoming) - .BindT(existing => UpdateStatistics(existing, incoming, connection, token)) + .BindT(existing => UpdateStatistics(existing, incoming, library, connection, token)) .BindT(existing => UpdateMetadata(existing, incoming, library, connection, token)) .BindT(existing => UpdateArtwork(existing, incoming)); @@ -114,6 +115,7 @@ namespace ErsatzTV.Core.Plex private async Task>> UpdateStatistics( MediaItemScanResult result, PlexMovie incoming, + PlexLibrary library, PlexConnection connection, PlexServerAuthToken token) { @@ -123,12 +125,18 @@ namespace ErsatzTV.Core.Plex if (incomingVersion.DateUpdated > existingVersion.DateUpdated || !existingVersion.Streams.Any()) { - Either maybeStatistics = - await _plexServerApiClient.GetStatistics(incoming.Key.Split("/").Last(), connection, token); + Either> maybeStatistics = + await _plexServerApiClient.GetMovieMetadataAndStatistics( + library, + incoming.Key.Split("/").Last(), + connection, + token); await maybeStatistics.Match( - async mediaVersion => + async tuple => { + (MovieMetadata _, MediaVersion mediaVersion) = tuple; + _logger.LogDebug( "Refreshing {Attribute} from {Path}", "Plex Statistics", @@ -259,7 +267,7 @@ namespace ErsatzTV.Core.Plex .ToList()) { existingMetadata.Directors.Remove(director); - if (await _movieRepository.RemoveDirector(director)) + if (await _metadataRepository.RemoveDirector(director)) { result.IsUpdated = true; } @@ -281,7 +289,7 @@ namespace ErsatzTV.Core.Plex .ToList()) { existingMetadata.Writers.Remove(writer); - if (await _movieRepository.RemoveWriter(writer)) + if (await _metadataRepository.RemoveWriter(writer)) { result.IsUpdated = true; } @@ -298,6 +306,28 @@ namespace ErsatzTV.Core.Plex } } + foreach (MetadataGuid guid in existingMetadata.Guids + .Filter(g => fullMetadata.Guids.All(g2 => g2.Guid != g.Guid)) + .ToList()) + { + existingMetadata.Guids.Remove(guid); + if (await _metadataRepository.RemoveGuid(guid)) + { + result.IsUpdated = true; + } + } + + foreach (MetadataGuid guid in fullMetadata.Guids + .Filter(g => existingMetadata.Guids.All(g2 => g2.Guid != g.Guid)) + .ToList()) + { + existingMetadata.Guids.Add(guid); + if (await _metadataRepository.AddGuid(existingMetadata, guid)) + { + result.IsUpdated = true; + } + } + if (fullMetadata.SortTitle != existingMetadata.SortTitle) { existingMetadata.SortTitle = fullMetadata.SortTitle; diff --git a/ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs b/ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs index cd082ada6..39989bd50 100644 --- a/ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs +++ b/ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using ErsatzTV.Core.Domain; @@ -70,17 +71,7 @@ namespace ErsatzTV.Core.Plex await maybeShow.Match( async result => { - if (result.IsAdded || incoming.ShowMetadata.Head().DateUpdated > - result.Item.ShowMetadata.Head().DateUpdated) - { - await ScanSeasons(library, result.Item, connection, token); - } - else - { - _logger.LogDebug( - "Skipping Plex show that has not been updated: {Show}", - incoming.ShowMetadata.Head().Title); - } + await ScanSeasons(library, result.Item, connection, token); if (result.IsAdded) { @@ -227,6 +218,28 @@ namespace ErsatzTV.Core.Plex } } + foreach (MetadataGuid guid in existingMetadata.Guids + .Filter(g => fullMetadata.Guids.All(g2 => g2.Guid != g.Guid)) + .ToList()) + { + existingMetadata.Guids.Remove(guid); + if (await _metadataRepository.RemoveGuid(guid)) + { + result.IsUpdated = true; + } + } + + foreach (MetadataGuid guid in fullMetadata.Guids + .Filter(g => existingMetadata.Guids.All(g2 => g2.Guid != g.Guid)) + .ToList()) + { + existingMetadata.Guids.Add(guid); + if (await _metadataRepository.AddGuid(existingMetadata, guid)) + { + result.IsUpdated = true; + } + } + if (result.IsUpdated) { await _metadataRepository.MarkAsUpdated(existingMetadata, fullMetadata.DateUpdated); @@ -278,7 +291,7 @@ namespace ErsatzTV.Core.Plex // TODO: figure out how to rebuild playlists Either maybeSeason = await _televisionRepository .GetOrAddPlexSeason(plexMediaSourceLibrary, incoming) - .BindT(existing => UpdateArtwork(existing, incoming)); + .BindT(existing => UpdateMetadataAndArtwork(existing, incoming)); await maybeSeason.Match( async season => await ScanEpisodes(plexMediaSourceLibrary, season, connection, token), @@ -308,13 +321,31 @@ namespace ErsatzTV.Core.Plex }); } - private async Task> UpdateArtwork(PlexSeason existing, PlexSeason incoming) + private async Task> UpdateMetadataAndArtwork( + PlexSeason existing, + PlexSeason incoming) { SeasonMetadata existingMetadata = existing.SeasonMetadata.Head(); SeasonMetadata incomingMetadata = incoming.SeasonMetadata.Head(); if (incomingMetadata.DateUpdated > existingMetadata.DateUpdated) { + foreach (MetadataGuid guid in existingMetadata.Guids + .Filter(g => incomingMetadata.Guids.All(g2 => g2.Guid != g.Guid)) + .ToList()) + { + existingMetadata.Guids.Remove(guid); + await _metadataRepository.RemoveGuid(guid); + } + + foreach (MetadataGuid guid in incomingMetadata.Guids + .Filter(g => existingMetadata.Guids.All(g2 => g2.Guid != g.Guid)) + .ToList()) + { + existingMetadata.Guids.Add(guid); + await _metadataRepository.AddGuid(existingMetadata, guid); + } + await UpdateArtworkIfNeeded(existingMetadata, incomingMetadata, ArtworkKind.Poster); await _metadataRepository.MarkAsUpdated(existingMetadata, incomingMetadata.DateUpdated); } @@ -344,18 +375,34 @@ namespace ErsatzTV.Core.Plex // TODO: figure out how to rebuild playlists Either maybeEpisode = await _televisionRepository .GetOrAddPlexEpisode(plexMediaSourceLibrary, incoming) - .BindT(existing => UpdateStatistics(existing, incoming, connection, token)) + .BindT( + existing => UpdateMetadataAndStatistics( + existing, + incoming, + plexMediaSourceLibrary, + connection, + token)) .BindT(existing => UpdateArtwork(existing, incoming)); - maybeEpisode.IfLeft( - error => _logger.LogWarning( - "Error processing plex episode at {Key}: {Error}", - incoming.Key, - error.Value)); + await maybeEpisode.Match( + async episode => + { + await _searchIndex.UpdateItems(_searchRepository, new List { episode }); + }, + error => + { + _logger.LogWarning( + "Error processing plex episode at {Key}: {Error}", + incoming.Key, + error.Value); + return Task.CompletedTask; + }); } var episodeKeys = episodeEntries.Map(s => s.Key).ToList(); - await _televisionRepository.RemoveMissingPlexEpisodes(season.Key, episodeKeys); + List ids = await _televisionRepository.RemoveMissingPlexEpisodes(season.Key, episodeKeys); + await _searchIndex.RemoveItems(ids); + _searchIndex.Commit(); return Unit.Default; }, @@ -370,9 +417,10 @@ namespace ErsatzTV.Core.Plex }); } - private async Task> UpdateStatistics( + private async Task> UpdateMetadataAndStatistics( PlexEpisode existing, PlexEpisode incoming, + PlexLibrary library, PlexConnection connection, PlexServerAuthToken token) { @@ -381,12 +429,36 @@ namespace ErsatzTV.Core.Plex if (incomingVersion.DateUpdated > existingVersion.DateUpdated || !existingVersion.Streams.Any()) { - Either maybeStatistics = - await _plexServerApiClient.GetStatistics(incoming.Key.Split("/").Last(), connection, token); + Either> maybeStatistics = + await _plexServerApiClient.GetEpisodeMetadataAndStatistics( + library, + incoming.Key.Split("/").Last(), + connection, + token); await maybeStatistics.Match( - async mediaVersion => + async tuple => { + (EpisodeMetadata incomingMetadata, MediaVersion mediaVersion) = tuple; + + EpisodeMetadata existingMetadata = existing.EpisodeMetadata.Head(); + + foreach (MetadataGuid guid in existingMetadata.Guids + .Filter(g => incomingMetadata.Guids.All(g2 => g2.Guid != g.Guid)) + .ToList()) + { + existingMetadata.Guids.Remove(guid); + await _metadataRepository.RemoveGuid(guid); + } + + foreach (MetadataGuid guid in incomingMetadata.Guids + .Filter(g => existingMetadata.Guids.All(g2 => g2.Guid != g.Guid)) + .ToList()) + { + existingMetadata.Guids.Add(guid); + await _metadataRepository.AddGuid(existingMetadata, guid); + } + existingVersion.SampleAspectRatio = mediaVersion.SampleAspectRatio; existingVersion.VideoScanKind = mediaVersion.VideoScanKind; existingVersion.DateUpdated = mediaVersion.DateUpdated; @@ -399,7 +471,9 @@ namespace ErsatzTV.Core.Plex return Right(existing); } - private async Task> UpdateArtwork(PlexEpisode existing, PlexEpisode incoming) + private async Task> UpdateArtwork( + PlexEpisode existing, + PlexEpisode incoming) { EpisodeMetadata existingMetadata = existing.EpisodeMetadata.Head(); EpisodeMetadata incomingMetadata = incoming.EpisodeMetadata.Head(); diff --git a/ErsatzTV.Core/Scheduling/MultiPartEpisodeGrouper.cs b/ErsatzTV.Core/Scheduling/MultiPartEpisodeGrouper.cs index 1931a76dd..e443fd3e9 100644 --- a/ErsatzTV.Core/Scheduling/MultiPartEpisodeGrouper.cs +++ b/ErsatzTV.Core/Scheduling/MultiPartEpisodeGrouper.cs @@ -15,15 +15,27 @@ namespace ErsatzTV.Core.Scheduling var groups = new List(); GroupedMediaItem group = null; var lastNumber = 0; + + void AddUngrouped(MediaItem item) + { + if (group != null && lastNumber != 0) + { + groups.Add(group); + group = null; + lastNumber = 0; + } + + groups.Add(new GroupedMediaItem(item, null)); + } + foreach (MediaItem item in sortedMediaItems) { if (item is Episode e) { - const string PATTERN = @"^.*\((\d+)\)( - .*)?$"; - Match match = Regex.Match(e.EpisodeMetadata.Head().Title, PATTERN); - if (match.Success) + string numberString = FindPartNumber(e); + if (numberString != null) { - var number = int.Parse(match.Groups[1].Value); + var number = int.Parse(numberString); if (number <= lastNumber && group != null) { groups.Add(group); @@ -48,28 +60,20 @@ namespace ErsatzTV.Core.Scheduling else { // this should never happen - throw new InvalidOperationException("Bad shuffle state"); + throw new InvalidOperationException( + $"Bad shuffle state; unexpected number {number} after {lastNumber} with no existing group"); } lastNumber = number; } else { - // this should never happen - throw new InvalidOperationException( - $"Bad shuffle state; unexpected number {number} after {lastNumber}"); + AddUngrouped(item); } } else { - if (group != null && lastNumber != 0) - { - groups.Add(group); - group = null; - lastNumber = 0; - } - - groups.Add(new GroupedMediaItem(item, null)); + AddUngrouped(item); } } else @@ -86,6 +90,20 @@ namespace ErsatzTV.Core.Scheduling return groups; } + private static string FindPartNumber(Episode e) + { + const string PATTERN = @"^.*\((\d+)\)( - .*)?$"; + Match match = Regex.Match(e.EpisodeMetadata.Head().Title, PATTERN); + if (match.Success) + { + return match.Groups[1].Value; + } + + const string PATTERN_2 = @"^.*Part (\d+)$"; + Match match2 = Regex.Match(e.EpisodeMetadata.Head().Title, PATTERN_2); + return match2.Success ? match2.Groups[1].Value : null; + } + public static IList FlattenGroups(GroupedMediaItem[] copy, int mediaItemCount) { var result = new MediaItem[mediaItemCount]; diff --git a/ErsatzTV.Infrastructure/Data/Configurations/Metadata/EpisodeMetadataConfiguration.cs b/ErsatzTV.Infrastructure/Data/Configurations/Metadata/EpisodeMetadataConfiguration.cs index ced822976..c00122570 100644 --- a/ErsatzTV.Infrastructure/Data/Configurations/Metadata/EpisodeMetadataConfiguration.cs +++ b/ErsatzTV.Infrastructure/Data/Configurations/Metadata/EpisodeMetadataConfiguration.cs @@ -17,6 +17,18 @@ namespace ErsatzTV.Infrastructure.Data.Configurations builder.HasMany(em => em.Actors) .WithOne() .OnDelete(DeleteBehavior.Cascade); + + builder.HasMany(mm => mm.Directors) + .WithOne() + .OnDelete(DeleteBehavior.Cascade); + + builder.HasMany(mm => mm.Writers) + .WithOne() + .OnDelete(DeleteBehavior.Cascade); + + builder.HasMany(mm => mm.Guids) + .WithOne() + .OnDelete(DeleteBehavior.Cascade); } } } diff --git a/ErsatzTV.Infrastructure/Data/Configurations/Metadata/MetadataGuidConfiguration.cs b/ErsatzTV.Infrastructure/Data/Configurations/Metadata/MetadataGuidConfiguration.cs new file mode 100644 index 000000000..9db968017 --- /dev/null +++ b/ErsatzTV.Infrastructure/Data/Configurations/Metadata/MetadataGuidConfiguration.cs @@ -0,0 +1,11 @@ +using ErsatzTV.Core.Domain; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace ErsatzTV.Infrastructure.Data.Configurations +{ + public class MetadataGuidConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) => builder.ToTable("MetadataGuid"); + } +} diff --git a/ErsatzTV.Infrastructure/Data/Configurations/Metadata/MovieMetadataConfiguration.cs b/ErsatzTV.Infrastructure/Data/Configurations/Metadata/MovieMetadataConfiguration.cs index c2a4123d2..ccb43dfea 100644 --- a/ErsatzTV.Infrastructure/Data/Configurations/Metadata/MovieMetadataConfiguration.cs +++ b/ErsatzTV.Infrastructure/Data/Configurations/Metadata/MovieMetadataConfiguration.cs @@ -37,6 +37,10 @@ namespace ErsatzTV.Infrastructure.Data.Configurations builder.HasMany(mm => mm.Writers) .WithOne() .OnDelete(DeleteBehavior.Cascade); + + builder.HasMany(mm => mm.Guids) + .WithOne() + .OnDelete(DeleteBehavior.Cascade); } } } diff --git a/ErsatzTV.Infrastructure/Data/Configurations/Metadata/SeasonMetadataConfiguration.cs b/ErsatzTV.Infrastructure/Data/Configurations/Metadata/SeasonMetadataConfiguration.cs index 5584a0e48..3dedb790d 100644 --- a/ErsatzTV.Infrastructure/Data/Configurations/Metadata/SeasonMetadataConfiguration.cs +++ b/ErsatzTV.Infrastructure/Data/Configurations/Metadata/SeasonMetadataConfiguration.cs @@ -13,6 +13,10 @@ namespace ErsatzTV.Infrastructure.Data.Configurations builder.HasMany(em => em.Artwork) .WithOne() .OnDelete(DeleteBehavior.Cascade); + + builder.HasMany(mm => mm.Guids) + .WithOne() + .OnDelete(DeleteBehavior.Cascade); } } } diff --git a/ErsatzTV.Infrastructure/Data/Configurations/Metadata/ShowMetadataConfiguration.cs b/ErsatzTV.Infrastructure/Data/Configurations/Metadata/ShowMetadataConfiguration.cs index cc2bfe87a..222d2ef70 100644 --- a/ErsatzTV.Infrastructure/Data/Configurations/Metadata/ShowMetadataConfiguration.cs +++ b/ErsatzTV.Infrastructure/Data/Configurations/Metadata/ShowMetadataConfiguration.cs @@ -29,6 +29,10 @@ namespace ErsatzTV.Infrastructure.Data.Configurations builder.HasMany(sm => sm.Actors) .WithOne() .OnDelete(DeleteBehavior.Cascade); + + builder.HasMany(mm => mm.Guids) + .WithOne() + .OnDelete(DeleteBehavior.Cascade); } } } diff --git a/ErsatzTV.Infrastructure/Data/Repositories/ArtworkRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/ArtworkRepository.cs index 81fed6aec..af7547537 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/ArtworkRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/ArtworkRepository.cs @@ -21,6 +21,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories WHERE A.ArtistMetadataId IS NULL AND A.EpisodeMetadataId IS NULL AND A.SeasonMetadataId IS NULL AND A.ShowMetadataId IS NULL AND A.MovieMetadataId IS NULL AND A.MusicVideoMetadataId IS NULL + AND A.ChannelId IS NULL AND NOT EXISTS (SELECT * FROM Actor WHERE Actor.ArtworkId = A.Id)") .Map(result => result.ToList()); diff --git a/ErsatzTV.Infrastructure/Data/Repositories/EmbyTelevisionRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/EmbyTelevisionRepository.cs index 43202a3ec..43f6b45d1 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/EmbyTelevisionRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/EmbyTelevisionRepository.cs @@ -88,6 +88,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories .ThenInclude(mm => mm.Actors) .Include(m => m.ShowMetadata) .ThenInclude(mm => mm.Artwork) + .Include(m => m.ShowMetadata) + .ThenInclude(mm => mm.Guids) .Filter(m => m.ItemId == show.ItemId) .OrderBy(m => m.ItemId) .SingleOrDefaultAsync(); @@ -177,6 +179,21 @@ namespace ErsatzTV.Infrastructure.Data.Repositories metadata.Actors.Add(actor); } + // guids + foreach (MetadataGuid guid in metadata.Guids + .Filter(g => incomingMetadata.Guids.All(g2 => g2.Guid != g.Guid)) + .ToList()) + { + metadata.Guids.Remove(guid); + } + + foreach (MetadataGuid guid in incomingMetadata.Guids + .Filter(g => metadata.Guids.All(g2 => g2.Guid != g.Guid)) + .ToList()) + { + metadata.Guids.Add(guid); + } + metadata.ReleaseDate = incomingMetadata.ReleaseDate; // poster @@ -246,6 +263,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories .Include(m => m.LibraryPath) .Include(m => m.SeasonMetadata) .ThenInclude(mm => mm.Artwork) + .Include(m => m.SeasonMetadata) + .ThenInclude(mm => mm.Guids) .Filter(m => m.ItemId == season.ItemId) .OrderBy(m => m.ItemId) .SingleOrDefaultAsync(); @@ -322,6 +341,21 @@ namespace ErsatzTV.Infrastructure.Data.Repositories fanArt.DateUpdated = incomingFanArt.DateUpdated; } + // guids + foreach (MetadataGuid guid in metadata.Guids + .Filter(g => incomingMetadata.Guids.All(g2 => g2.Guid != g.Guid)) + .ToList()) + { + metadata.Guids.Remove(guid); + } + + foreach (MetadataGuid guid in incomingMetadata.Guids + .Filter(g => metadata.Guids.All(g2 => g2.Guid != g.Guid)) + .ToList()) + { + metadata.Guids.Add(guid); + } + var paths = incomingMetadata.Artwork.Map(a => a.Path).ToList(); foreach (Artwork artworkToRemove in metadata.Artwork.Filter(a => !paths.Contains(a.Path))) { @@ -345,20 +379,37 @@ namespace ErsatzTV.Infrastructure.Data.Repositories await dbContext.Entry(episode).Reference(m => m.LibraryPath).LoadAsync(); await dbContext.Entry(episode.LibraryPath).Reference(lp => lp.Library).LoadAsync(); + await dbContext.Entry(episode).Reference(e => e.Season).LoadAsync(); return true; } - public async Task Update(EmbyEpisode episode) + public async Task> Update(EmbyEpisode episode) { await using TvContext dbContext = _dbContextFactory.CreateDbContext(); Option maybeExisting = await dbContext.EmbyEpisodes .Include(m => m.LibraryPath) + .ThenInclude(lp => lp.Library) .Include(m => m.MediaVersions) .ThenInclude(mv => mv.MediaFiles) .Include(m => m.MediaVersions) .ThenInclude(mv => mv.Streams) .Include(m => m.EpisodeMetadata) .ThenInclude(mm => mm.Artwork) + .Include(m => m.EpisodeMetadata) + .ThenInclude(mm => mm.Guids) + .Include(m => m.EpisodeMetadata) + .ThenInclude(mm => mm.Genres) + .Include(m => m.EpisodeMetadata) + .ThenInclude(mm => mm.Tags) + .Include(m => m.EpisodeMetadata) + .ThenInclude(mm => mm.Studios) + .Include(m => m.EpisodeMetadata) + .ThenInclude(mm => mm.Actors) + .Include(m => m.EpisodeMetadata) + .ThenInclude(mm => mm.Directors) + .Include(m => m.EpisodeMetadata) + .ThenInclude(mm => mm.Writers) + .Include(m => m.Season) .Filter(m => m.ItemId == episode.ItemId) .OrderBy(m => m.ItemId) .SingleOrDefaultAsync(); @@ -402,6 +453,51 @@ namespace ErsatzTV.Infrastructure.Data.Repositories thumbnail.DateUpdated = incomingThumbnail.DateUpdated; } + // directors + foreach (Director director in metadata.Directors + .Filter(d => incomingMetadata.Directors.All(d2 => d2.Name != d.Name)) + .ToList()) + { + metadata.Directors.Remove(director); + } + + foreach (Director director in incomingMetadata.Directors + .Filter(d => metadata.Directors.All(d2 => d2.Name != d.Name)) + .ToList()) + { + metadata.Directors.Add(director); + } + + // writers + foreach (Writer writer in metadata.Writers + .Filter(w => incomingMetadata.Writers.All(w2 => w2.Name != w.Name)) + .ToList()) + { + metadata.Writers.Remove(writer); + } + + foreach (Writer writer in incomingMetadata.Writers + .Filter(w => metadata.Writers.All(w2 => w2.Name != w.Name)) + .ToList()) + { + metadata.Writers.Add(writer); + } + + // guids + foreach (MetadataGuid guid in metadata.Guids + .Filter(g => incomingMetadata.Guids.All(g2 => g2.Guid != g.Guid)) + .ToList()) + { + metadata.Guids.Remove(guid); + } + + foreach (MetadataGuid guid in incomingMetadata.Guids + .Filter(g => metadata.Guids.All(g2 => g2.Guid != g.Guid)) + .ToList()) + { + metadata.Guids.Add(guid); + } + var paths = incomingMetadata.Artwork.Map(a => a.Path).ToList(); foreach (Artwork artworkToRemove in metadata.Artwork.Filter(a => !paths.Contains(a.Path))) { @@ -422,7 +518,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories await dbContext.SaveChangesAsync(); - return Unit.Default; + return maybeExisting; } public async Task> RemoveMissingShows(EmbyLibrary library, List showIds) @@ -454,14 +550,25 @@ namespace ErsatzTV.Infrastructure.Data.Repositories WHERE LP.LibraryId = @LibraryId AND js.ItemId IN @SeasonIds)", new { LibraryId = library.Id, SeasonIds = seasonIds }).ToUnit(); - public Task RemoveMissingEpisodes(EmbyLibrary library, List episodeIds) => - _dbConnection.ExecuteAsync( + public async Task> RemoveMissingEpisodes(EmbyLibrary library, List episodeIds) + { + List ids = await _dbConnection.QueryAsync( + @"SELECT m.Id FROM MediaItem m + INNER JOIN EmbyEpisode ee ON ee.Id = m.Id + INNER JOIN LibraryPath lp ON lp.Id = m.LibraryPathId + WHERE lp.LibraryId = @LibraryId AND ee.ItemId IN @EpisodeIds", + new { LibraryId = library.Id, EpisodeIds = episodeIds }).Map(result => result.ToList()); + + await _dbConnection.ExecuteAsync( @"DELETE FROM MediaItem WHERE Id IN (SELECT m.Id FROM MediaItem m - INNER JOIN EmbyEpisode je ON je.Id = m.Id + INNER JOIN EmbyEpisode ee ON ee.Id = m.Id INNER JOIN LibraryPath LP on m.LibraryPathId = LP.Id - WHERE LP.LibraryId = @LibraryId AND je.ItemId IN @EpisodeIds)", - new { LibraryId = library.Id, EpisodeIds = episodeIds }).ToUnit(); + WHERE LP.LibraryId = @LibraryId AND ee.ItemId IN @EpisodeIds)", + new { LibraryId = library.Id, EpisodeIds = episodeIds }); + + return ids; + } public async Task DeleteEmptySeasons(EmbyLibrary library) { diff --git a/ErsatzTV.Infrastructure/Data/Repositories/JellyfinTelevisionRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/JellyfinTelevisionRepository.cs index a07e49a0c..252171c0f 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/JellyfinTelevisionRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/JellyfinTelevisionRepository.cs @@ -88,6 +88,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories .ThenInclude(mm => mm.Actors) .Include(m => m.ShowMetadata) .ThenInclude(mm => mm.Artwork) + .Include(m => m.ShowMetadata) + .ThenInclude(mm => mm.Guids) .Filter(m => m.ItemId == show.ItemId) .OrderBy(m => m.ItemId) .SingleOrDefaultAsync(); @@ -177,6 +179,21 @@ namespace ErsatzTV.Infrastructure.Data.Repositories metadata.Actors.Add(actor); } + // guids + foreach (MetadataGuid guid in metadata.Guids + .Filter(g => incomingMetadata.Guids.All(g2 => g2.Guid != g.Guid)) + .ToList()) + { + metadata.Guids.Remove(guid); + } + + foreach (MetadataGuid guid in incomingMetadata.Guids + .Filter(g => metadata.Guids.All(g2 => g2.Guid != g.Guid)) + .ToList()) + { + metadata.Guids.Add(guid); + } + metadata.ReleaseDate = incomingMetadata.ReleaseDate; // poster @@ -263,6 +280,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories .Include(m => m.LibraryPath) .Include(m => m.SeasonMetadata) .ThenInclude(mm => mm.Artwork) + .Include(m => m.SeasonMetadata) + .ThenInclude(mm => mm.Guids) .Filter(m => m.ItemId == season.ItemId) .OrderBy(m => m.ItemId) .SingleOrDefaultAsync(); @@ -322,6 +341,21 @@ namespace ErsatzTV.Infrastructure.Data.Repositories fanArt.DateUpdated = incomingFanArt.DateUpdated; } + // guids + foreach (MetadataGuid guid in metadata.Guids + .Filter(g => incomingMetadata.Guids.All(g2 => g2.Guid != g.Guid)) + .ToList()) + { + metadata.Guids.Remove(guid); + } + + foreach (MetadataGuid guid in incomingMetadata.Guids + .Filter(g => metadata.Guids.All(g2 => g2.Guid != g.Guid)) + .ToList()) + { + metadata.Guids.Add(guid); + } + var paths = incomingMetadata.Artwork.Map(a => a.Path).ToList(); foreach (Artwork artworkToRemove in metadata.Artwork.Filter(a => !paths.Contains(a.Path))) { @@ -345,20 +379,37 @@ namespace ErsatzTV.Infrastructure.Data.Repositories await dbContext.Entry(episode).Reference(m => m.LibraryPath).LoadAsync(); await dbContext.Entry(episode.LibraryPath).Reference(lp => lp.Library).LoadAsync(); + await dbContext.Entry(episode).Reference(e => e.Season).LoadAsync(); return true; } - public async Task Update(JellyfinEpisode episode) + public async Task> Update(JellyfinEpisode episode) { await using TvContext dbContext = _dbContextFactory.CreateDbContext(); Option maybeExisting = await dbContext.JellyfinEpisodes .Include(m => m.LibraryPath) + .ThenInclude(lp => lp.Library) .Include(m => m.MediaVersions) .ThenInclude(mv => mv.MediaFiles) .Include(m => m.MediaVersions) .ThenInclude(mv => mv.Streams) .Include(m => m.EpisodeMetadata) .ThenInclude(mm => mm.Artwork) + .Include(m => m.EpisodeMetadata) + .ThenInclude(mm => mm.Guids) + .Include(m => m.EpisodeMetadata) + .ThenInclude(mm => mm.Genres) + .Include(m => m.EpisodeMetadata) + .ThenInclude(mm => mm.Tags) + .Include(m => m.EpisodeMetadata) + .ThenInclude(mm => mm.Studios) + .Include(m => m.EpisodeMetadata) + .ThenInclude(mm => mm.Actors) + .Include(m => m.EpisodeMetadata) + .ThenInclude(mm => mm.Directors) + .Include(m => m.EpisodeMetadata) + .ThenInclude(mm => mm.Writers) + .Include(m => m.Season) .Filter(m => m.ItemId == episode.ItemId) .OrderBy(m => m.ItemId) .SingleOrDefaultAsync(); @@ -402,6 +453,51 @@ namespace ErsatzTV.Infrastructure.Data.Repositories thumbnail.DateUpdated = incomingThumbnail.DateUpdated; } + // directors + foreach (Director director in metadata.Directors + .Filter(d => incomingMetadata.Directors.All(d2 => d2.Name != d.Name)) + .ToList()) + { + metadata.Directors.Remove(director); + } + + foreach (Director director in incomingMetadata.Directors + .Filter(d => metadata.Directors.All(d2 => d2.Name != d.Name)) + .ToList()) + { + metadata.Directors.Add(director); + } + + // writers + foreach (Writer writer in metadata.Writers + .Filter(w => incomingMetadata.Writers.All(w2 => w2.Name != w.Name)) + .ToList()) + { + metadata.Writers.Remove(writer); + } + + foreach (Writer writer in incomingMetadata.Writers + .Filter(w => metadata.Writers.All(w2 => w2.Name != w.Name)) + .ToList()) + { + metadata.Writers.Add(writer); + } + + // guids + foreach (MetadataGuid guid in metadata.Guids + .Filter(g => incomingMetadata.Guids.All(g2 => g2.Guid != g.Guid)) + .ToList()) + { + metadata.Guids.Remove(guid); + } + + foreach (MetadataGuid guid in incomingMetadata.Guids + .Filter(g => metadata.Guids.All(g2 => g2.Guid != g.Guid)) + .ToList()) + { + metadata.Guids.Add(guid); + } + var paths = incomingMetadata.Artwork.Map(a => a.Path).ToList(); foreach (Artwork artworkToRemove in metadata.Artwork.Filter(a => !paths.Contains(a.Path))) { @@ -422,7 +518,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories await dbContext.SaveChangesAsync(); - return Unit.Default; + return maybeExisting; } public async Task> RemoveMissingShows(JellyfinLibrary library, List showIds) @@ -454,14 +550,25 @@ namespace ErsatzTV.Infrastructure.Data.Repositories WHERE LP.LibraryId = @LibraryId AND js.ItemId IN @SeasonIds)", new { LibraryId = library.Id, SeasonIds = seasonIds }).ToUnit(); - public Task RemoveMissingEpisodes(JellyfinLibrary library, List episodeIds) => - _dbConnection.ExecuteAsync( + public async Task> RemoveMissingEpisodes(JellyfinLibrary library, List episodeIds) + { + List ids = await _dbConnection.QueryAsync( + @"SELECT m.Id FROM MediaItem m + INNER JOIN JellyfinEpisode je ON je.Id = m.Id + INNER JOIN LibraryPath lp ON lp.Id = m.LibraryPathId + WHERE lp.LibraryId = @LibraryId AND je.ItemId IN @EpisodeIds", + new { LibraryId = library.Id, EpisodeIds = episodeIds }).Map(result => result.ToList()); + + await _dbConnection.ExecuteAsync( @"DELETE FROM MediaItem WHERE Id IN (SELECT m.Id FROM MediaItem m INNER JOIN JellyfinEpisode je ON je.Id = m.Id INNER JOIN LibraryPath LP on m.LibraryPathId = LP.Id WHERE LP.LibraryId = @LibraryId AND je.ItemId IN @EpisodeIds)", - new { LibraryId = library.Id, EpisodeIds = episodeIds }).ToUnit(); + new { LibraryId = library.Id, EpisodeIds = episodeIds }); + + return ids; + } public async Task DeleteEmptySeasons(JellyfinLibrary library) { diff --git a/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs index 5c3cb76d2..63850a53c 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs @@ -155,9 +155,18 @@ namespace ErsatzTV.Infrastructure.Data.Repositories .ThenInclude(i => (i as Episode).EpisodeMetadata) .ThenInclude(em => em.Artwork) .Include(c => c.MediaItems) + .ThenInclude(i => (i as Episode).EpisodeMetadata) + .ThenInclude(em => em.Directors) + .Include(c => c.MediaItems) + .ThenInclude(i => (i as Episode).EpisodeMetadata) + .ThenInclude(em => em.Writers) + .Include(c => c.MediaItems) .ThenInclude(i => (i as Episode).Season) .ThenInclude(s => s.Show) .ThenInclude(s => s.ShowMetadata) + .Include(c => c.MediaItems) + .ThenInclude(i => (i as Episode).Season) + .ThenInclude(s => s.SeasonMetadata) .OrderBy(c => c.Id) .SingleOrDefaultAsync(c => c.Id == id) .Map(Optional); diff --git a/ErsatzTV.Infrastructure/Data/Repositories/MediaSourceRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/MediaSourceRepository.cs index 0507fd932..33593ada1 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/MediaSourceRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/MediaSourceRepository.cs @@ -354,10 +354,11 @@ namespace ErsatzTV.Infrastructure.Data.Repositories List movieIds = await context.PlexMovies.Map(pm => pm.Id).ToListAsync(); List showIds = await context.PlexShows.Map(ps => ps.Id).ToListAsync(); + List episodeIds = await context.PlexEpisodes.Map(pe => pe.Id).ToListAsync(); await context.SaveChangesAsync(); - return movieIds.Append(showIds).ToList(); + return movieIds.Append(showIds).Append(episodeIds).ToList(); } public async Task> DeletePlex(PlexMediaSource plexMediaSource) @@ -404,6 +405,14 @@ namespace ErsatzTV.Infrastructure.Data.Repositories WHERE l.Id IN @ids)", new { ids = libraryIds }); + List episodeIds = await _dbConnection.QueryAsync( + @"SELECT m.Id FROM MediaItem m + INNER JOIN PlexEpisode pe ON pe.Id = m.Id + INNER JOIN LibraryPath lp ON lp.Id = m.LibraryPathId + INNER JOIN Library l ON l.Id = lp.LibraryId + WHERE l.Id IN @ids", + new { ids = libraryIds }).Map(result => result.ToList()); + await _dbConnection.ExecuteAsync( @"DELETE FROM MediaItem WHERE Id IN (SELECT m.Id FROM MediaItem m @@ -439,7 +448,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories WHERE l.Id IN @ids)", new { ids = libraryIds }); - return movieIds.Append(showIds).ToList(); + return movieIds.Append(showIds).Append(episodeIds).ToList(); } public Task EnablePlexLibrarySync(IEnumerable libraryIds) => @@ -561,6 +570,14 @@ namespace ErsatzTV.Infrastructure.Data.Repositories WHERE l.Id IN @ids)", new { ids = libraryIds }); + List episodeIds = await _dbConnection.QueryAsync( + @"SELECT m.Id FROM MediaItem m + INNER JOIN JellyfinEpisode pe ON pe.Id = m.Id + INNER JOIN LibraryPath lp ON lp.Id = m.LibraryPathId + INNER JOIN Library l ON l.Id = lp.LibraryId + WHERE l.Id IN @ids", + new { ids = libraryIds }).Map(result => result.ToList()); + await _dbConnection.ExecuteAsync( @"DELETE FROM MediaItem WHERE Id IN (SELECT m.Id FROM MediaItem m @@ -596,7 +613,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories WHERE l.Id IN @ids)", new { ids = libraryIds }); - return movieIds.Append(showIds).ToList(); + return movieIds.Append(showIds).Append(episodeIds).ToList(); } public Task> GetJellyfinLibrary(int jellyfinLibraryId) @@ -708,9 +725,14 @@ namespace ErsatzTV.Infrastructure.Data.Repositories .Map(ps => ps.Id) .ToListAsync(); + List episodeIds = await context.JellyfinEpisodes + .Where(m => libraryIds.Contains(m.LibraryPath.LibraryId)) + .Map(ps => ps.Id) + .ToListAsync(); + await context.SaveChangesAsync(); - return movieIds.Append(showIds).ToList(); + return movieIds.Append(showIds).Append(episodeIds).ToList(); } public async Task UpsertEmby(string address, string serverName, string operatingSystem) @@ -904,9 +926,14 @@ namespace ErsatzTV.Infrastructure.Data.Repositories .Map(ps => ps.Id) .ToListAsync(); + List episodeIds = await context.EmbyEpisodes + .Where(m => libraryIds.Contains(m.LibraryPath.LibraryId)) + .Map(ps => ps.Id) + .ToListAsync(); + await context.SaveChangesAsync(); - return movieIds.Append(showIds).ToList(); + return movieIds.Append(showIds).Append(episodeIds).ToList(); } public Task EnableEmbyLibrarySync(IEnumerable libraryIds) => @@ -941,6 +968,14 @@ namespace ErsatzTV.Infrastructure.Data.Repositories WHERE l.Id IN @ids)", new { ids = libraryIds }); + List episodeIds = await _dbConnection.QueryAsync( + @"SELECT m.Id FROM MediaItem m + INNER JOIN EmbyEpisode pe ON pe.Id = m.Id + INNER JOIN LibraryPath lp ON lp.Id = m.LibraryPathId + INNER JOIN Library l ON l.Id = lp.LibraryId + WHERE l.Id IN @ids", + new { ids = libraryIds }).Map(result => result.ToList()); + await _dbConnection.ExecuteAsync( @"DELETE FROM MediaItem WHERE Id IN (SELECT m.Id FROM MediaItem m @@ -976,7 +1011,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories WHERE l.Id IN @ids)", new { ids = libraryIds }); - return movieIds.Append(showIds).ToList(); + return movieIds.Append(showIds).Append(episodeIds).ToList(); } } } diff --git a/ErsatzTV.Infrastructure/Data/Repositories/MetadataRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/MetadataRepository.cs index 2c2c1b56b..3d54a40ed 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/MetadataRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/MetadataRepository.cs @@ -75,6 +75,11 @@ namespace ErsatzTV.Infrastructure.Data.Repositories } } + foreach (MetadataGuid guid in Optional(metadata.Guids).Flatten()) + { + dbContext.Entry(guid).State = EntityState.Added; + } + if (metadata is MovieMetadata movieMetadata) { foreach (Director director in Optional(movieMetadata.Directors).Flatten()) @@ -88,6 +93,19 @@ namespace ErsatzTV.Infrastructure.Data.Repositories } } + if (metadata is EpisodeMetadata episodeMetadata) + { + foreach (Director director in Optional(episodeMetadata.Directors).Flatten()) + { + dbContext.Entry(director).State = EntityState.Added; + } + + foreach (Writer writer in Optional(episodeMetadata.Writers).Flatten()) + { + dbContext.Entry(writer).State = EntityState.Added; + } + } + return await dbContext.SaveChangesAsync() > 0; } @@ -261,6 +279,44 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @"UPDATE MovieMetadata SET ContentRating = @ContentRating WHERE Id = @Id", new { metadata.Id, ContentRating = contentRating }).ToUnit(); + public Task RemoveGuid(MetadataGuid guid) => + _dbConnection.ExecuteAsync("DELETE FROM MetadataGuid WHERE Id = @GuidId", new { GuidId = guid.Id }) + .Map(result => result > 0); + + public Task AddGuid(Metadata metadata, MetadataGuid guid) => + metadata switch + { + MovieMetadata => + _dbConnection.ExecuteAsync( + "INSERT INTO MetadataGuid (Guid, MovieMetadataId) VALUES (@Guid, @MetadataId)", + new { guid.Guid, MetadataId = metadata.Id }).Map(result => result > 0), + ShowMetadata => + _dbConnection.ExecuteAsync( + "INSERT INTO MetadataGuid (Guid, ShowMetadataId) VALUES (@Guid, @MetadataId)", + new { guid.Guid, MetadataId = metadata.Id }).Map(result => result > 0), + SeasonMetadata => + _dbConnection.ExecuteAsync( + "INSERT INTO MetadataGuid (Guid, SeasonMetadataId) VALUES (@Guid, @MetadataId)", + new { guid.Guid, MetadataId = metadata.Id }).Map(result => result > 0), + EpisodeMetadata => + _dbConnection.ExecuteAsync( + "INSERT INTO MetadataGuid (Guid, EpisodeMetadataId) VALUES (@Guid, @MetadataId)", + new { guid.Guid, MetadataId = metadata.Id }).Map(result => result > 0), + ArtistMetadata => + _dbConnection.ExecuteAsync( + "INSERT INTO MetadataGuid (Guid, ArtistMetadataId) VALUES (@Guid, @MetadataId)", + new { guid.Guid, MetadataId = metadata.Id }).Map(result => result > 0), + _ => throw new NotSupportedException() + }; + + public Task RemoveDirector(Director director) => + _dbConnection.ExecuteAsync("DELETE FROM Director WHERE Id = @DirectorId", new { DirectorId = director.Id }) + .Map(result => result > 0); + + public Task RemoveWriter(Writer writer) => + _dbConnection.ExecuteAsync("DELETE FROM Writer WHERE Id = @WriterId", new { WriterId = writer.Id }) + .Map(result => result > 0); + public Task RemoveGenre(Genre genre) => _dbConnection.ExecuteAsync("DELETE FROM Genre WHERE Id = @GenreId", new { GenreId = genre.Id }) .Map(result => result > 0); diff --git a/ErsatzTV.Infrastructure/Data/Repositories/MovieRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/MovieRepository.cs index b4af37481..498d01e76 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/MovieRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/MovieRepository.cs @@ -119,6 +119,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories .ThenInclude(mm => mm.Directors) .Include(i => i.MovieMetadata) .ThenInclude(mm => mm.Writers) + .Include(i => i.MovieMetadata) + .ThenInclude(mm => mm.Guids) .Include(i => i.MediaVersions) .ThenInclude(mv => mv.MediaFiles) .Include(i => i.MediaVersions) @@ -328,6 +330,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories .ThenInclude(mm => mm.Directors) .Include(m => m.MovieMetadata) .ThenInclude(mm => mm.Writers) + .Include(m => m.MovieMetadata) + .ThenInclude(mm => mm.Guids) .Filter(m => m.ItemId == movie.ItemId) .OrderBy(m => m.ItemId) .SingleOrDefaultAsync(); @@ -447,6 +451,21 @@ namespace ErsatzTV.Infrastructure.Data.Repositories metadata.Writers.Add(writer); } + // guids + foreach (MetadataGuid guid in metadata.Guids + .Filter(g => incomingMetadata.Guids.All(g2 => g2.Guid != g.Guid)) + .ToList()) + { + metadata.Guids.Remove(guid); + } + + foreach (MetadataGuid guid in incomingMetadata.Guids + .Filter(g => metadata.Guids.All(g2 => g2.Guid != g.Guid)) + .ToList()) + { + metadata.Guids.Add(guid); + } + metadata.ReleaseDate = incomingMetadata.ReleaseDate; // poster @@ -565,6 +584,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories .ThenInclude(mm => mm.Writers) .Include(m => m.MovieMetadata) .ThenInclude(mm => mm.Artwork) + .Include(m => m.MovieMetadata) + .ThenInclude(mm => mm.Guids) .Filter(m => m.ItemId == movie.ItemId) .OrderBy(m => m.ItemId) .SingleOrDefaultAsync(); @@ -684,6 +705,21 @@ namespace ErsatzTV.Infrastructure.Data.Repositories metadata.Writers.Add(writer); } + // guids + foreach (MetadataGuid guid in metadata.Guids + .Filter(g => incomingMetadata.Guids.All(g2 => g2.Guid != g.Guid)) + .ToList()) + { + metadata.Guids.Remove(guid); + } + + foreach (MetadataGuid guid in incomingMetadata.Guids + .Filter(g => metadata.Guids.All(g2 => g2.Guid != g.Guid)) + .ToList()) + { + metadata.Guids.Add(guid); + } + metadata.ReleaseDate = incomingMetadata.ReleaseDate; // poster @@ -737,19 +773,11 @@ namespace ErsatzTV.Infrastructure.Data.Repositories return maybeExisting; } - public Task RemoveDirector(Director director) => - _dbConnection.ExecuteAsync("DELETE FROM Director WHERE Id = @DirectorId", new { DirectorId = director.Id }) - .Map(result => result > 0); - public Task AddDirector(MovieMetadata metadata, Director director) => _dbConnection.ExecuteAsync( "INSERT INTO Director (Name, MovieMetadataId) VALUES (@Name, @MetadataId)", new { director.Name, MetadataId = metadata.Id }).Map(result => result > 0); - public Task RemoveWriter(Writer writer) => - _dbConnection.ExecuteAsync("DELETE FROM Writer WHERE Id = @WriterId", new { WriterId = writer.Id }) - .Map(result => result > 0); - public Task AddWriter(MovieMetadata metadata, Writer writer) => _dbConnection.ExecuteAsync( "INSERT INTO Writer (Name, MovieMetadataId) VALUES (@Name, @MetadataId)", diff --git a/ErsatzTV.Infrastructure/Data/Repositories/SearchRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/SearchRepository.cs index fdbc7eca9..65f8dba12 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/SearchRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/SearchRepository.cs @@ -47,6 +47,21 @@ namespace ErsatzTV.Infrastructure.Data.Repositories .ThenInclude(mm => mm.Writers) .Include(mi => (mi as Movie).MediaVersions) .ThenInclude(mm => mm.Streams) + .Include(mi => (mi as Episode).EpisodeMetadata) + .ThenInclude(em => em.Genres) + .Include(mi => (mi as Episode).EpisodeMetadata) + .ThenInclude(em => em.Tags) + .Include(mi => (mi as Episode).EpisodeMetadata) + .ThenInclude(em => em.Studios) + .Include(mi => (mi as Episode).EpisodeMetadata) + .ThenInclude(em => em.Actors) + .Include(mi => (mi as Episode).EpisodeMetadata) + .ThenInclude(em => em.Directors) + .Include(mi => (mi as Episode).EpisodeMetadata) + .ThenInclude(em => em.Writers) + .Include(mi => (mi as Episode).MediaVersions) + .ThenInclude(em => em.Streams) + .Include(mi => (mi as Episode).Season) .Include(mi => (mi as Show).ShowMetadata) .ThenInclude(mm => mm.Genres) .Include(mi => (mi as Show).ShowMetadata) diff --git a/ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs index f82e4ca93..901835317 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs @@ -31,6 +31,12 @@ namespace ErsatzTV.Infrastructure.Data.Repositories new { ShowIds = showIds }) .Map(c => c == showIds.Count); + public Task AllEpisodesExist(List episodeIds) => + _dbConnection.QuerySingleAsync( + "SELECT COUNT(*) FROM Episode WHERE Id in @EpisodeIds", + new { EpisodeIds = episodeIds }) + .Map(c => c == episodeIds.Count); + public async Task> GetAllShows() { await using TvContext dbContext = _dbContextFactory.CreateDbContext(); @@ -63,31 +69,6 @@ namespace ErsatzTV.Infrastructure.Data.Repositories .Map(Optional); } - public async Task GetShowCount() - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - return await dbContext.ShowMetadata - .AsNoTracking() - .GroupBy(sm => new { sm.Title, sm.Year }) - .CountAsync(); - } - - public async Task> GetPagedShows(int pageNumber, int pageSize) - { - await using TvContext dbContext = _dbContextFactory.CreateDbContext(); - return await dbContext.ShowMetadata.FromSqlRaw( - @"SELECT * FROM ShowMetadata WHERE Id IN - (SELECT MIN(Id) FROM ShowMetadata GROUP BY Title, Year, MetadataKind HAVING MetadataKind = MAX(MetadataKind)) - ORDER BY SortTitle - LIMIT {0} OFFSET {1}", - pageSize, - (pageNumber - 1) * pageSize) - .AsNoTracking() - .Include(mm => mm.Artwork) - .OrderBy(mm => mm.SortTitle) - .ToListAsync(); - } - public async Task> GetShowsForCards(List ids) { await using TvContext dbContext = _dbContextFactory.CreateDbContext(); @@ -99,6 +80,27 @@ namespace ErsatzTV.Infrastructure.Data.Repositories .ToListAsync(); } + public async Task> GetEpisodesForCards(List ids) + { + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + return await dbContext.EpisodeMetadata + .AsNoTracking() + .Filter(em => ids.Contains(em.EpisodeId)) + .Include(em => em.Artwork) + .Include(em => em.Directors) + .Include(em => em.Writers) + .Include(em => em.Episode) + .ThenInclude(e => e.Season) + .ThenInclude(s => s.SeasonMetadata) + .ThenInclude(sm => sm.Artwork) + .Include(em => em.Episode) + .ThenInclude(e => e.Season) + .ThenInclude(s => s.Show) + .ThenInclude(s => s.ShowMetadata) + .OrderBy(em => em.SortTitle) + .ToListAsync(); + } + public async Task> GetAllSeasons() { await using TvContext dbContext = _dbContextFactory.CreateDbContext(); @@ -187,6 +189,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories .AsNoTracking() .Filter(em => em.Episode.SeasonId == seasonId) .Include(em => em.Artwork) + .Include(em => em.Directors) + .Include(em => em.Writers) .Include(em => em.Episode) .ThenInclude(e => e.Season) .ThenInclude(s => s.Show) @@ -225,6 +229,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories .Include(s => s.ShowMetadata) .ThenInclude(sm => sm.Actors) .ThenInclude(a => a.Artwork) + .Include(s => s.ShowMetadata) + .ThenInclude(sm => sm.Guids) .Include(s => s.LibraryPath) .ThenInclude(lp => lp.Library) .OrderBy(s => s.Id) @@ -248,6 +254,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories metadata.Tags ??= new List(); metadata.Studios ??= new List(); metadata.Actors ??= new List(); + metadata.Guids ??= new List(); var show = new Show { LibraryPathId = libraryPathId, @@ -274,6 +281,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories Option maybeExisting = await dbContext.Seasons .Include(s => s.SeasonMetadata) .ThenInclude(sm => sm.Artwork) + .Include(s => s.SeasonMetadata) + .ThenInclude(sm => sm.Guids) .OrderBy(s => s.ShowId) .ThenBy(s => s.SeasonNumber) .SingleOrDefaultAsync(s => s.ShowId == show.Id && s.SeasonNumber == seasonNumber); @@ -293,12 +302,27 @@ namespace ErsatzTV.Infrastructure.Data.Repositories .Include(i => i.EpisodeMetadata) .ThenInclude(em => em.Artwork) .Include(i => i.EpisodeMetadata) + .ThenInclude(em => em.Genres) + .Include(i => i.EpisodeMetadata) + .ThenInclude(em => em.Tags) + .Include(i => i.EpisodeMetadata) + .ThenInclude(em => em.Studios) + .Include(i => i.EpisodeMetadata) .ThenInclude(em => em.Actors) .ThenInclude(a => a.Artwork) + .Include(i => i.EpisodeMetadata) + .ThenInclude(em => em.Guids) + .Include(i => i.EpisodeMetadata) + .ThenInclude(mm => mm.Directors) + .Include(i => i.EpisodeMetadata) + .ThenInclude(mm => mm.Writers) .Include(i => i.MediaVersions) .ThenInclude(mv => mv.MediaFiles) .Include(i => i.MediaVersions) .ThenInclude(mv => mv.Streams) + .Include(i => i.LibraryPath) + .ThenInclude(lp => lp.Library) + .Include(i => i.Season) .OrderBy(i => i.MediaVersions.First().MediaFiles.First().Path) .SingleOrDefaultAsync(i => i.MediaVersions.First().MediaFiles.First().Path == path); @@ -398,6 +422,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories .ThenInclude(a => a.Artwork) .Include(i => i.ShowMetadata) .ThenInclude(sm => sm.Artwork) + .Include(i => i.ShowMetadata) + .ThenInclude(sm => sm.Guids) .Include(i => i.LibraryPath) .ThenInclude(lp => lp.Library) .OrderBy(i => i.Key) @@ -415,7 +441,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories Option maybeExisting = await dbContext.PlexSeasons .AsNoTracking() .Include(i => i.SeasonMetadata) - .ThenInclude(mm => mm.Artwork) + .ThenInclude(sm => sm.Artwork) + .Include(i => i.SeasonMetadata) + .ThenInclude(sm => sm.Guids) .OrderBy(i => i.Key) .SingleOrDefaultAsync(i => i.Key == item.Key); @@ -431,6 +459,16 @@ namespace ErsatzTV.Infrastructure.Data.Repositories .AsNoTracking() .Include(i => i.EpisodeMetadata) .ThenInclude(mm => mm.Artwork) + .Include(i => i.EpisodeMetadata) + .ThenInclude(mm => mm.Genres) + .Include(i => i.EpisodeMetadata) + .ThenInclude(mm => mm.Tags) + .Include(i => i.EpisodeMetadata) + .ThenInclude(mm => mm.Studios) + .Include(i => i.EpisodeMetadata) + .ThenInclude(mm => mm.Directors) + .Include(i => i.EpisodeMetadata) + .ThenInclude(mm => mm.Writers) .Include(i => i.MediaVersions) .ThenInclude(mv => mv.MediaFiles) .Include(i => i.MediaVersions) @@ -438,6 +476,11 @@ namespace ErsatzTV.Infrastructure.Data.Repositories .Include(e => e.EpisodeMetadata) .ThenInclude(em => em.Actors) .ThenInclude(a => a.Artwork) + .Include(e => e.EpisodeMetadata) + .ThenInclude(em => em.Guids) + .Include(i => i.LibraryPath) + .ThenInclude(lp => lp.Library) + .Include(e => e.Season) .OrderBy(i => i.Key) .SingleOrDefaultAsync(i => i.Key == item.Key); @@ -456,15 +499,27 @@ namespace ErsatzTV.Infrastructure.Data.Repositories WHERE P.Key = @ShowKey AND ps.Key not in @Keys)", new { ShowKey = showKey, Keys = seasonKeys }).ToUnit(); - public Task RemoveMissingPlexEpisodes(string seasonKey, List episodeKeys) => - _dbConnection.ExecuteAsync( + public async Task> RemoveMissingPlexEpisodes(string seasonKey, List episodeKeys) + { + List ids = await _dbConnection.QueryAsync( + @"SELECT m.Id FROM MediaItem m + INNER JOIN Episode e ON m.Id = e.Id + INNER JOIN PlexEpisode pe ON pe.Id = m.Id + INNER JOIN PlexSeason P on P.Id = e.SeasonId + WHERE P.Key = @SeasonKey AND pe.Key not in @Keys", + new { SeasonKey = seasonKey, Keys = episodeKeys }).Map(result => result.ToList()); + + await _dbConnection.ExecuteAsync( @"DELETE FROM MediaItem WHERE Id IN (SELECT m.Id FROM MediaItem m INNER JOIN Episode e ON m.Id = e.Id INNER JOIN PlexEpisode pe ON pe.Id = m.Id INNER JOIN PlexSeason P on P.Id = e.SeasonId WHERE P.Key = @SeasonKey AND pe.Key not in @Keys)", - new { SeasonKey = seasonKey, Keys = episodeKeys }).ToUnit(); + new { SeasonKey = seasonKey, Keys = episodeKeys }); + + return ids; + } public async Task SetEpisodeNumber(Episode episode, int episodeNumber) { @@ -475,6 +530,16 @@ namespace ErsatzTV.Infrastructure.Data.Repositories return Unit.Default; } + public Task AddDirector(EpisodeMetadata metadata, Director director) => + _dbConnection.ExecuteAsync( + "INSERT INTO Director (Name, EpisodeMetadataId) VALUES (@Name, @MetadataId)", + new { director.Name, MetadataId = metadata.Id }).Map(result => result > 0); + + public Task AddWriter(EpisodeMetadata metadata, Writer writer) => + _dbConnection.ExecuteAsync( + "INSERT INTO Writer (Name, EpisodeMetadataId) VALUES (@Name, @MetadataId)", + new { writer.Name, MetadataId = metadata.Id }).Map(result => result > 0); + public async Task> GetShowItems(int showId) { IEnumerable ids = await _dbConnection.QueryAsync( @@ -613,7 +678,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories { new() { - DateAdded = DateTime.UtcNow + DateAdded = DateTime.UtcNow, + Guids = new List() } } }; @@ -651,7 +717,13 @@ namespace ErsatzTV.Infrastructure.Data.Repositories DateAdded = DateTime.UtcNow, DateUpdated = DateTime.MinValue, MetadataKind = MetadataKind.Fallback, - Actors = new List() + Actors = new List(), + Guids = new List(), + Writers = new List(), + Directors = new List(), + Genres = new List(), + Tags = new List(), + Studios = new List() } }, MediaVersions = new List @@ -668,6 +740,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories }; await dbContext.Episodes.AddAsync(episode); await dbContext.SaveChangesAsync(); + await dbContext.Entry(episode).Reference(i => i.LibraryPath).LoadAsync(); + await dbContext.Entry(episode.LibraryPath).Reference(lp => lp.Library).LoadAsync(); + await dbContext.Entry(episode).Reference(e => e.Season).LoadAsync(); return episode; } catch (Exception ex) @@ -730,10 +805,19 @@ namespace ErsatzTV.Infrastructure.Data.Repositories } item.LibraryPathId = library.Paths.Head().Id; + EpisodeMetadata metadata = item.EpisodeMetadata.Head(); + metadata.Genres ??= new List(); + metadata.Tags ??= new List(); + metadata.Studios ??= new List(); + metadata.Actors ??= new List(); + metadata.Directors ??= new List(); + metadata.Writers ??= new List(); await dbContext.PlexEpisodes.AddAsync(item); await dbContext.SaveChangesAsync(); await dbContext.Entry(item).Reference(i => i.LibraryPath).LoadAsync(); + await dbContext.Entry(item.LibraryPath).Reference(lp => lp.Library).LoadAsync(); + await dbContext.Entry(item).Reference(e => e.Season).LoadAsync(); return item; } catch (Exception ex) diff --git a/ErsatzTV.Infrastructure/Emby/EmbyApiClient.cs b/ErsatzTV.Infrastructure/Emby/EmbyApiClient.cs index 08a49ef5c..feefcfb80 100644 --- a/ErsatzTV.Infrastructure/Emby/EmbyApiClient.cs +++ b/ErsatzTV.Infrastructure/Emby/EmbyApiClient.cs @@ -244,7 +244,8 @@ namespace ErsatzTV.Infrastructure.Emby Actors = Optional(item.People).Flatten().Collect(r => ProjectToActor(r, dateAdded)).ToList(), Directors = Optional(item.People).Flatten().Collect(r => ProjectToDirector(r)).ToList(), Writers = Optional(item.People).Flatten().Collect(r => ProjectToWriter(r)).ToList(), - Artwork = new List() + Artwork = new List(), + Guids = GuidsFromProviderIds(item.ProviderIds) }; // set order on actors @@ -365,7 +366,8 @@ namespace ErsatzTV.Infrastructure.Emby Tags = Optional(item.Tags).Flatten().Map(t => new Tag { Name = t }).ToList(), Studios = Optional(item.Studios).Flatten().Map(s => new Studio { Name = s.Name }).ToList(), Actors = Optional(item.People).Flatten().Collect(r => ProjectToActor(r, dateAdded)).ToList(), - Artwork = new List() + Artwork = new List(), + Guids = GuidsFromProviderIds(item.ProviderIds) }; // set order on actors @@ -429,7 +431,8 @@ namespace ErsatzTV.Infrastructure.Emby SortTitle = _fallbackMetadataProvider.GetSortTitle(item.Name), Year = item.ProductionYear, DateAdded = dateAdded, - Artwork = new List() + Artwork = new List(), + Guids = GuidsFromProviderIds(item.ProviderIds) }; if (!string.IsNullOrWhiteSpace(item.ImageTags.Primary)) @@ -540,7 +543,10 @@ namespace ErsatzTV.Infrastructure.Emby Tags = new List(), Studios = new List(), Actors = new List(), - Artwork = new List() + Artwork = new List(), + Guids = GuidsFromProviderIds(item.ProviderIds), + Directors = Optional(item.People).Flatten().Collect(r => ProjectToDirector(r)).ToList(), + Writers = Optional(item.People).Flatten().Collect(r => ProjectToWriter(r)).ToList() }; if (DateTime.TryParse(item.PremiereDate, out DateTime releaseDate)) @@ -561,5 +567,30 @@ namespace ErsatzTV.Infrastructure.Emby return metadata; } + + private List GuidsFromProviderIds(EmbyProviderIdsResponse providerIds) + { + var result = new List(); + + if (providerIds != null) + { + if (!string.IsNullOrWhiteSpace(providerIds.Imdb)) + { + result.Add(new MetadataGuid { Guid = $"imdb://{providerIds.Imdb}" }); + } + + if (!string.IsNullOrWhiteSpace(providerIds.Tmdb)) + { + result.Add(new MetadataGuid { Guid = $"tmdb://{providerIds.Tmdb}" }); + } + + if (!string.IsNullOrWhiteSpace(providerIds.Tvdb)) + { + result.Add(new MetadataGuid { Guid = $"tvdb://{providerIds.Tvdb}" }); + } + } + + return result; + } } } diff --git a/ErsatzTV.Infrastructure/Emby/IEmbyApi.cs b/ErsatzTV.Infrastructure/Emby/IEmbyApi.cs index e4d07efec..2e995f587 100644 --- a/ErsatzTV.Infrastructure/Emby/IEmbyApi.cs +++ b/ErsatzTV.Infrastructure/Emby/IEmbyApi.cs @@ -28,7 +28,7 @@ namespace ErsatzTV.Infrastructure.Emby string parentId, [Query] string fields = - "Path,Genres,Tags,DateCreated,Etag,Overview,Taglines,Studios,People,ProductionYear,PremiereDate,MediaSources,OfficialRating", + "Path,Genres,Tags,DateCreated,Etag,Overview,Taglines,Studios,People,ProductionYear,PremiereDate,MediaSources,OfficialRating,ProviderIds", [Query] string includeItemTypes = "Movie", [Query] @@ -42,7 +42,7 @@ namespace ErsatzTV.Infrastructure.Emby string parentId, [Query] string fields = - "Path,Genres,Tags,DateCreated,Etag,Overview,Taglines,Studios,People,ProductionYear,PremiereDate,MediaSources,OfficialRating", + "Path,Genres,Tags,DateCreated,Etag,Overview,Taglines,Studios,People,ProductionYear,PremiereDate,MediaSources,OfficialRating,ProviderIds", [Query] string includeItemTypes = "Series", [Query] @@ -55,10 +55,12 @@ namespace ErsatzTV.Infrastructure.Emby [Query] string parentId, [Query] - string fields = "Path,DateCreated,Etag,Taglines", + string fields = "Path,DateCreated,Etag,Taglines,ProviderIds", [Query] string includeItemTypes = "Season", [Query] + string excludeLocationTypes = "Virtual", + [Query] bool recursive = true); [Get("/Items")] @@ -68,10 +70,13 @@ namespace ErsatzTV.Infrastructure.Emby [Query] string parentId, [Query] - string fields = "Path,DateCreated,Etag,Overview,ProductionYear,PremiereDate,MediaSources,LocationType", + string fields = + "Path,DateCreated,Etag,Overview,ProductionYear,PremiereDate,MediaSources,LocationType,ProviderIds,People", [Query] string includeItemTypes = "Episode", [Query] + string excludeLocationTypes = "Virtual", + [Query] bool recursive = true); } } diff --git a/ErsatzTV.Infrastructure/Emby/Models/EmbyLibraryItemResponse.cs b/ErsatzTV.Infrastructure/Emby/Models/EmbyLibraryItemResponse.cs index 443559c4d..32ecd45e1 100644 --- a/ErsatzTV.Infrastructure/Emby/Models/EmbyLibraryItemResponse.cs +++ b/ErsatzTV.Infrastructure/Emby/Models/EmbyLibraryItemResponse.cs @@ -15,6 +15,7 @@ namespace ErsatzTV.Infrastructure.Emby.Models public List Genres { get; set; } public List Tags { get; set; } public int ProductionYear { get; set; } + public EmbyProviderIdsResponse ProviderIds { get; set; } public string PremiereDate { get; set; } public List MediaStreams { get; set; } public List MediaSources { get; set; } diff --git a/ErsatzTV.Infrastructure/Emby/Models/EmbyProviderIdsResponse.cs b/ErsatzTV.Infrastructure/Emby/Models/EmbyProviderIdsResponse.cs new file mode 100644 index 000000000..30ad8e471 --- /dev/null +++ b/ErsatzTV.Infrastructure/Emby/Models/EmbyProviderIdsResponse.cs @@ -0,0 +1,9 @@ +namespace ErsatzTV.Infrastructure.Emby.Models +{ + public class EmbyProviderIdsResponse + { + public string Imdb { get; set; } + public string Tmdb { get; set; } + public string Tvdb { get; set; } + } +} diff --git a/ErsatzTV.Infrastructure/Jellyfin/IJellyfinApi.cs b/ErsatzTV.Infrastructure/Jellyfin/IJellyfinApi.cs index 8d9e88f77..ba2a5d63a 100644 --- a/ErsatzTV.Infrastructure/Jellyfin/IJellyfinApi.cs +++ b/ErsatzTV.Infrastructure/Jellyfin/IJellyfinApi.cs @@ -34,7 +34,8 @@ namespace ErsatzTV.Infrastructure.Jellyfin [Query] string parentId, [Query] - string fields = "Path,Genres,Tags,DateCreated,Etag,Overview,Taglines,Studios,People,OfficialRating", + string fields = + "Path,Genres,Tags,DateCreated,Etag,Overview,Taglines,Studios,People,OfficialRating,ProviderIds", [Query] string includeItemTypes = "Movie", [Query] @@ -49,7 +50,8 @@ namespace ErsatzTV.Infrastructure.Jellyfin [Query] string parentId, [Query] - string fields = "Path,Genres,Tags,DateCreated,Etag,Overview,Taglines,Studios,People,OfficialRating", + string fields = + "Path,Genres,Tags,DateCreated,Etag,Overview,Taglines,Studios,People,OfficialRating,ProviderIds", [Query] string includeItemTypes = "Series", [Query] @@ -64,7 +66,7 @@ namespace ErsatzTV.Infrastructure.Jellyfin [Query] string parentId, [Query] - string fields = "Path,DateCreated,Etag,Taglines", + string fields = "Path,DateCreated,Etag,Taglines,ProviderIds", [Query] string includeItemTypes = "Season", [Query] @@ -79,7 +81,7 @@ namespace ErsatzTV.Infrastructure.Jellyfin [Query] string parentId, [Query] - string fields = "Path,DateCreated,Etag,Overview", + string fields = "Path,DateCreated,Etag,Overview,ProviderIds,People", [Query] string includeItemTypes = "Episode", [Query] diff --git a/ErsatzTV.Infrastructure/Jellyfin/JellyfinApiClient.cs b/ErsatzTV.Infrastructure/Jellyfin/JellyfinApiClient.cs index 69fcc1cbf..7097abcb2 100644 --- a/ErsatzTV.Infrastructure/Jellyfin/JellyfinApiClient.cs +++ b/ErsatzTV.Infrastructure/Jellyfin/JellyfinApiClient.cs @@ -290,7 +290,8 @@ namespace ErsatzTV.Infrastructure.Jellyfin Actors = Optional(item.People).Flatten().Collect(r => ProjectToActor(r, dateAdded)).ToList(), Directors = Optional(item.People).Flatten().Collect(r => ProjectToDirector(r)).ToList(), Writers = Optional(item.People).Flatten().Collect(r => ProjectToWriter(r)).ToList(), - Artwork = new List() + Artwork = new List(), + Guids = GuidsFromProviderIds(item.ProviderIds) }; // set order on actors @@ -416,7 +417,8 @@ namespace ErsatzTV.Infrastructure.Jellyfin Tags = Optional(item.Tags).Flatten().Map(t => new Tag { Name = t }).ToList(), Studios = Optional(item.Studios).Flatten().Map(s => new Studio { Name = s.Name }).ToList(), Actors = Optional(item.People).Flatten().Collect(r => ProjectToActor(r, dateAdded)).ToList(), - Artwork = new List() + Artwork = new List(), + Guids = GuidsFromProviderIds(item.ProviderIds) }; // set order on actors @@ -485,7 +487,8 @@ namespace ErsatzTV.Infrastructure.Jellyfin SortTitle = _fallbackMetadataProvider.GetSortTitle(item.Name), Year = item.ProductionYear, DateAdded = dateAdded, - Artwork = new List() + Artwork = new List(), + Guids = GuidsFromProviderIds(item.ProviderIds) }; if (!string.IsNullOrWhiteSpace(item.ImageTags.Primary)) @@ -596,7 +599,10 @@ namespace ErsatzTV.Infrastructure.Jellyfin Tags = new List(), Studios = new List(), Actors = new List(), - Artwork = new List() + Artwork = new List(), + Guids = GuidsFromProviderIds(item.ProviderIds), + Directors = Optional(item.People).Flatten().Collect(r => ProjectToDirector(r)).ToList(), + Writers = Optional(item.People).Flatten().Collect(r => ProjectToWriter(r)).ToList() }; if (DateTime.TryParse(item.PremiereDate, out DateTime releaseDate)) @@ -617,5 +623,30 @@ namespace ErsatzTV.Infrastructure.Jellyfin return metadata; } + + private List GuidsFromProviderIds(JellyfinProviderIdsResponse providerIds) + { + var result = new List(); + + if (providerIds != null) + { + if (!string.IsNullOrWhiteSpace(providerIds.Imdb)) + { + result.Add(new MetadataGuid { Guid = $"imdb://{providerIds.Imdb}" }); + } + + if (!string.IsNullOrWhiteSpace(providerIds.Tmdb)) + { + result.Add(new MetadataGuid { Guid = $"tmdb://{providerIds.Tmdb}" }); + } + + if (!string.IsNullOrWhiteSpace(providerIds.Tvdb)) + { + result.Add(new MetadataGuid { Guid = $"tvdb://{providerIds.Tvdb}" }); + } + } + + return result; + } } } diff --git a/ErsatzTV.Infrastructure/Jellyfin/Models/JellyfinLibraryItemResponse.cs b/ErsatzTV.Infrastructure/Jellyfin/Models/JellyfinLibraryItemResponse.cs index 633e7e17a..06fdf5a91 100644 --- a/ErsatzTV.Infrastructure/Jellyfin/Models/JellyfinLibraryItemResponse.cs +++ b/ErsatzTV.Infrastructure/Jellyfin/Models/JellyfinLibraryItemResponse.cs @@ -15,6 +15,7 @@ namespace ErsatzTV.Infrastructure.Jellyfin.Models public List Genres { get; set; } public List Tags { get; set; } public int ProductionYear { get; set; } + public JellyfinProviderIdsResponse ProviderIds { get; set; } public string PremiereDate { get; set; } public List MediaStreams { get; set; } public string LocationType { get; set; } diff --git a/ErsatzTV.Infrastructure/Jellyfin/Models/JellyfinProviderIdsResponse.cs b/ErsatzTV.Infrastructure/Jellyfin/Models/JellyfinProviderIdsResponse.cs new file mode 100644 index 000000000..efaca5060 --- /dev/null +++ b/ErsatzTV.Infrastructure/Jellyfin/Models/JellyfinProviderIdsResponse.cs @@ -0,0 +1,9 @@ +namespace ErsatzTV.Infrastructure.Jellyfin.Models +{ + public class JellyfinProviderIdsResponse + { + public string Imdb { get; set; } + public string Tmdb { get; set; } + public string Tvdb { get; set; } + } +} diff --git a/ErsatzTV.Infrastructure/Migrations/20210527214039_Add_MetadataGuid.Designer.cs b/ErsatzTV.Infrastructure/Migrations/20210527214039_Add_MetadataGuid.Designer.cs new file mode 100644 index 000000000..238e16ead --- /dev/null +++ b/ErsatzTV.Infrastructure/Migrations/20210527214039_Add_MetadataGuid.Designer.cs @@ -0,0 +1,2862 @@ +// +using System; +using ErsatzTV.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace ErsatzTV.Infrastructure.Migrations +{ + [DbContext(typeof(TvContext))] + [Migration("20210527214039_Add_MetadataGuid")] + partial class Add_MetadataGuid + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.6"); + + 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("Role") + .HasColumnType("TEXT"); + + b.Property("SeasonMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ShowMetadataId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ArtistMetadataId"); + + b.HasIndex("ArtworkId") + .IsUnique(); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("SeasonMetadataId"); + + b.HasIndex("ShowMetadataId"); + + b.ToTable("Actor"); + }); + + 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"); + }); + + 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("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("Path") + .HasColumnType("TEXT"); + + b.Property("SeasonMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ShowMetadataId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ArtistMetadataId"); + + b.HasIndex("ChannelId"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("SeasonMetadataId"); + + b.HasIndex("ShowMetadataId"); + + b.ToTable("Artwork"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Channel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("FFmpegProfileId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("TEXT"); + + b.Property("PreferredLanguageCode") + .HasColumnType("TEXT"); + + b.Property("StreamingMode") + .HasColumnType("INTEGER"); + + b.Property("UniqueId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("FFmpegProfileId"); + + b.HasIndex("Number") + .IsUnique(); + + b.ToTable("Channel"); + }); + + 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"); + }); + + 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"); + }); + + 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"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Director", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("MovieMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MovieMetadataId"); + + b.ToTable("Director"); + }); + + 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"); + }); + + 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"); + }); + + 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("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"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.FFmpegProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AudioBitrate") + .HasColumnType("INTEGER"); + + b.Property("AudioBufferSize") + .HasColumnType("INTEGER"); + + b.Property("AudioChannels") + .HasColumnType("INTEGER"); + + b.Property("AudioCodec") + .HasColumnType("TEXT"); + + b.Property("AudioSampleRate") + .HasColumnType("INTEGER"); + + b.Property("FrameRate") + .HasColumnType("TEXT"); + + b.Property("HardwareAcceleration") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizeAudio") + .HasColumnType("INTEGER"); + + b.Property("NormalizeLoudness") + .HasColumnType("INTEGER"); + + b.Property("NormalizeVideo") + .HasColumnType("INTEGER"); + + b.Property("ResolutionId") + .HasColumnType("INTEGER"); + + b.Property("ThreadCount") + .HasColumnType("INTEGER"); + + b.Property("Transcode") + .HasColumnType("INTEGER"); + + b.Property("VideoBitrate") + .HasColumnType("INTEGER"); + + b.Property("VideoBufferSize") + .HasColumnType("INTEGER"); + + b.Property("VideoCodec") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ResolutionId"); + + b.ToTable("FFmpegProfile"); + }); + + 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("SeasonMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ShowMetadataId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ArtistMetadataId"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("SeasonMetadataId"); + + b.HasIndex("ShowMetadataId"); + + b.ToTable("Genre"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Address") + .HasColumnType("TEXT"); + + b.Property("JellyfinMediaSourceId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("JellyfinMediaSourceId"); + + b.ToTable("JellyfinConnection"); + }); + + 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"); + }); + + 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"); + }); + + 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"); + }); + + 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"); + }); + + 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"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LibraryPathId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("LibraryPathId"); + + b.ToTable("MediaItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("MediaSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Channels") + .HasColumnType("INTEGER"); + + b.Property("Codec") + .HasColumnType("TEXT"); + + b.Property("Default") + .HasColumnType("INTEGER"); + + b.Property("Forced") + .HasColumnType("INTEGER"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("MediaStreamKind") + .HasColumnType("INTEGER"); + + b.Property("MediaVersionId") + .HasColumnType("INTEGER"); + + b.Property("Profile") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MediaVersionId"); + + b.ToTable("MediaStream"); + }); + + 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("SampleAspectRatio") + .HasColumnType("TEXT"); + + b.Property("VideoScanKind") + .HasColumnType("INTEGER"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("EpisodeId"); + + b.HasIndex("MovieId"); + + b.HasIndex("MusicVideoId"); + + b.ToTable("MediaVersion"); + }); + + 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("SeasonMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ShowMetadataId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ArtistMetadataId"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("SeasonMetadataId"); + + b.HasIndex("ShowMetadataId"); + + b.ToTable("MetadataGuid"); + }); + + 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"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideoMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Album") + .HasColumnType("TEXT"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("MetadataKind") + .HasColumnType("INTEGER"); + + b.Property("MusicVideoId") + .HasColumnType("INTEGER"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("Plot") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("SortTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("MusicVideoId"); + + b.ToTable("MusicVideoMetadata"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("INTEGER"); + + b.Property("ProgramScheduleId") + .HasColumnType("INTEGER"); + + b.Property("ProgramSchedulePlayoutType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChannelId"); + + b.HasIndex("ProgramScheduleId"); + + b.ToTable("Playout"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CustomGroup") + .HasColumnType("INTEGER"); + + b.Property("CustomTitle") + .HasColumnType("TEXT"); + + b.Property("Finish") + .HasColumnType("TEXT"); + + b.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b.Property("PlayoutId") + .HasColumnType("INTEGER"); + + b.Property("Start") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("PlayoutId"); + + b.ToTable("PlayoutItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutProgramScheduleAnchor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CollectionId") + .HasColumnType("INTEGER"); + + b.Property("CollectionType") + .HasColumnType("INTEGER"); + + b.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b.Property("PlayoutId") + .HasColumnType("INTEGER"); + + b.Property("ProgramScheduleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CollectionId"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("PlayoutId"); + + b.HasIndex("ProgramScheduleId"); + + b.ToTable("PlayoutProgramScheduleAnchor"); + }); + + 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"); + }); + + 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"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("KeepMultiPartEpisodesTogether") + .HasColumnType("INTEGER"); + + b.Property("MediaCollectionPlaybackOrder") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("ProgramSchedule"); + }); + + 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("Index") + .HasColumnType("INTEGER"); + + b.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b.Property("ProgramScheduleId") + .HasColumnType("INTEGER"); + + b.Property("StartTime") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CollectionId"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("ProgramScheduleId"); + + b.ToTable("ProgramScheduleItem"); + }); + + 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"); + }); + + 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"); + }); + + 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"); + }); + + 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("SeasonMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ShowMetadataId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ArtistMetadataId"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("SeasonMetadataId"); + + b.HasIndex("ShowMetadataId"); + + b.ToTable("Studio"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Style", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArtistMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ArtistMetadataId"); + + b.ToTable("Style"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArtistMetadataId") + .HasColumnType("INTEGER"); + + b.Property("EpisodeMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MovieMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MusicVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("SeasonMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ShowMetadataId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ArtistMetadataId"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("SeasonMetadataId"); + + b.HasIndex("ShowMetadataId"); + + b.ToTable("Tag"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Writer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("MovieMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MovieMetadataId"); + + b.ToTable("Writer"); + }); + + 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"); + }); + + 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"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LocalLibrary", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Library"); + + b.ToTable("LocalLibrary"); + }); + + 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"); + }); + + 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"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Artist", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.ToTable("Artist"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Episode", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.Property("EpisodeNumber") + .HasColumnType("INTEGER"); + + b.Property("SeasonId") + .HasColumnType("INTEGER"); + + b.HasIndex("SeasonId"); + + b.ToTable("Episode"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Movie", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.ToTable("Movie"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideo", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.Property("ArtistId") + .HasColumnType("INTEGER"); + + b.HasIndex("ArtistId"); + + b.ToTable("MusicVideo"); + }); + + 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"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Show", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.ToTable("Show"); + }); + + 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"); + }); + + 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"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LocalMediaSource", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaSource"); + + b.ToTable("LocalMediaSource"); + }); + + 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"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemDuration", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); + + b.Property("OfflineTail") + .HasColumnType("INTEGER"); + + b.Property("PlayoutDuration") + .HasColumnType("TEXT"); + + b.ToTable("ProgramScheduleDurationItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemFlood", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); + + b.ToTable("ProgramScheduleFloodItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemMultiple", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.ToTable("ProgramScheduleMultipleItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemOne", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); + + b.ToTable("ProgramScheduleOneItem"); + }); + + 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"); + }); + + 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"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexEpisode", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Episode"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.ToTable("PlexEpisode"); + }); + + 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"); + }); + + 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"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMovie", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Movie"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.ToTable("PlexMovie"); + }); + + 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"); + }); + + 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"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexSeason", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Season"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.ToTable("PlexSeason"); + }); + + 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"); + }); + + 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"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexShow", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Show"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.ToTable("PlexShow"); + }); + + 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.SeasonMetadata", null) + .WithMany("Actors") + .HasForeignKey("SeasonMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.ShowMetadata", null) + .WithMany("Actors") + .HasForeignKey("ShowMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Artwork"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ArtistMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.Artist", "Artist") + .WithMany("ArtistMetadata") + .HasForeignKey("ArtistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Artist"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Artwork", b => + { + b.HasOne("ErsatzTV.Core.Domain.ArtistMetadata", null) + .WithMany("Artwork") + .HasForeignKey("ArtistMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.Channel", null) + .WithMany("Artwork") + .HasForeignKey("ChannelId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null) + .WithMany("Artwork") + .HasForeignKey("EpisodeMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MovieMetadata", null) + .WithMany("Artwork") + .HasForeignKey("MovieMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MusicVideoMetadata", null) + .WithMany("Artwork") + .HasForeignKey("MusicVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null) + .WithMany("Artwork") + .HasForeignKey("SeasonMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.ShowMetadata", null) + .WithMany("Artwork") + .HasForeignKey("ShowMetadataId") + .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.Navigation("FFmpegProfile"); + }); + + 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.MovieMetadata", null) + .WithMany("Directors") + .HasForeignKey("MovieMetadataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyConnection", b => + { + b.HasOne("ErsatzTV.Core.Domain.EmbyMediaSource", "EmbyMediaSource") + .WithMany("Connections") + .HasForeignKey("EmbyMediaSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EmbyMediaSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyPathReplacement", b => + { + b.HasOne("ErsatzTV.Core.Domain.EmbyMediaSource", "EmbyMediaSource") + .WithMany("PathReplacements") + .HasForeignKey("EmbyMediaSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EmbyMediaSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EpisodeMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.Episode", "Episode") + .WithMany("EpisodeMetadata") + .HasForeignKey("EpisodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Episode"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.FFmpegProfile", b => + { + b.HasOne("ErsatzTV.Core.Domain.Resolution", "Resolution") + .WithMany() + .HasForeignKey("ResolutionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Resolution"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.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.SeasonMetadata", null) + .WithMany("Genres") + .HasForeignKey("SeasonMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.ShowMetadata", null) + .WithMany("Genres") + .HasForeignKey("ShowMetadataId") + .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.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); + }); + + 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.SeasonMetadata", null) + .WithMany("Guids") + .HasForeignKey("SeasonMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.ShowMetadata", null) + .WithMany("Guids") + .HasForeignKey("ShowMetadataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Mood", b => + { + b.HasOne("ErsatzTV.Core.Domain.ArtistMetadata", null) + .WithMany("Moods") + .HasForeignKey("ArtistMetadataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MovieMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.Movie", "Movie") + .WithMany("MovieMetadata") + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Movie"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.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.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("MultipleRemaining") + .HasColumnType("INTEGER"); + + b1.Property("NextScheduleItemId") + .HasColumnType("INTEGER"); + + b1.Property("NextStart") + .HasColumnType("TEXT"); + + b1.HasKey("PlayoutId"); + + b1.HasIndex("NextScheduleItemId"); + + b1.ToTable("Playout"); + + b1.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", "NextScheduleItem") + .WithMany() + .HasForeignKey("NextScheduleItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b1.WithOwner() + .HasForeignKey("PlayoutId"); + + b1.Navigation("NextScheduleItem"); + }); + + b.Navigation("Anchor"); + + b.Navigation("Channel"); + + b.Navigation("ProgramSchedule"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem") + .WithMany() + .HasForeignKey("MediaItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout") + .WithMany("Items") + .HasForeignKey("PlayoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MediaItem"); + + b.Navigation("Playout"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutProgramScheduleAnchor", b => + { + b.HasOne("ErsatzTV.Core.Domain.Collection", "Collection") + .WithMany() + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem") + .WithMany() + .HasForeignKey("MediaItemId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.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.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("PlayoutProgramScheduleAnchor"); + + b1.WithOwner() + .HasForeignKey("PlayoutProgramScheduleAnchorId"); + }); + + b.Navigation("Collection"); + + b.Navigation("EnumeratorState"); + + b.Navigation("MediaItem"); + + b.Navigation("Playout"); + + b.Navigation("ProgramSchedule"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexConnection", b => + { + b.HasOne("ErsatzTV.Core.Domain.PlexMediaSource", "PlexMediaSource") + .WithMany("Connections") + .HasForeignKey("PlexMediaSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PlexMediaSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexPathReplacement", b => + { + b.HasOne("ErsatzTV.Core.Domain.PlexMediaSource", "PlexMediaSource") + .WithMany("PathReplacements") + .HasForeignKey("PlexMediaSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PlexMediaSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.Collection", "Collection") + .WithMany() + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem") + .WithMany() + .HasForeignKey("MediaItemId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule") + .WithMany("Items") + .HasForeignKey("ProgramScheduleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("MediaItem"); + + b.Navigation("ProgramSchedule"); + }); + + 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.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.SeasonMetadata", null) + .WithMany("Studios") + .HasForeignKey("SeasonMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.ShowMetadata", null) + .WithMany("Studios") + .HasForeignKey("ShowMetadataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Style", b => + { + b.HasOne("ErsatzTV.Core.Domain.ArtistMetadata", null) + .WithMany("Styles") + .HasForeignKey("ArtistMetadataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Tag", b => + { + b.HasOne("ErsatzTV.Core.Domain.ArtistMetadata", null) + .WithMany("Tags") + .HasForeignKey("ArtistMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null) + .WithMany("Tags") + .HasForeignKey("EpisodeMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.MovieMetadata", null) + .WithMany("Tags") + .HasForeignKey("MovieMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MusicVideoMetadata", null) + .WithMany("Tags") + .HasForeignKey("MusicVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null) + .WithMany("Tags") + .HasForeignKey("SeasonMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.ShowMetadata", null) + .WithMany("Tags") + .HasForeignKey("ShowMetadataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Writer", b => + { + b.HasOne("ErsatzTV.Core.Domain.MovieMetadata", null) + .WithMany("Writers") + .HasForeignKey("MovieMetadataId") + .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.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.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("Tags"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Channel", b => + { + b.Navigation("Artwork"); + + b.Navigation("Playouts"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Collection", b => + { + b.Navigation("CollectionItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EpisodeMetadata", b => + { + b.Navigation("Actors"); + + b.Navigation("Artwork"); + + b.Navigation("Genres"); + + b.Navigation("Guids"); + + b.Navigation("Studios"); + + b.Navigation("Tags"); + }); + + 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"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaSource", b => + { + b.Navigation("Libraries"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaVersion", b => + { + b.Navigation("MediaFiles"); + + b.Navigation("Streams"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MovieMetadata", b => + { + b.Navigation("Actors"); + + b.Navigation("Artwork"); + + b.Navigation("Directors"); + + b.Navigation("Genres"); + + b.Navigation("Guids"); + + b.Navigation("Studios"); + + b.Navigation("Tags"); + + b.Navigation("Writers"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideoMetadata", b => + { + b.Navigation("Actors"); + + b.Navigation("Artwork"); + + b.Navigation("Genres"); + + b.Navigation("Guids"); + + b.Navigation("Studios"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b => + { + b.Navigation("Items"); + + b.Navigation("ProgramScheduleAnchors"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramSchedule", b => + { + b.Navigation("Items"); + + b.Navigation("Playouts"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.SeasonMetadata", b => + { + b.Navigation("Actors"); + + b.Navigation("Artwork"); + + b.Navigation("Genres"); + + b.Navigation("Guids"); + + b.Navigation("Studios"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ShowMetadata", b => + { + b.Navigation("Actors"); + + b.Navigation("Artwork"); + + b.Navigation("Genres"); + + b.Navigation("Guids"); + + b.Navigation("Studios"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.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.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.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/20210527214039_Add_MetadataGuid.cs b/ErsatzTV.Infrastructure/Migrations/20210527214039_Add_MetadataGuid.cs new file mode 100644 index 000000000..487f778da --- /dev/null +++ b/ErsatzTV.Infrastructure/Migrations/20210527214039_Add_MetadataGuid.cs @@ -0,0 +1,124 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace ErsatzTV.Infrastructure.Migrations +{ + public partial class Add_MetadataGuid : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + // local and plex + migrationBuilder.Sql("UPDATE MovieMetadata SET DateUpdated = '0001-01-01 00:00:00'"); + migrationBuilder.Sql("UPDATE ShowMetadata SET DateUpdated = '0001-01-01 00:00:00'"); + migrationBuilder.Sql("UPDATE SeasonMetadata SET DateUpdated = '0001-01-01 00:00:00'"); + migrationBuilder.Sql("UPDATE EpisodeMetadata SET DateUpdated = '0001-01-01 00:00:00'"); + migrationBuilder.Sql( + @"UPDATE LibraryFolder SET Etag = NULL WHERE LibraryPathId IN + (SELECT LibraryPathId FROM LibraryPath LP + INNER JOIN Library L on LP.LibraryId = L.Id + WHERE L.MediaKind = 1)"); + + // emby + migrationBuilder.Sql("UPDATE EmbyMovie SET Etag = NULL"); + migrationBuilder.Sql("UPDATE EmbyShow SET Etag = NULL"); + migrationBuilder.Sql("UPDATE EmbySeason SET Etag = NULL"); + migrationBuilder.Sql("UPDATE EmbyEpisode SET Etag = NULL"); + + // jellyfin + migrationBuilder.Sql("UPDATE JellyfinMovie SET Etag = NULL"); + migrationBuilder.Sql("UPDATE JellyfinShow SET Etag = NULL"); + migrationBuilder.Sql("UPDATE JellyfinSeason SET Etag = NULL"); + migrationBuilder.Sql("UPDATE JellyfinEpisode SET Etag = NULL"); + + migrationBuilder.CreateTable( + name: "MetadataGuid", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Guid = table.Column(type: "TEXT", nullable: true), + ArtistMetadataId = table.Column(type: "INTEGER", nullable: true), + EpisodeMetadataId = table.Column(type: "INTEGER", nullable: true), + MovieMetadataId = table.Column(type: "INTEGER", nullable: true), + MusicVideoMetadataId = table.Column(type: "INTEGER", nullable: true), + SeasonMetadataId = table.Column(type: "INTEGER", nullable: true), + ShowMetadataId = table.Column(type: "INTEGER", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_MetadataGuid", x => x.Id); + table.ForeignKey( + name: "FK_MetadataGuid_ArtistMetadata_ArtistMetadataId", + column: x => x.ArtistMetadataId, + principalTable: "ArtistMetadata", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_MetadataGuid_EpisodeMetadata_EpisodeMetadataId", + column: x => x.EpisodeMetadataId, + principalTable: "EpisodeMetadata", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_MetadataGuid_MovieMetadata_MovieMetadataId", + column: x => x.MovieMetadataId, + principalTable: "MovieMetadata", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_MetadataGuid_MusicVideoMetadata_MusicVideoMetadataId", + column: x => x.MusicVideoMetadataId, + principalTable: "MusicVideoMetadata", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_MetadataGuid_SeasonMetadata_SeasonMetadataId", + column: x => x.SeasonMetadataId, + principalTable: "SeasonMetadata", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_MetadataGuid_ShowMetadata_ShowMetadataId", + column: x => x.ShowMetadataId, + principalTable: "ShowMetadata", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_MetadataGuid_ArtistMetadataId", + table: "MetadataGuid", + column: "ArtistMetadataId"); + + migrationBuilder.CreateIndex( + name: "IX_MetadataGuid_EpisodeMetadataId", + table: "MetadataGuid", + column: "EpisodeMetadataId"); + + migrationBuilder.CreateIndex( + name: "IX_MetadataGuid_MovieMetadataId", + table: "MetadataGuid", + column: "MovieMetadataId"); + + migrationBuilder.CreateIndex( + name: "IX_MetadataGuid_MusicVideoMetadataId", + table: "MetadataGuid", + column: "MusicVideoMetadataId"); + + migrationBuilder.CreateIndex( + name: "IX_MetadataGuid_SeasonMetadataId", + table: "MetadataGuid", + column: "SeasonMetadataId"); + + migrationBuilder.CreateIndex( + name: "IX_MetadataGuid_ShowMetadataId", + table: "MetadataGuid", + column: "ShowMetadataId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "MetadataGuid"); + } + } +} diff --git a/ErsatzTV.Infrastructure/Migrations/20210529014509_Add_EpisodeMetadataDirectorsWriters.Designer.cs b/ErsatzTV.Infrastructure/Migrations/20210529014509_Add_EpisodeMetadataDirectorsWriters.Designer.cs new file mode 100644 index 000000000..205fe8f63 --- /dev/null +++ b/ErsatzTV.Infrastructure/Migrations/20210529014509_Add_EpisodeMetadataDirectorsWriters.Designer.cs @@ -0,0 +1,2886 @@ +// +using System; +using ErsatzTV.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace ErsatzTV.Infrastructure.Migrations +{ + [DbContext(typeof(TvContext))] + [Migration("20210529014509_Add_EpisodeMetadataDirectorsWriters")] + partial class Add_EpisodeMetadataDirectorsWriters + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.6"); + + 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("Role") + .HasColumnType("TEXT"); + + b.Property("SeasonMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ShowMetadataId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ArtistMetadataId"); + + b.HasIndex("ArtworkId") + .IsUnique(); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("SeasonMetadataId"); + + b.HasIndex("ShowMetadataId"); + + b.ToTable("Actor"); + }); + + 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"); + }); + + 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("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("Path") + .HasColumnType("TEXT"); + + b.Property("SeasonMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ShowMetadataId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ArtistMetadataId"); + + b.HasIndex("ChannelId"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("SeasonMetadataId"); + + b.HasIndex("ShowMetadataId"); + + b.ToTable("Artwork"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Channel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("FFmpegProfileId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("TEXT"); + + b.Property("PreferredLanguageCode") + .HasColumnType("TEXT"); + + b.Property("StreamingMode") + .HasColumnType("INTEGER"); + + b.Property("UniqueId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("FFmpegProfileId"); + + b.HasIndex("Number") + .IsUnique(); + + b.ToTable("Channel"); + }); + + 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"); + }); + + 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"); + }); + + 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"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Director", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("EpisodeMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MovieMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.ToTable("Director"); + }); + + 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"); + }); + + 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"); + }); + + 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("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"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.FFmpegProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AudioBitrate") + .HasColumnType("INTEGER"); + + b.Property("AudioBufferSize") + .HasColumnType("INTEGER"); + + b.Property("AudioChannels") + .HasColumnType("INTEGER"); + + b.Property("AudioCodec") + .HasColumnType("TEXT"); + + b.Property("AudioSampleRate") + .HasColumnType("INTEGER"); + + b.Property("FrameRate") + .HasColumnType("TEXT"); + + b.Property("HardwareAcceleration") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizeAudio") + .HasColumnType("INTEGER"); + + b.Property("NormalizeLoudness") + .HasColumnType("INTEGER"); + + b.Property("NormalizeVideo") + .HasColumnType("INTEGER"); + + b.Property("ResolutionId") + .HasColumnType("INTEGER"); + + b.Property("ThreadCount") + .HasColumnType("INTEGER"); + + b.Property("Transcode") + .HasColumnType("INTEGER"); + + b.Property("VideoBitrate") + .HasColumnType("INTEGER"); + + b.Property("VideoBufferSize") + .HasColumnType("INTEGER"); + + b.Property("VideoCodec") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ResolutionId"); + + b.ToTable("FFmpegProfile"); + }); + + 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("SeasonMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ShowMetadataId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ArtistMetadataId"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("SeasonMetadataId"); + + b.HasIndex("ShowMetadataId"); + + b.ToTable("Genre"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Address") + .HasColumnType("TEXT"); + + b.Property("JellyfinMediaSourceId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("JellyfinMediaSourceId"); + + b.ToTable("JellyfinConnection"); + }); + + 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"); + }); + + 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"); + }); + + 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"); + }); + + 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"); + }); + + 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"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LibraryPathId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("LibraryPathId"); + + b.ToTable("MediaItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("MediaSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Channels") + .HasColumnType("INTEGER"); + + b.Property("Codec") + .HasColumnType("TEXT"); + + b.Property("Default") + .HasColumnType("INTEGER"); + + b.Property("Forced") + .HasColumnType("INTEGER"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("MediaStreamKind") + .HasColumnType("INTEGER"); + + b.Property("MediaVersionId") + .HasColumnType("INTEGER"); + + b.Property("Profile") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MediaVersionId"); + + b.ToTable("MediaStream"); + }); + + 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("SampleAspectRatio") + .HasColumnType("TEXT"); + + b.Property("VideoScanKind") + .HasColumnType("INTEGER"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("EpisodeId"); + + b.HasIndex("MovieId"); + + b.HasIndex("MusicVideoId"); + + b.ToTable("MediaVersion"); + }); + + 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("SeasonMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ShowMetadataId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ArtistMetadataId"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("SeasonMetadataId"); + + b.HasIndex("ShowMetadataId"); + + b.ToTable("MetadataGuid"); + }); + + 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"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideoMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Album") + .HasColumnType("TEXT"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("MetadataKind") + .HasColumnType("INTEGER"); + + b.Property("MusicVideoId") + .HasColumnType("INTEGER"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("Plot") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("SortTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("MusicVideoId"); + + b.ToTable("MusicVideoMetadata"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("INTEGER"); + + b.Property("ProgramScheduleId") + .HasColumnType("INTEGER"); + + b.Property("ProgramSchedulePlayoutType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChannelId"); + + b.HasIndex("ProgramScheduleId"); + + b.ToTable("Playout"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CustomGroup") + .HasColumnType("INTEGER"); + + b.Property("CustomTitle") + .HasColumnType("TEXT"); + + b.Property("Finish") + .HasColumnType("TEXT"); + + b.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b.Property("PlayoutId") + .HasColumnType("INTEGER"); + + b.Property("Start") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("PlayoutId"); + + b.ToTable("PlayoutItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutProgramScheduleAnchor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CollectionId") + .HasColumnType("INTEGER"); + + b.Property("CollectionType") + .HasColumnType("INTEGER"); + + b.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b.Property("PlayoutId") + .HasColumnType("INTEGER"); + + b.Property("ProgramScheduleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CollectionId"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("PlayoutId"); + + b.HasIndex("ProgramScheduleId"); + + b.ToTable("PlayoutProgramScheduleAnchor"); + }); + + 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"); + }); + + 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"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("KeepMultiPartEpisodesTogether") + .HasColumnType("INTEGER"); + + b.Property("MediaCollectionPlaybackOrder") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("ProgramSchedule"); + }); + + 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("Index") + .HasColumnType("INTEGER"); + + b.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b.Property("ProgramScheduleId") + .HasColumnType("INTEGER"); + + b.Property("StartTime") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CollectionId"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("ProgramScheduleId"); + + b.ToTable("ProgramScheduleItem"); + }); + + 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"); + }); + + 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"); + }); + + 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"); + }); + + 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("SeasonMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ShowMetadataId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ArtistMetadataId"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("SeasonMetadataId"); + + b.HasIndex("ShowMetadataId"); + + b.ToTable("Studio"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Style", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArtistMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ArtistMetadataId"); + + b.ToTable("Style"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArtistMetadataId") + .HasColumnType("INTEGER"); + + b.Property("EpisodeMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MovieMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MusicVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("SeasonMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ShowMetadataId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ArtistMetadataId"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("SeasonMetadataId"); + + b.HasIndex("ShowMetadataId"); + + b.ToTable("Tag"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Writer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("EpisodeMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MovieMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.ToTable("Writer"); + }); + + 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"); + }); + + 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"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LocalLibrary", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Library"); + + b.ToTable("LocalLibrary"); + }); + + 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"); + }); + + 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"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Artist", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.ToTable("Artist"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Episode", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.Property("EpisodeNumber") + .HasColumnType("INTEGER"); + + b.Property("SeasonId") + .HasColumnType("INTEGER"); + + b.HasIndex("SeasonId"); + + b.ToTable("Episode"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Movie", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.ToTable("Movie"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideo", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.Property("ArtistId") + .HasColumnType("INTEGER"); + + b.HasIndex("ArtistId"); + + b.ToTable("MusicVideo"); + }); + + 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"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Show", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.ToTable("Show"); + }); + + 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"); + }); + + 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"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LocalMediaSource", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaSource"); + + b.ToTable("LocalMediaSource"); + }); + + 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"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemDuration", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); + + b.Property("OfflineTail") + .HasColumnType("INTEGER"); + + b.Property("PlayoutDuration") + .HasColumnType("TEXT"); + + b.ToTable("ProgramScheduleDurationItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemFlood", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); + + b.ToTable("ProgramScheduleFloodItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemMultiple", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.ToTable("ProgramScheduleMultipleItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemOne", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); + + b.ToTable("ProgramScheduleOneItem"); + }); + + 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"); + }); + + 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"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexEpisode", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Episode"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.ToTable("PlexEpisode"); + }); + + 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"); + }); + + 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"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMovie", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Movie"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.ToTable("PlexMovie"); + }); + + 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"); + }); + + 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"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexSeason", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Season"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.ToTable("PlexSeason"); + }); + + 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"); + }); + + 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"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexShow", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Show"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.ToTable("PlexShow"); + }); + + 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.SeasonMetadata", null) + .WithMany("Actors") + .HasForeignKey("SeasonMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.ShowMetadata", null) + .WithMany("Actors") + .HasForeignKey("ShowMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Artwork"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ArtistMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.Artist", "Artist") + .WithMany("ArtistMetadata") + .HasForeignKey("ArtistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Artist"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Artwork", b => + { + b.HasOne("ErsatzTV.Core.Domain.ArtistMetadata", null) + .WithMany("Artwork") + .HasForeignKey("ArtistMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.Channel", null) + .WithMany("Artwork") + .HasForeignKey("ChannelId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null) + .WithMany("Artwork") + .HasForeignKey("EpisodeMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MovieMetadata", null) + .WithMany("Artwork") + .HasForeignKey("MovieMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MusicVideoMetadata", null) + .WithMany("Artwork") + .HasForeignKey("MusicVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null) + .WithMany("Artwork") + .HasForeignKey("SeasonMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.ShowMetadata", null) + .WithMany("Artwork") + .HasForeignKey("ShowMetadataId") + .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.Navigation("FFmpegProfile"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.CollectionItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.Collection", "Collection") + .WithMany("CollectionItems") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem") + .WithMany("CollectionItems") + .HasForeignKey("MediaItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("MediaItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Director", b => + { + b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null) + .WithMany("Directors") + .HasForeignKey("EpisodeMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MovieMetadata", null) + .WithMany("Directors") + .HasForeignKey("MovieMetadataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyConnection", b => + { + b.HasOne("ErsatzTV.Core.Domain.EmbyMediaSource", "EmbyMediaSource") + .WithMany("Connections") + .HasForeignKey("EmbyMediaSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EmbyMediaSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyPathReplacement", b => + { + b.HasOne("ErsatzTV.Core.Domain.EmbyMediaSource", "EmbyMediaSource") + .WithMany("PathReplacements") + .HasForeignKey("EmbyMediaSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EmbyMediaSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EpisodeMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.Episode", "Episode") + .WithMany("EpisodeMetadata") + .HasForeignKey("EpisodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Episode"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.FFmpegProfile", b => + { + b.HasOne("ErsatzTV.Core.Domain.Resolution", "Resolution") + .WithMany() + .HasForeignKey("ResolutionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Resolution"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.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.SeasonMetadata", null) + .WithMany("Genres") + .HasForeignKey("SeasonMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.ShowMetadata", null) + .WithMany("Genres") + .HasForeignKey("ShowMetadataId") + .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.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); + }); + + 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.SeasonMetadata", null) + .WithMany("Guids") + .HasForeignKey("SeasonMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.ShowMetadata", null) + .WithMany("Guids") + .HasForeignKey("ShowMetadataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Mood", b => + { + b.HasOne("ErsatzTV.Core.Domain.ArtistMetadata", null) + .WithMany("Moods") + .HasForeignKey("ArtistMetadataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MovieMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.Movie", "Movie") + .WithMany("MovieMetadata") + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Movie"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.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.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("MultipleRemaining") + .HasColumnType("INTEGER"); + + b1.Property("NextScheduleItemId") + .HasColumnType("INTEGER"); + + b1.Property("NextStart") + .HasColumnType("TEXT"); + + b1.HasKey("PlayoutId"); + + b1.HasIndex("NextScheduleItemId"); + + b1.ToTable("Playout"); + + b1.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", "NextScheduleItem") + .WithMany() + .HasForeignKey("NextScheduleItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b1.WithOwner() + .HasForeignKey("PlayoutId"); + + b1.Navigation("NextScheduleItem"); + }); + + b.Navigation("Anchor"); + + b.Navigation("Channel"); + + b.Navigation("ProgramSchedule"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem") + .WithMany() + .HasForeignKey("MediaItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout") + .WithMany("Items") + .HasForeignKey("PlayoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MediaItem"); + + b.Navigation("Playout"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutProgramScheduleAnchor", b => + { + b.HasOne("ErsatzTV.Core.Domain.Collection", "Collection") + .WithMany() + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem") + .WithMany() + .HasForeignKey("MediaItemId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.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.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("PlayoutProgramScheduleAnchor"); + + b1.WithOwner() + .HasForeignKey("PlayoutProgramScheduleAnchorId"); + }); + + b.Navigation("Collection"); + + b.Navigation("EnumeratorState"); + + b.Navigation("MediaItem"); + + b.Navigation("Playout"); + + b.Navigation("ProgramSchedule"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexConnection", b => + { + b.HasOne("ErsatzTV.Core.Domain.PlexMediaSource", "PlexMediaSource") + .WithMany("Connections") + .HasForeignKey("PlexMediaSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PlexMediaSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexPathReplacement", b => + { + b.HasOne("ErsatzTV.Core.Domain.PlexMediaSource", "PlexMediaSource") + .WithMany("PathReplacements") + .HasForeignKey("PlexMediaSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PlexMediaSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.Collection", "Collection") + .WithMany() + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem") + .WithMany() + .HasForeignKey("MediaItemId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule") + .WithMany("Items") + .HasForeignKey("ProgramScheduleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("MediaItem"); + + b.Navigation("ProgramSchedule"); + }); + + 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.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.SeasonMetadata", null) + .WithMany("Studios") + .HasForeignKey("SeasonMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.ShowMetadata", null) + .WithMany("Studios") + .HasForeignKey("ShowMetadataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Style", b => + { + b.HasOne("ErsatzTV.Core.Domain.ArtistMetadata", null) + .WithMany("Styles") + .HasForeignKey("ArtistMetadataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Tag", b => + { + b.HasOne("ErsatzTV.Core.Domain.ArtistMetadata", null) + .WithMany("Tags") + .HasForeignKey("ArtistMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null) + .WithMany("Tags") + .HasForeignKey("EpisodeMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.MovieMetadata", null) + .WithMany("Tags") + .HasForeignKey("MovieMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MusicVideoMetadata", null) + .WithMany("Tags") + .HasForeignKey("MusicVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null) + .WithMany("Tags") + .HasForeignKey("SeasonMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.ShowMetadata", null) + .WithMany("Tags") + .HasForeignKey("ShowMetadataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Writer", b => + { + b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null) + .WithMany("Writers") + .HasForeignKey("EpisodeMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MovieMetadata", null) + .WithMany("Writers") + .HasForeignKey("MovieMetadataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.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.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.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("Tags"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Channel", b => + { + b.Navigation("Artwork"); + + b.Navigation("Playouts"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Collection", b => + { + b.Navigation("CollectionItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EpisodeMetadata", b => + { + b.Navigation("Actors"); + + b.Navigation("Artwork"); + + b.Navigation("Directors"); + + b.Navigation("Genres"); + + b.Navigation("Guids"); + + b.Navigation("Studios"); + + b.Navigation("Tags"); + + b.Navigation("Writers"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Library", b => + { + b.Navigation("Paths"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LibraryPath", b => + { + b.Navigation("LibraryFolders"); + + b.Navigation("MediaItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaItem", b => + { + b.Navigation("CollectionItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaSource", b => + { + b.Navigation("Libraries"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaVersion", b => + { + b.Navigation("MediaFiles"); + + b.Navigation("Streams"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MovieMetadata", b => + { + b.Navigation("Actors"); + + b.Navigation("Artwork"); + + b.Navigation("Directors"); + + b.Navigation("Genres"); + + b.Navigation("Guids"); + + b.Navigation("Studios"); + + b.Navigation("Tags"); + + b.Navigation("Writers"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideoMetadata", b => + { + b.Navigation("Actors"); + + b.Navigation("Artwork"); + + b.Navigation("Genres"); + + b.Navigation("Guids"); + + b.Navigation("Studios"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b => + { + b.Navigation("Items"); + + b.Navigation("ProgramScheduleAnchors"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramSchedule", b => + { + b.Navigation("Items"); + + b.Navigation("Playouts"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.SeasonMetadata", b => + { + b.Navigation("Actors"); + + b.Navigation("Artwork"); + + b.Navigation("Genres"); + + b.Navigation("Guids"); + + b.Navigation("Studios"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ShowMetadata", b => + { + b.Navigation("Actors"); + + b.Navigation("Artwork"); + + b.Navigation("Genres"); + + b.Navigation("Guids"); + + b.Navigation("Studios"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.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.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.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/20210529014509_Add_EpisodeMetadataDirectorsWriters.cs b/ErsatzTV.Infrastructure/Migrations/20210529014509_Add_EpisodeMetadataDirectorsWriters.cs new file mode 100644 index 000000000..7b5251a0a --- /dev/null +++ b/ErsatzTV.Infrastructure/Migrations/20210529014509_Add_EpisodeMetadataDirectorsWriters.cs @@ -0,0 +1,75 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace ErsatzTV.Infrastructure.Migrations +{ + public partial class Add_EpisodeMetadataDirectorsWriters : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "EpisodeMetadataId", + table: "Writer", + type: "INTEGER", + nullable: true); + + migrationBuilder.AddColumn( + name: "EpisodeMetadataId", + table: "Director", + type: "INTEGER", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_Writer_EpisodeMetadataId", + table: "Writer", + column: "EpisodeMetadataId"); + + migrationBuilder.CreateIndex( + name: "IX_Director_EpisodeMetadataId", + table: "Director", + column: "EpisodeMetadataId"); + + migrationBuilder.AddForeignKey( + name: "FK_Director_EpisodeMetadata_EpisodeMetadataId", + table: "Director", + column: "EpisodeMetadataId", + principalTable: "EpisodeMetadata", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Writer_EpisodeMetadata_EpisodeMetadataId", + table: "Writer", + column: "EpisodeMetadataId", + principalTable: "EpisodeMetadata", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Director_EpisodeMetadata_EpisodeMetadataId", + table: "Director"); + + migrationBuilder.DropForeignKey( + name: "FK_Writer_EpisodeMetadata_EpisodeMetadataId", + table: "Writer"); + + migrationBuilder.DropIndex( + name: "IX_Writer_EpisodeMetadataId", + table: "Writer"); + + migrationBuilder.DropIndex( + name: "IX_Director_EpisodeMetadataId", + table: "Director"); + + migrationBuilder.DropColumn( + name: "EpisodeMetadataId", + table: "Writer"); + + migrationBuilder.DropColumn( + name: "EpisodeMetadataId", + table: "Director"); + } + } +} diff --git a/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs b/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs index 51fd56926..d0214148a 100644 --- a/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs +++ b/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs @@ -274,6 +274,9 @@ namespace ErsatzTV.Infrastructure.Migrations .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); + b.Property("EpisodeMetadataId") + .HasColumnType("INTEGER"); + b.Property("MovieMetadataId") .HasColumnType("INTEGER"); @@ -282,6 +285,8 @@ namespace ErsatzTV.Infrastructure.Migrations b.HasKey("Id"); + b.HasIndex("EpisodeMetadataId"); + b.HasIndex("MovieMetadataId"); b.ToTable("Director"); @@ -740,6 +745,50 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("MediaVersion"); }); + 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("SeasonMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ShowMetadataId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ArtistMetadataId"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("SeasonMetadataId"); + + b.HasIndex("ShowMetadataId"); + + b.ToTable("MetadataGuid"); + }); + modelBuilder.Entity("ErsatzTV.Core.Domain.Mood", b => { b.Property("Id") @@ -1281,6 +1330,9 @@ namespace ErsatzTV.Infrastructure.Migrations .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); + b.Property("EpisodeMetadataId") + .HasColumnType("INTEGER"); + b.Property("MovieMetadataId") .HasColumnType("INTEGER"); @@ -1289,6 +1341,8 @@ namespace ErsatzTV.Infrastructure.Migrations b.HasKey("Id"); + b.HasIndex("EpisodeMetadataId"); + b.HasIndex("MovieMetadataId"); b.ToTable("Writer"); @@ -1770,6 +1824,11 @@ namespace ErsatzTV.Infrastructure.Migrations 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") @@ -1957,6 +2016,37 @@ namespace ErsatzTV.Infrastructure.Migrations .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.SeasonMetadata", null) + .WithMany("Guids") + .HasForeignKey("SeasonMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.ShowMetadata", null) + .WithMany("Guids") + .HasForeignKey("ShowMetadataId") + .OnDelete(DeleteBehavior.Cascade); + }); + modelBuilder.Entity("ErsatzTV.Core.Domain.Mood", b => { b.HasOne("ErsatzTV.Core.Domain.ArtistMetadata", null) @@ -2255,6 +2345,11 @@ namespace ErsatzTV.Infrastructure.Migrations 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") @@ -2572,6 +2667,8 @@ namespace ErsatzTV.Infrastructure.Migrations b.Navigation("Genres"); + b.Navigation("Guids"); + b.Navigation("Moods"); b.Navigation("Studios"); @@ -2599,11 +2696,17 @@ namespace ErsatzTV.Infrastructure.Migrations b.Navigation("Artwork"); + b.Navigation("Directors"); + b.Navigation("Genres"); + b.Navigation("Guids"); + b.Navigation("Studios"); b.Navigation("Tags"); + + b.Navigation("Writers"); }); modelBuilder.Entity("ErsatzTV.Core.Domain.Library", b => @@ -2645,6 +2748,8 @@ namespace ErsatzTV.Infrastructure.Migrations b.Navigation("Genres"); + b.Navigation("Guids"); + b.Navigation("Studios"); b.Navigation("Tags"); @@ -2660,6 +2765,8 @@ namespace ErsatzTV.Infrastructure.Migrations b.Navigation("Genres"); + b.Navigation("Guids"); + b.Navigation("Studios"); b.Navigation("Tags"); @@ -2687,6 +2794,8 @@ namespace ErsatzTV.Infrastructure.Migrations b.Navigation("Genres"); + b.Navigation("Guids"); + b.Navigation("Studios"); b.Navigation("Tags"); @@ -2700,6 +2809,8 @@ namespace ErsatzTV.Infrastructure.Migrations b.Navigation("Genres"); + b.Navigation("Guids"); + b.Navigation("Studios"); b.Navigation("Tags"); diff --git a/ErsatzTV.Infrastructure/Plex/IPlexServerApi.cs b/ErsatzTV.Infrastructure/Plex/IPlexServerApi.cs index 4f5554603..d46e66e58 100644 --- a/ErsatzTV.Infrastructure/Plex/IPlexServerApi.cs +++ b/ErsatzTV.Infrastructure/Plex/IPlexServerApi.cs @@ -4,15 +4,16 @@ using Refit; namespace ErsatzTV.Infrastructure.Plex { - [Headers("Accept: application/json")] public interface IPlexServerApi { [Get("/library/sections")] + [Headers("Accept: application/json")] public Task>> GetLibraries( [Query] [AliasAs("X-Plex-Token")] string token); [Get("/library/sections/{key}/all")] + [Headers("Accept: application/json")] public Task>> GetLibrarySectionContents( string key, @@ -20,15 +21,33 @@ namespace ErsatzTV.Infrastructure.Plex string token); [Get("/library/metadata/{key}")] - public Task>> - GetMetadata( + [Headers("Accept: text/xml")] + public Task + GetVideoMetadata( + string key, + [Query] [AliasAs("X-Plex-Token")] + string token); + + [Get("/library/metadata/{key}")] + [Headers("Accept: text/xml")] + public Task + GetDirectoryMetadata( string key, [Query] [AliasAs("X-Plex-Token")] string token); [Get("/library/metadata/{key}/children")] - public Task>> - GetChildren( + [Headers("Accept: text/xml")] + public Task + GetShowChildren( + string key, + [Query] [AliasAs("X-Plex-Token")] + string token); + + [Get("/library/metadata/{key}/children")] + [Headers("Accept: text/xml")] + public Task + GetSeasonChildren( string key, [Query] [AliasAs("X-Plex-Token")] string token); diff --git a/ErsatzTV.Infrastructure/Plex/Models/PlexDirectorResponse.cs b/ErsatzTV.Infrastructure/Plex/Models/PlexDirectorResponse.cs index b4e84808f..7f8179293 100644 --- a/ErsatzTV.Infrastructure/Plex/Models/PlexDirectorResponse.cs +++ b/ErsatzTV.Infrastructure/Plex/Models/PlexDirectorResponse.cs @@ -1,7 +1,10 @@ -namespace ErsatzTV.Infrastructure.Plex.Models +using System.Xml.Serialization; + +namespace ErsatzTV.Infrastructure.Plex.Models { public class PlexDirectorResponse { + [XmlAttribute("tag")] public string Tag { get; set; } } } diff --git a/ErsatzTV.Infrastructure/Plex/Models/PlexGenreResponse.cs b/ErsatzTV.Infrastructure/Plex/Models/PlexGenreResponse.cs index e06e57bad..e963d7878 100644 --- a/ErsatzTV.Infrastructure/Plex/Models/PlexGenreResponse.cs +++ b/ErsatzTV.Infrastructure/Plex/Models/PlexGenreResponse.cs @@ -1,9 +1,16 @@ -namespace ErsatzTV.Infrastructure.Plex.Models +using System.Xml.Serialization; + +namespace ErsatzTV.Infrastructure.Plex.Models { public class PlexGenreResponse { + [XmlAttribute("id")] public int Id { get; set; } + + [XmlAttribute("filter")] public string Filter { get; set; } + + [XmlAttribute("tag")] public string Tag { get; set; } } } diff --git a/ErsatzTV.Infrastructure/Plex/Models/PlexGuidResponse.cs b/ErsatzTV.Infrastructure/Plex/Models/PlexGuidResponse.cs new file mode 100644 index 000000000..61ca35d72 --- /dev/null +++ b/ErsatzTV.Infrastructure/Plex/Models/PlexGuidResponse.cs @@ -0,0 +1,10 @@ +using System.Xml.Serialization; + +namespace ErsatzTV.Infrastructure.Plex.Models +{ + public class PlexGuidResponse + { + [XmlAttribute("id")] + public string Id { get; set; } + } +} diff --git a/ErsatzTV.Infrastructure/Plex/Models/PlexMediaContainerResponse.cs b/ErsatzTV.Infrastructure/Plex/Models/PlexMediaContainerResponse.cs index c39eeca83..766ffe8c7 100644 --- a/ErsatzTV.Infrastructure/Plex/Models/PlexMediaContainerResponse.cs +++ b/ErsatzTV.Infrastructure/Plex/Models/PlexMediaContainerResponse.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Xml.Serialization; namespace ErsatzTV.Infrastructure.Plex.Models { @@ -16,4 +17,32 @@ namespace ErsatzTV.Infrastructure.Plex.Models { public List Metadata { get; set; } } + + [XmlRoot("MediaContainer", Namespace = null)] + public class PlexXmlVideoMetadataResponseContainer + { + [XmlElement("Video")] + public PlexXmlMetadataResponse Metadata { get; set; } + } + + [XmlRoot("MediaContainer", Namespace = null)] + public class PlexXmlDirectoryMetadataResponseContainer + { + [XmlElement("Directory")] + public PlexXmlMetadataResponse Metadata { get; set; } + } + + [XmlRoot("MediaContainer", Namespace = null)] + public class PlexXmlSeasonsMetadataResponseContainer + { + [XmlElement("Directory")] + public List Metadata { get; set; } + } + + [XmlRoot("MediaContainer", Namespace = null)] + public class PlexXmlEpisodesMetadataResponseContainer + { + [XmlElement("Video")] + public List Metadata { get; set; } + } } diff --git a/ErsatzTV.Infrastructure/Plex/Models/PlexMediaResponse.cs b/ErsatzTV.Infrastructure/Plex/Models/PlexMediaResponse.cs index 05c6ba53d..7cf54e4cb 100644 --- a/ErsatzTV.Infrastructure/Plex/Models/PlexMediaResponse.cs +++ b/ErsatzTV.Infrastructure/Plex/Models/PlexMediaResponse.cs @@ -1,21 +1,50 @@ using System.Collections.Generic; +using System.Xml.Serialization; namespace ErsatzTV.Infrastructure.Plex.Models { - public class PlexMediaResponse + public class PlexMediaResponse { + [XmlAttribute("id")] public int Id { get; set; } + + [XmlAttribute("duration")] public int Duration { get; set; } + + [XmlAttribute("bitrate")] public int Bitrate { get; set; } + + [XmlAttribute("width")] public int Width { get; set; } + + [XmlAttribute("height")] public int Height { get; set; } + + [XmlAttribute("aspectRatio")] public double AspectRatio { get; set; } + + [XmlAttribute("audioChannels")] public int AudioChannels { get; set; } + + [XmlAttribute("audioCodec")] public string AudioCodec { get; set; } + + [XmlAttribute("videoCodec")] public string VideoCodec { get; set; } + + [XmlAttribute("videoResulution")] + public string VideoResolution { get; set; } + + [XmlAttribute("videoProfile")] public string VideoProfile { get; set; } + + [XmlAttribute("container")] public string Container { get; set; } + + [XmlAttribute("videoFrameRate")] public string VideoFrameRate { get; set; } - public List Part { get; set; } + + [XmlElement("Part")] + public List Part { get; set; } } } diff --git a/ErsatzTV.Infrastructure/Plex/Models/PlexMetadataResponse.cs b/ErsatzTV.Infrastructure/Plex/Models/PlexMetadataResponse.cs index 0c4c91d38..a4386b75d 100644 --- a/ErsatzTV.Infrastructure/Plex/Models/PlexMetadataResponse.cs +++ b/ErsatzTV.Infrastructure/Plex/Models/PlexMetadataResponse.cs @@ -1,26 +1,74 @@ using System.Collections.Generic; +using System.Xml.Serialization; namespace ErsatzTV.Infrastructure.Plex.Models { public class PlexMetadataResponse { + [XmlAttribute("key")] public string Key { get; set; } + + [XmlAttribute("title")] public string Title { get; set; } + + [XmlAttribute("contentRating")] public string ContentRating { get; set; } + + [XmlAttribute("summary")] public string Summary { get; set; } + + [XmlAttribute("year")] public int Year { get; set; } + + [XmlAttribute("tagline")] public string Tagline { get; set; } + + [XmlAttribute("thumb")] public string Thumb { get; set; } + + [XmlAttribute("art")] public string Art { get; set; } + + [XmlAttribute("originallyAvailableAt")] public string OriginallyAvailableAt { get; set; } + + [XmlAttribute("addedAt")] public long AddedAt { get; set; } + + [XmlAttribute("updatedAt")] public long UpdatedAt { get; set; } + + [XmlAttribute("index")] public int Index { get; set; } + + [XmlAttribute("studio")] public string Studio { get; set; } - public List Media { get; set; } + + [XmlAttribute("rating")] + public double Rating { get; set; } + + [XmlAttribute("audienceRating")] + public double AudienceRating { get; set; } + + [XmlAttribute("audienceRatingImage")] + public string AudienceRatingImage { get; set; } + + [XmlAttribute("ratingImage")] + public string RatingImage { get; set; } + + [XmlIgnore] + public virtual List> Media { get; set; } + + [XmlElement("Genre")] public List Genre { get; set; } + + [XmlElement("Role")] public List Role { get; set; } + + [XmlElement("Director")] public List Director { get; set; } + + [XmlElement("Writer")] public List Writer { get; set; } } } diff --git a/ErsatzTV.Infrastructure/Plex/Models/PlexPartResponse.cs b/ErsatzTV.Infrastructure/Plex/Models/PlexPartResponse.cs index 3cf46d1d9..c8a09a21d 100644 --- a/ErsatzTV.Infrastructure/Plex/Models/PlexPartResponse.cs +++ b/ErsatzTV.Infrastructure/Plex/Models/PlexPartResponse.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; - -namespace ErsatzTV.Infrastructure.Plex.Models +namespace ErsatzTV.Infrastructure.Plex.Models { public class PlexPartResponse { @@ -8,6 +6,5 @@ namespace ErsatzTV.Infrastructure.Plex.Models public string Key { get; set; } public int Duration { get; set; } public string File { get; set; } - public List Stream { get; set; } } } diff --git a/ErsatzTV.Infrastructure/Plex/Models/PlexRoleResponse.cs b/ErsatzTV.Infrastructure/Plex/Models/PlexRoleResponse.cs index f2048321d..c5a7c31d0 100644 --- a/ErsatzTV.Infrastructure/Plex/Models/PlexRoleResponse.cs +++ b/ErsatzTV.Infrastructure/Plex/Models/PlexRoleResponse.cs @@ -1,9 +1,16 @@ -namespace ErsatzTV.Infrastructure.Plex.Models +using System.Xml.Serialization; + +namespace ErsatzTV.Infrastructure.Plex.Models { public class PlexRoleResponse { + [XmlAttribute("tag")] public string Tag { get; set; } + + [XmlAttribute("role")] public string Role { get; set; } + + [XmlAttribute("thumb")] public string Thumb { get; set; } } } diff --git a/ErsatzTV.Infrastructure/Plex/Models/PlexStreamResponse.cs b/ErsatzTV.Infrastructure/Plex/Models/PlexStreamResponse.cs index 8d9619df0..c9e02cf84 100644 --- a/ErsatzTV.Infrastructure/Plex/Models/PlexStreamResponse.cs +++ b/ErsatzTV.Infrastructure/Plex/Models/PlexStreamResponse.cs @@ -1,18 +1,43 @@ -namespace ErsatzTV.Infrastructure.Plex.Models +using System.Xml.Serialization; + +namespace ErsatzTV.Infrastructure.Plex.Models { public class PlexStreamResponse { + [XmlAttribute("id")] public int Id { get; set; } + + [XmlAttribute("index")] public int Index { get; set; } + + [XmlAttribute("default")] public bool Default { get; set; } + + [XmlAttribute("forced")] public bool Forced { get; set; } + + [XmlAttribute("languageCode")] public string LanguageCode { get; set; } + + [XmlAttribute("streamType")] public int StreamType { get; set; } + + [XmlAttribute("codec")] public string Codec { get; set; } + + [XmlAttribute("profile")] public string Profile { get; set; } + + [XmlAttribute("channels")] public int Channels { get; set; } + + [XmlAttribute("anamorphic")] public bool Anamorphic { get; set; } + + [XmlAttribute("pixelAspectRatio")] public string PixelAspectRatio { get; set; } + + [XmlAttribute("scanType")] public string ScanType { get; set; } } } diff --git a/ErsatzTV.Infrastructure/Plex/Models/PlexWriterResponse.mcs.cs b/ErsatzTV.Infrastructure/Plex/Models/PlexWriterResponse.mcs.cs index 4ad565fae..352c5fad7 100644 --- a/ErsatzTV.Infrastructure/Plex/Models/PlexWriterResponse.mcs.cs +++ b/ErsatzTV.Infrastructure/Plex/Models/PlexWriterResponse.mcs.cs @@ -1,7 +1,10 @@ -namespace ErsatzTV.Infrastructure.Plex.Models +using System.Xml.Serialization; + +namespace ErsatzTV.Infrastructure.Plex.Models { public class PlexWriterResponse { + [XmlAttribute("tag")] public string Tag { get; set; } } } diff --git a/ErsatzTV.Infrastructure/Plex/Models/PlexXmlMetadataResponse.cs b/ErsatzTV.Infrastructure/Plex/Models/PlexXmlMetadataResponse.cs new file mode 100644 index 000000000..c91e62fb1 --- /dev/null +++ b/ErsatzTV.Infrastructure/Plex/Models/PlexXmlMetadataResponse.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.Xml.Serialization; + +namespace ErsatzTV.Infrastructure.Plex.Models +{ + public class PlexXmlMetadataResponse : PlexMetadataResponse + { + [XmlAttribute("guid")] + public string PlexGuid { get; set; } + + [XmlElement("Media")] + public new List> Media { get; set; } + + [XmlElement("Guid")] + public List Guid { get; set; } + } +} diff --git a/ErsatzTV.Infrastructure/Plex/Models/PlexXmlPartResponse.cs b/ErsatzTV.Infrastructure/Plex/Models/PlexXmlPartResponse.cs new file mode 100644 index 000000000..6d4d33f2e --- /dev/null +++ b/ErsatzTV.Infrastructure/Plex/Models/PlexXmlPartResponse.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Xml.Serialization; + +namespace ErsatzTV.Infrastructure.Plex.Models +{ + public class PlexXmlPartResponse + { + [XmlAttribute("id")] + public int Id { get; set; } + + [XmlAttribute("key")] + public string Key { get; set; } + + [XmlAttribute("duration")] + public int Duration { get; set; } + + [XmlAttribute("file")] + public string File { get; set; } + + [XmlElement("Stream")] + public List Stream { get; set; } + } +} diff --git a/ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs b/ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs index 72fb62fee..dde480f35 100644 --- a/ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs +++ b/ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using System.Xml.Serialization; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Metadata; @@ -87,9 +88,9 @@ namespace ErsatzTV.Infrastructure.Plex { try { - IPlexServerApi service = RestService.For(connection.Uri); - return await service.GetChildren(show.Key.Split("/").Reverse().Skip(1).Head(), token.AuthToken) - .Map(r => r.MediaContainer.Metadata) + IPlexServerApi service = XmlServiceFor(connection.Uri); + return await service.GetShowChildren(show.Key.Split("/").Reverse().Skip(1).Head(), token.AuthToken) + .Map(r => r.Metadata) .Map(list => list.Map(metadata => ProjectToSeason(metadata, library.MediaSourceId)).ToList()); } catch (Exception ex) @@ -106,9 +107,9 @@ namespace ErsatzTV.Infrastructure.Plex { try { - IPlexServerApi service = RestService.For(connection.Uri); - return await service.GetChildren(season.Key.Split("/").Reverse().Skip(1).Head(), token.AuthToken) - .Map(r => r.MediaContainer.Metadata.Filter(m => m.Media.Count > 0 && m.Media[0].Part.Count > 0)) + IPlexServerApi service = XmlServiceFor(connection.Uri); + return await service.GetSeasonChildren(season.Key.Split("/").Reverse().Skip(1).Head(), token.AuthToken) + .Map(r => r.Metadata.Filter(m => m.Media.Count > 0 && m.Media[0].Part.Count > 0)) .Map(list => list.Map(metadata => ProjectToEpisode(metadata, library.MediaSourceId)).ToList()); } catch (Exception ex) @@ -125,12 +126,13 @@ namespace ErsatzTV.Infrastructure.Plex { try { - IPlexServerApi service = RestService.For(connection.Uri); - return await service.GetMetadata(key, token.AuthToken) + IPlexServerApi service = XmlServiceFor(connection.Uri); + return await service.GetVideoMetadata(key, token.AuthToken) + .Map(Optional) .Map( - r => r.MediaContainer.Metadata.Filter(m => m.Media.Count > 0 && m.Media[0].Part.Count > 0) + r => r.Filter(m => m.Metadata.Media.Count > 0 && m.Metadata.Media[0].Part.Count > 0) .HeadOrNone()) - .MapT(response => ProjectToMovieMetadata(response, library.MediaSourceId)) + .MapT(response => ProjectToMovieMetadata(response.Metadata, library.MediaSourceId)) .Map(o => o.ToEither("Unable to locate metadata")); } catch (Exception ex) @@ -147,10 +149,10 @@ namespace ErsatzTV.Infrastructure.Plex { try { - IPlexServerApi service = RestService.For(connection.Uri); - return await service.GetMetadata(key, token.AuthToken) - .Map(r => r.MediaContainer.Metadata.HeadOrNone()) - .MapT(response => ProjectToShowMetadata(response, library.MediaSourceId)) + IPlexServerApi service = XmlServiceFor(connection.Uri); + return await service.GetDirectoryMetadata(key, token.AuthToken) + .Map(Optional) + .MapT(response => ProjectToShowMetadata(response.Metadata, library.MediaSourceId)) .Map(o => o.ToEither("Unable to locate metadata")); } catch (Exception ex) @@ -159,20 +161,30 @@ namespace ErsatzTV.Infrastructure.Plex } } - public async Task> GetStatistics( + public async Task>> GetMovieMetadataAndStatistics( + PlexLibrary library, string key, PlexConnection connection, PlexServerAuthToken token) { try { - IPlexServerApi service = RestService.For(connection.Uri); - return await service.GetMetadata(key, token.AuthToken) + IPlexServerApi service = XmlServiceFor(connection.Uri); + Option maybeResponse = await service + .GetVideoMetadata(key, token.AuthToken) + .Map(Optional) .Map( - r => r.MediaContainer.Metadata.Filter(m => m.Media.Count > 0 && m.Media[0].Part.Count > 0) - .HeadOrNone()) - .BindT(ProjectToMediaVersion) - .Map(o => o.ToEither("Unable to locate metadata")); + r => r.Filter(m => m.Metadata.Media.Count > 0 && m.Metadata.Media[0].Part.Count > 0) + .HeadOrNone()); + return maybeResponse.Match( + response => + { + Option maybeVersion = ProjectToMediaVersion(response.Metadata); + return maybeVersion.Match>>( + version => Tuple(ProjectToMovieMetadata(response.Metadata, library.MediaSourceId), version), + () => BaseError.New("Unable to locate metadata")); + }, + () => BaseError.New("Unable to locate metadata")); } catch (Exception ex) { @@ -180,6 +192,57 @@ namespace ErsatzTV.Infrastructure.Plex } } + public async Task>> GetEpisodeMetadataAndStatistics( + PlexLibrary library, + string key, + PlexConnection connection, + PlexServerAuthToken token) + { + try + { + IPlexServerApi service = XmlServiceFor(connection.Uri); + Option maybeResponse = await service + .GetVideoMetadata(key, token.AuthToken) + .Map(Optional) + .Map( + r => r.Filter(m => m.Metadata.Media.Count > 0 && m.Metadata.Media[0].Part.Count > 0) + .HeadOrNone()); + return maybeResponse.Match( + response => + { + Option maybeVersion = ProjectToMediaVersion(response.Metadata); + return maybeVersion.Match>>( + version => Tuple( + ProjectToEpisodeMetadata(response.Metadata, library.MediaSourceId), + version), + () => BaseError.New("Unable to locate metadata")); + }, + () => BaseError.New("Unable to locate metadata")); + } + catch (Exception ex) + { + return BaseError.New(ex.ToString()); + } + } + + private static IPlexServerApi XmlServiceFor(string uri) + { + var overrides = new XmlAttributeOverrides(); + var attrs = new XmlAttributes { XmlIgnore = true }; + overrides.Add(typeof(PlexMetadataResponse), "Media", attrs); + + return RestService.For( + uri, + new RefitSettings + { + ContentSerializer = new XmlContentSerializer( + new XmlContentSerializerSettings + { + XmlAttributeOverrides = overrides + }) + }); + } + private static Option Project(PlexLibraryResponse response) => response.Type switch { @@ -205,7 +268,7 @@ namespace ErsatzTV.Infrastructure.Plex private PlexMovie ProjectToMovie(PlexMetadataResponse response, int mediaSourceId) { - PlexMediaResponse media = response.Media.Head(); + PlexMediaResponse media = response.Media.Head(); PlexPartResponse part = media.Part.Head(); DateTime dateAdded = DateTimeOffset.FromUnixTimeSeconds(response.AddedAt).DateTime; DateTime lastWriteTime = DateTimeOffset.FromUnixTimeSeconds(response.UpdatedAt).DateTime; @@ -268,6 +331,23 @@ namespace ErsatzTV.Infrastructure.Plex Writers = Optional(response.Writer).Flatten().Map(w => new Writer { Name = w.Tag }).ToList() }; + if (response is PlexXmlMetadataResponse xml) + { + metadata.Guids = Optional(xml.Guid).Flatten().Map(g => new MetadataGuid { Guid = g.Id }).ToList(); + if (!string.IsNullOrWhiteSpace(xml.PlexGuid)) + { + string normalized = NormalizeGuid(xml.PlexGuid); + if (!string.IsNullOrWhiteSpace(normalized) && metadata.Guids.All(g => g.Guid != normalized)) + { + metadata.Guids.Add(new MetadataGuid { Guid = normalized }); + } + } + } + else + { + metadata.Guids = new List(); + } + if (!string.IsNullOrWhiteSpace(response.Studio)) { metadata.Studios.Add(new Studio { Name = response.Studio }); @@ -311,7 +391,7 @@ namespace ErsatzTV.Infrastructure.Plex return metadata; } - private Option ProjectToMediaVersion(PlexMetadataResponse response) + private Option ProjectToMediaVersion(PlexXmlMetadataResponse response) { List streams = response.Media.Head().Part.Head().Stream; DateTime dateUpdated = DateTimeOffset.FromUnixTimeSeconds(response.UpdatedAt).DateTime; @@ -418,6 +498,23 @@ namespace ErsatzTV.Infrastructure.Plex .ToList() }; + if (response is PlexXmlMetadataResponse xml) + { + metadata.Guids = Optional(xml.Guid).Flatten().Map(g => new MetadataGuid { Guid = g.Id }).ToList(); + if (!string.IsNullOrWhiteSpace(xml.PlexGuid)) + { + string normalized = NormalizeGuid(xml.PlexGuid); + if (!string.IsNullOrWhiteSpace(normalized) && metadata.Guids.All(g => g.Guid != normalized)) + { + metadata.Guids.Add(new MetadataGuid { Guid = normalized }); + } + } + } + else + { + metadata.Guids = new List(); + } + if (!string.IsNullOrWhiteSpace(response.Studio)) { metadata.Studios.Add(new Studio { Name = response.Studio }); @@ -461,7 +558,7 @@ namespace ErsatzTV.Infrastructure.Plex return metadata; } - private PlexSeason ProjectToSeason(PlexMetadataResponse response, int mediaSourceId) + private PlexSeason ProjectToSeason(PlexXmlMetadataResponse response, int mediaSourceId) { DateTime dateAdded = DateTimeOffset.FromUnixTimeSeconds(response.AddedAt).DateTime; DateTime lastWriteTime = DateTimeOffset.FromUnixTimeSeconds(response.UpdatedAt).DateTime; @@ -476,6 +573,16 @@ namespace ErsatzTV.Infrastructure.Plex DateUpdated = lastWriteTime }; + metadata.Guids = Optional(response.Guid).Flatten().Map(g => new MetadataGuid { Guid = g.Id }).ToList(); + if (!string.IsNullOrWhiteSpace(response.PlexGuid)) + { + string normalized = NormalizeGuid(response.PlexGuid); + if (!string.IsNullOrWhiteSpace(normalized) && metadata.Guids.All(g => g.Guid != normalized)) + { + metadata.Guids.Add(new MetadataGuid { Guid = normalized }); + } + } + if (!string.IsNullOrWhiteSpace(response.Thumb)) { var path = $"plex/{mediaSourceId}{response.Thumb}"; @@ -516,47 +623,14 @@ namespace ErsatzTV.Infrastructure.Plex return season; } - private PlexEpisode ProjectToEpisode(PlexMetadataResponse response, int mediaSourceId) + private PlexEpisode ProjectToEpisode(PlexXmlMetadataResponse response, int mediaSourceId) { - PlexMediaResponse media = response.Media.Head(); - PlexPartResponse part = media.Part.Head(); + PlexMediaResponse media = response.Media.Head(); + PlexXmlPartResponse part = media.Part.Head(); DateTime dateAdded = DateTimeOffset.FromUnixTimeSeconds(response.AddedAt).DateTime; DateTime lastWriteTime = DateTimeOffset.FromUnixTimeSeconds(response.UpdatedAt).DateTime; - var metadata = new EpisodeMetadata - { - MetadataKind = MetadataKind.External, - Title = response.Title, - SortTitle = _fallbackMetadataProvider.GetSortTitle(response.Title), - Plot = response.Summary, - Year = response.Year, - Tagline = response.Tagline, - DateAdded = dateAdded, - DateUpdated = lastWriteTime, - Actors = Optional(response.Role).Flatten().Map(r => ProjectToModel(r, dateAdded, lastWriteTime)) - .ToList() - }; - - if (DateTime.TryParse(response.OriginallyAvailableAt, out DateTime releaseDate)) - { - metadata.ReleaseDate = releaseDate; - } - - if (!string.IsNullOrWhiteSpace(response.Thumb)) - { - var path = $"plex/{mediaSourceId}{response.Thumb}"; - var artwork = new Artwork - { - ArtworkKind = ArtworkKind.Thumbnail, - Path = path, - DateAdded = dateAdded, - DateUpdated = lastWriteTime - }; - - metadata.Artwork ??= new List(); - metadata.Artwork.Add(artwork); - } - + EpisodeMetadata metadata = ProjectToEpisodeMetadata(response, mediaSourceId); var version = new MediaVersion { Name = "Main", @@ -589,6 +663,67 @@ namespace ErsatzTV.Infrastructure.Plex return episode; } + private EpisodeMetadata ProjectToEpisodeMetadata(PlexMetadataResponse response, int mediaSourceId) + { + DateTime dateAdded = DateTimeOffset.FromUnixTimeSeconds(response.AddedAt).DateTime; + DateTime lastWriteTime = DateTimeOffset.FromUnixTimeSeconds(response.UpdatedAt).DateTime; + + var metadata = new EpisodeMetadata + { + MetadataKind = MetadataKind.External, + Title = response.Title, + SortTitle = _fallbackMetadataProvider.GetSortTitle(response.Title), + Plot = response.Summary, + Year = response.Year, + Tagline = response.Tagline, + DateAdded = dateAdded, + DateUpdated = lastWriteTime, + Actors = Optional(response.Role).Flatten().Map(r => ProjectToModel(r, dateAdded, lastWriteTime)) + .ToList(), + Directors = Optional(response.Director).Flatten().Map(d => new Director { Name = d.Tag }).ToList(), + Writers = Optional(response.Writer).Flatten().Map(w => new Writer { Name = w.Tag }).ToList() + }; + + if (response is PlexXmlMetadataResponse xml) + { + metadata.Guids = Optional(xml.Guid).Flatten().Map(g => new MetadataGuid { Guid = g.Id }).ToList(); + if (!string.IsNullOrWhiteSpace(xml.PlexGuid)) + { + string normalized = NormalizeGuid(xml.PlexGuid); + if (!string.IsNullOrWhiteSpace(normalized) && metadata.Guids.All(g => g.Guid != normalized)) + { + metadata.Guids.Add(new MetadataGuid { Guid = normalized }); + } + } + } + else + { + metadata.Guids = new List(); + } + + if (DateTime.TryParse(response.OriginallyAvailableAt, out DateTime releaseDate)) + { + metadata.ReleaseDate = releaseDate; + } + + if (!string.IsNullOrWhiteSpace(response.Thumb)) + { + var path = $"plex/{mediaSourceId}{response.Thumb}"; + var artwork = new Artwork + { + ArtworkKind = ArtworkKind.Thumbnail, + Path = path, + DateAdded = dateAdded, + DateUpdated = lastWriteTime + }; + + metadata.Artwork ??= new List(); + metadata.Artwork.Add(artwork); + } + + return metadata; + } + private Actor ProjectToModel(PlexRoleResponse role, DateTime dateAdded, DateTime lastWriteTime) { var actor = new Actor { Name = role.Tag, Role = role.Role }; @@ -605,5 +740,32 @@ namespace ErsatzTV.Infrastructure.Plex return actor; } + + private string NormalizeGuid(string guid) + { + if (guid.StartsWith("plex://show") || + guid.StartsWith("plex://season") || + guid.StartsWith("plex://episode") || + guid.StartsWith("plex://movie")) + { + return guid; + } + + if (guid.StartsWith("com.plexapp.agents.thetvdb")) + { + string strip1 = guid.Replace("com.plexapp.agents.thetvdb://", string.Empty); + string strip2 = strip1.Split("?").Head(); + return $"tvdb://{strip2}"; + } + + if (guid.StartsWith("com.plexapp.agents.themoviedb")) + { + string strip1 = guid.Replace("com.plexapp.agents.themoviedb://", string.Empty); + string strip2 = strip1.Split("?").Head(); + return $"tmdb://{strip2}"; + } + + throw new NotSupportedException(guid); + } } } diff --git a/ErsatzTV.Infrastructure/Search/SearchIndex.cs b/ErsatzTV.Infrastructure/Search/SearchIndex.cs index f4957eff6..dd6b76363 100644 --- a/ErsatzTV.Infrastructure/Search/SearchIndex.cs +++ b/ErsatzTV.Infrastructure/Search/SearchIndex.cs @@ -56,6 +56,7 @@ namespace ErsatzTV.Infrastructure.Search private const string ShowType = "show"; private const string ArtistType = "artist"; private const string MusicVideoType = "music_video"; + private const string EpisodeType = "episode"; private readonly List _cultureInfos; private readonly ILogger _logger; @@ -71,7 +72,7 @@ namespace ErsatzTV.Infrastructure.Search _initialized = false; } - public int Version => 12; + public int Version => 13; public Task Initialize(ILocalFileSystem localFileSystem) { @@ -114,6 +115,9 @@ namespace ErsatzTV.Infrastructure.Search case MusicVideo musicVideo: UpdateMusicVideo(musicVideo); break; + case Episode episode: + UpdateEpisode(episode); + break; } } } @@ -143,6 +147,9 @@ namespace ErsatzTV.Infrastructure.Search case MusicVideo musicVideo: UpdateMusicVideo(musicVideo); break; + case Episode episode: + UpdateEpisode(episode); + break; } } @@ -577,6 +584,83 @@ namespace ErsatzTV.Infrastructure.Search } } + private void UpdateEpisode(Episode episode) + { + Option maybeMetadata = episode.EpisodeMetadata.HeadOrNone(); + if (maybeMetadata.IsSome) + { + EpisodeMetadata metadata = maybeMetadata.ValueUnsafe(); + + try + { + var doc = new Document + { + new StringField(IdField, episode.Id.ToString(), Field.Store.YES), + new StringField(TypeField, EpisodeType, Field.Store.NO), + new TextField(TitleField, metadata.Title, Field.Store.NO), + new StringField(SortTitleField, metadata.SortTitle.ToLowerInvariant(), Field.Store.NO), + new TextField(LibraryNameField, episode.LibraryPath.Library.Name, Field.Store.NO), + new StringField(LibraryIdField, episode.LibraryPath.Library.Id.ToString(), Field.Store.NO), + new StringField(TitleAndYearField, GetTitleAndYear(metadata), Field.Store.NO), + new StringField(JumpLetterField, GetJumpLetter(metadata), Field.Store.YES) + }; + + AddLanguages(doc, episode.MediaVersions); + + if (metadata.ReleaseDate.HasValue) + { + doc.Add( + new StringField( + ReleaseDateField, + metadata.ReleaseDate.Value.ToString("yyyyMMdd"), + Field.Store.NO)); + } + + if (!string.IsNullOrWhiteSpace(metadata.Plot)) + { + doc.Add(new TextField(PlotField, metadata.Plot ?? string.Empty, Field.Store.NO)); + } + + foreach (Genre genre in metadata.Genres) + { + doc.Add(new TextField(GenreField, genre.Name, Field.Store.NO)); + } + + foreach (Tag tag in metadata.Tags) + { + doc.Add(new TextField(TagField, tag.Name, Field.Store.NO)); + } + + foreach (Studio studio in metadata.Studios) + { + doc.Add(new TextField(StudioField, studio.Name, Field.Store.NO)); + } + + foreach (Actor actor in metadata.Actors) + { + doc.Add(new TextField(ActorField, actor.Name, Field.Store.NO)); + } + + foreach (Director director in metadata.Directors) + { + doc.Add(new TextField(DirectorField, director.Name, Field.Store.NO)); + } + + foreach (Writer writer in metadata.Writers) + { + doc.Add(new TextField(WriterField, writer.Name, Field.Store.NO)); + } + + _writer.UpdateDocument(new Term(IdField, episode.Id.ToString()), doc); + } + catch (Exception ex) + { + metadata.Episode = null; + _logger.LogWarning(ex, "Error indexing episode with metadata {@Metadata}", metadata); + } + } + } + private SearchItem ProjectToSearchItem(Document doc) => new(Convert.ToInt32(doc.Get(IdField))); private Query ParseQuery(string searchQuery, QueryParser parser) @@ -595,7 +679,13 @@ namespace ErsatzTV.Infrastructure.Search } private static string GetTitleAndYear(Metadata metadata) => - $"{metadata.Title}_{metadata.Year}".ToLowerInvariant(); + metadata switch + { + EpisodeMetadata em => + $"{em.Title}_{em.Year}_{em.Episode.Season.SeasonNumber}_{em.Episode.EpisodeNumber}" + .ToLowerInvariant(), + _ => $"{metadata.Title}_{metadata.Year}".ToLowerInvariant() + }; private static string GetJumpLetter(Metadata metadata) { diff --git a/ErsatzTV/Pages/EpisodeList.razor b/ErsatzTV/Pages/EpisodeList.razor new file mode 100644 index 000000000..1b1afc7e9 --- /dev/null +++ b/ErsatzTV/Pages/EpisodeList.razor @@ -0,0 +1,166 @@ +@page "/media/tv/episodes" +@page "/media/tv/episodes/page/{PageNumber:int}" +@using LanguageExt.UnsafeValueAccess +@using Microsoft.AspNetCore.WebUtilities +@using Microsoft.Extensions.Primitives +@using ErsatzTV.Application.MediaCards +@using ErsatzTV.Application.MediaCollections +@using ErsatzTV.Application.MediaCollections.Commands +@using ErsatzTV.Application.Search.Queries +@using Unit = LanguageExt.Unit +@inherits MultiSelectBase +@inject NavigationManager _navigationManager +@inject ChannelWriter _channel + + +
+ @if (IsSelectMode()) + { + @SelectionLabel() +
+ + Add To Collection + + + Clear Selection + +
+ } + else + { + @_query +
+ + + + + @Math.Min((PageNumber - 1) * PageSize + 1, _data.Count)-@Math.Min(_data.Count, PageNumber * PageSize) of @_data.Count + + + + +
+ } +
+
+ + + + + + + +@if (_data.PageMap.IsSome) +{ + +} + +@code { + private static int PageSize => 100; + + [Parameter] + public int PageNumber { get; set; } + + private TelevisionEpisodeCardResultsViewModel _data; + private string _query; + + protected override Task OnParametersSetAsync() + { + if (PageNumber == 0) + { + PageNumber = 1; + } + + string query = new Uri(_navigationManager.Uri).Query; + if (QueryHelpers.ParseQuery(query).TryGetValue("query", out StringValues value)) + { + _query = value; + } + else + { + _query = null; + } + + return RefreshData(); + } + + protected override async Task RefreshData() + { + string searchQuery = string.IsNullOrWhiteSpace(_query) ? "type:episode" : $"type:episode AND ({_query})"; + _data = await Mediator.Send(new QuerySearchIndexEpisodes(searchQuery, PageNumber, PageSize)); + } + + private void PrevPage() + { + var uri = $"/media/tv/episodes/page/{PageNumber - 1}"; + if (!string.IsNullOrWhiteSpace(_query)) + { + uri = QueryHelpers.AddQueryString(uri, "query", _query); + } + _navigationManager.NavigateTo(uri); + } + + private void NextPage() + { + var uri = $"/media/tv/episodes/page/{PageNumber + 1}"; + if (!string.IsNullOrWhiteSpace(_query)) + { + uri = QueryHelpers.AddQueryString(uri, "query", _query); + } + _navigationManager.NavigateTo(uri); + } + + private void SelectClicked(MediaCardViewModel card, MouseEventArgs e) + { + List GetSortedItems() + { + return _data.Cards.OrderBy(m => m.SortTitle).ToList(); + } + + SelectClicked(GetSortedItems, card, e); + } + + private async Task AddToCollection(MediaCardViewModel card) + { + if (card is TelevisionEpisodeCardViewModel episode) + { + var parameters = new DialogParameters { { "EntityType", "episode" }, { "EntityName", episode.Title } }; + var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall }; + + IDialogReference dialog = Dialog.Show("Add To Collection", parameters, options); + DialogResult result = await dialog.Result; + if (!result.Cancelled && result.Data is MediaCollectionViewModel collection) + { + var request = new AddEpisodeToCollection(collection.Id, episode.EpisodeId); + Either addResult = await Mediator.Send(request); + addResult.Match( + Left: error => + { + Snackbar.Add($"Unexpected error adding episode to collection: {error.Value}"); + Logger.LogError("Unexpected error adding episode to collection: {Error}", error.Value); + }, + Right: _ => Snackbar.Add($"Added {episode.Title} to collection {collection.Name}", Severity.Success)); + } + } + } + +} \ No newline at end of file diff --git a/ErsatzTV/Pages/MultiSelectBase.cs b/ErsatzTV/Pages/MultiSelectBase.cs index e3d1a677b..95f74a0f0 100644 --- a/ErsatzTV/Pages/MultiSelectBase.cs +++ b/ErsatzTV/Pages/MultiSelectBase.cs @@ -97,17 +97,19 @@ namespace ErsatzTV.Pages protected Task AddSelectionToCollection() => AddItemsToCollection( _selectedItems.OfType().Map(m => m.MovieId).ToList(), _selectedItems.OfType().Map(s => s.TelevisionShowId).ToList(), + _selectedItems.OfType().Map(e => e.EpisodeId).ToList(), _selectedItems.OfType().Map(a => a.ArtistId).ToList(), _selectedItems.OfType().Map(mv => mv.MusicVideoId).ToList()); protected async Task AddItemsToCollection( List movieIds, List showIds, + List episodeIds, List artistIds, List musicVideoIds, string entityName = "selected items") { - int count = movieIds.Count + showIds.Count + artistIds.Count + musicVideoIds.Count; + int count = movieIds.Count + showIds.Count + episodeIds.Count + artistIds.Count + musicVideoIds.Count; var parameters = new DialogParameters { { "EntityType", count.ToString() }, { "EntityName", entityName } }; @@ -117,7 +119,13 @@ namespace ErsatzTV.Pages DialogResult result = await dialog.Result; if (!result.Cancelled && result.Data is MediaCollectionViewModel collection) { - var request = new AddItemsToCollection(collection.Id, movieIds, showIds, artistIds, musicVideoIds); + var request = new AddItemsToCollection( + collection.Id, + movieIds, + showIds, + episodeIds, + artistIds, + musicVideoIds); Either addResult = await Mediator.Send(request); addResult.Match( diff --git a/ErsatzTV/Pages/Search.razor b/ErsatzTV/Pages/Search.razor index b0911ca9c..861ca00a1 100644 --- a/ErsatzTV/Pages/Search.razor +++ b/ErsatzTV/Pages/Search.razor @@ -45,6 +45,11 @@ @_shows.Count Shows } + if (_episodes.Count > 0) + { + @_episodes.Count Episodes + } + if (_artists.Count > 0) { @_artists.Count Artists @@ -120,6 +125,34 @@ } + @if (_episodes.Count > 0) + { +
+ + Episodes + + @if (_episodes.Count > 50) + { + See All >> + } +
+ + + @foreach (TelevisionEpisodeCardViewModel card in _episodes.Cards.OrderBy(s => s.SortTitle)) + { + + } + + } + @if (_artists.Count > 0) {
@@ -181,6 +214,7 @@ private string _query; private MovieCardResultsViewModel _movies; private TelevisionShowCardResultsViewModel _shows; + private TelevisionEpisodeCardResultsViewModel _episodes; private MusicVideoCardResultsViewModel _musicVideos; private ArtistCardResultsViewModel _artists; @@ -194,6 +228,7 @@ _movies = await Mediator.Send(new QuerySearchIndexMovies($"type:movie AND ({_query})", 1, 50)); _shows = await Mediator.Send(new QuerySearchIndexShows($"type:show AND ({_query})", 1, 50)); + _episodes = await Mediator.Send(new QuerySearchIndexEpisodes($"type:episode AND ({_query})", 1, 50)); _musicVideos = await Mediator.Send(new QuerySearchIndexMusicVideos($"type:music_video AND ({_query})", 1, 50)); _artists = await Mediator.Send(new QuerySearchIndexArtists($"type:artist AND ({_query})", 1, 50)); } @@ -205,6 +240,7 @@ { return _movies.Cards.OrderBy(m => m.SortTitle) .Append(_shows.Cards.OrderBy(s => s.SortTitle)) + .Append(_episodes.Cards.OrderBy(ep => ep.SortTitle)) .Append(_artists.Cards.OrderBy(a => a.SortTitle)) .Append(_musicVideos.Cards.OrderBy(mv => mv.SortTitle)) .ToList(); @@ -257,6 +293,27 @@ } } + if (card is TelevisionEpisodeCardViewModel episode) + { + var parameters = new DialogParameters { { "EntityType", "episode" }, { "EntityName", episode.Title } }; + var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall }; + + IDialogReference dialog = Dialog.Show("Add To Collection", parameters, options); + DialogResult result = await dialog.Result; + if (!result.Cancelled && result.Data is MediaCollectionViewModel collection) + { + var request = new AddEpisodeToCollection(collection.Id, episode.EpisodeId); + Either addResult = await Mediator.Send(request); + addResult.Match( + Left: error => + { + Snackbar.Add($"Unexpected error adding episode to collection: {error.Value}"); + Logger.LogError("Unexpected error adding episode to collection: {Error}", error.Value); + }, + Right: _ => Snackbar.Add($"Added {episode.Title} to collection {collection.Name}", Severity.Success)); + } + } + if (card is ArtistCardViewModel artist) { var parameters = new DialogParameters { { "EntityType", "artist" }, { "EntityName", artist.Title } }; @@ -320,6 +377,16 @@ return uri; } + private string GetEpisodesLink() + { + var uri = "/media/tv/episodes/page/1"; + if (!string.IsNullOrWhiteSpace(_query)) + { + uri = QueryHelpers.AddQueryString(uri, "query", _query); + } + return uri; + } + private string GetArtistsLink() { var uri = "/media/music/artists/page/1"; @@ -343,7 +410,7 @@ private async Task AddAllToCollection(MouseEventArgs _) { SearchResultAllItemsViewModel results = await Mediator.Send(new QuerySearchIndexAllItems(_query)); - await AddItemsToCollection(results.MovieIds, results.ShowIds, results.ArtistIds, results.MusicVideoIds, "search results"); + await AddItemsToCollection(results.MovieIds, results.ShowIds, results.EpisodeIds, results.ArtistIds, results.MusicVideoIds, "search results"); } } \ No newline at end of file diff --git a/ErsatzTV/Pages/TelevisionEpisodeList.razor b/ErsatzTV/Pages/TelevisionEpisodeList.razor index c7945c26d..150839a87 100644 --- a/ErsatzTV/Pages/TelevisionEpisodeList.razor +++ b/ErsatzTV/Pages/TelevisionEpisodeList.razor @@ -106,6 +106,39 @@
+
+
+ Released: @episode.Aired.ToShortDateString() +
+ @if (episode.Directors.Any()) + { + var sorted = episode.Directors.OrderBy(w => w).ToList(); + +
+ Directors:  + @sorted.Head() + @foreach (string director in sorted.Skip(1)) + { + ,  + @director + } +
+ } + @if (episode.Writers.Any()) + { + var sorted = episode.Writers.OrderBy(w => w).ToList(); + +
+ Writers:  + @sorted.Head() + @foreach (string writer in sorted.Skip(1)) + { + ,  + @writer + } +
+ } +
}