From 08fd175f5aa3a921d44934a11ccb8bcbd3956120 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Mon, 27 Oct 2025 15:43:23 -0400 Subject: Backport pull request #15187 from jellyfin/release-10.11.z Fix pagination and sorting for folders Original-merge: 7d1824ea27093322d5e8316ee38f375129f40386 Merged-by: crobibero Backported-by: Bond_009 --- MediaBrowser.Controller/Entities/Folder.cs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) (limited to 'MediaBrowser.Controller/Entities') diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index e9a383690..03ee44708 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -715,9 +715,18 @@ namespace MediaBrowser.Controller.Entities } else { - items = GetRecursiveChildren(user, query, out totalCount); + // Save pagination params before clearing them to prevent pagination from happening + // before sorting. PostFilterAndSort will apply pagination after sorting. + var limit = query.Limit; + var startIndex = query.StartIndex; query.Limit = null; - query.StartIndex = null; // override these here as they have already been applied + query.StartIndex = null; + + items = GetRecursiveChildren(user, query, out totalCount); + + // Restore pagination params so PostFilterAndSort can apply them after sorting + query.Limit = limit; + query.StartIndex = startIndex; } var result = PostFilterAndSort(items, query); @@ -980,20 +989,16 @@ namespace MediaBrowser.Controller.Entities else { // need to pass this param to the children. + // Note: Don't pass Limit/StartIndex here as pagination should happen after sorting in PostFilterAndSort var childQuery = new InternalItemsQuery { DisplayAlbumFolders = query.DisplayAlbumFolders, - Limit = query.Limit, - StartIndex = query.StartIndex, NameStartsWith = query.NameStartsWith, NameStartsWithOrGreater = query.NameStartsWithOrGreater, NameLessThan = query.NameLessThan }; items = GetChildren(user, true, out totalItemCount, childQuery).Where(filter); - - query.Limit = null; - query.StartIndex = null; } var result = PostFilterAndSort(items, query); -- cgit v1.2.3 From 5ea3910af96ead74d9267ec239e47a798a33e78f Mon Sep 17 00:00:00 2001 From: revam Date: Mon, 17 Nov 2025 14:08:47 -0500 Subject: Backport pull request #15263 from jellyfin/release-10.11.z Resolve symlinks for static media source infos Original-merge: 3b2d64995aab63ebaa6832c059a3cc0bdebe90dc Merged-by: crobibero Backported-by: Bond_009 --- .../IO/ManagedFileSystem.cs | 3 +- .../Library/DotIgnoreIgnoreRule.cs | 3 +- .../Trickplay/TrickplayManager.cs | 4 +- MediaBrowser.Controller/Entities/BaseItem.cs | 12 +++- MediaBrowser.Controller/IO/FileSystemHelper.cs | 74 ++++++++++++++++++++++ 5 files changed, 91 insertions(+), 5 deletions(-) (limited to 'MediaBrowser.Controller/Entities') diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index 97e89ca3d..fad97344b 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Security; using Jellyfin.Extensions; using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.IO; using MediaBrowser.Model.IO; using Microsoft.Extensions.Logging; @@ -260,7 +261,7 @@ namespace Emby.Server.Implementations.IO { try { - var targetFileInfo = (FileInfo?)fileInfo.ResolveLinkTarget(returnFinalTarget: true); + var targetFileInfo = FileSystemHelper.ResolveLinkTarget(fileInfo, returnFinalTarget: true); if (targetFileInfo is not null) { result.Exists = targetFileInfo.Exists; diff --git a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs index 959acd475..e53502046 100644 --- a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs +++ b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs @@ -1,6 +1,7 @@ using System; using System.IO; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Resolvers; using MediaBrowser.Model.IO; @@ -92,7 +93,7 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule private static string GetFileContent(FileInfo dirIgnoreFile) { - dirIgnoreFile = (FileInfo?)dirIgnoreFile.ResolveLinkTarget(returnFinalTarget: true) ?? dirIgnoreFile; + dirIgnoreFile = FileSystemHelper.ResolveLinkTarget(dirIgnoreFile, returnFinalTarget: true) ?? dirIgnoreFile; if (!dirIgnoreFile.Exists) { return string.Empty; diff --git a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs index 6f2d2a107..4505a377c 100644 --- a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs +++ b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs @@ -254,10 +254,10 @@ public class TrickplayManager : ITrickplayManager } // We support video backdrops, but we should not generate trickplay images for them - var parentDirectory = Directory.GetParent(mediaPath); + var parentDirectory = Directory.GetParent(video.Path); if (parentDirectory is not null && string.Equals(parentDirectory.Name, "backdrops", StringComparison.OrdinalIgnoreCase)) { - _logger.LogDebug("Ignoring backdrop media found at {Path} for item {ItemID}", mediaPath, video.Id); + _logger.LogDebug("Ignoring backdrop media found at {Path} for item {ItemID}", video.Path, video.Id); return; } diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 4989f0f3f..3c46d53e5 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -24,6 +24,7 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaSegments; using MediaBrowser.Controller.Persistence; @@ -1127,6 +1128,15 @@ namespace MediaBrowser.Controller.Entities var protocol = item.PathProtocol; + // Resolve the item path so everywhere we use the media source it will always point to + // the correct path even if symlinks are in use. Calling ResolveLinkTarget on a non-link + // path will return null, so it's safe to check for all paths. + var itemPath = item.Path; + if (protocol is MediaProtocol.File && FileSystemHelper.ResolveLinkTarget(itemPath, returnFinalTarget: true) is { Exists: true } linkInfo) + { + itemPath = linkInfo.FullName; + } + var info = new MediaSourceInfo { Id = item.Id.ToString("N", CultureInfo.InvariantCulture), @@ -1134,7 +1144,7 @@ namespace MediaBrowser.Controller.Entities MediaStreams = MediaSourceManager.GetMediaStreams(item.Id), MediaAttachments = MediaSourceManager.GetMediaAttachments(item.Id), Name = GetMediaSourceName(item), - Path = enablePathSubstitution ? GetMappedPath(item, item.Path, protocol) : item.Path, + Path = enablePathSubstitution ? GetMappedPath(item, itemPath, protocol) : itemPath, RunTimeTicks = item.RunTimeTicks, Container = item.Container, Size = item.Size, diff --git a/MediaBrowser.Controller/IO/FileSystemHelper.cs b/MediaBrowser.Controller/IO/FileSystemHelper.cs index 1a33c3aa8..324aea7e3 100644 --- a/MediaBrowser.Controller/IO/FileSystemHelper.cs +++ b/MediaBrowser.Controller/IO/FileSystemHelper.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using MediaBrowser.Model.IO; @@ -61,4 +62,77 @@ public static class FileSystemHelper } } } + + /// + /// Gets the target of the specified file link. + /// + /// + /// This helper exists because of this upstream runtime issue; https://github.com/dotnet/runtime/issues/92128. + /// + /// The path of the file link. + /// true to follow links to the final target; false to return the immediate next link. + /// + /// A if the is a link, regardless of if the target exists; otherwise, null. + /// + public static FileInfo? ResolveLinkTarget(string linkPath, bool returnFinalTarget = false) + { + // Check if the file exists so the native resolve handler won't throw at us. + if (!File.Exists(linkPath)) + { + return null; + } + + if (!returnFinalTarget) + { + return File.ResolveLinkTarget(linkPath, returnFinalTarget: false) as FileInfo; + } + + if (File.ResolveLinkTarget(linkPath, returnFinalTarget: false) is not FileInfo targetInfo) + { + return null; + } + + var currentPath = targetInfo.FullName; + var visited = new HashSet(StringComparer.Ordinal) { linkPath, currentPath }; + while (File.ResolveLinkTarget(currentPath, returnFinalTarget: false) is FileInfo linkInfo) + { + var targetPath = linkInfo.FullName; + + // If an infinite loop is detected, return the file info for the + // first link in the loop we encountered. + if (!visited.Add(targetPath)) + { + return new FileInfo(targetPath); + } + + targetInfo = linkInfo; + currentPath = targetPath; + + // Exit if the target doesn't exist, so the native resolve handler won't throw at us. + if (!targetInfo.Exists) + { + break; + } + } + + return targetInfo; + } + + /// + /// Gets the target of the specified file link. + /// + /// + /// This helper exists because of this upstream runtime issue; https://github.com/dotnet/runtime/issues/92128. + /// + /// The file info of the file link. + /// true to follow links to the final target; false to return the immediate next link. + /// + /// A if the is a link, regardless of if the target exists; otherwise, null. + /// + public static FileInfo? ResolveLinkTarget(FileInfo fileInfo, bool returnFinalTarget = false) + { + ArgumentNullException.ThrowIfNull(fileInfo); + + return ResolveLinkTarget(fileInfo.FullName, returnFinalTarget); + } } -- cgit v1.2.3 From c805c5e2b15698cebb9ebd01a961932cc6c7f075 Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Mon, 17 Nov 2025 14:08:49 -0500 Subject: Backport pull request #15373 from jellyfin/release-10.11.z Fix collection grouping in mixed libraries Original-merge: 13c4517a66e3f857cc4acc9b2fa3505297d554eb Merged-by: crobibero Backported-by: Bond_009 --- MediaBrowser.Controller/Entities/Folder.cs | 73 +++++++++++++++++++++++------- 1 file changed, 56 insertions(+), 17 deletions(-) (limited to 'MediaBrowser.Controller/Entities') diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 03ee44708..ed2d201e3 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -1052,12 +1052,49 @@ namespace MediaBrowser.Controller.Entities { ArgumentNullException.ThrowIfNull(items); - if (CollapseBoxSetItems(query, queryParent, user, configurationManager)) + if (!CollapseBoxSetItems(query, queryParent, user, configurationManager)) { - items = collectionManager.CollapseItemsWithinBoxSets(items, user); + return items; } - return items; + var config = configurationManager.Configuration; + + bool collapseMovies = config.EnableGroupingMoviesIntoCollections; + bool collapseSeries = config.EnableGroupingShowsIntoCollections; + + if (user is null || (collapseMovies && collapseSeries)) + { + return collectionManager.CollapseItemsWithinBoxSets(items, user); + } + + if (!collapseMovies && !collapseSeries) + { + return items; + } + + var collapsibleItems = new List(); + var remainingItems = new List(); + + foreach (var item in items) + { + if ((collapseMovies && item is Movie) || (collapseSeries && item is Series)) + { + collapsibleItems.Add(item); + } + else + { + remainingItems.Add(item); + } + } + + if (collapsibleItems.Count == 0) + { + return remainingItems; + } + + var collapsedItems = collectionManager.CollapseItemsWithinBoxSets(collapsibleItems, user); + + return collapsedItems.Concat(remainingItems); } private static bool CollapseBoxSetItems( @@ -1088,24 +1125,26 @@ namespace MediaBrowser.Controller.Entities } var param = query.CollapseBoxSetItems; - - if (!param.HasValue) + if (param.HasValue) { - if (user is not null && query.IncludeItemTypes.Any(type => - (type == BaseItemKind.Movie && !configurationManager.Configuration.EnableGroupingMoviesIntoCollections) || - (type == BaseItemKind.Series && !configurationManager.Configuration.EnableGroupingShowsIntoCollections))) - { - return false; - } + return param.Value && AllowBoxSetCollapsing(query); + } - if (query.IncludeItemTypes.Length == 0 - || query.IncludeItemTypes.Any(type => type == BaseItemKind.Movie || type == BaseItemKind.Series)) - { - param = true; - } + var config = configurationManager.Configuration; + + bool queryHasMovies = query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(BaseItemKind.Movie); + bool queryHasSeries = query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(BaseItemKind.Series); + + bool collapseMovies = config.EnableGroupingMoviesIntoCollections; + bool collapseSeries = config.EnableGroupingShowsIntoCollections; + + if (user is not null) + { + bool canCollapse = (queryHasMovies && collapseMovies) || (queryHasSeries && collapseSeries); + return canCollapse && AllowBoxSetCollapsing(query); } - return param.HasValue && param.Value && AllowBoxSetCollapsing(query); + return (queryHasMovies || queryHasSeries) && AllowBoxSetCollapsing(query); } private static bool AllowBoxSetCollapsing(InternalItemsQuery request) -- cgit v1.2.3 From 7d05c875f3865d0baa98a9a3dcfd05842354a764 Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Mon, 17 Nov 2025 14:08:50 -0500 Subject: Backport pull request #15380 from jellyfin/release-10.11.z Fix item count display for collapsed items Original-merge: 8f71922734d42591b3236f4c52d9692f1b191da2 Merged-by: crobibero Backported-by: Bond_009 --- MediaBrowser.Controller/Entities/Folder.cs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) (limited to 'MediaBrowser.Controller/Entities') diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index ed2d201e3..151b957fe 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -729,9 +729,7 @@ namespace MediaBrowser.Controller.Entities query.StartIndex = startIndex; } - var result = PostFilterAndSort(items, query); - result.TotalRecordCount = totalCount; - return result; + return PostFilterAndSort(items, query); } if (this is not UserRootFolder @@ -1001,9 +999,7 @@ namespace MediaBrowser.Controller.Entities items = GetChildren(user, true, out totalItemCount, childQuery).Where(filter); } - var result = PostFilterAndSort(items, query); - result.TotalRecordCount = totalItemCount; - return result; + return PostFilterAndSort(items, query); } protected QueryResult PostFilterAndSort(IEnumerable items, InternalItemsQuery query) @@ -1039,7 +1035,15 @@ namespace MediaBrowser.Controller.Entities items = UserViewBuilder.FilterForAdjacency(items.ToList(), query.AdjacentTo.Value); } - return UserViewBuilder.SortAndPage(items, null, query, LibraryManager); + var filteredItems = items as IReadOnlyList ?? items.ToList(); + var result = UserViewBuilder.SortAndPage(filteredItems, null, query, LibraryManager); + + if (query.EnableTotalRecordCount) + { + result.TotalRecordCount = filteredItems.Count; + } + + return result; } private static IEnumerable CollapseBoxSetItemsIfNeeded( -- cgit v1.2.3 From 70dcf3f7b30df007661a252a3bb1790a6b56b263 Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Wed, 3 Dec 2025 14:04:25 -0500 Subject: Backport pull request #15594 from jellyfin/release-10.11.z Fix isMovie filter logic Original-merge: 94f3725208caa030910b62b798ad2f78608d6fd6 Merged-by: crobibero Backported-by: Bond_009 --- .../Item/BaseItemRepository.cs | 17 ++++++++--------- MediaBrowser.Controller/Entities/Folder.cs | 2 +- 2 files changed, 9 insertions(+), 10 deletions(-) (limited to 'MediaBrowser.Controller/Entities') diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index f4bb94349..84168291a 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -1653,19 +1653,18 @@ public sealed class BaseItemRepository var tags = filter.Tags.ToList(); var excludeTags = filter.ExcludeTags.ToList(); - if (filter.IsMovie == true) + if (filter.IsMovie.HasValue) { - if (filter.IncludeItemTypes.Length == 0 - || filter.IncludeItemTypes.Contains(BaseItemKind.Movie) - || filter.IncludeItemTypes.Contains(BaseItemKind.Trailer)) + var shouldIncludeAllMovieTypes = filter.IsMovie.Value + && (filter.IncludeItemTypes.Length == 0 + || filter.IncludeItemTypes.Contains(BaseItemKind.Movie) + || filter.IncludeItemTypes.Contains(BaseItemKind.Trailer)); + + if (!shouldIncludeAllMovieTypes) { - baseQuery = baseQuery.Where(e => e.IsMovie); + baseQuery = baseQuery.Where(e => e.IsMovie == filter.IsMovie.Value); } } - else if (filter.IsMovie.HasValue) - { - baseQuery = baseQuery.Where(e => e.IsMovie == filter.IsMovie); - } if (filter.IsSeries.HasValue) { diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 151b957fe..59a967725 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -1409,7 +1409,7 @@ namespace MediaBrowser.Controller.Entities if (this is BoxSet && (query.OrderBy is null || query.OrderBy.Count == 0)) { realChildren = realChildren - .OrderBy(e => e.ProductionYear ?? int.MaxValue) + .OrderBy(e => e.PremiereDate ?? DateTime.MaxValue) .ToArray(); } -- cgit v1.2.3 From 8b2a8b94b6361e31eff58078225cf78d8a6c3fb1 Mon Sep 17 00:00:00 2001 From: evan314159 <110177090+evan314159@users.noreply.github.com> Date: Tue, 9 Dec 2025 12:15:46 +0800 Subject: avoid Take(0) when limit == 0 (#14608) Co-authored-by: Evan --- .../Library/SearchEngine.cs | 2 +- Emby.Server.Implementations/TV/TVSeriesManager.cs | 2 +- .../Devices/DeviceManager.cs | 2 +- .../Item/BaseItemRepository.cs | 44 +++++++++------------- .../Entities/UserViewBuilder.cs | 2 +- src/Jellyfin.LiveTv/Channels/ChannelManager.cs | 9 ++--- src/Jellyfin.LiveTv/LiveTvManager.cs | 4 +- 7 files changed, 26 insertions(+), 39 deletions(-) (limited to 'MediaBrowser.Controller/Entities') diff --git a/Emby.Server.Implementations/Library/SearchEngine.cs b/Emby.Server.Implementations/Library/SearchEngine.cs index 9d81b835c..c68211859 100644 --- a/Emby.Server.Implementations/Library/SearchEngine.cs +++ b/Emby.Server.Implementations/Library/SearchEngine.cs @@ -42,7 +42,7 @@ namespace Emby.Server.Implementations.Library results = results.GetRange(query.StartIndex.Value, totalRecordCount - query.StartIndex.Value); } - if (query.Limit.HasValue) + if (query.Limit.HasValue && query.Limit.Value > 0) { results = results.GetRange(0, Math.Min(query.Limit.Value, results.Count)); } diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs index ee2e18f73..cd98dbe86 100644 --- a/Emby.Server.Implementations/TV/TVSeriesManager.cs +++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs @@ -266,7 +266,7 @@ namespace Emby.Server.Implementations.TV items = items.Skip(query.StartIndex.Value); } - if (query.Limit.HasValue) + if (query.Limit.HasValue && query.Limit.Value > 0) { items = items.Take(query.Limit.Value); } diff --git a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs index 51a118645..bcf348f8c 100644 --- a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs +++ b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs @@ -158,7 +158,7 @@ namespace Jellyfin.Server.Implementations.Devices devices = devices.Skip(query.Skip.Value); } - if (query.Limit.HasValue) + if (query.Limit.HasValue && query.Limit.Value > 0) { devices = devices.Take(query.Limit.Value); } diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 57d874e59..dfe46ef8f 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -250,7 +250,7 @@ public sealed class BaseItemRepository public QueryResult GetItems(InternalItemsQuery filter) { ArgumentNullException.ThrowIfNull(filter); - if (!filter.EnableTotalRecordCount || (!filter.Limit.HasValue && (filter.StartIndex ?? 0) == 0)) + if (!filter.EnableTotalRecordCount || ((filter.Limit ?? 0) == 0 && (filter.StartIndex ?? 0) == 0)) { var returnList = GetItemList(filter); return new QueryResult( @@ -326,7 +326,7 @@ public sealed class BaseItemRepository .OrderByDescending(g => g.MaxDateCreated) .Select(g => g); - if (filter.Limit.HasValue) + if (filter.Limit.HasValue && filter.Limit.Value > 0) { subqueryGrouped = subqueryGrouped.Take(filter.Limit.Value); } @@ -367,7 +367,7 @@ public sealed class BaseItemRepository .OrderByDescending(g => g.LastPlayedDate) .Select(g => g.Key!); - if (filter.Limit.HasValue) + if (filter.Limit.HasValue && filter.Limit.Value > 0) { query = query.Take(filter.Limit.Value); } @@ -425,19 +425,14 @@ public sealed class BaseItemRepository private IQueryable ApplyQueryPaging(IQueryable dbQuery, InternalItemsQuery filter) { - if (filter.Limit.HasValue || filter.StartIndex.HasValue) + if (filter.StartIndex.HasValue && filter.StartIndex.Value > 0) { - var offset = filter.StartIndex ?? 0; - - if (offset > 0) - { - dbQuery = dbQuery.Skip(offset); - } + dbQuery = dbQuery.Skip(filter.StartIndex.Value); + } - if (filter.Limit.HasValue) - { - dbQuery = dbQuery.Take(filter.Limit.Value); - } + if (filter.Limit.HasValue && filter.Limit.Value > 0) + { + dbQuery = dbQuery.Take(filter.Limit.Value); } return dbQuery; @@ -1190,7 +1185,7 @@ public sealed class BaseItemRepository { ArgumentNullException.ThrowIfNull(filter); - if (!filter.Limit.HasValue) + if (!(filter.Limit.HasValue && filter.Limit.Value > 0)) { filter.EnableTotalRecordCount = false; } @@ -1269,19 +1264,14 @@ public sealed class BaseItemRepository result.TotalRecordCount = query.Count(); } - if (filter.Limit.HasValue || filter.StartIndex.HasValue) + if (filter.StartIndex.HasValue && filter.StartIndex.Value > 0) { - var offset = filter.StartIndex ?? 0; - - if (offset > 0) - { - query = query.Skip(offset); - } + query = query.Skip(filter.StartIndex.Value); + } - if (filter.Limit.HasValue) - { - query = query.Take(filter.Limit.Value); - } + if (filter.Limit.HasValue && filter.Limit.Value > 0) + { + query = query.Take(filter.Limit.Value); } IQueryable? itemCountQuery = null; @@ -1362,7 +1352,7 @@ public sealed class BaseItemRepository private static void PrepareFilterQuery(InternalItemsQuery query) { - if (query.Limit.HasValue && query.EnableGroupByMetadataKey) + if (query.Limit.HasValue && query.Limit.Value > 0 && query.EnableGroupByMetadataKey) { query.Limit = query.Limit.Value + 4; } diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs index 4f9e9261b..bed7554b1 100644 --- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs +++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs @@ -455,7 +455,7 @@ namespace MediaBrowser.Controller.Entities var itemsArray = totalRecordLimit.HasValue ? items.Take(totalRecordLimit.Value).ToArray() : items.ToArray(); var totalCount = itemsArray.Length; - if (query.Limit.HasValue) + if (query.Limit.HasValue && query.Limit.Value > 0) { itemsArray = itemsArray.Skip(query.StartIndex ?? 0).Take(query.Limit.Value).ToArray(); } diff --git a/src/Jellyfin.LiveTv/Channels/ChannelManager.cs b/src/Jellyfin.LiveTv/Channels/ChannelManager.cs index 8ee129a57..2b8e5a0a0 100644 --- a/src/Jellyfin.LiveTv/Channels/ChannelManager.cs +++ b/src/Jellyfin.LiveTv/Channels/ChannelManager.cs @@ -240,12 +240,9 @@ namespace Jellyfin.LiveTv.Channels var all = channels; var totalCount = all.Count; - if (query.StartIndex.HasValue || query.Limit.HasValue) - { - int startIndex = query.StartIndex ?? 0; - int count = query.Limit is null ? totalCount - startIndex : Math.Min(query.Limit.Value, totalCount - startIndex); - all = all.GetRange(startIndex, count); - } + int startIndex = query.StartIndex ?? 0; + int count = (query.Limit ?? 0) > 0 ? Math.Min(query.Limit.Value, totalCount - startIndex) : totalCount - startIndex; + all = all.GetRange(query.StartIndex ?? 0, count); if (query.RefreshLatestChannelItems) { diff --git a/src/Jellyfin.LiveTv/LiveTvManager.cs b/src/Jellyfin.LiveTv/LiveTvManager.cs index 53bc6751f..1d18ade9d 100644 --- a/src/Jellyfin.LiveTv/LiveTvManager.cs +++ b/src/Jellyfin.LiveTv/LiveTvManager.cs @@ -287,7 +287,7 @@ namespace Jellyfin.LiveTv GenreIds = query.GenreIds }; - if (query.Limit.HasValue) + if (query.Limit.HasValue && query.Limit.Value > 0) { internalQuery.Limit = Math.Max(query.Limit.Value * 4, 200); } @@ -305,7 +305,7 @@ namespace Jellyfin.LiveTv IEnumerable programs = orderedPrograms; - if (query.Limit.HasValue) + if (query.Limit.HasValue && query.Limit.Value > 0) { programs = programs.Take(query.Limit.Value); } -- cgit v1.2.3 From 55570043759bcfa3c76df7e94d3303257171c970 Mon Sep 17 00:00:00 2001 From: gnattu Date: Sun, 28 Dec 2025 07:22:17 -0500 Subject: Backport pull request #15689 from jellyfin/release-10.11.z Use original name for MusicAritist matching Original-merge: 4c5a3fbff34a603ff0344e0b42d07bc17f31f92c Merged-by: crobibero Backported-by: Bond_009 --- Emby.Server.Implementations/Library/LibraryManager.cs | 1 + Jellyfin.Server.Implementations/Item/BaseItemRepository.cs | 11 +++++++++-- MediaBrowser.Controller/Entities/InternalItemsQuery.cs | 2 ++ 3 files changed, 12 insertions(+), 2 deletions(-) (limited to 'MediaBrowser.Controller/Entities') diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 30c3e89b4..f35d85f65 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -1058,6 +1058,7 @@ namespace Emby.Server.Implementations.Library { IncludeItemTypes = [BaseItemKind.MusicArtist], Name = name, + UseRawName = true, DtoOptions = options }).Cast() .OrderBy(i => i.IsAccessedByName ? 1 : 0) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 6b060430e..98072918c 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -1987,8 +1987,15 @@ public sealed class BaseItemRepository if (!string.IsNullOrWhiteSpace(filter.Name)) { - var cleanName = GetCleanValue(filter.Name); - baseQuery = baseQuery.Where(e => e.CleanName == cleanName); + if (filter.UseRawName == true) + { + baseQuery = baseQuery.Where(e => e.Name == filter.Name); + } + else + { + var cleanName = GetCleanValue(filter.Name); + baseQuery = baseQuery.Where(e => e.CleanName == cleanName); + } } // These are the same, for now diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs index b32b64f5d..076a59292 100644 --- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs @@ -125,6 +125,8 @@ namespace MediaBrowser.Controller.Entities public string? Name { get; set; } + public bool? UseRawName { get; set; } + public string? Person { get; set; } public Guid[] PersonIds { get; set; } -- cgit v1.2.3 From 252ab45473d0a3b5c2f45dee42ced5e6179c6028 Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Sun, 28 Dec 2025 07:22:25 -0500 Subject: Backport pull request #15767 from jellyfin/release-10.11.z Fix collections display order Original-merge: 22da5187c88a60118cac03bc77427efa72b97888 Merged-by: crobibero Backported-by: Bond_009 --- MediaBrowser.Controller/Entities/Folder.cs | 7 ------- MediaBrowser.Controller/Entities/Movies/BoxSet.cs | 8 +++++++- 2 files changed, 7 insertions(+), 8 deletions(-) (limited to 'MediaBrowser.Controller/Entities') diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 59a967725..d2a3290c4 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -1406,13 +1406,6 @@ namespace MediaBrowser.Controller.Entities .Where(e => query is null || UserViewBuilder.FilterItem(e, query)) .ToArray(); - if (this is BoxSet && (query.OrderBy is null || query.OrderBy.Count == 0)) - { - realChildren = realChildren - .OrderBy(e => e.PremiereDate ?? DateTime.MaxValue) - .ToArray(); - } - var childCount = realChildren.Length; if (result.Count < limit) { diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs index 1d1fb2c39..3999c3e07 100644 --- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs +++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs @@ -124,7 +124,7 @@ namespace MediaBrowser.Controller.Entities.Movies if (sortBy == ItemSortBy.Default) { - return items; + return items; } return LibraryManager.Sort(items, user, new[] { sortBy }, SortOrder.Ascending); @@ -136,6 +136,12 @@ namespace MediaBrowser.Controller.Entities.Movies return Sort(children, user).ToArray(); } + public override IReadOnlyList GetChildren(User user, bool includeLinkedChildren, out int totalItemCount, InternalItemsQuery query = null) + { + var children = base.GetChildren(user, includeLinkedChildren, out totalItemCount, query); + return Sort(children, user).ToArray(); + } + public override IReadOnlyList GetRecursiveChildren(User user, InternalItemsQuery query, out int totalCount) { var children = base.GetRecursiveChildren(user, query, out totalCount); -- cgit v1.2.3 From 928a8458dd62007abc0082f478432608e9153f6e Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Sun, 28 Dec 2025 07:22:28 -0500 Subject: Backport pull request #15786 from jellyfin/release-10.11.z Fix parental rating filtering with sub-scores Original-merge: 5804d6840c0276d3aef81bfec6af82e496672f01 Merged-by: crobibero Backported-by: Bond_009 --- MediaBrowser.Controller/Entities/BaseItem.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) (limited to 'MediaBrowser.Controller/Entities') diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 3c46d53e5..d9d2d0e3a 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1620,12 +1620,17 @@ namespace MediaBrowser.Controller.Entities return isAllowed; } - if (maxAllowedSubRating is not null) + if (!maxAllowedRating.HasValue) { - return (ratingScore.SubScore ?? 0) <= maxAllowedSubRating && ratingScore.Score <= maxAllowedRating.Value; + return true; + } + + if (ratingScore.Score != maxAllowedRating.Value) + { + return ratingScore.Score < maxAllowedRating.Value; } - return !maxAllowedRating.HasValue || ratingScore.Score <= maxAllowedRating.Value; + return !maxAllowedSubRating.HasValue || (ratingScore.SubScore ?? 0) <= maxAllowedSubRating.Value; } public ParentalRatingScore GetParentalRatingScore() -- cgit v1.2.3 From d270957c8254a24d8c073ad9d1d9316e950538ed Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Sat, 10 Jan 2026 06:11:26 -0500 Subject: Backport pull request #15950 from jellyfin/release-10.11.z Revert "always sort season by index number" Original-merge: 32d2414de0b3d119929c063714b6e4f0023893c7 Merged-by: Bond-009 Backported-by: Bond_009 --- MediaBrowser.Controller/Entities/TV/Series.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'MediaBrowser.Controller/Entities') diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs index 427c2995b..6396631f9 100644 --- a/MediaBrowser.Controller/Entities/TV/Series.cs +++ b/MediaBrowser.Controller/Entities/TV/Series.cs @@ -214,7 +214,7 @@ namespace MediaBrowser.Controller.Entities.TV query.AncestorWithPresentationUniqueKey = null; query.SeriesPresentationUniqueKey = seriesKey; query.IncludeItemTypes = new[] { BaseItemKind.Season }; - query.OrderBy = new[] { (ItemSortBy.IndexNumber, SortOrder.Ascending) }; + query.OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }; if (user is not null && !user.DisplayMissingEpisodes) { @@ -247,6 +247,10 @@ namespace MediaBrowser.Controller.Entities.TV query.AncestorWithPresentationUniqueKey = null; query.SeriesPresentationUniqueKey = seriesKey; + if (query.OrderBy.Count == 0) + { + query.OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }; + } if (query.IncludeItemTypes.Length == 0) { -- cgit v1.2.3 From 09edca8b7a9174c374a7d03bb1ec3aea32d02ffd Mon Sep 17 00:00:00 2001 From: MarcoCoreDuo <90222533+MarcoCoreDuo@users.noreply.github.com> Date: Sun, 18 Jan 2026 11:30:38 -0500 Subject: Backport pull request #15899 from jellyfin/release-10.11.z Fix watched state not kept on Media replace/rename Original-merge: 8433b6d8a41f66f6eef36bb950927c6a6afa1a36 Merged-by: joshuaboniface Backported-by: Bond_009 --- CONTRIBUTORS.md | 1 + .../Library/LibraryManager.cs | 6 ++++ .../Item/BaseItemRepository.cs | 37 ++++++++++++++-------- MediaBrowser.Controller/Entities/BaseItem.cs | 3 ++ MediaBrowser.Controller/Library/ILibraryManager.cs | 8 +++++ .../Persistence/IItemRepository.cs | 8 +++++ MediaBrowser.Providers/Manager/MetadataService.cs | 11 +++++-- 7 files changed, 57 insertions(+), 17 deletions(-) (limited to 'MediaBrowser.Controller/Entities') diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 171509382..1770db60b 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -210,6 +210,7 @@ - [bjorntp](https://github.com/bjorntp) - [martenumberto](https://github.com/martenumberto) - [ZeusCraft10](https://github.com/ZeusCraft10) + - [MarcoCoreDuo](https://github.com/MarcoCoreDuo) # Emby Contributors diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index f35d85f65..bdf04edc2 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -2202,6 +2202,12 @@ namespace Emby.Server.Implementations.Library public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken) => UpdateItemsAsync([item], parent, updateReason, cancellationToken); + /// + public async Task ReattachUserDataAsync(BaseItem item, CancellationToken cancellationToken) + { + await _itemRepository.ReattachUserDataAsync(item, cancellationToken).ConfigureAwait(false); + } + public async Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason) { if (item.IsFileProtocol) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 3b3d3c4f4..646a9c483 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -624,7 +624,6 @@ public sealed class BaseItemRepository var ids = tuples.Select(f => f.Item.Id).ToArray(); var existingItems = context.BaseItems.Where(e => ids.Contains(e.Id)).Select(f => f.Id).ToArray(); - var newItems = tuples.Where(e => !existingItems.Contains(e.Item.Id)).ToArray(); foreach (var item in tuples) { @@ -658,19 +657,6 @@ public sealed class BaseItemRepository context.SaveChanges(); - foreach (var item in newItems) - { - // reattach old userData entries - var userKeys = item.UserDataKey.ToArray(); - var retentionDate = (DateTime?)null; - context.UserData - .Where(e => e.ItemId == PlaceholderId) - .Where(e => userKeys.Contains(e.CustomDataKey)) - .ExecuteUpdate(e => e - .SetProperty(f => f.ItemId, item.Item.Id) - .SetProperty(f => f.RetentionDate, retentionDate)); - } - var itemValueMaps = tuples .Select(e => (e.Item, Values: GetItemValuesToSave(e.Item, e.InheritedTags))) .ToArray(); @@ -766,6 +752,29 @@ public sealed class BaseItemRepository transaction.Commit(); } + /// + public async Task ReattachUserDataAsync(BaseItemDto item, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(item); + cancellationToken.ThrowIfCancellationRequested(); + + var dbContext = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); + + await using (dbContext.ConfigureAwait(false)) + { + var userKeys = item.GetUserDataKeys().ToArray(); + var retentionDate = (DateTime?)null; + await dbContext.UserData + .Where(e => e.ItemId == PlaceholderId) + .Where(e => userKeys.Contains(e.CustomDataKey)) + .ExecuteUpdateAsync( + e => e + .SetProperty(f => f.ItemId, item.Id) + .SetProperty(f => f.RetentionDate, retentionDate), + cancellationToken).ConfigureAwait(false); + } + } + /// public BaseItemDto? RetrieveItem(Guid id) { diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index d9d2d0e3a..7586b99e7 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -2053,6 +2053,9 @@ namespace MediaBrowser.Controller.Entities public virtual async Task UpdateToRepositoryAsync(ItemUpdateType updateReason, CancellationToken cancellationToken) => await LibraryManager.UpdateItemAsync(this, GetParent(), updateReason, cancellationToken).ConfigureAwait(false); + public async Task ReattachUserDataAsync(CancellationToken cancellationToken) => + await LibraryManager.ReattachUserDataAsync(this, cancellationToken).ConfigureAwait(false); + /// /// Validates that images within the item are still on the filesystem. /// diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index fcc5ed672..675812ac2 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -281,6 +281,14 @@ namespace MediaBrowser.Controller.Library /// Returns a Task that can be awaited. Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken); + /// + /// Reattaches the user data to the item. + /// + /// The item. + /// The cancellation token. + /// A task that represents the asynchronous reattachment operation. + Task ReattachUserDataAsync(BaseItem item, CancellationToken cancellationToken); + /// /// Retrieves the item. /// diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs index 00c492742..bf80b7d0a 100644 --- a/MediaBrowser.Controller/Persistence/IItemRepository.cs +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -35,6 +35,14 @@ public interface IItemRepository Task SaveImagesAsync(BaseItem item, CancellationToken cancellationToken = default); + /// + /// Reattaches the user data to the item. + /// + /// The item. + /// The cancellation token. + /// A task that represents the asynchronous reattachment operation. + Task ReattachUserDataAsync(BaseItem item, CancellationToken cancellationToken); + /// /// Retrieves the item. /// diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index a2102ca9c..e9cb46eab 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -153,7 +153,7 @@ namespace MediaBrowser.Providers.Manager if (isFirstRefresh) { - await SaveItemAsync(metadataResult, ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false); + await SaveItemAsync(metadataResult, ItemUpdateType.MetadataImport, false, cancellationToken).ConfigureAwait(false); } // Next run metadata providers @@ -247,7 +247,7 @@ namespace MediaBrowser.Providers.Manager } // Save to database - await SaveItemAsync(metadataResult, updateType, cancellationToken).ConfigureAwait(false); + await SaveItemAsync(metadataResult, updateType, isFirstRefresh, cancellationToken).ConfigureAwait(false); } return updateType; @@ -275,9 +275,14 @@ namespace MediaBrowser.Providers.Manager } } - protected async Task SaveItemAsync(MetadataResult result, ItemUpdateType reason, CancellationToken cancellationToken) + protected async Task SaveItemAsync(MetadataResult result, ItemUpdateType reason, bool reattachUserData, CancellationToken cancellationToken) { await result.Item.UpdateToRepositoryAsync(reason, cancellationToken).ConfigureAwait(false); + if (reattachUserData) + { + await result.Item.ReattachUserDataAsync(cancellationToken).ConfigureAwait(false); + } + if (result.Item.SupportsPeople && result.People is not null) { var baseItem = result.Item; -- cgit v1.2.3 From a37e83d448598cfd06fee7f52ee7130248d6bac3 Mon Sep 17 00:00:00 2001 From: dfederm Date: Sat, 14 Feb 2026 05:57:25 -0500 Subject: Backport pull request #16227 from jellyfin/release-10.11.z Reattach user data after item removal during library scan Original-merge: be712956932a9337f0706fd8ef68eb53feb3f4ff Merged-by: Bond-009 Backported-by: Bond_009 --- MediaBrowser.Controller/Entities/Folder.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) (limited to 'MediaBrowser.Controller/Entities') diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index d2a3290c4..2ecb6cbdf 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -452,6 +452,7 @@ namespace MediaBrowser.Controller.Entities // That's all the new and changed ones - now see if any have been removed and need cleanup var itemsRemoved = currentChildren.Values.Except(validChildren).ToList(); var shouldRemove = !IsRoot || allowRemoveRoot; + var actuallyRemoved = new List(); // If it's an AggregateFolder, don't remove if (shouldRemove && itemsRemoved.Count > 0) { @@ -467,6 +468,7 @@ namespace MediaBrowser.Controller.Entities { Logger.LogDebug("Removed item: {Path}", item.Path); + actuallyRemoved.Add(item); item.SetParent(null); LibraryManager.DeleteItem(item, new DeleteOptions { DeleteFileLocation = false }, this, false); } @@ -477,6 +479,20 @@ namespace MediaBrowser.Controller.Entities { LibraryManager.CreateItems(newItems, this, cancellationToken); } + + // After removing items, reattach any detached user data to remaining children + // that share the same user data keys (eg. same episode replaced with a new file). + if (actuallyRemoved.Count > 0) + { + var removedKeys = actuallyRemoved.SelectMany(i => i.GetUserDataKeys()).ToHashSet(); + foreach (var child in validChildren) + { + if (child.GetUserDataKeys().Any(removedKeys.Contains)) + { + await child.ReattachUserDataAsync(cancellationToken).ConfigureAwait(false); + } + } + } } else { -- cgit v1.2.3