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 Boxset support #168

Draft
wants to merge 9 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 7 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
28 changes: 28 additions & 0 deletions Jellyfin.Plugin.Tvdb/Providers/ExternalId/TvdbBoxSetExternalId.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;

namespace Jellyfin.Plugin.Tvdb.Providers.ExternalId
{
/// <summary>
/// The TvdbBoxSetExternalId class.
/// </summary>
public class TvdbBoxSetExternalId : IExternalId
{
/// <inheritdoc />
public string ProviderName => TvdbPlugin.ProviderName + " Numerical";

/// <inheritdoc />
public string Key => TvdbPlugin.ProviderId;

/// <inheritdoc />
public ExternalIdMediaType? Type => ExternalIdMediaType.BoxSet;

/// <inheritdoc />
public string? UrlFormatString => null;

/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is BoxSet;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;

namespace Jellyfin.Plugin.Tvdb.Providers.ExternalId
{
/// <summary>
/// The TvdbBoxSetSlugExternalId class.
/// </summary>
public class TvdbBoxSetSlugExternalId : IExternalId
{
/// <inheritdoc />
public string ProviderName => TvdbPlugin.ProviderName;

/// <inheritdoc />
public string Key => TvdbPlugin.SlugProviderId;

/// <inheritdoc />
public ExternalIdMediaType? Type => ExternalIdMediaType.BoxSet;

/// <inheritdoc />
public string? UrlFormatString => TvdbUtils.TvdbBaseUrl + "lists/{0}";

/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is BoxSet;
}
}
79 changes: 79 additions & 0 deletions Jellyfin.Plugin.Tvdb/Providers/TvdbBoxSetImageProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
using Microsoft.Extensions.Logging;

namespace Jellyfin.Plugin.Tvdb.Providers
{
/// <summary>
/// The TvdbBoxSetProvider class.
/// </summary>
public class TvdbBoxSetImageProvider : IRemoteImageProvider
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger<TvdbEpisodeProvider> _logger;
private readonly TvdbClientManager _tvdbClientManager;

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

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

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

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

/// <inheritdoc />
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
{
if (!item.HasTvdbId())
{
return Enumerable.Empty<RemoteImageInfo>();
}

var boxSetRecord = await _tvdbClientManager.GetBoxSetExtendedByIdAsync(item.GetTvdbId(), cancellationToken).ConfigureAwait(false);
var remoteImageInfo = new RemoteImageInfo
{
ProviderName = Name,
Type = ImageType.Primary,
Url = boxSetRecord.Image,
};

return new List<RemoteImageInfo> { remoteImageInfo };
}

/// <inheritdoc />
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(new Uri(url), cancellationToken);
}
}
}
221 changes: 221 additions & 0 deletions Jellyfin.Plugin.Tvdb/Providers/TvdbBoxSetProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Providers;
using Microsoft.Extensions.Logging;
using Tvdb.Sdk;

