mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-06-18 05:30:34 +01:00
Defer primary deletion if we replace primary until after item creation
This commit is contained in:
@@ -486,8 +486,8 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
var itemsRemoved = currentChildren.Values.Except(validChildren).ToList();
|
var itemsRemoved = currentChildren.Values.Except(validChildren).ToList();
|
||||||
var shouldRemove = !IsRoot || allowRemoveRoot;
|
var shouldRemove = !IsRoot || allowRemoveRoot;
|
||||||
// If it's an AggregateFolder, don't remove
|
// If it's an AggregateFolder, don't remove
|
||||||
// Collect old primaries that need demotion to alternates of newly created primaries
|
// Collect replaced primaries for deferred deletion (after CreateItems)
|
||||||
var oldPrimariesToDemote = new List<(Video OldPrimary, Video NewPrimary)>();
|
var replacedPrimaries = new List<(Video OldPrimary, Video NewPrimary)>();
|
||||||
|
|
||||||
if (shouldRemove && itemsRemoved.Count > 0)
|
if (shouldRemove && itemsRemoved.Count > 0)
|
||||||
{
|
{
|
||||||
@@ -518,6 +518,29 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Defer deletion if this primary video is being replaced by a new primary
|
||||||
|
// that takes over its alternates. Deleting now would trigger premature
|
||||||
|
// promotion inside DeleteItem and write stale paths to collection NFOs.
|
||||||
|
if (item is Video primaryVideo
|
||||||
|
&& !primaryVideo.PrimaryVersionId.HasValue
|
||||||
|
&& primaryVideo.OwnerId.IsEmpty()
|
||||||
|
&& (primaryVideo.LocalAlternateVersions ?? []).Any(p => alternateVersionPaths.Contains(p)))
|
||||||
|
{
|
||||||
|
var newPrimary = newItems
|
||||||
|
.OfType<Video>()
|
||||||
|
.FirstOrDefault(v => (v.LocalAlternateVersions ?? [])
|
||||||
|
.Any(p => (primaryVideo.LocalAlternateVersions ?? [])
|
||||||
|
.Any(op => string.Equals(op, p, StringComparison.OrdinalIgnoreCase))));
|
||||||
|
if (newPrimary is not null)
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Deferring deletion of replaced primary: {Path}", item.Path);
|
||||||
|
replacedPrimaries.Add((primaryVideo, newPrimary));
|
||||||
|
actuallyRemoved.Add(item);
|
||||||
|
item.SetParent(null);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (item.IsFileProtocol)
|
if (item.IsFileProtocol)
|
||||||
{
|
{
|
||||||
Logger.LogDebug("Removed item: {Path}", item.Path);
|
Logger.LogDebug("Removed item: {Path}", item.Path);
|
||||||
@@ -527,27 +550,6 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
LibraryManager.DeleteItem(item, new DeleteOptions { DeleteFileLocation = false }, this, false);
|
LibraryManager.DeleteItem(item, new DeleteOptions { DeleteFileLocation = false }, this, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect items that need demotion AFTER all deletions have run.
|
|
||||||
// DeleteItem may promote an alternate to primary (clearing its OwnerId),
|
|
||||||
// so we must check OwnerId after the deletion loop to see the updated state.
|
|
||||||
foreach (var item in itemsRemoved.Except(actuallyRemoved))
|
|
||||||
{
|
|
||||||
if (item is Video video
|
|
||||||
&& video.OwnerId.IsEmpty()
|
|
||||||
&& !string.IsNullOrEmpty(item.Path)
|
|
||||||
&& alternateVersionPaths.Contains(item.Path))
|
|
||||||
{
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newItems.Count > 0)
|
if (newItems.Count > 0)
|
||||||
@@ -555,43 +557,43 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
LibraryManager.CreateItems(newItems, this, cancellationToken);
|
LibraryManager.CreateItems(newItems, this, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Demote old primaries that are now alternate versions of newly created primaries
|
// Process deferred replaced-primary deletions now that new primaries exist in DB/cache.
|
||||||
foreach (var (oldPrimary, newPrimary) in oldPrimariesToDemote)
|
// This avoids the premature promotion that would occur if DeleteItem ran before CreateItems.
|
||||||
|
foreach (var (oldPrimary, newPrimary) in replacedPrimaries)
|
||||||
{
|
{
|
||||||
Logger.LogInformation(
|
Logger.LogInformation(
|
||||||
"Demoting old primary {OldName} ({OldId}) to alternate of new primary {NewName} ({NewId})",
|
"Processing deferred deletion of replaced primary {OldName} ({OldId}), new primary {NewName} ({NewId})",
|
||||||
oldPrimary.Name,
|
oldPrimary.Name,
|
||||||
oldPrimary.Id,
|
oldPrimary.Id,
|
||||||
newPrimary.Name,
|
newPrimary.Name,
|
||||||
newPrimary.Id);
|
newPrimary.Id);
|
||||||
|
|
||||||
// First: update old primary's alternate items to point to new primary.
|
// Reroute collection/playlist references from old primary to new primary
|
||||||
// Order matters — update alternates FIRST so they don't get orphan-deleted
|
await LibraryManager.RerouteLinkedChildReferencesAsync(oldPrimary.Id, newPrimary.Id).ConfigureAwait(false);
|
||||||
// when old primary's arrays are cleared.
|
|
||||||
var oldAlternateIds = LibraryManager.GetLocalAlternateVersionIds(oldPrimary)
|
// Transfer alternates from old primary to new primary
|
||||||
|
var localAlternateIds = LibraryManager.GetLocalAlternateVersionIds(oldPrimary).ToHashSet();
|
||||||
|
var allAlternateIds = localAlternateIds
|
||||||
.Concat(LibraryManager.GetLinkedAlternateVersions(oldPrimary).Select(v => v.Id))
|
.Concat(LibraryManager.GetLinkedAlternateVersions(oldPrimary).Select(v => v.Id))
|
||||||
.Distinct()
|
.Distinct()
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
foreach (var altId in oldAlternateIds)
|
foreach (var altId in allAlternateIds)
|
||||||
{
|
{
|
||||||
if (LibraryManager.GetItemById(altId) is Video altVideo && !altVideo.Id.Equals(newPrimary.Id))
|
if (LibraryManager.GetItemById(altId) is Video altVideo && !altVideo.Id.Equals(newPrimary.Id))
|
||||||
{
|
{
|
||||||
altVideo.SetPrimaryVersionId(newPrimary.Id);
|
altVideo.SetPrimaryVersionId(newPrimary.Id);
|
||||||
altVideo.OwnerId = newPrimary.Id;
|
altVideo.OwnerId = localAlternateIds.Contains(altVideo.Id) ? newPrimary.Id : Guid.Empty;
|
||||||
await altVideo.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
|
await altVideo.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then: demote old primary — clear its arrays and set it as alternate of new primary
|
// Clear alternate arrays so DeleteItem won't trigger promotion
|
||||||
oldPrimary.LocalAlternateVersions = [];
|
oldPrimary.LocalAlternateVersions = [];
|
||||||
oldPrimary.LinkedAlternateVersions = [];
|
oldPrimary.LinkedAlternateVersions = [];
|
||||||
oldPrimary.SetPrimaryVersionId(newPrimary.Id);
|
|
||||||
oldPrimary.OwnerId = newPrimary.Id;
|
|
||||||
await oldPrimary.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
// Re-route playlist/collection references from old primary to new primary
|
// Safe to delete now — no promotion will happen
|
||||||
await LibraryManager.RerouteLinkedChildReferencesAsync(oldPrimary.Id, newPrimary.Id).ConfigureAwait(false);
|
LibraryManager.DeleteItem(oldPrimary, new DeleteOptions { DeleteFileLocation = false }, this, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// After removing items, reattach any detached user data to remaining children
|
// After removing items, reattach any detached user data to remaining children
|
||||||
|
|||||||
Reference in New Issue
Block a user