aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShadowghost <Ghost_of_Stone@web.de>2026-05-13 00:58:03 +0200
committerShadowghost <Ghost_of_Stone@web.de>2026-05-13 00:58:33 +0200
commitb1b45199444e369b12844661f09d1cd0830d25f7 (patch)
tree8c49e54b9f07f30bfafe4f1268edacbe33811827
parent4ebce3907062ade1937440628eebd665440b338d (diff)
Apply review suggestions
-rw-r--r--Emby.Server.Implementations/Library/SimilarItems/MovieSimilarItemsProvider.cs14
-rw-r--r--Emby.Server.Implementations/Library/SimilarItems/SimilarItemsManager.cs45
-rw-r--r--MediaBrowser.Controller/Library/ILocalSimilarItemsProvider.cs38
-rw-r--r--MediaBrowser.Controller/Library/IRemoteSimilarItemsProvider.cs38
-rw-r--r--MediaBrowser.Providers/Plugins/ListenBrainz/Api/ListenBrainzLabsClient.cs36
5 files changed, 131 insertions, 40 deletions
diff --git a/Emby.Server.Implementations/Library/SimilarItems/MovieSimilarItemsProvider.cs b/Emby.Server.Implementations/Library/SimilarItems/MovieSimilarItemsProvider.cs
index f9547c2c38..93aa0574c0 100644
--- a/Emby.Server.Implementations/Library/SimilarItems/MovieSimilarItemsProvider.cs
+++ b/Emby.Server.Implementations/Library/SimilarItems/MovieSimilarItemsProvider.cs
@@ -1,3 +1,4 @@
+using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
@@ -15,7 +16,7 @@ namespace Emby.Server.Implementations.Library.SimilarItems;
/// <summary>
/// Provides similar items for movies and trailers.
/// </summary>
-public class MovieSimilarItemsProvider : ILocalSimilarItemsProvider<Movie>, ILocalSimilarItemsProvider<Trailer>
+public sealed class MovieSimilarItemsProvider : ILocalSimilarItemsProvider<Movie>, ILocalSimilarItemsProvider<Trailer>
{
private readonly ILibraryManager _libraryManager;
private readonly IServerConfigurationManager _serverConfigurationManager;
@@ -51,6 +52,17 @@ public class MovieSimilarItemsProvider : ILocalSimilarItemsProvider<Movie>, ILoc
return Task.FromResult(GetSimilarMovieItems(item, query));
}
+ bool ILocalSimilarItemsProvider.Supports(Type itemType)
+ => typeof(Movie).IsAssignableFrom(itemType) || typeof(Trailer).IsAssignableFrom(itemType);
+
+ Task<IReadOnlyList<BaseItem>> ILocalSimilarItemsProvider.GetSimilarItemsAsync(BaseItem item, SimilarItemsQuery query, CancellationToken cancellationToken)
+ => item switch
+ {
+ Movie movie => GetSimilarItemsAsync(movie, query, cancellationToken),
+ Trailer trailer => GetSimilarItemsAsync(trailer, query, cancellationToken),
+ _ => throw new ArgumentException($"Unsupported item type {item.GetType()}", nameof(item))
+ };
+
private IReadOnlyList<BaseItem> GetSimilarMovieItems(BaseItem item, SimilarItemsQuery query)
{
var includeItemTypes = new List<BaseItemKind> { BaseItemKind.Movie };
diff --git a/Emby.Server.Implementations/Library/SimilarItems/SimilarItemsManager.cs b/Emby.Server.Implementations/Library/SimilarItems/SimilarItemsManager.cs
index ddafed3d67..b56779cf3f 100644
--- a/Emby.Server.Implementations/Library/SimilarItems/SimilarItemsManager.cs
+++ b/Emby.Server.Implementations/Library/SimilarItems/SimilarItemsManager.cs
@@ -1,10 +1,8 @@
using System;
-using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
-using System.Reflection;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
@@ -28,10 +26,6 @@ namespace Emby.Server.Implementations.Library.SimilarItems;
/// </summary>
public class SimilarItemsManager : ISimilarItemsManager
{
- private static readonly ConcurrentDictionary<Type, MethodInfo> _genericMethodCache = new();
- private static readonly MethodInfo _getSimilarItemsInternalMethod = typeof(SimilarItemsManager)
- .GetMethod(nameof(GetSimilarItemsInternalAsync), BindingFlags.NonPublic | BindingFlags.Instance)!;
-
private readonly ILogger<SimilarItemsManager> _logger;
private readonly IServerApplicationPaths _appPaths;
private readonly ILibraryManager _libraryManager;
@@ -67,10 +61,10 @@ public class SimilarItemsManager : ISimilarItemsManager
public IReadOnlyList<ISimilarItemsProvider> GetSimilarItemsProviders<T>()
where T : BaseItem
{
+ var itemType = typeof(T);
return _similarItemsProviders
- .OfType<ILocalSimilarItemsProvider<T>>()
- .Cast<ISimilarItemsProvider>()
- .Concat(_similarItemsProviders.OfType<IRemoteSimilarItemsProvider<T>>())
+ .Where(p => (p is ILocalSimilarItemsProvider local && local.Supports(itemType))
+ || (p is IRemoteSimilarItemsProvider remote && remote.Supports(itemType)))
.ToList();
}
@@ -88,22 +82,6 @@ public class SimilarItemsManager : ISimilarItemsManager
ArgumentNullException.ThrowIfNull(excludeArtistIds);
var itemType = item.GetType();
- var method = _genericMethodCache.GetOrAdd(itemType, static type => _getSimilarItemsInternalMethod.MakeGenericMethod(type));
-
- var task = (Task<IReadOnlyList<BaseItem>>)method.Invoke(this, [item, excludeArtistIds, user, dtoOptions, limit, libraryOptions, cancellationToken])!;
- return await task.ConfigureAwait(false);
- }
-
- private async Task<IReadOnlyList<BaseItem>> GetSimilarItemsInternalAsync<T>(
- T item,
- IReadOnlyList<Guid> excludeArtistIds,
- User? user,
- DtoOptions dtoOptions,
- int? limit,
- LibraryOptions? libraryOptions,
- CancellationToken cancellationToken)
- where T : BaseItem
- {
var requestedLimit = limit ?? 50;
var itemKind = item.GetBaseItemKind();
@@ -114,11 +92,16 @@ public class SimilarItemsManager : ISimilarItemsManager
}
// Local providers are always enabled. Remote providers must be explicitly enabled.
- var localProviders = _similarItemsProviders.OfType<ILocalSimilarItemsProvider<T>>().Cast<ISimilarItemsProvider>().ToList();
- var remoteProviders = _similarItemsProviders.OfType<IRemoteSimilarItemsProvider<T>>().Cast<ISimilarItemsProvider>();
+ var localProviders = _similarItemsProviders
+ .OfType<ILocalSimilarItemsProvider>()
+ .Where(p => p.Supports(itemType))
+ .ToList();
+ var remoteProviders = _similarItemsProviders
+ .OfType<IRemoteSimilarItemsProvider>()
+ .Where(p => p.Supports(itemType));
var matchingProviders = new List<ISimilarItemsProvider>(localProviders);
- var typeOptions = libraryOptions?.GetTypeOptions(typeof(T).Name);
+ var typeOptions = libraryOptions?.GetTypeOptions(itemType.Name);
if (typeOptions?.SimilarItemProviders?.Length > 0)
{
matchingProviders.AddRange(remoteProviders
@@ -143,7 +126,7 @@ public class SimilarItemsManager : ISimilarItemsManager
try
{
- if (provider is ILocalSimilarItemsProvider<T> localProvider)
+ if (provider is ILocalSimilarItemsProvider localProvider)
{
var query = new SimilarItemsQuery
{
@@ -165,9 +148,9 @@ public class SimilarItemsManager : ISimilarItemsManager
}
}
}
- else if (provider is IRemoteSimilarItemsProvider<T> remoteProvider)
+ else if (provider is IRemoteSimilarItemsProvider remoteProvider)
{
- var cachePath = GetSimilarItemsCachePath(provider.Name, typeof(T).Name, item.Id);
+ var cachePath = GetSimilarItemsCachePath(provider.Name, itemType.Name, item.Id);
var cachedReferences = await TryReadSimilarItemsCacheAsync(cachePath, cancellationToken).ConfigureAwait(false);
if (cachedReferences is not null)
diff --git a/MediaBrowser.Controller/Library/ILocalSimilarItemsProvider.cs b/MediaBrowser.Controller/Library/ILocalSimilarItemsProvider.cs
index 9bf0121f5f..b8e41ec810 100644
--- a/MediaBrowser.Controller/Library/ILocalSimilarItemsProvider.cs
+++ b/MediaBrowser.Controller/Library/ILocalSimilarItemsProvider.cs
@@ -1,3 +1,4 @@
+using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
@@ -6,11 +7,37 @@ using MediaBrowser.Controller.Entities;
namespace MediaBrowser.Controller.Library;
/// <summary>
+/// Provides similar items from the local library.
+/// Returns fully resolved BaseItems directly - no additional resolution needed.
+/// </summary>
+public interface ILocalSimilarItemsProvider : ISimilarItemsProvider
+{
+ /// <summary>
+ /// Determines whether the provider can handle items of the specified type.
+ /// </summary>
+ /// <param name="itemType">The item type.</param>
+ /// <returns><c>true</c> if the provider handles this item type; otherwise <c>false</c>.</returns>
+ bool Supports(Type itemType);
+
+ /// <summary>
+ /// Gets similar items from the local library.
+ /// </summary>
+ /// <param name="item">The source item to find similar items for.</param>
+ /// <param name="query">The query options (user, limit, exclusions, etc.).</param>
+ /// <param name="cancellationToken">Cancellation token.</param>
+ /// <returns>The list of similar items from the library.</returns>
+ Task<IReadOnlyList<BaseItem>> GetSimilarItemsAsync(
+ BaseItem item,
+ SimilarItemsQuery query,
+ CancellationToken cancellationToken);
+}
+
+/// <summary>
/// Provides similar items from the local library for a specific item type.
/// Returns fully resolved BaseItems directly - no additional resolution needed.
/// </summary>
/// <typeparam name="TItemType">The type of item this provider handles.</typeparam>
-public interface ILocalSimilarItemsProvider<TItemType> : ISimilarItemsProvider
+public interface ILocalSimilarItemsProvider<TItemType> : ILocalSimilarItemsProvider
where TItemType : BaseItem
{
/// <summary>
@@ -24,4 +51,13 @@ public interface ILocalSimilarItemsProvider<TItemType> : ISimilarItemsProvider
TItemType item,
SimilarItemsQuery query,
CancellationToken cancellationToken);
+
+ bool ILocalSimilarItemsProvider.Supports(Type itemType)
+ => typeof(TItemType).IsAssignableFrom(itemType);
+
+ Task<IReadOnlyList<BaseItem>> ILocalSimilarItemsProvider.GetSimilarItemsAsync(
+ BaseItem item,
+ SimilarItemsQuery query,
+ CancellationToken cancellationToken)
+ => GetSimilarItemsAsync((TItemType)item, query, cancellationToken);
}
diff --git a/MediaBrowser.Controller/Library/IRemoteSimilarItemsProvider.cs b/MediaBrowser.Controller/Library/IRemoteSimilarItemsProvider.cs
index a77b6628d9..3803e51769 100644
--- a/MediaBrowser.Controller/Library/IRemoteSimilarItemsProvider.cs
+++ b/MediaBrowser.Controller/Library/IRemoteSimilarItemsProvider.cs
@@ -1,3 +1,4 @@
+using System;
using System.Collections.Generic;
using System.Threading;
using MediaBrowser.Controller.Entities;
@@ -5,11 +6,37 @@ using MediaBrowser.Controller.Entities;
namespace MediaBrowser.Controller.Library;
/// <summary>
+/// Provides similar item references from remote/external sources.
+/// Returns lightweight references with ProviderIds that the manager resolves to library items.
+/// </summary>
+public interface IRemoteSimilarItemsProvider : ISimilarItemsProvider
+{
+ /// <summary>
+ /// Determines whether the provider can handle items of the specified type.
+ /// </summary>
+ /// <param name="itemType">The item type.</param>
+ /// <returns><c>true</c> if the provider handles this item type; otherwise <c>false</c>.</returns>
+ bool Supports(Type itemType);
+
+ /// <summary>
+ /// Gets similar item references from an external source as an async stream.
+ /// </summary>
+ /// <param name="item">The source item to find similar items for.</param>
+ /// <param name="query">The query options (user, limit, exclusions).</param>
+ /// <param name="cancellationToken">Cancellation token.</param>
+ /// <returns>An async enumerable of similar item references.</returns>
+ IAsyncEnumerable<SimilarItemReference> GetSimilarItemsAsync(
+ BaseItem item,
+ SimilarItemsQuery query,
+ CancellationToken cancellationToken);
+}
+
+/// <summary>
/// Provides similar item references from remote/external sources for a specific item type.
/// Returns lightweight references with ProviderIds that the manager resolves to library items.
/// </summary>
/// <typeparam name="TItemType">The type of item this provider handles.</typeparam>
-public interface IRemoteSimilarItemsProvider<TItemType> : ISimilarItemsProvider
+public interface IRemoteSimilarItemsProvider<TItemType> : IRemoteSimilarItemsProvider
where TItemType : BaseItem
{
/// <summary>
@@ -23,4 +50,13 @@ public interface IRemoteSimilarItemsProvider<TItemType> : ISimilarItemsProvider
TItemType item,
SimilarItemsQuery query,
CancellationToken cancellationToken);
+
+ bool IRemoteSimilarItemsProvider.Supports(Type itemType)
+ => typeof(TItemType).IsAssignableFrom(itemType);
+
+ IAsyncEnumerable<SimilarItemReference> IRemoteSimilarItemsProvider.GetSimilarItemsAsync(
+ BaseItem item,
+ SimilarItemsQuery query,
+ CancellationToken cancellationToken)
+ => GetSimilarItemsAsync((TItemType)item, query, cancellationToken);
}
diff --git a/MediaBrowser.Providers/Plugins/ListenBrainz/Api/ListenBrainzLabsClient.cs b/MediaBrowser.Providers/Plugins/ListenBrainz/Api/ListenBrainzLabsClient.cs
index e57aa3ed1d..e080370b8c 100644
--- a/MediaBrowser.Providers/Plugins/ListenBrainz/Api/ListenBrainzLabsClient.cs
+++ b/MediaBrowser.Providers/Plugins/ListenBrainz/Api/ListenBrainzLabsClient.cs
@@ -15,11 +15,11 @@ namespace MediaBrowser.Providers.Plugins.ListenBrainz.Api;
/// <summary>
/// Client for the ListenBrainz Labs API.
/// </summary>
-public class ListenBrainzLabsClient
+public class ListenBrainzLabsClient : IDisposable
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger<ListenBrainzLabsClient> _logger;
- private readonly Lock _rateLimitLock = new();
+ private readonly SemaphoreSlim _rateLimitLock = new(1, 1);
private DateTime _lastRequestTime = DateTime.MinValue;
@@ -52,7 +52,7 @@ public class ListenBrainzLabsClient
var rateLimit = config?.RateLimit ?? PluginConfiguration.DefaultRateLimit;
// Enforce rate limit
- EnforceRateLimit(rateLimit);
+ await EnforceRateLimitAsync(rateLimit, cancellationToken).ConfigureAwait(false);
var url = $"{baseUrl}/similar-artists/json?artist_mbids={artistMbid}&algorithm={algorithm}";
@@ -86,19 +86,43 @@ public class ListenBrainzLabsClient
}
}
- private void EnforceRateLimit(double rateLimitSeconds)
+ /// <inheritdoc />
+ public void Dispose()
{
- lock (_rateLimitLock)
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <summary>
+ /// Releases unmanaged and - optionally - managed resources.
+ /// </summary>
+ /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _rateLimitLock.Dispose();
+ }
+ }
+
+ private async Task EnforceRateLimitAsync(double rateLimitSeconds, CancellationToken cancellationToken)
+ {
+ await _rateLimitLock.WaitAsync(cancellationToken).ConfigureAwait(false);
+ try
{
var timeSinceLastRequest = DateTime.UtcNow - _lastRequestTime;
var requiredDelay = TimeSpan.FromSeconds(rateLimitSeconds) - timeSinceLastRequest;
if (requiredDelay > TimeSpan.Zero)
{
- Thread.Sleep(requiredDelay);
+ await Task.Delay(requiredDelay, cancellationToken).ConfigureAwait(false);
}
_lastRequestTime = DateTime.UtcNow;
}
+ finally
+ {
+ _rateLimitLock.Release();
+ }
}
}