< Summary - Jellyfin

Information
Class: Jellyfin.Server.Migrations.Routines.RecomputeSeriesPresentationKey
Assembly: jellyfin
File(s): /srv/git/jellyfin/Jellyfin.Server/Migrations/Routines/20260723120000_RecomputeSeriesPresentationKey.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 35
Coverable lines: 35
Total lines: 102
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 8
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 7/27/2026 - 12:16:14 AM Line coverage: 0% (0/35) Branch coverage: 0% (0/8) Total lines: 102 7/27/2026 - 12:16:14 AM Line coverage: 0% (0/35) Branch coverage: 0% (0/8) Total lines: 102

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
PerformAsync()0%7280%

File(s)

/srv/git/jellyfin/Jellyfin.Server/Migrations/Routines/20260723120000_RecomputeSeriesPresentationKey.cs

#LineLine coverage
 1using System;
 2using System.Diagnostics;
 3using System.Linq;
 4using System.Threading;
 5using System.Threading.Tasks;
 6using Jellyfin.Data.Enums;
 7using Jellyfin.Database.Implementations;
 8using Jellyfin.Server.ServerSetupApp;
 9using MediaBrowser.Controller.Entities;
 10using MediaBrowser.Controller.Entities.TV;
 11using MediaBrowser.Controller.Library;
 12using Microsoft.EntityFrameworkCore;
 13using Microsoft.Extensions.Logging;
 14
 15namespace Jellyfin.Server.Migrations.Routines;
 16
 17/// <summary>
 18/// Recomputes the presentation unique key for every series so existing items adopt the folder-set-free key format.
 19/// </summary>
 20[JellyfinMigration("2026-07-23T12:00:00", nameof(RecomputeSeriesPresentationKey))]
 21[JellyfinMigrationBackup(JellyfinDb = true)]
 22internal class RecomputeSeriesPresentationKey : IAsyncMigrationRoutine
 23{
 24    private readonly IStartupLogger<RecomputeSeriesPresentationKey> _logger;
 25    private readonly ILibraryManager _libraryManager;
 26    private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
 27
 28    /// <summary>
 29    /// Initializes a new instance of the <see cref="RecomputeSeriesPresentationKey"/> class.
 30    /// </summary>
 31    /// <param name="logger">The startup logger.</param>
 32    /// <param name="libraryManager">The library manager.</param>
 33    /// <param name="dbProvider">The database context factory.</param>
 34    public RecomputeSeriesPresentationKey(
 35        IStartupLogger<RecomputeSeriesPresentationKey> logger,
 36        ILibraryManager libraryManager,
 37        IDbContextFactory<JellyfinDbContext> dbProvider)
 38    {
 039        _logger = logger;
 040        _libraryManager = libraryManager;
 041        _dbProvider = dbProvider;
 042    }
 43
 44    /// <inheritdoc />
 45    public async Task PerformAsync(CancellationToken cancellationToken)
 46    {
 047        var series = _libraryManager.GetItemList(new InternalItemsQuery
 048        {
 049            IncludeItemTypes = [BaseItemKind.Series]
 050        }).OfType<Series>().ToArray();
 51
 052        _logger.LogInformation("Recomputing presentation unique key for {Count} series", series.Length);
 53
 54        const int ProgressInterval = 250;
 055        var sw = Stopwatch.StartNew();
 056        var processed = 0;
 057        var updated = 0;
 58
 059        var dbContext = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
 060        await using (dbContext.ConfigureAwait(false))
 61        {
 062            foreach (var item in series)
 63            {
 064                cancellationToken.ThrowIfCancellationRequested();
 65
 066                if (++processed % ProgressInterval == 0)
 67                {
 068                    _logger.LogInformation("Processed {Processed}/{Total} series - Updated: {Updated} - Time: {Elapsed}"
 69                }
 70
 071                var oldKey = item.PresentationUniqueKey;
 072                var newKey = item.CreatePresentationUniqueKey();
 073                if (string.Equals(oldKey, newKey, StringComparison.Ordinal))
 74                {
 75                    continue;
 76                }
 77
 78                // Write only the changed column instead of re-persisting the whole item.
 079                var id = item.Id;
 080                await dbContext.BaseItems
 081                    .Where(e => e.Id.Equals(id))
 082                    .ExecuteUpdateAsync(e => e.SetProperty(f => f.PresentationUniqueKey, newKey), cancellationToken)
 083                    .ConfigureAwait(false);
 84
 85                // Seasons and episodes cache the series key in SeriesPresentationUniqueKey and are matched
 86                // to the series by it. Re-point every child still carrying the old key in a single set-based
 87                // update so they stay attached without waiting for the next scan.
 088                if (!string.IsNullOrEmpty(oldKey))
 89                {
 090                    await dbContext.BaseItems
 091                        .Where(e => e.SeriesPresentationUniqueKey == oldKey)
 092                        .ExecuteUpdateAsync(e => e.SetProperty(f => f.SeriesPresentationUniqueKey, newKey), cancellation
 093                        .ConfigureAwait(false);
 94                }
 95
 096                updated++;
 097            }
 98        }
 99
 0100        _logger.LogInformation("Recomputed presentation unique key for {Updated} of {Count} series in {Elapsed}", update
 0101    }
 102}