aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShadowghost <Ghost_of_Stone@web.de>2026-08-31 08:36:33 +0200
committerShadowghost <Ghost_of_Stone@web.de>2026-08-31 08:37:57 +0200
commitff36560575db2e8ee866c8569e0c2fb51f80b446 (patch)
tree6e29d0c6c6bbc385f46b148363132f0fbbf50696
parente9abaa519c10e8f8337544120f477164bbd5645b (diff)
Enable local similarity providers on libraries upgraded from 10.11
-rw-r--r--Jellyfin.Server/Migrations/Routines/20260831100000_EnableLocalSimilarityProviders.cs135
1 files changed, 135 insertions, 0 deletions
diff --git a/Jellyfin.Server/Migrations/Routines/20260831100000_EnableLocalSimilarityProviders.cs b/Jellyfin.Server/Migrations/Routines/20260831100000_EnableLocalSimilarityProviders.cs
new file mode 100644
index 0000000000..54b9a7ef42
--- /dev/null
+++ b/Jellyfin.Server/Migrations/Routines/20260831100000_EnableLocalSimilarityProviders.cs
@@ -0,0 +1,135 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Server.Migrations.Stages;
+using Jellyfin.Server.ServerSetupApp;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Configuration;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Migrations.Routines;
+
+/// <summary>
+/// Enables the local similarity providers on libraries that predate the similar items settings.
+/// </summary>
+[JellyfinMigration("2026-08-31T10:00:00", nameof(EnableLocalSimilarityProviders), Stage = JellyfinMigrationStageTypes.AppInitialisation)]
+internal class EnableLocalSimilarityProviders : IAsyncMigrationRoutine
+{
+ private readonly ILibraryManager _libraryManager;
+ private readonly IProviderManager _providerManager;
+ private readonly ILogger _logger;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="EnableLocalSimilarityProviders"/> class.
+ /// </summary>
+ /// <param name="libraryManager">The library manager.</param>
+ /// <param name="providerManager">The provider manager.</param>
+ /// <param name="startupLogger">The startup logger for Startup UI integration.</param>
+ /// <param name="logger">The logger.</param>
+ public EnableLocalSimilarityProviders(
+ ILibraryManager libraryManager,
+ IProviderManager providerManager,
+ IStartupLogger<EnableLocalSimilarityProviders> startupLogger,
+ ILogger<EnableLocalSimilarityProviders> logger)
+ {
+ _libraryManager = libraryManager;
+ _providerManager = providerManager;
+ _logger = startupLogger.With(logger);
+ }
+
+ /// <inheritdoc />
+ public Task PerformAsync(CancellationToken cancellationToken)
+ {
+ // Libraries created before similar items became configurable have an empty provider list,
+ // which the library editor renders as "everything unchecked" instead of falling back to the
+ // defaults it uses for new libraries. Seed the local providers so they stay enabled.
+ var localProvidersByType = GetLocalProvidersByItemType();
+ if (localProvidersByType.Count == 0)
+ {
+ return Task.CompletedTask;
+ }
+
+ foreach (var virtualFolder in _libraryManager.GetVirtualFolders(false))
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var options = virtualFolder.LibraryOptions;
+ if (options?.TypeOptions is null || options.TypeOptions.Length == 0)
+ {
+ continue;
+ }
+
+ // Some virtual folders don't have a proper item id.
+ if (!Guid.TryParse(virtualFolder.ItemId, out var folderId))
+ {
+ continue;
+ }
+
+ var collectionFolder = _libraryManager.GetItemById<CollectionFolder>(folderId);
+ if (collectionFolder is null)
+ {
+ _logger.LogWarning("Could not find collection folder for virtual folder '{LibraryName}' with id '{FolderId}'. Skipping.", virtualFolder.Name, folderId);
+ continue;
+ }
+
+ var changed = false;
+ foreach (var typeOptions in options.TypeOptions)
+ {
+ if (typeOptions.Type is null || !localProvidersByType.TryGetValue(typeOptions.Type, out var localProviders))
+ {
+ continue;
+ }
+
+ var enabled = typeOptions.SimilarItemProviders ?? [];
+ var missing = localProviders.Where(name => !enabled.Contains(name, StringComparer.OrdinalIgnoreCase)).ToArray();
+ if (missing.Length == 0)
+ {
+ continue;
+ }
+
+ // Local providers rank ahead of remote ones, and the enabled list doubles as the
+ // priority order when no explicit order was saved.
+ typeOptions.SimilarItemProviders = [.. missing, .. enabled];
+ if (typeOptions.SimilarItemProviderOrder is { Length: > 0 } order)
+ {
+ typeOptions.SimilarItemProviderOrder = [.. missing, .. order];
+ }
+
+ changed = true;
+ _logger.LogInformation("Enabled local similarity providers {Providers} for '{ItemType}' in library '{LibraryName}'.", missing, typeOptions.Type, virtualFolder.Name);
+ }
+
+ if (changed)
+ {
+ collectionFolder.UpdateLibraryOptions(options);
+ }
+ }
+
+ return Task.CompletedTask;
+ }
+
+ private Dictionary<string, string[]> GetLocalProvidersByItemType()
+ {
+ var result = new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase);
+
+ foreach (var summary in _providerManager.GetAllMetadataPlugins())
+ {
+ var names = summary.Plugins
+ .Where(p => p.Type == MetadataPluginType.LocalSimilarityProvider)
+ .Select(p => p.Name)
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .ToArray();
+
+ if (names.Length > 0)
+ {
+ result[summary.ItemType] = names;
+ }
+ }
+
+ return result;
+ }
+}