diff options
| author | Shadowghost <Ghost_of_Stone@web.de> | 2026-07-29 14:40:49 +0200 |
|---|---|---|
| committer | Shadowghost <Ghost_of_Stone@web.de> | 2026-07-29 14:40:49 +0200 |
| commit | 8293eb26b995a21f88bff60dcf93da8d98080cc0 (patch) | |
| tree | 6bd36928ddf44b9b729d36e47ce4d618cb10a465 /Jellyfin.Server.Implementations | |
| parent | dc300fae53b6b42090341cb517238bbc76479e02 (diff) | |
Fix playlist entries being lost on migration and library scans
Diffstat (limited to 'Jellyfin.Server.Implementations')
| -rw-r--r-- | Jellyfin.Server.Implementations/Item/BaseItemMapper.cs | 2 | ||||
| -rw-r--r-- | Jellyfin.Server.Implementations/Item/ItemPersistenceService.cs | 56 |
2 files changed, 51 insertions, 7 deletions
diff --git a/Jellyfin.Server.Implementations/Item/BaseItemMapper.cs b/Jellyfin.Server.Implementations/Item/BaseItemMapper.cs index c64e6ac068..958d11e21e 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemMapper.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemMapper.cs @@ -183,7 +183,7 @@ public static class BaseItemMapper if (dto is Folder folder) { folder.DateLastMediaAdded = entity.DateLastMediaAdded ?? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc); - if (entity.LinkedChildEntities is not null && entity.LinkedChildEntities.Count > 0) + if (entity.LinkedChildEntities is not null) { folder.LinkedChildren = entity.LinkedChildEntities .OrderBy(e => e.SortOrder) diff --git a/Jellyfin.Server.Implementations/Item/ItemPersistenceService.cs b/Jellyfin.Server.Implementations/Item/ItemPersistenceService.cs index 9201a031d5..827c766449 100644 --- a/Jellyfin.Server.Implementations/Item/ItemPersistenceService.cs +++ b/Jellyfin.Server.Implementations/Item/ItemPersistenceService.cs @@ -428,23 +428,60 @@ public class ItemPersistenceService : IItemPersistenceService foreach (var item in tuples) { + // A container that was never hydrated cannot be used to rewrite its links: its empty + // array means "unknown", so clearing the stored rows would silently empty the item. + if (item.Item is Folder { LinkedChildrenLoaded: false }) + { + continue; + } + if (item.Item is Folder or Video && allLinkedChildrenByParent.TryGetValue(item.Item.Id, out var existingLinks) && existingLinks.Count > 0) { - context.LinkedChildren.RemoveRange(existingLinks); + // A video only owns its alternate version links; any other link on that parent is + // written by the folder branch below and must survive. + var staleLinks = item.Item is Folder + ? existingLinks + : existingLinks + .Where(e => e.ChildType is DbLinkedChildType.LocalAlternateVersion or DbLinkedChildType.LinkedAlternateVersion) + .ToList(); + + if (staleLinks.Count > 0) + { + context.LinkedChildren.RemoveRange(staleLinks); + } } } context.SaveChanges(); + // A LinkedChild's ItemId is only a cache. + var cachedChildIds = tuples + .Select(t => t.Item) + .OfType<Folder>() + .Where(f => f.LinkedChildrenLoaded) + .SelectMany(f => f.LinkedChildren) + .Where(lc => lc.ItemId.HasValue && !lc.ItemId.Value.IsEmpty()) + .Select(lc => lc.ItemId!.Value) + .Distinct() + .ToList(); + + var knownChildIds = cachedChildIds.Count > 0 + ? context.BaseItems + .WhereOneOrMany(cachedChildIds, e => e.Id) + .Select(e => e.Id) + .ToHashSet() + : []; + foreach (var item in tuples) { - if (item.Item is Folder folder && folder.LinkedChildren.Length > 0) + if (item.Item is Folder { LinkedChildrenLoaded: true } folder && folder.LinkedChildren.Length > 0) { #pragma warning disable CS0618 // Type or member is obsolete - legacy path resolution for old data var pathsToResolve = folder.LinkedChildren - .Where(lc => (!lc.ItemId.HasValue || lc.ItemId.Value.IsEmpty()) && !string.IsNullOrEmpty(lc.Path)) + .Where(lc => !string.IsNullOrEmpty(lc.Path) + && (!lc.ItemId.HasValue || lc.ItemId.Value.IsEmpty() || !knownChildIds.Contains(lc.ItemId.Value))) .Select(lc => lc.Path) .Distinct() .ToList(); @@ -461,12 +498,16 @@ public class ItemPersistenceService : IItemPersistenceService foreach (var linkedChild in folder.LinkedChildren) { var childItemId = linkedChild.ItemId; - if (!childItemId.HasValue || childItemId.Value.IsEmpty()) + if (!childItemId.HasValue || childItemId.Value.IsEmpty() || !knownChildIds.Contains(childItemId.Value)) { if (!string.IsNullOrEmpty(linkedChild.Path) && pathToIdMap.TryGetValue(linkedChild.Path, out var resolvedId)) { childItemId = resolvedId; } + else if (Guid.TryParse(linkedChild.LibraryItemId, out var libraryItemId) && !libraryItemId.IsEmpty()) + { + childItemId = libraryItemId; + } } #pragma warning restore CS0618 @@ -500,11 +541,14 @@ public class ItemPersistenceService : IItemPersistenceService { if (!existingChildIds.Contains(childId)) { +#pragma warning disable CS0618 // Type or member is obsolete - legacy path is logged for diagnostics _logger.LogWarning( - "Skipping LinkedChild for parent {ParentName} ({ParentId}): child item {ChildId} does not exist in database", + "Skipping LinkedChild for parent {ParentName} ({ParentId}): child item {ChildId} (path {ChildPath}) does not exist in database", item.Item.Name, item.Item.Id, - childId); + childId, + linkedChild.Path ?? "unknown"); +#pragma warning restore CS0618 continue; } |
