Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Caching for api calls #118

Merged
merged 7 commits into from
Mar 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions Jellyfin.Plugin.Tvdb/ScheduledTasks/PurgeCacheTask.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Task to purge TheTVDB plugin cache.
/// </summary>
public class PurgeCacheTask : IScheduledTask
{
private readonly ILogger<PurgeCacheTask> _logger;
private readonly TvdbClientManager _tvdbClientManager;

/// <summary>
/// Initializes a new instance of the <see cref="PurgeCacheTask"/> class.
/// </summary>
/// <param name="logger">Instance of the <see cref="ILogger{TvdbScheduledTask}"/> interface.</param>
/// <param name="tvdbClientManager">Instance of <see cref="TvdbClientManager"/>.</param>
public PurgeCacheTask(
ILogger<PurgeCacheTask> logger,
TvdbClientManager tvdbClientManager)
{
_logger = logger;
_tvdbClientManager = tvdbClientManager;
}

/// <inheritdoc/>
public string Name => "Purge TheTVDB plugin cache";

/// <inheritdoc/>
public string Key => "PurgeTheTVDBPluginCache";

/// <inheritdoc/>
public string Description => "Purges the TheTVDB Cache";

/// <inheritdoc/>
public string Category => "TheTVDB";

/// <inheritdoc/>
public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
{
if (_tvdbClientManager.PurgeCache())
{
_logger.LogInformation("TheTvdb plugin cache purged successfully");
}
else
{
_logger.LogError("TheTvdb plugin cache purge failed");
}

return Task.CompletedTask;
}

/// <inheritdoc/>
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
{
return Enumerable.Empty<TaskTriggerInfo>();
}
}
}
143 changes: 141 additions & 2 deletions Jellyfin.Plugin.Tvdb/TvdbClientManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -18,13 +19,15 @@ namespace Jellyfin.Plugin.Tvdb;
/// <summary>
/// Tvdb client manager.
/// </summary>
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;
Expand All @@ -38,6 +41,7 @@ public TvdbClientManager(IApplicationHost applicationHost)
_serviceProvider = ConfigureService(applicationHost);
_httpClientFactory = _serviceProvider.GetRequiredService<IHttpClientFactory>();
_sdkClientSettings = _serviceProvider.GetRequiredService<SdkClientSettings>();
_memoryCache = new MemoryCache(new MemoryCacheOptions());

_tokenUpdatedAt = DateTime.MinValue;
}
Expand Down Expand Up @@ -95,10 +99,17 @@ public async Task<IReadOnlyList<SearchResult>> GetSeriesByNameAsync(
string language,
CancellationToken cancellationToken)
{
var key = $"TvdbSeriesSearch_{name}";
if (_memoryCache.TryGetValue(key, out IReadOnlyList<SearchResult> series))
{
return series;
}

var searchClient = _serviceProvider.GetRequiredService<ISearchClient>();
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;
}

Expand All @@ -114,10 +125,17 @@ public async Task<SeriesBaseRecord> 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<ISeriesClient>();
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;
}

Expand All @@ -137,10 +155,17 @@ public async Task<SeriesExtendedRecord> 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<ISeriesClient>();
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;
}

Expand All @@ -158,10 +183,17 @@ public async Task<Data2> 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<ISeriesClient>();
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;
}

Expand All @@ -177,10 +209,17 @@ public async Task<SeasonExtendedRecord> 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<ISeasonsClient>();
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;
}

Expand All @@ -196,10 +235,17 @@ public async Task<EpisodeExtendedRecord> 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<IEpisodesClient>();
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;
}

Expand All @@ -215,10 +261,17 @@ public async Task<IReadOnlyList<SearchByRemoteIdResult>> GetSeriesByRemoteIdAsyn
string language,
CancellationToken cancellationToken)
{
var key = $"TvdbSeriesRemoteId_{remoteId}";
if (_memoryCache.TryGetValue(key, out IReadOnlyList<SearchByRemoteIdResult> series))
{
return series;
}

var searchClient = _serviceProvider.GetRequiredService<ISearchClient>();
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;
}

Expand All @@ -234,10 +287,17 @@ public async Task<PeopleBaseRecord> 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<IPeopleClient>();
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;
}

Expand All @@ -253,10 +313,17 @@ public async Task<ArtworkExtendedRecord> 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<IArtworkClient>();
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;
}

Expand All @@ -272,10 +339,17 @@ public async Task<SeriesExtendedRecord> 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<ISeriesClient>();
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;
}

Expand All @@ -286,10 +360,17 @@ public async Task<SeriesExtendedRecord> GetSeriesImagesAsync(
/// <returns>All tvdb languages.</returns>
public async Task<IReadOnlyList<Language>> GetLanguagesAsync(CancellationToken cancellationToken)
{
var key = "TvdbLanguages";
if (_memoryCache.TryGetValue(key, out IReadOnlyList<Language> languages))
{
return languages;
}

var languagesClient = _serviceProvider.GetRequiredService<ILanguagesClient>();
await LoginAsync().ConfigureAwait(false);
var languagesResult = await languagesClient.GetAllLanguagesAsync(cancellationToken: cancellationToken)
.ConfigureAwait(false);
_memoryCache.Set(key, languagesResult.Data, TimeSpan.FromDays(1));
return languagesResult.Data;
}

Expand All @@ -300,10 +381,17 @@ public async Task<IReadOnlyList<Language>> GetLanguagesAsync(CancellationToken c
/// <returns>All tvdb artwork types.</returns>
public async Task<IReadOnlyList<ArtworkType>> GetArtworkTypeAsync(CancellationToken cancellationToken)
{
var key = "TvdbArtworkTypes";
if (_memoryCache.TryGetValue(key, out IReadOnlyList<ArtworkType> artworkTypes))
{
return artworkTypes;
}

var artworkTypesClient = _serviceProvider.GetRequiredService<IArtwork_TypesClient>();
await LoginAsync().ConfigureAwait(false);
var artworkTypesResult = await artworkTypesClient.GetAllArtworkTypesAsync(cancellationToken: cancellationToken)
.ConfigureAwait(false);
_memoryCache.Set(key, artworkTypesResult.Data, TimeSpan.FromDays(1));
return artworkTypesResult.Data;
}

Expand Down Expand Up @@ -331,6 +419,7 @@ public async Task<IReadOnlyList<ArtworkType>> 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)
{
Expand Down Expand Up @@ -359,11 +448,19 @@ public async Task<IReadOnlyList<ArtworkType>> 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;
Expand Down Expand Up @@ -393,7 +490,30 @@ public async Task<IReadOnlyList<ArtworkType>> 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;
}
}

/// <summary>
/// Purge the cache.
/// </summary>
/// <returns>True if success else false.</returns>
public bool PurgeCache()
{
if (_memoryCache is MemoryCache memoryCache)
{
memoryCache.Compact(1);
Shadowghost marked this conversation as resolved.
Show resolved Hide resolved
return true;
}
else
{
return false;
}
}

Expand Down Expand Up @@ -441,4 +561,23 @@ private ServiceProvider ConfigureService(IApplicationHost applicationHost)

return services.BuildServiceProvider();
}

/// <inheritdoc/>
public void Dispose()
{
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)
{
_memoryCache?.Dispose();
}
}
}