From 95330223f49c6ba8b1d77fbe4e4dad4fc6ba9be9 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Thu, 23 Jul 2026 22:38:45 +0200 Subject: Speedup migration --- ...0260723120000_RecomputeSeriesPresentationKey.cs | 76 ++++++++++++---------- 1 file changed, 40 insertions(+), 36 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) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (++processed % ProgressInterval == 0) - { - _logger.LogInformation("Processed {Processed}/{Total} series - Updated: {Updated} - Time: {Elapsed}", processed, series.Length, updated, sw.Elapsed); - } - var oldKey = item.PresentationUniqueKey; - var newKey = item.CreatePresentationUniqueKey(); - if (string.Equals(oldKey, newKey, StringComparison.Ordinal)) + var dbContext = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + foreach (var item in series) { - continue; - } + cancellationToken.ThrowIfCancellationRequested(); - item.PresentationUniqueKey = newKey; - await item.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false); + if (++processed % ProgressInterval == 0) + { + _logger.LogInformation("Processed {Processed}/{Total} series - Updated: {Updated} - Time: {Elapsed}", processed, series.Length, updated, sw.Elapsed); + } - // 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 + var oldKey = item.PresentationUniqueKey; + var newKey = item.CreatePresentationUniqueKey(); + if (string.Equals(oldKey, newKey, StringComparison.Ordinal)) { - SeriesPresentationUniqueKey = oldKey, - IncludeItemTypes = [BaseItemKind.Season, BaseItemKind.Episode] - }); + continue; + } - foreach (var child in children) + // 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)) { - if (child is IHasSeries hasSeries - && !string.Equals(hasSeries.SeriesPresentationUniqueKey, newKey, StringComparison.Ordinal)) - { - hasSeries.SeriesPresentationUniqueKey = newKey; - await child.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false); - } + await dbContext.BaseItems + .Where(e => e.SeriesPresentationUniqueKey == oldKey) + .ExecuteUpdateAsync(e => e.SetProperty(f => f.SeriesPresentationUniqueKey, newKey), cancellationToken) + .ConfigureAwait(false); } - } - updated++; + updated++; + } } _logger.LogInformation("Recomputed presentation unique key for {Updated} of {Count} series in {Elapsed}", updated, series.Length, sw.Elapsed); -- cgit v1.2.3