diff --git a/ErsatzTV.Application/Movies/Mapper.cs b/ErsatzTV.Application/Movies/Mapper.cs index 1ac0e33f0..3b96840e1 100644 --- a/ErsatzTV.Application/Movies/Mapper.cs +++ b/ErsatzTV.Application/Movies/Mapper.cs @@ -26,6 +26,8 @@ namespace ErsatzTV.Application.Movies metadata.Genres.Map(g => g.Name).ToList(), metadata.Tags.Map(t => t.Name).ToList(), metadata.Studios.Map(s => s.Name).ToList(), + (metadata.ContentRating ?? string.Empty).Split("/").Map(s => s.Trim()) + .Where(x => !string.IsNullOrWhiteSpace(x)).ToList(), LanguagesForMovie(movie), metadata.Actors.OrderBy(a => a.Order).ThenBy(a => a.Id) .Map(a => MediaCards.Mapper.ProjectToViewModel(a, maybeJellyfin, maybeEmby)) diff --git a/ErsatzTV.Application/Movies/MovieViewModel.cs b/ErsatzTV.Application/Movies/MovieViewModel.cs index 88bd597d7..77e634554 100644 --- a/ErsatzTV.Application/Movies/MovieViewModel.cs +++ b/ErsatzTV.Application/Movies/MovieViewModel.cs @@ -11,6 +11,7 @@ namespace ErsatzTV.Application.Movies List Genres, List Tags, List Studios, + List ContentRatings, List Languages, List Actors) { diff --git a/ErsatzTV.Application/Television/Mapper.cs b/ErsatzTV.Application/Television/Mapper.cs index 3539d64d1..9bd991e84 100644 --- a/ErsatzTV.Application/Television/Mapper.cs +++ b/ErsatzTV.Application/Television/Mapper.cs @@ -30,6 +30,10 @@ namespace ErsatzTV.Application.Television show.ShowMetadata.HeadOrNone().Map(m => m.Tags.Map(g => g.Name).ToList()).IfNone(new List()), show.ShowMetadata.HeadOrNone().Map(m => m.Studios.Map(s => s.Name).ToList()) .IfNone(new List()), + show.ShowMetadata.HeadOrNone() + .Map( + m => (m.ContentRating ?? string.Empty).Split("/").Map(s => s.Trim()) + .Where(x => !string.IsNullOrWhiteSpace(x)).ToList()).IfNone(new List()), LanguagesForShow(languages), show.ShowMetadata.HeadOrNone() .Map( diff --git a/ErsatzTV.Application/Television/TelevisionShowViewModel.cs b/ErsatzTV.Application/Television/TelevisionShowViewModel.cs index 4bafdba1d..a22329aaa 100644 --- a/ErsatzTV.Application/Television/TelevisionShowViewModel.cs +++ b/ErsatzTV.Application/Television/TelevisionShowViewModel.cs @@ -14,6 +14,7 @@ namespace ErsatzTV.Application.Television List Genres, List Tags, List Studios, + List ContentRatings, List Languages, List Actors); } diff --git a/ErsatzTV.Core/Domain/Metadata/MetadataKind.cs b/ErsatzTV.Core/Domain/Metadata/MetadataKind.cs index d480e9b53..7f4262d62 100644 --- a/ErsatzTV.Core/Domain/Metadata/MetadataKind.cs +++ b/ErsatzTV.Core/Domain/Metadata/MetadataKind.cs @@ -4,6 +4,6 @@ { Fallback = 0, Sidecar = 1, - External + External = 2 } } diff --git a/ErsatzTV.Core/Domain/Metadata/MovieMetadata.cs b/ErsatzTV.Core/Domain/Metadata/MovieMetadata.cs index 6c57e1dce..b6cc67e37 100644 --- a/ErsatzTV.Core/Domain/Metadata/MovieMetadata.cs +++ b/ErsatzTV.Core/Domain/Metadata/MovieMetadata.cs @@ -2,6 +2,7 @@ { public class MovieMetadata : Metadata { + public string ContentRating { get; set; } public string Outline { get; set; } public string Plot { get; set; } public string Tagline { get; set; } diff --git a/ErsatzTV.Core/Domain/Metadata/ShowMetadata.cs b/ErsatzTV.Core/Domain/Metadata/ShowMetadata.cs index 855ba29d3..54f5b3f46 100644 --- a/ErsatzTV.Core/Domain/Metadata/ShowMetadata.cs +++ b/ErsatzTV.Core/Domain/Metadata/ShowMetadata.cs @@ -2,6 +2,7 @@ { public class ShowMetadata : Metadata { + public string ContentRating { get; set; } public string Outline { get; set; } public string Plot { get; set; } public string Tagline { get; set; } diff --git a/ErsatzTV.Core/Interfaces/Repositories/IMetadataRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/IMetadataRepository.cs index 6871f2bbf..060385574 100644 --- a/ErsatzTV.Core/Interfaces/Repositories/IMetadataRepository.cs +++ b/ErsatzTV.Core/Interfaces/Repositories/IMetadataRepository.cs @@ -24,5 +24,9 @@ namespace ErsatzTV.Core.Interfaces.Repositories Task MarkAsUpdated(SeasonMetadata metadata, DateTime dateUpdated); Task MarkAsUpdated(MovieMetadata metadata, DateTime dateUpdated); Task MarkAsUpdated(EpisodeMetadata metadata, DateTime dateUpdated); + Task MarkAsExternal(ShowMetadata metadata); + Task SetContentRating(ShowMetadata metadata, string contentRating); + Task MarkAsExternal(MovieMetadata metadata); + Task SetContentRating(MovieMetadata metadata, string contentRating); } } diff --git a/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs b/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs index 5aa029745..156099f44 100644 --- a/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs +++ b/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs @@ -232,6 +232,7 @@ namespace ErsatzTV.Core.Metadata Optional(movie.MovieMetadata).Flatten().HeadOrNone().Match( async existing => { + existing.ContentRating = metadata.ContentRating; existing.Outline = metadata.Outline; existing.Plot = metadata.Plot; existing.Tagline = metadata.Tagline; @@ -276,6 +277,7 @@ namespace ErsatzTV.Core.Metadata Optional(show.ShowMetadata).Flatten().HeadOrNone().Match( async existing => { + existing.ContentRating = metadata.ContentRating; existing.Outline = metadata.Outline; existing.Plot = metadata.Plot; existing.Tagline = metadata.Tagline; @@ -474,6 +476,7 @@ namespace ErsatzTV.Core.Metadata Plot = nfo.Plot, Outline = nfo.Outline, Tagline = nfo.Tagline, + ContentRating = nfo.ContentRating, Year = GetYear(nfo.Year, nfo.Premiered), ReleaseDate = GetAired(nfo.Year, nfo.Premiered), Genres = nfo.Genres.Map(g => new Genre { Name = g }).ToList(), @@ -571,6 +574,7 @@ namespace ErsatzTV.Core.Metadata DateUpdated = dateUpdated, Title = nfo.Title, Year = nfo.Year, + ContentRating = nfo.ContentRating, ReleaseDate = nfo.Premiered, Plot = nfo.Plot, Outline = nfo.Outline, diff --git a/ErsatzTV.Core/Metadata/MovieFolderScanner.cs b/ErsatzTV.Core/Metadata/MovieFolderScanner.cs index ed6880e95..c319b5b25 100644 --- a/ErsatzTV.Core/Metadata/MovieFolderScanner.cs +++ b/ErsatzTV.Core/Metadata/MovieFolderScanner.cs @@ -108,7 +108,7 @@ namespace ErsatzTV.Core.Metadata .HeadOrNone(); // skip folder if etag matches - if (await knownFolder.Map(f => f.Etag).IfNoneAsync(string.Empty) == etag) + if (await knownFolder.Map(f => f.Etag ?? string.Empty).IfNoneAsync(string.Empty) == etag) { continue; } diff --git a/ErsatzTV.Core/Metadata/MusicVideoFolderScanner.cs b/ErsatzTV.Core/Metadata/MusicVideoFolderScanner.cs index 58ad9d73b..7a25ec239 100644 --- a/ErsatzTV.Core/Metadata/MusicVideoFolderScanner.cs +++ b/ErsatzTV.Core/Metadata/MusicVideoFolderScanner.cs @@ -252,7 +252,7 @@ namespace ErsatzTV.Core.Metadata .HeadOrNone(); // skip folder if etag matches - if (await knownFolder.Map(f => f.Etag).IfNoneAsync(string.Empty) == etag) + if (await knownFolder.Map(f => f.Etag ?? string.Empty).IfNoneAsync(string.Empty) == etag) { continue; } diff --git a/ErsatzTV.Core/Metadata/Nfo/TvShowNfo.cs b/ErsatzTV.Core/Metadata/Nfo/TvShowNfo.cs index 241a5cb29..5577bcee0 100644 --- a/ErsatzTV.Core/Metadata/Nfo/TvShowNfo.cs +++ b/ErsatzTV.Core/Metadata/Nfo/TvShowNfo.cs @@ -21,6 +21,9 @@ namespace ErsatzTV.Core.Metadata.Nfo [XmlElement("tagline")] public string Tagline { get; set; } + [XmlElement("mpaa")] + public string ContentRating { get; set; } + [XmlElement("premiered")] public string Premiered { get; set; } diff --git a/ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs b/ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs index b974330be..6b702f938 100644 --- a/ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs +++ b/ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs @@ -158,7 +158,7 @@ namespace ErsatzTV.Core.Metadata .HeadOrNone(); // skip folder if etag matches - if (await knownFolder.Map(f => f.Etag).IfNoneAsync(string.Empty) == etag) + if (await knownFolder.Map(f => f.Etag ?? string.Empty).IfNoneAsync(string.Empty) == etag) { continue; } diff --git a/ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs b/ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs index ffcf02134..6a1cf0524 100644 --- a/ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs +++ b/ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs @@ -173,6 +173,19 @@ namespace ErsatzTV.Core.Plex await maybeMetadata.Match( async fullMetadata => { + if (existingMetadata.MetadataKind != MetadataKind.External) + { + existingMetadata.MetadataKind = MetadataKind.External; + await _metadataRepository.MarkAsExternal(existingMetadata); + } + + if (existingMetadata.ContentRating != fullMetadata.ContentRating) + { + existingMetadata.ContentRating = fullMetadata.ContentRating; + await _metadataRepository.SetContentRating(existingMetadata, fullMetadata.ContentRating); + result.IsUpdated = true; + } + foreach (Genre genre in existingMetadata.Genres .Filter(g => fullMetadata.Genres.All(g2 => g2.Name != g.Name)) .ToList()) diff --git a/ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs b/ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs index f44004299..cd082ada6 100644 --- a/ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs +++ b/ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs @@ -146,6 +146,19 @@ namespace ErsatzTV.Core.Plex await maybeMetadata.Match( async fullMetadata => { + if (existingMetadata.MetadataKind != MetadataKind.External) + { + existingMetadata.MetadataKind = MetadataKind.External; + await _metadataRepository.MarkAsExternal(existingMetadata); + } + + if (existingMetadata.ContentRating != fullMetadata.ContentRating) + { + existingMetadata.ContentRating = fullMetadata.ContentRating; + await _metadataRepository.SetContentRating(existingMetadata, fullMetadata.ContentRating); + result.IsUpdated = true; + } + foreach (Genre genre in existingMetadata.Genres .Filter(g => fullMetadata.Genres.All(g2 => g2.Name != g.Name)) .ToList()) diff --git a/ErsatzTV.Infrastructure/Data/Repositories/EmbyTelevisionRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/EmbyTelevisionRepository.cs index 288fbc46c..43202a3ec 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/EmbyTelevisionRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/EmbyTelevisionRepository.cs @@ -105,6 +105,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories // metadata ShowMetadata metadata = existing.ShowMetadata.Head(); ShowMetadata incomingMetadata = show.ShowMetadata.Head(); + metadata.MetadataKind = incomingMetadata.MetadataKind; + metadata.ContentRating = incomingMetadata.ContentRating; metadata.Title = incomingMetadata.Title; metadata.SortTitle = incomingMetadata.SortTitle; metadata.Plot = incomingMetadata.Plot; diff --git a/ErsatzTV.Infrastructure/Data/Repositories/JellyfinTelevisionRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/JellyfinTelevisionRepository.cs index f20703dc6..a07e49a0c 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/JellyfinTelevisionRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/JellyfinTelevisionRepository.cs @@ -105,6 +105,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories // metadata ShowMetadata metadata = existing.ShowMetadata.Head(); ShowMetadata incomingMetadata = show.ShowMetadata.Head(); + metadata.MetadataKind = incomingMetadata.MetadataKind; + metadata.ContentRating = incomingMetadata.ContentRating; metadata.Title = incomingMetadata.Title; metadata.SortTitle = incomingMetadata.SortTitle; metadata.Plot = incomingMetadata.Plot; diff --git a/ErsatzTV.Infrastructure/Data/Repositories/MetadataRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/MetadataRepository.cs index f63db2c11..987aa3fbd 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/MetadataRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/MetadataRepository.cs @@ -228,6 +228,26 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @"UPDATE EpisodeMetadata SET DateUpdated = @DateUpdated WHERE Id = @Id", new { DateUpdated = dateUpdated, metadata.Id }).ToUnit(); + public Task MarkAsExternal(ShowMetadata metadata) => + _dbConnection.ExecuteAsync( + @"UPDATE ShowMetadata SET MetadataKind = @Kind WHERE Id = @Id", + new { metadata.Id, Kind = (int) MetadataKind.External }).ToUnit(); + + public Task SetContentRating(ShowMetadata metadata, string contentRating) => + _dbConnection.ExecuteAsync( + @"UPDATE ShowMetadata SET ContentRating = @ContentRating WHERE Id = @Id", + new { metadata.Id, ContentRating = contentRating }).ToUnit(); + + public Task MarkAsExternal(MovieMetadata metadata) => + _dbConnection.ExecuteAsync( + @"UPDATE MovieMetadata SET MetadataKind = @Kind WHERE Id = @Id", + new { metadata.Id, Kind = (int) MetadataKind.External }).ToUnit(); + + public Task SetContentRating(MovieMetadata metadata, string contentRating) => + _dbConnection.ExecuteAsync( + @"UPDATE MovieMetadata SET ContentRating = @ContentRating WHERE Id = @Id", + new { metadata.Id, ContentRating = contentRating }).ToUnit(); + public Task RemoveGenre(Genre genre) => _dbConnection.ExecuteAsync("DELETE FROM Genre WHERE Id = @GenreId", new { GenreId = genre.Id }) .Map(result => result > 0); diff --git a/ErsatzTV.Infrastructure/Data/Repositories/MovieRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/MovieRepository.cs index c137de8ca..f5b9e723f 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/MovieRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/MovieRepository.cs @@ -327,6 +327,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories // metadata MovieMetadata metadata = existing.MovieMetadata.Head(); MovieMetadata incomingMetadata = movie.MovieMetadata.Head(); + metadata.MetadataKind = incomingMetadata.MetadataKind; + metadata.ContentRating = incomingMetadata.ContentRating; metadata.Title = incomingMetadata.Title; metadata.SortTitle = incomingMetadata.SortTitle; metadata.Plot = incomingMetadata.Plot; @@ -528,6 +530,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories // metadata MovieMetadata metadata = existing.MovieMetadata.Head(); MovieMetadata incomingMetadata = movie.MovieMetadata.Head(); + metadata.MetadataKind = incomingMetadata.MetadataKind; + metadata.ContentRating = incomingMetadata.ContentRating; metadata.Title = incomingMetadata.Title; metadata.SortTitle = incomingMetadata.SortTitle; metadata.Plot = incomingMetadata.Plot; diff --git a/ErsatzTV.Infrastructure/Emby/EmbyApiClient.cs b/ErsatzTV.Infrastructure/Emby/EmbyApiClient.cs index 64208b1ec..06a6f3d02 100644 --- a/ErsatzTV.Infrastructure/Emby/EmbyApiClient.cs +++ b/ErsatzTV.Infrastructure/Emby/EmbyApiClient.cs @@ -230,12 +230,14 @@ namespace ErsatzTV.Infrastructure.Emby var metadata = new MovieMetadata { + MetadataKind = MetadataKind.External, Title = item.Name, SortTitle = _fallbackMetadataProvider.GetSortTitle(item.Name), Plot = item.Overview, Year = item.ProductionYear, Tagline = Optional(item.Taglines).Flatten().HeadOrNone().IfNone(string.Empty), DateAdded = dateAdded, + ContentRating = item.OfficialRating, 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(), @@ -324,12 +326,14 @@ namespace ErsatzTV.Infrastructure.Emby var metadata = new ShowMetadata { + MetadataKind = MetadataKind.External, Title = item.Name, SortTitle = _fallbackMetadataProvider.GetSortTitle(item.Name), Plot = item.Overview, Year = item.ProductionYear, Tagline = Optional(item.Taglines).Flatten().HeadOrNone().IfNone(string.Empty), DateAdded = dateAdded, + ContentRating = item.OfficialRating, 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(), @@ -393,6 +397,7 @@ namespace ErsatzTV.Infrastructure.Emby var metadata = new SeasonMetadata { + MetadataKind = MetadataKind.External, Title = item.Name, SortTitle = _fallbackMetadataProvider.GetSortTitle(item.Name), Year = item.ProductionYear, @@ -498,6 +503,7 @@ namespace ErsatzTV.Infrastructure.Emby var metadata = new EpisodeMetadata { + MetadataKind = MetadataKind.External, Title = item.Name, SortTitle = _fallbackMetadataProvider.GetSortTitle(item.Name), Plot = item.Overview, diff --git a/ErsatzTV.Infrastructure/Emby/IEmbyApi.cs b/ErsatzTV.Infrastructure/Emby/IEmbyApi.cs index 34837a112..318608619 100644 --- a/ErsatzTV.Infrastructure/Emby/IEmbyApi.cs +++ b/ErsatzTV.Infrastructure/Emby/IEmbyApi.cs @@ -28,7 +28,7 @@ namespace ErsatzTV.Infrastructure.Emby string parentId, [Query] string fields = - "Path,Genres,Tags,DateCreated,Etag,Overview,Taglines,Studios,People,ProductionYear,PremiereDate,MediaSources", + "Path,Genres,Tags,DateCreated,Etag,Overview,Taglines,Studios,People,ProductionYear,PremiereDate,MediaSources,OfficialRating", [Query] string includeItemTypes = "Movie"); @@ -40,7 +40,7 @@ namespace ErsatzTV.Infrastructure.Emby string parentId, [Query] string fields = - "Path,Genres,Tags,DateCreated,Etag,Overview,Taglines,Studios,People,ProductionYear,PremiereDate,MediaSources", + "Path,Genres,Tags,DateCreated,Etag,Overview,Taglines,Studios,People,ProductionYear,PremiereDate,MediaSources,OfficialRating", [Query] string includeItemTypes = "Series"); diff --git a/ErsatzTV.Infrastructure/Emby/Models/EmbyLibraryItemResponse.cs b/ErsatzTV.Infrastructure/Emby/Models/EmbyLibraryItemResponse.cs index b423cb2b2..443559c4d 100644 --- a/ErsatzTV.Infrastructure/Emby/Models/EmbyLibraryItemResponse.cs +++ b/ErsatzTV.Infrastructure/Emby/Models/EmbyLibraryItemResponse.cs @@ -9,6 +9,7 @@ namespace ErsatzTV.Infrastructure.Emby.Models public string Id { get; set; } public string Etag { get; set; } public string Path { get; set; } + public string OfficialRating { get; set; } public DateTimeOffset DateCreated { get; set; } public long RunTimeTicks { get; set; } public List Genres { get; set; } diff --git a/ErsatzTV.Infrastructure/Jellyfin/IJellyfinApi.cs b/ErsatzTV.Infrastructure/Jellyfin/IJellyfinApi.cs index 65036ae25..a4834e0f5 100644 --- a/ErsatzTV.Infrastructure/Jellyfin/IJellyfinApi.cs +++ b/ErsatzTV.Infrastructure/Jellyfin/IJellyfinApi.cs @@ -34,7 +34,7 @@ namespace ErsatzTV.Infrastructure.Jellyfin [Query] string parentId, [Query] - string fields = "Path,Genres,Tags,DateCreated,Etag,Overview,Taglines,Studios,People", + string fields = "Path,Genres,Tags,DateCreated,Etag,Overview,Taglines,Studios,People,OfficialRating", [Query] string includeItemTypes = "Movie"); @@ -47,7 +47,7 @@ namespace ErsatzTV.Infrastructure.Jellyfin [Query] string parentId, [Query] - string fields = "Path,Genres,Tags,DateCreated,Etag,Overview,Taglines,Studios,People", + string fields = "Path,Genres,Tags,DateCreated,Etag,Overview,Taglines,Studios,People,OfficialRating", [Query] string includeItemTypes = "Series"); diff --git a/ErsatzTV.Infrastructure/Jellyfin/JellyfinApiClient.cs b/ErsatzTV.Infrastructure/Jellyfin/JellyfinApiClient.cs index 788f32648..12969b83f 100644 --- a/ErsatzTV.Infrastructure/Jellyfin/JellyfinApiClient.cs +++ b/ErsatzTV.Infrastructure/Jellyfin/JellyfinApiClient.cs @@ -276,12 +276,14 @@ namespace ErsatzTV.Infrastructure.Jellyfin var metadata = new MovieMetadata { + MetadataKind = MetadataKind.External, Title = item.Name, SortTitle = _fallbackMetadataProvider.GetSortTitle(item.Name), Plot = item.Overview, Year = item.ProductionYear, Tagline = Optional(item.Taglines).Flatten().HeadOrNone().IfNone(string.Empty), DateAdded = dateAdded, + ContentRating = item.OfficialRating, 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(), @@ -375,12 +377,14 @@ namespace ErsatzTV.Infrastructure.Jellyfin var metadata = new ShowMetadata { + MetadataKind = MetadataKind.External, Title = item.Name, SortTitle = _fallbackMetadataProvider.GetSortTitle(item.Name), Plot = item.Overview, Year = item.ProductionYear, Tagline = Optional(item.Taglines).Flatten().HeadOrNone().IfNone(string.Empty), DateAdded = dateAdded, + ContentRating = item.OfficialRating, 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(), @@ -449,6 +453,7 @@ namespace ErsatzTV.Infrastructure.Jellyfin var metadata = new SeasonMetadata { + MetadataKind = MetadataKind.External, Title = item.Name, SortTitle = _fallbackMetadataProvider.GetSortTitle(item.Name), Year = item.ProductionYear, @@ -494,7 +499,7 @@ namespace ErsatzTV.Infrastructure.Jellyfin } catch (Exception ex) { - _logger.LogWarning(ex, "Error projecting Jellyfin show"); + _logger.LogWarning(ex, "Error projecting Jellyfin season"); return None; } } @@ -542,7 +547,7 @@ namespace ErsatzTV.Infrastructure.Jellyfin } catch (Exception ex) { - _logger.LogWarning(ex, "Error projecting Jellyfin movie"); + _logger.LogWarning(ex, "Error projecting Jellyfin episode"); return None; } } @@ -554,6 +559,7 @@ namespace ErsatzTV.Infrastructure.Jellyfin var metadata = new EpisodeMetadata { + MetadataKind = MetadataKind.External, Title = item.Name, SortTitle = _fallbackMetadataProvider.GetSortTitle(item.Name), Plot = item.Overview, diff --git a/ErsatzTV.Infrastructure/Jellyfin/Models/JellyfinLibraryItemResponse.cs b/ErsatzTV.Infrastructure/Jellyfin/Models/JellyfinLibraryItemResponse.cs index 4c37d52ce..633e7e17a 100644 --- a/ErsatzTV.Infrastructure/Jellyfin/Models/JellyfinLibraryItemResponse.cs +++ b/ErsatzTV.Infrastructure/Jellyfin/Models/JellyfinLibraryItemResponse.cs @@ -9,6 +9,7 @@ namespace ErsatzTV.Infrastructure.Jellyfin.Models public string Id { get; set; } public string Etag { get; set; } public string Path { get; set; } + public string OfficialRating { get; set; } public DateTimeOffset DateCreated { get; set; } public long RunTimeTicks { get; set; } public List Genres { get; set; } diff --git a/ErsatzTV.Infrastructure/Migrations/20210527013212_Add_MovieMetadataShowMetadataContentRating.Designer.cs b/ErsatzTV.Infrastructure/Migrations/20210527013212_Add_MovieMetadataShowMetadataContentRating.Designer.cs new file mode 100644 index 000000000..71f3d9277 --- /dev/null +++ b/ErsatzTV.Infrastructure/Migrations/20210527013212_Add_MovieMetadataShowMetadataContentRating.Designer.cs @@ -0,0 +1,2717 @@ +// +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("20210527013212_Add_MovieMetadataShowMetadataContentRating")] + partial class Add_MovieMetadataShowMetadataContentRating + { + 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.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.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.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.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("Genres"); + + b.Navigation("Studios"); + + b.Navigation("Tags"); + }); + + 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/20210527013212_Add_MovieMetadataShowMetadataContentRating.cs b/ErsatzTV.Infrastructure/Migrations/20210527013212_Add_MovieMetadataShowMetadataContentRating.cs new file mode 100644 index 000000000..8abf5cb5e --- /dev/null +++ b/ErsatzTV.Infrastructure/Migrations/20210527013212_Add_MovieMetadataShowMetadataContentRating.cs @@ -0,0 +1,50 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace ErsatzTV.Infrastructure.Migrations +{ + public partial class Add_MovieMetadataShowMetadataContentRating : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + // local and plex + migrationBuilder.Sql("UPDATE MovieMetadata SET DateUpdated = '0001-01-01 00:00:00'"); + migrationBuilder.Sql("UPDATE ShowMetadata SET DateUpdated = '0001-01-01 00:00:00'"); + migrationBuilder.Sql( + @"UPDATE LibraryFolder SET Etag = NULL WHERE LibraryPathId IN + (SELECT LibraryPathId FROM LibraryPath LP + INNER JOIN Library L on LP.LibraryId = L.Id + WHERE L.MediaKind = 1)"); + + // emby + migrationBuilder.Sql("UPDATE EmbyMovie SET Etag = NULL"); + migrationBuilder.Sql("UPDATE EmbyShow SET Etag = NULL"); + + // jellyfin + migrationBuilder.Sql("UPDATE JellyfinMovie SET Etag = NULL"); + migrationBuilder.Sql("UPDATE JellyfinShow SET Etag = NULL"); + + migrationBuilder.AddColumn( + "ContentRating", + "ShowMetadata", + "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + "ContentRating", + "MovieMetadata", + "TEXT", + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + "ContentRating", + "ShowMetadata"); + + migrationBuilder.DropColumn( + "ContentRating", + "MovieMetadata"); + } + } +} diff --git a/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs b/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs index 92fa1cb75..209dedb07 100644 --- a/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs +++ b/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs @@ -794,6 +794,9 @@ namespace ErsatzTV.Infrastructure.Migrations .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); + b.Property("ContentRating") + .HasColumnType("TEXT"); + b.Property("DateAdded") .HasColumnType("TEXT"); @@ -1170,6 +1173,9 @@ namespace ErsatzTV.Infrastructure.Migrations .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); + b.Property("ContentRating") + .HasColumnType("TEXT"); + b.Property("DateAdded") .HasColumnType("TEXT"); diff --git a/ErsatzTV.Infrastructure/Plex/Models/PlexMetadataResponse.cs b/ErsatzTV.Infrastructure/Plex/Models/PlexMetadataResponse.cs index b0641f872..b9316396c 100644 --- a/ErsatzTV.Infrastructure/Plex/Models/PlexMetadataResponse.cs +++ b/ErsatzTV.Infrastructure/Plex/Models/PlexMetadataResponse.cs @@ -6,6 +6,7 @@ namespace ErsatzTV.Infrastructure.Plex.Models { public string Key { get; set; } public string Title { get; set; } + public string ContentRating { get; set; } public string Summary { get; set; } public int Year { get; set; } public string Tagline { get; set; } diff --git a/ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs b/ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs index 15ee71465..63b938e0a 100644 --- a/ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs +++ b/ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs @@ -250,11 +250,13 @@ namespace ErsatzTV.Infrastructure.Plex var metadata = new MovieMetadata { + MetadataKind = MetadataKind.External, Title = response.Title, SortTitle = _fallbackMetadataProvider.GetSortTitle(response.Title), Plot = response.Summary, Year = response.Year, Tagline = response.Tagline, + ContentRating = response.ContentRating, DateAdded = dateAdded, DateUpdated = lastWriteTime, Genres = Optional(response.Genre).Flatten().Map(g => new Genre { Name = g.Tag }).ToList(), @@ -398,11 +400,13 @@ namespace ErsatzTV.Infrastructure.Plex var metadata = new ShowMetadata { + MetadataKind = MetadataKind.External, Title = response.Title, SortTitle = _fallbackMetadataProvider.GetSortTitle(response.Title), Plot = response.Summary, Year = response.Year, Tagline = response.Tagline, + ContentRating = response.ContentRating, DateAdded = dateAdded, DateUpdated = lastWriteTime, Genres = Optional(response.Genre).Flatten().Map(g => new Genre { Name = g.Tag }).ToList(), @@ -462,6 +466,7 @@ namespace ErsatzTV.Infrastructure.Plex var metadata = new SeasonMetadata { + MetadataKind = MetadataKind.External, Title = response.Title, SortTitle = _fallbackMetadataProvider.GetSortTitle(response.Title), Year = response.Year, @@ -518,6 +523,7 @@ namespace ErsatzTV.Infrastructure.Plex var metadata = new EpisodeMetadata { + MetadataKind = MetadataKind.External, Title = response.Title, SortTitle = _fallbackMetadataProvider.GetSortTitle(response.Title), Plot = response.Summary, diff --git a/ErsatzTV.Infrastructure/Search/SearchIndex.cs b/ErsatzTV.Infrastructure/Search/SearchIndex.cs index 2ed10ac14..8d31809ff 100644 --- a/ErsatzTV.Infrastructure/Search/SearchIndex.cs +++ b/ErsatzTV.Infrastructure/Search/SearchIndex.cs @@ -11,6 +11,9 @@ using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Search; using LanguageExt; using LanguageExt.UnsafeValueAccess; +using Lucene.Net.Analysis; +using Lucene.Net.Analysis.Core; +using Lucene.Net.Analysis.Miscellaneous; using Lucene.Net.Analysis.Standard; using Lucene.Net.Documents; using Lucene.Net.Index; @@ -45,6 +48,7 @@ namespace ErsatzTV.Infrastructure.Search private const string StyleField = "style"; private const string MoodField = "mood"; private const string ActorField = "actor"; + private const string ContentRatingField = "content_rating"; private const string MovieType = "movie"; private const string ShowType = "show"; @@ -65,7 +69,7 @@ namespace ErsatzTV.Infrastructure.Search _initialized = false; } - public int Version => 9; + public int Version => 10; public Task Initialize(ILocalFileSystem localFileSystem) { @@ -162,9 +166,14 @@ namespace ErsatzTV.Infrastructure.Search var searcher = new IndexSearcher(reader); int hitsLimit = limit == 0 ? searcher.IndexReader.MaxDoc : skip + limit; using var analyzer = new StandardAnalyzer(AppLuceneVersion); + var customAnalyzers = new Dictionary + { + { ContentRatingField, new KeywordAnalyzer() } + }; + using var analyzerWrapper = new PerFieldAnalyzerWrapper(analyzer, customAnalyzers); QueryParser parser = !string.IsNullOrWhiteSpace(searchField) - ? new QueryParser(AppLuceneVersion, searchField, analyzer) - : new MultiFieldQueryParser(AppLuceneVersion, new[] { TitleField }, analyzer); + ? new QueryParser(AppLuceneVersion, searchField, analyzerWrapper) + : new MultiFieldQueryParser(AppLuceneVersion, new[] { TitleField }, analyzerWrapper); parser.AllowLeadingWildcard = true; Query query = ParseQuery(searchQuery, parser); var filter = new DuplicateFilter(TitleAndYearField); @@ -266,6 +275,15 @@ namespace ErsatzTV.Infrastructure.Search AddLanguages(doc, movie.MediaVersions); + if (!string.IsNullOrWhiteSpace(metadata.ContentRating)) + { + foreach (string contentRating in (metadata.ContentRating ?? string.Empty).Split("/") + .Map(x => x.Trim()).Where(x => !string.IsNullOrWhiteSpace(x))) + { + doc.Add(new StringField(ContentRatingField, contentRating, Field.Store.NO)); + } + } + if (metadata.ReleaseDate.HasValue) { doc.Add( @@ -368,6 +386,15 @@ namespace ErsatzTV.Infrastructure.Search doc.Add(new TextField(LanguageField, cultureInfo.EnglishName, Field.Store.NO)); } + if (!string.IsNullOrWhiteSpace(metadata.ContentRating)) + { + foreach (string contentRating in (metadata.ContentRating ?? string.Empty).Split("/") + .Map(x => x.Trim()).Where(x => !string.IsNullOrWhiteSpace(x))) + { + doc.Add(new StringField(ContentRatingField, contentRating, Field.Store.NO)); + } + } + if (metadata.ReleaseDate.HasValue) { doc.Add( diff --git a/ErsatzTV/Pages/Movie.razor b/ErsatzTV/Pages/Movie.razor index e9c43e0e0..4fda2b9f0 100644 --- a/ErsatzTV/Pages/Movie.razor +++ b/ErsatzTV/Pages/Movie.razor @@ -63,6 +63,18 @@ + @if (_sortedContentRatings.Any()) + { +
+ Content Ratings:  + @_sortedContentRatings.Head() + @foreach (string contentRating in _sortedContentRatings.Skip(1)) + { + + @contentRating + } +
+ } @if (_sortedLanguages.Any()) {
@@ -136,6 +148,7 @@ public int MovieId { get; set; } private MovieViewModel _movie; + private List _sortedContentRatings = new(); private List _sortedLanguages = new(); private List _sortedStudios = new(); private List _sortedGenres = new(); @@ -147,6 +160,7 @@ _mediator.Send(new GetMovieById(MovieId)).IfSomeAsync(vm => { _movie = vm; + _sortedContentRatings = _movie.ContentRatings.OrderBy(cr => cr).ToList(); _sortedLanguages = _movie.Languages.OrderBy(ci => ci.EnglishName).ToList(); _sortedStudios = _movie.Studios.OrderBy(s => s).ToList(); _sortedGenres = _movie.Genres.OrderBy(g => g).ToList(); diff --git a/ErsatzTV/Pages/TelevisionSeasonList.razor b/ErsatzTV/Pages/TelevisionSeasonList.razor index e1bc29b85..0ba146c91 100644 --- a/ErsatzTV/Pages/TelevisionSeasonList.razor +++ b/ErsatzTV/Pages/TelevisionSeasonList.razor @@ -77,6 +77,18 @@
+ @if (_sortedContentRatings.Any()) + { +
+ Content Ratings:  + @_sortedContentRatings.Head() + @foreach (string contentRating in _sortedContentRatings.Skip(1)) + { + + @contentRating + } +
+ } @if (_sortedLanguages.Any()) {
@@ -161,6 +173,7 @@ public int ShowId { get; set; } private TelevisionShowViewModel _show; + private List _sortedContentRatings = new(); private List _sortedLanguages = new(); private List _sortedStudios = new(); private List _sortedGenres = new(); @@ -179,6 +192,7 @@ .IfSomeAsync(vm => { _show = vm; + _sortedContentRatings = _show.ContentRatings.OrderBy(cr => cr).ToList(); _sortedLanguages = _show.Languages.OrderBy(ci => ci.EnglishName).ToList(); _sortedStudios = _show.Studios.OrderBy(s => s).ToList(); _sortedGenres = _show.Genres.OrderBy(g => g).ToList(); diff --git a/docs/user-guide/search.md b/docs/user-guide/search.md index 62609c9b5..32a6e344c 100644 --- a/docs/user-guide/search.md +++ b/docs/user-guide/search.md @@ -19,6 +19,7 @@ The following fields are available for searching movies: - `studio`: The movie studio - `actor`: An actor from the movie - `library_name`: The name of the library that contains the movie +- `content_rating`: The movie content rating (case-sensitive) - `language`: The movie audio stream language - `release_date`: The movie release date (YYYYMMDD) - `type`: Always `movie` @@ -34,6 +35,7 @@ The following fields are available for searching shows: - `studio`: The show studio - `actor`: An actor from the show - `library_name`: The name of the library that contains the show +- `content_rating`: The movie content rating (case-sensitive) - `language`: The show audio stream language - `release_date`: The show release date (YYYYMMDD) - `type`: Always `show`