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 Comic Vine Person provider #85

Merged
merged 6 commits into from
Jun 28, 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
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ protected BaseComicVineProvider(ILogger<BaseComicVineProvider> logger, IComicVin
{
Type issue when issue == typeof(IssueDetails) => ComicVineApiUrls.IssueDetailUrl,
Type volume when volume == typeof(VolumeDetails) => ComicVineApiUrls.VolumeDetailUrl,
Type person when person == typeof(PersonDetails) => ComicVineApiUrls.PersonDetailUrl,
_ => throw new InvalidOperationException($"Unexpected resource type {typeof(T)}.")
};

Expand Down Expand Up @@ -222,5 +223,71 @@ protected IReadOnlyList<T> GetFromApiResponse<T>(BaseApiResponse<T> response)

return null;
}

/// <summary>
/// Gets images URLs from a list of images.
/// </summary>
/// <param name="imageList">The list of images.</param>
/// <returns>The list of images URLs.</returns>
protected IEnumerable<string> ProcessImages(ImageList? imageList)
{
if (imageList == null)
{
yield break;
}

if (!string.IsNullOrWhiteSpace(imageList.SuperUrl))
{
yield return imageList.SuperUrl;
}
else if (!string.IsNullOrWhiteSpace(imageList.OriginalUrl))
{
yield return imageList.OriginalUrl;
}
else if (!string.IsNullOrWhiteSpace(imageList.MediumUrl))
{
yield return imageList.MediumUrl;
}
else if (!string.IsNullOrWhiteSpace(imageList.SmallUrl))
{
yield return imageList.SmallUrl;
}
else if (!string.IsNullOrWhiteSpace(imageList.ThumbUrl))
{
yield return imageList.ThumbUrl;
}
}

