From 95330223f49c6ba8b1d77fbe4e4dad4fc6ba9be9 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Thu, 23 Jul 2026 22:38:45 +0200 Subject: [PATCH] Speedup migration --- ...23120000_RecomputeSeriesPresentationKey.cs | 82 ++++++++++--------- 1 file changed, 43 insertions(+), 39 deletions(-) diff --git a/Jellyfin.Server/Migrations/Routines/20260723120000_RecomputeSeriesPresentationKey.cs b/Jellyfin.Server/Migrations/Routines/20260723120000_RecomputeSeriesPresentationKey.cs index 3542c580a8..60bb3fd1db 100644 --- a/Jellyfin.Server/Migrations/Routines/20260723120000_RecomputeSeriesPresentationKey.cs +++ b/Jellyfin.Server/Migrations/Routines/20260723120000_RecomputeSeriesPresentationKey.cs @@ -4,10 +4,12 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations; using Jellyfin.Server.ServerSetupApp; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; namespace Jellyfin.Server.Migrations.Routines; @@ -21,18 +23,22 @@ internal class RecomputeSeriesPresentationKey : IAsyncMigrationRoutine { private readonly IStartupLogger _logger; private readonly ILibraryManager _libraryManager; + private readonly IDbContextFactory _dbProvider; /// /// Initializes a new instance of the class. /// /// The startup logger. /// The library manager. + /// The database context factory. public RecomputeSeriesPresentationKey( IStartupLogger logger, - ILibraryManager libraryManager) + ILibraryManager libraryManager, + IDbContextFactory dbProvider) { _logger = logger; _libraryManager = libraryManager; + _dbProvider = dbProvider; } /// @@ -40,57 +46,55 @@ internal class RecomputeSeriesPresentationKey : IAsyncMigrationRoutine { var series = _libraryManager.GetItemList(new InternalItemsQuery { - IncludeItemTypes = new[] { BaseItemKind.Series } + IncludeItemTypes = [BaseItemKind.Series] }).OfType().ToArray(); _logger.LogInformation("Recomputing presentation unique key for {Count} series", series.Length); - const int ProgressInterval = 500; + const int ProgressInterval = 250; var sw = Stopwatch.StartNew(); var processed = 0; var updated = 0; - foreach (var item in series) + + var dbContext = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) { - cancellationToken.ThrowIfCancellationRequested(); - - if (++processed % ProgressInterval == 0) + foreach (var item in series) { - _logger.LogInformation("Processed {Processed}/{Total} series - Updated: {Updated} - Time: {Elapsed}", processed, series.Length, updated, sw.Elapsed); - } + cancellationToken.ThrowIfCancellationRequested(); - var oldKey = item.PresentationUniqueKey; - var newKey = item.CreatePresentationUniqueKey(); - if (string.Equals(oldKey, newKey, StringComparison.Ordinal)) - { - continue; - } - - item.PresentationUniqueKey = newKey; - await item.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false); - - // Seasons and episodes cache the series key in SeriesPresentationUniqueKey and are matched - // to the series by it. Look them up by the old key (they still carry it) and re-point - // them at the new key so they stay attached without waiting for the next scan. - if (!string.IsNullOrEmpty(oldKey)) - { - var children = _libraryManager.GetItemList(new InternalItemsQuery + if (++processed % ProgressInterval == 0) { - SeriesPresentationUniqueKey = oldKey, - IncludeItemTypes = [BaseItemKind.Season, BaseItemKind.Episode] - }); - - foreach (var child in children) - { - if (child is IHasSeries hasSeries - && !string.Equals(hasSeries.SeriesPresentationUniqueKey, newKey, StringComparison.Ordinal)) - { - hasSeries.SeriesPresentationUniqueKey = newKey; - await child.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false); - } + _logger.LogInformation("Processed {Processed}/{Total} series - Updated: {Updated} - Time: {Elapsed}", processed, series.Length, updated, sw.Elapsed); } - } - updated++; + var oldKey = item.PresentationUniqueKey; + var newKey = item.CreatePresentationUniqueKey(); + if (string.Equals(oldKey, newKey, StringComparison.Ordinal)) + { + continue; + } + + // Write only the changed column instead of re-persisting the whole item. + var id = item.Id; + await dbContext.BaseItems + .Where(e => e.Id.Equals(id)) + .ExecuteUpdateAsync(e => e.SetProperty(f => f.PresentationUniqueKey, newKey), cancellationToken) + .ConfigureAwait(false); + + // Seasons and episodes cache the series key in SeriesPresentationUniqueKey and are matched + // to the series by it. Re-point every child still carrying the old key in a single set-based + // update so they stay attached without waiting for the next scan. + if (!string.IsNullOrEmpty(oldKey)) + { + await dbContext.BaseItems + .Where(e => e.SeriesPresentationUniqueKey == oldKey) + .ExecuteUpdateAsync(e => e.SetProperty(f => f.SeriesPresentationUniqueKey, newKey), cancellationToken) + .ConfigureAwait(false); + } + + updated++; + } } _logger.LogInformation("Recomputed presentation unique key for {Updated} of {Count} series in {Elapsed}", updated, series.Length, sw.Elapsed);