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();
+ }
+ }
}