From 4ce03ffa21c8bc8a93ac004a6c691d85f78d1503 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Sun, 15 Feb 2026 17:34:06 +0100 Subject: [PATCH] Fix orphaned alt version cleanup --- .../Routines/MigrateLinkedChildren.cs | 36 ++++++++++++++++--- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLinkedChildren.cs b/Jellyfin.Server/Migrations/Routines/MigrateLinkedChildren.cs index b00f53bbe7..d43cd785cd 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateLinkedChildren.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateLinkedChildren.cs @@ -278,7 +278,7 @@ internal class MigrateLinkedChildren : IDatabaseMigrationRoutine { _logger.LogInformation("Starting cleanup of orphaned alternate version BaseItems..."); - // Find BaseItems that have PrimaryVersionId set (they were alternate versions) + // Check 1: Find BaseItems that have PrimaryVersionId set (they were alternate versions) // but no LinkedChild entry references them — meaning they're orphaned. // This happens when a version file is renamed: the old BaseItem remains // in the DB with a stale PrimaryVersionId but nothing links to it anymore. @@ -288,15 +288,41 @@ internal class MigrateLinkedChildren : IDatabaseMigrationRoutine .Select(b => b.Id) .ToList(); - if (orphanedVersionIds.Count == 0) + _logger.LogInformation("Found {Count} orphaned alternate versions with stale PrimaryVersionId.", orphanedVersionIds.Count); + + // Check 2: Find generic Video items that share a parent folder with a Movie/Episode + // but are not linked as alternate versions. These are orphaned entries from renamed + // alternate version files where PrimaryVersionId was already cleared by the old server. + var specificVideoTypes = new[] + { + "MediaBrowser.Controller.Entities.Movies.Movie", + "MediaBrowser.Controller.Entities.TV.Episode" + }; + + var orphanedVideoIds = context.BaseItems + .Where(b => b.Type == "MediaBrowser.Controller.Entities.Video") + .Where(b => b.ExtraType == null && !b.OwnerId.HasValue) + .Where(b => !context.LinkedChildren.Any(lc => lc.ChildId.Equals(b.Id))) + .Where(b => b.ParentId.HasValue + && context.BaseItems.Any(sibling => + sibling.ParentId.Equals(b.ParentId) + && specificVideoTypes.Contains(sibling.Type))) + .Select(b => b.Id) + .ToList(); + + _logger.LogInformation("Found {Count} orphaned generic Video items in movie/episode folders.", orphanedVideoIds.Count); + + var allOrphanedIds = orphanedVersionIds.Union(orphanedVideoIds).ToList(); + + if (allOrphanedIds.Count == 0) { _logger.LogInformation("No orphaned alternate version BaseItems found."); return; } - _logger.LogInformation("Found {Count} orphaned alternate version BaseItems to remove.", orphanedVersionIds.Count); + _logger.LogInformation("Removing {Count} total orphaned alternate version BaseItems.", allOrphanedIds.Count); - foreach (var id in orphanedVersionIds) + foreach (var id in allOrphanedIds) { var item = _libraryManager.GetItemById(id); if (item is not null) @@ -305,7 +331,7 @@ internal class MigrateLinkedChildren : IDatabaseMigrationRoutine } } - _logger.LogInformation("Removed {Count} orphaned alternate version BaseItems.", orphanedVersionIds.Count); + _logger.LogInformation("Removed {Count} orphaned alternate version BaseItems.", allOrphanedIds.Count); } private void CleanupOrphanedLinkedChildren(JellyfinDbContext context)