Merge pull request #17748 from Shadowghost/fix-default-config-12.x
Fix ListenBrainz settings and similar item defaults
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user