From 5996c4afce11249804d24f1caa3a99b390543c4d Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Sat, 17 Jan 2026 17:10:07 +0100 Subject: Complete LinkedChildren integration and batch DTO optimizations This commit integrates remaining performance changes: - Add batch user data fetching in DtoService to reduce N+1 queries - Add GetNextUpEpisodesBatch in TVSeriesManager for efficient batch retrieval - Update Video/Movie/BoxSet to use LibraryManager for alternate versions - Transition LinkedChild to use ItemId instead of Path (obsolete Path/LibraryItemId) - Update providers and controllers for LinkedChildren-based references - Add NextUpEpisodeBatchResult for batched episode queries - Integrate IDescendantQueryProvider in SqliteDatabaseProvider --- MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs') diff --git a/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs b/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs index eccf8a606d..bdc5b5df29 100644 --- a/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs +++ b/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs @@ -70,7 +70,7 @@ public class BoxSetMetadataService : MetadataService if (mergeMetadataSettings) { // TODO: Change to only replace when currently empty or requested. This is currently not done because the metadata service is not handling attaching collection items based on the provider responses - targetItem.LinkedChildren = sourceItem.LinkedChildren.Concat(targetItem.LinkedChildren).DistinctBy(i => i.Path).ToArray(); + targetItem.LinkedChildren = sourceItem.LinkedChildren.Concat(targetItem.LinkedChildren).DistinctBy(i => i.ItemId).ToArray(); } } -- cgit v1.2.3 From 98d7c8d59fa3180e50ee311dfc53164325210896 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Sat, 7 Feb 2026 08:44:42 +0100 Subject: Make sure we deduplicate LinkedChildren --- .../Item/BaseItemRepository.cs | 16 ++++++++++++++-- .../Routines/RemoveDuplicatePlaylistChildren.cs | 11 +++++++---- MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs | 7 ++++++- .../Playlists/PlaylistMetadataService.cs | 7 ++++++- 4 files changed, 33 insertions(+), 8 deletions(-) (limited to 'MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs') diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 20df42583d..3ba6750045 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -1553,7 +1553,13 @@ public sealed class BaseItemRepository } } - var childIdsToCheck = resolvedChildren.Select(c => c.ChildId).Distinct().ToList(); + // Deduplicate by ChildId, keeping the last occurrence (for playlist ordering) + resolvedChildren = resolvedChildren + .GroupBy(c => c.ChildId) + .Select(g => g.Last()) + .ToList(); + + var childIdsToCheck = resolvedChildren.Select(c => c.ChildId).ToList(); var existingChildIds = childIdsToCheck.Count > 0 ? context.BaseItems .Where(e => childIdsToCheck.Contains(e.Id)) @@ -1646,8 +1652,14 @@ public sealed class BaseItemRepository } } + // Deduplicate by ChildId, keeping the last occurrence + newLinkedChildren = newLinkedChildren + .GroupBy(c => c.ChildId) + .Select(g => g.Last()) + .ToList(); + // Validate that all child items exist - var childIdsToCheck = newLinkedChildren.Select(c => c.ChildId).Distinct().ToList(); + var childIdsToCheck = newLinkedChildren.Select(c => c.ChildId).ToList(); var existingChildIds = childIdsToCheck.Count > 0 ? context.BaseItems .Where(e => childIdsToCheck.Contains(e.Id)) diff --git a/Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs b/Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs index 23f212424b..1545ebdc8e 100644 --- a/Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs +++ b/Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs @@ -45,10 +45,13 @@ internal class RemoveDuplicatePlaylistChildren : IMigrationRoutine var linkedChildren = playlist.LinkedChildren; if (linkedChildren.Length > 0) { - var nullItemChildren = linkedChildren.Where(c => c.ItemId is null); - var deduplicatedChildren = linkedChildren.DistinctBy(c => c.ItemId); - var newLinkedChildren = nullItemChildren.Concat(deduplicatedChildren); - playlist.LinkedChildren = linkedChildren; + var newLinkedChildren = linkedChildren + .Where(c => c.ItemId is null || c.ItemId.Value.Equals(Guid.Empty)) + .Concat(linkedChildren + .Where(c => c.ItemId.HasValue && !c.ItemId.Value.Equals(Guid.Empty)) + .DistinctBy(c => c.ItemId)) + .ToArray(); + playlist.LinkedChildren = newLinkedChildren; playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult(); _playlistManager.SavePlaylistFile(playlist); } diff --git a/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs b/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs index bdc5b5df29..3a872f687c 100644 --- a/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs +++ b/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using MediaBrowser.Controller.Configuration; @@ -70,7 +71,11 @@ public class BoxSetMetadataService : MetadataService if (mergeMetadataSettings) { // TODO: Change to only replace when currently empty or requested. This is currently not done because the metadata service is not handling attaching collection items based on the provider responses - targetItem.LinkedChildren = sourceItem.LinkedChildren.Concat(targetItem.LinkedChildren).DistinctBy(i => i.ItemId).ToArray(); +#pragma warning disable CS0618 // Type or member is obsolete - fallback for legacy path-based dedup + targetItem.LinkedChildren = sourceItem.LinkedChildren.Concat(targetItem.LinkedChildren) + .DistinctBy(i => i.ItemId.HasValue && !i.ItemId.Value.Equals(Guid.Empty) ? i.ItemId.Value.ToString() : i.Path ?? string.Empty) + .ToArray(); +#pragma warning restore CS0618 } } diff --git a/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs b/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs index 45b61319b7..429830c70c 100644 --- a/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs +++ b/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using MediaBrowser.Controller.Configuration; @@ -72,7 +73,11 @@ public class PlaylistMetadataService : MetadataService } else { - targetItem.LinkedChildren = sourceItem.LinkedChildren.Concat(targetItem.LinkedChildren).DistinctBy(i => i.ItemId).ToArray(); +#pragma warning disable CS0618 // Type or member is obsolete - fallback for legacy path-based dedup + targetItem.LinkedChildren = sourceItem.LinkedChildren.Concat(targetItem.LinkedChildren) + .DistinctBy(i => i.ItemId.HasValue && !i.ItemId.Value.Equals(Guid.Empty) ? i.ItemId.Value.ToString() : i.Path ?? string.Empty) + .ToArray(); +#pragma warning restore CS0618 } if (replaceData || targetItem.Shares.Count == 0) -- cgit v1.2.3 From f5d966fcc3eb3c5f8aa39a22caa8cf615029e1c5 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Sat, 7 Feb 2026 09:37:13 +0100 Subject: Remove Collection and Playlist cleanup task --- .../Localization/Core/en-US.json | 4 +- .../Tasks/CleanupCollectionAndPlaylistPathsTask.cs | 145 --------------------- .../BoxSets/BoxSetMetadataService.cs | 14 +- .../Playlists/PlaylistMetadataService.cs | 23 ++-- 4 files changed, 26 insertions(+), 160 deletions(-) delete mode 100644 Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs (limited to 'MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs') diff --git a/Emby.Server.Implementations/Localization/Core/en-US.json b/Emby.Server.Implementations/Localization/Core/en-US.json index c09d5af96c..c40448151c 100644 --- a/Emby.Server.Implementations/Localization/Core/en-US.json +++ b/Emby.Server.Implementations/Localization/Core/en-US.json @@ -130,9 +130,7 @@ "TaskOptimizeDatabaseDescription": "Compacts database and truncates free space. Running this task after scanning the library or doing other changes that imply database modifications might improve performance.", "TaskKeyframeExtractor": "Keyframe Extractor", "TaskKeyframeExtractorDescription": "Extracts keyframes from video files to create more precise HLS playlists. This task may run for a long time.", - "TaskCleanCollectionsAndPlaylists": "Clean up collections and playlists", - "TaskCleanCollectionsAndPlaylistsDescription": "Removes items from collections and playlists that no longer exist.", - "TaskExtractMediaSegments": "Media Segment Scan", +"TaskExtractMediaSegments": "Media Segment Scan", "TaskExtractMediaSegmentsDescription": "Extracts or obtains media segments from MediaSegment enabled plugins.", "TaskMoveTrickplayImages": "Migrate Trickplay Image Location", "TaskMoveTrickplayImagesDescription": "Moves existing trickplay files according to the library settings.", diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs deleted file mode 100644 index 1c2038d839..0000000000 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs +++ /dev/null @@ -1,145 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Jellyfin.Extensions; -using MediaBrowser.Controller.Collections; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.Movies; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Playlists; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.Tasks; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.ScheduledTasks.Tasks; - -/// -/// Deletes path references from collections and playlists that no longer exists. -/// -public class CleanupCollectionAndPlaylistPathsTask : IScheduledTask -{ - private readonly ILocalizationManager _localization; - private readonly ICollectionManager _collectionManager; - private readonly IPlaylistManager _playlistManager; - private readonly ILogger _logger; - private readonly IProviderManager _providerManager; - private readonly ILibraryManager _libraryManager; - - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - public CleanupCollectionAndPlaylistPathsTask( - ILocalizationManager localization, - ICollectionManager collectionManager, - IPlaylistManager playlistManager, - ILogger logger, - IProviderManager providerManager, - ILibraryManager libraryManager) - { - _localization = localization; - _collectionManager = collectionManager; - _playlistManager = playlistManager; - _logger = logger; - _providerManager = providerManager; - _libraryManager = libraryManager; - } - - /// - public string Name => _localization.GetLocalizedString("TaskCleanCollectionsAndPlaylists"); - - /// - public string Key => "CleanCollectionsAndPlaylists"; - - /// - public string Description => _localization.GetLocalizedString("TaskCleanCollectionsAndPlaylistsDescription"); - - /// - public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory"); - - /// - public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken) - { - var collectionsFolder = await _collectionManager.GetCollectionsFolder(false).ConfigureAwait(false); - if (collectionsFolder is null) - { - _logger.LogDebug("There is no collections folder to be found"); - } - else - { - var collections = collectionsFolder.Children.OfType().ToArray(); - _logger.LogDebug("Found {CollectionLength} boxsets", collections.Length); - - for (var index = 0; index < collections.Length; index++) - { - var collection = collections[index]; - _logger.LogDebug("Checking boxset {CollectionName}", collection.Name); - - await CleanupLinkedChildrenAsync(collection, cancellationToken).ConfigureAwait(false); - progress.Report(50D / collections.Length * (index + 1)); - } - } - - var playlistsFolder = _playlistManager.GetPlaylistsFolder(); - if (playlistsFolder is null) - { - _logger.LogDebug("There is no playlists folder to be found"); - return; - } - - var playlists = playlistsFolder.Children.OfType().ToArray(); - _logger.LogDebug("Found {PlaylistLength} playlists", playlists.Length); - - for (var index = 0; index < playlists.Length; index++) - { - var playlist = playlists[index]; - _logger.LogDebug("Checking playlist {PlaylistName}", playlist.Name); - - await CleanupLinkedChildrenAsync(playlist, cancellationToken).ConfigureAwait(false); - progress.Report(50D / playlists.Length * (index + 1)); - } - } - - private async Task CleanupLinkedChildrenAsync(T folder, CancellationToken cancellationToken) - where T : Folder - { - List? itemsToRemove = null; - foreach (var linkedChild in folder.LinkedChildren) - { - if (linkedChild.ItemId.HasValue - && !linkedChild.ItemId.Value.IsEmpty() - && _libraryManager.GetItemById(linkedChild.ItemId.Value) is not null) - { - continue; - } - - _logger.LogInformation("Item in {FolderName} with ItemId {ItemId} no longer exists in library", folder.Name, linkedChild.ItemId); - (itemsToRemove ??= []).Add(linkedChild); - } - - if (itemsToRemove is not null) - { - _logger.LogDebug("Updating {FolderName}", folder.Name); - folder.LinkedChildren = folder.LinkedChildren.Except(itemsToRemove).ToArray(); - await _providerManager.SaveMetadataAsync(folder, ItemUpdateType.MetadataEdit).ConfigureAwait(false); - await folder.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false); - } - } - - /// - public IEnumerable GetDefaultTriggers() - { - yield return new TaskTriggerInfo - { - Type = TaskTriggerInfoType.StartupTrigger, - }; - } -} diff --git a/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs b/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs index 3a872f687c..5f80151dd3 100644 --- a/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs +++ b/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs @@ -70,12 +70,18 @@ public class BoxSetMetadataService : MetadataService if (mergeMetadataSettings) { - // TODO: Change to only replace when currently empty or requested. This is currently not done because the metadata service is not handling attaching collection items based on the provider responses + // Only merge LinkedChildren from metadata for external collections (not managed by Jellyfin). + // For internal collections, the database LinkedChildren table is the source of truth. + var targetPath = targetItem.Path; + if (!string.IsNullOrEmpty(targetPath) + && !FileSystem.ContainsSubPath(ServerConfigurationManager.ApplicationPaths.DataPath, targetPath)) + { #pragma warning disable CS0618 // Type or member is obsolete - fallback for legacy path-based dedup - targetItem.LinkedChildren = sourceItem.LinkedChildren.Concat(targetItem.LinkedChildren) - .DistinctBy(i => i.ItemId.HasValue && !i.ItemId.Value.Equals(Guid.Empty) ? i.ItemId.Value.ToString() : i.Path ?? string.Empty) - .ToArray(); + targetItem.LinkedChildren = sourceItem.LinkedChildren.Concat(targetItem.LinkedChildren) + .DistinctBy(i => i.ItemId.HasValue && !i.ItemId.Value.Equals(Guid.Empty) ? i.ItemId.Value.ToString() : i.Path ?? string.Empty) + .ToArray(); #pragma warning restore CS0618 + } } } diff --git a/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs b/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs index 429830c70c..0438bc7c95 100644 --- a/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs +++ b/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs @@ -67,17 +67,24 @@ public class PlaylistMetadataService : MetadataService { targetItem.PlaylistMediaType = sourceItem.PlaylistMediaType; - if (replaceData || targetItem.LinkedChildren.Length == 0) - { - targetItem.LinkedChildren = sourceItem.LinkedChildren; - } - else + // Only merge LinkedChildren from metadata for external playlists (not managed by Jellyfin). + // For internal playlists, the database LinkedChildren table is the source of truth. + var targetPath = targetItem.Path; + if (!string.IsNullOrEmpty(targetPath) + && !FileSystem.ContainsSubPath(ServerConfigurationManager.ApplicationPaths.DataPath, targetPath)) { + if (replaceData || targetItem.LinkedChildren.Length == 0) + { + targetItem.LinkedChildren = sourceItem.LinkedChildren; + } + else + { #pragma warning disable CS0618 // Type or member is obsolete - fallback for legacy path-based dedup - targetItem.LinkedChildren = sourceItem.LinkedChildren.Concat(targetItem.LinkedChildren) - .DistinctBy(i => i.ItemId.HasValue && !i.ItemId.Value.Equals(Guid.Empty) ? i.ItemId.Value.ToString() : i.Path ?? string.Empty) - .ToArray(); + targetItem.LinkedChildren = sourceItem.LinkedChildren.Concat(targetItem.LinkedChildren) + .DistinctBy(i => i.ItemId.HasValue && !i.ItemId.Value.Equals(Guid.Empty) ? i.ItemId.Value.ToString() : i.Path ?? string.Empty) + .ToArray(); #pragma warning restore CS0618 + } } if (replaceData || targetItem.Shares.Count == 0) -- cgit v1.2.3