diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLinkedChildren.cs b/Jellyfin.Server/Migrations/Routines/MigrateLinkedChildren.cs
new file mode 100644
index 0000000000..15108a07b4
--- /dev/null
+++ b/Jellyfin.Server/Migrations/Routines/MigrateLinkedChildren.cs
@@ -0,0 +1,361 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json;
+using Jellyfin.Database.Implementations;
+using Jellyfin.Database.Implementations.Entities;
+using Jellyfin.Extensions;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using LinkedChildType = Jellyfin.Database.Implementations.Entities.LinkedChildType;
+
+namespace Jellyfin.Server.Migrations.Routines;
+
+///
+/// Migrates LinkedChildren data from JSON Data column to the LinkedChildren table.
+///
+[JellyfinMigration("2026-01-13T12:00:00", nameof(MigrateLinkedChildren))]
+[JellyfinMigrationBackup(JellyfinDb = true)]
+internal class MigrateLinkedChildren : IDatabaseMigrationRoutine
+{
+ private readonly ILogger _logger;
+ private readonly IDbContextFactory _dbProvider;
+
+ public MigrateLinkedChildren(
+ ILoggerFactory loggerFactory,
+ IDbContextFactory dbProvider)
+ {
+ _logger = loggerFactory.CreateLogger();
+ _dbProvider = dbProvider;
+ }
+
+ ///
+ public void Perform()
+ {
+ using var context = _dbProvider.CreateDbContext();
+
+ var containerTypes = new[]
+ {
+ "MediaBrowser.Controller.Entities.Movies.BoxSet",
+ "MediaBrowser.Controller.Playlists.Playlist",
+ "MediaBrowser.Controller.Entities.CollectionFolder"
+ };
+
+ var videoTypes = new[]
+ {
+ "MediaBrowser.Controller.Entities.Video",
+ "MediaBrowser.Controller.Entities.Movies.Movie"
+ };
+
+ var itemsWithData = context.BaseItems
+ .Where(b => b.Data != null && (containerTypes.Contains(b.Type) || videoTypes.Contains(b.Type)))
+ .Select(b => new { b.Id, b.Data, b.Type })
+ .ToList();
+
+ _logger.LogInformation("Found {Count} potential items with LinkedChildren data to process.", itemsWithData.Count);
+
+ var pathToIdMap = context.BaseItems
+ .Where(b => b.Path != null)
+ .Select(b => new { b.Id, b.Path })
+ .GroupBy(b => b.Path!)
+ .ToDictionary(g => g.Key, g => g.First().Id);
+
+ var linkedChildrenToAdd = new List();
+ var processedCount = 0;
+
+ foreach (var item in itemsWithData)
+ {
+ if (string.IsNullOrEmpty(item.Data))
+ {
+ continue;
+ }
+
+ try
+ {
+ using var doc = JsonDocument.Parse(item.Data);
+
+ var isVideo = item.Type == "MediaBrowser.Controller.Entities.Video" || item.Type == "MediaBrowser.Controller.Entities.Movies.Movie";
+
+ // Handle Video alternate versions
+ if (isVideo)
+ {
+ ProcessVideoAlternateVersions(doc.RootElement, item.Id, pathToIdMap, linkedChildrenToAdd);
+ }
+
+ // Handle LinkedChildren (for containers and other items)
+ if (!doc.RootElement.TryGetProperty("LinkedChildren", out var linkedChildrenElement) || linkedChildrenElement.ValueKind != JsonValueKind.Array)
+ {
+ processedCount++;
+ continue;
+ }
+
+ var isPlaylist = item.Type == "MediaBrowser.Controller.Playlists.Playlist";
+ var sortOrder = 0;
+ foreach (var childElement in linkedChildrenElement.EnumerateArray())
+ {
+ Guid? childId = null;
+ if (childElement.TryGetProperty("ItemId", out var itemIdProp) && itemIdProp.ValueKind != JsonValueKind.Null)
+ {
+ var itemIdStr = itemIdProp.GetString();
+ if (!string.IsNullOrEmpty(itemIdStr) && Guid.TryParse(itemIdStr, out var parsedId))
+ {
+ childId = parsedId;
+ }
+ }
+
+ if (!childId.HasValue || childId.Value.IsEmpty())
+ {
+ if (childElement.TryGetProperty("Path", out var pathProp))
+ {
+ var path = pathProp.GetString();
+ if (!string.IsNullOrEmpty(path) && pathToIdMap.TryGetValue(path, out var resolvedId))
+ {
+ childId = resolvedId;
+ }
+ }
+ }
+
+ if (!childId.HasValue || childId.Value.IsEmpty())
+ {
+ if (childElement.TryGetProperty("LibraryItemId", out var libIdProp))
+ {
+ var libIdStr = libIdProp.GetString();
+ if (!string.IsNullOrEmpty(libIdStr) && Guid.TryParse(libIdStr, out var parsedLibId))
+ {
+ childId = parsedLibId;
+ }
+ }
+ }
+
+ if (!childId.HasValue || childId.Value.IsEmpty())
+ {
+ continue;
+ }
+
+ var childType = LinkedChildType.Manual;
+ if (childElement.TryGetProperty("Type", out var typeProp))
+ {
+ if (typeProp.ValueKind == JsonValueKind.Number)
+ {
+ childType = (LinkedChildType)typeProp.GetInt32();
+ }
+ else if (typeProp.ValueKind == JsonValueKind.String)
+ {
+ var typeStr = typeProp.GetString();
+ if (Enum.TryParse(typeStr, out var parsedType))
+ {
+ childType = parsedType;
+ }
+ }
+ }
+
+ linkedChildrenToAdd.Add(new LinkedChildEntity
+ {
+ ParentId = item.Id,
+ ChildId = childId.Value,
+ ChildType = childType,
+ SortOrder = isPlaylist ? sortOrder : null
+ });
+
+ sortOrder++;
+ }
+
+ processedCount++;
+ }
+ catch (JsonException ex)
+ {
+ _logger.LogWarning(ex, "Failed to parse JSON for item {ItemId}", item.Id);
+ }
+ }
+
+ if (linkedChildrenToAdd.Count > 0)
+ {
+ _logger.LogInformation("Inserting {Count} LinkedChildren records.", linkedChildrenToAdd.Count);
+
+ var existingKeys = context.LinkedChildren
+ .Select(lc => new { lc.ParentId, lc.ChildId })
+ .ToHashSet();
+
+ var toInsert = linkedChildrenToAdd
+ .Where(lc => !existingKeys.Contains(new { lc.ParentId, lc.ChildId }))
+ .ToList();
+
+ if (toInsert.Count > 0)
+ {
+ var childIds = toInsert.Select(lc => lc.ChildId).Distinct().ToList();
+ var existingChildIds = context.BaseItems
+ .Where(b => childIds.Contains(b.Id))
+ .Select(b => b.Id)
+ .ToHashSet();
+
+ toInsert = toInsert.Where(lc => existingChildIds.Contains(lc.ChildId)).ToList();
+
+ context.LinkedChildren.AddRange(toInsert);
+ context.SaveChanges();
+
+ _logger.LogInformation("Successfully inserted {Count} LinkedChildren records.", toInsert.Count);
+ }
+ else
+ {
+ _logger.LogInformation("All LinkedChildren records already exist, nothing to insert.");
+ }
+ }
+ else
+ {
+ _logger.LogInformation("No LinkedChildren data found to migrate.");
+ }
+
+ _logger.LogInformation("LinkedChildren migration completed. Processed {Count} items.", processedCount);
+
+ CleanupOrphanedLinkedChildren(context);
+ }
+
+ private void CleanupOrphanedLinkedChildren(JellyfinDbContext context)
+ {
+ _logger.LogInformation("Starting cleanup of orphaned LinkedChildren records...");
+
+ // Find all LinkedChildren where the ChildId doesn't exist in BaseItems
+ var orphanedLinkedChildren = context.LinkedChildren
+ .Where(lc => !context.BaseItems.Any(b => b.Id.Equals(lc.ChildId)))
+ .ToList();
+
+ if (orphanedLinkedChildren.Count == 0)
+ {
+ _logger.LogInformation("No orphaned LinkedChildren found.");
+ return;
+ }
+
+ _logger.LogInformation("Found {Count} orphaned LinkedChildren records to remove.", orphanedLinkedChildren.Count);
+
+ var orphanedByParent = context.LinkedChildren
+ .Where(lc => !context.BaseItems.Any(b => b.Id.Equals(lc.ParentId)))
+ .ToList();
+
+ if (orphanedByParent.Count > 0)
+ {
+ _logger.LogInformation("Found {Count} LinkedChildren with non-existent parent.", orphanedByParent.Count);
+ orphanedLinkedChildren.AddRange(orphanedByParent);
+ }
+
+ // Remove all orphaned records
+ var distinctOrphaned = orphanedLinkedChildren.DistinctBy(lc => new { lc.ParentId, lc.ChildId }).ToList();
+ context.LinkedChildren.RemoveRange(distinctOrphaned);
+ context.SaveChanges();
+
+ _logger.LogInformation("Successfully removed {Count} orphaned LinkedChildren records.", distinctOrphaned.Count);
+ }
+
+ private void ProcessVideoAlternateVersions(
+ JsonElement root,
+ Guid parentId,
+ Dictionary pathToIdMap,
+ List linkedChildrenToAdd)
+ {
+ if (root.TryGetProperty("LocalAlternateVersions", out var localAlternateVersionsElement)
+ && localAlternateVersionsElement.ValueKind == JsonValueKind.Array)
+ {
+ foreach (var pathElement in localAlternateVersionsElement.EnumerateArray())
+ {
+ if (pathElement.ValueKind != JsonValueKind.String)
+ {
+ continue;
+ }
+
+ var path = pathElement.GetString();
+ if (string.IsNullOrEmpty(path))
+ {
+ continue;
+ }
+
+ // Try to resolve the path to an ItemId
+ if (pathToIdMap.TryGetValue(path, out var childId))
+ {
+ linkedChildrenToAdd.Add(new LinkedChildEntity
+ {
+ ParentId = parentId,
+ ChildId = childId,
+ ChildType = LinkedChildType.LocalAlternateVersion,
+ SortOrder = null
+ });
+
+ _logger.LogDebug(
+ "Migrating LocalAlternateVersion: Parent={ParentId}, Child={ChildId}, Path={Path}",
+ parentId,
+ childId,
+ path);
+ }
+ else
+ {
+ _logger.LogWarning(
+ "Could not resolve LocalAlternateVersion path to ItemId: {Path} for parent {ParentId}",
+ path,
+ parentId);
+ }
+ }
+ }
+
+ if (root.TryGetProperty("LinkedAlternateVersions", out var linkedAlternateVersionsElement)
+ && linkedAlternateVersionsElement.ValueKind == JsonValueKind.Array)
+ {
+ foreach (var linkedChildElement in linkedAlternateVersionsElement.EnumerateArray())
+ {
+ Guid? childId = null;
+
+ // Try to get ItemId
+ if (linkedChildElement.TryGetProperty("ItemId", out var itemIdProp) && itemIdProp.ValueKind != JsonValueKind.Null)
+ {
+ var itemIdStr = itemIdProp.GetString();
+ if (!string.IsNullOrEmpty(itemIdStr) && Guid.TryParse(itemIdStr, out var parsedId))
+ {
+ childId = parsedId;
+ }
+ }
+
+ // Try to get from Path if ItemId not available
+ if (!childId.HasValue || childId.Value.IsEmpty())
+ {
+ if (linkedChildElement.TryGetProperty("Path", out var pathProp))
+ {
+ var path = pathProp.GetString();
+ if (!string.IsNullOrEmpty(path) && pathToIdMap.TryGetValue(path, out var resolvedId))
+ {
+ childId = resolvedId;
+ }
+ }
+ }
+
+ // Try LibraryItemId as fallback
+ if (!childId.HasValue || childId.Value.IsEmpty())
+ {
+ if (linkedChildElement.TryGetProperty("LibraryItemId", out var libIdProp))
+ {
+ var libIdStr = libIdProp.GetString();
+ if (!string.IsNullOrEmpty(libIdStr) && Guid.TryParse(libIdStr, out var parsedLibId))
+ {
+ childId = parsedLibId;
+ }
+ }
+ }
+
+ if (!childId.HasValue || childId.Value.IsEmpty())
+ {
+ _logger.LogWarning("Could not resolve LinkedAlternateVersion child ID for parent {ParentId}", parentId);
+ continue;
+ }
+
+ linkedChildrenToAdd.Add(new LinkedChildEntity
+ {
+ ParentId = parentId,
+ ChildId = childId.Value,
+ ChildType = LinkedChildType.LinkedAlternateVersion,
+ SortOrder = null
+ });
+
+ _logger.LogDebug(
+ "Migrating LinkedAlternateVersion: Parent={ParentId}, Child={ChildId}",
+ parentId,
+ childId.Value);
+ }
+ }
+ }
+}