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)