Skip to content

Commit

Permalink
Make Tvdb Client usage more resilient and robust (#112)
Browse files Browse the repository at this point in the history
* catch JsonException

* refactor duplicate code and catch JsonException
  • Loading branch information
BobSilent authored Feb 29, 2024
1 parent a143e0c commit dbe5456
Show file tree
Hide file tree
Showing 7 changed files with 372 additions and 307 deletions.
24 changes: 24 additions & 0 deletions Jellyfin.Plugin.Tvdb/CollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Collections;
using System.Collections.Generic;

namespace Jellyfin.Plugin.Tvdb;

internal static class CollectionExtensions
{
/// <summary>
/// Adds the <paramref name="item"/> if it is not <see langword="null"/>.
/// </summary>
/// <typeparam name="T">Data type of the collection items.</typeparam>
/// <param name="collection">Input <see cref="ICollection"/>.</param>
/// <param name="item">Item to add.</param>
/// <returns>The input collection.</returns>
public static ICollection<T> AddIfNotNull<T>(this ICollection<T> collection, T? item)
{
if (item != null)
{
collection.Add(item);
}

return collection;
}
}
24 changes: 3 additions & 21 deletions Jellyfin.Plugin.Tvdb/Providers/TvdbEpisodeImageProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;

using Microsoft.Extensions.Logging;
using Tvdb.Sdk;

namespace Jellyfin.Plugin.Tvdb.Providers
{
Expand Down Expand Up @@ -94,11 +95,7 @@ await _tvdbClientManager
.GetEpisodesAsync(Convert.ToInt32(episodeTvdbId, CultureInfo.InvariantCulture), language, cancellationToken)
.ConfigureAwait(false);

var image = GetImageInfo(episodeResult);
if (image != null)
{
imageResult.Add(image);
}
imageResult.AddIfNotNull(episodeResult.CreateImageInfo(Name));
}
catch (Exception e)
{
Expand All @@ -114,20 +111,5 @@ public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken
{
return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(new Uri(url), cancellationToken);
}

private RemoteImageInfo? GetImageInfo(EpisodeExtendedRecord episode)
{
if (string.IsNullOrEmpty(episode.Image))
{
return null;
}

return new RemoteImageInfo
{
ProviderName = Name,
Url = episode.Image,
Type = ImageType.Primary
};
}
}
}
199 changes: 98 additions & 101 deletions Jellyfin.Plugin.Tvdb/Providers/TvdbSeasonImageProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,135 +3,132 @@
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.Providers;

using Microsoft.Extensions.Logging;

using Tvdb.Sdk;
using RatingType = MediaBrowser.Model.Dto.RatingType;

namespace Jellyfin.Plugin.Tvdb.Providers
namespace Jellyfin.Plugin.Tvdb.Providers;

/// <summary>
/// Tvdb season image provider.
/// </summary>
public class TvdbSeasonImageProvider : IRemoteImageProvider
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger<TvdbSeasonImageProvider> _logger;
private readonly TvdbClientManager _tvdbClientManager;

/// <summary>
/// Tvdb season image provider.
/// Initializes a new instance of the <see cref="TvdbSeasonImageProvider"/> class.
/// </summary>
public class TvdbSeasonImageProvider : IRemoteImageProvider
/// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger{TvdbSeasonImageProvider}"/> interface.</param>
/// <param name="tvdbClientManager">Instance of <see cref="TvdbClientManager"/>.</param>
public TvdbSeasonImageProvider(
IHttpClientFactory httpClientFactory,
ILogger<TvdbSeasonImageProvider> logger,
TvdbClientManager tvdbClientManager)
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger<TvdbSeasonImageProvider> _logger;
private readonly TvdbClientManager _tvdbClientManager;

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

/// <inheritdoc />
public string Name => TvdbPlugin.ProviderName;

/// <inheritdoc />
public bool Supports(BaseItem item)
{
return item is Season;
}