/// <summary>
/// Gets the issue id from the site detail URL.
/// <para>
/// Issues have a unique Id, but also a different one used for the API.
/// The URL to the issue detail page also includes a slug before the id.
/// </para>
/// <listheader>For example:</listheader>
/// <list type="bullet">
/// <item>
/// <term>id</term>
/// <description>441467</description>
/// </item>
/// <item>
/// <term>api_detail_url</term>
/// <description>https://comicvine.gamespot.com/api/issue/4000-441467</description>
/// </item>
/// <item>
/// <term>site_detail_url</term>
/// <description>https://comicvine.gamespot.com/attack-on-titan-10-fortress-of-blood/4000-441467</description>
/// </item>
/// </list>
/// <para>
/// We need to keep the last two parts of the site detail URL (the slug and the id) as the provider id for the IExternalId implementation to work.
/// </para>
/// </summary>
/// <param name="siteDetailUrl">The site detail URL.</param>
/// <returns>The slug and id.</returns>
protected static string GetProviderIdFromSiteDetailUrl(string siteDetailUrl)
{
return siteDetailUrl.Replace(ComicVineApiUrls.BaseWebsiteUrl, string.Empty, StringComparison.OrdinalIgnoreCase).Trim('/');
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ public class ComicVineImageProvider : BaseComicVineProvider, IRemoteImageProvide
/// <param name="logger">Instance of the <see cref="ILogger{ComicVineImageProvider}"/> interface.</param>
/// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
/// <param name="apiKeyProvider">Instance of the <see cref="IComicVineApiKeyProvider"/> interface.</param>
public ComicVineImageProvider(IComicVineMetadataCacheManager comicVineMetadataCacheManager, ILogger<ComicVineImageProvider> logger, IHttpClientFactory httpClientFactory, IComicVineApiKeyProvider apiKeyProvider)
public ComicVineImageProvider(
IComicVineMetadataCacheManager comicVineMetadataCacheManager,
ILogger<ComicVineImageProvider> logger,
IHttpClientFactory httpClientFactory,
IComicVineApiKeyProvider apiKeyProvider)
: base(logger, comicVineMetadataCacheManager, httpClientFactory, apiKeyProvider)
{
_logger = logger;
Expand Down Expand Up @@ -68,7 +72,7 @@ public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, Cancell
return Enumerable.Empty<RemoteImageInfo>();
}

var images = ProcessIssueImages(issueDetails)
var images = ProcessImages(issueDetails.Image)
.Select(url => new RemoteImageInfo
{
Url = url,
Expand All @@ -78,44 +82,6 @@ public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, Cancell
return images;
}

/// <summary>
/// Gets images URLs from the issue.
/// </summary>
/// <param name="issueDetails">The issue details.</param>
/// <returns>The list of images URLs.</returns>
private IEnumerable<string> ProcessIssueImages(IssueDetails issueDetails)
{
if (issueDetails.Image == null)
{
return Enumerable.Empty<string>();
}

var images = new List<string>();

if (!string.IsNullOrWhiteSpace(issueDetails.Image.SuperUrl))
{
images.Add(issueDetails.Image.SuperUrl);
}
else if (!string.IsNullOrWhiteSpace(issueDetails.Image.OriginalUrl))
{
images.Add(issueDetails.Image.OriginalUrl);
}
else if (!string.IsNullOrWhiteSpace(issueDetails.Image.MediumUrl))
{
images.Add(issueDetails.Image.MediumUrl);
}
else if (!string.IsNullOrWhiteSpace(issueDetails.Image.SmallUrl))
{
images.Add(issueDetails.Image.SmallUrl);
}
else if (!string.IsNullOrWhiteSpace(issueDetails.Image.ThumbUrl))
{
images.Add(issueDetails.Image.ThumbUrl);
}

return images;
}

/// <inheritdoc/>
public async Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ public class ComicVineMetadataProvider : BaseComicVineProvider, IRemoteMetadataP
/// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
/// <param name="comicVineMetadataCacheManager">Instance of the <see cref="IComicVineMetadataCacheManager"/> interface.</param>
/// <param name="apiKeyProvider">Instance of the <see cref="IComicVineApiKeyProvider"/> interface.</param>
public ComicVineMetadataProvider(ILogger<ComicVineMetadataProvider> logger, IHttpClientFactory httpClientFactory, IComicVineMetadataCacheManager comicVineMetadataCacheManager, IComicVineApiKeyProvider apiKeyProvider)
public ComicVineMetadataProvider(
ILogger<ComicVineMetadataProvider> logger,
IHttpClientFactory httpClientFactory,
IComicVineMetadataCacheManager comicVineMetadataCacheManager,
IComicVineApiKeyProvider apiKeyProvider)
: base(logger, comicVineMetadataCacheManager, httpClientFactory, apiKeyProvider)
{
_logger = logger;
Expand Down Expand Up @@ -362,37 +366,5 @@ internal string GetSearchString(BookInfo item)

return result.Trim();
}

/// <summary>
/// Gets the issue id from the site detail URL.
/// <para>
/// Issues have a unique Id, but also a different one used for the API.
/// The URL to the issue detail page also includes a slug before the id.
/// </para>
/// <listheader>For example:</listheader>
/// <list type="bullet">
/// <item>
/// <term>id</term>
/// <description>441467</description>
/// </item>
/// <item>
/// <term>api_detail_url</term>
/// <description>https://comicvine.gamespot.com/api/issue/4000-441467</description>
/// </item>
/// <item>
/// <term>site_detail_url</term>
/// <description>https://comicvine.gamespot.com/attack-on-titan-10-fortress-of-blood/4000-441467</description>
/// </item>
/// </list>
/// <para>
/// We need to keep the last two parts of the site detail URL (the slug and the id) as the provider id for the IExternalId implementation to work.
/// </para>
/// </summary>
/// <param name="siteDetailUrl">The site detail URL.</param>
/// <returns>The slug and id.</returns>
private static string GetProviderIdFromSiteDetailUrl(string siteDetailUrl)
{
return siteDetailUrl.Replace(ComicVineApiUrls.BaseWebsiteUrl, string.Empty, StringComparison.OrdinalIgnoreCase).Trim('/');
}
}
}
10 changes: 10 additions & 0 deletions Jellyfin.Plugin.Bookshelf/Providers/ComicVine/ComicVineApiUrls.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,15 @@ internal static class ComicVineApiUrls
/// Gets the URL used to fetch a specific volume.
/// </summary>
public const string VolumeDetailUrl = BaseUrl + @"/volume/{1}?api_key={0}&format=json&field_list=api_detail_url,id,name,site_detail_url,count_of_issues,description,publisher";

