diff options
3 files changed, 290 insertions, 41 deletions
diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs index 4be9b04baa..d2f8e5060c 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs @@ -35,6 +35,18 @@ public sealed partial class BaseItemRepository // instance across several lambdas, and this filter is combined into a tree more than once. private static Expression<Func<BaseItemEntity, bool>> IsFolderFilter => e => e.IsFolder; + // "und" is the language filters' stand-in for a track that declares no language at all. + private static bool IsUndetermined(string language) + => string.Equals(language, "und", StringComparison.OrdinalIgnoreCase); + + // The primary versions whose alternate version satisfies a dimension bound. Anchored on + // PrimaryVersionId so the filtered index carries it rather than a scan of every item. + private static IQueryable<Guid> VersionsMatchingDimension(JellyfinDbContext context, Expression<Func<BaseItemEntity, bool>> bound) + => context.BaseItems + .Where(v => v.PrimaryVersionId != null) + .Where(bound) + .Select(v => v.PrimaryVersionId!.Value); + /// <inheritdoc /> public IQueryable<BaseItemEntity> TranslateQuery( IQueryable<BaseItemEntity> baseQuery, @@ -70,15 +82,28 @@ public sealed partial class BaseItemRepository include4K = true; } + // A 4K remux of an SD primary is a version of the same item, so the resolution a caller + // filters on is the best any of the item's versions offers, not just the primary file's. + // The filtered PrimaryVersionId index keeps this to the few items that have versions. + var versionsAtResolution = context.BaseItems + .Where(v => v.PrimaryVersionId != null + && v.Width > 0 + && ((includeSD && v.Width < HDWidth) + || (includeHD && v.Width >= HDWidth && !(v.Width >= UHDWidth || v.Height >= UHDHeight)) + || (include4K && (v.Width >= UHDWidth || v.Height >= UHDHeight)))) + .Select(v => v.PrimaryVersionId!.Value); + // Non-folders: check own resolution directly (no subquery). // Folders (Series, BoxSets): EXISTS check on descendants/linked children. // Using navigation properties (a.Item, lc.Child) produces efficient // EXISTS + JOIN instead of nested IN (SELECT ...) subqueries. baseQuery = baseQuery.Where(e => - (!e.IsFolder && e.Width > 0 - && ((includeSD && e.Width < HDWidth) - || (includeHD && e.Width >= HDWidth && !(e.Width >= UHDWidth || e.Height >= UHDHeight)) - || (include4K && (e.Width >= UHDWidth || e.Height >= UHDHeight)))) + (!e.IsFolder + && ((e.Width > 0 + && ((includeSD && e.Width < HDWidth) + || (includeHD && e.Width >= HDWidth && !(e.Width >= UHDWidth || e.Height >= UHDHeight)) + || (include4K && (e.Width >= UHDWidth || e.Height >= UHDHeight)))) + || versionsAtResolution.Contains(e.Id))) || (e.IsFolder && (e.Children!.Any(a => a.Item.Width > 0 @@ -93,24 +118,31 @@ public sealed partial class BaseItemRepository || (include4K && (lc.Child.Width >= UHDWidth || lc.Child.Height >= UHDHeight))))))); } + // Same reasoning as the resolution filter: a dimension bound is met if any version meets it. if (minWidth.HasValue) { - baseQuery = baseQuery.Where(e => e.Width >= minWidth); + var versionsWideEnough = VersionsMatchingDimension(context, v => v.Width >= minWidth); + baseQuery = baseQuery.Where(e => e.Width >= minWidth || versionsWideEnough.Contains(e.Id)); } if (filter.MinHeight.HasValue) { - baseQuery = baseQuery.Where(e => e.Height >= filter.MinHeight); + var minHeight = filter.MinHeight; + var versionsTallEnough = VersionsMatchingDimension(context, v => v.Height >= minHeight); + baseQuery = baseQuery.Where(e => e.Height >= minHeight || versionsTallEnough.Contains(e.Id)); } if (maxWidth.HasValue) { - baseQuery = baseQuery.Where(e => e.Width <= maxWidth); + var versionsNarrowEnough = VersionsMatchingDimension(context, v => v.Width <= maxWidth); + baseQuery = baseQuery.Where(e => e.Width <= maxWidth || versionsNarrowEnough.Contains(e.Id)); } if (filter.MaxHeight.HasValue) { - baseQuery = baseQuery.Where(e => e.Height <= filter.MaxHeight); + var maxHeight = filter.MaxHeight; + var versionsShortEnough = VersionsMatchingDimension(context, v => v.Height <= maxHeight); + baseQuery = baseQuery.Where(e => e.Height <= maxHeight || versionsShortEnough.Contains(e.Id)); } if (filter.IsLocked.HasValue) @@ -762,103 +794,143 @@ public sealed partial class BaseItemRepository if (!string.IsNullOrWhiteSpace(filter.HasNoAudioTrackWithLanguage)) { var lang = filter.HasNoAudioTrackWithLanguage; - var foldersWithAudio = DescendantQueryHelper.GetFolderIdsMatching(context, new HasMediaStreamType(MediaStreamTypeEntity.Audio, lang)); + var undetermined = IsUndetermined(lang); + var criteria = new HasMediaStreamType(MediaStreamTypeEntity.Audio, lang); + // A track only an alternate version carries still belongs to the item a caller sees, so the + // item's own streams alone do not decide this. Same for every stream filter below. + var versionsWithAudio = DescendantQueryHelper.GetPrimaryVersionIdsMatching(context, criteria); + var foldersWithAudio = DescendantQueryHelper.GetFolderIdsMatching(context, criteria); baseQuery = baseQuery .Where(e => - (!e.IsFolder && !e.MediaStreams!.Any(ms => ms.StreamType == MediaStreamTypeEntity.Audio && ms.Language == lang)) + (!e.IsFolder + && !e.MediaStreams!.Any(ms => ms.StreamType == MediaStreamTypeEntity.Audio + && (ms.Language == lang || (undetermined && string.IsNullOrEmpty(ms.Language)))) + && !versionsWithAudio.Contains(e.Id)) || (e.IsFolder && !foldersWithAudio.Contains(e.Id))); } if (!string.IsNullOrWhiteSpace(filter.HasNoInternalSubtitleTrackWithLanguage)) { var lang = filter.HasNoInternalSubtitleTrackWithLanguage; - var foldersWithSubtitles = DescendantQueryHelper.GetFolderIdsMatching(context, new HasMediaStreamType(MediaStreamTypeEntity.Subtitle, lang, IsExternal: false)); + var undetermined = IsUndetermined(lang); + var criteria = new HasMediaStreamType(MediaStreamTypeEntity.Subtitle, lang, IsExternal: false); + var versionsWithSubtitles = DescendantQueryHelper.GetPrimaryVersionIdsMatching(context, criteria); + var foldersWithSubtitles = DescendantQueryHelper.GetFolderIdsMatching(context, criteria); baseQuery = baseQuery .Where(e => - (!e.IsFolder && !e.MediaStreams!.Any(ms => ms.StreamType == MediaStreamTypeEntity.Subtitle && !ms.IsExternal && ms.Language == lang)) + (!e.IsFolder + && !e.MediaStreams!.Any(ms => ms.StreamType == MediaStreamTypeEntity.Subtitle && !ms.IsExternal + && (ms.Language == lang || (undetermined && string.IsNullOrEmpty(ms.Language)))) + && !versionsWithSubtitles.Contains(e.Id)) || (e.IsFolder && !foldersWithSubtitles.Contains(e.Id))); } if (!string.IsNullOrWhiteSpace(filter.HasNoExternalSubtitleTrackWithLanguage)) { var lang = filter.HasNoExternalSubtitleTrackWithLanguage; - var foldersWithSubtitles = DescendantQueryHelper.GetFolderIdsMatching(context, new HasMediaStreamType(MediaStreamTypeEntity.Subtitle, lang, IsExternal: true)); + var undetermined = IsUndetermined(lang); + var criteria = new HasMediaStreamType(MediaStreamTypeEntity.Subtitle, lang, IsExternal: true); + var versionsWithSubtitles = DescendantQueryHelper.GetPrimaryVersionIdsMatching(context, criteria); + var foldersWithSubtitles = DescendantQueryHelper.GetFolderIdsMatching(context, criteria); baseQuery = baseQuery .Where(e => - (!e.IsFolder && !e.MediaStreams!.Any(ms => ms.StreamType == MediaStreamTypeEntity.Subtitle && ms.IsExternal && ms.Language == lang)) + (!e.IsFolder + && !e.MediaStreams!.Any(ms => ms.StreamType == MediaStreamTypeEntity.Subtitle && ms.IsExternal + && (ms.Language == lang || (undetermined && string.IsNullOrEmpty(ms.Language)))) + && !versionsWithSubtitles.Contains(e.Id)) || (e.IsFolder && !foldersWithSubtitles.Contains(e.Id))); } if (!string.IsNullOrWhiteSpace(filter.HasNoSubtitleTrackWithLanguage)) { var lang = filter.HasNoSubtitleTrackWithLanguage; - var foldersWithSubtitles = DescendantQueryHelper.GetFolderIdsMatching(context, new HasMediaStreamType(MediaStreamTypeEntity.Subtitle, lang)); + var undetermined = IsUndetermined(lang); + var criteria = new HasMediaStreamType(MediaStreamTypeEntity.Subtitle, lang); + var versionsWithSubtitles = DescendantQueryHelper.GetPrimaryVersionIdsMatching(context, criteria); + var foldersWithSubtitles = DescendantQueryHelper.GetFolderIdsMatching(context, criteria); baseQuery = baseQuery .Where(e => - (!e.IsFolder && !e.MediaStreams!.Any(ms => ms.StreamType == MediaStreamTypeEntity.Subtitle && ms.Language == lang)) + (!e.IsFolder + && !e.MediaStreams!.Any(ms => ms.StreamType == MediaStreamTypeEntity.Subtitle + && (ms.Language == lang || (undetermined && string.IsNullOrEmpty(ms.Language)))) + && !versionsWithSubtitles.Contains(e.Id)) || (e.IsFolder && !foldersWithSubtitles.Contains(e.Id))); } if (filter.HasSubtitles.HasValue) { var hasSubtitles = filter.HasSubtitles.Value; - var foldersWithSubtitles = DescendantQueryHelper.GetFolderIdsMatching(context, new HasSubtitles()); + var criteria = new HasSubtitles(); + var versionsWithSubtitles = DescendantQueryHelper.GetPrimaryVersionIdsMatching(context, criteria); + var foldersWithSubtitles = DescendantQueryHelper.GetFolderIdsMatching(context, criteria); if (hasSubtitles) { baseQuery = baseQuery .Where(e => - (!e.IsFolder && e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle)) + (!e.IsFolder && (e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle) + || versionsWithSubtitles.Contains(e.Id))) || (e.IsFolder && foldersWithSubtitles.Contains(e.Id))); } else { baseQuery = baseQuery .Where(e => - (!e.IsFolder && !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle)) + (!e.IsFolder && !e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle) + && !versionsWithSubtitles.Contains(e.Id)) || (e.IsFolder && !foldersWithSubtitles.Contains(e.Id))); } } if (filter.SubtitleLanguages.Count > 0) { - var foldersWithSubtitles = DescendantQueryHelper.GetFolderIdsMatching(context, new HasMediaStreamType(MediaStreamTypeEntity.Subtitle, filter.SubtitleLanguages)); + var criteria = new HasMediaStreamType(MediaStreamTypeEntity.Subtitle, filter.SubtitleLanguages); + var versionsWithSubtitles = DescendantQueryHelper.GetPrimaryVersionIdsMatching(context, criteria); + var foldersWithSubtitles = DescendantQueryHelper.GetFolderIdsMatching(context, criteria); baseQuery = baseQuery .Where(e => - (!e.IsFolder && e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle - && (filter.SubtitleLanguages.Contains(f.Language) || (filter.SubtitleLanguages.Contains("und") && string.IsNullOrEmpty(f.Language))))) + (!e.IsFolder && (e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Subtitle + && (filter.SubtitleLanguages.Contains(f.Language) || (filter.SubtitleLanguages.Contains("und") && string.IsNullOrEmpty(f.Language)))) + || versionsWithSubtitles.Contains(e.Id))) || (e.IsFolder && foldersWithSubtitles.Contains(e.Id))); } if (filter.AudioLanguages.Count > 0) { - var foldersWithAudio = DescendantQueryHelper.GetFolderIdsMatching(context, new HasMediaStreamType(MediaStreamTypeEntity.Audio, filter.AudioLanguages)); + var criteria = new HasMediaStreamType(MediaStreamTypeEntity.Audio, filter.AudioLanguages); + var versionsWithAudio = DescendantQueryHelper.GetPrimaryVersionIdsMatching(context, criteria); + var foldersWithAudio = DescendantQueryHelper.GetFolderIdsMatching(context, criteria); baseQuery = baseQuery .Where(e => - (!e.IsFolder && e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Audio - && (filter.AudioLanguages.Contains(f.Language) || (filter.AudioLanguages.Contains("und") && string.IsNullOrEmpty(f.Language))))) + (!e.IsFolder && (e.MediaStreams!.Any(f => f.StreamType == MediaStreamTypeEntity.Audio + && (filter.AudioLanguages.Contains(f.Language) || (filter.AudioLanguages.Contains("und") && string.IsNullOrEmpty(f.Language)))) + || versionsWithAudio.Contains(e.Id))) || (e.IsFolder && foldersWithAudio.Contains(e.Id))); } if (filter.HasChapterImages.HasValue) { var hasChapterImages = filter.HasChapterImages.Value; - var foldersWithChapterImages = DescendantQueryHelper.GetFolderIdsMatching(context, new HasChapterImages()); + var criteria = new HasChapterImages(); + var versionsWithChapterImages = DescendantQueryHelper.GetPrimaryVersionIdsMatching(context, criteria); + var foldersWithChapterImages = DescendantQueryHelper.GetFolderIdsMatching(context, criteria); if (hasChapterImages) { baseQuery = baseQuery .Where(e => - (!e.IsFolder && e.Chapters!.Any(f => f.ImagePath != null)) + (!e.IsFolder && (e.Chapters!.Any(f => f.ImagePath != null) + || versionsWithChapterImages.Contains(e.Id))) || (e.IsFolder && foldersWithChapterImages.Contains(e.Id))); } else { baseQuery = baseQuery .Where(e => - (!e.IsFolder && !e.Chapters!.Any(f => f.ImagePath != null)) + (!e.IsFolder && !e.Chapters!.Any(f => f.ImagePath != null) + && !versionsWithChapterImages.Contains(e.Id)) || (e.IsFolder && !foldersWithChapterImages.Contains(e.Id))); } } diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/DescendantQueryHelper.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/DescendantQueryHelper.cs index 1e1c8780e8..5a17a46d9e 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/DescendantQueryHelper.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/DescendantQueryHelper.cs @@ -102,17 +102,7 @@ public static class DescendantQueryHelper ArgumentNullException.ThrowIfNull(context); ArgumentNullException.ThrowIfNull(criteria); - var matchingItemIds = criteria switch - { - HasSubtitles => context.MediaStreamInfos - .Where(ms => ms.StreamType == MediaStreamTypeEntity.Subtitle) - .Select(ms => ms.ItemId), - HasChapterImages => context.Chapters - .Where(c => c.ImagePath != null) - .Select(c => c.ItemId), - HasMediaStreamType m => GetMatchingMediaStreamItemIds(context, m), - _ => throw new ArgumentOutOfRangeException(nameof(criteria), $"Unknown criteria type: {criteria.GetType().Name}") - }; + var matchingItemIds = GetItemIdsMatching(context, criteria); // One hop up the closure covers every ancestor level. var hierarchyAncestors = context.AncestorIds @@ -144,7 +134,63 @@ public static class DescendantQueryHelper .Distinct(); } - private static IQueryable<Guid> GetMatchingMediaStreamItemIds(JellyfinDbContext context, HasMediaStreamType criteria) + /// <summary> + /// Gets a queryable of the IDs of the items whose media matches the criteria. + /// </summary> + /// <param name="context">Database context.</param> + /// <param name="criteria">The matching criteria to apply.</param> + /// <returns>Queryable of item IDs.</returns> + /// <remarks> + /// An alternate version is a second file for its primary version and is never listed on its own, so a + /// track only that file carries is reported against the primary: the item a caller can actually see. + /// </remarks> + public static IQueryable<Guid> GetItemIdsMatching(JellyfinDbContext context, FolderMatchCriteria criteria) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(criteria); + + return MatchingMediaOwners(context, criteria) + .Select(e => e.PrimaryVersionId ?? e.Id); + } + + /// <summary> + /// Gets a queryable of the IDs of the primary versions whose alternate version's media matches the + /// criteria. + /// </summary> + /// <param name="context">Database context.</param> + /// <param name="criteria">The matching criteria to apply.</param> + /// <returns>Queryable of primary version item IDs.</returns> + /// <remarks> + /// For callers that already test an item's own media with their own indexed predicate: this covers + /// exactly what such a predicate misses, and the filtered PrimaryVersionId index keeps it to the few + /// items that have versions at all. + /// </remarks> + public static IQueryable<Guid> GetPrimaryVersionIdsMatching(JellyfinDbContext context, FolderMatchCriteria criteria) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(criteria); + + return MatchingMediaOwners(context, criteria) + .Where(e => e.PrimaryVersionId.HasValue) + .Select(e => e.PrimaryVersionId!.Value); + } + + // The items whose own media matches, as their BaseItems rows so the version group can be read off + // them. One definition of "matches" per criteria, so the projections above cannot drift apart. + private static IQueryable<BaseItemEntity> MatchingMediaOwners(JellyfinDbContext context, FolderMatchCriteria criteria) + => criteria switch + { + HasSubtitles => context.MediaStreamInfos + .Where(ms => ms.StreamType == MediaStreamTypeEntity.Subtitle) + .Select(ms => ms.Item), + HasChapterImages => context.Chapters + .Where(c => c.ImagePath != null) + .Select(c => c.Item), + HasMediaStreamType m => GetMatchingMediaStreams(context, m).Select(ms => ms.Item), + _ => throw new ArgumentOutOfRangeException(nameof(criteria), $"Unknown criteria type: {criteria.GetType().Name}") + }; + + private static IQueryable<MediaStreamInfo> GetMatchingMediaStreams(JellyfinDbContext context, HasMediaStreamType criteria) { var query = context.MediaStreamInfos .Where(ms => ms.StreamType == criteria.StreamType @@ -157,7 +203,7 @@ public static class DescendantQueryHelper query = query.Where(ms => ms.IsExternal == isExternal); } - return query.Select(ms => ms.ItemId); + return query; } private static IQueryable<Guid> ClosureDescendants(JellyfinDbContext context, IReadOnlyList<Guid> roots) diff --git a/tests/Jellyfin.Server.Implementations.Tests/Item/BaseItemRepositoryStreamFilterTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Item/BaseItemRepositoryStreamFilterTests.cs index cc6b097664..12a7fc1aef 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Item/BaseItemRepositoryStreamFilterTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Item/BaseItemRepositoryStreamFilterTests.cs @@ -29,6 +29,12 @@ public sealed class BaseItemRepositoryStreamFilterTests : SqliteDbTestFixture private readonly Guid _linkedSeries = Guid.NewGuid(); private readonly Guid _linkedEpisode = Guid.NewGuid(); + // A version group in a library of its own, so it cannot move the assertions above: an SD primary + // that carries nothing, and a 4K second file carrying the subtitles, chapter image and audio. + private readonly Guid _versionLibrary = Guid.NewGuid(); + private readonly Guid _versionedMovie = Guid.NewGuid(); + private readonly Guid _alternateVersion = Guid.NewGuid(); + public BaseItemRepositoryStreamFilterTests() { using (var ctx = CreateDbContext()) @@ -105,6 +111,74 @@ public sealed class BaseItemRepositoryStreamFilterTests : SqliteDbTestFixture Assert.DoesNotContain(_withoutSubtitles, ids); } + [Fact] + public void HasSubtitles_MatchesAnItemWhoseAlternateVersionCarriesThem() + { + var ids = _repository.GetItemIdsList(new InternalItemsQuery { HasSubtitles = true }); + + Assert.Contains(_versionedMovie, ids); + Assert.Contains(_versionLibrary, ids); + // The second file is never listed on its own, which is why its tracks have to count for the primary. + Assert.DoesNotContain(_alternateVersion, ids); + } + + [Fact] + public void HasSubtitles_Negated_ExcludesAnItemWhoseAlternateVersionCarriesThem() + { + var ids = _repository.GetItemIdsList(new InternalItemsQuery { HasSubtitles = false }); + + Assert.DoesNotContain(_versionedMovie, ids); + Assert.DoesNotContain(_versionLibrary, ids); + } + + [Fact] + public void SubtitleLanguages_MatchesTheLanguageOnAnAlternateVersion() + { + Assert.Contains(_versionedMovie, _repository.GetItemIdsList(new InternalItemsQuery { SubtitleLanguages = ["ger"] })); + Assert.DoesNotContain(_versionedMovie, _repository.GetItemIdsList(new InternalItemsQuery { SubtitleLanguages = ["fre"] })); + } + + [Fact] + public void HasNoSubtitleTrackWithLanguage_ExcludesAnItemWhoseAlternateVersionHasIt() + { + var ids = _repository.GetItemIdsList(new InternalItemsQuery { HasNoSubtitleTrackWithLanguage = "ger" }); + + Assert.DoesNotContain(_versionedMovie, ids); + Assert.DoesNotContain(_versionLibrary, ids); + } + + [Fact] + public void AudioLanguages_MatchesTheLanguageOnAnAlternateVersion() + { + Assert.Contains(_versionedMovie, _repository.GetItemIdsList(new InternalItemsQuery { AudioLanguages = ["fre"] })); + } + + [Fact] + public void HasNoAudioTrackWithLanguage_ExcludesAnItemWhoseAlternateVersionHasIt() + { + Assert.DoesNotContain(_versionedMovie, _repository.GetItemIdsList(new InternalItemsQuery { HasNoAudioTrackWithLanguage = "fre" })); + } + + [Fact] + public void HasChapterImages_MatchesAnItemWhoseAlternateVersionCarriesThem() + { + Assert.Contains(_versionedMovie, _repository.GetItemIdsList(new InternalItemsQuery { HasChapterImages = true })); + } + + [Fact] + public void Is4K_MatchesAnItemWhoseAlternateVersionIs4K() + { + // The primary file is SD; the resolution a caller can actually play is the 4K second file's. + Assert.Contains(_versionedMovie, _repository.GetItemIdsList(new InternalItemsQuery { Is4K = true })); + } + + [Fact] + public void MinWidth_MatchesAnItemWhoseAlternateVersionIsWideEnough() + { + Assert.Contains(_versionedMovie, _repository.GetItemIdsList(new InternalItemsQuery { MinWidth = 3000 })); + Assert.DoesNotContain(_withSubtitles, _repository.GetItemIdsList(new InternalItemsQuery { MinWidth = 3000 })); + } + private void Seed(JellyfinDbContext context) { context.BaseItems.Add(new BaseItemEntity { Id = _library, Type = FolderType, Name = "Library", IsFolder = true }); @@ -178,6 +252,63 @@ public sealed class BaseItemRepositoryStreamFilterTests : SqliteDbTestFixture Item = null! }); + SeedVersionGroup(context); + context.SaveChanges(); } + + // An SD primary whose only extras live on a 4K second file, so every filter has to reach through + // PrimaryVersionId to answer correctly. + private void SeedVersionGroup(JellyfinDbContext context) + { + context.BaseItems.Add(new BaseItemEntity { Id = _versionLibrary, Type = FolderType, Name = "Version library", IsFolder = true }); + context.BaseItems.Add(new BaseItemEntity { Id = _versionedMovie, Type = MovieType, Name = "Versioned movie", Width = 720, Height = 480 }); + context.BaseItems.Add(new BaseItemEntity + { + Id = _alternateVersion, + Type = MovieType, + Name = "Versioned movie 4K", + PrimaryVersionId = _versionedMovie, + Width = 3840, + Height = 2160 + }); + + foreach (var itemId in new[] { _versionedMovie, _alternateVersion }) + { + context.AncestorIds.Add(new AncestorId + { + ItemId = itemId, + ParentItemId = _versionLibrary, + Item = null!, + ParentItem = null! + }); + } + + context.MediaStreamInfos.Add(new MediaStreamInfo + { + ItemId = _alternateVersion, + StreamIndex = 0, + StreamType = MediaStreamTypeEntity.Subtitle, + Language = "ger", + Item = null! + }); + + context.MediaStreamInfos.Add(new MediaStreamInfo + { + ItemId = _alternateVersion, + StreamIndex = 1, + StreamType = MediaStreamTypeEntity.Audio, + Language = "fre", + Item = null! + }); + + context.Chapters.Add(new Chapter + { + ItemId = _alternateVersion, + ChapterIndex = 0, + StartPositionTicks = 0, + ImagePath = "/alternate-chapter.jpg", + Item = null! + }); + } } |
