diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 46564b24cb..97263e97ee 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -998,6 +998,10 @@ public sealed class BaseItemRepository if (filter.CollapseBoxSetItems == true) { dbQuery = ApplyBoxSetCollapsing(context, dbQuery, filter.CollapseBoxSetItemTypes); + + // Apply name-range filters after collapse so BoxSets are filtered by their own name, + // not by their children's names. + dbQuery = ApplyNameFilters(dbQuery, filter); } dbQuery = ApplyOrder(dbQuery, filter, context); @@ -1078,6 +1082,29 @@ public sealed class BaseItemRepository return context.BaseItems.Where(e => collapsedIds.Contains(e.Id)); } + private static IQueryable ApplyNameFilters(IQueryable dbQuery, InternalItemsQuery filter) + { + if (!string.IsNullOrWhiteSpace(filter.NameStartsWith)) + { + var nameStartsWithLower = filter.NameStartsWith.ToLowerInvariant(); + dbQuery = dbQuery.Where(e => e.SortName!.ToLower().StartsWith(nameStartsWithLower)); + } + + if (!string.IsNullOrWhiteSpace(filter.NameStartsWithOrGreater)) + { + var startsOrGreaterLower = filter.NameStartsWithOrGreater.ToLowerInvariant(); + dbQuery = dbQuery.Where(e => e.SortName!.ToLower().CompareTo(startsOrGreaterLower) >= 0); + } + + if (!string.IsNullOrWhiteSpace(filter.NameLessThan)) + { + var lessThanLower = filter.NameLessThan.ToLowerInvariant(); + dbQuery = dbQuery.Where(e => e.SortName!.ToLower().CompareTo(lessThanLower) < 0); + } + + return dbQuery; + } + private static IQueryable ApplyNavigations(IQueryable dbQuery, InternalItemsQuery filter) { if (filter.TrailerTypes.Length > 0 || filter.IncludeItemTypes.Contains(BaseItemKind.Trailer)) @@ -3073,22 +3100,13 @@ public sealed class BaseItemRepository } } - if (!string.IsNullOrWhiteSpace(filter.NameStartsWith)) + // When box set collapsing is active, defer name-range filters to after the collapse. + // Otherwise, items are filtered by their own name but then collapsed into a BoxSet + // whose name may fall in a different range (e.g. "21 Jump Street" is under "#" + // but its BoxSet "Jump Street Collection" should appear under "J"). + if (filter.CollapseBoxSetItems != true) { - var nameStartsWithLower = filter.NameStartsWith.ToLowerInvariant(); - baseQuery = baseQuery.Where(e => e.SortName!.ToLower().StartsWith(nameStartsWithLower)); - } - - if (!string.IsNullOrWhiteSpace(filter.NameStartsWithOrGreater)) - { - var startsOrGreaterLower = filter.NameStartsWithOrGreater.ToLowerInvariant(); - baseQuery = baseQuery.Where(e => e.SortName!.ToLower().CompareTo(startsOrGreaterLower) >= 0); - } - - if (!string.IsNullOrWhiteSpace(filter.NameLessThan)) - { - var lessThanLower = filter.NameLessThan.ToLowerInvariant(); - baseQuery = baseQuery.Where(e => e.SortName!.ToLower().CompareTo(lessThanLower) < 0); + baseQuery = ApplyNameFilters(baseQuery, filter); } if (filter.ImageTypes.Length > 0) diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 4916ead69a..1b574a2814 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -900,6 +900,11 @@ namespace MediaBrowser.Controller.Entities if (user is not null) { items = CollapseBoxSetItemsIfNeeded(items, query, this, user, ConfigurationManager, CollectionManager); + + // After collapse, BoxSets may have replaced items whose names matched the filter + // but the BoxSet's own name may not match. Re-apply name filtering so BoxSets + // appear under the correct letter (e.g. "Jump Street" under J, not under #). + items = ApplyNameFilter(items, query); } var filteredItems = items as IReadOnlyList ?? items.ToList(); @@ -913,6 +918,26 @@ namespace MediaBrowser.Controller.Entities return result; } + private static IEnumerable ApplyNameFilter(IEnumerable items, InternalItemsQuery query) + { + if (!string.IsNullOrWhiteSpace(query.NameStartsWith)) + { + items = items.Where(i => i.SortName.StartsWith(query.NameStartsWith, StringComparison.OrdinalIgnoreCase)); + } + + if (!string.IsNullOrWhiteSpace(query.NameStartsWithOrGreater)) + { + items = items.Where(i => string.Compare(i.SortName, query.NameStartsWithOrGreater, StringComparison.OrdinalIgnoreCase) >= 0); + } + + if (!string.IsNullOrWhiteSpace(query.NameLessThan)) + { + items = items.Where(i => string.Compare(i.SortName, query.NameLessThan, StringComparison.OrdinalIgnoreCase) < 0); + } + + return items; + } + private static IEnumerable CollapseBoxSetItemsIfNeeded( IEnumerable items, InternalItemsQuery query,