aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShadowghost <Ghost_of_Stone@web.de>2026-07-29 13:13:44 +0200
committerShadowghost <Ghost_of_Stone@web.de>2026-07-29 13:13:44 +0200
commitd8fc0a991433bd6ec9688ce16f944e21671fc945 (patch)
treee37b01b368bb247c9e3a996088207579da0fae35
parentafb716566d04102fa6c8e1b12977d175db301081 (diff)
Fix AdjacentTo being ignored on non-recursive item queries
-rw-r--r--Jellyfin.Server.Implementations/Item/BaseItemRepository.QueryBuilding.cs29
-rw-r--r--Jellyfin.Server.Implementations/Item/BaseItemRepository.Querying.cs2
-rw-r--r--Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs27
-rw-r--r--MediaBrowser.Controller/Entities/Folder.cs10
-rw-r--r--MediaBrowser.Controller/Entities/UserViewBuilder.cs39
5 files changed, 58 insertions, 49 deletions
diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.QueryBuilding.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.QueryBuilding.cs
index a4de9feb05..54926b7cd9 100644
--- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.QueryBuilding.cs
+++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.QueryBuilding.cs
@@ -35,11 +35,40 @@ public sealed partial class BaseItemRepository
{
dbQuery = TranslateQuery(dbQuery, context, filter);
dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
+ dbQuery = ApplyAdjacencyFilter(context, dbQuery, filter);
dbQuery = ApplyQueryPaging(dbQuery, filter);
dbQuery = ApplyNavigations(dbQuery, filter);
return dbQuery;
}
+ /// <summary>
+ /// Trims an ordered query down to the AdjacentTo item and its immediate neighbours.
+ /// </summary>
+ private IQueryable<BaseItemEntity> ApplyAdjacencyFilter(JellyfinDbContext context, IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
+ {
+ if (filter.AdjacentTo.IsNullOrEmpty())
+ {
+ return dbQuery;
+ }
+
+ // Adjacency is relative to the result set and the order the query asked for, so the ids have
+ // to be read back in that order.
+ var orderedIds = dbQuery.Select(e => e.Id).ToList();
+ var index = orderedIds.IndexOf(filter.AdjacentTo.Value);
+ if (index < 0)
+ {
+ // The item isn't part of this result set, so it has no neighbours in it either.
+ return dbQuery.Take(0);
+ }
+
+ var start = Math.Max(index - 1, 0);
+ var adjacentIds = orderedIds.GetRange(start, Math.Min(index + 2, orderedIds.Count) - start);
+
+ var adjacentQuery = context.BaseItems.AsNoTracking().AsSingleQuery().Where(e => adjacentIds.Contains(e.Id));
+
+ return ApplyOrder(adjacentQuery, filter, context);
+ }
+
private IQueryable<BaseItemEntity> ApplyQueryPaging(IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
{
if (filter.Limit.HasValue || filter.StartIndex.HasValue)
diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.Querying.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.Querying.cs
index 1ff8d8f863..b02d91b458 100644
--- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.Querying.cs
+++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.Querying.cs
@@ -49,6 +49,7 @@ public sealed partial class BaseItemRepository
dbQuery = TranslateQuery(dbQuery, context, filter);
dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
+ dbQuery = ApplyAdjacencyFilter(context, dbQuery, filter);
if (filter.EnableTotalRecordCount)
{
@@ -75,6 +76,7 @@ public sealed partial class BaseItemRepository
dbQuery = TranslateQuery(dbQuery, context, filter);
dbQuery = ApplyGroupingFilter(context, dbQuery, filter);
+ dbQuery = ApplyAdjacencyFilter(context, dbQuery, filter);
dbQuery = ApplyQueryPaging(dbQuery, filter);
var hasRandomSort = filter.OrderBy.Any(e => e.OrderBy == ItemSortBy.Random);
diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs
index 47f8a40b9c..1b73087296 100644
--- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs
+++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs
@@ -1243,33 +1243,6 @@ public sealed partial class BaseItemRepository
}
}
- if (filter.AdjacentTo.HasValue && !filter.AdjacentTo.Value.IsEmpty())
- {
- var adjacentToId = filter.AdjacentTo.Value;
- var targetItem = context.BaseItems.Where(e => e.Id == adjacentToId).Select(e => new { e.SortName, e.Id }).FirstOrDefault();
- if (targetItem is not null)
- {
- var targetSortName = targetItem.SortName ?? string.Empty;
-
- // Fetch both prev and next adjacent items in a single query using Concat (UNION ALL).
- var adjacentIds = context.BaseItems
- .Where(e => string.Compare(e.SortName, targetSortName) < 0)
- .OrderByDescending(e => e.SortName)
- .Select(e => e.Id)
- .Take(1)
- .Concat(
- context.BaseItems
- .Where(e => string.Compare(e.SortName, targetSortName) > 0)
- .OrderBy(e => e.SortName)
- .Select(e => e.Id)
- .Take(1))
- .ToList();
-
- adjacentIds.Add(adjacentToId);
- baseQuery = baseQuery.Where(e => adjacentIds.Contains(e.Id));
- }
- }
-
return baseQuery;
}
}
diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs
index b1f7f29bad..ed11f21f52 100644
--- a/MediaBrowser.Controller/Entities/Folder.cs
+++ b/MediaBrowser.Controller/Entities/Folder.cs
@@ -1085,15 +1085,7 @@ namespace MediaBrowser.Controller.Entities
items = ApplyNameFilter(items, query);
}
- var filteredItems = items as IReadOnlyList<BaseItem> ?? items.ToList();
- var result = UserViewBuilder.SortAndPage(filteredItems, null, query, LibraryManager);
-
- if (query.EnableTotalRecordCount)
- {
- result.TotalRecordCount = filteredItems.Count;
- }
-
- return result;
+ return UserViewBuilder.SortAndPage(items, null, query, LibraryManager);
}
private static IEnumerable<BaseItem> ApplyNameFilter(IEnumerable<BaseItem> items, InternalItemsQuery query)
diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
index 9ba103cc8b..fe7866fe65 100644
--- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs
+++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
@@ -490,6 +490,13 @@ namespace MediaBrowser.Controller.Entities
}
var itemsArray = totalRecordLimit.HasValue ? items.Take(totalRecordLimit.Value).ToArray() : items.ToArray();
+
+ // Adjacency is defined by the order the query asked for, so it has to run after sorting but before paging.
+ if (!query.AdjacentTo.IsNullOrEmpty())
+ {
+ itemsArray = FilterForAdjacency(itemsArray, query.AdjacentTo.Value).ToArray();
+ }
+
var totalCount = itemsArray.Length;
if (query.Limit.HasValue && query.Limit.Value > 0)
@@ -886,26 +893,32 @@ namespace MediaBrowser.Controller.Entities
return _userViewManager.GetUserSubView(parent.Id, type, localizationKey, sortName);
}
- public static IEnumerable<BaseItem> FilterForAdjacency(List<BaseItem> list, Guid adjacentTo)
+ /// <summary>
+ /// Trims an ordered list down to the requested item and its immediate neighbours.
+ /// </summary>
+ /// <param name="list">The items in the order the query returned them.</param>
+ /// <param name="adjacentTo">The id of the item to return the neighbours of.</param>
+ /// <returns>The previous item, the requested item and the next item, in order.</returns>
+ public static IEnumerable<BaseItem> FilterForAdjacency(IReadOnlyList<BaseItem> list, Guid adjacentTo)
{
- var adjacentToItem = list.FirstOrDefault(i => i.Id.Equals(adjacentTo));
-
- var index = list.IndexOf(adjacentToItem);
-
- var previousId = Guid.Empty;
- var nextId = Guid.Empty;
-
- if (index > 0)
+ var index = -1;
+ for (var i = 0; i < list.Count; i++)
{
- previousId = list[index - 1].Id;
+ if (list[i].Id.Equals(adjacentTo))
+ {
+ index = i;
+ break;
+ }
}
- if (index < list.Count - 1)
+ // The item isn't part of this result set, so it has no neighbours in it either.
+ if (index < 0)
{
- nextId = list[index + 1].Id;
+ return [];
}
- return list.Where(i => i.Id.Equals(previousId) || i.Id.Equals(nextId) || i.Id.Equals(adjacentTo));
+ var start = Math.Max(index - 1, 0);
+ return list.Skip(start).Take(Math.Min(index + 2, list.Count) - start);
}
}
}