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; /// /// Provides similar items for movies and trailers. /// -public class MovieSimilarItemsProvider : ILocalSimilarItemsProvider, ILocalSimilarItemsProvider +public sealed class MovieSimilarItemsProvider : ILocalSimilarItemsProvider, ILocalSimilarItemsProvider { private readonly ILibraryManager _libraryManager; private readonly IServerConfigurationManager _serverConfigurationManager; @@ -51,6 +52,17 @@ public class MovieSimilarItemsProvider : ILocalSimilarItemsProvider, ILoc return Task.FromResult(GetSimilarMovieItems(item, query)); } + bool ILocalSimilarItemsProvider.Supports(Type itemType) + => typeof(Movie).IsAssignableFrom(itemType) || typeof(Trailer).IsAssignableFrom(itemType); + + Task> 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 GetSimilarMovieItems(BaseItem item, SimilarItemsQuery query) { var includeItemTypes = new List { 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; /// public class SimilarItemsManager : ISimilarItemsManager { - private static readonly ConcurrentDictionary _genericMethodCache = new(); - private static readonly MethodInfo _getSimilarItemsInternalMethod = typeof(SimilarItemsManager) - .GetMethod(nameof(GetSimilarItemsInternalAsync), BindingFlags.NonPublic | BindingFlags.Instance)!; - private readonly ILogger _logger; private readonly IServerApplicationPaths _appPaths; private readonly ILibraryManager _libraryManager; @@ -67,10 +61,10 @@ public class SimilarItemsManager : ISimilarItemsManager public IReadOnlyList GetSimilarItemsProviders() where T : BaseItem { + var itemType = typeof(T); return _similarItemsProviders - .OfType>() - .Cast() - .Concat(_similarItemsProviders.OfType>()) + .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>)method.Invoke(this, [item, excludeArtistIds, user, dtoOptions, limit, libraryOptions, cancellationToken])!; - return await task.ConfigureAwait(false); - } - - private async Task> GetSimilarItemsInternalAsync( - T item, - IReadOnlyList 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>().Cast().ToList(); - var remoteProviders = _similarItemsProviders.OfType>().Cast(); + var localProviders = _similarItemsProviders + .OfType() + .Where(p => p.Supports(itemType)) + .ToList(); + var remoteProviders = _similarItemsProviders + .OfType() + .Where(p => p.Supports(itemType)); var matchingProviders = new List(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 localProvider) + if (provider is ILocalSimilarItemsProvider localProvider) { var query = new SimilarItemsQuery { @@ -165,9 +148,9 @@ public class SimilarItemsManager : ISimilarItemsManager } } } - else if (provider is IRemoteSimilarItemsProvider 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; @@ -5,12 +6,38 @@ using MediaBrowser.Controller.Entities; namespace MediaBrowser.Controller.Library; +/// +/// Provides similar items from the local library. +/// Returns fully resolved BaseItems directly - no additional resolution needed. +/// +public interface ILocalSimilarItemsProvider : ISimilarItemsProvider +{ + /// + /// Determines whether the provider can handle items of the specified type. + /// + /// The item type. + /// true if the provider handles this item type; otherwise false. + bool Supports(Type itemType); + + /// + /// Gets similar items from the local library. + /// + /// The source item to find similar items for. + /// The query options (user, limit, exclusions, etc.). + /// Cancellation token. + /// The list of similar items from the library. + Task> GetSimilarItemsAsync( + BaseItem item, + SimilarItemsQuery query, + CancellationToken cancellationToken); +} + /// /// Provides similar items from the local library for a specific item type. /// Returns fully resolved BaseItems directly - no additional resolution needed. /// /// The type of item this provider handles. -public interface ILocalSimilarItemsProvider : ISimilarItemsProvider +public interface ILocalSimilarItemsProvider : ILocalSimilarItemsProvider where TItemType : BaseItem { /// @@ -24,4 +51,13 @@ public interface ILocalSimilarItemsProvider : ISimilarItemsProvider TItemType item, SimilarItemsQuery query, CancellationToken cancellationToken); + + bool ILocalSimilarItemsProvider.Supports(Type itemType) + => typeof(TItemType).IsAssignableFrom(itemType); + + Task> 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,15 +1,42 @@ +using System; using System.Collections.Generic; using System.Threading; using MediaBrowser.Controller.Entities; namespace MediaBrowser.Controller.Library; +/// +/// Provides similar item references from remote/external sources. +/// Returns lightweight references with ProviderIds that the manager resolves to library items. +/// +public interface IRemoteSimilarItemsProvider : ISimilarItemsProvider +{ + /// + /// Determines whether the provider can handle items of the specified type. + /// + /// The item type. + /// true if the provider handles this item type; otherwise false. + bool Supports(Type itemType); + + /// + /// Gets similar item references from an external source as an async stream. + /// + /// The source item to find similar items for. + /// The query options (user, limit, exclusions). + /// Cancellation token. + /// An async enumerable of similar item references. + IAsyncEnumerable GetSimilarItemsAsync( + BaseItem item, + SimilarItemsQuery query, + CancellationToken cancellationToken); +} + /// /// 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. /// /// The type of item this provider handles. -public interface IRemoteSimilarItemsProvider : ISimilarItemsProvider +public interface IRemoteSimilarItemsProvider : IRemoteSimilarItemsProvider where TItemType : BaseItem { /// @@ -23,4 +50,13 @@ public interface IRemoteSimilarItemsProvider : ISimilarItemsProvider TItemType item, SimilarItemsQuery query, CancellationToken cancellationToken); + + bool IRemoteSimilarItemsProvider.Supports(Type itemType) + => typeof(TItemType).IsAssignableFrom(itemType); + + IAsyncEnumerable 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; /// /// Client for the ListenBrainz Labs API. /// -public class ListenBrainzLabsClient +public class ListenBrainzLabsClient : IDisposable { private readonly IHttpClientFactory _httpClientFactory; private readonly ILogger _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) + /// + public void Dispose() { - lock (_rateLimitLock) + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + 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(); + } } }