aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Server/Migrations
diff options
context:
space:
mode:
authorShadowghost <Ghost_of_Stone@web.de>2026-01-17 15:11:45 +0100
committerShadowghost <Ghost_of_Stone@web.de>2026-01-18 19:47:02 +0100
commitc350fd0f40d9bfa2d1740a45aaa5d439e5ef5151 (patch)
tree3a4521636361ed979b578b88836b524d5f3e4e7e /Jellyfin.Server/Migrations
parent139d23ddc29b6bafad5f8e6ba9eddc8484ab0713 (diff)
Remove ExtraIds column and use OwnerId relationship for extras
- Remove ExtraIds property from BaseItemEntity and BaseItem - Update RefreshExtras to query via OwnerId instead of cached ExtraIds - Update GetExtras methods to query database via OwnerIds filter - Add OwnerIds and ExtraTypes filter support to InternalItemsQuery - Add filter handling in BaseItemRepository for new query options - Update HasSpecialFeature/HasTrailer filters to use Extras relationship - Add CleanupOrphanedExtras migration routine - Add database migration to drop ExtraIds column
Diffstat (limited to 'Jellyfin.Server/Migrations')
-rw-r--r--Jellyfin.Server/Migrations/Routines/CleanupOrphanedExtras.cs119
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs6
2 files changed, 121 insertions, 4 deletions
diff --git a/Jellyfin.Server/Migrations/Routines/CleanupOrphanedExtras.cs b/Jellyfin.Server/Migrations/Routines/CleanupOrphanedExtras.cs
new file mode 100644
index 0000000000..ffde30f0ca
--- /dev/null
+++ b/Jellyfin.Server/Migrations/Routines/CleanupOrphanedExtras.cs
@@ -0,0 +1,119 @@
+using System;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Database.Implementations;
+using Jellyfin.Server.Migrations.Stages;
+using Jellyfin.Server.ServerSetupApp;
+using MediaBrowser.Controller.Channels;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Controller.MediaSegments;
+using MediaBrowser.Controller.Persistence;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Migrations.Routines;
+
+/// <summary>
+/// Removes orphaned extras (items with OwnerId pointing to non-existent items).
+/// Must run before EF migrations that add FK constraints on OwnerId.
+/// </summary>
+[JellyfinMigration("2026-01-13T23:00:00", nameof(CleanupOrphanedExtras), Stage = JellyfinMigrationStageTypes.CoreInitialisation)]
+[JellyfinMigrationBackup(JellyfinDb = true)]
+public class CleanupOrphanedExtras : IAsyncMigrationRoutine
+{
+ private readonly IStartupLogger<CleanupOrphanedExtras> _logger;
+ private readonly IDbContextFactory<JellyfinDbContext> _dbContextFactory;
+ private readonly ILibraryManager _libraryManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CleanupOrphanedExtras"/> class.
+ /// </summary>
+ /// <param name="logger">The startup logger.</param>
+ /// <param name="dbContextFactory">The database context factory.</param>
+ /// <param name="libraryManager">The library manager.</param>
+ /// <param name="itemRepository">The item repository.</param>
+ /// <param name="channelManager">The channel manager.</param>
+ /// <param name="recordingsManager">The recordings manager.</param>
+ /// <param name="mediaSourceManager">The media source manager.</param>
+ /// <param name="mediaSegmentManager">The media segments manager.</param>
+ /// <param name="configurationManager">The configuration manager.</param>
+ public CleanupOrphanedExtras(
+ IStartupLogger<CleanupOrphanedExtras> logger,
+ IDbContextFactory<JellyfinDbContext> dbContextFactory,
+ ILibraryManager libraryManager,
+ IItemRepository itemRepository,
+ IChannelManager channelManager,
+ IRecordingsManager recordingsManager,
+ IMediaSourceManager mediaSourceManager,
+ IMediaSegmentManager mediaSegmentManager,
+ IServerConfigurationManager configurationManager)
+ {
+ _logger = logger;
+ _dbContextFactory = dbContextFactory;
+ _libraryManager = libraryManager;
+ BaseItem.LibraryManager ??= libraryManager;
+ BaseItem.ItemRepository ??= itemRepository;
+ BaseItem.ChannelManager ??= channelManager;
+ BaseItem.MediaSourceManager ??= mediaSourceManager;
+ BaseItem.MediaSegmentManager ??= mediaSegmentManager;
+ BaseItem.ConfigurationManager ??= configurationManager;
+ Video.RecordingsManager ??= recordingsManager;
+ }
+
+ /// <inheritdoc/>
+ public async Task PerformAsync(CancellationToken cancellationToken)
+ {
+ var context = await _dbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
+ await using (context.ConfigureAwait(false))
+ {
+ var orphanedItemIds = await context.BaseItems
+ .Where(b => b.OwnerId.HasValue && !b.OwnerId.Value.Equals(Guid.Empty))
+ .Where(b => !context.BaseItems.Any(parent => parent.Id.Equals(b.OwnerId!.Value)))
+ .Select(b => b.Id)
+ .ToListAsync(cancellationToken)
+ .ConfigureAwait(false);
+
+ if (orphanedItemIds.Count == 0)
+ {
+ _logger.LogInformation("No orphaned extras found, skipping migration.");
+ return;
+ }
+
+ _logger.LogInformation("Found {Count} orphaned extras to remove", orphanedItemIds.Count);
+
+ var deleteOptions = new DeleteOptions
+ {
+ DeleteFileLocation = false // Extras don't have their own media files
+ };
+
+ var deletedCount = 0;
+ foreach (var itemId in orphanedItemIds)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var item = _libraryManager.GetItemById(itemId);
+ if (item is null)
+ {
+ _logger.LogDebug("Item {ItemId} not found in library, may have been already deleted", itemId);
+ continue;
+ }
+
+ try
+ {
+ _libraryManager.DeleteItem(item, deleteOptions, notifyParentItem: false);
+ deletedCount++;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Failed to delete orphaned item {ItemId} ({ItemName})", item.Id, item.Name);
+ }
+ }
+
+ _logger.LogInformation("Successfully removed {Count} orphaned extras", deletedCount);
+ }
+ }
+}
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs
index 3150f70baa..2c96f00761 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs
@@ -1176,10 +1176,8 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
entity.ProductionLocations = productionLocations;
}
- if (reader.TryGetString(index++, out var extraIds))
- {
- entity.ExtraIds = extraIds;
- }
+ // Skip ExtraIds column (removed - extras are now tracked via OwnerId relationship)
+ index++;
if (reader.TryGetInt32(index++, out var totalBitrate))
{