diff options
| author | Shadowghost <Ghost_of_Stone@web.de> | 2026-05-13 00:58:03 +0200 |
|---|---|---|
| committer | Shadowghost <Ghost_of_Stone@web.de> | 2026-05-13 00:58:33 +0200 |
| commit | b1b45199444e369b12844661f09d1cd0830d25f7 (patch) | |
| tree | 8c49e54b9f07f30bfafe4f1268edacbe33811827 | |
| parent | 4ebce3907062ade1937440628eebd665440b338d (diff) | |
Apply review suggestions
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(); + } } } |
