diff --git a/Jellyfin.Plugin.Tvdb/ScheduledTasks/PurgeCacheTask.cs b/Jellyfin.Plugin.Tvdb/ScheduledTasks/PurgeCacheTask.cs new file mode 100644 index 0000000..f949c4f --- /dev/null +++ b/Jellyfin.Plugin.Tvdb/ScheduledTasks/PurgeCacheTask.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Model.Tasks; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Plugin.Tvdb.ScheduledTasks +{ + /// + /// Task to purge TheTVDB plugin cache. + /// + public class PurgeCacheTask : IScheduledTask + { + private readonly ILogger _logger; + private readonly TvdbClientManager _tvdbClientManager; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of . + public PurgeCacheTask( + ILogger logger, + TvdbClientManager tvdbClientManager) + { + _logger = logger; + _tvdbClientManager = tvdbClientManager; + } + + /// + public string Name => "Purge TheTVDB plugin cache"; + + /// + public string Key => "PurgeTheTVDBPluginCache"; + + /// + public string Description => "Purges the TheTVDB Cache"; + + /// + public string Category => "TheTVDB"; + + /// + public Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken) + { + if (_tvdbClientManager.PurgeCache()) + { + _logger.LogInformation("TheTvdb plugin cache purged successfully"); + } + else + { + _logger.LogError("TheTvdb plugin cache purge failed"); + } + + return Task.CompletedTask; + } + + /// + public IEnumerable GetDefaultTriggers() + { + return Enumerable.Empty(); + } + } +} diff --git a/Jellyfin.Plugin.Tvdb/TvdbClientManager.cs b/Jellyfin.Plugin.Tvdb/TvdbClientManager.cs index 52d3913..a07be96 100644 --- a/Jellyfin.Plugin.Tvdb/TvdbClientManager.cs +++ b/Jellyfin.Plugin.Tvdb/TvdbClientManager.cs @@ -10,6 +10,7 @@ using Jellyfin.Plugin.Tvdb.Configuration; using MediaBrowser.Common; using MediaBrowser.Controller.Providers; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; using Tvdb.Sdk; @@ -18,13 +19,15 @@ namespace Jellyfin.Plugin.Tvdb; /// /// Tvdb client manager. /// -public class TvdbClientManager +public class TvdbClientManager : IDisposable { private const string TvdbHttpClient = "TvdbHttpClient"; + private const int CacheDurationInHours = 1; private static readonly SemaphoreSlim _tokenUpdateLock = new SemaphoreSlim(1, 1); private readonly IHttpClientFactory _httpClientFactory; private readonly IServiceProvider _serviceProvider; + private readonly IMemoryCache _memoryCache; private readonly SdkClientSettings _sdkClientSettings; private DateTime _tokenUpdatedAt; @@ -38,6 +41,7 @@ public TvdbClientManager(IApplicationHost applicationHost) _serviceProvider = ConfigureService(applicationHost); _httpClientFactory = _serviceProvider.GetRequiredService(); _sdkClientSettings = _serviceProvider.GetRequiredService(); + _memoryCache = new MemoryCache(new MemoryCacheOptions()); _tokenUpdatedAt = DateTime.MinValue; } @@ -95,10 +99,17 @@ public async Task> GetSeriesByNameAsync( string language, CancellationToken cancellationToken) { + var key = $"TvdbSeriesSearch_{name}"; + if (_memoryCache.TryGetValue(key, out IReadOnlyList series)) + { + return series; + } + var searchClient = _serviceProvider.GetRequiredService(); await LoginAsync().ConfigureAwait(false); var searchResult = await searchClient.GetSearchResultsAsync(query: name, type: "series", limit: 5, cancellationToken: cancellationToken) .ConfigureAwait(false); + _memoryCache.Set(key, searchResult.Data, TimeSpan.FromHours(CacheDurationInHours)); return searchResult.Data; } @@ -114,10 +125,17 @@ public async Task GetSeriesByIdAsync( string language, CancellationToken cancellationToken) { + var key = $"TvdbSeries_{tvdbId.ToString(CultureInfo.InvariantCulture)}"; + if (_memoryCache.TryGetValue(key, out SeriesBaseRecord series)) + { + return series; + } + var seriesClient = _serviceProvider.GetRequiredService(); await LoginAsync().ConfigureAwait(false); var seriesResult = await seriesClient.GetSeriesBaseAsync(id: tvdbId, cancellationToken: cancellationToken) .ConfigureAwait(false); + _memoryCache.Set(key, seriesResult.Data, TimeSpan.FromHours(CacheDurationInHours)); return seriesResult.Data; } @@ -137,10 +155,17 @@ public async Task GetSeriesExtendedByIdAsync( Meta4? meta = null, bool? small = null) { + var key = $"TvdbSeriesExtended_{tvdbId.ToString(CultureInfo.InvariantCulture)}"; + if (_memoryCache.TryGetValue(key, out SeriesExtendedRecord series)) + { + return series; + } + var seriesClient = _serviceProvider.GetRequiredService(); await LoginAsync().ConfigureAwait(false); var seriesResult = await seriesClient.GetSeriesExtendedAsync(id: tvdbId, meta: meta, @short: small, cancellationToken: cancellationToken) .ConfigureAwait(false); + _memoryCache.Set(key, seriesResult.Data, TimeSpan.FromHours(CacheDurationInHours)); return seriesResult.Data; } @@ -158,10 +183,17 @@ public async Task GetSeriesEpisodesAsync( string seasonType, CancellationToken cancellationToken) { + var key = $"TvdbSeriesEpisodes_{tvdbId.ToString(CultureInfo.InvariantCulture)}"; + if (_memoryCache.TryGetValue(key, out Data2 series)) + { + return series; + } + var seriesClient = _serviceProvider.GetRequiredService(); await LoginAsync().ConfigureAwait(false); var seriesResult = await seriesClient.GetSeriesEpisodesAsync(id: tvdbId, season_type: seasonType, cancellationToken: cancellationToken, page: 0) .ConfigureAwait(false); + _memoryCache.Set(key, seriesResult.Data, TimeSpan.FromHours(CacheDurationInHours)); return seriesResult.Data; } @@ -177,10 +209,17 @@ public async Task GetSeasonByIdAsync( string language, CancellationToken cancellationToken) { + var key = $"TvdbSeason_{seasonTvdbId.ToString(CultureInfo.InvariantCulture)}"; + if (_memoryCache.TryGetValue(key, out SeasonExtendedRecord season)) + { + return season; + } + var seasonClient = _serviceProvider.GetRequiredService(); await LoginAsync().ConfigureAwait(false); var seasonResult = await seasonClient.GetSeasonExtendedAsync(id: seasonTvdbId, cancellationToken: cancellationToken) .ConfigureAwait(false); + _memoryCache.Set(key, seasonResult.Data, TimeSpan.FromHours(CacheDurationInHours)); return seasonResult.Data; } @@ -196,10 +235,17 @@ public async Task GetEpisodesAsync( string language, CancellationToken cancellationToken) { + var key = $"TvdbEpisode_{episodeTvdbId.ToString(CultureInfo.InvariantCulture)}"; + if (_memoryCache.TryGetValue(key, out EpisodeExtendedRecord episode)) + { + return episode; + } + var episodeClient = _serviceProvider.GetRequiredService(); await LoginAsync().ConfigureAwait(false); var episodeResult = await episodeClient.GetEpisodeExtendedAsync(id: episodeTvdbId, meta: Meta.Translations, cancellationToken: cancellationToken) .ConfigureAwait(false); + _memoryCache.Set(key, episodeResult.Data, TimeSpan.FromHours(CacheDurationInHours)); return episodeResult.Data; } @@ -215,10 +261,17 @@ public async Task> GetSeriesByRemoteIdAsyn string language, CancellationToken cancellationToken) { + var key = $"TvdbSeriesRemoteId_{remoteId}"; + if (_memoryCache.TryGetValue(key, out IReadOnlyList series)) + { + return series; + } + var searchClient = _serviceProvider.GetRequiredService(); await LoginAsync().ConfigureAwait(false); var searchResult = await searchClient.GetSearchResultsByRemoteIdAsync(remoteId: remoteId, cancellationToken: cancellationToken) .ConfigureAwait(false); + _memoryCache.Set(key, searchResult.Data, TimeSpan.FromHours(CacheDurationInHours)); return searchResult.Data; } @@ -234,10 +287,17 @@ public async Task GetActorAsync( string language, CancellationToken cancellationToken) { + var key = $"TvdbPeople_{tvdbId.ToString(CultureInfo.InvariantCulture)}"; + if (_memoryCache.TryGetValue(key, out PeopleBaseRecord people)) + { + return people; + } + var peopleClient = _serviceProvider.GetRequiredService(); await LoginAsync().ConfigureAwait(false); var peopleResult = await peopleClient.GetPeopleBaseAsync(id: tvdbId, cancellationToken: cancellationToken) .ConfigureAwait(false); + _memoryCache.Set(key, peopleResult.Data, TimeSpan.FromHours(CacheDurationInHours)); return peopleResult.Data; } @@ -253,10 +313,17 @@ public async Task GetImageAsync( string language, CancellationToken cancellationToken) { + var key = $"TvdbArtwork_{imageTvdbId.ToString(CultureInfo.InvariantCulture)}"; + if (_memoryCache.TryGetValue(key, out ArtworkExtendedRecord artwork)) + { + return artwork; + } + var artworkClient = _serviceProvider.GetRequiredService(); await LoginAsync().ConfigureAwait(false); var artworkResult = await artworkClient.GetArtworkExtendedAsync(id: imageTvdbId, cancellationToken: cancellationToken) .ConfigureAwait(false); + _memoryCache.Set(key, artworkResult.Data, TimeSpan.FromHours(CacheDurationInHours)); return artworkResult.Data; } @@ -272,10 +339,17 @@ public async Task GetSeriesImagesAsync( string language, CancellationToken cancellationToken) { + var key = $"TvdbSeriesArtwork_{tvdbId.ToString(CultureInfo.InvariantCulture)}"; + if (_memoryCache.TryGetValue(key, out SeriesExtendedRecord series)) + { + return series; + } + var seriesClient = _serviceProvider.GetRequiredService(); await LoginAsync().ConfigureAwait(false); var seriesResult = await seriesClient.GetSeriesArtworksAsync(id: tvdbId, cancellationToken: cancellationToken) .ConfigureAwait(false); + _memoryCache.Set(key, seriesResult.Data, TimeSpan.FromHours(CacheDurationInHours)); return seriesResult.Data; } @@ -286,10 +360,17 @@ public async Task GetSeriesImagesAsync( /// All tvdb languages. public async Task> GetLanguagesAsync(CancellationToken cancellationToken) { + var key = "TvdbLanguages"; + if (_memoryCache.TryGetValue(key, out IReadOnlyList languages)) + { + return languages; + } + var languagesClient = _serviceProvider.GetRequiredService(); await LoginAsync().ConfigureAwait(false); var languagesResult = await languagesClient.GetAllLanguagesAsync(cancellationToken: cancellationToken) .ConfigureAwait(false); + _memoryCache.Set(key, languagesResult.Data, TimeSpan.FromDays(1)); return languagesResult.Data; } @@ -300,10 +381,17 @@ public async Task> GetLanguagesAsync(CancellationToken c /// All tvdb artwork types. public async Task> GetArtworkTypeAsync(CancellationToken cancellationToken) { + var key = "TvdbArtworkTypes"; + if (_memoryCache.TryGetValue(key, out IReadOnlyList artworkTypes)) + { + return artworkTypes; + } + var artworkTypesClient = _serviceProvider.GetRequiredService(); await LoginAsync().ConfigureAwait(false); var artworkTypesResult = await artworkTypesClient.GetAllArtworkTypesAsync(cancellationToken: cancellationToken) .ConfigureAwait(false); + _memoryCache.Set(key, artworkTypesResult.Data, TimeSpan.FromDays(1)); return artworkTypesResult.Data; } @@ -331,6 +419,7 @@ public async Task> GetArtworkTypeAsync(CancellationTo int? seasonNumber = null; string? airDate = null; bool special = false; + string? key = null; // Prefer SxE over premiere date as it is more robust if (searchInfo.IndexNumber.HasValue && searchInfo.ParentIndexNumber.HasValue) { @@ -359,11 +448,19 @@ public async Task> GetArtworkTypeAsync(CancellationTo seasonNumber = searchInfo.ParentIndexNumber.Value; break; } + + key = $"FindTvdbEpisodeId_{seriesTvdbIdString}_{seasonNumber.Value.ToString(CultureInfo.InvariantCulture)}_{episodeNumber.Value.ToString(CultureInfo.InvariantCulture)}"; } else if (searchInfo.PremiereDate.HasValue) { // tvdb expects yyyy-mm-dd format airDate = searchInfo.PremiereDate.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); + key = $"FindTvdbEpisodeId_{seriesTvdbIdString}_{airDate}"; + } + + if (key != null && _memoryCache.TryGetValue(key, out string? episodeTvdbId)) + { + return episodeTvdbId; } Response56 seriesResponse; @@ -393,7 +490,30 @@ public async Task> GetArtworkTypeAsync(CancellationTo } else { - return seriesData.Episodes[0].Id?.ToString(CultureInfo.InvariantCulture); + var tvdbId = seriesData.Episodes[0].Id?.ToString(CultureInfo.InvariantCulture); + if (key != null) + { + _memoryCache.Set(key, tvdbId, TimeSpan.FromHours(CacheDurationInHours)); + } + + return tvdbId; + } + } + + /// + /// Purge the cache. + /// + /// True if success else false. + public bool PurgeCache() + { + if (_memoryCache is MemoryCache memoryCache) + { + memoryCache.Compact(1); + return true; + } + else + { + return false; } } @@ -441,4 +561,23 @@ private ServiceProvider ConfigureService(IApplicationHost applicationHost) return services.BuildServiceProvider(); } + + /// + public void Dispose() + { + 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) + { + _memoryCache?.Dispose(); + } + } }