aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs15
-rw-r--r--Jellyfin.Api/Controllers/VideosController.cs3
-rw-r--r--Jellyfin.Server.Implementations/Item/BaseItemRepository.cs39
-rw-r--r--MediaBrowser.Controller/Library/ILibraryManager.cs9
-rw-r--r--MediaBrowser.Controller/Persistence/IItemRepository.cs17
5 files changed, 83 insertions, 0 deletions
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index 2acfd68c36..830c918541 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -428,6 +428,9 @@ namespace Emby.Server.Implementations.Library
newPrimary.SetPrimaryVersionId(null);
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);
+
// Update remaining alternates to point to new primary
foreach (var alternate in alternateVersions.Skip(1))
{
@@ -436,6 +439,12 @@ namespace Emby.Server.Implementations.Library
}
}
}
+ else if (item is Video alternateVideo && !string.IsNullOrEmpty(alternateVideo.PrimaryVersionId)
+ && Guid.TryParse(alternateVideo.PrimaryVersionId, out var primaryId))
+ {
+ // If deleting an alternate version, re-route references to its primary
+ _itemRepository.RerouteLinkedChildren(alternateVideo.Id, primaryId);
+ }
var children = item.IsFolder
? ((Folder)item).GetRecursiveChildren(false)
@@ -3480,5 +3489,11 @@ namespace Emby.Server.Implementations.Library
_fileSystem.CreateShortcut(lnk, _appHost.ReverseVirtualPath(path));
RemoveContentTypeOverrides(path);
}
+
+ /// <inheritdoc />
+ public int RerouteLinkedChildReferences(Guid fromChildId, Guid toChildId)
+ {
+ return _itemRepository.RerouteLinkedChildren(fromChildId, toChildId);
+ }
}
}
diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs
index 2237e36b5f..1c8b695458 100644
--- a/Jellyfin.Api/Controllers/VideosController.cs
+++ b/Jellyfin.Api/Controllers/VideosController.cs
@@ -222,6 +222,9 @@ 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);
+
if (!alternateVersionsOfPrimary.Any(i => i.ItemId.HasValue && i.ItemId.Value.Equals(item.Id)))
{
alternateVersionsOfPrimary.Add(new LinkedChild
diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
index 9e6b100b66..22178e57f7 100644
--- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
+++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
@@ -3843,4 +3843,43 @@ public sealed class BaseItemRepository
return result;
}
+
+ /// <inheritdoc/>
+ public IReadOnlyList<Guid> GetManualLinkedParentIds(Guid childId)
+ {
+ using var context = _dbProvider.CreateDbContext();
+ return context.LinkedChildren
+ .Where(lc => lc.ChildId == childId && lc.ChildType == DbLinkedChildType.Manual)
+ .Select(lc => lc.ParentId)
+ .Distinct()
+ .ToList();
+ }
+
+ /// <inheritdoc/>
+ public int RerouteLinkedChildren(Guid fromChildId, Guid toChildId)
+ {
+ using var context = _dbProvider.CreateDbContext();
+
+ // Get parents that already reference toChildId (to avoid duplicates)
+ var parentsWithTarget = context.LinkedChildren
+ .Where(lc => lc.ChildId == toChildId && lc.ChildType == DbLinkedChildType.Manual)
+ .Select(lc => lc.ParentId)
+ .ToHashSet();
+
+ // Update references that won't create duplicates
+ var updated = context.LinkedChildren
+ .Where(lc => lc.ChildId == fromChildId
+ && lc.ChildType == DbLinkedChildType.Manual
+ && !parentsWithTarget.Contains(lc.ParentId))
+ .ExecuteUpdate(s => s.SetProperty(e => e.ChildId, toChildId));
+
+ // Remove references that would be duplicates
+ context.LinkedChildren
+ .Where(lc => lc.ChildId == fromChildId
+ && lc.ChildType == DbLinkedChildType.Manual
+ && parentsWithTarget.Contains(lc.ParentId))
+ .ExecuteDelete();
+
+ return updated;
+ }
}
diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs
index c19d15d85f..48859de04b 100644
--- a/MediaBrowser.Controller/Library/ILibraryManager.cs
+++ b/MediaBrowser.Controller/Library/ILibraryManager.cs
@@ -714,5 +714,14 @@ namespace MediaBrowser.Controller.Library
/// <param name="virtualFolderPath">The path to the virtualfolder.</param>
/// <param name="pathInfo">The new virtualfolder.</param>
public void CreateShortcut(string virtualFolderPath, MediaPathInfo pathInfo);
+
+ /// <summary>
+ /// Re-routes LinkedChildren references from one child to another.
+ /// Used when video versions change to maintain playlist/BoxSet integrity.
+ /// </summary>
+ /// <param name="fromChildId">The child ID to re-route from.</param>
+ /// <param name="toChildId">The child ID to re-route to.</param>
+ /// <returns>Number of references updated.</returns>
+ int RerouteLinkedChildReferences(Guid fromChildId, Guid toChildId);
}
}
diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs
index f7ed39730e..504adff86c 100644
--- a/MediaBrowser.Controller/Persistence/IItemRepository.cs
+++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs
@@ -210,4 +210,21 @@ public interface IItemRepository
/// <param name="userId">The user ID for access filtering.</param>
/// <returns>Dictionary mapping parent ID to child count.</returns>
Dictionary<Guid, int> GetChildCountBatch(IReadOnlyList<Guid> parentIds, Guid? userId);
+
+ /// <summary>
+ /// Gets parent IDs (Playlists/BoxSets) that reference the specified child with LinkedChildType.Manual.
+ /// </summary>
+ /// <param name="childId">The child item ID.</param>
+ /// <returns>List of parent IDs that reference the child.</returns>
+ IReadOnlyList<Guid> GetManualLinkedParentIds(Guid childId);
+
+ /// <summary>
+ /// Updates LinkedChildren references from one child to another, preserving SortOrder.
+ /// Handles duplicates: if parent already references toChildId, removes the old reference instead.
+ /// Used when video versions change to maintain collection integrity.
+ /// </summary>
+ /// <param name="fromChildId">The child ID to re-route from.</param>
+ /// <param name="toChildId">The child ID to re-route to.</param>
+ /// <returns>Number of references updated.</returns>
+ int RerouteLinkedChildren(Guid fromChildId, Guid toChildId);
}