using ErsatzTV.Application.LibraryBrowse; using ErsatzTV.Core.Api.LibraryBrowse; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Search; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Search; using ErsatzTV.Tests.Support; using NSubstitute; using NUnit.Framework; using Shouldly; namespace ErsatzTV.Tests.Application.LibraryBrowse; [TestFixture] public class GetLibraryBrowseItemsHandlerTests { private InMemoryTvContext _db = null!; private ISearchIndex _searchIndex = null!; [SetUp] public async Task SetUp() { _db = await InMemoryTvContext.CreateAsync(); _searchIndex = Substitute.For(); } [TearDown] public async Task TearDown() => await _db.DisposeAsync(); [Test] public async Task Handle_Should_Search_Index_With_Type_Library_And_Title_Filters() { _searchIndex.Search( Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any>(), Arg.Any()) .Returns(new SearchResult([], 0)); var handler = new GetLibraryBrowseItemsHandler(_searchIndex, _db.Factory); await handler.Handle( new GetLibraryBrowseItems("star", 5, LibraryBrowseMediaType.Movie, 2, 25), CancellationToken.None); await _searchIndex.Received(1).Search( Arg.Is(q => q.Contains("type:movie") && q.Contains("library_id:5") && q.Contains("star")), string.Empty, 50, 25, Arg.Is>(fields => fields.SequenceEqual(new[] { LuceneSearchIndex.TitleAndYearSearchField })), Arg.Any()); } [Test] public async Task Handle_Should_Hydrate_Media_Items_In_Search_Order_With_Artwork_Duration_And_Counts() { await SeedLibraryGraph(); _searchIndex.Search( Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any>(), Arg.Any()) .Returns(new SearchResult( [ new SearchItem(LuceneSearchIndex.ShowType, 20), new SearchItem(LuceneSearchIndex.MovieType, 10) ], 2)); var handler = new GetLibraryBrowseItemsHandler(_searchIndex, _db.Factory); PagedLibraryBrowseItemsResponseModel result = await handler.Handle( new GetLibraryBrowseItems("", null, null, 0, 10), CancellationToken.None); result.TotalCount.ShouldBe(2); result.Page[0].MediaType.ShouldBe(LibraryBrowseMediaType.TelevisionShow); result.Page[0].Title.ShouldBe("Collision Show"); result.Page[0].ItemCount.ShouldBe(1); result.Page[0].MediaItemId.ShouldBe(20); result.Page[1].MediaType.ShouldBe(LibraryBrowseMediaType.Movie); result.Page[1].Title.ShouldBe("Collision Movie"); result.Page[1].Artwork.ShouldBe("/artwork/posters/movie-poster.jpg"); result.Page[1].Duration.ShouldBe(TimeSpan.FromMinutes(95)); result.Page[1].MediaItemId.ShouldBe(10); } [Test] public async Task Handle_Should_Return_Typed_Collection_And_Playlist_Picker_Targets() { await SeedCollectionGraph(); _searchIndex.Search( Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any>(), Arg.Any()) .Returns(new SearchResult([], 0)); var handler = new GetLibraryBrowseItemsHandler(_searchIndex, _db.Factory); PagedLibraryBrowseItemsResponseModel result = await handler.Handle( new GetLibraryBrowseItems("", null, null, 0, 10), CancellationToken.None); result.TotalCount.ShouldBe(5); result.Page.ShouldContain(i => i.MediaType == LibraryBrowseMediaType.Collection && i.CollectionKind == "Manual" && i.CollectionType == CollectionType.Collection && i.CollectionId == 20 && i.ItemCount == 1); result.Page.ShouldContain(i => i.MediaType == LibraryBrowseMediaType.SmartCollection && i.CollectionKind == "Smart" && i.CollectionType == CollectionType.SmartCollection && i.SmartCollectionId == 30); result.Page.ShouldContain(i => i.MediaType == LibraryBrowseMediaType.MultiCollection && i.CollectionKind == "Multi" && i.CollectionType == CollectionType.MultiCollection && i.MultiCollectionId == 40); result.Page.ShouldContain(i => i.MediaType == LibraryBrowseMediaType.RerunCollection && i.CollectionKind == "Rerun" && i.CollectionType == CollectionType.RerunFirstRun && i.RerunCollectionId == 50); result.Page.ShouldContain(i => i.MediaType == LibraryBrowseMediaType.Playlist && i.CollectionType == CollectionType.Playlist && i.PlaylistId == 60); } [Test] public async Task Handle_Should_Page_Across_Media_Manual_Collections_And_Smart_Collections() { await SeedMediaAndCollectionPagingGraph(); _searchIndex.Search( Arg.Any(), Arg.Any(), Arg.Is(offset => offset == 0), Arg.Any(), Arg.Any>(), Arg.Any()) .Returns(new SearchResult( [ new SearchItem(LuceneSearchIndex.MovieType, 101), new SearchItem(LuceneSearchIndex.MovieType, 102) ], 2)); _searchIndex.Search( Arg.Any(), Arg.Any(), Arg.Is(offset => offset > 0), Arg.Any(), Arg.Any>(), Arg.Any()) .Returns(new SearchResult([], 2)); var handler = new GetLibraryBrowseItemsHandler(_searchIndex, _db.Factory); PagedLibraryBrowseItemsResponseModel page0 = await handler.Handle( new GetLibraryBrowseItems("", null, null, 0, 3), CancellationToken.None); PagedLibraryBrowseItemsResponseModel page1 = await handler.Handle( new GetLibraryBrowseItems("", null, null, 1, 3), CancellationToken.None); PagedLibraryBrowseItemsResponseModel page2 = await handler.Handle( new GetLibraryBrowseItems("", null, null, 2, 3), CancellationToken.None); page0.TotalCount.ShouldBe(7); page0.Page.Select(i => i.Title).ShouldBe(["Movie One", "Movie Two", "Manual A"]); page1.TotalCount.ShouldBe(7); page1.Page.Select(i => i.Title).ShouldBe(["Manual B", "Manual C", "Smart A"]); page2.TotalCount.ShouldBe(7); page2.Page.Select(i => i.Title).ShouldBe(["Smart B"]); } [Test] public async Task Handle_Should_Hydrate_Manual_Collection_Artwork_And_Total_Duration() { await SeedManualCollectionMetadataGraph(); _searchIndex.Search( Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any>(), Arg.Any()) .Returns(new SearchResult([], 0)); var handler = new GetLibraryBrowseItemsHandler(_searchIndex, _db.Factory); PagedLibraryBrowseItemsResponseModel result = await handler.Handle( new GetLibraryBrowseItems("", null, LibraryBrowseMediaType.Collection, 0, 10), CancellationToken.None); result.TotalCount.ShouldBe(1); result.Page.Count.ShouldBe(1); result.Page[0].Title.ShouldBe("Manual Metadata"); result.Page[0].Artwork.ShouldBe("/artwork/posters/first-poster.jpg"); result.Page[0].Duration.ShouldBe(TimeSpan.FromMinutes(75)); result.Page[0].ItemCount.ShouldBe(2); } [Test] public async Task Handle_Should_Hydrate_Seasons_With_Composed_Titles_And_Episode_Counts() { await SeedSeasonGraph(); _searchIndex.Search( Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any>(), Arg.Any()) .Returns(new SearchResult( [ new SearchItem(LuceneSearchIndex.SeasonType, 301), new SearchItem(LuceneSearchIndex.SeasonType, 302) ], 2)); var handler = new GetLibraryBrowseItemsHandler(_searchIndex, _db.Factory); PagedLibraryBrowseItemsResponseModel result = await handler.Handle( new GetLibraryBrowseItems("", null, LibraryBrowseMediaType.TelevisionSeason, 0, 10), CancellationToken.None); result.TotalCount.ShouldBe(2); result.Page[0].Title.ShouldBe("Season Show - Season 2"); result.Page[0].ItemCount.ShouldBe(2); result.Page[0].CollectionType.ShouldBe(CollectionType.TelevisionSeason); result.Page[0].MediaItemId.ShouldBe(301); // Season 301 has its own poster. result.Page[0].Artwork.ShouldBe("/artwork/posters/season-2.jpg"); result.Page[1].Title.ShouldBe("Season Show - Specials"); result.Page[1].ItemCount.ShouldBe(1); result.Page[1].MediaItemId.ShouldBe(302); // Specials has no poster of its own, so it falls back to the show's poster. result.Page[1].Artwork.ShouldBe("/artwork/posters/show.jpg"); } [Test] public async Task Handle_Should_Browse_Seasons_For_A_Specific_Show_By_ParentId() { await SeedSeasonGraph(); // ParentId drill-in bypasses the search index entirely. var handler = new GetLibraryBrowseItemsHandler(_searchIndex, _db.Factory); PagedLibraryBrowseItemsResponseModel result = await handler.Handle( new GetLibraryBrowseItems("", null, LibraryBrowseMediaType.TelevisionSeason, 0, 10, 300), CancellationToken.None); result.TotalCount.ShouldBe(2); // Ordered by season number: Specials (0) then Season 2. result.Page[0].Title.ShouldBe("Season Show - Specials"); result.Page[0].MediaItemId.ShouldBe(302); result.Page[1].Title.ShouldBe("Season Show - Season 2"); result.Page[1].MediaItemId.ShouldBe(301); await _searchIndex.DidNotReceive().Search( Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any>(), Arg.Any()); } [Test] public async Task Handle_Should_Browse_Episodes_For_A_Specific_Season_By_ParentId() { await SeedEpisodeDrillInGraph(); var handler = new GetLibraryBrowseItemsHandler(_searchIndex, _db.Factory); PagedLibraryBrowseItemsResponseModel result = await handler.Handle( new GetLibraryBrowseItems("", null, LibraryBrowseMediaType.Episode, 0, 10, 701), CancellationToken.None); result.TotalCount.ShouldBe(3); // Ordered by episode number regardless of insertion order. result.Page[0].MediaItemId.ShouldBe(712); result.Page[1].MediaItemId.ShouldBe(711); result.Page[2].MediaItemId.ShouldBe(713); // #220: every episode carries its parent season id so the SPA can route to the season // detail page and anchor to the episode (`/app/media/seasons/{seasonId}#episode-{id}`). result.Page.ShouldAllBe(p => p.SeasonId == 701); await _searchIndex.DidNotReceive().Search( Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any>(), Arg.Any()); } [Test] public async Task Handle_Should_Leave_SeasonId_Null_For_Non_Episode_Kinds() { await SeedLibraryGraph(); _searchIndex.Search( Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any>(), Arg.Any()) .Returns(new SearchResult( [ new SearchItem(LuceneSearchIndex.ShowType, 20), new SearchItem(LuceneSearchIndex.MovieType, 10) ], 2)); var handler = new GetLibraryBrowseItemsHandler(_searchIndex, _db.Factory); PagedLibraryBrowseItemsResponseModel result = await handler.Handle( new GetLibraryBrowseItems("", null, null, 0, 10), CancellationToken.None); result.Page.ShouldAllBe(p => p.SeasonId == null); } [Test] public async Task Handle_Should_Browse_Music_Videos_For_A_Specific_Artist_By_ParentId() { await SeedMusicVideoDrillInGraph(); var handler = new GetLibraryBrowseItemsHandler(_searchIndex, _db.Factory); PagedLibraryBrowseItemsResponseModel result = await handler.Handle( new GetLibraryBrowseItems("", null, LibraryBrowseMediaType.MusicVideo, 0, 10, 801), CancellationToken.None); result.TotalCount.ShouldBe(2); // Ordered by album then track: "Album A"/track 1 before "Album B"/track 1. result.Page[0].MediaItemId.ShouldBe(812); result.Page[1].MediaItemId.ShouldBe(811); await _searchIndex.DidNotReceive().Search( Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any>(), Arg.Any()); } [Test] public async Task Handle_Should_Root_Jellyfin_Artwork_Urls_With_FillHeight() { await SeedJellyfinMovieGraph(); _searchIndex.Search( Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any>(), Arg.Any()) .Returns(new SearchResult([new SearchItem(LuceneSearchIndex.MovieType, 601)], 1)); var handler = new GetLibraryBrowseItemsHandler(_searchIndex, _db.Factory); PagedLibraryBrowseItemsResponseModel result = await handler.Handle( new GetLibraryBrowseItems("", null, LibraryBrowseMediaType.Movie, 0, 10), CancellationToken.None); result.TotalCount.ShouldBe(1); result.Page[0].Artwork.ShouldBe("/artwork/posters/jellyfin/Items/abc/Images/Primary?tag=xyz&fillHeight=440"); } [Test] public async Task Handle_Should_Hydrate_Artists_With_Music_Video_Counts() { await SeedArtistGraph(); _searchIndex.Search( Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any>(), Arg.Any()) .Returns(new SearchResult([new SearchItem(LuceneSearchIndex.ArtistType, 401)], 1)); var handler = new GetLibraryBrowseItemsHandler(_searchIndex, _db.Factory); PagedLibraryBrowseItemsResponseModel result = await handler.Handle( new GetLibraryBrowseItems("", null, LibraryBrowseMediaType.Artist, 0, 10), CancellationToken.None); result.TotalCount.ShouldBe(1); result.Page[0].Title.ShouldBe("Counted Artist"); result.Page[0].ItemCount.ShouldBe(2); result.Page[0].Artwork.ShouldBe("/artwork/thumbnails/artist-thumb.jpg"); result.Page[0].CollectionType.ShouldBe(CollectionType.Artist); result.Page[0].MediaItemId.ShouldBe(401); } [Test] public async Task Handle_Should_Apply_Library_Filter_To_Manual_Collections_Only() { await SeedLibraryFilteredCollectionsGraph(); _searchIndex.Search( Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any>(), Arg.Any()) .Returns(new SearchResult([], 0)); var handler = new GetLibraryBrowseItemsHandler(_searchIndex, _db.Factory); PagedLibraryBrowseItemsResponseModel result = await handler.Handle( new GetLibraryBrowseItems("", 501, null, 0, 10), CancellationToken.None); result.TotalCount.ShouldBe(1); result.Page.Count.ShouldBe(1); result.Page[0].Title.ShouldBe("Included Manual"); result.Page[0].MediaType.ShouldBe(LibraryBrowseMediaType.Collection); result.Page[0].CollectionId.ShouldBe(510); } private async Task SeedLibraryGraph() { await using TvContext context = _db.CreateContext(); var library = new LocalLibrary { Id = 1, Name = "Local Movies", MediaKind = LibraryMediaKind.Movies, Paths = [] }; var path = new LibraryPath { Id = 1, Path = "/media", Library = library, LibraryFolders = [], MediaItems = [] }; library.Paths.Add(path); var movie = new Movie { Id = 10, LibraryPath = path, Collections = [], CollectionItems = [], TraktListItems = [], MediaVersions = [new MediaVersion { Duration = TimeSpan.FromMinutes(95) }], MovieMetadata = [ MakeMovieMetadata("Collision Movie", "movie-poster.jpg") ] }; var show = new Show { Id = 20, LibraryPath = path, Collections = [], CollectionItems = [], TraktListItems = [], Seasons = [], ShowMetadata = [MakeShowMetadata("Collision Show", "show-poster.jpg")] }; var season = new Season { Id = 11, LibraryPath = path, Show = show, Collections = [], CollectionItems = [], TraktListItems = [], Episodes = [], SeasonMetadata = [] }; var episode = new Episode { Id = 12, LibraryPath = path, Season = season, Collections = [], CollectionItems = [], TraktListItems = [], EpisodeMetadata = [], MediaVersions = [new MediaVersion { Duration = TimeSpan.FromMinutes(25) }] }; season.Episodes.Add(episode); show.Seasons.Add(season); path.MediaItems.AddRange([movie, show, season, episode]); context.LocalLibraries.Add(library); context.Movies.Add(movie); context.Shows.Add(show); context.Seasons.Add(season); context.Episodes.Add(episode); await context.SaveChangesAsync(); } private async Task SeedCollectionGraph() { await using TvContext context = _db.CreateContext(); var collection = new Collection { Id = 20, Name = "Manual", MediaItems = [], CollectionItems = [] }; var movie = new Movie { Id = 21, Collections = [collection], CollectionItems = [], TraktListItems = [], MovieMetadata = [], MediaVersions = [] }; collection.MediaItems.Add(movie); var smart = new SmartCollection { Id = 30, Name = "Smart", Query = "tag:kids" }; var multi = new MultiCollection { Id = 40, Name = "Multi", Collections = [collection], SmartCollections = [smart] }; var rerun = new RerunCollection { Id = 50, Name = "Rerun", CollectionType = CollectionType.Collection }; var playlist = new Playlist { Id = 60, Name = "Playlist", Items = [new PlaylistItem { CollectionType = CollectionType.Collection, CollectionId = 20 }] }; context.Collections.Add(collection); context.Movies.Add(movie); context.SmartCollections.Add(smart); context.MultiCollections.Add(multi); context.RerunCollections.Add(rerun); context.Playlists.Add(playlist); await context.SaveChangesAsync(); } private async Task SeedMediaAndCollectionPagingGraph() { await using TvContext context = _db.CreateContext(); (LocalLibrary library, LibraryPath path) = MakeLibrary(100, "Paging Library"); var movie1 = MakeMovie(101, path, "Movie One", "movie-one.jpg", TimeSpan.FromMinutes(10)); var movie2 = MakeMovie(102, path, "Movie Two", "movie-two.jpg", TimeSpan.FromMinutes(20)); var manualA = MakeCollection(201, "Manual A"); var manualB = MakeCollection(202, "Manual B"); var manualC = MakeCollection(203, "Manual C"); context.LocalLibraries.Add(library); context.Movies.AddRange(movie1, movie2); context.Collections.AddRange(manualA, manualB, manualC); context.SmartCollections.AddRange( new SmartCollection { Id = 301, Name = "Smart A", Query = "tag:a", MultiCollections = [], MultiCollectionSmartItems = [] }, new SmartCollection { Id = 302, Name = "Smart B", Query = "tag:b", MultiCollections = [], MultiCollectionSmartItems = [] }); await context.SaveChangesAsync(); } private async Task SeedManualCollectionMetadataGraph() { await using TvContext context = _db.CreateContext(); (LocalLibrary library, LibraryPath path) = MakeLibrary(200, "Manual Metadata Library"); var collection = MakeCollection(210, "Manual Metadata"); var first = MakeMovie(211, path, "First", "first-poster.jpg", TimeSpan.FromMinutes(30)); var second = MakeMovie(212, path, "Second", "second-poster.jpg", TimeSpan.FromMinutes(45)); collection.MediaItems.AddRange([first, second]); context.LocalLibraries.Add(library); context.Collections.Add(collection); context.Movies.AddRange(first, second); await context.SaveChangesAsync(); } private async Task SeedSeasonGraph() { await using TvContext context = _db.CreateContext(); (LocalLibrary library, LibraryPath path) = MakeLibrary(300, "Season Library"); var show = new Show { Id = 300, LibraryPath = path, Collections = [], CollectionItems = [], TraktListItems = [], Seasons = [], ShowMetadata = [MakeShowMetadata("Season Show", "show.jpg")] }; var season = MakeSeason(301, path, show, 2, "season-2.jpg"); var specials = MakeSeason(302, path, show, 0, string.Empty); season.Episodes.AddRange([ MakeEpisode(311, path, season), MakeEpisode(312, path, season) ]); specials.Episodes.Add(MakeEpisode(313, path, specials)); show.Seasons.AddRange([season, specials]); path.MediaItems.AddRange([show, season, specials, .. season.Episodes, .. specials.Episodes]); context.LocalLibraries.Add(library); context.Shows.Add(show); context.Seasons.AddRange(season, specials); await context.SaveChangesAsync(); } private async Task SeedEpisodeDrillInGraph() { await using TvContext context = _db.CreateContext(); (LocalLibrary library, LibraryPath path) = MakeLibrary(700, "Episode Library"); var show = new Show { Id = 700, LibraryPath = path, Collections = [], CollectionItems = [], TraktListItems = [], Seasons = [], ShowMetadata = [MakeShowMetadata("Drill Show", "show.jpg")] }; var season = MakeSeason(701, path, show, 1, "season.jpg"); // Inserted out of episode-number order to prove the handler sorts by number. Episode e2 = MakeNumberedEpisode(711, path, season, 2); Episode e1 = MakeNumberedEpisode(712, path, season, 1); Episode e3 = MakeNumberedEpisode(713, path, season, 3); season.Episodes.AddRange([e2, e1, e3]); show.Seasons.Add(season); path.MediaItems.AddRange([show, season, e2, e1, e3]); context.LocalLibraries.Add(library); context.Shows.Add(show); context.Seasons.Add(season); context.Episodes.AddRange(e2, e1, e3); await context.SaveChangesAsync(); } private async Task SeedMusicVideoDrillInGraph() { await using TvContext context = _db.CreateContext(); (LocalLibrary library, LibraryPath path) = MakeLibrary(800, "Music Video Library"); var artist = new Artist { Id = 801, LibraryPath = path, Collections = [], CollectionItems = [], TraktListItems = [], MusicVideos = [], ArtistMetadata = [MakeArtistMetadata("Drill Artist", "artist.jpg")] }; MusicVideo mvB = MakeTitledMusicVideo(811, path, artist, "Track On B", "Album B", 1); MusicVideo mvA = MakeTitledMusicVideo(812, path, artist, "Track On A", "Album A", 1); artist.MusicVideos.AddRange([mvB, mvA]); path.MediaItems.AddRange([artist, mvB, mvA]); context.LocalLibraries.Add(library); context.Artists.Add(artist); context.MusicVideos.AddRange(mvB, mvA); await context.SaveChangesAsync(); } private static Episode MakeNumberedEpisode(int id, LibraryPath path, Season season, int episodeNumber) { Episode episode = MakeEpisode(id, path, season); episode.EpisodeMetadata = [ new EpisodeMetadata { Title = $"Episode {episodeNumber}", SortTitle = $"Episode {episodeNumber}", EpisodeNumber = episodeNumber, Artwork = [], Genres = [], Tags = [], Studios = [], Actors = [], Guids = [], Subtitles = [], Directors = [], Writers = [] } ]; return episode; } private static MusicVideo MakeTitledMusicVideo( int id, LibraryPath path, Artist artist, string title, string album, int track) { MusicVideo musicVideo = MakeMusicVideo(id, path, artist); musicVideo.MusicVideoMetadata = [ new MusicVideoMetadata { Title = title, SortTitle = title, Album = album, Track = track, Artwork = [], Genres = [], Tags = [], Studios = [], Actors = [], Guids = [], Subtitles = [] } ]; return musicVideo; } private async Task SeedArtistGraph() { await using TvContext context = _db.CreateContext(); (LocalLibrary library, LibraryPath path) = MakeLibrary(400, "Artist Library"); var artist = new Artist { Id = 401, LibraryPath = path, Collections = [], CollectionItems = [], TraktListItems = [], MusicVideos = [], ArtistMetadata = [MakeArtistMetadata("Counted Artist", "artist-thumb.jpg")] }; var musicVideo1 = MakeMusicVideo(411, path, artist); var musicVideo2 = MakeMusicVideo(412, path, artist); artist.MusicVideos.AddRange([musicVideo1, musicVideo2]); path.MediaItems.AddRange([artist, musicVideo1, musicVideo2]); context.LocalLibraries.Add(library); context.Artists.Add(artist); context.MusicVideos.AddRange(musicVideo1, musicVideo2); await context.SaveChangesAsync(); } private async Task SeedJellyfinMovieGraph() { await using TvContext context = _db.CreateContext(); (LocalLibrary library, LibraryPath path) = MakeLibrary(600, "Jellyfin Library"); var movie = new Movie { Id = 601, LibraryPath = path, Collections = [], CollectionItems = [], TraktListItems = [], MediaVersions = [new MediaVersion { Duration = TimeSpan.FromMinutes(100) }], MovieMetadata = [ new MovieMetadata { Title = "Jellyfin Movie", SortTitle = "Jellyfin Movie", Artwork = [ new Artwork { Path = "jellyfin://Items/abc/Images/Primary?tag=xyz", ArtworkKind = ArtworkKind.Poster } ], Genres = [], Tags = [], Studios = [], Actors = [], Guids = [], Subtitles = [], Directors = [], Writers = [] } ] }; path.MediaItems.Add(movie); context.LocalLibraries.Add(library); context.Movies.Add(movie); await context.SaveChangesAsync(); } private async Task SeedLibraryFilteredCollectionsGraph() { await using TvContext context = _db.CreateContext(); (LocalLibrary includedLibrary, LibraryPath includedPath) = MakeLibrary(501, "Included Library"); (LocalLibrary excludedLibrary, LibraryPath excludedPath) = MakeLibrary(502, "Excluded Library"); var includedCollection = MakeCollection(510, "Included Manual"); var excludedCollection = MakeCollection(511, "Excluded Manual"); var includedMovie = MakeMovie(520, includedPath, "Included Movie", "included.jpg", TimeSpan.FromMinutes(5)); var excludedMovie = MakeMovie(521, excludedPath, "Excluded Movie", "excluded.jpg", TimeSpan.FromMinutes(5)); includedCollection.MediaItems.Add(includedMovie); excludedCollection.MediaItems.Add(excludedMovie); context.LocalLibraries.AddRange(includedLibrary, excludedLibrary); context.Movies.AddRange(includedMovie, excludedMovie); context.Collections.AddRange(includedCollection, excludedCollection); context.SmartCollections.Add(new SmartCollection { Id = 530, Name = "Smart", Query = "tag", MultiCollections = [], MultiCollectionSmartItems = [] }); context.MultiCollections.Add(new MultiCollection { Id = 540, Name = "Multi", Collections = [], SmartCollections = [], MultiCollectionItems = [], MultiCollectionSmartItems = [] }); context.RerunCollections.Add(new RerunCollection { Id = 550, Name = "Rerun", CollectionType = CollectionType.Collection }); context.Playlists.Add(new Playlist { Id = 560, Name = "Playlist", Items = [] }); await context.SaveChangesAsync(); } private static (LocalLibrary Library, LibraryPath Path) MakeLibrary(int id, string name) { var library = new LocalLibrary { Id = id, Name = name, MediaKind = LibraryMediaKind.Movies, Paths = [] }; var path = new LibraryPath { Id = id, Path = $"/media/{id}", Library = library, LibraryFolders = [], MediaItems = [] }; library.Paths.Add(path); return (library, path); } private static Collection MakeCollection(int id, string name) => new() { Id = id, Name = name, MediaItems = [], CollectionItems = [], MultiCollections = [], MultiCollectionItems = [] }; private static Movie MakeMovie(int id, LibraryPath path, string title, string poster, TimeSpan duration) => new() { Id = id, LibraryPath = path, Collections = [], CollectionItems = [], TraktListItems = [], MovieMetadata = [MakeMovieMetadata(title, poster)], MediaVersions = [new MediaVersion { Duration = duration }] }; private static Season MakeSeason(int id, LibraryPath path, Show show, int seasonNumber, string poster) => new() { Id = id, LibraryPath = path, Show = show, SeasonNumber = seasonNumber, Collections = [], CollectionItems = [], TraktListItems = [], Episodes = [], SeasonMetadata = [MakeSeasonMetadata(poster)] }; private static Episode MakeEpisode(int id, LibraryPath path, Season season) => new() { Id = id, LibraryPath = path, Season = season, Collections = [], CollectionItems = [], TraktListItems = [], EpisodeMetadata = [], MediaVersions = [] }; private static MusicVideo MakeMusicVideo(int id, LibraryPath path, Artist artist) => new() { Id = id, LibraryPath = path, Artist = artist, Collections = [], CollectionItems = [], TraktListItems = [], MusicVideoMetadata = [], MediaVersions = [] }; private static MovieMetadata MakeMovieMetadata(string title, string poster) => new() { Title = title, SortTitle = title, Artwork = [new Artwork { Path = poster, ArtworkKind = ArtworkKind.Poster }], Genres = [], Tags = [], Studios = [], Actors = [], Guids = [], Subtitles = [], Directors = [], Writers = [] }; private static ShowMetadata MakeShowMetadata(string title, string poster) => new() { Title = title, SortTitle = title, Artwork = [new Artwork { Path = poster, ArtworkKind = ArtworkKind.Poster }], Genres = [], Tags = [], Studios = [], Actors = [], Guids = [], Subtitles = [] }; private static SeasonMetadata MakeSeasonMetadata(string poster = "") => new() { Title = string.Empty, SortTitle = string.Empty, Artwork = string.IsNullOrEmpty(poster) ? [] : [new Artwork { Path = poster, ArtworkKind = ArtworkKind.Poster }], Genres = [], Tags = [], Studios = [], Actors = [], Guids = [], Subtitles = [] }; private static ArtistMetadata MakeArtistMetadata(string title, string thumbnail) => new() { Title = title, SortTitle = title, Artwork = [new Artwork { Path = thumbnail, ArtworkKind = ArtworkKind.Thumbnail }], Genres = [], Tags = [], Studios = [], Actors = [], Guids = [], Subtitles = [], Styles = [], Moods = [] }; }