Fix filters

This commit is contained in:
Shadowghost
2026-04-11 15:44:52 +02:00
parent 24a0df9a39
commit d8bbb4dfe8
4 changed files with 103 additions and 43 deletions

View File

@@ -3747,6 +3747,12 @@ namespace Emby.Server.Implementations.Library
/// <inheritdoc />
public QueryFiltersLegacy GetQueryFiltersLegacy(InternalItemsQuery query)
{
if (query.User is not null)
{
AddUserToQuery(query, query.User);
}
SetTopParentOrAncestorIds(query);
return _itemRepository.GetQueryFiltersLegacy(query);
}
}

View File

@@ -60,9 +60,7 @@ public class FilterController : BaseJellyfinApiController
BaseItem? item = null;
if (includeItemTypes.Length != 1
|| !(includeItemTypes[0] == BaseItemKind.BoxSet
|| includeItemTypes[0] == BaseItemKind.Playlist
|| includeItemTypes[0] == BaseItemKind.Trailer
|| !(includeItemTypes[0] == BaseItemKind.Trailer
|| includeItemTypes[0] == BaseItemKind.Program))
{
item = _libraryManager.GetParentItem(parentId, user?.Id);
@@ -127,9 +125,7 @@ public class FilterController : BaseJellyfinApiController
BaseItem? parentItem = null;
if (includeItemTypes.Length == 1
&& (includeItemTypes[0] == BaseItemKind.BoxSet
|| includeItemTypes[0] == BaseItemKind.Playlist
|| includeItemTypes[0] == BaseItemKind.Trailer
&& (includeItemTypes[0] == BaseItemKind.Trailer
|| includeItemTypes[0] == BaseItemKind.Program))
{
parentItem = null;

View File

@@ -299,6 +299,18 @@ public class ItemsController : BaseJellyfinApiController
recursive = true;
includeItemTypes = new[] { BaseItemKind.Playlist };
}
else if (folder is ICollectionFolder && includeItemTypes.Length == 0)
{
// When the client doesn't specify recursive/includeItemTypes, force the query
// through the database path where all filters (IsHD, genres, etc.) are applied.
recursive = true;
includeItemTypes = collectionType switch
{
CollectionType.boxsets => [BaseItemKind.BoxSet],
null => [BaseItemKind.Movie, BaseItemKind.Series], // mixed
_ => []
};
}
if (item is not UserRootFolder
// api keys can always access all folders

View File

@@ -66,10 +66,27 @@ public sealed partial class BaseItemRepository
include4K = true;
}
// 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 =>
(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))))
|| (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))))
|| 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)))))));
}
if (minWidth.HasValue)
@@ -443,44 +460,63 @@ public sealed partial class BaseItemRepository
if (filter.IsPlayed.HasValue)
{
// We should probably figure this out for all folders, but for right now, this is the only place where we need it
if (filter.IncludeItemTypes.Length == 1 && filter.IncludeItemTypes[0] == BaseItemKind.Series)
var hasSeries = filter.IncludeItemTypes.Contains(BaseItemKind.Series);
var hasBoxSet = filter.IncludeItemTypes.Contains(BaseItemKind.BoxSet);
if (hasSeries || hasBoxSet)
{
var userId = filter.User!.Id;
var seriesWithEpisodes = context.BaseItems
.Where(e => !e.IsFolder && !e.IsVirtualItem && e.SeriesId.HasValue)
.Select(e => e.SeriesId!.Value)
.Distinct();
var seriesWithUnplayedEpisodes = context.BaseItems
.Where(e => !e.IsFolder && !e.IsVirtualItem && e.SeriesId.HasValue
&& !e.UserData!.Any(ud => ud.UserId == userId && ud.Played))
.Select(e => e.SeriesId!.Value)
.Distinct();
var isPlayed = filter.IsPlayed.Value;
var seriesTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Series];
var boxSetTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.BoxSet];
// Series: played = all episodes played, unplayed = any episode unplayed
var seriesWithEpisodes = hasSeries
? context.BaseItems
.Where(e => !e.IsFolder && !e.IsVirtualItem && e.SeriesId.HasValue)
.Select(e => e.SeriesId!.Value)
.Distinct()
: Enumerable.Empty<Guid>().AsQueryable();
var seriesWithUnplayedEpisodes = hasSeries
? context.BaseItems
.Where(e => !e.IsFolder && !e.IsVirtualItem && e.SeriesId.HasValue
&& !e.UserData!.Any(ud => ud.UserId == userId && ud.Played))
.Select(e => e.SeriesId!.Value)
.Distinct()
: Enumerable.Empty<Guid>().AsQueryable();
// BoxSet: played = all children played
IEnumerable<Guid> playedBoxSetIds = [];
if (hasBoxSet)
{
var boxSetIds = baseQuery.Where(e => e.Type == boxSetTypeName).Select(e => e.Id).ToList();
var playedCounts = GetPlayedAndTotalCountBatch(boxSetIds, filter.User!);
playedBoxSetIds = playedCounts
.Where(kvp => kvp.Value.Total > 0 && kvp.Value.Played == kvp.Value.Total)
.Select(kvp => kvp.Key);
}
// Non-folder items: check UserData directly
var playedItemIds = context.UserData
.Where(ud => ud.UserId == userId && ud.Played)
.Select(ud => ud.ItemId);
if (isPlayed)
{
baseQuery = baseQuery.Where(s =>
seriesWithEpisodes.Contains(s.Id) && !seriesWithUnplayedEpisodes.Contains(s.Id));
baseQuery = baseQuery.Where(e =>
(e.Type == seriesTypeName && seriesWithEpisodes.Contains(e.Id) && !seriesWithUnplayedEpisodes.Contains(e.Id))
|| (e.Type == boxSetTypeName && playedBoxSetIds.Contains(e.Id))
|| (e.Type != seriesTypeName && e.Type != boxSetTypeName && playedItemIds.Contains(e.Id)));
}
else
{
baseQuery = baseQuery.Where(s =>
!seriesWithEpisodes.Contains(s.Id) || seriesWithUnplayedEpisodes.Contains(s.Id));
baseQuery = baseQuery.Where(e =>
(e.Type == seriesTypeName && (!seriesWithEpisodes.Contains(e.Id) || seriesWithUnplayedEpisodes.Contains(e.Id)))
|| (e.Type == boxSetTypeName && !playedBoxSetIds.Contains(e.Id))
|| (e.Type != seriesTypeName && e.Type != boxSetTypeName && !playedItemIds.Contains(e.Id)));
}
}
else if (filter.IncludeItemTypes.Length == 1 && filter.IncludeItemTypes[0] == BaseItemKind.BoxSet)
{
var boxSetIds = baseQuery.Select(e => e.Id).ToList();
var playedCounts = GetPlayedAndTotalCountBatch(boxSetIds, filter.User!);
var playedBoxSetIds = playedCounts
.Where(kvp => kvp.Value.Total > 0 && kvp.Value.Played == kvp.Value.Total)
.Select(kvp => kvp.Key);
var isPlayedBoxSet = filter.IsPlayed.Value;
baseQuery = baseQuery.Where(s => playedBoxSetIds.Contains(s.Id) == isPlayedBoxSet);
}
else
{
var playedItemIds = context.UserData
@@ -493,9 +529,13 @@ public sealed partial class BaseItemRepository
if (filter.IsResumable.HasValue)
{
if (filter.IncludeItemTypes.Length == 1 && filter.IncludeItemTypes[0] == BaseItemKind.Series)
var hasSeries = filter.IncludeItemTypes.Contains(BaseItemKind.Series);
if (hasSeries)
{
var userId = filter.User!.Id;
var seriesTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Series];
var isResumable = filter.IsResumable.Value;
// Series with at least one in-progress episode.
var seriesWithInProgressEpisodes = context.BaseItems
@@ -518,14 +558,20 @@ public sealed partial class BaseItemRepository
.Select(e => e.SeriesId!.Value)
.Distinct();
var isResumable = filter.IsResumable.Value;
// Non-series items: resumable if PlaybackPositionTicks > 0
var resumableItemIds = context.UserData
.Where(ud => ud.UserId == userId && ud.PlaybackPositionTicks > 0)
.Select(ud => ud.ItemId);
// A series is resumable if it has an in-progress episode,
// or if it has both played and unplayed episodes (partially watched).
baseQuery = baseQuery.Where(s =>
(seriesWithInProgressEpisodes.Contains(s.Id)
|| (seriesWithPlayedEpisodes.Contains(s.Id) && seriesWithUnplayedEpisodes.Contains(s.Id)))
== isResumable);
baseQuery = baseQuery.Where(e =>
(e.Type == seriesTypeName
&& (seriesWithInProgressEpisodes.Contains(e.Id)
|| (seriesWithPlayedEpisodes.Contains(e.Id) && seriesWithUnplayedEpisodes.Contains(e.Id)))
== isResumable)
|| (e.Type != seriesTypeName
&& resumableItemIds.Contains(e.Id) == isResumable));
}
else
{