/// <inheritdoc />
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
{
yield return ImageType.Primary;
yield return ImageType.Banner;
yield return ImageType.Backdrop;
}

/// <inheritdoc />
public string Name => TvdbPlugin.ProviderName;
/// <inheritdoc />
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
{
var season = (Season)item;
var series = season.Series;

/// <inheritdoc />
public bool Supports(BaseItem item)
if (series == null || !season.IndexNumber.HasValue || !TvdbSeriesProvider.IsValidSeries(series.ProviderIds))
{
return item is Season;
return Enumerable.Empty<RemoteImageInfo>();
}

/// <inheritdoc />
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
var languages = await _tvdbClientManager.GetLanguagesAsync(cancellationToken)
.ConfigureAwait(false);
var languageLookup = languages
.ToDictionary(l => l.Id, StringComparer.OrdinalIgnoreCase);

var artworkTypes = await _tvdbClientManager.GetArtworkTypeAsync(cancellationToken)
.ConfigureAwait(false);
var seasonArtworkTypeLookup = artworkTypes

Check failure on line 84 in Jellyfin.Plugin.Tvdb/Providers/TvdbSeasonImageProvider.cs

View workflow job for this annotation

GitHub Actions / call / Analyze (csharp)

The type 'long?' cannot be used as type parameter 'TKey' in the generic type or method 'Enumerable.ToDictionary<TSource, TKey>(IEnumerable<TSource>, Func<TSource, TKey>)'. Nullability of type argument 'long?' doesn't match 'notnull' constraint.

Check failure on line 84 in Jellyfin.Plugin.Tvdb/Providers/TvdbSeasonImageProvider.cs

View workflow job for this annotation

GitHub Actions / call / test

The type 'long?' cannot be used as type parameter 'TKey' in the generic type or method 'Enumerable.ToDictionary<TSource, TKey>(IEnumerable<TSource>, Func<TSource, TKey>)'. Nullability of type argument 'long?' doesn't match 'notnull' constraint.

Check failure on line 84 in Jellyfin.Plugin.Tvdb/Providers/TvdbSeasonImageProvider.cs

View workflow job for this annotation

GitHub Actions / call / build

The type 'long?' cannot be used as type parameter 'TKey' in the generic type or method 'Enumerable.ToDictionary<TSource, TKey>(IEnumerable<TSource>, Func<TSource, TKey>)'. Nullability of type argument 'long?' doesn't match 'notnull' constraint.
.Where(t => string.Equals(t.RecordType, "season", StringComparison.OrdinalIgnoreCase))
.ToDictionary(t => t.Id);

var seriesTvdbId = Convert.ToInt32(series.GetProviderId(TvdbPlugin.ProviderId), CultureInfo.InvariantCulture);
var seasonNumber = season.IndexNumber.Value;

var seasonArtworks = await GetSeasonArtworks(seriesTvdbId, seasonNumber, cancellationToken)
.ConfigureAwait(false);

var remoteImages = new List<RemoteImageInfo>();
foreach (var artwork in seasonArtworks)
{
yield return ImageType.Primary;
yield return ImageType.Banner;
yield return ImageType.Backdrop;
var artworkType = seasonArtworkTypeLookup.GetValueOrDefault(artwork.Type);
var imageType = artworkType.GetImageType();
var artworkLanguage = artwork.Language is null ? null : languageLookup.GetValueOrDefault(artwork.Language);

// only add if valid RemoteImageInfo
remoteImages.AddIfNotNull(artwork.CreateImageInfo(Name, imageType, artworkLanguage));
}

/// <inheritdoc />
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
return remoteImages.OrderByLanguageDescending(item.GetPreferredMetadataLanguage());
}

