From 12f718e7bb1b3b247e90fe2769d2a6b22b9f37af Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 10 Jun 2026 10:09:01 +0200 Subject: Fix CleanName and CleanValue refresh --- .../Routines/20251008120000_RefreshCleanNames.cs | 102 ------------ .../20260610120000_RefreshCleanNamesAndValues.cs | 173 +++++++++++++++++++++ 2 files changed, 173 insertions(+), 102 deletions(-) delete mode 100644 Jellyfin.Server/Migrations/Routines/20251008120000_RefreshCleanNames.cs create mode 100644 Jellyfin.Server/Migrations/Routines/20260610120000_RefreshCleanNamesAndValues.cs diff --git a/Jellyfin.Server/Migrations/Routines/20251008120000_RefreshCleanNames.cs b/Jellyfin.Server/Migrations/Routines/20251008120000_RefreshCleanNames.cs deleted file mode 100644 index eca50ac100..0000000000 --- a/Jellyfin.Server/Migrations/Routines/20251008120000_RefreshCleanNames.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System; -using System.Diagnostics; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Jellyfin.Database.Implementations; -using Jellyfin.Extensions; -using Jellyfin.Server.ServerSetupApp; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace Jellyfin.Server.Migrations.Routines; - -/// -/// Migration to refresh CleanName values for all library items. -/// -[JellyfinMigration("2025-10-08T12:00:00", nameof(RefreshCleanNames))] -[JellyfinMigrationBackup(JellyfinDb = true)] -public class RefreshCleanNames : IAsyncMigrationRoutine -{ - private readonly IStartupLogger _logger; - private readonly IDbContextFactory _dbProvider; - - /// - /// Initializes a new instance of the class. - /// - /// The logger. - /// Instance of the interface. - public RefreshCleanNames( - IStartupLogger logger, - IDbContextFactory dbProvider) - { - _logger = logger; - _dbProvider = dbProvider; - } - - /// - public async Task PerformAsync(CancellationToken cancellationToken) - { - const int Limit = 10000; - int itemCount = 0; - - var sw = Stopwatch.StartNew(); - - using var context = _dbProvider.CreateDbContext(); - var records = context.BaseItems.Count(b => !string.IsNullOrEmpty(b.Name)); - _logger.LogInformation("Refreshing CleanName for {Count} library items", records); - - var processedInPartition = 0; - - await foreach (var item in context.BaseItems - .Where(b => !string.IsNullOrEmpty(b.Name)) - .OrderBy(e => e.Id) - .WithPartitionProgress((partition) => _logger.LogInformation("Processed: {Offset}/{Total} - Updated: {UpdatedCount} - Time: {Elapsed}", partition * Limit, records, itemCount, sw.Elapsed)) - .PartitionEagerAsync(Limit, cancellationToken) - .WithCancellation(cancellationToken) - .ConfigureAwait(false)) - { - try - { - var newCleanName = string.IsNullOrWhiteSpace(item.Name) ? string.Empty : item.Name.GetCleanValue(); - if (!string.Equals(newCleanName, item.CleanName, StringComparison.Ordinal)) - { - _logger.LogDebug( - "Updating CleanName for item {Id}: '{OldValue}' -> '{NewValue}'", - item.Id, - item.CleanName, - newCleanName); - item.CleanName = newCleanName; - itemCount++; - } - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Failed to update CleanName for item {Id} ({Name})", item.Id, item.Name); - } - - processedInPartition++; - - if (processedInPartition >= Limit) - { - await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - // Clear tracked entities to avoid memory growth across partitions - context.ChangeTracker.Clear(); - processedInPartition = 0; - } - } - - // Save any remaining changes after the loop - if (processedInPartition > 0) - { - await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - context.ChangeTracker.Clear(); - } - - _logger.LogInformation( - "Refreshed CleanName for {UpdatedCount} out of {TotalCount} items in {Time}", - itemCount, - records, - sw.Elapsed); - } -} diff --git a/Jellyfin.Server/Migrations/Routines/20260610120000_RefreshCleanNamesAndValues.cs b/Jellyfin.Server/Migrations/Routines/20260610120000_RefreshCleanNamesAndValues.cs new file mode 100644 index 0000000000..7ade727d9b --- /dev/null +++ b/Jellyfin.Server/Migrations/Routines/20260610120000_RefreshCleanNamesAndValues.cs @@ -0,0 +1,173 @@ +using System; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Database.Implementations; +using Jellyfin.Extensions; +using Jellyfin.Server.ServerSetupApp; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.Migrations.Routines; + +/// +/// Migration to refresh CleanName values for all library items and CleanValue values for all item values. +/// +[JellyfinMigration("2026-06-10T12:00:00", nameof(RefreshCleanNamesAndValues))] +[JellyfinMigrationBackup(JellyfinDb = true)] +public class RefreshCleanNamesAndValues : IAsyncMigrationRoutine +{ + private readonly IStartupLogger _logger; + private readonly IDbContextFactory _dbProvider; + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + /// Instance of the interface. + public RefreshCleanNamesAndValues( + IStartupLogger logger, + IDbContextFactory dbProvider) + { + _logger = logger; + _dbProvider = dbProvider; + } + + /// + public async Task PerformAsync(CancellationToken cancellationToken) + { + await RefreshCleanNamesAsync(cancellationToken).ConfigureAwait(false); + await RefreshCleanValuesAsync(cancellationToken).ConfigureAwait(false); + } + + private async Task RefreshCleanNamesAsync(CancellationToken cancellationToken) + { + const int Limit = 10000; + int itemCount = 0; + + var sw = Stopwatch.StartNew(); + + using var context = _dbProvider.CreateDbContext(); + var records = context.BaseItems.Count(b => !string.IsNullOrEmpty(b.Name)); + _logger.LogInformation("Refreshing CleanName for {Count} library items", records); + + var processedInPartition = 0; + + await foreach (var item in context.BaseItems + .Where(b => !string.IsNullOrEmpty(b.Name)) + .OrderBy(e => e.Id) + .WithPartitionProgress((partition) => _logger.LogInformation("Processed: {Offset}/{Total} - Updated: {UpdatedCount} - Time: {Elapsed}", partition * Limit, records, itemCount, sw.Elapsed)) + .PartitionEagerAsync(Limit, cancellationToken) + .WithCancellation(cancellationToken) + .ConfigureAwait(false)) + { + try + { + var newCleanName = string.IsNullOrWhiteSpace(item.Name) ? string.Empty : item.Name.GetCleanValue(); + if (!string.Equals(newCleanName, item.CleanName, StringComparison.Ordinal)) + { + _logger.LogDebug( + "Updating CleanName for item {Id}: '{OldValue}' -> '{NewValue}'", + item.Id, + item.CleanName, + newCleanName); + item.CleanName = newCleanName; + itemCount++; + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to update CleanName for item {Id} ({Name})", item.Id, item.Name); + } + + processedInPartition++; + + if (processedInPartition >= Limit) + { + await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + // Clear tracked entities to avoid memory growth across partitions + context.ChangeTracker.Clear(); + processedInPartition = 0; + } + } + + // Save any remaining changes after the loop + if (processedInPartition > 0) + { + await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + context.ChangeTracker.Clear(); + } + + _logger.LogInformation( + "Refreshed CleanName for {UpdatedCount} out of {TotalCount} items in {Time}", + itemCount, + records, + sw.Elapsed); + } + + private async Task RefreshCleanValuesAsync(CancellationToken cancellationToken) + { + const int Limit = 10000; + int itemCount = 0; + + var sw = Stopwatch.StartNew(); + + using var context = _dbProvider.CreateDbContext(); + var records = context.ItemValues.Count(b => !string.IsNullOrEmpty(b.Value)); + _logger.LogInformation("Refreshing CleanValue for {Count} item values", records); + + var processedInPartition = 0; + + await foreach (var item in context.ItemValues + .Where(b => !string.IsNullOrEmpty(b.Value)) + .OrderBy(e => e.ItemValueId) + .WithPartitionProgress((partition) => _logger.LogInformation("Processed: {Offset}/{Total} - Updated: {UpdatedCount} - Time: {Elapsed}", partition * Limit, records, itemCount, sw.Elapsed)) + .PartitionEagerAsync(Limit, cancellationToken) + .WithCancellation(cancellationToken) + .ConfigureAwait(false)) + { + try + { + var newCleanValue = string.IsNullOrWhiteSpace(item.Value) ? string.Empty : item.Value.GetCleanValue(); + if (!string.Equals(newCleanValue, item.CleanValue, StringComparison.Ordinal)) + { + _logger.LogDebug( + "Updating CleanValue for item value {Id}: '{OldValue}' -> '{NewValue}'", + item.ItemValueId, + item.CleanValue, + newCleanValue); + item.CleanValue = newCleanValue; + itemCount++; + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to update CleanValue for item value {Id} ({Value})", item.ItemValueId, item.Value); + } + + processedInPartition++; + + if (processedInPartition >= Limit) + { + await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + // Clear tracked entities to avoid memory growth across partitions + context.ChangeTracker.Clear(); + processedInPartition = 0; + } + } + + // Save any remaining changes after the loop + if (processedInPartition > 0) + { + await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + context.ChangeTracker.Clear(); + } + + _logger.LogInformation( + "Refreshed CleanValue for {UpdatedCount} out of {TotalCount} item values in {Time}", + itemCount, + records, + sw.Elapsed); + } +} -- cgit v1.2.3