< Summary - Jellyfin

Information
Class: Jellyfin.Server.Migrations.Routines.RefreshForcedSortNames
Assembly: jellyfin
File(s): /srv/git/jellyfin/Jellyfin.Server/Migrations/Routines/20260722120000_RefreshForcedSortNames.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 47
Coverable lines: 47
Total lines: 113
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 10
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/26/2026 - 12:17:51 AM Line coverage: 0% (0/47) Branch coverage: 0% (0/10) Total lines: 113 7/26/2026 - 12:17:51 AM Line coverage: 0% (0/47) Branch coverage: 0% (0/10) Total lines: 113

Metrics

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

File(s)

/srv/git/jellyfin/Jellyfin.Server/Migrations/Routines/20260722120000_RefreshForcedSortNames.cs

#LineLine coverage
 1using System;
 2using System.Diagnostics;
 3using System.Linq;
 4using System.Threading;
 5using System.Threading.Tasks;
 6using Jellyfin.Database.Implementations;
 7using Jellyfin.Extensions;
 8using Jellyfin.Server.ServerSetupApp;
 9using MediaBrowser.Controller.Configuration;
 10using MediaBrowser.Controller.Entities;
 11using Microsoft.EntityFrameworkCore;
 12using Microsoft.Extensions.Logging;
 13
 14namespace Jellyfin.Server.Migrations.Routines;
 15
 16/// <summary>
 17/// Migration to recompute the SortName of all items that have a forced sort name.
 18/// </summary>
 19[JellyfinMigration("2026-07-22T12:00:00", nameof(RefreshForcedSortNames))]
 20[JellyfinMigrationBackup(JellyfinDb = true)]
 21public class RefreshForcedSortNames : IAsyncMigrationRoutine
 22{
 23    private readonly IStartupLogger<RefreshForcedSortNames> _logger;
 24    private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
 25    private readonly IServerConfigurationManager _configurationManager;
 26
 27    /// <summary>
 28    /// Initializes a new instance of the <see cref="RefreshForcedSortNames"/> class.
 29    /// </summary>
 30    /// <param name="logger">The logger.</param>
 31    /// <param name="dbProvider">Instance of the <see cref="IDbContextFactory{JellyfinDbContext}"/> interface.</param>
 32    /// <param name="configurationManager">The server configuration manager providing the sort rules.</param>
 33    public RefreshForcedSortNames(
 34        IStartupLogger<RefreshForcedSortNames> logger,
 35        IDbContextFactory<JellyfinDbContext> dbProvider,
 36        IServerConfigurationManager configurationManager)
 37    {
 038        _logger = logger;
 039        _dbProvider = dbProvider;
 040        _configurationManager = configurationManager;
 041    }
 42
 43    /// <inheritdoc />
 44    public async Task PerformAsync(CancellationToken cancellationToken)
 45    {
 46        const int Limit = 10000;
 047        int itemCount = 0;
 48
 049        var configuration = _configurationManager.Configuration;
 50        // Only the Person type disables alphanumeric sorting; everything else uses the cleaning rules.
 051        var personType = typeof(Person).ToString();
 52
 053        var sw = Stopwatch.StartNew();
 54
 055        using var context = _dbProvider.CreateDbContext();
 056        var records = context.BaseItems.Count(b => !string.IsNullOrEmpty(b.ForcedSortName));
 057        _logger.LogInformation("Refreshing SortName for {Count} library items with a forced sort name", records);
 58
 059        var processedInPartition = 0;
 60
 061        await foreach (var item in context.BaseItems
 062                          .Where(b => !string.IsNullOrEmpty(b.ForcedSortName))
 063                          .OrderBy(e => e.Id)
 064                          .WithPartitionProgress((partition) => _logger.LogInformation("Processed: {Offset}/{Total} - Up
 065                          .PartitionEagerAsync(Limit, cancellationToken)
 066                          .WithCancellation(cancellationToken)
 067                          .ConfigureAwait(false))
 68        {
 69            try
 70            {
 071                var enableAlphaNumericSorting = !string.Equals(item.Type, personType, StringComparison.Ordinal);
 072                var newSortName = BaseItem.GetSortName(item.ForcedSortName!, enableAlphaNumericSorting, configuration);
 073                if (!string.Equals(newSortName, item.SortName, StringComparison.Ordinal))
 74                {
 075                    _logger.LogDebug(
 076                        "Updating SortName for item {Id}: '{OldValue}' -> '{NewValue}'",
 077                        item.Id,
 078                        item.SortName,
 079                        newSortName);
 080                    item.SortName = newSortName;
 081                    itemCount++;
 82                }
 083            }
 084            catch (Exception ex)
 85            {
 086                _logger.LogWarning(ex, "Failed to update SortName for item {Id} ({Name})", item.Id, item.Name);
 087            }
 88
 089            processedInPartition++;
 90
 091            if (processedInPartition >= Limit)
 92            {
 093                await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
 94                // Clear tracked entities to avoid memory growth across partitions
 095                context.ChangeTracker.Clear();
 096                processedInPartition = 0;
 97            }
 98        }
 99
 100        // Save any remaining changes after the loop
 0101        if (processedInPartition > 0)
 102        {
 0103            await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
 0104            context.ChangeTracker.Clear();
 105        }
 106
 0107        _logger.LogInformation(
 0108            "Refreshed SortName for {UpdatedCount} out of {TotalCount} items in {Time}",
 0109            itemCount,
 0110            records,
 0111            sw.Elapsed);
 0112    }
 113}