aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Server.Implementations
diff options
context:
space:
mode:
authorShadowghost <Ghost_of_Stone@web.de>2026-07-29 14:40:49 +0200
committerShadowghost <Ghost_of_Stone@web.de>2026-07-29 14:40:49 +0200
commit8293eb26b995a21f88bff60dcf93da8d98080cc0 (patch)
tree6bd36928ddf44b9b729d36e47ce4d618cb10a465 /Jellyfin.Server.Implementations
parentdc300fae53b6b42090341cb517238bbc76479e02 (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.cs2
-rw-r--r--Jellyfin.Server.Implementations/Item/ItemPersistenceService.cs56
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;
}