namespace Jellyfin.Plugin.Tvdb.Providers
{
/// <summary>
/// The TvdbBoxSetProvider class.
/// </summary>
public class TvdbBoxSetProvider : IRemoteMetadataProvider<BoxSet, BoxSetInfo>
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger<TvdbEpisodeProvider> _logger;
private readonly ILibraryManager _libraryManager;
private readonly TvdbClientManager _tvdbClientManager;

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

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

/// <inheritdoc />
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(BoxSetInfo searchInfo, CancellationToken cancellationToken)
{
if (!string.IsNullOrEmpty(searchInfo.Name))
{
// Search for box sets
return await FindBoxSet(searchInfo.Name, searchInfo.MetadataLanguage, cancellationToken).ConfigureAwait(false);
}

// Return empty list if no results found
return Enumerable.Empty<RemoteSearchResult>();
}

/// <inheritdoc />
public async Task<MetadataResult<BoxSet>> GetMetadata(BoxSetInfo info, CancellationToken cancellationToken)
{
var result = new MetadataResult<BoxSet>
{
QueriedById = true,
};
// Tvdb Box sets are only supported by tvdb ids
if (!info.HasTvdbId())
{
result.QueriedById = false;
await Identify(info).ConfigureAwait(false);
}

cancellationToken.ThrowIfCancellationRequested();

if (info.HasTvdbId())
{
result.Item = new BoxSet();
result.HasMetadata = true;

await FetchBoxSetMetadata(result, info, cancellationToken)
.ConfigureAwait(false);
}

return result;
}

private async Task<IEnumerable<RemoteSearchResult>> FindBoxSet(string name, string language, CancellationToken cancellationToken)
{
_logger.LogDebug("TvdbSearch: Finding id for item: {Name}", name);
var results = await FindBoxSetsInternal(name, language, cancellationToken).ConfigureAwait(false);

return results;
}

private async Task<List<RemoteSearchResult>> FindBoxSetsInternal(string name, string language, CancellationToken cancellationToken)
{
var parsedName = _libraryManager.ParseName(name);
var comparableName = TvdbUtils.GetComparableName(parsedName.Name);

var list = new List<Tuple<List<string>, RemoteSearchResult>>();
IReadOnlyList<SearchResult> result;
try
{
result = await _tvdbClientManager.GetBoxSetByNameAsync(comparableName, cancellationToken)
.ConfigureAwait(false);
}
catch (Exception e)
{
_logger.LogError(e, "No BoxSet results found for {Name}", comparableName);
return new List<RemoteSearchResult>();
}
Comment on lines +107 to +111

Check notice

Code scanning / CodeQL

Generic catch clause Note

Generic catch clause.

foreach (var boxSetSearchResult in result)
{
var tvdbTitles = new List<string>
{
boxSetSearchResult.Translations.GetTranslatedNamedOrDefault(language) ?? boxSetSearchResult.Name
};
if (boxSetSearchResult.Aliases is not null)
{
tvdbTitles.AddRange(boxSetSearchResult.Aliases);
}

var remoteSearchResult = new RemoteSearchResult
{
Name = tvdbTitles.FirstOrDefault(),
SearchProviderName = Name
};

if (!string.IsNullOrEmpty(boxSetSearchResult.Image_url))
{
remoteSearchResult.ImageUrl = boxSetSearchResult.Image_url;
}

remoteSearchResult.SetTvdbId(boxSetSearchResult.Tvdb_id);
list.Add(new Tuple<List<string>, RemoteSearchResult>(tvdbTitles, remoteSearchResult));
}

return list
.OrderBy(i => i.Item1.Contains(name, StringComparer.OrdinalIgnoreCase) ? 0 : 1)
.ThenBy(i => i.Item1.Any(title => title.Contains(parsedName.Name, StringComparison.OrdinalIgnoreCase)) ? 0 : 1)
.ThenBy(i => i.Item1.Any(title => title.Contains(comparableName, StringComparison.OrdinalIgnoreCase)) ? 0 : 1)
.ThenBy(i => list.IndexOf(i))
.Select(i => i.Item2)
.ToList();
}

private async Task Identify(BoxSetInfo info)
{
if (info.HasTvdbId())
{
return;
}

var remoteSearchResults = await FindBoxSet(info.Name, info.MetadataLanguage, CancellationToken.None)
.ConfigureAwait(false);

var entry = remoteSearchResults.FirstOrDefault();
if (entry.HasTvdbId(out var tvdbId))
{
info.SetTvdbId(tvdbId);
}
}

private async Task FetchBoxSetMetadata(
MetadataResult<BoxSet> result,
BoxSetInfo boxSetInfo,
CancellationToken cancellationToken)
{
var boxSetMetadata = result.Item;

if (boxSetInfo.HasTvdbId(out var tvdbIdTxt))
{
boxSetMetadata.SetTvdbId(tvdbIdTxt);
}

if (string.IsNullOrWhiteSpace(tvdbIdTxt))
{
_logger.LogWarning("No valid tvdb id found for BoxSet {TvdbId}:{BoxSetName}", tvdbIdTxt, boxSetInfo.Name);
return;
}

var tvdbId = Convert.ToInt32(tvdbIdTxt, CultureInfo.InvariantCulture);
try
{
var boxSetResult =
await _tvdbClientManager
.GetBoxSetExtendedByIdAsync(tvdbId, cancellationToken)
.ConfigureAwait(false);
MapBoxSetToResult(result, boxSetResult, boxSetInfo);
}
catch (Exception e)
{
_logger.LogError(e, "Failed to retrieve BoxSet with id {TvdbId}:{BoxSetName}", tvdbId, boxSetInfo.Name);
return;
}
Comment on lines +192 to +196

Check notice

Code scanning / CodeQL

Generic catch clause Note

Generic catch clause.
}

private void MapBoxSetToResult(MetadataResult<BoxSet> result, ListExtendedRecord tvdbBoxSet, BoxSetInfo info)
{
BoxSet boxSet = result.Item;
boxSet.SetTvdbId(tvdbBoxSet.Id);
// Tvdb uses 3 letter code for language (prob ISO 639-2)
// Reverts to OriginalName if no translation is found
// boxSet.Name = tvdbBoxSet.Translations.GetTranslatedNamedOrDefault(info.MetadataLanguage) ?? TvdbUtils.ReturnOriginalLanguageOrDefault(tvdbMovie.Name);
boxSet.Name = tvdbBoxSet.Name;
// boxSet.Overview = tvdbBoxSet.Translations.GetTranslatedOverviewOrDefault(info.MetadataLanguage);
boxSet.Overview = tvdbBoxSet.Overview;
boxSet.OriginalTitle = tvdbBoxSet.Name;
result.ResultLanguage = info.MetadataLanguage;
// Attempts to default to USA if not found
boxSet.SetProviderIdIfHasValue(TvdbPlugin.SlugProviderId, tvdbBoxSet.Url);
}

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