diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ffb17083..d4ef13841 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,10 +13,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Fix occasional erroneous log messages when HLS channel playback times out because all clients have left - Fix fallback filler playback - Fix stream continuity when error messages are displayed +- Fix duplicate scanning within other video libraries (i.e. folders would be scanned multiple times) ### Added - Add `show_genre` and `show_tag` to search index for seasons and episodes - Use `aired` value to source release date from music video nfo metadata +- Add NFO metadata support to `Other Video` libraries + - `Other Video` NFO metadata must be in the movie NFO metadata format ## [0.5.5-beta] - 2022-05-03 ### Fixed diff --git a/ErsatzTV.Application/MediaSources/Commands/ScanLocalLibraryHandler.cs b/ErsatzTV.Application/MediaSources/Commands/ScanLocalLibraryHandler.cs index f7d9ca9e9..75aae5eab 100644 --- a/ErsatzTV.Application/MediaSources/Commands/ScanLocalLibraryHandler.cs +++ b/ErsatzTV.Application/MediaSources/Commands/ScanLocalLibraryHandler.cs @@ -5,6 +5,7 @@ using ErsatzTV.Core.Interfaces.Locking; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Metadata; +using Humanizer; using Microsoft.Extensions.Logging; namespace ErsatzTV.Application.MediaSources; @@ -147,7 +148,7 @@ public class ScanLocalLibraryHandler : IRequestHandler _otherVideoNfoReader = new OtherVideoNfoReader(new Mock().Object); + + private OtherVideoNfoReader _otherVideoNfoReader; + + [Test] + public async Task ParsingNfo_Should_Return_Error() + { + await using var stream = + new MemoryStream(Encoding.UTF8.GetBytes(@"https://www.themoviedb.org/movie/11-star-wars")); + + Either result = await _otherVideoNfoReader.Read(stream); + + result.IsLeft.Should().BeTrue(); + } + + [Test] + public async Task MetadataNfo_Should_Return_Nfo() + { + await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(@"")); + + Either result = await _otherVideoNfoReader.Read(stream); + + result.IsRight.Should().BeTrue(); + } + + [Test] + public async Task CombinationNfo_Should_Return_Nfo() + { + await using var stream = new MemoryStream( + Encoding.UTF8.GetBytes( + @" +https://www.themoviedb.org/movie/11-star-wars")); + + Either result = await _otherVideoNfoReader.Read(stream); + + result.IsRight.Should().BeTrue(); + } + + [Test] + public async Task FullSample_Should_Return_Nfo() + { + await using var stream = new MemoryStream( + Encoding.UTF8.GetBytes( + @" + + Zack Snyder's Justice League + Zack Snyder's Justice League + Justice League 2 + + + 8.300000 + 197786 + + + 8.700000 + 3461 + + + 8.195670 + 4247 + + + 0 + 140 + + Determined to ensure Superman's ultimate sacrifice was not in vain, Bruce Wayne aligns forces with Diana Prince with plans to recruit a team of metahumans to protect the world from an approaching threat of catastrophic proportions. + + 242 + https://assets.fanart.tv/fanart/movies/791373/movieposter/zack-snyders-justice-league-603fdb873f474.jpg + https://image.tmdb.org/t/p/original/tnAuB8q5vv7Ax9UAEje5Xi4BXik.jpg + https://assets.fanart.tv/fanart/movies/791373/moviethumb/zack-snyders-justice-league-6050310135cf6.jpg + https://image.tmdb.org/t/p/original/wcYBuOZDP6Vi8Ye4qax3Zx9dCan.jpg + https://assets.fanart.tv/fanart/movies/791373/movieposter/zack-snyders-justice-league-603fdba9bdd16.jpg + https://assets.fanart.tv/fanart/movies/791373/hdmovielogo/zack-snyders-justice-league-5ed3f2e4952e9.png + https://assets.fanart.tv/fanart/movies/791373/moviebanner/zack-snyders-justice-league-6050049514d4c.jpg + + https://assets.fanart.tv/fanart/movies/791373/moviebackground/zack-snyders-justice-league-5fee5b9fe0e0d.jpg + https://image.tmdb.org/t/p/original/43NwryODVEsbBDC0jK3wYfVyb5q.jpg + + Australia:M + 0 + + 791373 + tt12361974 + 791373 + SuperHero + TV Recording + + Justice League Collection + Based on the DC Comics superhero team + + USA + Chris Terrio + Zack Snyder + 2021-03-18 + 2021 + + + + Warner Bros. Pictures + + + + + + + + eng + + + + + Ben Affleck + Bruce Wayne / Batman + 0 + https://image.tmdb.org/t/p/original/u525jeDOzg9hVdvYfeehTGnw7Aa.jpg + + + Henry Cavill + Clark Kent / Superman / Kal-El + 1 + https://image.tmdb.org/t/p/original/hErUwonrQgY5Y7RfxOfv8Fq11MB.jpg + + + Gal Gadot + Diana Prince / Wonder Woman + 2 + https://image.tmdb.org/t/p/original/fysvehTvU6bE3JgxaOTRfvQJzJ4.jpg + + + 0.000000 + 0.000000 + + 2021-03-26 11:35:50 +")); + + Either result = await _otherVideoNfoReader.Read(stream); + + result.IsRight.Should().BeTrue(); + + foreach (OtherVideoNfo nfo in result.RightToSeq()) + { + nfo.Title.Should().Be("Zack Snyder's Justice League"); + nfo.SortTitle.Should().Be("Justice League 2"); + nfo.Outline.Should().BeNullOrEmpty(); + nfo.Year.Should().Be(2021); + nfo.ContentRating.Should().Be("Australia:M"); + + nfo.Premiered.IsSome.Should().BeTrue(); + foreach (DateTime premiered in nfo.Premiered) + { + premiered.Should().Be(new DateTime(2021, 03, 18)); + } + + nfo.Plot.Should().Be( + "Determined to ensure Superman's ultimate sacrifice was not in vain, Bruce Wayne aligns forces with Diana Prince with plans to recruit a team of metahumans to protect the world from an approaching threat of catastrophic proportions."); + nfo.Tagline.Should().BeNullOrEmpty(); + nfo.Genres.Should().BeEquivalentTo(new List { "SuperHero" }); + nfo.Tags.Should().BeEquivalentTo(new List { "TV Recording" }); + nfo.Studios.Should().BeEquivalentTo(new List { "Warner Bros. Pictures" }); + nfo.Actors.Should().BeEquivalentTo( + new List + { + new() + { + Name = "Ben Affleck", Order = 0, Role = "Bruce Wayne / Batman", + Thumb = "https://image.tmdb.org/t/p/original/u525jeDOzg9hVdvYfeehTGnw7Aa.jpg" + }, + new() + { + Name = "Henry Cavill", Order = 1, Role = "Clark Kent / Superman / Kal-El", + Thumb = "https://image.tmdb.org/t/p/original/hErUwonrQgY5Y7RfxOfv8Fq11MB.jpg" + }, + new() + { + Name = "Gal Gadot", Order = 2, Role = "Diana Prince / Wonder Woman", + Thumb = "https://image.tmdb.org/t/p/original/fysvehTvU6bE3JgxaOTRfvQJzJ4.jpg" + } + }); + nfo.Writers.Should().BeEquivalentTo(new List { "Chris Terrio" }); + nfo.Directors.Should().BeEquivalentTo(new List { "Zack Snyder" }); + nfo.UniqueIds.Should().BeEquivalentTo( + new List + { + new() { Type = "imdb", Guid = "tt12361974", Default = false }, + new() { Type = "tmdb", Guid = "791373", Default = true } + }); + } + } + + [Test] + public async Task MetadataNfo_With_Tag_Should_Return_Nfo() + { + await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(@"Test Tag")); + + Either result = await _otherVideoNfoReader.Read(stream); + + result.IsRight.Should().BeTrue(); + foreach (OtherVideoNfo nfo in result.RightToSeq()) + { + nfo.Tags.Should().BeEquivalentTo(new List { "Test Tag" }); + } + } + + [Test] + public async Task MetadataNfo_With_Outline_Should_Return_Nfo() + { + await using var stream = + new MemoryStream(Encoding.UTF8.GetBytes(@"Test Outline")); + + Either result = await _otherVideoNfoReader.Read(stream); + + result.IsRight.Should().BeTrue(); + foreach (OtherVideoNfo nfo in result.RightToSeq()) + { + nfo.Outline.Should().Be("Test Outline"); + } + } +} diff --git a/ErsatzTV.Core/Domain/Metadata/OtherVideoMetadata.cs b/ErsatzTV.Core/Domain/Metadata/OtherVideoMetadata.cs index e37af02bd..4fd8ab285 100644 --- a/ErsatzTV.Core/Domain/Metadata/OtherVideoMetadata.cs +++ b/ErsatzTV.Core/Domain/Metadata/OtherVideoMetadata.cs @@ -2,6 +2,12 @@ public class OtherVideoMetadata : Metadata { + public string ContentRating { get; set; } + public string Outline { get; set; } + public string Plot { get; set; } + public string Tagline { get; set; } public int OtherVideoId { get; set; } public OtherVideo OtherVideo { get; set; } + public List Directors { get; set; } + public List Writers { get; set; } } diff --git a/ErsatzTV.Core/Interfaces/Metadata/ILocalMetadataProvider.cs b/ErsatzTV.Core/Interfaces/Metadata/ILocalMetadataProvider.cs index 6b10a2ebb..2193e2a48 100644 --- a/ErsatzTV.Core/Interfaces/Metadata/ILocalMetadataProvider.cs +++ b/ErsatzTV.Core/Interfaces/Metadata/ILocalMetadataProvider.cs @@ -11,6 +11,7 @@ public interface ILocalMetadataProvider Task RefreshSidecarMetadata(Episode episode, string nfoFileName); Task RefreshSidecarMetadata(Artist artist, string nfoFileName); Task RefreshSidecarMetadata(MusicVideo musicVideo, string nfoFileName); + Task RefreshSidecarMetadata(OtherVideo otherVideo, string nfoFileName); Task RefreshTagMetadata(Song song, string ffprobePath); Task RefreshFallbackMetadata(Movie movie); Task RefreshFallbackMetadata(Episode episode); diff --git a/ErsatzTV.Core/Interfaces/Metadata/Nfo/IOtherVideoNfoReader.cs b/ErsatzTV.Core/Interfaces/Metadata/Nfo/IOtherVideoNfoReader.cs new file mode 100644 index 000000000..813c4e6d3 --- /dev/null +++ b/ErsatzTV.Core/Interfaces/Metadata/Nfo/IOtherVideoNfoReader.cs @@ -0,0 +1,8 @@ +using ErsatzTV.Core.Metadata.Nfo; + +namespace ErsatzTV.Core.Interfaces.Metadata.Nfo; + +public interface IOtherVideoNfoReader +{ + Task> Read(Stream input); +} diff --git a/ErsatzTV.Core/Interfaces/Repositories/IOtherVideoRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/IOtherVideoRepository.cs index c162c1415..858f9247b 100644 --- a/ErsatzTV.Core/Interfaces/Repositories/IOtherVideoRepository.cs +++ b/ErsatzTV.Core/Interfaces/Repositories/IOtherVideoRepository.cs @@ -8,9 +8,12 @@ public interface IOtherVideoRepository Task>> GetOrAdd(LibraryPath libraryPath, string path); Task> FindOtherVideoPaths(LibraryPath libraryPath); Task> DeleteByPath(LibraryPath libraryPath, string path); + Task AddGenre(OtherVideoMetadata metadata, Genre genre); Task AddTag(OtherVideoMetadata metadata, Tag tag); + Task AddStudio(OtherVideoMetadata metadata, Studio studio); + Task AddActor(OtherVideoMetadata metadata, Actor actor); + Task AddDirector(OtherVideoMetadata metadata, Director director); + Task AddWriter(OtherVideoMetadata metadata, Writer writer); Task> GetOtherVideosForCards(List ids); - // Task GetOtherVideoCount(int artistId); - // Task> GetPagedOtherVideos(int artistId, int pageNumber, int pageSize); } diff --git a/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs b/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs index 7de95a7b3..18d237f2d 100644 --- a/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs +++ b/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs @@ -24,6 +24,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider private readonly IMovieRepository _movieRepository; private readonly IMusicVideoNfoReader _musicVideoNfoReader; private readonly IMusicVideoRepository _musicVideoRepository; + private readonly IOtherVideoNfoReader _otherVideoNfoReader; private readonly IOtherVideoRepository _otherVideoRepository; private readonly ISongRepository _songRepository; private readonly ITelevisionRepository _televisionRepository; @@ -44,6 +45,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider IArtistNfoReader artistNfoReader, IMusicVideoNfoReader musicVideoNfoReader, ITvShowNfoReader tvShowNfoReader, + IOtherVideoNfoReader otherVideoNfoReader, ILocalStatisticsProvider localStatisticsProvider, IClient client, ILogger logger) @@ -62,6 +64,7 @@ public class LocalMetadataProvider : ILocalMetadataProvider _artistNfoReader = artistNfoReader; _musicVideoNfoReader = musicVideoNfoReader; _tvShowNfoReader = tvShowNfoReader; + _otherVideoNfoReader = otherVideoNfoReader; _localStatisticsProvider = localStatisticsProvider; _client = client; _logger = logger; @@ -107,38 +110,77 @@ public class LocalMetadataProvider : ILocalMetadataProvider return fallbackMetadata; } - public Task RefreshSidecarMetadata(Movie movie, string nfoFileName) => - LoadMovieMetadata(movie, nfoFileName).Bind( - maybeMetadata => maybeMetadata.Match( - metadata => ApplyMetadataUpdate(movie, metadata), - () => Task.FromResult(false))); - - public Task RefreshSidecarMetadata(Show televisionShow, string nfoFileName) => - LoadTelevisionShowMetadata(nfoFileName).Bind( - maybeMetadata => maybeMetadata.Match( - metadata => ApplyMetadataUpdate(televisionShow, metadata), - () => Task.FromResult(false))); - - public Task RefreshSidecarMetadata(Episode episode, string nfoFileName) => - LoadEpisodeMetadata(episode, nfoFileName).Bind(metadata => ApplyMetadataUpdate(episode, metadata)); - - public Task RefreshSidecarMetadata(Artist artist, string nfoFileName) => - LoadArtistMetadata(nfoFileName).Bind( - maybeMetadata => maybeMetadata.Match( - metadata => ApplyMetadataUpdate(artist, metadata), - () => Task.FromResult(false))); - - public Task RefreshSidecarMetadata(MusicVideo musicVideo, string nfoFileName) => - LoadMusicVideoMetadata(nfoFileName).Bind( - maybeMetadata => maybeMetadata.Match( - metadata => ApplyMetadataUpdate(musicVideo, metadata), - () => RefreshFallbackMetadata(musicVideo))); - - public Task RefreshTagMetadata(Song song, string ffprobePath) => - LoadSongMetadata(song, ffprobePath).Bind( - maybeMetadata => maybeMetadata.Match( - metadata => ApplyMetadataUpdate(song, metadata), - () => RefreshFallbackMetadata(song))); + public async Task RefreshSidecarMetadata(Movie movie, string nfoFileName) + { + Option maybeMetadata = await LoadMovieMetadata(movie, nfoFileName); + foreach (MovieMetadata metadata in maybeMetadata) + { + return await ApplyMetadataUpdate(movie, metadata); + } + + return false; + } + + public async Task RefreshSidecarMetadata(Show televisionShow, string nfoFileName) + { + Option maybeMetadata = await LoadTelevisionShowMetadata(nfoFileName); + foreach (ShowMetadata metadata in maybeMetadata) + { + return await ApplyMetadataUpdate(televisionShow, metadata); + } + + return false; + } + + public async Task RefreshSidecarMetadata(Episode episode, string nfoFileName) + { + List metadata = await LoadEpisodeMetadata(episode, nfoFileName); + return await ApplyMetadataUpdate(episode, metadata); + } + + public async Task RefreshSidecarMetadata(Artist artist, string nfoFileName) + { + Option maybeMetadata = await LoadArtistMetadata(nfoFileName); + foreach (ArtistMetadata metadata in maybeMetadata) + { + return await ApplyMetadataUpdate(artist, metadata); + } + + return false; + } + + public async Task RefreshSidecarMetadata(MusicVideo musicVideo, string nfoFileName) + { + Option maybeMetadata = await LoadMusicVideoMetadata(nfoFileName); + foreach (MusicVideoMetadata metadata in maybeMetadata) + { + return await ApplyMetadataUpdate(musicVideo, metadata); + } + + return await RefreshFallbackMetadata(musicVideo); + } + + public async Task RefreshSidecarMetadata(OtherVideo otherVideo, string nfoFileName) + { + Option maybeMetadata = await LoadOtherVideoMetadata(nfoFileName); + foreach (OtherVideoMetadata metadata in maybeMetadata) + { + return await ApplyMetadataUpdate(otherVideo, metadata); + } + + return await RefreshFallbackMetadata(otherVideo); + } + + public async Task RefreshTagMetadata(Song song, string ffprobePath) + { + Option maybeMetadata = await LoadSongMetadata(song, ffprobePath); + foreach (SongMetadata metadata in maybeMetadata) + { + return await ApplyMetadataUpdate(song, metadata); + } + + return await RefreshFallbackMetadata(song); + } public Task RefreshFallbackMetadata(Movie movie) => ApplyMetadataUpdate(movie, _fallbackMetadataProvider.GetFallbackMetadata(movie)); @@ -149,20 +191,38 @@ public class LocalMetadataProvider : ILocalMetadataProvider public Task RefreshFallbackMetadata(Artist artist, string artistFolder) => ApplyMetadataUpdate(artist, _fallbackMetadataProvider.GetFallbackMetadataForArtist(artistFolder)); - public Task RefreshFallbackMetadata(OtherVideo otherVideo) => - _fallbackMetadataProvider.GetFallbackMetadata(otherVideo).Match( - metadata => ApplyMetadataUpdate(otherVideo, metadata), - () => Task.FromResult(false)); + public async Task RefreshFallbackMetadata(OtherVideo otherVideo) + { + Option maybeMetadata = _fallbackMetadataProvider.GetFallbackMetadata(otherVideo); + foreach (OtherVideoMetadata metadata in maybeMetadata) + { + return await ApplyMetadataUpdate(otherVideo, metadata); + } - public Task RefreshFallbackMetadata(Song song) => - _fallbackMetadataProvider.GetFallbackMetadata(song).Match( - metadata => ApplyMetadataUpdate(song, metadata), - () => Task.FromResult(false)); + return false; + } - public Task RefreshFallbackMetadata(MusicVideo musicVideo) => - _fallbackMetadataProvider.GetFallbackMetadata(musicVideo).Match( - metadata => ApplyMetadataUpdate(musicVideo, metadata), - () => Task.FromResult(false)); + public async Task RefreshFallbackMetadata(Song song) + { + Option maybeMetadata = _fallbackMetadataProvider.GetFallbackMetadata(song); + foreach (SongMetadata metadata in maybeMetadata) + { + return await ApplyMetadataUpdate(song, metadata); + } + + return false; + } + + public async Task RefreshFallbackMetadata(MusicVideo musicVideo) + { + Option maybeMetadata = _fallbackMetadataProvider.GetFallbackMetadata(musicVideo); + foreach (MusicVideoMetadata metadata in maybeMetadata) + { + return await ApplyMetadataUpdate(musicVideo, metadata); + } + + return false; + } public Task RefreshFallbackMetadata(Show televisionShow, string showFolder) => ApplyMetadataUpdate(televisionShow, _fallbackMetadataProvider.GetFallbackMetadataForShow(showFolder)); @@ -767,6 +827,10 @@ public class LocalMetadataProvider : ILocalMetadataProvider Option maybeMetadata = Optional(otherVideo.OtherVideoMetadata).Flatten().HeadOrNone(); foreach (OtherVideoMetadata existing in maybeMetadata) { + existing.ContentRating = metadata.ContentRating; + existing.Outline = metadata.Outline; + existing.Plot = metadata.Plot; + existing.Tagline = metadata.Tagline; existing.Title = metadata.Title; if (existing.DateAdded == SystemTime.MinValueUtc) @@ -776,6 +840,9 @@ public class LocalMetadataProvider : ILocalMetadataProvider existing.DateUpdated = metadata.DateUpdated; existing.MetadataKind = metadata.MetadataKind; + existing.OriginalTitle = metadata.OriginalTitle; + existing.ReleaseDate = metadata.ReleaseDate; + existing.Year = metadata.Year; existing.SortTitle = string.IsNullOrWhiteSpace(metadata.SortTitle) ? _fallbackMetadataProvider.GetSortTitle(metadata.Title) : metadata.SortTitle; @@ -784,10 +851,70 @@ public class LocalMetadataProvider : ILocalMetadataProvider bool updated = await UpdateMetadataCollections( existing, metadata, - (_, _) => Task.FromResult(false), + _otherVideoRepository.AddGenre, _otherVideoRepository.AddTag, - (_, _) => Task.FromResult(false), - (_, _) => Task.FromResult(false)); + _otherVideoRepository.AddStudio, + _otherVideoRepository.AddActor); + + foreach (Director director in existing.Directors + .Filter(d => metadata.Directors.All(d2 => d2.Name != d.Name)).ToList()) + { + existing.Directors.Remove(director); + if (await _metadataRepository.RemoveDirector(director)) + { + updated = true; + } + } + + foreach (Director director in metadata.Directors + .Filter(d => existing.Directors.All(d2 => d2.Name != d.Name)).ToList()) + { + existing.Directors.Add(director); + if (await _otherVideoRepository.AddDirector(existing, director)) + { + updated = true; + } + } + + foreach (Writer writer in existing.Writers + .Filter(w => metadata.Writers.All(w2 => w2.Name != w.Name)).ToList()) + { + existing.Writers.Remove(writer); + if (await _metadataRepository.RemoveWriter(writer)) + { + updated = true; + } + } + + foreach (Writer writer in metadata.Writers + .Filter(w => existing.Writers.All(w2 => w2.Name != w.Name)).ToList()) + { + existing.Writers.Add(writer); + if (await _otherVideoRepository.AddWriter(existing, writer)) + { + updated = true; + } + } + + foreach (MetadataGuid guid in existing.Guids + .Filter(g => metadata.Guids.All(g2 => g2.Guid != g.Guid)).ToList()) + { + existing.Guids.Remove(guid); + if (await _metadataRepository.RemoveGuid(guid)) + { + updated = true; + } + } + + foreach (MetadataGuid guid in metadata.Guids + .Filter(g => existing.Guids.All(g2 => g2.Guid != g.Guid)).ToList()) + { + existing.Guids.Add(guid); + if (await _metadataRepository.AddGuid(existing, guid)) + { + updated = true; + } + } return await _metadataRepository.Update(existing) || updated; } @@ -1068,6 +1195,81 @@ public class LocalMetadataProvider : ILocalMetadataProvider } } + private async Task> LoadOtherVideoMetadata(string nfoFileName) + { + try + { + await using FileStream fileStream = File.Open(nfoFileName, FileMode.Open, FileAccess.Read); + Either maybeNfo = await _otherVideoNfoReader.Read(fileStream); + foreach (BaseError error in maybeNfo.LeftToSeq()) + { + _logger.LogInformation( + "Failed to read OtherVideo nfo metadata from {Path}: {Error}", + nfoFileName, + error.ToString()); + + return None; + } + + foreach (OtherVideoNfo nfo in maybeNfo.RightToSeq()) + { + DateTime dateAdded = DateTime.UtcNow; + DateTime dateUpdated = File.GetLastWriteTimeUtc(nfoFileName); + + var year = 0; + if (nfo.Year > 1000) + { + year = nfo.Year; + } + + DateTime releaseDate = year > 0 + ? new DateTimeOffset(year, 1, 1, 0, 0, 0, TimeSpan.Zero).UtcDateTime + : SystemTime.MinValueUtc; + + foreach (DateTime premiered in nfo.Premiered) + { + if (year == 0) + { + year = premiered.Year; + } + + releaseDate = premiered; + } + + return new OtherVideoMetadata + { + MetadataKind = MetadataKind.Sidecar, + DateAdded = dateAdded, + DateUpdated = dateUpdated, + Title = nfo.Title, + SortTitle = nfo.SortTitle, + Year = year, + ContentRating = nfo.ContentRating, + ReleaseDate = releaseDate, + Plot = nfo.Plot, + Outline = nfo.Outline, + Tagline = nfo.Tagline, + 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), + Directors = nfo.Directors.Map(d => new Director { Name = d }).ToList(), + Writers = nfo.Writers.Map(w => new Writer { Name = w }).ToList(), + Guids = nfo.UniqueIds + .Map(id => new MetadataGuid { Guid = $"{id.Type}://{id.Guid}" }) + .ToList() + }; + } + } + catch (Exception ex) + { + _logger.LogInformation(ex, "Failed to read OtherVideo nfo metadata from {Path}", nfoFileName); + _client.Notify(ex); + } + + return None; + } + private static int? GetYear(int? year, Option premiered) { if (year is > 1000) diff --git a/ErsatzTV.Core/Metadata/Nfo/OtherVideoNfo.cs b/ErsatzTV.Core/Metadata/Nfo/OtherVideoNfo.cs new file mode 100644 index 000000000..bc5276b0d --- /dev/null +++ b/ErsatzTV.Core/Metadata/Nfo/OtherVideoNfo.cs @@ -0,0 +1,52 @@ +using System.Xml.Serialization; + +namespace ErsatzTV.Core.Metadata.Nfo; + +[XmlRoot("movie")] +public class OtherVideoNfo +{ + [XmlElement("title")] + public string Title { get; set; } + + [XmlElement("sorttitle")] + public string SortTitle { get; set; } + + [XmlElement("outline")] + public string Outline { get; set; } + + [XmlElement("year")] + public int Year { get; set; } + + [XmlElement("mpaa")] + public string ContentRating { get; set; } + + [XmlElement("premiered")] + public Option Premiered { get; set; } + + [XmlElement("plot")] + public string Plot { get; set; } + + [XmlElement("tagline")] + public string Tagline { get; set; } + + [XmlElement("genre")] + public List Genres { get; set; } + + [XmlElement("tag")] + public List Tags { get; set; } + + [XmlElement("studio")] + public List Studios { get; set; } + + [XmlElement("actor")] + public List Actors { get; set; } + + [XmlElement("credits")] + public List Writers { get; set; } + + [XmlElement("director")] + public List Directors { get; set; } + + [XmlElement("uniqueid")] + public List UniqueIds { get; set; } +} diff --git a/ErsatzTV.Core/Metadata/Nfo/OtherVideoNfoReader.cs b/ErsatzTV.Core/Metadata/Nfo/OtherVideoNfoReader.cs new file mode 100644 index 000000000..7cb443b4a --- /dev/null +++ b/ErsatzTV.Core/Metadata/Nfo/OtherVideoNfoReader.cs @@ -0,0 +1,114 @@ +using System.Xml; +using Bugsnag; +using ErsatzTV.Core.Errors; +using ErsatzTV.Core.Interfaces.Metadata.Nfo; + +namespace ErsatzTV.Core.Metadata.Nfo; + +public class OtherVideoNfoReader : NfoReader, IOtherVideoNfoReader +{ + private readonly IClient _client; + + public OtherVideoNfoReader(IClient client) => _client = client; + + public async Task> Read(Stream input) + { + try + { + var settings = new XmlReaderSettings { Async = true, ConformanceLevel = ConformanceLevel.Fragment }; + using var reader = XmlReader.Create(input, settings); + OtherVideoNfo nfo = null; + var done = false; + + while (!done && await reader.ReadAsync()) + { + switch (reader.NodeType) + { + case XmlNodeType.Element: + switch (reader.Name.ToLowerInvariant()) + { + case "movie": + nfo = new OtherVideoNfo + { + Genres = new List(), + Tags = new List(), + Studios = new List(), + Actors = new List(), + Writers = new List(), + Directors = new List(), + UniqueIds = new List() + }; + break; + case "title": + await ReadStringContent(reader, nfo, (movie, title) => movie.Title = title); + break; + case "sorttitle": + await ReadStringContent(reader, nfo, (movie, sortTitle) => movie.SortTitle = sortTitle); + break; + case "outline": + await ReadStringContent(reader, nfo, (movie, outline) => movie.Outline = outline); + break; + case "year": + await ReadIntContent(reader, nfo, (movie, year) => movie.Year = year); + break; + case "mpaa": + await ReadStringContent( + reader, + nfo, + (movie, contentRating) => movie.ContentRating = contentRating); + break; + case "premiered": + await ReadDateTimeContent( + reader, + nfo, + (movie, premiered) => movie.Premiered = premiered); + break; + case "plot": + await ReadStringContent(reader, nfo, (movie, plot) => movie.Plot = plot); + break; + case "genre": + await ReadStringContent(reader, nfo, (movie, genre) => movie.Genres.Add(genre)); + break; + case "tag": + await ReadStringContent(reader, nfo, (movie, tag) => movie.Tags.Add(tag)); + break; + case "studio": + await ReadStringContent(reader, nfo, (movie, studio) => movie.Studios.Add(studio)); + break; + case "actor": + ReadActor(reader, nfo, (movie, actor) => movie.Actors.Add(actor)); + break; + case "credits": + await ReadStringContent(reader, nfo, (movie, writer) => movie.Writers.Add(writer)); + break; + case "director": + await ReadStringContent( + reader, + nfo, + (movie, director) => movie.Directors.Add(director)); + break; + case "uniqueid": + await ReadUniqueId(reader, nfo, (movie, uniqueid) => movie.UniqueIds.Add(uniqueid)); + break; + } + + break; + case XmlNodeType.EndElement: + if (reader.Name == "movie") + { + done = true; + } + + break; + } + } + + return Optional(nfo).ToEither((BaseError)new FailedToReadNfo()); + } + catch (Exception ex) + { + _client.Notify(ex); + return new FailedToReadNfo(ex.ToString()); + } + } +} diff --git a/ErsatzTV.Core/Metadata/OtherVideoFolderScanner.cs b/ErsatzTV.Core/Metadata/OtherVideoFolderScanner.cs index 5d0e5e1ac..9ac3bacea 100644 --- a/ErsatzTV.Core/Metadata/OtherVideoFolderScanner.cs +++ b/ErsatzTV.Core/Metadata/OtherVideoFolderScanner.cs @@ -77,15 +77,17 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan var foldersCompleted = 0; + var allFolders = new System.Collections.Generic.HashSet(); var folderQueue = new Queue(); - if (ShouldIncludeFolder(libraryPath.Path)) + if (ShouldIncludeFolder(libraryPath.Path) && allFolders.Add(libraryPath.Path)) { folderQueue.Enqueue(libraryPath.Path); } foreach (string folder in _localFileSystem.ListSubdirectories(libraryPath.Path) .Filter(ShouldIncludeFolder) + .Filter(allFolders.Add) .OrderBy(identity)) { folderQueue.Enqueue(folder); @@ -115,6 +117,7 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan foreach (string subdirectory in _localFileSystem.ListSubdirectories(otherVideoFolder) .Filter(ShouldIncludeFolder) + .Filter(allFolders.Add) .OrderBy(identity)) { folderQueue.Enqueue(subdirectory); @@ -205,15 +208,38 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan try { OtherVideo otherVideo = result.Item; - if (!Optional(otherVideo.OtherVideoMetadata).Flatten().Any()) + string path = otherVideo.MediaVersions.Head().MediaFiles.Head().Path; + + Option maybeNfoFile = new List { Path.ChangeExtension(path, "nfo") } + .Filter(_localFileSystem.FileExists) + .HeadOrNone(); + + if (maybeNfoFile.IsNone) { - otherVideo.OtherVideoMetadata ??= new List(); + if (!Optional(otherVideo.OtherVideoMetadata).Flatten().Any()) + { + _logger.LogDebug("Refreshing {Attribute} for {Path}", "Fallback Metadata", path); + if (await _localMetadataProvider.RefreshFallbackMetadata(otherVideo)) + { + result.IsUpdated = true; + } + } + } + + foreach (string nfoFile in maybeNfoFile) + { + bool shouldUpdate = Optional(otherVideo.OtherVideoMetadata).Flatten().HeadOrNone().Match( + m => m.MetadataKind == MetadataKind.Fallback || + m.DateUpdated != _localFileSystem.GetLastWriteTime(nfoFile), + true); - string path = otherVideo.MediaVersions.Head().MediaFiles.Head().Path; - _logger.LogDebug("Refreshing {Attribute} for {Path}", "Fallback Metadata", path); - if (await _localMetadataProvider.RefreshFallbackMetadata(otherVideo)) + if (shouldUpdate) { - result.IsUpdated = true; + _logger.LogDebug("Refreshing {Attribute} from {Path}", "Sidecar Metadata", nfoFile); + if (await _localMetadataProvider.RefreshSidecarMetadata(otherVideo, nfoFile)) + { + result.IsUpdated = true; + } } } diff --git a/ErsatzTV.Infrastructure/Data/Configurations/Metadata/OtherVideoMetadataConfiguration.cs b/ErsatzTV.Infrastructure/Data/Configurations/Metadata/OtherVideoMetadataConfiguration.cs index 4062a1258..3efac2e90 100644 --- a/ErsatzTV.Infrastructure/Data/Configurations/Metadata/OtherVideoMetadataConfiguration.cs +++ b/ErsatzTV.Infrastructure/Data/Configurations/Metadata/OtherVideoMetadataConfiguration.cs @@ -10,11 +10,35 @@ public class OtherVideoMetadataConfiguration : IEntityTypeConfiguration mm.Artwork) + builder.HasMany(ovm => ovm.Artwork) .WithOne() .OnDelete(DeleteBehavior.Cascade); - builder.HasMany(mm => mm.Tags) + builder.HasMany(ovm => ovm.Genres) + .WithOne() + .OnDelete(DeleteBehavior.Cascade); + + builder.HasMany(ovm => ovm.Tags) + .WithOne() + .OnDelete(DeleteBehavior.Cascade); + + builder.HasMany(ovm => ovm.Studios) + .WithOne() + .OnDelete(DeleteBehavior.Cascade); + + builder.HasMany(ovm => ovm.Actors) + .WithOne() + .OnDelete(DeleteBehavior.Cascade); + + builder.HasMany(ovm => ovm.Directors) + .WithOne() + .OnDelete(DeleteBehavior.Cascade); + + builder.HasMany(ovm => ovm.Writers) + .WithOne() + .OnDelete(DeleteBehavior.Cascade); + + builder.HasMany(ovm => ovm.Guids) .WithOne() .OnDelete(DeleteBehavior.Cascade); diff --git a/ErsatzTV.Infrastructure/Data/Repositories/MetadataRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/MetadataRepository.cs index 098e7aacd..9141003d5 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/MetadataRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/MetadataRepository.cs @@ -465,6 +465,10 @@ public class MetadataRepository : IMetadataRepository await dbContext.Connection.ExecuteAsync( "INSERT INTO MetadataGuid (Guid, ArtistMetadataId) VALUES (@Guid, @MetadataId)", new { guid.Guid, MetadataId = metadata.Id }).Map(result => result > 0), + OtherVideoMetadata => + await dbContext.Connection.ExecuteAsync( + "INSERT INTO MetadataGuid (Guid, OtherVideoMetadataId) VALUES (@Guid, @MetadataId)", + new { guid.Guid, MetadataId = metadata.Id }).Map(result => result > 0), _ => throw new NotSupportedException() }; } diff --git a/ErsatzTV.Infrastructure/Data/Repositories/OtherVideoRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/OtherVideoRepository.cs index f88208dd7..c3838fb7a 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/OtherVideoRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/OtherVideoRepository.cs @@ -20,10 +20,23 @@ public class OtherVideoRepository : IOtherVideoRepository await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); Option maybeExisting = await dbContext.OtherVideos .AsNoTracking() - .Include(ov => ov.OtherVideoMetadata) - .ThenInclude(ovm => ovm.Artwork) - .Include(ov => ov.OtherVideoMetadata) + .Include(i => i.OtherVideoMetadata) + .ThenInclude(ovm => ovm.Genres) + .Include(i => i.OtherVideoMetadata) .ThenInclude(ovm => ovm.Tags) + .Include(i => i.OtherVideoMetadata) + .ThenInclude(ovm => ovm.Studios) + .Include(i => i.OtherVideoMetadata) + .ThenInclude(ovm => ovm.Guids) + .Include(i => i.OtherVideoMetadata) + .ThenInclude(ovm => ovm.Actors) + .Include(i => i.OtherVideoMetadata) + .ThenInclude(ovm => ovm.Actors) + .ThenInclude(a => a.Artwork) + .Include(i => i.OtherVideoMetadata) + .ThenInclude(ovm => ovm.Directors) + .Include(i => i.OtherVideoMetadata) + .ThenInclude(ovm => ovm.Writers) .Include(ov => ov.LibraryPath) .ThenInclude(lp => lp.Library) .Include(ov => ov.MediaVersions) @@ -82,6 +95,14 @@ public class OtherVideoRepository : IOtherVideoRepository return ids; } + public async Task AddGenre(OtherVideoMetadata metadata, Genre genre) + { + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + return await dbContext.Connection.ExecuteAsync( + "INSERT INTO Genre (Name, OtherVideoMetadataId) VALUES (@Name, @MetadataId)", + new { genre.Name, MetadataId = metadata.Id }).Map(result => result > 0); + } + public async Task AddTag(OtherVideoMetadata metadata, Tag tag) { await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); @@ -90,6 +111,57 @@ public class OtherVideoRepository : IOtherVideoRepository new { tag.Name, MetadataId = metadata.Id, tag.ExternalCollectionId }).Map(result => result > 0); } + public async Task AddStudio(OtherVideoMetadata metadata, Studio studio) + { + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + return await dbContext.Connection.ExecuteAsync( + "INSERT INTO Studio (Name, OtherVideoMetadataId) VALUES (@Name, @MetadataId)", + new { studio.Name, MetadataId = metadata.Id }).Map(result => result > 0); + } + + public async Task AddActor(OtherVideoMetadata metadata, Actor actor) + { + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + + int? artworkId = null; + + if (actor.Artwork != null) + { + artworkId = await dbContext.Connection.QuerySingleAsync( + @"INSERT INTO Artwork (ArtworkKind, DateAdded, DateUpdated, Path) + VALUES (@ArtworkKind, @DateAdded, @DateUpdated, @Path); + SELECT last_insert_rowid()", + new + { + ArtworkKind = (int)actor.Artwork.ArtworkKind, + actor.Artwork.DateAdded, + actor.Artwork.DateUpdated, + actor.Artwork.Path + }); + } + + return await dbContext.Connection.ExecuteAsync( + "INSERT INTO Actor (Name, Role, \"Order\", OtherVideoMetadataId, ArtworkId) VALUES (@Name, @Role, @Order, @MetadataId, @ArtworkId)", + new { actor.Name, actor.Role, actor.Order, MetadataId = metadata.Id, ArtworkId = artworkId }) + .Map(result => result > 0); + } + + public async Task AddDirector(OtherVideoMetadata metadata, Director director) + { + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + return await dbContext.Connection.ExecuteAsync( + "INSERT INTO Director (Name, OtherVideoMetadataId) VALUES (@Name, @MetadataId)", + new { director.Name, MetadataId = metadata.Id }).Map(result => result > 0); + } + + public async Task AddWriter(OtherVideoMetadata metadata, Writer writer) + { + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + return await dbContext.Connection.ExecuteAsync( + "INSERT INTO Writer (Name, OtherVideoMetadataId) VALUES (@Name, @MetadataId)", + new { writer.Name, MetadataId = metadata.Id }).Map(result => result > 0); + } + public async Task> GetOtherVideosForCards(List ids) { await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); diff --git a/ErsatzTV.Infrastructure/Data/Repositories/SearchRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/SearchRepository.cs index 185b7b1e1..34ca2530d 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/SearchRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/SearchRepository.cs @@ -98,9 +98,19 @@ public class SearchRepository : ISearchRepository .Include(mi => (mi as Artist).ArtistMetadata) .ThenInclude(mm => mm.Moods) .Include(mi => (mi as OtherVideo).OtherVideoMetadata) - .ThenInclude(mm => mm.Tags) + .ThenInclude(ovm => ovm.Genres) + .Include(mi => (mi as OtherVideo).OtherVideoMetadata) + .ThenInclude(ovm => ovm.Tags) + .Include(mi => (mi as OtherVideo).OtherVideoMetadata) + .ThenInclude(ovm => ovm.Studios) + .Include(mi => (mi as OtherVideo).OtherVideoMetadata) + .ThenInclude(ovm => ovm.Actors) + .Include(mi => (mi as OtherVideo).OtherVideoMetadata) + .ThenInclude(ovm => ovm.Directors) + .Include(mi => (mi as OtherVideo).OtherVideoMetadata) + .ThenInclude(ovm => ovm.Writers) .Include(mi => (mi as OtherVideo).MediaVersions) - .ThenInclude(mm => mm.Streams) + .ThenInclude(ovm => ovm.Streams) .Include(mi => (mi as Song).SongMetadata) .ThenInclude(mm => mm.Tags) .Include(mi => (mi as Song).SongMetadata) @@ -242,9 +252,19 @@ public class SearchRepository : ISearchRepository .Include(mi => (mi as Artist).ArtistMetadata) .ThenInclude(mm => mm.Moods) .Include(mi => (mi as OtherVideo).OtherVideoMetadata) - .ThenInclude(mm => mm.Tags) + .ThenInclude(ovm => ovm.Genres) + .Include(mi => (mi as OtherVideo).OtherVideoMetadata) + .ThenInclude(ovm => ovm.Tags) + .Include(mi => (mi as OtherVideo).OtherVideoMetadata) + .ThenInclude(ovm => ovm.Studios) + .Include(mi => (mi as OtherVideo).OtherVideoMetadata) + .ThenInclude(ovm => ovm.Actors) + .Include(mi => (mi as OtherVideo).OtherVideoMetadata) + .ThenInclude(ovm => ovm.Directors) + .Include(mi => (mi as OtherVideo).OtherVideoMetadata) + .ThenInclude(ovm => ovm.Writers) .Include(mi => (mi as OtherVideo).MediaVersions) - .ThenInclude(mm => mm.Streams) + .ThenInclude(ovm => ovm.Streams) .Include(mi => (mi as Song).SongMetadata) .ThenInclude(mm => mm.Tags) .Include(mi => (mi as Song).SongMetadata) diff --git a/ErsatzTV.Infrastructure/Migrations/20220506005328_Expand_OtherVideoMetadata.Designer.cs b/ErsatzTV.Infrastructure/Migrations/20220506005328_Expand_OtherVideoMetadata.Designer.cs new file mode 100644 index 000000000..ff372e1dd --- /dev/null +++ b/ErsatzTV.Infrastructure/Migrations/20220506005328_Expand_OtherVideoMetadata.Designer.cs @@ -0,0 +1,4279 @@ +// +using System; +using ErsatzTV.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace ErsatzTV.Infrastructure.Migrations +{ + [DbContext(typeof(TvContext))] + [Migration("20220506005328_Expand_OtherVideoMetadata")] + partial class Expand_OtherVideoMetadata + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "6.0.4"); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Actor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArtistMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ArtworkId") + .HasColumnType("INTEGER"); + + b.Property("EpisodeMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MovieMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MusicVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("OtherVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Role") + .HasColumnType("TEXT"); + + b.Property("SeasonMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ShowMetadataId") + .HasColumnType("INTEGER"); + + b.Property("SongMetadataId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ArtistMetadataId"); + + b.HasIndex("ArtworkId") + .IsUnique(); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("OtherVideoMetadataId"); + + b.HasIndex("SeasonMetadataId"); + + b.HasIndex("ShowMetadataId"); + + b.HasIndex("SongMetadataId"); + + b.ToTable("Actor", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ArtistMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArtistId") + .HasColumnType("INTEGER"); + + b.Property("Biography") + .HasColumnType("TEXT"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("Disambiguation") + .HasColumnType("TEXT"); + + b.Property("Formed") + .HasColumnType("TEXT"); + + b.Property("MetadataKind") + .HasColumnType("INTEGER"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("SortTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ArtistId"); + + b.ToTable("ArtistMetadata", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Artwork", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArtistMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ArtworkKind") + .HasColumnType("INTEGER"); + + b.Property("BlurHash43") + .HasColumnType("TEXT"); + + b.Property("BlurHash54") + .HasColumnType("TEXT"); + + b.Property("BlurHash64") + .HasColumnType("TEXT"); + + b.Property("ChannelId") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("EpisodeMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MovieMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MusicVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("OtherVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("SeasonMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ShowMetadataId") + .HasColumnType("INTEGER"); + + b.Property("SongMetadataId") + .HasColumnType("INTEGER"); + + b.Property("SourcePath") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ArtistMetadataId"); + + b.HasIndex("ChannelId"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("OtherVideoMetadataId"); + + b.HasIndex("SeasonMetadataId"); + + b.HasIndex("ShowMetadataId"); + + b.HasIndex("SongMetadataId"); + + b.ToTable("Artwork", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Channel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Categories") + .HasColumnType("TEXT"); + + b.Property("FFmpegProfileId") + .HasColumnType("INTEGER"); + + b.Property("FallbackFillerId") + .HasColumnType("INTEGER"); + + b.Property("Group") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("ErsatzTV"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("TEXT"); + + b.Property("PreferredAudioLanguageCode") + .HasColumnType("TEXT"); + + b.Property("PreferredSubtitleLanguageCode") + .HasColumnType("TEXT"); + + b.Property("StreamingMode") + .HasColumnType("INTEGER"); + + b.Property("SubtitleMode") + .HasColumnType("INTEGER"); + + b.Property("UniqueId") + .HasColumnType("TEXT"); + + b.Property("WatermarkId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("FFmpegProfileId"); + + b.HasIndex("FallbackFillerId"); + + b.HasIndex("Number") + .IsUnique(); + + b.HasIndex("WatermarkId"); + + b.ToTable("Channel", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ChannelWatermark", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DurationSeconds") + .HasColumnType("INTEGER"); + + b.Property("FrequencyMinutes") + .HasColumnType("INTEGER"); + + b.Property("HorizontalMarginPercent") + .HasColumnType("INTEGER"); + + b.Property("Image") + .HasColumnType("TEXT"); + + b.Property("ImageSource") + .HasColumnType("INTEGER"); + + b.Property("Location") + .HasColumnType("INTEGER"); + + b.Property("Mode") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Opacity") + .HasColumnType("INTEGER"); + + b.Property("Size") + .HasColumnType("INTEGER"); + + b.Property("VerticalMarginPercent") + .HasColumnType("INTEGER"); + + b.Property("WidthPercent") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("ChannelWatermark", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Collection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("UseCustomPlaybackOrder") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.CollectionItem", b => + { + b.Property("CollectionId") + .HasColumnType("INTEGER"); + + b.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b.Property("CustomIndex") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionId", "MediaItemId"); + + b.HasIndex("MediaItemId"); + + b.ToTable("CollectionItem", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ConfigElement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("ConfigElement", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Director", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("EpisodeMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MovieMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OtherVideoMetadataId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("OtherVideoMetadataId"); + + b.ToTable("Director", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyCollection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Etag") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("EmbyCollection", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Address") + .HasColumnType("TEXT"); + + b.Property("EmbyMediaSourceId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("EmbyMediaSourceId"); + + b.ToTable("EmbyConnection", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyPathReplacement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("EmbyMediaSourceId") + .HasColumnType("INTEGER"); + + b.Property("EmbyPath") + .HasColumnType("TEXT"); + + b.Property("LocalPath") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("EmbyMediaSourceId"); + + b.ToTable("EmbyPathReplacement", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EpisodeMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("EpisodeId") + .HasColumnType("INTEGER"); + + b.Property("EpisodeNumber") + .HasColumnType("INTEGER"); + + b.Property("MetadataKind") + .HasColumnType("INTEGER"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("Outline") + .HasColumnType("TEXT"); + + b.Property("Plot") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("SortTitle") + .HasColumnType("TEXT"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("EpisodeId"); + + b.ToTable("EpisodeMetadata", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.FFmpegProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AudioBitrate") + .HasColumnType("INTEGER"); + + b.Property("AudioBufferSize") + .HasColumnType("INTEGER"); + + b.Property("AudioChannels") + .HasColumnType("INTEGER"); + + b.Property("AudioFormat") + .HasColumnType("INTEGER"); + + b.Property("AudioSampleRate") + .HasColumnType("INTEGER"); + + b.Property("DeinterlaceVideo") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("HardwareAcceleration") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizeFramerate") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(false); + + b.Property("NormalizeLoudness") + .HasColumnType("INTEGER"); + + b.Property("ResolutionId") + .HasColumnType("INTEGER"); + + b.Property("ThreadCount") + .HasColumnType("INTEGER"); + + b.Property("VaapiDevice") + .HasColumnType("TEXT"); + + b.Property("VaapiDriver") + .HasColumnType("INTEGER"); + + b.Property("VideoBitrate") + .HasColumnType("INTEGER"); + + b.Property("VideoBufferSize") + .HasColumnType("INTEGER"); + + b.Property("VideoFormat") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ResolutionId"); + + b.ToTable("FFmpegProfile", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Filler.FillerPreset", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CollectionId") + .HasColumnType("INTEGER"); + + b.Property("CollectionType") + .HasColumnType("INTEGER"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.Property("Duration") + .HasColumnType("TEXT"); + + b.Property("FillerKind") + .HasColumnType("INTEGER"); + + b.Property("FillerMode") + .HasColumnType("INTEGER"); + + b.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b.Property("MultiCollectionId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("PadToNearestMinute") + .HasColumnType("INTEGER"); + + b.Property("SmartCollectionId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CollectionId"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("MultiCollectionId"); + + b.HasIndex("SmartCollectionId"); + + b.ToTable("FillerPreset", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Genre", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArtistMetadataId") + .HasColumnType("INTEGER"); + + b.Property("EpisodeMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MovieMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MusicVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OtherVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("SeasonMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ShowMetadataId") + .HasColumnType("INTEGER"); + + b.Property("SongMetadataId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ArtistMetadataId"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("OtherVideoMetadataId"); + + b.HasIndex("SeasonMetadataId"); + + b.HasIndex("ShowMetadataId"); + + b.HasIndex("SongMetadataId"); + + b.ToTable("Genre"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinCollection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Etag") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("JellyfinCollection", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Address") + .HasColumnType("TEXT"); + + b.Property("JellyfinMediaSourceId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("JellyfinMediaSourceId"); + + b.ToTable("JellyfinConnection", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinPathReplacement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("JellyfinMediaSourceId") + .HasColumnType("INTEGER"); + + b.Property("JellyfinPath") + .HasColumnType("TEXT"); + + b.Property("LocalPath") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("JellyfinMediaSourceId"); + + b.ToTable("JellyfinPathReplacement", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LanguageCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("EnglishName") + .HasColumnType("TEXT"); + + b.Property("FrenchName") + .HasColumnType("TEXT"); + + b.Property("ThreeCode1") + .HasColumnType("TEXT"); + + b.Property("ThreeCode2") + .HasColumnType("TEXT"); + + b.Property("TwoCode") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("LanguageCode", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Library", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastScan") + .HasColumnType("TEXT"); + + b.Property("MediaKind") + .HasColumnType("INTEGER"); + + b.Property("MediaSourceId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MediaSourceId"); + + b.ToTable("Library", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LibraryFolder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Etag") + .HasColumnType("TEXT"); + + b.Property("LibraryPathId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryPathId"); + + b.ToTable("LibraryFolder", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LibraryPath", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastScan") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("LibraryPath", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaChapter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("EndTime") + .HasColumnType("TEXT"); + + b.Property("MediaVersionId") + .HasColumnType("INTEGER"); + + b.Property("StartTime") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MediaVersionId"); + + b.ToTable("MediaChapter", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("MediaVersionId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MediaVersionId"); + + b.HasIndex("Path") + .IsUnique(); + + b.ToTable("MediaFile", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LibraryPathId") + .HasColumnType("INTEGER"); + + b.Property("State") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("LibraryPathId"); + + b.ToTable("MediaItem", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("MediaSource", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AttachedPic") + .HasColumnType("INTEGER"); + + b.Property("BitsPerRawSample") + .HasColumnType("INTEGER"); + + b.Property("Channels") + .HasColumnType("INTEGER"); + + b.Property("Codec") + .HasColumnType("TEXT"); + + b.Property("Default") + .HasColumnType("INTEGER"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("Forced") + .HasColumnType("INTEGER"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("MediaStreamKind") + .HasColumnType("INTEGER"); + + b.Property("MediaVersionId") + .HasColumnType("INTEGER"); + + b.Property("MimeType") + .HasColumnType("TEXT"); + + b.Property("PixelFormat") + .HasColumnType("TEXT"); + + b.Property("Profile") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MediaVersionId"); + + b.ToTable("MediaStream", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("DisplayAspectRatio") + .HasColumnType("TEXT"); + + b.Property("Duration") + .HasColumnType("TEXT"); + + b.Property("EpisodeId") + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("MovieId") + .HasColumnType("INTEGER"); + + b.Property("MusicVideoId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OtherVideoId") + .HasColumnType("INTEGER"); + + b.Property("RFrameRate") + .HasColumnType("TEXT"); + + b.Property("SampleAspectRatio") + .HasColumnType("TEXT"); + + b.Property("SongId") + .HasColumnType("INTEGER"); + + b.Property("VideoScanKind") + .HasColumnType("INTEGER"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("EpisodeId"); + + b.HasIndex("MovieId"); + + b.HasIndex("MusicVideoId"); + + b.HasIndex("OtherVideoId"); + + b.HasIndex("SongId"); + + b.ToTable("MediaVersion", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MetadataGuid", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArtistMetadataId") + .HasColumnType("INTEGER"); + + b.Property("EpisodeMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Guid") + .HasColumnType("TEXT"); + + b.Property("MovieMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MusicVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("OtherVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("SeasonMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ShowMetadataId") + .HasColumnType("INTEGER"); + + b.Property("SongMetadataId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ArtistMetadataId"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("OtherVideoMetadataId"); + + b.HasIndex("SeasonMetadataId"); + + b.HasIndex("ShowMetadataId"); + + b.HasIndex("SongMetadataId"); + + b.ToTable("MetadataGuid", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Mood", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArtistMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ArtistMetadataId"); + + b.ToTable("Mood"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MovieMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ContentRating") + .HasColumnType("TEXT"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("MetadataKind") + .HasColumnType("INTEGER"); + + b.Property("MovieId") + .HasColumnType("INTEGER"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("Outline") + .HasColumnType("TEXT"); + + b.Property("Plot") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("SortTitle") + .HasColumnType("TEXT"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("MovieId"); + + b.ToTable("MovieMetadata", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MultiCollection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("MultiCollection", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MultiCollectionItem", b => + { + b.Property("MultiCollectionId") + .HasColumnType("INTEGER"); + + b.Property("CollectionId") + .HasColumnType("INTEGER"); + + b.Property("PlaybackOrder") + .HasColumnType("INTEGER"); + + b.Property("ScheduleAsGroup") + .HasColumnType("INTEGER"); + + b.HasKey("MultiCollectionId", "CollectionId"); + + b.HasIndex("CollectionId"); + + b.ToTable("MultiCollectionItem", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MultiCollectionSmartItem", b => + { + b.Property("MultiCollectionId") + .HasColumnType("INTEGER"); + + b.Property("SmartCollectionId") + .HasColumnType("INTEGER"); + + b.Property("PlaybackOrder") + .HasColumnType("INTEGER"); + + b.Property("ScheduleAsGroup") + .HasColumnType("INTEGER"); + + b.HasKey("MultiCollectionId", "SmartCollectionId"); + + b.HasIndex("SmartCollectionId"); + + b.ToTable("MultiCollectionSmartItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideoArtist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("MusicVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MusicVideoMetadataId"); + + b.ToTable("MusicVideoArtist"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideoMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Album") + .HasColumnType("TEXT"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("MetadataKind") + .HasColumnType("INTEGER"); + + b.Property("MusicVideoId") + .HasColumnType("INTEGER"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("Plot") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("SortTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("MusicVideoId"); + + b.ToTable("MusicVideoMetadata", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.OtherVideoMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ContentRating") + .HasColumnType("TEXT"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("MetadataKind") + .HasColumnType("INTEGER"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("OtherVideoId") + .HasColumnType("INTEGER"); + + b.Property("Outline") + .HasColumnType("TEXT"); + + b.Property("Plot") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("SortTitle") + .HasColumnType("TEXT"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OtherVideoId"); + + b.ToTable("OtherVideoMetadata", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("INTEGER"); + + b.Property("DailyRebuildTime") + .HasColumnType("TEXT"); + + b.Property("ProgramScheduleId") + .HasColumnType("INTEGER"); + + b.Property("ProgramSchedulePlayoutType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChannelId"); + + b.HasIndex("ProgramScheduleId"); + + b.ToTable("Playout", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterTitle") + .HasColumnType("TEXT"); + + b.Property("CustomTitle") + .HasColumnType("TEXT"); + + b.Property("FillerKind") + .HasColumnType("INTEGER"); + + b.Property("Finish") + .HasColumnType("TEXT"); + + b.Property("GuideFinish") + .HasColumnType("TEXT"); + + b.Property("GuideGroup") + .HasColumnType("INTEGER"); + + b.Property("InPoint") + .HasColumnType("TEXT"); + + b.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b.Property("OutPoint") + .HasColumnType("TEXT"); + + b.Property("PlayoutId") + .HasColumnType("INTEGER"); + + b.Property("PreferredAudioLanguageCode") + .HasColumnType("TEXT"); + + b.Property("PreferredSubtitleLanguageCode") + .HasColumnType("TEXT"); + + b.Property("Start") + .HasColumnType("TEXT"); + + b.Property("SubtitleMode") + .HasColumnType("INTEGER"); + + b.Property("WatermarkId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("PlayoutId"); + + b.HasIndex("WatermarkId"); + + b.ToTable("PlayoutItem", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutProgramScheduleAnchor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AnchorDate") + .HasColumnType("TEXT"); + + b.Property("CollectionId") + .HasColumnType("INTEGER"); + + b.Property("CollectionType") + .HasColumnType("INTEGER"); + + b.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b.Property("MultiCollectionId") + .HasColumnType("INTEGER"); + + b.Property("PlayoutId") + .HasColumnType("INTEGER"); + + b.Property("ProgramScheduleId") + .HasColumnType("INTEGER"); + + b.Property("SmartCollectionId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CollectionId"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("MultiCollectionId"); + + b.HasIndex("PlayoutId"); + + b.HasIndex("ProgramScheduleId"); + + b.HasIndex("SmartCollectionId"); + + b.ToTable("PlayoutProgramScheduleAnchor", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("PlexMediaSourceId") + .HasColumnType("INTEGER"); + + b.Property("Uri") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PlexMediaSourceId"); + + b.ToTable("PlexConnection", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexPathReplacement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LocalPath") + .HasColumnType("TEXT"); + + b.Property("PlexMediaSourceId") + .HasColumnType("INTEGER"); + + b.Property("PlexPath") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PlexMediaSourceId"); + + b.ToTable("PlexPathReplacement", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("KeepMultiPartEpisodesTogether") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("ShuffleScheduleItems") + .HasColumnType("INTEGER"); + + b.Property("TreatCollectionsAsShows") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("ProgramSchedule", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CollectionId") + .HasColumnType("INTEGER"); + + b.Property("CollectionType") + .HasColumnType("INTEGER"); + + b.Property("CustomTitle") + .HasColumnType("TEXT"); + + b.Property("FallbackFillerId") + .HasColumnType("INTEGER"); + + b.Property("GuideMode") + .HasColumnType("INTEGER"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b.Property("MidRollFillerId") + .HasColumnType("INTEGER"); + + b.Property("MultiCollectionId") + .HasColumnType("INTEGER"); + + b.Property("PlaybackOrder") + .HasColumnType("INTEGER"); + + b.Property("PostRollFillerId") + .HasColumnType("INTEGER"); + + b.Property("PreRollFillerId") + .HasColumnType("INTEGER"); + + b.Property("PreferredAudioLanguageCode") + .HasColumnType("TEXT"); + + b.Property("PreferredSubtitleLanguageCode") + .HasColumnType("TEXT"); + + b.Property("ProgramScheduleId") + .HasColumnType("INTEGER"); + + b.Property("SmartCollectionId") + .HasColumnType("INTEGER"); + + b.Property("StartTime") + .HasColumnType("TEXT"); + + b.Property("SubtitleMode") + .HasColumnType("INTEGER"); + + b.Property("TailFillerId") + .HasColumnType("INTEGER"); + + b.Property("WatermarkId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CollectionId"); + + b.HasIndex("FallbackFillerId"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("MidRollFillerId"); + + b.HasIndex("MultiCollectionId"); + + b.HasIndex("PostRollFillerId"); + + b.HasIndex("PreRollFillerId"); + + b.HasIndex("ProgramScheduleId"); + + b.HasIndex("SmartCollectionId"); + + b.HasIndex("TailFillerId"); + + b.HasIndex("WatermarkId"); + + b.ToTable("ProgramScheduleItem", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Resolution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Resolution", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.SeasonMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("MetadataKind") + .HasColumnType("INTEGER"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("Outline") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("SeasonId") + .HasColumnType("INTEGER"); + + b.Property("SortTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId"); + + b.ToTable("SeasonMetadata", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ShowMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ContentRating") + .HasColumnType("TEXT"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("MetadataKind") + .HasColumnType("INTEGER"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("Outline") + .HasColumnType("TEXT"); + + b.Property("Plot") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("ShowId") + .HasColumnType("INTEGER"); + + b.Property("SortTitle") + .HasColumnType("TEXT"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ShowId"); + + b.ToTable("ShowMetadata", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.SmartCollection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Query") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("SmartCollection", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.SongMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Album") + .HasColumnType("TEXT"); + + b.Property("AlbumArtist") + .HasColumnType("TEXT"); + + b.Property("Artist") + .HasColumnType("TEXT"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("MetadataKind") + .HasColumnType("INTEGER"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("SongId") + .HasColumnType("INTEGER"); + + b.Property("SortTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Track") + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SongId"); + + b.ToTable("SongMetadata", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Studio", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArtistMetadataId") + .HasColumnType("INTEGER"); + + b.Property("EpisodeMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MovieMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MusicVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OtherVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("SeasonMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ShowMetadataId") + .HasColumnType("INTEGER"); + + b.Property("SongMetadataId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ArtistMetadataId"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("OtherVideoMetadataId"); + + b.HasIndex("SeasonMetadataId"); + + b.HasIndex("ShowMetadataId"); + + b.HasIndex("SongMetadataId"); + + b.ToTable("Studio", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Style", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArtistMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ArtistMetadataId"); + + b.ToTable("Style"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Subtitle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArtistMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Codec") + .HasColumnType("TEXT"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("Default") + .HasColumnType("INTEGER"); + + b.Property("EpisodeMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Forced") + .HasColumnType("INTEGER"); + + b.Property("IsExtracted") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(false); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("MovieMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MusicVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("OtherVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("SDH") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(false); + + b.Property("SeasonMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ShowMetadataId") + .HasColumnType("INTEGER"); + + b.Property("SongMetadataId") + .HasColumnType("INTEGER"); + + b.Property("StreamIndex") + .HasColumnType("INTEGER"); + + b.Property("SubtitleKind") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ArtistMetadataId"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("OtherVideoMetadataId"); + + b.HasIndex("SeasonMetadataId"); + + b.HasIndex("ShowMetadataId"); + + b.HasIndex("SongMetadataId"); + + b.ToTable("Subtitle", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArtistMetadataId") + .HasColumnType("INTEGER"); + + b.Property("EpisodeMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ExternalCollectionId") + .HasColumnType("TEXT"); + + b.Property("MovieMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MusicVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OtherVideoMetadataId") + .HasColumnType("INTEGER"); + + b.Property("SeasonMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ShowMetadataId") + .HasColumnType("INTEGER"); + + b.Property("SongMetadataId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ArtistMetadataId"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("OtherVideoMetadataId"); + + b.HasIndex("SeasonMetadataId"); + + b.HasIndex("ShowMetadataId"); + + b.HasIndex("SongMetadataId"); + + b.ToTable("Tag"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TraktList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("ItemCount") + .HasColumnType("INTEGER"); + + b.Property("List") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("TraktId") + .HasColumnType("INTEGER"); + + b.Property("User") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("TraktList", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TraktListItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Episode") + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b.Property("Rank") + .HasColumnType("INTEGER"); + + b.Property("Season") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TraktId") + .HasColumnType("INTEGER"); + + b.Property("TraktListId") + .HasColumnType("INTEGER"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("TraktListId"); + + b.ToTable("TraktListItem", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TraktListItemGuid", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Guid") + .HasColumnType("TEXT"); + + b.Property("TraktListItemId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("TraktListItemId"); + + b.ToTable("TraktListItemGuid", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Writer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("EpisodeMetadataId") + .HasColumnType("INTEGER"); + + b.Property("MovieMetadataId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OtherVideoMetadataId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("OtherVideoMetadataId"); + + b.ToTable("Writer", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Emby.EmbyPathInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("EmbyLibraryId") + .HasColumnType("INTEGER"); + + b.Property("NetworkPath") + .HasColumnType("TEXT"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("EmbyLibraryId"); + + b.ToTable("EmbyPathInfo"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Jellyfin.JellyfinPathInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("JellyfinLibraryId") + .HasColumnType("INTEGER"); + + b.Property("NetworkPath") + .HasColumnType("TEXT"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("JellyfinLibraryId"); + + b.ToTable("JellyfinPathInfo"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Artist", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.ToTable("Artist", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyLibrary", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Library"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ShouldSyncItems") + .HasColumnType("INTEGER"); + + b.ToTable("EmbyLibrary", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyMediaSource", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaSource"); + + b.Property("OperatingSystem") + .HasColumnType("TEXT"); + + b.Property("ServerName") + .HasColumnType("TEXT"); + + b.ToTable("EmbyMediaSource", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Episode", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.Property("SeasonId") + .HasColumnType("INTEGER"); + + b.HasIndex("SeasonId"); + + b.ToTable("Episode", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinLibrary", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Library"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ShouldSyncItems") + .HasColumnType("INTEGER"); + + b.ToTable("JellyfinLibrary", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinMediaSource", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaSource"); + + b.Property("OperatingSystem") + .HasColumnType("TEXT"); + + b.Property("ServerName") + .HasColumnType("TEXT"); + + b.ToTable("JellyfinMediaSource", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LocalLibrary", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Library"); + + b.ToTable("LocalLibrary", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LocalMediaSource", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaSource"); + + b.ToTable("LocalMediaSource", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Movie", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.ToTable("Movie", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideo", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.Property("ArtistId") + .HasColumnType("INTEGER"); + + b.HasIndex("ArtistId"); + + b.ToTable("MusicVideo", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.OtherVideo", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.ToTable("OtherVideo", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexLibrary", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Library"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("ShouldSyncItems") + .HasColumnType("INTEGER"); + + b.ToTable("PlexLibrary", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaFile", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaFile"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("PlexId") + .HasColumnType("INTEGER"); + + b.ToTable("PlexMediaFile", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSource", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaSource"); + + b.Property("ClientIdentifier") + .HasColumnType("TEXT"); + + b.Property("Platform") + .HasColumnType("TEXT"); + + b.Property("PlatformVersion") + .HasColumnType("TEXT"); + + b.Property("ProductVersion") + .HasColumnType("TEXT"); + + b.Property("ServerName") + .HasColumnType("TEXT"); + + b.ToTable("PlexMediaSource", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemDuration", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); + + b.Property("PlayoutDuration") + .HasColumnType("TEXT"); + + b.Property("TailMode") + .HasColumnType("INTEGER"); + + b.ToTable("ProgramScheduleDurationItem", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemFlood", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); + + b.ToTable("ProgramScheduleFloodItem", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemMultiple", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.ToTable("ProgramScheduleMultipleItem", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemOne", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); + + b.ToTable("ProgramScheduleOneItem", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Season", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.Property("ShowId") + .HasColumnType("INTEGER"); + + b.HasIndex("ShowId"); + + b.ToTable("Season", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Show", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.ToTable("Show", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Song", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.ToTable("Song", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyEpisode", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Episode"); + + b.Property("Etag") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.ToTable("EmbyEpisode", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyMovie", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Movie"); + + b.Property("Etag") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.ToTable("EmbyMovie", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbySeason", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Season"); + + b.Property("Etag") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.ToTable("EmbySeason", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyShow", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Show"); + + b.Property("Etag") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.ToTable("EmbyShow", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinEpisode", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Episode"); + + b.Property("Etag") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.ToTable("JellyfinEpisode", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinMovie", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Movie"); + + b.Property("Etag") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.ToTable("JellyfinMovie", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinSeason", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Season"); + + b.Property("Etag") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.ToTable("JellyfinSeason", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinShow", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Show"); + + b.Property("Etag") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.ToTable("JellyfinShow", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexEpisode", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Episode"); + + b.Property("Etag") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.ToTable("PlexEpisode", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMovie", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Movie"); + + b.Property("Etag") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.ToTable("PlexMovie", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexSeason", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Season"); + + b.Property("Etag") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.ToTable("PlexSeason", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexShow", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Show"); + + b.Property("Etag") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.ToTable("PlexShow", (string)null); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Actor", b => + { + b.HasOne("ErsatzTV.Core.Domain.ArtistMetadata", null) + .WithMany("Actors") + .HasForeignKey("ArtistMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.Artwork", "Artwork") + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.Actor", "ArtworkId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null) + .WithMany("Actors") + .HasForeignKey("EpisodeMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MovieMetadata", null) + .WithMany("Actors") + .HasForeignKey("MovieMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MusicVideoMetadata", null) + .WithMany("Actors") + .HasForeignKey("MusicVideoMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.OtherVideoMetadata", null) + .WithMany("Actors") + .HasForeignKey("OtherVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null) + .WithMany("Actors") + .HasForeignKey("SeasonMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.ShowMetadata", null) + .WithMany("Actors") + .HasForeignKey("ShowMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SongMetadata", null) + .WithMany("Actors") + .HasForeignKey("SongMetadataId"); + + b.Navigation("Artwork"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ArtistMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.Artist", "Artist") + .WithMany("ArtistMetadata") + .HasForeignKey("ArtistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Artist"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Artwork", b => + { + b.HasOne("ErsatzTV.Core.Domain.ArtistMetadata", null) + .WithMany("Artwork") + .HasForeignKey("ArtistMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.Channel", null) + .WithMany("Artwork") + .HasForeignKey("ChannelId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null) + .WithMany("Artwork") + .HasForeignKey("EpisodeMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MovieMetadata", null) + .WithMany("Artwork") + .HasForeignKey("MovieMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MusicVideoMetadata", null) + .WithMany("Artwork") + .HasForeignKey("MusicVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.OtherVideoMetadata", null) + .WithMany("Artwork") + .HasForeignKey("OtherVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null) + .WithMany("Artwork") + .HasForeignKey("SeasonMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.ShowMetadata", null) + .WithMany("Artwork") + .HasForeignKey("ShowMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SongMetadata", null) + .WithMany("Artwork") + .HasForeignKey("SongMetadataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Channel", b => + { + b.HasOne("ErsatzTV.Core.Domain.FFmpegProfile", "FFmpegProfile") + .WithMany() + .HasForeignKey("FFmpegProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.Filler.FillerPreset", "FallbackFiller") + .WithMany() + .HasForeignKey("FallbackFillerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("ErsatzTV.Core.Domain.ChannelWatermark", "Watermark") + .WithMany() + .HasForeignKey("WatermarkId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("FFmpegProfile"); + + b.Navigation("FallbackFiller"); + + b.Navigation("Watermark"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.CollectionItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.Collection", "Collection") + .WithMany("CollectionItems") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem") + .WithMany("CollectionItems") + .HasForeignKey("MediaItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("MediaItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Director", b => + { + b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null) + .WithMany("Directors") + .HasForeignKey("EpisodeMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MovieMetadata", null) + .WithMany("Directors") + .HasForeignKey("MovieMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.OtherVideoMetadata", null) + .WithMany("Directors") + .HasForeignKey("OtherVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyConnection", b => + { + b.HasOne("ErsatzTV.Core.Domain.EmbyMediaSource", "EmbyMediaSource") + .WithMany("Connections") + .HasForeignKey("EmbyMediaSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EmbyMediaSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyPathReplacement", b => + { + b.HasOne("ErsatzTV.Core.Domain.EmbyMediaSource", "EmbyMediaSource") + .WithMany("PathReplacements") + .HasForeignKey("EmbyMediaSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EmbyMediaSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EpisodeMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.Episode", "Episode") + .WithMany("EpisodeMetadata") + .HasForeignKey("EpisodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Episode"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.FFmpegProfile", b => + { + b.HasOne("ErsatzTV.Core.Domain.Resolution", "Resolution") + .WithMany() + .HasForeignKey("ResolutionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Resolution"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Filler.FillerPreset", b => + { + b.HasOne("ErsatzTV.Core.Domain.Collection", "Collection") + .WithMany() + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem") + .WithMany() + .HasForeignKey("MediaItemId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MultiCollection", "MultiCollection") + .WithMany() + .HasForeignKey("MultiCollectionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SmartCollection", "SmartCollection") + .WithMany() + .HasForeignKey("SmartCollectionId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Collection"); + + b.Navigation("MediaItem"); + + b.Navigation("MultiCollection"); + + b.Navigation("SmartCollection"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Genre", b => + { + b.HasOne("ErsatzTV.Core.Domain.ArtistMetadata", null) + .WithMany("Genres") + .HasForeignKey("ArtistMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null) + .WithMany("Genres") + .HasForeignKey("EpisodeMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.MovieMetadata", null) + .WithMany("Genres") + .HasForeignKey("MovieMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MusicVideoMetadata", null) + .WithMany("Genres") + .HasForeignKey("MusicVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.OtherVideoMetadata", null) + .WithMany("Genres") + .HasForeignKey("OtherVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null) + .WithMany("Genres") + .HasForeignKey("SeasonMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.ShowMetadata", null) + .WithMany("Genres") + .HasForeignKey("ShowMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SongMetadata", null) + .WithMany("Genres") + .HasForeignKey("SongMetadataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinConnection", b => + { + b.HasOne("ErsatzTV.Core.Domain.JellyfinMediaSource", "JellyfinMediaSource") + .WithMany("Connections") + .HasForeignKey("JellyfinMediaSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JellyfinMediaSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinPathReplacement", b => + { + b.HasOne("ErsatzTV.Core.Domain.JellyfinMediaSource", "JellyfinMediaSource") + .WithMany("PathReplacements") + .HasForeignKey("JellyfinMediaSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JellyfinMediaSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Library", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaSource", "MediaSource") + .WithMany("Libraries") + .HasForeignKey("MediaSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MediaSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LibraryFolder", b => + { + b.HasOne("ErsatzTV.Core.Domain.LibraryPath", "LibraryPath") + .WithMany("LibraryFolders") + .HasForeignKey("LibraryPathId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("LibraryPath"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LibraryPath", b => + { + b.HasOne("ErsatzTV.Core.Domain.Library", "Library") + .WithMany("Paths") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaChapter", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaVersion", "MediaVersion") + .WithMany("Chapters") + .HasForeignKey("MediaVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MediaVersion"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaFile", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaVersion", "MediaVersion") + .WithMany("MediaFiles") + .HasForeignKey("MediaVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MediaVersion"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.LibraryPath", "LibraryPath") + .WithMany("MediaItems") + .HasForeignKey("LibraryPathId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("LibraryPath"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaStream", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaVersion", "MediaVersion") + .WithMany("Streams") + .HasForeignKey("MediaVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MediaVersion"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaVersion", b => + { + b.HasOne("ErsatzTV.Core.Domain.Episode", null) + .WithMany("MediaVersions") + .HasForeignKey("EpisodeId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.Movie", null) + .WithMany("MediaVersions") + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MusicVideo", null) + .WithMany("MediaVersions") + .HasForeignKey("MusicVideoId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.OtherVideo", null) + .WithMany("MediaVersions") + .HasForeignKey("OtherVideoId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.Song", null) + .WithMany("MediaVersions") + .HasForeignKey("SongId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MetadataGuid", b => + { + b.HasOne("ErsatzTV.Core.Domain.ArtistMetadata", null) + .WithMany("Guids") + .HasForeignKey("ArtistMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null) + .WithMany("Guids") + .HasForeignKey("EpisodeMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MovieMetadata", null) + .WithMany("Guids") + .HasForeignKey("MovieMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MusicVideoMetadata", null) + .WithMany("Guids") + .HasForeignKey("MusicVideoMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.OtherVideoMetadata", null) + .WithMany("Guids") + .HasForeignKey("OtherVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null) + .WithMany("Guids") + .HasForeignKey("SeasonMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.ShowMetadata", null) + .WithMany("Guids") + .HasForeignKey("ShowMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SongMetadata", null) + .WithMany("Guids") + .HasForeignKey("SongMetadataId"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Mood", b => + { + b.HasOne("ErsatzTV.Core.Domain.ArtistMetadata", null) + .WithMany("Moods") + .HasForeignKey("ArtistMetadataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MovieMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.Movie", "Movie") + .WithMany("MovieMetadata") + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Movie"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MultiCollectionItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.Collection", "Collection") + .WithMany("MultiCollectionItems") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.MultiCollection", "MultiCollection") + .WithMany("MultiCollectionItems") + .HasForeignKey("MultiCollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("MultiCollection"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MultiCollectionSmartItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.MultiCollection", "MultiCollection") + .WithMany("MultiCollectionSmartItems") + .HasForeignKey("MultiCollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.SmartCollection", "SmartCollection") + .WithMany("MultiCollectionSmartItems") + .HasForeignKey("SmartCollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MultiCollection"); + + b.Navigation("SmartCollection"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideoArtist", b => + { + b.HasOne("ErsatzTV.Core.Domain.MusicVideoMetadata", null) + .WithMany("Artists") + .HasForeignKey("MusicVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideoMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.MusicVideo", "MusicVideo") + .WithMany("MusicVideoMetadata") + .HasForeignKey("MusicVideoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MusicVideo"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.OtherVideoMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.OtherVideo", "OtherVideo") + .WithMany("OtherVideoMetadata") + .HasForeignKey("OtherVideoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OtherVideo"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b => + { + b.HasOne("ErsatzTV.Core.Domain.Channel", "Channel") + .WithMany("Playouts") + .HasForeignKey("ChannelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule") + .WithMany("Playouts") + .HasForeignKey("ProgramScheduleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("ErsatzTV.Core.Domain.PlayoutAnchor", "Anchor", b1 => + { + b1.Property("PlayoutId") + .HasColumnType("INTEGER"); + + b1.Property("DurationFinish") + .HasColumnType("TEXT"); + + b1.Property("InDurationFiller") + .HasColumnType("INTEGER"); + + b1.Property("InFlood") + .HasColumnType("INTEGER"); + + b1.Property("MultipleRemaining") + .HasColumnType("INTEGER"); + + b1.Property("NextGuideGroup") + .HasColumnType("INTEGER"); + + b1.Property("NextStart") + .HasColumnType("TEXT"); + + b1.HasKey("PlayoutId"); + + b1.ToTable("PlayoutAnchor", (string)null); + + b1.WithOwner() + .HasForeignKey("PlayoutId"); + + b1.OwnsOne("ErsatzTV.Core.Domain.CollectionEnumeratorState", "ScheduleItemsEnumeratorState", b2 => + { + b2.Property("PlayoutAnchorPlayoutId") + .HasColumnType("INTEGER"); + + b2.Property("Index") + .HasColumnType("INTEGER"); + + b2.Property("Seed") + .HasColumnType("INTEGER"); + + b2.HasKey("PlayoutAnchorPlayoutId"); + + b2.ToTable("ScheduleItemsEnumeratorState", (string)null); + + b2.WithOwner() + .HasForeignKey("PlayoutAnchorPlayoutId"); + }); + + b1.Navigation("ScheduleItemsEnumeratorState"); + }); + + b.Navigation("Anchor"); + + b.Navigation("Channel"); + + b.Navigation("ProgramSchedule"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem") + .WithMany() + .HasForeignKey("MediaItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout") + .WithMany("Items") + .HasForeignKey("PlayoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.ChannelWatermark", "Watermark") + .WithMany() + .HasForeignKey("WatermarkId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("MediaItem"); + + b.Navigation("Playout"); + + b.Navigation("Watermark"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutProgramScheduleAnchor", b => + { + b.HasOne("ErsatzTV.Core.Domain.Collection", "Collection") + .WithMany() + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem") + .WithMany() + .HasForeignKey("MediaItemId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MultiCollection", "MultiCollection") + .WithMany() + .HasForeignKey("MultiCollectionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout") + .WithMany("ProgramScheduleAnchors") + .HasForeignKey("PlayoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule") + .WithMany() + .HasForeignKey("ProgramScheduleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.SmartCollection", "SmartCollection") + .WithMany() + .HasForeignKey("SmartCollectionId"); + + b.OwnsOne("ErsatzTV.Core.Domain.CollectionEnumeratorState", "EnumeratorState", b1 => + { + b1.Property("PlayoutProgramScheduleAnchorId") + .HasColumnType("INTEGER"); + + b1.Property("Index") + .HasColumnType("INTEGER"); + + b1.Property("Seed") + .HasColumnType("INTEGER"); + + b1.HasKey("PlayoutProgramScheduleAnchorId"); + + b1.ToTable("CollectionEnumeratorState", (string)null); + + b1.WithOwner() + .HasForeignKey("PlayoutProgramScheduleAnchorId"); + }); + + b.Navigation("Collection"); + + b.Navigation("EnumeratorState"); + + b.Navigation("MediaItem"); + + b.Navigation("MultiCollection"); + + b.Navigation("Playout"); + + b.Navigation("ProgramSchedule"); + + b.Navigation("SmartCollection"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexConnection", b => + { + b.HasOne("ErsatzTV.Core.Domain.PlexMediaSource", "PlexMediaSource") + .WithMany("Connections") + .HasForeignKey("PlexMediaSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PlexMediaSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexPathReplacement", b => + { + b.HasOne("ErsatzTV.Core.Domain.PlexMediaSource", "PlexMediaSource") + .WithMany("PathReplacements") + .HasForeignKey("PlexMediaSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PlexMediaSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.Collection", "Collection") + .WithMany() + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.Filler.FillerPreset", "FallbackFiller") + .WithMany() + .HasForeignKey("FallbackFillerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem") + .WithMany() + .HasForeignKey("MediaItemId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.Filler.FillerPreset", "MidRollFiller") + .WithMany() + .HasForeignKey("MidRollFillerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("ErsatzTV.Core.Domain.MultiCollection", "MultiCollection") + .WithMany() + .HasForeignKey("MultiCollectionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.Filler.FillerPreset", "PostRollFiller") + .WithMany() + .HasForeignKey("PostRollFillerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("ErsatzTV.Core.Domain.Filler.FillerPreset", "PreRollFiller") + .WithMany() + .HasForeignKey("PreRollFillerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule") + .WithMany("Items") + .HasForeignKey("ProgramScheduleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.SmartCollection", "SmartCollection") + .WithMany() + .HasForeignKey("SmartCollectionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.Filler.FillerPreset", "TailFiller") + .WithMany() + .HasForeignKey("TailFillerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("ErsatzTV.Core.Domain.ChannelWatermark", "Watermark") + .WithMany() + .HasForeignKey("WatermarkId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Collection"); + + b.Navigation("FallbackFiller"); + + b.Navigation("MediaItem"); + + b.Navigation("MidRollFiller"); + + b.Navigation("MultiCollection"); + + b.Navigation("PostRollFiller"); + + b.Navigation("PreRollFiller"); + + b.Navigation("ProgramSchedule"); + + b.Navigation("SmartCollection"); + + b.Navigation("TailFiller"); + + b.Navigation("Watermark"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.SeasonMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.Season", "Season") + .WithMany("SeasonMetadata") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ShowMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.Show", "Show") + .WithMany("ShowMetadata") + .HasForeignKey("ShowId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Show"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.SongMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.Song", "Song") + .WithMany("SongMetadata") + .HasForeignKey("SongId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Song"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Studio", b => + { + b.HasOne("ErsatzTV.Core.Domain.ArtistMetadata", null) + .WithMany("Studios") + .HasForeignKey("ArtistMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null) + .WithMany("Studios") + .HasForeignKey("EpisodeMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.MovieMetadata", null) + .WithMany("Studios") + .HasForeignKey("MovieMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MusicVideoMetadata", null) + .WithMany("Studios") + .HasForeignKey("MusicVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.OtherVideoMetadata", null) + .WithMany("Studios") + .HasForeignKey("OtherVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null) + .WithMany("Studios") + .HasForeignKey("SeasonMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.ShowMetadata", null) + .WithMany("Studios") + .HasForeignKey("ShowMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SongMetadata", null) + .WithMany("Studios") + .HasForeignKey("SongMetadataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Style", b => + { + b.HasOne("ErsatzTV.Core.Domain.ArtistMetadata", null) + .WithMany("Styles") + .HasForeignKey("ArtistMetadataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Subtitle", b => + { + b.HasOne("ErsatzTV.Core.Domain.ArtistMetadata", null) + .WithMany("Subtitles") + .HasForeignKey("ArtistMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null) + .WithMany("Subtitles") + .HasForeignKey("EpisodeMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MovieMetadata", null) + .WithMany("Subtitles") + .HasForeignKey("MovieMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MusicVideoMetadata", null) + .WithMany("Subtitles") + .HasForeignKey("MusicVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.OtherVideoMetadata", null) + .WithMany("Subtitles") + .HasForeignKey("OtherVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null) + .WithMany("Subtitles") + .HasForeignKey("SeasonMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.ShowMetadata", null) + .WithMany("Subtitles") + .HasForeignKey("ShowMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.SongMetadata", null) + .WithMany("Subtitles") + .HasForeignKey("SongMetadataId"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Tag", b => + { + b.HasOne("ErsatzTV.Core.Domain.ArtistMetadata", null) + .WithMany("Tags") + .HasForeignKey("ArtistMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null) + .WithMany("Tags") + .HasForeignKey("EpisodeMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.MovieMetadata", null) + .WithMany("Tags") + .HasForeignKey("MovieMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MusicVideoMetadata", null) + .WithMany("Tags") + .HasForeignKey("MusicVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.OtherVideoMetadata", null) + .WithMany("Tags") + .HasForeignKey("OtherVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null) + .WithMany("Tags") + .HasForeignKey("SeasonMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.ShowMetadata", null) + .WithMany("Tags") + .HasForeignKey("ShowMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SongMetadata", null) + .WithMany("Tags") + .HasForeignKey("SongMetadataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TraktListItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem") + .WithMany("TraktListItems") + .HasForeignKey("MediaItemId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("ErsatzTV.Core.Domain.TraktList", "TraktList") + .WithMany("Items") + .HasForeignKey("TraktListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MediaItem"); + + b.Navigation("TraktList"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TraktListItemGuid", b => + { + b.HasOne("ErsatzTV.Core.Domain.TraktListItem", "TraktListItem") + .WithMany("Guids") + .HasForeignKey("TraktListItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TraktListItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Writer", b => + { + b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null) + .WithMany("Writers") + .HasForeignKey("EpisodeMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MovieMetadata", null) + .WithMany("Writers") + .HasForeignKey("MovieMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.OtherVideoMetadata", null) + .WithMany("Writers") + .HasForeignKey("OtherVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Emby.EmbyPathInfo", b => + { + b.HasOne("ErsatzTV.Core.Domain.EmbyLibrary", null) + .WithMany("PathInfos") + .HasForeignKey("EmbyLibraryId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Jellyfin.JellyfinPathInfo", b => + { + b.HasOne("ErsatzTV.Core.Domain.JellyfinLibrary", null) + .WithMany("PathInfos") + .HasForeignKey("JellyfinLibraryId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Artist", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.Artist", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyLibrary", b => + { + b.HasOne("ErsatzTV.Core.Domain.Library", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.EmbyLibrary", "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.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.JellyfinLibrary", b => + { + b.HasOne("ErsatzTV.Core.Domain.Library", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.JellyfinLibrary", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinMediaSource", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaSource", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.JellyfinMediaSource", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LocalLibrary", b => + { + b.HasOne("ErsatzTV.Core.Domain.Library", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.LocalLibrary", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LocalMediaSource", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaSource", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.LocalMediaSource", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Movie", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.Movie", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideo", b => + { + b.HasOne("ErsatzTV.Core.Domain.Artist", "Artist") + .WithMany("MusicVideos") + .HasForeignKey("ArtistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.MusicVideo", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Artist"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.OtherVideo", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.OtherVideo", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.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.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.Season", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.Season", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.Show", "Show") + .WithMany("Seasons") + .HasForeignKey("ShowId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Show"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Show", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.Show", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Song", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.Song", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyEpisode", b => + { + b.HasOne("ErsatzTV.Core.Domain.Episode", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.EmbyEpisode", "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.EmbySeason", b => + { + b.HasOne("ErsatzTV.Core.Domain.Season", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.EmbySeason", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyShow", b => + { + b.HasOne("ErsatzTV.Core.Domain.Show", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.EmbyShow", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinEpisode", b => + { + b.HasOne("ErsatzTV.Core.Domain.Episode", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.JellyfinEpisode", "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.JellyfinSeason", b => + { + b.HasOne("ErsatzTV.Core.Domain.Season", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.JellyfinSeason", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinShow", b => + { + b.HasOne("ErsatzTV.Core.Domain.Show", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.JellyfinShow", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexEpisode", b => + { + b.HasOne("ErsatzTV.Core.Domain.Episode", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.PlexEpisode", "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.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.PlexShow", b => + { + b.HasOne("ErsatzTV.Core.Domain.Show", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.PlexShow", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ArtistMetadata", b => + { + b.Navigation("Actors"); + + b.Navigation("Artwork"); + + b.Navigation("Genres"); + + b.Navigation("Guids"); + + b.Navigation("Moods"); + + b.Navigation("Studios"); + + b.Navigation("Styles"); + + b.Navigation("Subtitles"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Channel", b => + { + b.Navigation("Artwork"); + + b.Navigation("Playouts"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Collection", b => + { + b.Navigation("CollectionItems"); + + b.Navigation("MultiCollectionItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EpisodeMetadata", b => + { + b.Navigation("Actors"); + + b.Navigation("Artwork"); + + b.Navigation("Directors"); + + b.Navigation("Genres"); + + b.Navigation("Guids"); + + b.Navigation("Studios"); + + b.Navigation("Subtitles"); + + b.Navigation("Tags"); + + b.Navigation("Writers"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Library", b => + { + b.Navigation("Paths"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LibraryPath", b => + { + b.Navigation("LibraryFolders"); + + b.Navigation("MediaItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaItem", b => + { + b.Navigation("CollectionItems"); + + b.Navigation("TraktListItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaSource", b => + { + b.Navigation("Libraries"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaVersion", b => + { + b.Navigation("Chapters"); + + b.Navigation("MediaFiles"); + + b.Navigation("Streams"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MovieMetadata", b => + { + b.Navigation("Actors"); + + b.Navigation("Artwork"); + + b.Navigation("Directors"); + + b.Navigation("Genres"); + + b.Navigation("Guids"); + + b.Navigation("Studios"); + + b.Navigation("Subtitles"); + + b.Navigation("Tags"); + + b.Navigation("Writers"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MultiCollection", b => + { + b.Navigation("MultiCollectionItems"); + + b.Navigation("MultiCollectionSmartItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideoMetadata", b => + { + b.Navigation("Actors"); + + b.Navigation("Artists"); + + b.Navigation("Artwork"); + + b.Navigation("Genres"); + + b.Navigation("Guids"); + + b.Navigation("Studios"); + + b.Navigation("Subtitles"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.OtherVideoMetadata", b => + { + b.Navigation("Actors"); + + b.Navigation("Artwork"); + + b.Navigation("Directors"); + + b.Navigation("Genres"); + + b.Navigation("Guids"); + + b.Navigation("Studios"); + + b.Navigation("Subtitles"); + + b.Navigation("Tags"); + + b.Navigation("Writers"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b => + { + b.Navigation("Items"); + + b.Navigation("ProgramScheduleAnchors"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramSchedule", b => + { + b.Navigation("Items"); + + b.Navigation("Playouts"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.SeasonMetadata", b => + { + b.Navigation("Actors"); + + b.Navigation("Artwork"); + + b.Navigation("Genres"); + + b.Navigation("Guids"); + + b.Navigation("Studios"); + + b.Navigation("Subtitles"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ShowMetadata", b => + { + b.Navigation("Actors"); + + b.Navigation("Artwork"); + + b.Navigation("Genres"); + + b.Navigation("Guids"); + + b.Navigation("Studios"); + + b.Navigation("Subtitles"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.SmartCollection", b => + { + b.Navigation("MultiCollectionSmartItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.SongMetadata", b => + { + b.Navigation("Actors"); + + b.Navigation("Artwork"); + + b.Navigation("Genres"); + + b.Navigation("Guids"); + + b.Navigation("Studios"); + + b.Navigation("Subtitles"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TraktList", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.TraktListItem", b => + { + b.Navigation("Guids"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Artist", b => + { + b.Navigation("ArtistMetadata"); + + b.Navigation("MusicVideos"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyLibrary", b => + { + b.Navigation("PathInfos"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyMediaSource", b => + { + b.Navigation("Connections"); + + b.Navigation("PathReplacements"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Episode", b => + { + b.Navigation("EpisodeMetadata"); + + b.Navigation("MediaVersions"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinLibrary", b => + { + b.Navigation("PathInfos"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinMediaSource", b => + { + b.Navigation("Connections"); + + b.Navigation("PathReplacements"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Movie", b => + { + b.Navigation("MediaVersions"); + + b.Navigation("MovieMetadata"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideo", b => + { + b.Navigation("MediaVersions"); + + b.Navigation("MusicVideoMetadata"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.OtherVideo", b => + { + b.Navigation("MediaVersions"); + + b.Navigation("OtherVideoMetadata"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSource", b => + { + b.Navigation("Connections"); + + b.Navigation("PathReplacements"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Season", b => + { + b.Navigation("Episodes"); + + b.Navigation("SeasonMetadata"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Show", b => + { + b.Navigation("Seasons"); + + b.Navigation("ShowMetadata"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Song", b => + { + b.Navigation("MediaVersions"); + + b.Navigation("SongMetadata"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ErsatzTV.Infrastructure/Migrations/20220506005328_Expand_OtherVideoMetadata.cs b/ErsatzTV.Infrastructure/Migrations/20220506005328_Expand_OtherVideoMetadata.cs new file mode 100644 index 000000000..e21f1d93f --- /dev/null +++ b/ErsatzTV.Infrastructure/Migrations/20220506005328_Expand_OtherVideoMetadata.cs @@ -0,0 +1,209 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ErsatzTV.Infrastructure.Migrations +{ + public partial class Expand_OtherVideoMetadata : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Actor_OtherVideoMetadata_OtherVideoMetadataId", + table: "Actor"); + + migrationBuilder.DropForeignKey( + name: "FK_Genre_OtherVideoMetadata_OtherVideoMetadataId", + table: "Genre"); + + migrationBuilder.DropForeignKey( + name: "FK_MetadataGuid_OtherVideoMetadata_OtherVideoMetadataId", + table: "MetadataGuid"); + + migrationBuilder.DropForeignKey( + name: "FK_Studio_OtherVideoMetadata_OtherVideoMetadataId", + table: "Studio"); + + migrationBuilder.AddColumn( + name: "OtherVideoMetadataId", + table: "Writer", + type: "INTEGER", + nullable: true); + + migrationBuilder.AddColumn( + name: "ContentRating", + table: "OtherVideoMetadata", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "Outline", + table: "OtherVideoMetadata", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "Plot", + table: "OtherVideoMetadata", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "Tagline", + table: "OtherVideoMetadata", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "OtherVideoMetadataId", + table: "Director", + type: "INTEGER", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_Writer_OtherVideoMetadataId", + table: "Writer", + column: "OtherVideoMetadataId"); + + migrationBuilder.CreateIndex( + name: "IX_Director_OtherVideoMetadataId", + table: "Director", + column: "OtherVideoMetadataId"); + + migrationBuilder.AddForeignKey( + name: "FK_Actor_OtherVideoMetadata_OtherVideoMetadataId", + table: "Actor", + column: "OtherVideoMetadataId", + principalTable: "OtherVideoMetadata", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Director_OtherVideoMetadata_OtherVideoMetadataId", + table: "Director", + column: "OtherVideoMetadataId", + principalTable: "OtherVideoMetadata", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Genre_OtherVideoMetadata_OtherVideoMetadataId", + table: "Genre", + column: "OtherVideoMetadataId", + principalTable: "OtherVideoMetadata", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_MetadataGuid_OtherVideoMetadata_OtherVideoMetadataId", + table: "MetadataGuid", + column: "OtherVideoMetadataId", + principalTable: "OtherVideoMetadata", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Studio_OtherVideoMetadata_OtherVideoMetadataId", + table: "Studio", + column: "OtherVideoMetadataId", + principalTable: "OtherVideoMetadata", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Writer_OtherVideoMetadata_OtherVideoMetadataId", + table: "Writer", + column: "OtherVideoMetadataId", + principalTable: "OtherVideoMetadata", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Actor_OtherVideoMetadata_OtherVideoMetadataId", + table: "Actor"); + + migrationBuilder.DropForeignKey( + name: "FK_Director_OtherVideoMetadata_OtherVideoMetadataId", + table: "Director"); + + migrationBuilder.DropForeignKey( + name: "FK_Genre_OtherVideoMetadata_OtherVideoMetadataId", + table: "Genre"); + + migrationBuilder.DropForeignKey( + name: "FK_MetadataGuid_OtherVideoMetadata_OtherVideoMetadataId", + table: "MetadataGuid"); + + migrationBuilder.DropForeignKey( + name: "FK_Studio_OtherVideoMetadata_OtherVideoMetadataId", + table: "Studio"); + + migrationBuilder.DropForeignKey( + name: "FK_Writer_OtherVideoMetadata_OtherVideoMetadataId", + table: "Writer"); + + migrationBuilder.DropIndex( + name: "IX_Writer_OtherVideoMetadataId", + table: "Writer"); + + migrationBuilder.DropIndex( + name: "IX_Director_OtherVideoMetadataId", + table: "Director"); + + migrationBuilder.DropColumn( + name: "OtherVideoMetadataId", + table: "Writer"); + + migrationBuilder.DropColumn( + name: "ContentRating", + table: "OtherVideoMetadata"); + + migrationBuilder.DropColumn( + name: "Outline", + table: "OtherVideoMetadata"); + + migrationBuilder.DropColumn( + name: "Plot", + table: "OtherVideoMetadata"); + + migrationBuilder.DropColumn( + name: "Tagline", + table: "OtherVideoMetadata"); + + migrationBuilder.DropColumn( + name: "OtherVideoMetadataId", + table: "Director"); + + migrationBuilder.AddForeignKey( + name: "FK_Actor_OtherVideoMetadata_OtherVideoMetadataId", + table: "Actor", + column: "OtherVideoMetadataId", + principalTable: "OtherVideoMetadata", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_Genre_OtherVideoMetadata_OtherVideoMetadataId", + table: "Genre", + column: "OtherVideoMetadataId", + principalTable: "OtherVideoMetadata", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_MetadataGuid_OtherVideoMetadata_OtherVideoMetadataId", + table: "MetadataGuid", + column: "OtherVideoMetadataId", + principalTable: "OtherVideoMetadata", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_Studio_OtherVideoMetadata_OtherVideoMetadataId", + table: "Studio", + column: "OtherVideoMetadataId", + principalTable: "OtherVideoMetadata", + principalColumn: "Id"); + } + } +} diff --git a/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs b/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs index 415e62869..80d9c41a0 100644 --- a/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs +++ b/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs @@ -388,12 +388,17 @@ namespace ErsatzTV.Infrastructure.Migrations b.Property("Name") .HasColumnType("TEXT"); + b.Property("OtherVideoMetadataId") + .HasColumnType("INTEGER"); + b.HasKey("Id"); b.HasIndex("EpisodeMetadataId"); b.HasIndex("MovieMetadataId"); + b.HasIndex("OtherVideoMetadataId"); + b.ToTable("Director", (string)null); }); @@ -1296,6 +1301,9 @@ namespace ErsatzTV.Infrastructure.Migrations .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); + b.Property("ContentRating") + .HasColumnType("TEXT"); + b.Property("DateAdded") .HasColumnType("TEXT"); @@ -1311,12 +1319,21 @@ namespace ErsatzTV.Infrastructure.Migrations b.Property("OtherVideoId") .HasColumnType("INTEGER"); + b.Property("Outline") + .HasColumnType("TEXT"); + + b.Property("Plot") + .HasColumnType("TEXT"); + b.Property("ReleaseDate") .HasColumnType("TEXT"); b.Property("SortTitle") .HasColumnType("TEXT"); + b.Property("Tagline") + .HasColumnType("TEXT"); + b.Property("Title") .HasColumnType("TEXT"); @@ -2142,12 +2159,17 @@ namespace ErsatzTV.Infrastructure.Migrations b.Property("Name") .HasColumnType("TEXT"); + b.Property("OtherVideoMetadataId") + .HasColumnType("INTEGER"); + b.HasKey("Id"); b.HasIndex("EpisodeMetadataId"); b.HasIndex("MovieMetadataId"); + b.HasIndex("OtherVideoMetadataId"); + b.ToTable("Writer", (string)null); }); @@ -2603,7 +2625,8 @@ namespace ErsatzTV.Infrastructure.Migrations b.HasOne("ErsatzTV.Core.Domain.OtherVideoMetadata", null) .WithMany("Actors") - .HasForeignKey("OtherVideoMetadataId"); + .HasForeignKey("OtherVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null) .WithMany("Actors") @@ -2735,6 +2758,11 @@ namespace ErsatzTV.Infrastructure.Migrations .WithMany("Directors") .HasForeignKey("MovieMetadataId") .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.OtherVideoMetadata", null) + .WithMany("Directors") + .HasForeignKey("OtherVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); }); modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyConnection", b => @@ -2835,7 +2863,8 @@ namespace ErsatzTV.Infrastructure.Migrations b.HasOne("ErsatzTV.Core.Domain.OtherVideoMetadata", null) .WithMany("Genres") - .HasForeignKey("OtherVideoMetadataId"); + .HasForeignKey("OtherVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null) .WithMany("Genres") @@ -3001,7 +3030,8 @@ namespace ErsatzTV.Infrastructure.Migrations b.HasOne("ErsatzTV.Core.Domain.OtherVideoMetadata", null) .WithMany("Guids") - .HasForeignKey("OtherVideoMetadataId"); + .HasForeignKey("OtherVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null) .WithMany("Guids") @@ -3429,7 +3459,8 @@ namespace ErsatzTV.Infrastructure.Migrations b.HasOne("ErsatzTV.Core.Domain.OtherVideoMetadata", null) .WithMany("Studios") - .HasForeignKey("OtherVideoMetadataId"); + .HasForeignKey("OtherVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null) .WithMany("Studios") @@ -3573,6 +3604,11 @@ namespace ErsatzTV.Infrastructure.Migrations .WithMany("Writers") .HasForeignKey("MovieMetadataId") .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.OtherVideoMetadata", null) + .WithMany("Writers") + .HasForeignKey("OtherVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); }); modelBuilder.Entity("ErsatzTV.Core.Emby.EmbyPathInfo", b => @@ -4054,6 +4090,8 @@ namespace ErsatzTV.Infrastructure.Migrations b.Navigation("Artwork"); + b.Navigation("Directors"); + b.Navigation("Genres"); b.Navigation("Guids"); @@ -4063,6 +4101,8 @@ namespace ErsatzTV.Infrastructure.Migrations b.Navigation("Subtitles"); b.Navigation("Tags"); + + b.Navigation("Writers"); }); modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b => diff --git a/ErsatzTV.Infrastructure/Search/SearchIndex.cs b/ErsatzTV.Infrastructure/Search/SearchIndex.cs index 89f33bd43..17237e915 100644 --- a/ErsatzTV.Infrastructure/Search/SearchIndex.cs +++ b/ErsatzTV.Infrastructure/Search/SearchIndex.cs @@ -952,13 +952,61 @@ public sealed class SearchIndex : ISearchIndex doc.Add(new Int32Field(WidthField, version.Width, 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( + new StringField( + ReleaseDateField, + metadata.ReleaseDate.Value.ToString("yyyyMMdd"), + Field.Store.NO)); + } + doc.Add(new StringField(AddedDateField, metadata.DateAdded.ToString("yyyyMMdd"), Field.Store.NO)); + if (!string.IsNullOrWhiteSpace(metadata.Plot)) + { + doc.Add(new TextField(PlotField, metadata.Plot ?? string.Empty, Field.Store.NO)); + } + + foreach (Genre genre in metadata.Genres) + { + doc.Add(new TextField(GenreField, genre.Name, Field.Store.NO)); + } + foreach (Tag tag in metadata.Tags) { doc.Add(new TextField(TagField, tag.Name, Field.Store.NO)); } + foreach (Studio studio in metadata.Studios) + { + doc.Add(new TextField(StudioField, studio.Name, Field.Store.NO)); + } + + foreach (Actor actor in metadata.Actors) + { + doc.Add(new TextField(ActorField, actor.Name, Field.Store.NO)); + } + + foreach (Director director in metadata.Directors) + { + doc.Add(new TextField(DirectorField, director.Name, Field.Store.NO)); + } + + foreach (Writer writer in metadata.Writers) + { + doc.Add(new TextField(WriterField, writer.Name, Field.Store.NO)); + } + _writer.UpdateDocument(new Term(IdField, otherVideo.Id.ToString()), doc); } catch (Exception ex) diff --git a/ErsatzTV/Startup.cs b/ErsatzTV/Startup.cs index 8b1cd0903..d5e2ea6d6 100644 --- a/ErsatzTV/Startup.cs +++ b/ErsatzTV/Startup.cs @@ -415,6 +415,7 @@ public class Startup services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); // services.AddTransient(typeof(IRequestHandler<,>), typeof(GetRecentLogEntriesHandler<>)); diff --git a/README.md b/README.md index 57d877be9..eec7d7c9a 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Want to join the community or have a question? Join us on [Discord](https://disc - [Emby](https://emby.media/) media and metadata - Song and music video libraries - Pre-roll, mid-roll, post-roll filler options -- Picture-based subtitle burn-in +- Subtitle burn-in ## License diff --git a/docs/user-guide/local-libraries.md b/docs/user-guide/local-libraries.md index 10844e6f8..59b6bf26d 100644 --- a/docs/user-guide/local-libraries.md +++ b/docs/user-guide/local-libraries.md @@ -13,12 +13,19 @@ Each movie folder may contain a `movie.nfo` file, or an NFO file with exactly th ErsatzTV will read the following fields from the movie NFO: - Title +- Sort Title +- Outline - Year +- MPAA - Premiered - Plot - Genre(s) - Tag(s) - Studio(s) +- Actor(s) +- Credit(s) +- Director(s) +- Unique Id(s) ### Movie Fallback Metadata @@ -115,6 +122,26 @@ When no artist NFO is found, the artist metadata will only contain a name, which The `Other Videos` library has no folder requirements, but folders can be a useful source of metadata. +### NFO Metadata + +Each other video may have a corresponding NFO file with exactly the same name, except for the `.nfo` extension. The NFO must use the movie format. See [Kodi Wiki](https://kodi.wiki/view/NFO_files/Movies) for more information. +ErsatzTV will read the following fields from the other video NFO: + +- Title +- Sort Title +- Outline +- Year +- MPAA +- Premiered +- Plot +- Genre(s) +- Tag(s) +- Studio(s) +- Actor(s) +- Credit(s) +- Director(s) +- Unique Id(s) + ### Other Video Fallback Metadata Other videos will have a tag added to their metadata for every containing folder, including the top-level folder. As an example, consider adding a commercials folder with the following files: diff --git a/docs/user-guide/search.md b/docs/user-guide/search.md index 9558ae18a..36892d819 100644 --- a/docs/user-guide/search.md +++ b/docs/user-guide/search.md @@ -114,12 +114,22 @@ The following fields are available for searching music videos: The following fields are available for searching other videos: -- `title`: The filename of the video (without extension) -- `tag`: All of the video's parent folders -- `minutes`: the rounded-up whole number duration of the video in minutes -- `added_date`: The date the other video was added to ErsatzTV (YYYYMMDD) -- `height`: The other video height -- `width`: The other video width +- `title`: The NFO title or the filename of the video (without extension) +- `genre`: The video genre +- `tag`: The video tag +- `plot`: The video plot +- `studio`: The video studio +- `actor`: An actor from the video +- `director`: A director from the video +- `writer`: A writer from the video +- `library_name`: The name of the library that contains the video +- `content_rating`: The video content rating (case-sensitive) +- `language`: The video audio stream language +- `release_date`: The video release date (YYYYMMDD) +- `added_date`: The date the video was added to ErsatzTV (YYYYMMDD) +- `minutes`: The rounded-up whole number duration of the video in minutes +- `height`: The video height +- `width`: The video width - `type`: Always `other_video` ### Songs