/// <summary>
/// Gets the URL used to search for persons.
/// </summary>
public const string PersonSearchUrl = BaseUrl + @"/search?api_key={0}&format=json&resources=person&query={1}";

/// <summary>
/// Gets the URL used to fetch a specific person.
/// </summary>
public const string PersonDetailUrl = BaseUrl + @"/person/{1}?api_key={0}&format=json&field_list=api_detail_url,id,name,site_detail_url,aliases,birth,country,death,deck,description,email,gender,hometown,image,website";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,17 @@ public class PersonCredit
/// <summary>
/// Gets the list of roles for this person.
/// </summary>
public IEnumerable<PersonCreditRole> Roles => Role.Split(", ").Select(r => Enum.Parse<PersonCreditRole>(r, true));
public IEnumerable<PersonCreditRole> Roles
{
get
{
if (string.IsNullOrWhiteSpace(Role))
{
return Enumerable.Empty<PersonCreditRole>();
}

return Role.Split(", ", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Select(r => Enum.Parse<PersonCreditRole>(r, true));
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using System;
using System.Globalization;
using System.Text.Json.Serialization;

namespace Jellyfin.Plugin.Bookshelf.Providers.ComicVine.Models
{
/// <summary>
/// Details of a person.
/// </summary>
public class PersonDetails : PersonCredit
{
/// <summary>
/// Gets the list of aliases the person is known by. A \n (newline) seperates each alias.
/// </summary>
public string? Aliases { get; init; }

/// <summary>
/// Gets a date, if one exists, that the person was born on. Not an origin date.
/// </summary>
public string? Birth { get; init; }

/// <summary>
/// Gets a date, if one exists, that the person was born on. Not an origin date.
/// </summary>
[JsonIgnore]
public DateTime? BirthDate => ParseAsUTC(Birth);

/// <summary>
/// Gets the country the person resides in.
/// </summary>
public string? Country { get; init; }

/// <summary>
/// Gets the date this person died on.
/// </summary>
public string? Death { get; init; }

/// <summary>
/// Gets the date this person died on.
/// </summary>
[JsonIgnore]
public DateTime? DeathDate => ParseAsUTC(Death);

/// <summary>
/// Gets a brief summary of the person.
/// </summary>
public string? Deck { get; init; }

/// <summary>
/// Gets the description of the person.
/// </summary>
public string? Description { get; init; }

/// <summary>
/// Gets the email of this person.
/// </summary>
public string? Email { get; init; }

/// <summary>
/// Gets the gender of the person. Available options are: Male (1), Female (2), Other (0).
/// </summary>
public int? Gender { get; init; }

/// <summary>
/// Gets the city or town the person resides in.
/// </summary>
public string? Hometown { get; init; }

/// <summary>
/// Gets the main image of the person.
/// </summary>
public ImageList? Image { get; init; }

/// <summary>
/// Gets the URL to the person website.
/// </summary>
public string? Website { get; init; }

private static DateTime? ParseAsUTC(string? value)
{
if (!string.IsNullOrWhiteSpace(value) && DateTime.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out DateTime result))
{
return result;
}

return null;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;

namespace Jellyfin.Plugin.Bookshelf.Providers.ComicVine
{
/// <inheritdoc />
public class ComicVinePersonExternalId : IExternalId
{
/// <inheritdoc />
public string ProviderName => ComicVineConstants.ProviderName;

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

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

/// <inheritdoc />
public string? UrlFormatString => ComicVineApiUrls.BaseWebsiteUrl + "/{0}";

/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Person;
}
}
Loading