aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Server
diff options
context:
space:
mode:
Diffstat (limited to 'Jellyfin.Server')
-rw-r--r--Jellyfin.Server/Filters/CachingOpenApiProvider.cs32
-rw-r--r--Jellyfin.Server/Migrations/Routines/RefreshCleanNames.cs105
2 files changed, 116 insertions, 21 deletions
diff --git a/Jellyfin.Server/Filters/CachingOpenApiProvider.cs b/Jellyfin.Server/Filters/CachingOpenApiProvider.cs
index 4169f2fb3..b560ec50e 100644
--- a/Jellyfin.Server/Filters/CachingOpenApiProvider.cs
+++ b/Jellyfin.Server/Filters/CachingOpenApiProvider.cs
@@ -1,5 +1,5 @@
using System;
-using System.Threading;
+using AsyncKeyedLock;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
@@ -17,7 +17,7 @@ internal sealed class CachingOpenApiProvider : ISwaggerProvider
private const string CacheKey = "openapi.json";
private static readonly MemoryCacheEntryOptions _cacheOptions = new() { SlidingExpiration = TimeSpan.FromMinutes(5) };
- private static readonly SemaphoreSlim _lock = new(1, 1);
+ private static readonly AsyncNonKeyedLocker _lock = new(1);
private static readonly TimeSpan _lockTimeout = TimeSpan.FromSeconds(1);
private readonly IMemoryCache _memoryCache;
@@ -50,30 +50,20 @@ internal sealed class CachingOpenApiProvider : ISwaggerProvider
return AdjustDocument(openApiDocument, host, basePath);
}
- var acquired = _lock.Wait(_lockTimeout);
- try
+ using var acquired = _lock.LockOrNull(_lockTimeout);
+ if (_memoryCache.TryGetValue(CacheKey, out openApiDocument) && openApiDocument is not null)
{
- if (_memoryCache.TryGetValue(CacheKey, out openApiDocument) && openApiDocument is not null)
- {
- return AdjustDocument(openApiDocument, host, basePath);
- }
-
- if (!acquired)
- {
- throw new InvalidOperationException("OpenApi document is generating");
- }
-
- openApiDocument = _swaggerGenerator.GetSwagger(documentName);
- _memoryCache.Set(CacheKey, openApiDocument, _cacheOptions);
return AdjustDocument(openApiDocument, host, basePath);
}
- finally
+
+ if (acquired is null)
{
- if (acquired)
- {
- _lock.Release();
- }
+ throw new InvalidOperationException("OpenApi document is generating");
}
+
+ openApiDocument = _swaggerGenerator.GetSwagger(documentName);
+ _memoryCache.Set(CacheKey, openApiDocument, _cacheOptions);
+ return AdjustDocument(openApiDocument, host, basePath);
}
private OpenApiDocument AdjustDocument(OpenApiDocument document, string? host, string? basePath)
diff --git a/Jellyfin.Server/Migrations/Routines/RefreshCleanNames.cs b/Jellyfin.Server/Migrations/Routines/RefreshCleanNames.cs
new file mode 100644
index 000000000..eadabf677
--- /dev/null
+++ b/Jellyfin.Server/Migrations/Routines/RefreshCleanNames.cs
@@ -0,0 +1,105 @@
+using System;
+using System.Diagnostics;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Database.Implementations;
+using Jellyfin.Database.Implementations.Entities;
+using Jellyfin.Extensions;
+using Jellyfin.Server.Implementations.Item;
+using Jellyfin.Server.ServerSetupApp;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Migrations.Routines;
+
+/// <summary>
+/// Migration to refresh CleanName values for all library items.
+/// </summary>
+[JellyfinMigration("2025-10-08T12:00:00", nameof(RefreshCleanNames))]
+[JellyfinMigrationBackup(JellyfinDb = true)]
+public class RefreshCleanNames : IAsyncMigrationRoutine
+{
+ private readonly IStartupLogger<RefreshCleanNames> _logger;
+ private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="RefreshCleanNames"/> class.
+ /// </summary>
+ /// <param name="logger">The logger.</param>
+ /// <param name="dbProvider">Instance of the <see cref="IDbContextFactory{JellyfinDbContext}"/> interface.</param>
+ public RefreshCleanNames(
+ IStartupLogger<RefreshCleanNames> logger,
+ IDbContextFactory<JellyfinDbContext> dbProvider)
+ {
+ _logger = logger;
+ _dbProvider = dbProvider;
+ }
+
+ /// <inheritdoc />
+ public async Task PerformAsync(CancellationToken cancellationToken)
+ {
+ const int Limit = 1000;
+ 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 : BaseItemRepository.GetCleanValue(item.Name);
+ 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);
+ }
+}