private async Task<IReadOnlyList<ArtworkBaseRecord>> GetSeasonArtworks(int seriesTvdbId, int seasonNumber, CancellationToken cancellationToken)
{
try
{
var season = (Season)item;
var series = season.Series;

if (series == null || !season.IndexNumber.HasValue || !TvdbSeriesProvider.IsValidSeries(series.ProviderIds))
{
return Enumerable.Empty<RemoteImageInfo>();
}

var tvdbId = Convert.ToInt32(series.GetProviderId(TvdbPlugin.ProviderId), CultureInfo.InvariantCulture);
var seasonNumber = season.IndexNumber.Value;
var language = item.GetPreferredMetadataLanguage();
var remoteImages = new List<RemoteImageInfo>();
var seriesInfo = await _tvdbClientManager.GetSeriesExtendedByIdAsync(tvdbId, language, cancellationToken, small: true).ConfigureAwait(false);
var seriesInfo = await _tvdbClientManager.GetSeriesExtendedByIdAsync(seriesTvdbId, string.Empty, cancellationToken, small: true)
.ConfigureAwait(false);
var seasonTvdbId = seriesInfo.Seasons.FirstOrDefault(s => s.Number == seasonNumber)?.Id;

var seasonInfo = await _tvdbClientManager.GetSeasonByIdAsync(Convert.ToInt32(seasonTvdbId, CultureInfo.InvariantCulture), language, cancellationToken).ConfigureAwait(false);
var seasonImages = seasonInfo.Artwork;
var languages = _tvdbClientManager.GetLanguagesAsync(CancellationToken.None).Result;
var artworkTypes = _tvdbClientManager.GetArtworkTypeAsync(CancellationToken.None).Result;

foreach (var image in seasonImages)
{
ImageType type;
// Checks if valid image type, if not, skip
try
{
type = TvdbUtils.GetImageTypeFromKeyType(artworkTypes.FirstOrDefault(x => x.Id == image.Type && string.Equals(x.RecordType, "season", StringComparison.OrdinalIgnoreCase))?.Name);
}
catch (Exception)
{
continue;
}

var imageInfo = new RemoteImageInfo
{
RatingType = RatingType.Score,
Url = image.Image,
Width = Convert.ToInt32(image.Width, CultureInfo.InvariantCulture),
Height = Convert.ToInt32(image.Height, CultureInfo.InvariantCulture),
Type = type,
ProviderName = Name,
ThumbnailUrl = image.Thumbnail
};

// Tvdb uses 3 letter code for language (prob ISO 639-2)
var artworkLanguage = languages.FirstOrDefault(lang => string.Equals(lang.Id, image.Language, StringComparison.OrdinalIgnoreCase))?.Id;
if (!string.IsNullOrEmpty(artworkLanguage))
{
imageInfo.Language = TvdbUtils.NormalizeLanguageToJellyfin(artworkLanguage)?.ToLowerInvariant();
}

remoteImages.Add(imageInfo);
}

return remoteImages.OrderByDescending(i =>
{
if (string.Equals(language, i.Language, StringComparison.OrdinalIgnoreCase))
{
return 2;
}
else if (!string.IsNullOrEmpty(i.Language))
{
return 1;
}

return 0;
});
var seasonInfo = await _tvdbClientManager.GetSeasonByIdAsync(seasonTvdbId ?? 0, string.Empty, cancellationToken)
.ConfigureAwait(false);
return seasonInfo.Artwork;
}

/// <inheritdoc />
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
catch (Exception ex) when (
(ex is SeriesException seriesEx && seriesEx.InnerException is JsonException)
|| (ex is SeasonsException seasonEx && seasonEx.InnerException is JsonException))
{
return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(new Uri(url), cancellationToken);
_logger.LogError(ex, "Failed to retrieve season images for series {TvDbId}", seriesTvdbId);
return Array.Empty<ArtworkBaseRecord>();
}
}

/// <inheritdoc />
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(new Uri(url), cancellationToken);
}
}
Loading

0 comments on commit dbe5456

Please sign in to comment.