RefreshCleanNames.cs 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. using System;
  2. using System.Diagnostics;
  3. using System.Linq;
  4. using System.Text;
  5. using System.Threading;
  6. using System.Threading.Tasks;
  7. using Jellyfin.Database.Implementations;
  8. using Jellyfin.Database.Implementations.Entities;
  9. using Jellyfin.Extensions;
  10. using Jellyfin.Server.Implementations.Item;
  11. using Jellyfin.Server.ServerSetupApp;
  12. using Microsoft.EntityFrameworkCore;
  13. using Microsoft.Extensions.Logging;
  14. namespace Jellyfin.Server.Migrations.Routines;
  15. /// <summary>
  16. /// Migration to refresh CleanName values for all library items.
  17. /// </summary>
  18. [JellyfinMigration("2025-10-08T12:00:00", nameof(RefreshCleanNames))]
  19. [JellyfinMigrationBackup(JellyfinDb = true)]
  20. public class RefreshCleanNames : IAsyncMigrationRoutine
  21. {
  22. private readonly IStartupLogger<RefreshCleanNames> _logger;
  23. private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
  24. /// <summary>
  25. /// Initializes a new instance of the <see cref="RefreshCleanNames"/> class.
  26. /// </summary>
  27. /// <param name="logger">The logger.</param>
  28. /// <param name="dbProvider">Instance of the <see cref="IDbContextFactory{JellyfinDbContext}"/> interface.</param>
  29. public RefreshCleanNames(
  30. IStartupLogger<RefreshCleanNames> logger,
  31. IDbContextFactory<JellyfinDbContext> dbProvider)
  32. {
  33. _logger = logger;
  34. _dbProvider = dbProvider;
  35. }
  36. /// <inheritdoc />
  37. public async Task PerformAsync(CancellationToken cancellationToken)
  38. {
  39. const int Limit = 1000;
  40. int itemCount = 0;
  41. var sw = Stopwatch.StartNew();
  42. using var context = _dbProvider.CreateDbContext();
  43. var records = context.BaseItems.Count(b => !string.IsNullOrEmpty(b.Name));
  44. _logger.LogInformation("Refreshing CleanName for {Count} library items", records);
  45. var processedInPartition = 0;
  46. await foreach (var item in context.BaseItems
  47. .Where(b => !string.IsNullOrEmpty(b.Name))
  48. .OrderBy(e => e.Id)
  49. .WithPartitionProgress((partition) => _logger.LogInformation("Processed: {Offset}/{Total} - Updated: {UpdatedCount} - Time: {Elapsed}", partition * Limit, records, itemCount, sw.Elapsed))
  50. .PartitionEagerAsync(Limit, cancellationToken)
  51. .WithCancellation(cancellationToken)
  52. .ConfigureAwait(false))
  53. {
  54. try
  55. {
  56. var newCleanName = string.IsNullOrWhiteSpace(item.Name) ? string.Empty : BaseItemRepository.GetCleanValue(item.Name);
  57. if (!string.Equals(newCleanName, item.CleanName, StringComparison.Ordinal))
  58. {
  59. _logger.LogDebug(
  60. "Updating CleanName for item {Id}: '{OldValue}' -> '{NewValue}'",
  61. item.Id,
  62. item.CleanName,
  63. newCleanName);
  64. item.CleanName = newCleanName;
  65. itemCount++;
  66. }
  67. }
  68. catch (Exception ex)
  69. {
  70. _logger.LogWarning(ex, "Failed to update CleanName for item {Id} ({Name})", item.Id, item.Name);
  71. }
  72. processedInPartition++;
  73. if (processedInPartition >= Limit)
  74. {
  75. await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
  76. // Clear tracked entities to avoid memory growth across partitions
  77. context.ChangeTracker.Clear();
  78. processedInPartition = 0;
  79. }
  80. }
  81. // Save any remaining changes after the loop
  82. if (processedInPartition > 0)
  83. {
  84. await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
  85. context.ChangeTracker.Clear();
  86. }
  87. _logger.LogInformation(
  88. "Refreshed CleanName for {UpdatedCount} out of {TotalCount} items in {Time}",
  89. itemCount,
  90. records,
  91. sw.Elapsed);
  92. }
  93. }