diff --git a/Jellyfin.Plugin.AniList/Providers/AniList/AniListExternalId.cs b/Jellyfin.Plugin.AniList/Providers/AniList/AniListAnimeExternalId.cs similarity index 92% rename from Jellyfin.Plugin.AniList/Providers/AniList/AniListExternalId.cs rename to Jellyfin.Plugin.AniList/Providers/AniList/AniListAnimeExternalId.cs index 419a637..d8a966a 100644 --- a/Jellyfin.Plugin.AniList/Providers/AniList/AniListExternalId.cs +++ b/Jellyfin.Plugin.AniList/Providers/AniList/AniListAnimeExternalId.cs @@ -6,7 +6,7 @@ namespace Jellyfin.Plugin.AniList.Providers.AniList { - public class AniListExternalId : IExternalId + public class AniListAnimeExternalId : IExternalId { public bool Supports(IHasProviderIds item) => item is Series || item is Movie; diff --git a/Jellyfin.Plugin.AniList/Providers/AniList/AniListImageProvider.cs b/Jellyfin.Plugin.AniList/Providers/AniList/AniListAnimeImageProvider.cs similarity index 93% rename from Jellyfin.Plugin.AniList/Providers/AniList/AniListImageProvider.cs rename to Jellyfin.Plugin.AniList/Providers/AniList/AniListAnimeImageProvider.cs index 7fd845e..a1ce525 100644 --- a/Jellyfin.Plugin.AniList/Providers/AniList/AniListImageProvider.cs +++ b/Jellyfin.Plugin.AniList/Providers/AniList/AniListAnimeImageProvider.cs @@ -15,10 +15,10 @@ namespace Jellyfin.Plugin.AniList.Providers.AniList { - public class AniListImageProvider : IRemoteImageProvider + public class AniListAnimeImageProvider : IRemoteImageProvider { private readonly AniListApi _aniListApi; - public AniListImageProvider() + public AniListAnimeImageProvider() { _aniListApi = new AniListApi(); } @@ -44,7 +44,7 @@ public async Task> GetImages(string aid, Cancellati if (!string.IsNullOrEmpty(aid)) { - Media media = await _aniListApi.GetAnime(aid); + Media media = await _aniListApi.GetAnime(aid, cancellationToken); if (media != null) { if (media.GetImageUrl() != null) diff --git a/Jellyfin.Plugin.AniList/Providers/AniList/AniListApi.cs b/Jellyfin.Plugin.AniList/Providers/AniList/AniListApi.cs index 1cbe12c..1b18e9d 100644 --- a/Jellyfin.Plugin.AniList/Providers/AniList/AniListApi.cs +++ b/Jellyfin.Plugin.AniList/Providers/AniList/AniListApi.cs @@ -2,28 +2,30 @@ using System.Collections.Generic; using System.Linq; using System.Net.Http; +using System.Text; using System.Text.Json; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Plugin.AniList.Configuration; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Providers; +using Jellyfin.Extensions; +using Microsoft.Extensions.Logging; namespace Jellyfin.Plugin.AniList.Providers.AniList { /// /// Based on the new API from AniList /// 🛈 This code works with the API Interface (v2) from AniList - /// 🛈 https://anilist.gitbooks.io/anilist-apiv2-docs + /// 🛈 https://anilist.gitbook.io/anilist-apiv2-docs /// 🛈 THIS IS AN UNOFFICAL API INTERFACE FOR JELLYFIN /// public class AniListApi { - private const string SearchLink = @"https://graphql.anilist.co/api/v2?query= -query ($query: String, $type: MediaType) { + private const string BaseApiUrl = "https://graphql.anilist.co/"; + + private const string SearchAnimeGraphqlQuery = @" +query ($query: String) { Page { - media(search: $query, type: $type) { + media(search: $query, type: ANIME) { id title { romaji @@ -42,10 +44,11 @@ public class AniListApi } } } -}&variables={ ""query"":""{0}"",""type"":""ANIME""}"; - public string AnimeLink = @"https://graphql.anilist.co/api/v2?query= -query($id: Int!, $type: MediaType) { - Media(id: $id, type: $type) { +}"; + + private const string GetAnimeGraphqlQuery = @" +query($id: Int!) { + Media(id: $id, type: ANIME) { id title { romaji @@ -102,7 +105,7 @@ public class AniListApi isAnimationStudio } } - characters(sort: [ROLE]) { + characters(sort: [ROLE, FAVOURITES_DESC]) { edges { node { id @@ -129,15 +132,67 @@ public class AniListApi medium large } - language + languageV2 } } } } -}&variables={ ""id"":""{0}"",""type"":""ANIME""}"; +}"; - static AniListApi() - { + private const string SearchStaffGraphqlQuery = @" +query($query: String) { + Page { + staff(search: $query) { + id + name { + first + last + full + native + } + image { + large + medium + } + } + } +}"; + + private const string GetStaffGraphqlQuery = @" +query($id: Int!) { + Staff(id: $id) { + id + name { + first + last + full + native + } + image { + large + medium + } + description(asHtml: true) + homeTown + dateOfBirth { + year + month + day + } + dateOfDeath { + year + month + day + } + } +}"; + + private class GraphQlRequest { + [JsonPropertyName("query")] + public string Query { get; set; } + + [JsonPropertyName("variables")] + public Dictionary Variables { get; set; } } /// @@ -145,9 +200,17 @@ static AniListApi() /// /// /// - public async Task GetAnime(string id) + public async Task GetAnime(string id, CancellationToken cancellationToken) { - return (await WebRequestAPI(AnimeLink.Replace("{0}", id))).data?.Media; + RootObject result = await WebRequestAPI( + new GraphQlRequest { + Query = GetAnimeGraphqlQuery, + Variables = new Dictionary {{"id", id}}, + }, + cancellationToken + ); + + return result.data?.Media; } /// @@ -158,13 +221,7 @@ public async Task GetAnime(string id) /// public async Task Search_GetSeries(string title, CancellationToken cancellationToken) { - // Reimplemented instead of calling Search_GetSeries_list() for efficiency - RootObject WebContent = await WebRequestAPI(SearchLink.Replace("{0}", title)); - foreach (MediaSearchResult media in WebContent.data.Page.media) - { - return media; - } - return null; + return (await Search_GetSeries_list(title, cancellationToken)).FirstOrDefault(); } /// @@ -175,7 +232,15 @@ public async Task Search_GetSeries(string title, Cancellation /// public async Task> Search_GetSeries_list(string title, CancellationToken cancellationToken) { - return (await WebRequestAPI(SearchLink.Replace("{0}", title))).data.Page.media; + RootObject result = await WebRequestAPI( + new GraphQlRequest { + Query = SearchAnimeGraphqlQuery, + Variables = new Dictionary {{"query", title}}, + }, + cancellationToken + ); + + return result.data.Page.media; } /// @@ -200,20 +265,45 @@ public async Task FindSeries(string title, CancellationToken cancellatio return null; } + public async Task GetStaff(int id, CancellationToken cancellationToken) + { + RootObject result = await WebRequestAPI( + new GraphQlRequest { + Query = GetStaffGraphqlQuery, + Variables = new Dictionary {{"id", id.ToString()}}, + }, + cancellationToken + ); + + return result.data?.Staff; + } + + public async Task> SearchStaff(string query, CancellationToken cancellationToken) + { + RootObject result = await WebRequestAPI( + new GraphQlRequest { + Query = SearchStaffGraphqlQuery, + Variables = new Dictionary {{"query", query}}, + }, + cancellationToken + ); + + return result.data?.Page.staff; + } + /// - /// GET and parse JSON content from link, deserialize into a RootObject + /// Send a GraphQL request, deserialize into a RootObject /// - /// + /// The GraphQl request payload /// - public async Task WebRequestAPI(string link) + private async Task WebRequestAPI(GraphQlRequest request, CancellationToken cancellationToken) { var httpClient = Plugin.Instance.GetHttpClient(); - using (HttpContent content = new FormUrlEncodedContent(Enumerable.Empty>())) - using (var response = await httpClient.PostAsync(link, content).ConfigureAwait(false)) - using (var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) - { - return await JsonSerializer.DeserializeAsync(responseStream).ConfigureAwait(false); - } + + using HttpContent content = new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"); + using var response = await httpClient.PostAsync(BaseApiUrl, content, cancellationToken).ConfigureAwait(false); + using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + return await JsonSerializer.DeserializeAsync(responseStream, cancellationToken: cancellationToken).ConfigureAwait(false); } } } diff --git a/Jellyfin.Plugin.AniList/Providers/AniList/AniListMovieProvider.cs b/Jellyfin.Plugin.AniList/Providers/AniList/AniListMovieProvider.cs index fb850b1..ee4ab11 100644 --- a/Jellyfin.Plugin.AniList/Providers/AniList/AniListMovieProvider.cs +++ b/Jellyfin.Plugin.AniList/Providers/AniList/AniListMovieProvider.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using System.Net.Http; using System.Net.Http.Headers; +using System.Text.Json; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Entities; @@ -44,7 +45,7 @@ public async Task> GetMetadata(MovieInfo info, Cancellatio var aid = info.ProviderIds.GetOrDefault(ProviderNames.AniList); if (!string.IsNullOrEmpty(aid)) { - media = await _aniListApi.GetAnime(aid); + media = await _aniListApi.GetAnime(aid, cancellationToken); } else { @@ -58,7 +59,7 @@ public async Task> GetMetadata(MovieInfo info, Cancellatio msr = await _aniListApi.Search_GetSeries(searchName, cancellationToken); if (msr != null) { - media = await _aniListApi.GetAnime(msr.id.ToString()); + media = await _aniListApi.GetAnime(msr.id.ToString(), cancellationToken); } } if(!config.UseAnitomyLibrary || media == null) @@ -69,7 +70,7 @@ public async Task> GetMetadata(MovieInfo info, Cancellatio msr = await _aniListApi.Search_GetSeries(searchName, cancellationToken); if (msr != null) { - media = await _aniListApi.GetAnime(msr.id.ToString()); + media = await _aniListApi.GetAnime(msr.id.ToString(), cancellationToken); } } } @@ -93,7 +94,7 @@ public async Task> GetSearchResults(MovieInfo se var aid = searchInfo.ProviderIds.GetOrDefault(ProviderNames.AniList); if (!string.IsNullOrEmpty(aid)) { - Media aid_result = await _aniListApi.GetAnime(aid).ConfigureAwait(false); + Media aid_result = await _aniListApi.GetAnime(aid, cancellationToken).ConfigureAwait(false); if (aid_result != null) { results.Add(aid_result.ToSearchResult()); diff --git a/Jellyfin.Plugin.AniList/Providers/AniList/AniListPersonExternalId.cs b/Jellyfin.Plugin.AniList/Providers/AniList/AniListPersonExternalId.cs new file mode 100644 index 0000000..2bfdaaa --- /dev/null +++ b/Jellyfin.Plugin.AniList/Providers/AniList/AniListPersonExternalId.cs @@ -0,0 +1,25 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Providers; + +namespace Jellyfin.Plugin.AniList.Providers.AniList +{ + public class AniListPersonExternalId : IExternalId + { + public bool Supports(IHasProviderIds item) + => item is Person; + + public string ProviderName + => "AniList"; + + public string Key + => ProviderNames.AniList; + + public ExternalIdMediaType? Type + => ExternalIdMediaType.Person; + + public string UrlFormatString + => "https://anilist.co/staff/{0}/"; + } +} diff --git a/Jellyfin.Plugin.AniList/Providers/AniList/AniListPersonImageProvider.cs b/Jellyfin.Plugin.AniList/Providers/AniList/AniListPersonImageProvider.cs new file mode 100644 index 0000000..9053c1a --- /dev/null +++ b/Jellyfin.Plugin.AniList/Providers/AniList/AniListPersonImageProvider.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using System.Net.Http; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Providers; + +namespace Jellyfin.Plugin.AniList.Providers.AniList +{ + public class AniListPersonImageProvider : IRemoteImageProvider + { + private readonly ImageType[] supportedTypes = { ImageType.Primary }; + private readonly AniListApi _aniListApi; + + public AniListPersonImageProvider() + { + _aniListApi = new AniListApi(); + } + + public string Name => "AniList"; + + public bool Supports(BaseItem item) => item is Person; + + public IEnumerable GetSupportedImages(BaseItem item) => supportedTypes; + + public async Task> GetImages(BaseItem item, CancellationToken cancellationToken) + { + var results = new List(); + + if (!item.TryGetProviderId(ProviderNames.AniList, out string stringId) + || !int.TryParse(stringId, out int id)) { + return results; + } + + Staff staff = await _aniListApi.GetStaff(id, cancellationToken); + if (staff == null) { + return results; + } + + string imageUrl = staff.image.GetBestImage(); + if (string.IsNullOrEmpty(imageUrl)) { + return results; + } + + results.Add(new RemoteImageInfo { + ProviderName = Name, + Type = ImageType.Primary, + Url = imageUrl, + }); + + return results; + } + + public async Task GetImageResponse(string url, CancellationToken cancellationToken) + { + var httpClient = Plugin.Instance.GetHttpClient(); + return await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false); + } + } +} diff --git a/Jellyfin.Plugin.AniList/Providers/AniList/AniListPersonProvider.cs b/Jellyfin.Plugin.AniList/Providers/AniList/AniListPersonProvider.cs new file mode 100644 index 0000000..23b7298 --- /dev/null +++ b/Jellyfin.Plugin.AniList/Providers/AniList/AniListPersonProvider.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Providers; +using Microsoft.Extensions.Logging; + + +//API v2 +namespace Jellyfin.Plugin.AniList.Providers.AniList +{ + public class AniListPersonProvider : IRemoteMetadataProvider, IHasOrder + { + private readonly IApplicationPaths _paths; + private readonly ILogger _log; + private readonly AniListApi _aniListApi; + public int Order => -2; + public string Name => "AniList"; + + public AniListPersonProvider(IApplicationPaths appPaths, ILogger logger) + { + _log = logger; + _aniListApi = new AniListApi(); + _paths = appPaths; + } + + public async Task> GetMetadata(PersonLookupInfo info, CancellationToken cancellationToken) + { + var result = new MetadataResult(); + + int anilistId = int.Parse(info.GetProviderId(ProviderNames.AniList)); + Staff staff = await _aniListApi.GetStaff(anilistId, cancellationToken); + + if (staff == null) { + return result; + } + + result.Item = staff.ToPerson(); + result.HasMetadata = true; + return result; + } + + public async Task> GetSearchResults(PersonLookupInfo searchInfo, CancellationToken cancellationToken) + { + return (await _aniListApi.SearchStaff(searchInfo.Name, cancellationToken)) + .Select(DataModelExtensions.ToSearchResult); + } + + public async Task GetImageResponse(string url, CancellationToken cancellationToken) + { + var httpClient = Plugin.Instance.GetHttpClient(); + return await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false); + } + } +} diff --git a/Jellyfin.Plugin.AniList/Providers/AniList/AniListSeriesProvider.cs b/Jellyfin.Plugin.AniList/Providers/AniList/AniListSeriesProvider.cs index 8875d31..a5b401b 100644 --- a/Jellyfin.Plugin.AniList/Providers/AniList/AniListSeriesProvider.cs +++ b/Jellyfin.Plugin.AniList/Providers/AniList/AniListSeriesProvider.cs @@ -42,7 +42,7 @@ public async Task> GetMetadata(SeriesInfo info, Cancellat var aid = info.ProviderIds.GetOrDefault(ProviderNames.AniList); if (!string.IsNullOrEmpty(aid)) { - media = await _aniListApi.GetAnime(aid); + media = await _aniListApi.GetAnime(aid, cancellationToken); } else { @@ -57,7 +57,7 @@ public async Task> GetMetadata(SeriesInfo info, Cancellat msr = await _aniListApi.Search_GetSeries(searchName, cancellationToken); if (msr != null) { - media = await _aniListApi.GetAnime(msr.id.ToString()); + media = await _aniListApi.GetAnime(msr.id.ToString(), cancellationToken); } } if(!config.UseAnitomyLibrary || media == null) @@ -68,7 +68,7 @@ public async Task> GetMetadata(SeriesInfo info, Cancellat msr = await _aniListApi.Search_GetSeries(info.Name, cancellationToken); if (msr != null) { - media = await _aniListApi.GetAnime(msr.id.ToString()); + media = await _aniListApi.GetAnime(msr.id.ToString(), cancellationToken); } } } @@ -92,7 +92,7 @@ public async Task> GetSearchResults(SeriesInfo s var aid = searchInfo.ProviderIds.GetOrDefault(ProviderNames.AniList); if (!string.IsNullOrEmpty(aid)) { - Media aid_result = await _aniListApi.GetAnime(aid).ConfigureAwait(false); + Media aid_result = await _aniListApi.GetAnime(aid, cancellationToken).ConfigureAwait(false); if (aid_result != null) { results.Add(aid_result.ToSearchResult()); diff --git a/Jellyfin.Plugin.AniList/Providers/AniList/ApiModel.cs b/Jellyfin.Plugin.AniList/Providers/AniList/ApiModel.cs index a08eb15..f2613cb 100644 --- a/Jellyfin.Plugin.AniList/Providers/AniList/ApiModel.cs +++ b/Jellyfin.Plugin.AniList/Providers/AniList/ApiModel.cs @@ -1,8 +1,7 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; -using System.Text; +using System.Text.Json.Serialization; using MediaBrowser.Model.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Controller.Entities; @@ -12,8 +11,6 @@ namespace Jellyfin.Plugin.AniList.Providers.AniList { - using System.Collections.Generic; - public class Title { public string romaji { get; set; } @@ -28,7 +25,7 @@ public class CoverImage public string extraLarge { get; set; } } - public class ApiDate + public class FuzzyDate { public int? year { get; set; } public int? month { get; set; } @@ -38,12 +35,14 @@ public class ApiDate public class Page { public List media { get; set; } + public List staff { get; set; } } public class Data { public Page Page { get; set; } public Media Media { get; set; } + public Staff Staff { get; set; } } /// @@ -54,7 +53,7 @@ public class MediaSearchResult { public int id { get; set; } public Title title { get; set; } - public ApiDate startDate { get; set; } + public FuzzyDate startDate { get; set; } public CoverImage coverImage { get; set; } /// @@ -92,19 +91,6 @@ public string GetImageUrl() return this.coverImage.extraLarge ?? this.coverImage.large ?? this.coverImage.medium; } - /// - /// Returns the start date as a DateTime object or null if not available - /// - /// - public DateTime? GetStartDate() - { - if (this.startDate.year == null || this.startDate.month == null || this.startDate.day == null) - { - return null; - } - return new DateTime(this.startDate.year.Value, this.startDate.month.Value, this.startDate.day.Value); - } - /// /// Convert a Media/MediaSearchResult object to a RemoteSearchResult /// @@ -116,7 +102,7 @@ public RemoteSearchResult ToSearchResult() { Name = this.GetPreferredTitle(config.TitlePreference, "en"), ProductionYear = this.startDate.year, - PremiereDate = this.GetStartDate(), + PremiereDate = this.startDate?.ToDateTime(), ImageUrl = this.GetImageUrl(), SearchProviderName = ProviderNames.AniList, ProviderIds = new Dictionary() {{ProviderNames.AniList, this.id.ToString()}} @@ -124,15 +110,15 @@ public RemoteSearchResult ToSearchResult() } } - public class Media: MediaSearchResult + public class Media : MediaSearchResult { public int? averageScore { get; set; } public string bannerImage { get; set; } public object chapters { get; set; } - public Characters characters { get; set; } + public CharacterConnection characters { get; set; } public string description { get; set; } public int? duration { get; set; } - public ApiDate endDate { get; set; } + public FuzzyDate endDate { get; set; } public int? episodes { get; set; } public string format { get; set; } public List genres { get; set; } @@ -159,19 +145,6 @@ public float GetRating() return (this.averageScore ?? 0) / 10f; } - /// - /// Returns the end date as a DateTime object or null if not available - /// - /// - public DateTime? GetEndDate() - { - if (this.endDate.year == null || this.endDate.month == null || this.endDate.day == null) - { - return null; - } - return new DateTime(this.endDate.year.Value, this.endDate.month.Value, this.endDate.day.Value); - } - /// /// Returns a list of studio names /// @@ -194,24 +167,28 @@ public List GetPeopleInfo() { PluginConfiguration config = Plugin.Instance.Configuration; List lpi = new List(); + foreach (CharacterEdge edge in this.characters.edges) { - foreach (VoiceActor va in edge.voiceActors) + foreach (Staff va in edge.voiceActors) { if (config.FilterPeopleByTitlePreference) { - if (config.TitlePreference != TitlePreferenceType.Localized && va.language != "JAPANESE") { + if (config.TitlePreference != TitlePreferenceType.Localized + && !va.language.Equals("Japanese", StringComparison.InvariantCultureIgnoreCase)) { continue; } - if (config.TitlePreference == TitlePreferenceType.Localized && va.language == "JAPANESE") { + if (config.TitlePreference == TitlePreferenceType.Localized + && va.language.Equals("Japanese", StringComparison.InvariantCultureIgnoreCase)) { continue; } } + PeopleHelper.AddPerson(lpi, new PersonInfo { Name = va.name.full, - ImageUrl = va.image.large ?? va.image.medium, + ImageUrl = va.image.GetBestImage(), Role = edge.node.name.full, Type = PersonType.Actor, - ProviderIds = new Dictionary() {{ProviderNames.AniList, this.id.ToString()}} + ProviderIds = new Dictionary() {{ProviderNames.AniList, va.id.ToString()}} }); } } @@ -264,10 +241,10 @@ public Series ToSeries() OriginalTitle = this.GetPreferredTitle(config.OriginalTitlePreference, "en"), Overview = this.description, ProductionYear = this.startDate.year, - PremiereDate = this.GetStartDate(), - EndDate = this.GetStartDate(), + PremiereDate = this.startDate?.ToDateTime(), + EndDate = this.endDate?.ToDateTime(), CommunityRating = this.GetRating(), - RunTimeTicks = this.duration.HasValue ? TimeSpan.FromMinutes(this.duration.Value).Ticks : (long?)null, + RunTimeTicks = this.duration.HasValue ? TimeSpan.FromMinutes(this.duration.Value).Ticks : null, Genres = this.GetGenres().ToArray(), Tags = this.GetTagNames().ToArray(), Studios = this.GetStudioNames().ToArray(), @@ -298,8 +275,8 @@ public Movie ToMovie() OriginalTitle = this.GetPreferredTitle(config.OriginalTitlePreference, "en"), Overview = this.description, ProductionYear = this.startDate.year, - PremiereDate = this.GetStartDate(), - EndDate = this.GetStartDate(), + PremiereDate = this.startDate?.ToDateTime(), + EndDate = this.endDate?.ToDateTime(), CommunityRating = this.GetRating(), Genres = this.GetGenres().ToArray(), Tags = this.GetTagNames().ToArray(), @@ -332,11 +309,11 @@ public class Image public class Character { public int id { get; set; } - public Name2 name { get; set; } + public CharacterName name { get; set; } public Image image { get; set; } } - public class Name2 + public class CharacterName { public string first { get; set; } public string last { get; set; } @@ -344,22 +321,35 @@ public class Name2 public string native { get; set; } } - public class VoiceActor + public class Staff { public int id { get; set; } - public Name2 name { get; set; } - public Image image { get; set; } + public StaffName name { get; set; } + [JsonPropertyName("languageV2")] public string language { get; set; } + public Image image { get; set; } + public string description { get; set; } + public FuzzyDate dateOfBirth { get; set; } + public FuzzyDate dateOfDeath { get; set; } + public string homeTown { get; set; } + } + + public class StaffName + { + public string first { get; set; } + public string last { get; set; } + public string full { get; set; } + public string native { get; set; } } public class CharacterEdge { public Character node { get; set; } public string role { get; set; } - public List voiceActors { get; set; } + public List voiceActors { get; set; } } - public class Characters + public class CharacterConnection { public List edges { get; set; } } @@ -389,4 +379,57 @@ public class RootObject { public Data data { get; set; } } + + public static class DataModelExtensions { + public static DateTime? ToDateTime(this FuzzyDate date) { + if (date.day == null || date.month == null || date.year == null) { + return null; + } + + return new DateTime(date.year.Value, date.month.Value, date.day.Value); + } + + public static Person ToPerson(this Staff staff) { + Person person = new Person { + Name = staff.name.full, + OriginalTitle = staff.name.native, + Overview = staff.description, + PremiereDate = staff.dateOfBirth?.ToDateTime(), + EndDate = staff.dateOfDeath?.ToDateTime(), + ProviderIds = new Dictionary() {{ProviderNames.AniList, staff.id.ToString()}} + }; + + if (!string.IsNullOrWhiteSpace(staff.homeTown)) { + person.ProductionLocations = new[] { staff.homeTown }; + } + + return person; + } + + public static RemoteSearchResult ToSearchResult(this Staff staff) { + return new RemoteSearchResult() { + SearchProviderName = ProviderNames.AniList, + Name = staff.name.full, + ImageUrl = staff.image.GetBestImage(), + ProviderIds = new Dictionary() {{ProviderNames.AniList, staff.id.ToString()}} + }; + } + + public static string GetBestImage(this Image image) { + if (IsValidImage(image.large)) { + return image.large; + } + + if (IsValidImage(image.medium)) { + return image.medium; + } + + return null; + } + + private static bool IsValidImage(string imageUrl) { + // Filter out the default "No image" picture. + return !string.IsNullOrEmpty(imageUrl) && !imageUrl.EndsWith("default.jpg"); + } + } }