From 2d0d497961a7ca990384bd180f381824d5591cb8 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 25 Feb 2026 21:03:46 +0100 Subject: [PATCH] Update saved metadata on primary change --- .../Library/LibraryManager.cs | 25 ++++++++++++++++--- Jellyfin.Api/Controllers/VideosController.cs | 2 +- .../Item/BaseItemRepository.cs | 18 ++++++++++--- MediaBrowser.Controller/Entities/Folder.cs | 2 +- .../Library/ILibraryManager.cs | 4 +-- .../Persistence/IItemRepository.cs | 4 +-- 6 files changed, 42 insertions(+), 13 deletions(-) diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index c0782c8f9b..144238e3e0 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -441,7 +441,7 @@ namespace Emby.Server.Implementations.Library newPrimary.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult(); // Re-route playlist/collection references from deleted primary to new primary - _itemRepository.RerouteLinkedChildren(video.Id, newPrimary.Id); + RerouteLinkedChildReferencesAsync(video.Id, newPrimary.Id).GetAwaiter().GetResult(); // Update remaining alternates to point to new primary foreach (var alternate in alternateVersions.Skip(1)) @@ -455,7 +455,7 @@ namespace Emby.Server.Implementations.Library else if (item is Video alternateVideo && alternateVideo.PrimaryVersionId.HasValue) { // If deleting an alternate version, re-route references to its primary - _itemRepository.RerouteLinkedChildren(alternateVideo.Id, alternateVideo.PrimaryVersionId.Value); + RerouteLinkedChildReferencesAsync(alternateVideo.Id, alternateVideo.PrimaryVersionId.Value).GetAwaiter().GetResult(); } var children = item.IsFolder @@ -3655,9 +3655,26 @@ namespace Emby.Server.Implementations.Library } /// - public int RerouteLinkedChildReferences(Guid fromChildId, Guid toChildId) + public async Task RerouteLinkedChildReferencesAsync(Guid fromChildId, Guid toChildId) { - return _itemRepository.RerouteLinkedChildren(fromChildId, toChildId); + var affectedParentIds = _itemRepository.RerouteLinkedChildren(fromChildId, toChildId); + + // Update in-memory LinkedChildren and re-save metadata (NFO) for affected parents + foreach (var parentId in affectedParentIds) + { + if (GetItemById(parentId) is Folder parent) + { + foreach (var lc in parent.LinkedChildren) + { + if (lc.ItemId.HasValue && lc.ItemId.Value.Equals(fromChildId)) + { + lc.ItemId = toChildId; + } + } + + await RunMetadataSavers(parent, ItemUpdateType.MetadataEdit).ConfigureAwait(false); + } + } } /// diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index 155871770a..2161391105 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -223,7 +223,7 @@ public class VideosController : BaseJellyfinApiController await item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); // Re-route any playlist/collection references from this item to the primary - _libraryManager.RerouteLinkedChildReferences(item.Id, primaryVersion.Id); + await _libraryManager.RerouteLinkedChildReferencesAsync(item.Id, primaryVersion.Id).ConfigureAwait(false); if (!alternateVersionsOfPrimary.Any(i => i.ItemId.HasValue && i.ItemId.Value.Equals(item.Id))) { diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index dea4e04108..6e80d38d7d 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -4439,10 +4439,22 @@ public sealed class BaseItemRepository } /// - public int RerouteLinkedChildren(Guid fromChildId, Guid toChildId) + public IReadOnlyList RerouteLinkedChildren(Guid fromChildId, Guid toChildId) { using var context = _dbProvider.CreateDbContext(); + // Collect all affected parent IDs before modifying + var affectedParentIds = context.LinkedChildren + .Where(lc => lc.ChildId == fromChildId && lc.ChildType == DbLinkedChildType.Manual) + .Select(lc => lc.ParentId) + .Distinct() + .ToList(); + + if (affectedParentIds.Count == 0) + { + return affectedParentIds; + } + // Get parents that already reference toChildId (to avoid duplicates) var parentsWithTarget = context.LinkedChildren .Where(lc => lc.ChildId == toChildId && lc.ChildType == DbLinkedChildType.Manual) @@ -4450,7 +4462,7 @@ public sealed class BaseItemRepository .ToHashSet(); // Update references that won't create duplicates - var updated = context.LinkedChildren + context.LinkedChildren .Where(lc => lc.ChildId == fromChildId && lc.ChildType == DbLinkedChildType.Manual && !parentsWithTarget.Contains(lc.ParentId)) @@ -4463,7 +4475,7 @@ public sealed class BaseItemRepository && parentsWithTarget.Contains(lc.ParentId)) .ExecuteDelete(); - return updated; + return affectedParentIds; } /// diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 3428d1fc12..dce5664672 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -590,7 +590,7 @@ namespace MediaBrowser.Controller.Entities await oldPrimary.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false); // Re-route playlist/collection references from old primary to new primary - LibraryManager.RerouteLinkedChildReferences(oldPrimary.Id, newPrimary.Id); + await LibraryManager.RerouteLinkedChildReferencesAsync(oldPrimary.Id, newPrimary.Id).ConfigureAwait(false); } // After removing items, reattach any detached user data to remaining children diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index 5cfb940891..bcabfff772 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -767,8 +767,8 @@ namespace MediaBrowser.Controller.Library /// /// The child ID to re-route from. /// The child ID to re-route to. - /// Number of references updated. - int RerouteLinkedChildReferences(Guid fromChildId, Guid toChildId); + /// A representing the asynchronous operation. + Task RerouteLinkedChildReferencesAsync(Guid fromChildId, Guid toChildId); /// /// Gets legacy query filters for filtering UI. diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs index bcbbcc4785..52250b4058 100644 --- a/MediaBrowser.Controller/Persistence/IItemRepository.cs +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -254,8 +254,8 @@ public interface IItemRepository /// /// The child ID to re-route from. /// The child ID to re-route to. - /// Number of references updated. - int RerouteLinkedChildren(Guid fromChildId, Guid toChildId); + /// List of parent item IDs whose LinkedChildren were modified. + IReadOnlyList RerouteLinkedChildren(Guid fromChildId, Guid toChildId); /// /// Creates or updates a LinkedChild entry linking a parent to a child item.