Multiple fixes and improvements
Co-Authored-By: Cody Robibero <cody@robibe.ro>
This commit is contained in:
@@ -36,8 +36,8 @@ public sealed partial class BaseItemRepository
|
||||
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);
|
||||
private static string NormalizeLanguage(string language)
|
||||
=> string.Equals(language, "und", StringComparison.OrdinalIgnoreCase) ? "und" : language;
|
||||
|
||||
// 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.
|
||||
@@ -82,40 +82,57 @@ 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);
|
||||
// A 4K remux of an SD primary is a version of the same item, so the bucket a caller filters
|
||||
// on is the best any of the item's versions offers, not just the primary file's. Three sets,
|
||||
// because a bucket is as much about what the version group does not have as what it does, and
|
||||
// because an unprobed primary can still be placed by a version that does carry dimensions.
|
||||
// The filtered PrimaryVersionId index keeps all three to the few items that have versions.
|
||||
var versionsSd = VersionsMatchingDimension(context, v => v.Width > 0 && v.Width < HDWidth);
|
||||
var versionsHd = VersionsMatchingDimension(context, v => v.Width >= HDWidth);
|
||||
var versions4K = VersionsMatchingDimension(context, v => v.Width >= UHDWidth || v.Height >= UHDHeight);
|
||||
|
||||
// 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.
|
||||
// Only the SD test needs the Width > 0 guard against a row with no dimensions: such a row
|
||||
// cannot reach the HD or 4K bound anyway, and EF lowers the HD bucket's negated "not itself
|
||||
// 4K" guard to CASE WHEN ... THEN 0 ELSE 1, which already reads unknown as not 4K rather
|
||||
// than propagating a null. Folders (Series, BoxSets) answer on their descendants, bucketed
|
||||
// exactly as a top-level item is so that the two cannot disagree; the navigation properties
|
||||
// (a.Item, lc.Child) give EXISTS + JOIN rather than nested IN (SELECT ...).
|
||||
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))))
|
||||
|| versionsAtResolution.Contains(e.Id)))
|
||||
&& ((includeSD
|
||||
&& ((e.Width > 0 && e.Width < HDWidth) || versionsSd.Contains(e.Id))
|
||||
&& !versionsHd.Contains(e.Id)
|
||||
&& !versions4K.Contains(e.Id))
|
||||
|| (includeHD
|
||||
&& (e.Width >= HDWidth || versionsHd.Contains(e.Id))
|
||||
&& !(e.Width >= UHDWidth || e.Height >= UHDHeight)
|
||||
&& !versions4K.Contains(e.Id))
|
||||
|| (include4K
|
||||
&& (e.Width >= UHDWidth || e.Height >= UHDHeight || versions4K.Contains(e.Id)))))
|
||||
|| (e.IsFolder
|
||||
&& (e.Children!.Any(a =>
|
||||
a.Item.Width > 0
|
||||
&& ((includeSD && a.Item.Width < HDWidth)
|
||||
|| (includeHD && a.Item.Width >= HDWidth && !(a.Item.Width >= UHDWidth || a.Item.Height >= UHDHeight))
|
||||
|| (include4K && (a.Item.Width >= UHDWidth || a.Item.Height >= UHDHeight))))
|
||||
(includeSD
|
||||
&& ((a.Item.Width > 0 && a.Item.Width < HDWidth) || versionsSd.Contains(a.ItemId))
|
||||
&& !versionsHd.Contains(a.ItemId)
|
||||
&& !versions4K.Contains(a.ItemId))
|
||||
|| (includeHD
|
||||
&& (a.Item.Width >= HDWidth || versionsHd.Contains(a.ItemId))
|
||||
&& !(a.Item.Width >= UHDWidth || a.Item.Height >= UHDHeight)
|
||||
&& !versions4K.Contains(a.ItemId))
|
||||
|| (include4K
|
||||
&& (a.Item.Width >= UHDWidth || a.Item.Height >= UHDHeight || versions4K.Contains(a.ItemId))))
|
||||
|| context.LinkedChildren.Any(lc =>
|
||||
lc.ParentId == e.Id
|
||||
&& lc.Child!.Width > 0
|
||||
&& ((includeSD && lc.Child.Width < HDWidth)
|
||||
|| (includeHD && lc.Child.Width >= HDWidth && !(lc.Child.Width >= UHDWidth || lc.Child.Height >= UHDHeight))
|
||||
|| (include4K && (lc.Child.Width >= UHDWidth || lc.Child.Height >= UHDHeight)))))));
|
||||
&& ((includeSD
|
||||
&& ((lc.Child!.Width > 0 && lc.Child!.Width < HDWidth) || versionsSd.Contains(lc.ChildId))
|
||||
&& !versionsHd.Contains(lc.ChildId)
|
||||
&& !versions4K.Contains(lc.ChildId))
|
||||
|| (includeHD
|
||||
&& (lc.Child!.Width >= HDWidth || versionsHd.Contains(lc.ChildId))
|
||||
&& !(lc.Child!.Width >= UHDWidth || lc.Child!.Height >= UHDHeight)
|
||||
&& !versions4K.Contains(lc.ChildId))
|
||||
|| (include4K
|
||||
&& (lc.Child!.Width >= UHDWidth || lc.Child!.Height >= UHDHeight || versions4K.Contains(lc.ChildId))))))));
|
||||
}
|
||||
|
||||
// Same reasoning as the resolution filter: a dimension bound is met if any version meets it.
|
||||
@@ -132,17 +149,19 @@ public sealed partial class BaseItemRepository
|
||||
baseQuery = baseQuery.Where(e => e.Height >= minHeight || versionsTallEnough.Contains(e.Id));
|
||||
}
|
||||
|
||||
// An upper bound inverts that: it is met only if no version breaches it, since the item's
|
||||
// resolution is the best its version group offers.
|
||||
if (maxWidth.HasValue)
|
||||
{
|
||||
var versionsNarrowEnough = VersionsMatchingDimension(context, v => v.Width <= maxWidth);
|
||||
baseQuery = baseQuery.Where(e => e.Width <= maxWidth || versionsNarrowEnough.Contains(e.Id));
|
||||
var versionsTooWide = VersionsMatchingDimension(context, v => v.Width > maxWidth);
|
||||
baseQuery = baseQuery.Where(e => e.Width <= maxWidth && !versionsTooWide.Contains(e.Id));
|
||||
}
|
||||
|
||||
if (filter.MaxHeight.HasValue)
|
||||
{
|
||||
var maxHeight = filter.MaxHeight;
|
||||
var versionsShortEnough = VersionsMatchingDimension(context, v => v.Height <= maxHeight);
|
||||
baseQuery = baseQuery.Where(e => e.Height <= maxHeight || versionsShortEnough.Contains(e.Id));
|
||||
var versionsTooTall = VersionsMatchingDimension(context, v => v.Height > maxHeight);
|
||||
baseQuery = baseQuery.Where(e => e.Height <= maxHeight && !versionsTooTall.Contains(e.Id));
|
||||
}
|
||||
|
||||
if (filter.IsLocked.HasValue)
|
||||
@@ -793,8 +812,8 @@ public sealed partial class BaseItemRepository
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(filter.HasNoAudioTrackWithLanguage))
|
||||
{
|
||||
var lang = filter.HasNoAudioTrackWithLanguage;
|
||||
var undetermined = IsUndetermined(lang);
|
||||
var lang = NormalizeLanguage(filter.HasNoAudioTrackWithLanguage);
|
||||
var undetermined = string.Equals(lang, "und", StringComparison.Ordinal);
|
||||
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.
|
||||
@@ -812,8 +831,8 @@ public sealed partial class BaseItemRepository
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(filter.HasNoInternalSubtitleTrackWithLanguage))
|
||||
{
|
||||
var lang = filter.HasNoInternalSubtitleTrackWithLanguage;
|
||||
var undetermined = IsUndetermined(lang);
|
||||
var lang = NormalizeLanguage(filter.HasNoInternalSubtitleTrackWithLanguage);
|
||||
var undetermined = string.Equals(lang, "und", StringComparison.Ordinal);
|
||||
var criteria = new HasMediaStreamType(MediaStreamTypeEntity.Subtitle, lang, IsExternal: false);
|
||||
var versionsWithSubtitles = DescendantQueryHelper.GetPrimaryVersionIdsMatching(context, criteria);
|
||||
var foldersWithSubtitles = DescendantQueryHelper.GetFolderIdsMatching(context, criteria);
|
||||
@@ -829,8 +848,8 @@ public sealed partial class BaseItemRepository
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(filter.HasNoExternalSubtitleTrackWithLanguage))
|
||||
{
|
||||
var lang = filter.HasNoExternalSubtitleTrackWithLanguage;
|
||||
var undetermined = IsUndetermined(lang);
|
||||
var lang = NormalizeLanguage(filter.HasNoExternalSubtitleTrackWithLanguage);
|
||||
var undetermined = string.Equals(lang, "und", StringComparison.Ordinal);
|
||||
var criteria = new HasMediaStreamType(MediaStreamTypeEntity.Subtitle, lang, IsExternal: true);
|
||||
var versionsWithSubtitles = DescendantQueryHelper.GetPrimaryVersionIdsMatching(context, criteria);
|
||||
var foldersWithSubtitles = DescendantQueryHelper.GetFolderIdsMatching(context, criteria);
|
||||
@@ -846,8 +865,8 @@ public sealed partial class BaseItemRepository
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(filter.HasNoSubtitleTrackWithLanguage))
|
||||
{
|
||||
var lang = filter.HasNoSubtitleTrackWithLanguage;
|
||||
var undetermined = IsUndetermined(lang);
|
||||
var lang = NormalizeLanguage(filter.HasNoSubtitleTrackWithLanguage);
|
||||
var undetermined = string.Equals(lang, "und", StringComparison.Ordinal);
|
||||
var criteria = new HasMediaStreamType(MediaStreamTypeEntity.Subtitle, lang);
|
||||
var versionsWithSubtitles = DescendantQueryHelper.GetPrimaryVersionIdsMatching(context, criteria);
|
||||
var foldersWithSubtitles = DescendantQueryHelper.GetFolderIdsMatching(context, criteria);
|
||||
|
||||
@@ -102,20 +102,25 @@ public static class DescendantQueryHelper
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
ArgumentNullException.ThrowIfNull(criteria);
|
||||
|
||||
var matchingItemIds = GetItemIdsMatching(context, criteria);
|
||||
// Both sides of a version group can hold a folder a caller would see as matching: the
|
||||
// alternate carries its own AncestorIds rows and may sit in a different library than the
|
||||
// primary it is reported against, and the primary is the item that becomes visible.
|
||||
var reportedItemIds = MatchingMediaOwnerIds(context, criteria)
|
||||
.Concat(GetPrimaryVersionIdsMatching(context, criteria))
|
||||
.Distinct();
|
||||
|
||||
// One hop up the closure covers every ancestor level.
|
||||
var hierarchyAncestors = context.AncestorIds
|
||||
.Where(e => matchingItemIds.Contains(e.ItemId))
|
||||
.Where(e => reportedItemIds.Contains(e.ItemId))
|
||||
.Select(e => e.ParentItemId);
|
||||
|
||||
var linkParents = ResolveLinkParents(context, matchingItemIds, hierarchyAncestors);
|
||||
var linkParents = ResolveLinkParents(context, reportedItemIds, hierarchyAncestors);
|
||||
|
||||
// Read back as a sub-select so the result stays composable. LinkedChildren is the cheapest
|
||||
// source: owning a link is what put an id in the set, and ParentId is its leading key.
|
||||
var linkedParents = context.LinkedChildren
|
||||
.WhereOneOrMany(linkParents, e => e.ParentId)
|
||||
.Select(e => e.ParentId);
|
||||
// Read back as a sub-select so the result stays composable. Off the primary key, which is one
|
||||
// row per id: LinkedChildren would yield one row per link and lean on the outer Distinct.
|
||||
var linkedParents = context.BaseItems
|
||||
.WhereOneOrMany(linkParents, e => e.Id)
|
||||
.Select(e => e.Id);
|
||||
|
||||
var linkedParentAncestors = context.AncestorIds
|
||||
.WhereOneOrMany(linkParents, e => e.ItemId)
|
||||
@@ -134,25 +139,6 @@ public static class DescendantQueryHelper
|
||||
.Distinct();
|
||||
}
|
||||
|
||||
/// <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.
|
||||
@@ -170,23 +156,47 @@ public static class DescendantQueryHelper
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
ArgumentNullException.ThrowIfNull(criteria);
|
||||
|
||||
return MatchingMediaOwners(context, criteria)
|
||||
.Where(e => e.PrimaryVersionId.HasValue)
|
||||
.Select(e => e.PrimaryVersionId!.Value);
|
||||
// Anchored on the alternates rather than on the matches: "has a primary version" is served by
|
||||
// the partial PrimaryVersionId index, which holds only the few items that are second files, so
|
||||
// this costs a seek each into the stream index instead of a second pass over every stream row.
|
||||
var alternates = context.BaseItems.Where(v => v.PrimaryVersionId.HasValue);
|
||||
|
||||
if (criteria is HasChapterImages)
|
||||
{
|
||||
return alternates
|
||||
.Where(v => context.Chapters.Any(c => c.ItemId.Equals(v.Id) && c.ImagePath != null))
|
||||
.Select(v => v.PrimaryVersionId!.Value);
|
||||
}
|
||||
|
||||
var matchingStreams = MatchingMediaStreams(context, criteria);
|
||||
|
||||
return alternates
|
||||
.Where(v => matchingStreams.Any(ms => ms.ItemId.Equals(v.Id)))
|
||||
.Select(v => v.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)
|
||||
// The ids of the items whose own media matches. Kept to the stream and chapter tables so their
|
||||
// covering indexes answer this outright: projecting the BaseItems navigation instead would add a
|
||||
// primary-key lookup per stream row rather than one per matching item, and the leading key of both
|
||||
// indexes leaves the ids already grouped, so the Distinct costs no sort.
|
||||
private static IQueryable<Guid> MatchingMediaOwnerIds(JellyfinDbContext context, FolderMatchCriteria criteria)
|
||||
=> criteria is HasChapterImages
|
||||
? context.Chapters
|
||||
.Where(c => c.ImagePath != null)
|
||||
.Select(c => c.ItemId)
|
||||
.Distinct()
|
||||
: MatchingMediaStreams(context, criteria)
|
||||
.Select(ms => ms.ItemId)
|
||||
.Distinct();
|
||||
|
||||
// The stream rows a criteria matches. One definition, so the owner projection and the alternate
|
||||
// projection cannot drift apart despite reading it from opposite ends.
|
||||
private static IQueryable<MediaStreamInfo> MatchingMediaStreams(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),
|
||||
.Where(ms => ms.StreamType == MediaStreamTypeEntity.Subtitle),
|
||||
HasMediaStreamType m => GetMatchingMediaStreams(context, m),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(criteria), $"Unknown criteria type: {criteria.GetType().Name}")
|
||||
};
|
||||
|
||||
|
||||
+239
@@ -35,6 +35,29 @@ public sealed class BaseItemRepositoryStreamFilterTests : SqliteDbTestFixture
|
||||
private readonly Guid _versionedMovie = Guid.NewGuid();
|
||||
private readonly Guid _alternateVersion = Guid.NewGuid();
|
||||
|
||||
// A series in the same library, so the folder branch of the resolution filter has a version group
|
||||
// to reach through as well: an SD episode whose second file is 4K.
|
||||
private readonly Guid _versionedSeries = Guid.NewGuid();
|
||||
private readonly Guid _versionedEpisode = Guid.NewGuid();
|
||||
private readonly Guid _episodeAlternate = Guid.NewGuid();
|
||||
|
||||
// An unprobed primary: only its second file carries dimensions, and they are SD.
|
||||
private readonly Guid _unprobedMovie = Guid.NewGuid();
|
||||
private readonly Guid _unprobedAlternate = Guid.NewGuid();
|
||||
|
||||
// A plain SD movie with no second file, as the control the version groups are read against.
|
||||
private readonly Guid _sdMovie = Guid.NewGuid();
|
||||
|
||||
// An unprobed primary whose only second file is HD, so the HD bucket has to place it off nulls.
|
||||
private readonly Guid _hdOnlyByVersion = Guid.NewGuid();
|
||||
private readonly Guid _hdOnlyAlternate = Guid.NewGuid();
|
||||
|
||||
// Three files for one movie: the HD one would place it in the HD bucket on its own, the 4K one has
|
||||
// to win. Only a group holding both can tell the HD bucket's upper guard from its lower one.
|
||||
private readonly Guid _threeWayMovie = Guid.NewGuid();
|
||||
private readonly Guid _threeWayHd = Guid.NewGuid();
|
||||
private readonly Guid _threeWay4K = Guid.NewGuid();
|
||||
|
||||
public BaseItemRepositoryStreamFilterTests()
|
||||
{
|
||||
using (var ctx = CreateDbContext())
|
||||
@@ -179,6 +202,90 @@ public sealed class BaseItemRepositoryStreamFilterTests : SqliteDbTestFixture
|
||||
Assert.DoesNotContain(_withSubtitles, _repository.GetItemIdsList(new InternalItemsQuery { MinWidth = 3000 }));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MaxWidth_ExcludesAnItemWhoseAlternateVersionBreachesTheBound()
|
||||
{
|
||||
// The SD primary is narrow enough on its own, but the 4K second file is what a caller would play.
|
||||
Assert.DoesNotContain(_versionedMovie, _repository.GetItemIdsList(new InternalItemsQuery { MaxWidth = 1920 }));
|
||||
Assert.Contains(_sdMovie, _repository.GetItemIdsList(new InternalItemsQuery { MaxWidth = 1920 }));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MaxHeight_ExcludesAnItemWhoseAlternateVersionBreachesTheBound()
|
||||
{
|
||||
Assert.DoesNotContain(_versionedMovie, _repository.GetItemIdsList(new InternalItemsQuery { MaxHeight = 1080 }));
|
||||
Assert.Contains(_sdMovie, _repository.GetItemIdsList(new InternalItemsQuery { MaxHeight = 1080 }));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHD_False_ExcludesAnSdPrimaryWhoseAlternateVersionIsBetter()
|
||||
{
|
||||
var ids = _repository.GetItemIdsList(new InternalItemsQuery { IsHD = false });
|
||||
|
||||
// 720x480 on its own, but the version group tops out at 4K.
|
||||
Assert.DoesNotContain(_versionedMovie, ids);
|
||||
Assert.Contains(_sdMovie, ids);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHD_False_MatchesAPrimaryPlacedOnlyByItsAlternateVersion()
|
||||
{
|
||||
// The primary carries no dimensions at all; the SD second file is the group's best.
|
||||
Assert.Contains(_unprobedMovie, _repository.GetItemIdsList(new InternalItemsQuery { IsHD = false }));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHD_True_ExcludesAnItemWhoseVersionGroupReaches4K()
|
||||
{
|
||||
var ids = _repository.GetItemIdsList(new InternalItemsQuery { IsHD = true });
|
||||
|
||||
Assert.DoesNotContain(_versionedMovie, ids);
|
||||
Assert.DoesNotContain(_unprobedMovie, ids);
|
||||
// The 1920-wide second file alone would say HD; the 4K third file is the group's best.
|
||||
Assert.DoesNotContain(_threeWayMovie, ids);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Is4K_MatchesAnItemWhoseVersionGroupHoldsBothHdAnd4K()
|
||||
{
|
||||
Assert.Contains(_threeWayMovie, _repository.GetItemIdsList(new InternalItemsQuery { Is4K = true }));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHD_True_MatchesAPrimaryPlacedOnlyByItsAlternateVersion()
|
||||
{
|
||||
// The primary carries no dimensions of its own; the HD second file is the group's best.
|
||||
Assert.Contains(_hdOnlyByVersion, _repository.GetItemIdsList(new InternalItemsQuery { IsHD = true }));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Is4K_MatchesTheSeriesOfAnEpisodeWhoseAlternateVersionIs4K()
|
||||
{
|
||||
// The folder branch buckets a descendant the same way the item branch buckets a top-level item.
|
||||
Assert.Contains(_versionedSeries, _repository.GetItemIdsList(new InternalItemsQuery { Is4K = true }));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHD_False_ExcludesTheSeriesOfAnSdEpisodeWithABetterAlternateVersion()
|
||||
{
|
||||
// Before the version group was consulted on descendants too, the SD episode alone matched here
|
||||
// while the same pair at top level did not.
|
||||
Assert.DoesNotContain(_versionedSeries, _repository.GetItemIdsList(new InternalItemsQuery { IsHD = false }));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("und")]
|
||||
[InlineData("UND")]
|
||||
public void HasNoAudioTrackWithLanguage_TreatsUndeterminedCaseInsensitively(string language)
|
||||
{
|
||||
var ids = _repository.GetItemIdsList(new InternalItemsQuery { HasNoAudioTrackWithLanguage = language });
|
||||
|
||||
// The alternate version carries an audio track with no language, which is what "und" stands for,
|
||||
// so the item it is reported against does have one.
|
||||
Assert.DoesNotContain(_unprobedMovie, ids);
|
||||
Assert.Contains(_versionedMovie, ids);
|
||||
}
|
||||
|
||||
private void Seed(JellyfinDbContext context)
|
||||
{
|
||||
context.BaseItems.Add(new BaseItemEntity { Id = _library, Type = FolderType, Name = "Library", IsFolder = true });
|
||||
@@ -302,6 +409,9 @@ public sealed class BaseItemRepositoryStreamFilterTests : SqliteDbTestFixture
|
||||
Item = null!
|
||||
});
|
||||
|
||||
SeedVersionedSeries(context);
|
||||
SeedUnprobedVersionGroup(context);
|
||||
|
||||
context.Chapters.Add(new Chapter
|
||||
{
|
||||
ItemId = _alternateVersion,
|
||||
@@ -311,4 +421,133 @@ public sealed class BaseItemRepositoryStreamFilterTests : SqliteDbTestFixture
|
||||
Item = null!
|
||||
});
|
||||
}
|
||||
|
||||
// The same SD primary / 4K second file pair one level down, so the resolution filter has to answer
|
||||
// for the series off its descendants.
|
||||
private void SeedVersionedSeries(JellyfinDbContext context)
|
||||
{
|
||||
context.BaseItems.Add(new BaseItemEntity { Id = _versionedSeries, Type = FolderType, Name = "Versioned series", IsFolder = true });
|
||||
context.BaseItems.Add(new BaseItemEntity { Id = _versionedEpisode, Type = MovieType, Name = "Versioned episode", Width = 720, Height = 480 });
|
||||
context.BaseItems.Add(new BaseItemEntity
|
||||
{
|
||||
Id = _episodeAlternate,
|
||||
Type = MovieType,
|
||||
Name = "Versioned episode 4K",
|
||||
PrimaryVersionId = _versionedEpisode,
|
||||
Width = 3840,
|
||||
Height = 2160
|
||||
});
|
||||
|
||||
context.AncestorIds.Add(new AncestorId
|
||||
{
|
||||
ItemId = _versionedSeries,
|
||||
ParentItemId = _versionLibrary,
|
||||
Item = null!,
|
||||
ParentItem = null!
|
||||
});
|
||||
|
||||
foreach (var itemId in new[] { _versionedEpisode, _episodeAlternate })
|
||||
{
|
||||
context.AncestorIds.Add(new AncestorId
|
||||
{
|
||||
ItemId = itemId,
|
||||
ParentItemId = _versionedSeries,
|
||||
Item = null!,
|
||||
ParentItem = null!
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// A primary that was never probed, so only its second file can place it in a bucket. Its audio track
|
||||
// declares no language, which is what the "und" filters stand in for.
|
||||
private void SeedUnprobedVersionGroup(JellyfinDbContext context)
|
||||
{
|
||||
context.BaseItems.Add(new BaseItemEntity { Id = _sdMovie, Type = MovieType, Name = "SD movie", Width = 720, Height = 480 });
|
||||
context.AncestorIds.Add(new AncestorId
|
||||
{
|
||||
ItemId = _sdMovie,
|
||||
ParentItemId = _versionLibrary,
|
||||
Item = null!,
|
||||
ParentItem = null!
|
||||
});
|
||||
|
||||
context.BaseItems.Add(new BaseItemEntity { Id = _unprobedMovie, Type = MovieType, Name = "Unprobed movie" });
|
||||
context.BaseItems.Add(new BaseItemEntity
|
||||
{
|
||||
Id = _unprobedAlternate,
|
||||
Type = MovieType,
|
||||
Name = "Unprobed movie SD",
|
||||
PrimaryVersionId = _unprobedMovie,
|
||||
Width = 720,
|
||||
Height = 480
|
||||
});
|
||||
|
||||
foreach (var itemId in new[] { _unprobedMovie, _unprobedAlternate })
|
||||
{
|
||||
context.AncestorIds.Add(new AncestorId
|
||||
{
|
||||
ItemId = itemId,
|
||||
ParentItemId = _versionLibrary,
|
||||
Item = null!,
|
||||
ParentItem = null!
|
||||
});
|
||||
}
|
||||
|
||||
context.MediaStreamInfos.Add(new MediaStreamInfo
|
||||
{
|
||||
ItemId = _unprobedAlternate,
|
||||
StreamIndex = 0,
|
||||
StreamType = MediaStreamTypeEntity.Audio,
|
||||
Item = null!
|
||||
});
|
||||
|
||||
SeedMixedVersionGroups(context);
|
||||
}
|
||||
|
||||
// The two groups that separate the HD bucket's lower bound from its upper one: one that only a 4K
|
||||
// third file keeps out of HD, and one that only an HD second file puts into it.
|
||||
private void SeedMixedVersionGroups(JellyfinDbContext context)
|
||||
{
|
||||
context.BaseItems.Add(new BaseItemEntity { Id = _threeWayMovie, Type = MovieType, Name = "Three-way movie", Width = 720, Height = 480 });
|
||||
context.BaseItems.Add(new BaseItemEntity
|
||||
{
|
||||
Id = _threeWayHd,
|
||||
Type = MovieType,
|
||||
Name = "Three-way movie HD",
|
||||
PrimaryVersionId = _threeWayMovie,
|
||||
Width = 1920,
|
||||
Height = 1080
|
||||
});
|
||||
context.BaseItems.Add(new BaseItemEntity
|
||||
{
|
||||
Id = _threeWay4K,
|
||||
Type = MovieType,
|
||||
Name = "Three-way movie 4K",
|
||||
PrimaryVersionId = _threeWayMovie,
|
||||
Width = 3840,
|
||||
Height = 2160
|
||||
});
|
||||
|
||||
context.BaseItems.Add(new BaseItemEntity { Id = _hdOnlyByVersion, Type = MovieType, Name = "HD only by version" });
|
||||
context.BaseItems.Add(new BaseItemEntity
|
||||
{
|
||||
Id = _hdOnlyAlternate,
|
||||
Type = MovieType,
|
||||
Name = "HD only by version, HD file",
|
||||
PrimaryVersionId = _hdOnlyByVersion,
|
||||
Width = 1920,
|
||||
Height = 1080
|
||||
});
|
||||
|
||||
foreach (var itemId in new[] { _threeWayMovie, _threeWayHd, _threeWay4K, _hdOnlyByVersion, _hdOnlyAlternate })
|
||||
{
|
||||
context.AncestorIds.Add(new AncestorId
|
||||
{
|
||||
ItemId = itemId,
|
||||
ParentItemId = _versionLibrary,
|
||||
Item = null!,
|
||||
ParentItem = null!
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user