From 0bbed69e85bed6667147627840c97d2ff6e83b83 Mon Sep 17 00:00:00 2001 From: Jason Dove Date: Thu, 27 May 2021 09:24:47 -0500 Subject: [PATCH] add movie directors and writers (#219) --- ErsatzTV.Application/Movies/Mapper.cs | 4 +- ErsatzTV.Application/Movies/MovieViewModel.cs | 4 +- ErsatzTV.Core/Domain/Metadata/Director.cs | 8 + .../Domain/Metadata/MovieMetadata.cs | 6 +- ErsatzTV.Core/Domain/Metadata/Writer.cs | 8 + .../Repositories/IMovieRepository.cs | 4 + .../Metadata/LocalMetadataProvider.cs | 44 +- ErsatzTV.Core/Metadata/Nfo/MovieNfo.cs | 6 + ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs | 44 + .../Metadata/DirectorConfiguration.cs | 11 + .../Metadata/MovieMetadataConfiguration.cs | 8 + .../Data/Configurations/Metadata/Writer.cs | 11 + .../Data/Repositories/MetadataRepository.cs | 13 + .../Data/Repositories/MovieRepository.cs | 100 + .../Data/Repositories/SearchRepository.cs | 4 + ErsatzTV.Infrastructure/Emby/EmbyApiClient.cs | 33 +- .../Jellyfin/JellyfinApiClient.cs | 33 +- ..._MovieMetadataDirectorsWriters.Designer.cs | 2775 +++++++++++++++++ ...22609_Add_MovieMetadataDirectorsWriters.cs | 83 + .../Migrations/TvContextModelSnapshot.cs | 66 + .../Plex/Models/PlexDirectorResponse.cs | 7 + .../Plex/Models/PlexMetadataResponse.cs | 2 + .../Plex/Models/PlexWriterResponse.mcs.cs | 7 + .../Plex/PlexServerApiClient.cs | 4 +- ErsatzTV.Infrastructure/Search/SearchIndex.cs | 14 +- ErsatzTV/Pages/Movie.razor | 28 + 26 files changed, 3315 insertions(+), 12 deletions(-) create mode 100644 ErsatzTV.Core/Domain/Metadata/Director.cs create mode 100644 ErsatzTV.Core/Domain/Metadata/Writer.cs create mode 100644 ErsatzTV.Infrastructure/Data/Configurations/Metadata/DirectorConfiguration.cs create mode 100644 ErsatzTV.Infrastructure/Data/Configurations/Metadata/Writer.cs create mode 100644 ErsatzTV.Infrastructure/Migrations/20210527122609_Add_MovieMetadataDirectorsWriters.Designer.cs create mode 100644 ErsatzTV.Infrastructure/Migrations/20210527122609_Add_MovieMetadataDirectorsWriters.cs create mode 100644 ErsatzTV.Infrastructure/Plex/Models/PlexDirectorResponse.cs create mode 100644 ErsatzTV.Infrastructure/Plex/Models/PlexWriterResponse.mcs.cs diff --git a/ErsatzTV.Application/Movies/Mapper.cs b/ErsatzTV.Application/Movies/Mapper.cs index 3b96840e1..3f9682a3c 100644 --- a/ErsatzTV.Application/Movies/Mapper.cs +++ b/ErsatzTV.Application/Movies/Mapper.cs @@ -31,7 +31,9 @@ namespace ErsatzTV.Application.Movies LanguagesForMovie(movie), metadata.Actors.OrderBy(a => a.Order).ThenBy(a => a.Id) .Map(a => MediaCards.Mapper.ProjectToViewModel(a, maybeJellyfin, maybeEmby)) - .ToList()) + .ToList(), + metadata.Directors.Map(d => d.Name).ToList(), + metadata.Writers.Map(w => w.Name).ToList()) { Poster = Artwork(metadata, ArtworkKind.Poster, maybeJellyfin, maybeEmby), FanArt = Artwork(metadata, ArtworkKind.FanArt, maybeJellyfin, maybeEmby) diff --git a/ErsatzTV.Application/Movies/MovieViewModel.cs b/ErsatzTV.Application/Movies/MovieViewModel.cs index 77e634554..83fdf6b46 100644 --- a/ErsatzTV.Application/Movies/MovieViewModel.cs +++ b/ErsatzTV.Application/Movies/MovieViewModel.cs @@ -13,7 +13,9 @@ namespace ErsatzTV.Application.Movies List Studios, List ContentRatings, List Languages, - List Actors) + List Actors, + List Directors, + List Writers) { public string Poster { get; set; } public string FanArt { get; set; } diff --git a/ErsatzTV.Core/Domain/Metadata/Director.cs b/ErsatzTV.Core/Domain/Metadata/Director.cs new file mode 100644 index 000000000..80c878892 --- /dev/null +++ b/ErsatzTV.Core/Domain/Metadata/Director.cs @@ -0,0 +1,8 @@ +namespace ErsatzTV.Core.Domain +{ + public class Director + { + public int Id { get; set; } + public string Name { get; set; } + } +} diff --git a/ErsatzTV.Core/Domain/Metadata/MovieMetadata.cs b/ErsatzTV.Core/Domain/Metadata/MovieMetadata.cs index b6cc67e37..f9709118f 100644 --- a/ErsatzTV.Core/Domain/Metadata/MovieMetadata.cs +++ b/ErsatzTV.Core/Domain/Metadata/MovieMetadata.cs @@ -1,4 +1,6 @@ -namespace ErsatzTV.Core.Domain +using System.Collections.Generic; + +namespace ErsatzTV.Core.Domain { public class MovieMetadata : Metadata { @@ -8,5 +10,7 @@ public string Tagline { get; set; } public int MovieId { get; set; } public Movie Movie { get; set; } + public List Directors { get; set; } + public List Writers { get; set; } } } diff --git a/ErsatzTV.Core/Domain/Metadata/Writer.cs b/ErsatzTV.Core/Domain/Metadata/Writer.cs new file mode 100644 index 000000000..48036b7fc --- /dev/null +++ b/ErsatzTV.Core/Domain/Metadata/Writer.cs @@ -0,0 +1,8 @@ +namespace ErsatzTV.Core.Domain +{ + public class Writer + { + public int Id { get; set; } + public string Name { get; set; } + } +} diff --git a/ErsatzTV.Core/Interfaces/Repositories/IMovieRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/IMovieRepository.cs index 926b891d6..5d47dd35f 100644 --- a/ErsatzTV.Core/Interfaces/Repositories/IMovieRepository.cs +++ b/ErsatzTV.Core/Interfaces/Repositories/IMovieRepository.cs @@ -33,5 +33,9 @@ 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/Metadata/LocalMetadataProvider.cs b/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs index 156099f44..ca47d074f 100644 --- a/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs +++ b/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs @@ -260,6 +260,46 @@ namespace ErsatzTV.Core.Metadata _movieRepository.AddStudio, _movieRepository.AddActor); + foreach (Director director in existing.Directors + .Filter(g => metadata.Directors.All(g2 => g2.Name != g.Name)).ToList()) + { + existing.Directors.Remove(director); + if (await _movieRepository.RemoveDirector(director)) + { + updated = true; + } + } + + foreach (Director director in metadata.Directors + .Filter(g => existing.Directors.All(g2 => g2.Name != g.Name)).ToList()) + { + existing.Directors.Add(director); + if (await _movieRepository.AddDirector(existing, director)) + { + updated = true; + } + } + + foreach (Writer writer in existing.Writers + .Filter(g => metadata.Writers.All(g2 => g2.Name != g.Name)).ToList()) + { + existing.Writers.Remove(writer); + if (await _movieRepository.RemoveWriter(writer)) + { + updated = true; + } + } + + foreach (Writer writer in metadata.Writers + .Filter(g => existing.Writers.All(g2 => g2.Name != g.Name)).ToList()) + { + existing.Writers.Add(writer); + if (await _movieRepository.AddWriter(existing, writer)) + { + updated = true; + } + } + return await _metadataRepository.Update(existing) || updated; }, async () => @@ -582,7 +622,9 @@ 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), + Directors = nfo.Directors.Map(d => new Director { Name = d }).ToList(), + Writers = nfo.Writers.Map(w => new Writer { Name = w }).ToList() }; }, None); diff --git a/ErsatzTV.Core/Metadata/Nfo/MovieNfo.cs b/ErsatzTV.Core/Metadata/Nfo/MovieNfo.cs index 0da0a3e66..9d2ff4f27 100644 --- a/ErsatzTV.Core/Metadata/Nfo/MovieNfo.cs +++ b/ErsatzTV.Core/Metadata/Nfo/MovieNfo.cs @@ -39,5 +39,11 @@ 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; } } } diff --git a/ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs b/ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs index 6a1cf0524..331859295 100644 --- a/ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs +++ b/ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs @@ -254,6 +254,50 @@ namespace ErsatzTV.Core.Plex } } + foreach (Director director in existingMetadata.Directors + .Filter(g => fullMetadata.Directors.All(g2 => g2.Name != g.Name)) + .ToList()) + { + existingMetadata.Directors.Remove(director); + if (await _movieRepository.RemoveDirector(director)) + { + result.IsUpdated = true; + } + } + + foreach (Director director in fullMetadata.Directors + .Filter(g => existingMetadata.Directors.All(g2 => g2.Name != g.Name)) + .ToList()) + { + existingMetadata.Directors.Add(director); + if (await _movieRepository.AddDirector(existingMetadata, director)) + { + result.IsUpdated = true; + } + } + + foreach (Writer writer in existingMetadata.Writers + .Filter(g => fullMetadata.Writers.All(g2 => g2.Name != g.Name)) + .ToList()) + { + existingMetadata.Writers.Remove(writer); + if (await _movieRepository.RemoveWriter(writer)) + { + result.IsUpdated = true; + } + } + + foreach (Writer writer in fullMetadata.Writers + .Filter(g => existingMetadata.Writers.All(g2 => g2.Name != g.Name)) + .ToList()) + { + existingMetadata.Writers.Add(writer); + if (await _movieRepository.AddWriter(existingMetadata, writer)) + { + result.IsUpdated = true; + } + } + if (fullMetadata.SortTitle != existingMetadata.SortTitle) { existingMetadata.SortTitle = fullMetadata.SortTitle; diff --git a/ErsatzTV.Infrastructure/Data/Configurations/Metadata/DirectorConfiguration.cs b/ErsatzTV.Infrastructure/Data/Configurations/Metadata/DirectorConfiguration.cs new file mode 100644 index 000000000..5f6d9dd8f --- /dev/null +++ b/ErsatzTV.Infrastructure/Data/Configurations/Metadata/DirectorConfiguration.cs @@ -0,0 +1,11 @@ +using ErsatzTV.Core.Domain; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace ErsatzTV.Infrastructure.Data.Configurations +{ + public class DirectorConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) => builder.ToTable("Director"); + } +} diff --git a/ErsatzTV.Infrastructure/Data/Configurations/Metadata/MovieMetadataConfiguration.cs b/ErsatzTV.Infrastructure/Data/Configurations/Metadata/MovieMetadataConfiguration.cs index e91c021c6..c2a4123d2 100644 --- a/ErsatzTV.Infrastructure/Data/Configurations/Metadata/MovieMetadataConfiguration.cs +++ b/ErsatzTV.Infrastructure/Data/Configurations/Metadata/MovieMetadataConfiguration.cs @@ -29,6 +29,14 @@ namespace ErsatzTV.Infrastructure.Data.Configurations builder.HasMany(mm => mm.Actors) .WithOne() .OnDelete(DeleteBehavior.Cascade); + + builder.HasMany(mm => mm.Directors) + .WithOne() + .OnDelete(DeleteBehavior.Cascade); + + builder.HasMany(mm => mm.Writers) + .WithOne() + .OnDelete(DeleteBehavior.Cascade); } } } diff --git a/ErsatzTV.Infrastructure/Data/Configurations/Metadata/Writer.cs b/ErsatzTV.Infrastructure/Data/Configurations/Metadata/Writer.cs new file mode 100644 index 000000000..924ca88fe --- /dev/null +++ b/ErsatzTV.Infrastructure/Data/Configurations/Metadata/Writer.cs @@ -0,0 +1,11 @@ +using ErsatzTV.Core.Domain; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace ErsatzTV.Infrastructure.Data.Configurations +{ + public class WriterConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) => builder.ToTable("Writer"); + } +} diff --git a/ErsatzTV.Infrastructure/Data/Repositories/MetadataRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/MetadataRepository.cs index 987aa3fbd..2c2c1b56b 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/MetadataRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/MetadataRepository.cs @@ -75,6 +75,19 @@ namespace ErsatzTV.Infrastructure.Data.Repositories } } + if (metadata is MovieMetadata movieMetadata) + { + foreach (Director director in Optional(movieMetadata.Directors).Flatten()) + { + dbContext.Entry(director).State = EntityState.Added; + } + + foreach (Writer writer in Optional(movieMetadata.Writers).Flatten()) + { + dbContext.Entry(writer).State = EntityState.Added; + } + } + return await dbContext.SaveChangesAsync() > 0; } diff --git a/ErsatzTV.Infrastructure/Data/Repositories/MovieRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/MovieRepository.cs index f5b9e723f..b4af37481 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/MovieRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/MovieRepository.cs @@ -49,6 +49,10 @@ namespace ErsatzTV.Infrastructure.Data.Repositories .Include(m => m.MovieMetadata) .ThenInclude(m => m.Actors) .ThenInclude(a => a.Artwork) + .Include(i => i.MovieMetadata) + .ThenInclude(mm => mm.Directors) + .Include(i => i.MovieMetadata) + .ThenInclude(mm => mm.Writers) .Include(m => m.MediaVersions) .ThenInclude(mv => mv.Streams) .OrderBy(m => m.Id) @@ -70,7 +74,13 @@ namespace ErsatzTV.Infrastructure.Data.Repositories .ThenInclude(mm => mm.Studios) .Include(i => i.MovieMetadata) .ThenInclude(mm => mm.Actors) + .Include(i => i.MovieMetadata) + .ThenInclude(mm => mm.Actors) .ThenInclude(a => a.Artwork) + .Include(i => i.MovieMetadata) + .ThenInclude(mm => mm.Directors) + .Include(i => i.MovieMetadata) + .ThenInclude(mm => mm.Writers) .Include(i => i.LibraryPath) .ThenInclude(lp => lp.Library) .Include(i => i.MediaVersions) @@ -105,6 +115,10 @@ namespace ErsatzTV.Infrastructure.Data.Repositories .ThenInclude(a => a.Artwork) .Include(i => i.MovieMetadata) .ThenInclude(mm => mm.Artwork) + .Include(i => i.MovieMetadata) + .ThenInclude(mm => mm.Directors) + .Include(i => i.MovieMetadata) + .ThenInclude(mm => mm.Writers) .Include(i => i.MediaVersions) .ThenInclude(mv => mv.MediaFiles) .Include(i => i.MediaVersions) @@ -310,6 +324,10 @@ namespace ErsatzTV.Infrastructure.Data.Repositories .ThenInclude(mm => mm.Actors) .Include(m => m.MovieMetadata) .ThenInclude(mm => mm.Artwork) + .Include(m => m.MovieMetadata) + .ThenInclude(mm => mm.Directors) + .Include(m => m.MovieMetadata) + .ThenInclude(mm => mm.Writers) .Filter(m => m.ItemId == movie.ItemId) .OrderBy(m => m.ItemId) .SingleOrDefaultAsync(); @@ -399,6 +417,36 @@ namespace ErsatzTV.Infrastructure.Data.Repositories metadata.Actors.Add(actor); } + // 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); + } + metadata.ReleaseDate = incomingMetadata.ReleaseDate; // poster @@ -512,6 +560,10 @@ namespace ErsatzTV.Infrastructure.Data.Repositories .Include(m => m.MovieMetadata) .ThenInclude(mm => mm.Actors) .Include(m => m.MovieMetadata) + .ThenInclude(mm => mm.Directors) + .Include(m => m.MovieMetadata) + .ThenInclude(mm => mm.Writers) + .Include(m => m.MovieMetadata) .ThenInclude(mm => mm.Artwork) .Filter(m => m.ItemId == movie.ItemId) .OrderBy(m => m.ItemId) @@ -602,6 +654,36 @@ namespace ErsatzTV.Infrastructure.Data.Repositories metadata.Actors.Add(actor); } + // 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); + } + metadata.ReleaseDate = incomingMetadata.ReleaseDate; // poster @@ -655,6 +737,24 @@ 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)", + new { writer.Name, MetadataId = metadata.Id }).Map(result => result > 0); + private static async Task>> AddMovie( TvContext dbContext, int libraryPathId, diff --git a/ErsatzTV.Infrastructure/Data/Repositories/SearchRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/SearchRepository.cs index 6e0404ef3..fdbc7eca9 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/SearchRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/SearchRepository.cs @@ -41,6 +41,10 @@ namespace ErsatzTV.Infrastructure.Data.Repositories .ThenInclude(mm => mm.Studios) .Include(mi => (mi as Movie).MovieMetadata) .ThenInclude(mm => mm.Actors) + .Include(mi => (mi as Movie).MovieMetadata) + .ThenInclude(mm => mm.Directors) + .Include(mi => (mi as Movie).MovieMetadata) + .ThenInclude(mm => mm.Writers) .Include(mi => (mi as Movie).MediaVersions) .ThenInclude(mm => mm.Streams) .Include(mi => (mi as Show).ShowMetadata) diff --git a/ErsatzTV.Infrastructure/Emby/EmbyApiClient.cs b/ErsatzTV.Infrastructure/Emby/EmbyApiClient.cs index 06a6f3d02..08a49ef5c 100644 --- a/ErsatzTV.Infrastructure/Emby/EmbyApiClient.cs +++ b/ErsatzTV.Infrastructure/Emby/EmbyApiClient.cs @@ -241,7 +241,9 @@ namespace ErsatzTV.Infrastructure.Emby Genres = Optional(item.Genres).Flatten().Map(g => new Genre { Name = g }).ToList(), 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().Map(r => ProjectToModel(r, dateAdded)).ToList(), + 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() }; @@ -281,8 +283,13 @@ namespace ErsatzTV.Infrastructure.Emby return metadata; } - private Actor ProjectToModel(EmbyPersonResponse person, DateTime dateAdded) + private Option ProjectToActor(EmbyPersonResponse person, DateTime dateAdded) { + if (person.Type?.ToLowerInvariant() != "actor") + { + return None; + } + var actor = new Actor { Name = person.Name, Role = person.Role }; if (!string.IsNullOrWhiteSpace(person.Id) && !string.IsNullOrWhiteSpace(person.PrimaryImageTag)) { @@ -297,6 +304,26 @@ namespace ErsatzTV.Infrastructure.Emby return actor; } + private static Option ProjectToDirector(EmbyPersonResponse person) + { + if (person.Type?.ToLowerInvariant() != "director") + { + return None; + } + + return new Director { Name = person.Name }; + } + + private static Option ProjectToWriter(EmbyPersonResponse person) + { + if (person.Type?.ToLowerInvariant() != "writer") + { + return None; + } + + return new Writer { Name = person.Name }; + } + private Option ProjectToShow(EmbyLibraryItemResponse item) { try @@ -337,7 +364,7 @@ namespace ErsatzTV.Infrastructure.Emby Genres = Optional(item.Genres).Flatten().Map(g => new Genre { Name = g }).ToList(), 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().Map(r => ProjectToModel(r, dateAdded)).ToList(), + Actors = Optional(item.People).Flatten().Collect(r => ProjectToActor(r, dateAdded)).ToList(), Artwork = new List() }; diff --git a/ErsatzTV.Infrastructure/Jellyfin/JellyfinApiClient.cs b/ErsatzTV.Infrastructure/Jellyfin/JellyfinApiClient.cs index 12969b83f..69fcc1cbf 100644 --- a/ErsatzTV.Infrastructure/Jellyfin/JellyfinApiClient.cs +++ b/ErsatzTV.Infrastructure/Jellyfin/JellyfinApiClient.cs @@ -287,7 +287,9 @@ namespace ErsatzTV.Infrastructure.Jellyfin Genres = Optional(item.Genres).Flatten().Map(g => new Genre { Name = g }).ToList(), 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().Map(r => ProjectToModel(r, dateAdded)).ToList(), + 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() }; @@ -327,8 +329,13 @@ namespace ErsatzTV.Infrastructure.Jellyfin return metadata; } - private Actor ProjectToModel(JellyfinPersonResponse person, DateTime dateAdded) + private static Option ProjectToActor(JellyfinPersonResponse person, DateTime dateAdded) { + if (person.Type?.ToLowerInvariant() != "actor") + { + return None; + } + var actor = new Actor { Name = person.Name, Role = person.Role }; if (!string.IsNullOrWhiteSpace(person.Id) && !string.IsNullOrWhiteSpace(person.PrimaryImageTag)) { @@ -343,6 +350,26 @@ namespace ErsatzTV.Infrastructure.Jellyfin return actor; } + private static Option ProjectToDirector(JellyfinPersonResponse person) + { + if (person.Type?.ToLowerInvariant() != "director") + { + return None; + } + + return new Director { Name = person.Name }; + } + + private static Option ProjectToWriter(JellyfinPersonResponse person) + { + if (person.Type?.ToLowerInvariant() != "writer") + { + return None; + } + + return new Writer { Name = person.Name }; + } + private Option ProjectToShow(JellyfinLibraryItemResponse item) { try @@ -388,7 +415,7 @@ namespace ErsatzTV.Infrastructure.Jellyfin Genres = Optional(item.Genres).Flatten().Map(g => new Genre { Name = g }).ToList(), 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().Map(r => ProjectToModel(r, dateAdded)).ToList(), + Actors = Optional(item.People).Flatten().Collect(r => ProjectToActor(r, dateAdded)).ToList(), Artwork = new List() }; diff --git a/ErsatzTV.Infrastructure/Migrations/20210527122609_Add_MovieMetadataDirectorsWriters.Designer.cs b/ErsatzTV.Infrastructure/Migrations/20210527122609_Add_MovieMetadataDirectorsWriters.Designer.cs new file mode 100644 index 000000000..3f75b4f01 --- /dev/null +++ b/ErsatzTV.Infrastructure/Migrations/20210527122609_Add_MovieMetadataDirectorsWriters.Designer.cs @@ -0,0 +1,2775 @@ +// +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("20210527122609_Add_MovieMetadataDirectorsWriters")] + partial class Add_MovieMetadataDirectorsWriters + { + 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.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.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("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("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("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("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("Studios"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ShowMetadata", b => + { + b.Navigation("Actors"); + + b.Navigation("Artwork"); + + b.Navigation("Genres"); + + 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/20210527122609_Add_MovieMetadataDirectorsWriters.cs b/ErsatzTV.Infrastructure/Migrations/20210527122609_Add_MovieMetadataDirectorsWriters.cs new file mode 100644 index 000000000..115300460 --- /dev/null +++ b/ErsatzTV.Infrastructure/Migrations/20210527122609_Add_MovieMetadataDirectorsWriters.cs @@ -0,0 +1,83 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace ErsatzTV.Infrastructure.Migrations +{ + public partial class Add_MovieMetadataDirectorsWriters : 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 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"); + + // jellyfin + migrationBuilder.Sql("UPDATE JellyfinMovie SET Etag = NULL"); + + migrationBuilder.CreateTable( + "Director", + table => new + { + Id = table.Column("INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column("TEXT", nullable: true), + MovieMetadataId = table.Column("INTEGER", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Director", x => x.Id); + table.ForeignKey( + "FK_Director_MovieMetadata_MovieMetadataId", + x => x.MovieMetadataId, + "MovieMetadata", + "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + "Writer", + table => new + { + Id = table.Column("INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column("TEXT", nullable: true), + MovieMetadataId = table.Column("INTEGER", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Writer", x => x.Id); + table.ForeignKey( + "FK_Writer_MovieMetadata_MovieMetadataId", + x => x.MovieMetadataId, + "MovieMetadata", + "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + "IX_Director_MovieMetadataId", + "Director", + "MovieMetadataId"); + + migrationBuilder.CreateIndex( + "IX_Writer_MovieMetadataId", + "Writer", + "MovieMetadataId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + "Director"); + + migrationBuilder.DropTable( + "Writer"); + } + } +} diff --git a/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs b/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs index 209dedb07..951114f05 100644 --- a/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs +++ b/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs @@ -282,6 +282,27 @@ namespace ErsatzTV.Infrastructure.Migrations 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 => @@ -1332,6 +1353,27 @@ namespace ErsatzTV.Infrastructure.Migrations 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 => @@ -1878,6 +1920,16 @@ namespace ErsatzTV.Infrastructure.Migrations 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 => @@ -2417,6 +2469,16 @@ namespace ErsatzTV.Infrastructure.Migrations .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 => @@ -2859,11 +2921,15 @@ namespace ErsatzTV.Infrastructure.Migrations b.Navigation("Artwork"); + b.Navigation("Directors"); + b.Navigation("Genres"); b.Navigation("Studios"); b.Navigation("Tags"); + + b.Navigation("Writers"); }); modelBuilder.Entity( diff --git a/ErsatzTV.Infrastructure/Plex/Models/PlexDirectorResponse.cs b/ErsatzTV.Infrastructure/Plex/Models/PlexDirectorResponse.cs new file mode 100644 index 000000000..b4e84808f --- /dev/null +++ b/ErsatzTV.Infrastructure/Plex/Models/PlexDirectorResponse.cs @@ -0,0 +1,7 @@ +namespace ErsatzTV.Infrastructure.Plex.Models +{ + public class PlexDirectorResponse + { + public string Tag { get; set; } + } +} diff --git a/ErsatzTV.Infrastructure/Plex/Models/PlexMetadataResponse.cs b/ErsatzTV.Infrastructure/Plex/Models/PlexMetadataResponse.cs index b9316396c..0c4c91d38 100644 --- a/ErsatzTV.Infrastructure/Plex/Models/PlexMetadataResponse.cs +++ b/ErsatzTV.Infrastructure/Plex/Models/PlexMetadataResponse.cs @@ -20,5 +20,7 @@ namespace ErsatzTV.Infrastructure.Plex.Models public List Media { get; set; } public List Genre { get; set; } public List Role { get; set; } + public List Director { get; set; } + public List Writer { get; set; } } } diff --git a/ErsatzTV.Infrastructure/Plex/Models/PlexWriterResponse.mcs.cs b/ErsatzTV.Infrastructure/Plex/Models/PlexWriterResponse.mcs.cs new file mode 100644 index 000000000..4ad565fae --- /dev/null +++ b/ErsatzTV.Infrastructure/Plex/Models/PlexWriterResponse.mcs.cs @@ -0,0 +1,7 @@ +namespace ErsatzTV.Infrastructure.Plex.Models +{ + public class PlexWriterResponse + { + public string Tag { get; set; } + } +} diff --git a/ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs b/ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs index 63b938e0a..72fb62fee 100644 --- a/ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs +++ b/ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs @@ -263,7 +263,9 @@ namespace ErsatzTV.Infrastructure.Plex Tags = new List(), Studios = new List(), Actors = Optional(response.Role).Flatten().Map(r => ProjectToModel(r, dateAdded, lastWriteTime)) - .ToList() + .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 (!string.IsNullOrWhiteSpace(response.Studio)) diff --git a/ErsatzTV.Infrastructure/Search/SearchIndex.cs b/ErsatzTV.Infrastructure/Search/SearchIndex.cs index 8d31809ff..3375c9a16 100644 --- a/ErsatzTV.Infrastructure/Search/SearchIndex.cs +++ b/ErsatzTV.Infrastructure/Search/SearchIndex.cs @@ -49,6 +49,8 @@ namespace ErsatzTV.Infrastructure.Search private const string MoodField = "mood"; private const string ActorField = "actor"; private const string ContentRatingField = "content_rating"; + private const string DirectorField = "director"; + private const string WriterField = "writer"; private const string MovieType = "movie"; private const string ShowType = "show"; @@ -69,7 +71,7 @@ namespace ErsatzTV.Infrastructure.Search _initialized = false; } - public int Version => 10; + public int Version => 11; public Task Initialize(ILocalFileSystem localFileSystem) { @@ -318,6 +320,16 @@ namespace ErsatzTV.Infrastructure.Search 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, movie.Id.ToString()), doc); } catch (Exception ex) diff --git a/ErsatzTV/Pages/Movie.razor b/ErsatzTV/Pages/Movie.razor index 4fda2b9f0..35c8f6b05 100644 --- a/ErsatzTV/Pages/Movie.razor +++ b/ErsatzTV/Pages/Movie.razor @@ -99,6 +99,30 @@ } } + @if (_sortedDirectors.Any()) + { +
+ Directors:  + @_sortedDirectors.Head() + @foreach (string director in _sortedDirectors.Skip(1)) + { + + @director + } +
+ } + @if (_sortedWriters.Any()) + { +
+ Writers:  + @_sortedWriters.Head() + @foreach (string writer in _sortedWriters.Skip(1)) + { + + @writer + } +
+ } @if (_sortedGenres.Any()) {
@@ -150,6 +174,8 @@ private MovieViewModel _movie; private List _sortedContentRatings = new(); private List _sortedLanguages = new(); + private List _sortedDirectors = new(); + private List _sortedWriters = new(); private List _sortedStudios = new(); private List _sortedGenres = new(); private List _sortedTags = new(); @@ -165,6 +191,8 @@ _sortedStudios = _movie.Studios.OrderBy(s => s).ToList(); _sortedGenres = _movie.Genres.OrderBy(g => g).ToList(); _sortedTags = _movie.Tags.OrderBy(t => t).ToList(); + _sortedDirectors = _movie.Directors.OrderBy(d => d).ToList(); + _sortedWriters = _movie.Writers.OrderBy(w => w).ToList(); }); private async Task AddToCollection()