aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Controller/Entities
diff options
context:
space:
mode:
authorShadowghost <Ghost_of_Stone@web.de>2026-02-23 20:25:55 +0100
committerShadowghost <Ghost_of_Stone@web.de>2026-02-23 20:25:55 +0100
commit66c11231b209ade94831bc200f840b38aaba3160 (patch)
treefaf3deb19673187d69ae1cb55a29201118bb60f6 /MediaBrowser.Controller/Entities
parent34c1e45bc2e496e9589775576c0f0a99e9159a0c (diff)
Fix promotion logic
Diffstat (limited to 'MediaBrowser.Controller/Entities')
-rw-r--r--MediaBrowser.Controller/Entities/Folder.cs52
1 files changed, 52 insertions, 0 deletions
diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs
index ecf99accb6..e6c94d3cbe 100644
--- a/MediaBrowser.Controller/Entities/Folder.cs
+++ b/MediaBrowser.Controller/Entities/Folder.cs
@@ -486,6 +486,9 @@ namespace MediaBrowser.Controller.Entities
var itemsRemoved = currentChildren.Values.Except(validChildren).ToList();
var shouldRemove = !IsRoot || allowRemoveRoot;
// If it's an AggregateFolder, don't remove
+ // Collect old primaries that need demotion to alternates of newly created primaries
+ var oldPrimariesToDemote = new List<(Video OldPrimary, Video NewPrimary)>();
+
if (shouldRemove && itemsRemoved.Count > 0)
{
// Build a set of paths that are alternate versions of valid children
@@ -517,6 +520,19 @@ namespace MediaBrowser.Controller.Entities
// Check if path is in LocalAlternateVersions of any valid child
if (!string.IsNullOrEmpty(item.Path) && alternateVersionPaths.Contains(item.Path))
{
+ // If this was a primary (no PrimaryVersionId, no OwnerId), it needs demotion
+ if (video.OwnerId.IsEmpty())
+ {
+ var newPrimary = newItems
+ .OfType<Video>()
+ .FirstOrDefault(v => (v.LocalAlternateVersions ?? [])
+ .Any(p => string.Equals(p, item.Path, StringComparison.OrdinalIgnoreCase)));
+ if (newPrimary is not null)
+ {
+ oldPrimariesToDemote.Add((video, newPrimary));
+ }
+ }
+
Logger.LogDebug("Item path matches an alternate version, skipping deletion: {Path}", item.Path);
continue;
}
@@ -538,6 +554,42 @@ namespace MediaBrowser.Controller.Entities
LibraryManager.CreateItems(newItems, this, cancellationToken);
}
+ // Demote old primaries that are now alternate versions of newly created primaries
+ foreach (var (oldPrimary, newPrimary) in oldPrimariesToDemote)
+ {
+ Logger.LogInformation(
+ "Demoting old primary {OldName} ({OldId}) to alternate of new primary {NewName} ({NewId})",
+ oldPrimary.Name,
+ oldPrimary.Id,
+ newPrimary.Name,
+ newPrimary.Id);
+
+ // First: update old primary's alternate items to point to new primary.
+ // Order matters — update alternates FIRST so they don't get orphan-deleted
+ // when old primary's arrays are cleared.
+ var oldAlternateIds = LibraryManager.GetLocalAlternateVersionIds(oldPrimary)
+ .Concat(LibraryManager.GetLinkedAlternateVersions(oldPrimary).Select(v => v.Id))
+ .Distinct()
+ .ToList();
+
+ foreach (var altId in oldAlternateIds)
+ {
+ if (LibraryManager.GetItemById(altId) is Video altVideo && !altVideo.Id.Equals(newPrimary.Id))
+ {
+ altVideo.SetPrimaryVersionId(newPrimary.Id);
+ altVideo.OwnerId = newPrimary.Id;
+ await altVideo.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ // Then: demote old primary — clear its arrays and set it as alternate of new primary
+ oldPrimary.LocalAlternateVersions = [];
+ oldPrimary.LinkedAlternateVersions = [];
+ oldPrimary.SetPrimaryVersionId(newPrimary.Id);
+ oldPrimary.OwnerId = newPrimary.Id;
+ await oldPrimary.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
+ }
+
// 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)