diff options
| author | Cody Robibero <cody@robibe.ro> | 2026-08-31 17:46:30 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-08-31 17:46:30 -0400 |
| commit | fe73b1a94903a39bd89e0b0b542ede35d24266a8 (patch) | |
| tree | ca948011e77ce08a7e8ce460e35587650af8befa | |
| parent | 80324b19fb6be5fc92526a8495a7a772ab9d6bbd (diff) | |
| parent | 05844d60c1714a2f551fdcfc46b6a7ffbc9ae645 (diff) | |
Merge pull request #17748 from Shadowghost/fix-default-config-12.x
Fix ListenBrainz settings and similar item defaults
3 files changed, 164 insertions, 12 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..e665725ced --- /dev/null +++ b/Jellyfin.Server/Migrations/Routines/20260831100000_EnableLocalSimilarityProviders.cs @@ -0,0 +1,146 @@ +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 MediaBrowser.Model.Entities; +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(); + + EnableLocalProviders(virtualFolder, localProvidersByType); + } + + return Task.CompletedTask; + } + + private void EnableLocalProviders(VirtualFolderInfo virtualFolder, Dictionary<string, string[]> localProvidersByType) + { + var options = virtualFolder.LibraryOptions; + if (options?.TypeOptions is null || options.TypeOptions.Length == 0) + { + return; + } + + // Some virtual folders don't have a proper item id. + if (!Guid.TryParse(virtualFolder.ItemId, out var folderId)) + { + return; + } + + 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); + return; + } + + var changed = false; + foreach (var typeOptions in options.TypeOptions) + { + changed |= EnableLocalProviders(typeOptions, localProvidersByType, virtualFolder.Name); + } + + if (changed) + { + collectionFolder.UpdateLibraryOptions(options); + } + } + + private bool EnableLocalProviders(TypeOptions typeOptions, Dictionary<string, string[]> localProvidersByType, string libraryName) + { + if (typeOptions.Type is null || !localProvidersByType.TryGetValue(typeOptions.Type, out var localProviders)) + { + return false; + } + + var enabled = typeOptions.SimilarItemProviders ?? []; + var missing = localProviders.Where(name => !enabled.Contains(name, StringComparer.OrdinalIgnoreCase)).ToArray(); + if (missing.Length == 0) + { + return false; + } + + // 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]; + } + + _logger.LogInformation("Enabled local similarity providers {Providers} for '{ItemType}' in library '{LibraryName}'.", missing, typeOptions.Type, libraryName); + return true; + } + + 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; + } +} diff --git a/MediaBrowser.Providers/Plugins/ListenBrainz/Configuration/config.html b/MediaBrowser.Providers/Plugins/ListenBrainz/Configuration/config.html index dec21d1b42..d485fe555a 100644 --- a/MediaBrowser.Providers/Plugins/ListenBrainz/Configuration/config.html +++ b/MediaBrowser.Providers/Plugins/ListenBrainz/Configuration/config.html @@ -7,7 +7,6 @@ <div id="configPage" data-role="page" class="page type-interior pluginConfigurationPage configPage" data-require="emby-input,emby-button,emby-select"> <div data-role="content"> <div class="content-primary"> - <img id="listenBrainzLogo" alt="ListenBrainz" style="max-width:240px;display:block;margin:0 auto 1em;" /> <h1>ListenBrainz</h1> <p>Get similar artist recommendations from ListenBrainz Labs.</p> <form class="configForm"> @@ -18,12 +17,12 @@ <div class="selectContainer"> <label class="selectLabel" for="algorithm">Similarity Algorithm</label> <select is="emby-select" id="algorithm" class="emby-select-withcolor"> - <option value="0" selected>~5 years / 1825 days (Recommended)</option> - <option value="1">~5 years / 1800 days</option> - <option value="2">~20 years / 7500 days</option> - <option value="3">~20 years / 7500 days (high contribution)</option> - <option value="4">~25 years / 9000 days</option> - <option value="5">~75 days (recent)</option> + <option value="SessionBased1825Days" selected>~5 years / 1825 days (Recommended)</option> + <option value="SessionBased1800Days">~5 years / 1800 days</option> + <option value="SessionBased7500Days">~20 years / 7500 days</option> + <option value="SessionBased7500DaysHighContribution">~20 years / 7500 days (high contribution)</option> + <option value="SessionBased9000Days">~25 years / 9000 days</option> + <option value="SessionBased75Days">~75 days (recent)</option> </select> <div class="fieldDescription">The algorithm used for artist similarity calculation.</div> </div> @@ -52,13 +51,14 @@ </div> <script type="text/javascript"> var ListenBrainzPluginConfig = { - uniquePluginId: "a5b2e8c1-9d4f-4a3b-8c7e-6f1a2b3c4d5e" + uniquePluginId: "a5b2e8c1-9d4f-4a3b-8c7e-6f1a2b3c4d5e", + defaultAlgorithm: "SessionBased1825Days" }; document.querySelector('.configPage') .addEventListener('pageshow', function () { Dashboard.showLoadingMsg(); - document.querySelector('#listenBrainzLogo').src = ApiClient.getUrl('web/ConfigurationPage', { name: 'ListenBrainzLogo' }); + ApiClient.getPluginConfiguration(ListenBrainzPluginConfig.uniquePluginId).then(function (config) { var labsServer = document.querySelector('#labsServer'); labsServer.value = config.LabsServer; @@ -67,7 +67,13 @@ cancelable: false })); - document.querySelector('#algorithm').value = config.Algorithm; + // The API serialises the algorithm as its enum name, so an unknown value here + // means a config written by an older build; fall back to the default. + var algorithm = document.querySelector('#algorithm'); + algorithm.value = config.Algorithm; + if (!algorithm.value) { + algorithm.value = ListenBrainzPluginConfig.defaultAlgorithm; + } var rateLimit = document.querySelector('#rateLimit'); rateLimit.value = config.RateLimit; @@ -93,7 +99,7 @@ ApiClient.getPluginConfiguration(ListenBrainzPluginConfig.uniquePluginId).then(function (config) { config.LabsServer = document.querySelector('#labsServer').value; - config.Algorithm = parseInt(document.querySelector('#algorithm').value, 10); + config.Algorithm = document.querySelector('#algorithm').value; config.RateLimit = document.querySelector('#rateLimit').value; config.SimilarItemsCacheDays = parseInt(document.querySelector('#similarItemsCacheDays').value, 10); diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Configuration/PluginConfiguration.cs b/MediaBrowser.Providers/Plugins/Tmdb/Configuration/PluginConfiguration.cs index b3a67189bb..c94a6455bc 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Configuration/PluginConfiguration.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Configuration/PluginConfiguration.cs @@ -128,6 +128,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb /// <summary> /// Gets or sets the cache duration in days for similar item results. A value of 0 disables caching. /// </summary> - public int SimilarItemsCacheDays { get; set; } = 7; + public int SimilarItemsCacheDays { get; set; } = 90; } } |
