Merge remote-tracking branch 'upstream/master' into feature/KitsuIO

This commit is contained in:
crobibero 2020-07-19 15:37:28 -06:00
commit 5aba3fab14
17 changed files with 626 additions and 441 deletions

View File

@ -73,11 +73,11 @@
$('#titleLanguage', page).val(config.TitlePreference).change();
$('#chkMaxGenres', page).val(config.MaxGenres).change();
$('#chkTidyGenres', page).checked = config.TidyGenreList;
$('#chkAddAnimeGenre', page).checked = config.AddAnimeGenre;
document.getElementById('chkTidyGenres').checked = config.TidyGenreList;
document.getElementById('chkAddAnimeGenre').checked = config.AddAnimeGenre;
$('#chkAniDbWaitTime', page).val(config.AniDbWaitTime).change();
$('#chkAniDbOrderWithSeasons', page).checked = config.AniDbOrderWithSeasons;
$('#chkAniDbReplaceGraves', page).checked = config.AniDbReplaceGraves;
document.getElementById('chkAniDbOrderWithSeasons').checked = config.AniDbOrderWithSeasons;
document.getElementById('chkAniDbReplaceGraves').checked = config.AniDbReplaceGraves;
Dashboard.hideLoadingMsg();
});
@ -91,11 +91,11 @@
config.TitlePreference = $('#titleLanguage', page).val();
config.MaxGenres = $('#chkMaxGenres').val();
config.TidyGenreList = $('#chkTidyGenres').checked;
config.AddAnimeGenre = $('#chkAddAnimeGenre').checked;
config.TidyGenreList = document.getElementById('chkTidyGenres').checked;
config.AddAnimeGenre = document.getElementById('chkAddAnimeGenre').checked;
config.AniDbWaitTime = $('#chkAniDbWaitTime').val();
config.AniDbOrderWithSeasons = $('#chkAniDbOrderWithSeasons').checked;
config.AniDbReplaceGraves = $('#chkAniDbReplaceGraves').checked;
config.AniDbOrderWithSeasons = document.getElementById('chkAniDbOrderWithSeasons').checked;
config.AniDbReplaceGraves = document.getElementById('chkAniDbReplaceGraves').checked;
ApiClient.updatePluginConfiguration(AnimeConfigurationPage.pluginUniqueId, config).then(function (result) {
Dashboard.processPluginConfigurationUpdateResult(result);

View File

@ -3,12 +3,12 @@
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<RootNamespace>Jellyfin.Plugin.Anime</RootNamespace>
<AssemblyVersion>8.0.0.0</AssemblyVersion>
<FileVersion>8.0.0.0</FileVersion>
<AssemblyVersion>9.0.0.0</AssemblyVersion>
<FileVersion>9.0.0.0</FileVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Jellyfin.Controller" Version="10.*" />
<PackageReference Include="Jellyfin.Controller" Version="10.*-*" />
</ItemGroup>
<ItemGroup>

View File

@ -3,7 +3,6 @@ using System.Collections.Generic;
using Jellyfin.Plugin.Anime.Configuration;
using Jellyfin.Plugin.Anime.Providers.AniDB.Identity;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Serialization;
@ -16,14 +15,15 @@ namespace Jellyfin.Plugin.Anime
public Plugin(
IApplicationPaths applicationPaths,
IXmlSerializer xmlSerializer,
ILogger logger)
ILogger<AniDbTitleMatcher> matcherLogger,
ILogger<AniDbTitleDownloader> downloaderLogger)
: base(applicationPaths, xmlSerializer)
{
Instance = this;
AniDbTitleMatcher.DefaultInstance = new AniDbTitleMatcher(
logger,
new AniDbTitleDownloader(logger, applicationPaths));
matcherLogger,
new AniDbTitleDownloader(downloaderLogger, applicationPaths));
}
/// <inheritdoc />

View File

@ -2,6 +2,7 @@
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
namespace Jellyfin.Plugin.Anime.Providers.AniDB
{
@ -10,12 +11,15 @@ namespace Jellyfin.Plugin.Anime.Providers.AniDB
public bool Supports(IHasProviderIds item)
=> item is Series || item is Movie;
public string Name
public string ProviderName
=> "AniDB";
public string Key
=> ProviderNames.AniDb;
public ExternalIdMediaType? Type
=> null;
public string UrlFormatString
=> "https://anidb.net/perl-bin/animedb.pl?show=anime&aid={0}";
}

View File

@ -21,9 +21,9 @@ namespace Jellyfin.Plugin.Anime.Providers.AniDB.Identity
private const string TitlesUrl = "https://anidb.net/api/anime-titles.xml.gz";
private static readonly HttpClient _httpClient;
private readonly ILogger _logger;
private readonly ILogger<AniDbTitleDownloader> _logger;
public AniDbTitleDownloader(ILogger logger, IApplicationPaths applicationPaths)
public AniDbTitleDownloader(ILogger<AniDbTitleDownloader> logger, IApplicationPaths applicationPaths)
{
_logger = logger;
Paths = GetDataPath(applicationPaths);

View File

@ -37,8 +37,8 @@ namespace Jellyfin.Plugin.Anime.Providers.AniDB.Identity
/// </summary>
public static IAniDbTitleMatcher DefaultInstance { get; set; }
private readonly ILogger _logger;
public readonly IAniDbTitleDownloader _downloader;
private readonly ILogger<AniDbTitleMatcher> _logger;
private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1);
public static Dictionary<string, TitleInfo> _titles;
@ -48,7 +48,7 @@ namespace Jellyfin.Plugin.Anime.Providers.AniDB.Identity
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="downloader">The AniDB title downloader.</param>
public AniDbTitleMatcher(ILogger logger, IAniDbTitleDownloader downloader)
public AniDbTitleMatcher(ILogger<AniDbTitleMatcher> logger, IAniDbTitleDownloader downloader)
{
_logger = logger;
_downloader = downloader;

View File

@ -13,14 +13,14 @@ namespace Jellyfin.Plugin.Anime.Providers.AniDB.Metadata
public class AniDbMovieProvider : IRemoteMetadataProvider<Movie, MovieInfo>
{
private readonly AniDbSeriesProvider _seriesProvider;
private readonly ILogger _log;
private readonly ILogger<AniDbMovieProvider> _logger;
public string Name => "AniDB";
public AniDbMovieProvider(IApplicationPaths appPaths, IHttpClient httpClient, ILoggerFactory loggerFactory)
public AniDbMovieProvider(IApplicationPaths appPaths, IHttpClient httpClient, ILogger<AniDbMovieProvider> logger)
{
_seriesProvider = new AniDbSeriesProvider(appPaths, httpClient);
_log = loggerFactory.CreateLogger("AniDB");
_logger = logger;
}
public async Task<MetadataResult<Movie>> GetMetadata(MovieInfo info, CancellationToken cancellationToken)

View File

@ -16,7 +16,7 @@ namespace Jellyfin.Plugin.Anime.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
/// 🛈 THIS IS AN UNOFFICAL API INTERFACE FOR EMBY
/// 🛈 THIS IS AN UNOFFICAL API INTERFACE FOR JELLYFIN
/// </summary>
public class AniListApi
{
@ -34,89 +34,82 @@ query ($query: String, $type: MediaType) {
coverImage {
medium
large
extraLarge
}
format
type
averageScore
popularity
episodes
season
hashtag
isAdult
startDate {
year
month
day
}
endDate {
year
month
day
}
}
}
}&variables={ ""query"":""{0}"",""type"":""ANIME""}";
public string AniList_anime_link = @"https://graphql.anilist.co/api/v2?query=query($id: Int!, $type: MediaType) {
Media(id: $id, type: $type)
{
id
title {
romaji
english
native
public string AnimeLink = @"https://graphql.anilist.co/api/v2?query=
query($id: Int!, $type: MediaType) {
Media(id: $id, type: $type) {
id
title {
romaji
english
native
userPreferred
}
startDate {
year
month
day
}
endDate {
year
month
day
}
coverImage {
large
medium
}
bannerImage
format
}
startDate {
year
month
day
}
endDate {
year
month
day
}
coverImage {
medium
large
extraLarge
}
bannerImage
format
type
status
episodes
chapters
volumes
season
seasonYear
description
averageScore
meanScore
genres
synonyms
duration
tags {
id
name
category
}
nextAiringEpisode {
airingAt
timeUntilAiring
airingAt
timeUntilAiring
episode
}
}
}&variables={ ""id"":""{0}"",""type"":""ANIME""}";
private const string AniList_anime_char_link = @"https://graphql.anilist.co/api/v2?query=query($id: Int!, $type: MediaType, $page: Int = 1) {
Media(id: $id, type: $type) {
id
characters(page: $page, sort: [ROLE]) {
pageInfo {
total
perPage
hasNextPage
currentPage
lastPage
studios {
nodes {
id
name
isAnimationStudio
}
}
characters(sort: [ROLE]) {
edges {
node {
id
name {
first
last
full
}
image {
medium
@ -129,6 +122,7 @@ query ($query: String, $type: MediaType) {
name {
first
last
full
native
}
image {
@ -149,233 +143,78 @@ query ($query: String, $type: MediaType) {
}
/// <summary>
/// API call to get the anime with the id
/// API call to get the anime with the given id
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public async Task<RemoteSearchResult> GetAnime(string id)
public async Task<Media> GetAnime(string id)
{
RootObject WebContent = await WebRequestAPI(AniList_anime_link.Replace("{0}",id));
var result = new RemoteSearchResult
{
Name = ""
};
result.SearchProviderName = WebContent.data.Media.title.romaji;
result.ImageUrl = WebContent.data.Media.coverImage.large;
result.SetProviderId(ProviderNames.AniList, id);
result.Overview = WebContent.data.Media.description;
return result;
return (await WebRequestAPI(AnimeLink.Replace("{0}", id))).data?.Media;
}
/// <summary>
/// API call to select the lang
/// </summary>
/// <param name="WebContent"></param>
/// <param name="preference"></param>
/// <param name="language"></param>
/// <returns></returns>
private string SelectName(RootObject WebContent, TitlePreferenceType preference, string language)
{
if (preference == TitlePreferenceType.Localized && language == "en")
return WebContent.data.Media.title.english;
if (preference == TitlePreferenceType.Japanese)
return WebContent.data.Media.title.native;
return WebContent.data.Media.title.romaji;
}
/// <summary>
/// API call to get the title with the right lang
/// </summary>
/// <param name="lang"></param>
/// <param name="WebContent"></param>
/// <returns></returns>
public string Get_title(string lang, RootObject WebContent)
{
switch (lang)
{
case "en":
return WebContent.data.Media.title.english;
case "jap":
return WebContent.data.Media.title.native;
//Default is jap_r
default:
return WebContent.data.Media.title.romaji;
}
}
public async Task<List<PersonInfo>> GetPersonInfo(int id, CancellationToken cancellationToken)
{
List<PersonInfo> lpi = new List<PersonInfo>();
RootObject WebContent = await WebRequestAPI(AniList_anime_char_link.Replace("{0}", id.ToString()));
foreach (Edge edge in WebContent.data.Media.characters.edges)
{
PersonInfo pi = new PersonInfo();
pi.Name = edge.node.name.first+" "+ edge.node.name.last;
pi.ImageUrl = edge.node.image.large;
pi.Role = edge.role;
}
return lpi;
}
/// <summary>
/// Convert int to Guid
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public async static Task<Guid> ToGuid(int value, CancellationToken cancellationToken)
{
byte[] bytes = new byte[16];
await Task.Run(() => BitConverter.GetBytes(value).CopyTo(bytes, 0), cancellationToken);
return new Guid(bytes);
}
/// <summary>
/// API call to get the genre of the anime
/// </summary>
/// <param name="WebContent"></param>
/// <returns></returns>
public List<string> Get_Genre(RootObject WebContent)
{
return WebContent.data.Media.genres;
}
/// <summary>
/// API call to get the img url
/// </summary>
/// <param name="WebContent"></param>
/// <returns></returns>
public string Get_ImageUrl(RootObject WebContent)
{
return WebContent.data.Media.coverImage.large;
}
/// <summary>
/// API call too get the rating
/// </summary>
/// <param name="WebContent"></param>
/// <returns></returns>
public string Get_Rating(RootObject WebContent)
{
return (WebContent.data.Media.averageScore / 10).ToString();
}
/// <summary>
/// API call to get the description
/// </summary>
/// <param name="WebContent"></param>
/// <returns></returns>
public string Get_Overview(RootObject WebContent)
{
return WebContent.data.Media.description;
}
/// <summary>
/// API call to search a title and return the right one back
/// API call to search a title and return the first result
/// </summary>
/// <param name="title"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<string> Search_GetSeries(string title, CancellationToken cancellationToken)
public async Task<MediaSearchResult> Search_GetSeries(string title, CancellationToken cancellationToken)
{
string result = null;
// Reimplemented instead of calling Search_GetSeries_list() for efficiency
RootObject WebContent = await WebRequestAPI(SearchLink.Replace("{0}", title));
foreach (Medium media in WebContent.data.Page.media) {
//get id
try
{
if (await Equals_check.Compare_strings(media.title.romaji, title, cancellationToken))
{
return media.id.ToString();
}
if (await Equals_check.Compare_strings(media.title.english, title, cancellationToken))
{
return media.id.ToString();
}
//Disabled due to false result.
/*if (await Task.Run(() => Equals_check.Compare_strings(media.title.native, title)))
{
return media.id.ToString();
}*/
}
catch (Exception) { }
}
return result;
}
/// <summary>
/// API call to search a title and return a list back
/// </summary>
/// <param name="title"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<List<string>> Search_GetSeries_list(string title, CancellationToken cancellationToken)
{
List<string> result = new List<string>();
RootObject WebContent = await WebRequestAPI(SearchLink.Replace("{0}", title));
foreach (Medium media in WebContent.data.Page.media)
foreach (MediaSearchResult media in WebContent.data.Page.media)
{
//get id
try
{
if (await Equals_check.Compare_strings(media.title.romaji, title, cancellationToken))
{
result.Add(media.id.ToString());
}
if (await Equals_check.Compare_strings(media.title.english, title, cancellationToken))
{
result.Add(media.id.ToString());
}
//Disabled due to false result.
/*if (await Task.Run(() => Equals_check.Compare_strings(media.title.native, title)))
{
result.Add(media.id.ToString());
}*/
}
catch (Exception) { }
}
return result;
}
/// <summary>
/// SEARCH Title
/// </summary>
public async Task<string> FindSeries(string title, CancellationToken cancellationToken)
{
string aid = await Search_GetSeries(title, cancellationToken);
if (!string.IsNullOrEmpty(aid))
{
return aid;
}
aid = await Search_GetSeries(await Equals_check.Clear_name(title, cancellationToken), cancellationToken);
if (!string.IsNullOrEmpty(aid))
{
return aid;
return media;
}
return null;
}
/// <summary>
/// GET website content from the link
/// API call to search a title and return a list of results
/// </summary>
/// <param name="title"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<List<MediaSearchResult>> Search_GetSeries_list(string title, CancellationToken cancellationToken)
{
return (await WebRequestAPI(SearchLink.Replace("{0}", title))).data.Page.media;
}
/// <summary>
/// Search for anime with the given title. Attempts to fuzzy search by removing special characters
/// </summary>
/// <param name="title"></param>
/// <returns></returns>
public async Task<string> FindSeries(string title, CancellationToken cancellationToken)
{
MediaSearchResult result = await Search_GetSeries(title, cancellationToken);
if (result != null)
{
return result.id.ToString();
}
result = await Search_GetSeries(await Equals_check.Clear_name(title, cancellationToken), cancellationToken);
if (result != null)
{
return result.id.ToString();
}
return null;
}
/// <summary>
/// GET and parse JSON content from link, deserialize into a RootObject
/// </summary>
/// <param name="link"></param>
/// <returns></returns>
public async Task<RootObject> WebRequestAPI(string link)
{
using (HttpContent content = new FormUrlEncodedContent(Enumerable.Empty<KeyValuePair<string, string>>()))
using (var response = await _httpClient.PostAsync(link, content).ConfigureAwait(false))
using (var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
{
return await JsonSerializer.DeserializeAsync<RootObject>(responseStream).ConfigureAwait(false);
}
using (HttpContent content = new FormUrlEncodedContent(Enumerable.Empty<KeyValuePair<string, string>>()))
using (var response = await _httpClient.PostAsync(link, content).ConfigureAwait(false))
using (var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
{
return await JsonSerializer.DeserializeAsync<RootObject>(responseStream).ConfigureAwait(false);
}
}
}
}

View File

@ -1,20 +1,25 @@
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
namespace Jellyfin.Plugin.Anime.Providers.AniList
{
public class AniListExternalId : IExternalId
{
public bool Supports(IHasProviderIds item)
=> item is Series;
=> item is Series || item is Movie;
public string Name
public string ProviderName
=> "AniList";
public string Key
=> ProviderNames.AniList;
public ExternalIdMediaType? Type
=> ExternalIdMediaType.Series;
public string UrlFormatString
=> "https://anilist.co/anime/{0}/";
}

View File

@ -0,0 +1,84 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
namespace Jellyfin.Plugin.Anime.Providers.AniList
{
public class AniListImageProvider : IRemoteImageProvider
{
private readonly IHttpClient _httpClient;
private readonly AniListApi _aniListApi;
public AniListImageProvider(IHttpClient httpClient)
{
_httpClient = httpClient;
_aniListApi = new AniListApi();
}
public string Name => "AniList";
public bool Supports(BaseItem item) => item is Series || item is Season || item is Movie;
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
{
return new[] { ImageType.Primary, ImageType.Banner };
}
public Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
{
var seriesId = item.GetProviderId(ProviderNames.AniList);
return GetImages(seriesId, cancellationToken);
}
public async Task<IEnumerable<RemoteImageInfo>> GetImages(string aid, CancellationToken cancellationToken)
{
var list = new List<RemoteImageInfo>();
if (!string.IsNullOrEmpty(aid))
{
Media media = await _aniListApi.GetAnime(aid);
if (media != null)
{
if (media.GetImageUrl() != null)
{
list.Add(new RemoteImageInfo
{
ProviderName = Name,
Type = ImageType.Primary,
Url = media.GetImageUrl()
});
}
if (media.bannerImage != null)
{
list.Add(new RemoteImageInfo
{
ProviderName = Name,
Type = ImageType.Banner,
Url = media.bannerImage
});
}
}
}
return list;
}
public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
{
return _httpClient.GetResponse(new HttpRequestOptions
{
UserAgent = Constants.UserAgent,
CancellationToken = cancellationToken,
Url = url
});
}
}
}

View File

@ -0,0 +1,113 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
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 MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
//API v2
namespace Jellyfin.Plugin.Anime.Providers.AniList
{
public class AniListMovieProvider : IRemoteMetadataProvider<Movie, MovieInfo>, IHasOrder
{
private readonly IHttpClient _httpClient;
private readonly IApplicationPaths _paths;
private readonly ILogger _log;
private readonly AniListApi _aniListApi;
public int Order => -2;
public string Name => "AniList";
public AniListMovieProvider(IApplicationPaths appPaths, IHttpClient httpClient, ILogger<AniListMovieProvider> logger)
{
_log = logger;
_httpClient = httpClient;
_aniListApi = new AniListApi();
_paths = appPaths;
}
public async Task<MetadataResult<Movie>> GetMetadata(MovieInfo info, CancellationToken cancellationToken)
{
var result = new MetadataResult<Movie>();
Media media = null;
var aid = info.ProviderIds.GetOrDefault(ProviderNames.AniList);
if (!string.IsNullOrEmpty(aid))
{
media = await _aniListApi.GetAnime(aid);
}
else
{
_log.LogInformation("Start AniList... Searching({Name})", info.Name);
MediaSearchResult msr = await _aniListApi.Search_GetSeries(info.Name, cancellationToken);
if (msr != null)
{
media = await _aniListApi.GetAnime(msr.id.ToString());
}
}
if (media != null)
{
result.HasMetadata = true;
result.Item = media.ToMovie();
result.People = media.GetPeopleInfo();
result.Provider = ProviderNames.AniList;
StoreImageUrl(media.id.ToString(), media.GetImageUrl(), "image");
}
return result;
}
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(MovieInfo searchInfo, CancellationToken cancellationToken)
{
var results = new List<RemoteSearchResult>();
var aid = searchInfo.ProviderIds.GetOrDefault(ProviderNames.AniList);
if (!string.IsNullOrEmpty(aid))
{
Media aid_result = await _aniListApi.GetAnime(aid).ConfigureAwait(false);
if (aid_result != null)
{
results.Add(aid_result.ToSearchResult());
}
}
if (!string.IsNullOrEmpty(searchInfo.Name))
{
List<MediaSearchResult> name_results = await _aniListApi.Search_GetSeries_list(searchInfo.Name, cancellationToken).ConfigureAwait(false);
foreach (var media in name_results)
{
results.Add(media.ToSearchResult());
}
}
return results;
}
private void StoreImageUrl(string series, string url, string type)
{
var path = Path.Combine(_paths.CachePath, "anilist", type, series + ".txt");
var directory = Path.GetDirectoryName(path);
Directory.CreateDirectory(directory);
File.WriteAllText(path, url);
}
public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
{
return _httpClient.GetResponse(new HttpRequestOptions
{
UserAgent = Constants.UserAgent,
CancellationToken = cancellationToken,
Url = url
});
}
}
}

View File

@ -20,7 +20,7 @@ namespace Jellyfin.Plugin.Anime.Providers.AniList
{
private readonly IHttpClient _httpClient;
private readonly IApplicationPaths _paths;
private readonly ILogger _log;
private readonly ILogger<AniListSeriesProvider> _log;
private readonly AniListApi _aniListApi;
public int Order => -2;
public string Name => "AniList";
@ -36,33 +36,30 @@ namespace Jellyfin.Plugin.Anime.Providers.AniList
public async Task<MetadataResult<Series>> GetMetadata(SeriesInfo info, CancellationToken cancellationToken)
{
var result = new MetadataResult<Series>();
Media media = null;
var aid = info.ProviderIds.GetOrDefault(ProviderNames.AniList);
if (string.IsNullOrEmpty(aid))
{
_log.LogInformation("Start AniList... Searching({Name})", info.Name);
aid = await _aniListApi.FindSeries(info.Name, cancellationToken);
}
if (!string.IsNullOrEmpty(aid))
{
RootObject WebContent = await _aniListApi.WebRequestAPI(_aniListApi.AniList_anime_link.Replace("{0}", aid));
result.Item = new Series();
result.HasMetadata = true;
result.People = await _aniListApi.GetPersonInfo(WebContent.data.Media.id, cancellationToken);
result.Item.ProviderIds.Add(ProviderNames.AniList, aid);
result.Item.Overview = WebContent.data.Media.description;
try
media = await _aniListApi.GetAnime(aid);
}
else
{
_log.LogInformation("Start AniList... Searching({Name})", info.Name);
MediaSearchResult msr = await _aniListApi.Search_GetSeries(info.Name, cancellationToken);
if (msr != null)
{
//AniList has a max rating of 5
result.Item.CommunityRating = WebContent.data.Media.averageScore / 10;
media = await _aniListApi.GetAnime(msr.id.ToString());
}
catch (Exception) { }
foreach (var genre in _aniListApi.Get_Genre(WebContent))
result.Item.AddGenre(genre);
GenreHelper.CleanupGenres(result.Item);
StoreImageUrl(aid, WebContent.data.Media.coverImage.large, "image");
}
if (media != null)
{
result.HasMetadata = true;
result.Item = media.ToSeries();
result.People = media.GetPeopleInfo();
result.Provider = ProviderNames.AniList;
StoreImageUrl(media.id.ToString(), media.GetImageUrl(), "image");
}
return result;
@ -70,27 +67,28 @@ namespace Jellyfin.Plugin.Anime.Providers.AniList
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeriesInfo searchInfo, CancellationToken cancellationToken)
{
var results = new Dictionary<string, RemoteSearchResult>();
var results = new List<RemoteSearchResult>();
var aid = searchInfo.ProviderIds.GetOrDefault(ProviderNames.AniList);
if (!string.IsNullOrEmpty(aid))
{
if (!results.ContainsKey(aid))
Media aid_result = await _aniListApi.GetAnime(aid).ConfigureAwait(false);
if (aid_result != null)
{
results.Add(aid, await _aniListApi.GetAnime(aid).ConfigureAwait(false));
results.Add(aid_result.ToSearchResult());
}
}
if (!string.IsNullOrEmpty(searchInfo.Name))
{
List<string> ids = await _aniListApi.Search_GetSeries_list(searchInfo.Name, cancellationToken).ConfigureAwait(false);
foreach (string a in ids)
List<MediaSearchResult> name_results = await _aniListApi.Search_GetSeries_list(searchInfo.Name, cancellationToken).ConfigureAwait(false);
foreach (var media in name_results)
{
results.Add(a, await _aniListApi.GetAnime(a).ConfigureAwait(false));
results.Add(media.ToSearchResult());
}
}
return results.Values;
return results;
}
private void StoreImageUrl(string series, string url, string type)
@ -112,57 +110,4 @@ namespace Jellyfin.Plugin.Anime.Providers.AniList
});
}
}
public class AniListSeriesImageProvider : IRemoteImageProvider
{
private readonly IHttpClient _httpClient;
private readonly AniListApi _aniListApi;
public AniListSeriesImageProvider(IHttpClient httpClient)
{
_httpClient = httpClient;
_aniListApi = new AniListApi();
}
public string Name => "AniList";
public bool Supports(BaseItem item) => item is Series || item is Season;
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
{
return new[] { ImageType.Primary };
}
public Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
{
var seriesId = item.GetProviderId(ProviderNames.AniList);
return GetImages(seriesId, cancellationToken);
}
public async Task<IEnumerable<RemoteImageInfo>> GetImages(string aid, CancellationToken cancellationToken)
{
var list = new List<RemoteImageInfo>();
if (!string.IsNullOrEmpty(aid))
{
var primary = _aniListApi.Get_ImageUrl(await _aniListApi.WebRequestAPI(_aniListApi.AniList_anime_link.Replace("{0}", aid)));
list.Add(new RemoteImageInfo
{
ProviderName = Name,
Type = ImageType.Primary,
Url = primary
});
}
return list;
}
public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
{
return _httpClient.GetResponse(new HttpRequestOptions
{
UserAgent = Constants.UserAgent,
CancellationToken = cancellationToken,
Url = url
});
}
}
}

View File

@ -1,6 +1,12 @@
using System;
using System.Collections.Generic;
using System.Text;
using MediaBrowser.Model.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Entities.Movies;
using Jellyfin.Plugin.Anime.Configuration;
namespace Jellyfin.Plugin.Anime.Providers.AniList
{
@ -17,51 +23,19 @@ namespace Jellyfin.Plugin.Anime.Providers.AniList
{
public string medium { get; set; }
public string large { get; set; }
public string extraLarge { get; set; }
}
public class StartDate
public class ApiDate
{
public int year { get; set; }
public int month { get; set; }
public int day { get; set; }
}
public class EndDate
{
public int year { get; set; }
public int month { get; set; }
public int day { get; set; }
}
public class Medium
{
public int id { get; set; }
public Title title { get; set; }
public CoverImage coverImage { get; set; }
public string format { get; set; }
public string type { get; set; }
public int averageScore { get; set; }
public int popularity { get; set; }
public int episodes { get; set; }
public string season { get; set; }
public string hashtag { get; set; }
public bool isAdult { get; set; }
public StartDate startDate { get; set; }
public EndDate endDate { get; set; }
public object bannerImage { get; set; }
public string status { get; set; }
public object chapters { get; set; }
public object volumes { get; set; }
public string description { get; set; }
public int meanScore { get; set; }
public List<string> genres { get; set; }
public List<object> synonyms { get; set; }
public object nextAiringEpisode { get; set; }
public int? year { get; set; }
public int? month { get; set; }
public int? day { get; set; }
}
public class Page
{
public List<Medium> media { get; set; }
public List<MediaSearchResult> media { get; set; }
}
public class Data
@ -70,31 +44,234 @@ namespace Jellyfin.Plugin.Anime.Providers.AniList
public Media Media { get; set; }
}
public class Media
/// <summary>
/// A slimmed down version of Media to avoid confusion and reduce
/// the size of responses when searching.
/// </summary>
public class MediaSearchResult
{
public Characters characters { get; set; }
public int popularity { get; set; }
public object hashtag { get; set; }
public bool isAdult { get; set; }
public int id { get; set; }
public Title title { get; set; }
public StartDate startDate { get; set; }
public EndDate endDate { get; set; }
public ApiDate startDate { get; set; }
public CoverImage coverImage { get; set; }
public object bannerImage { get; set; }
public string format { get; set; }
public string type { get; set; }
public string status { get; set; }
public int episodes { get; set; }
/// <summary>
/// Get the title in configured language
/// </summary>
/// <param name="language"></param>
/// <returns></returns>
public string GetPreferredTitle(string language)
{
PluginConfiguration config = Plugin.Instance.Configuration;
if (config.TitlePreference == TitlePreferenceType.Localized)
{
if (language == "en")
{
return this.title.english;
}
if (language == "jap")
{
return this.title.native;
}
}
if (config.TitlePreference == TitlePreferenceType.Japanese)
{
return this.title.native;
}
return this.title.romaji;
}
/// <summary>
/// Get the highest quality image url
/// </summary>
/// <returns></returns>
public string GetImageUrl()
{
return this.coverImage.extraLarge ?? this.coverImage.large ?? this.coverImage.medium;
}
/// <summary>
/// Returns the start date as a DateTime object or null if not available
/// </summary>
/// <returns></returns>
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);
}
/// <summary>
/// Convert a Media/MediaSearchResult object to a RemoteSearchResult
/// </summary>
/// <returns></returns>
public RemoteSearchResult ToSearchResult()
{
return new RemoteSearchResult
{
Name = this.GetPreferredTitle("en"),
ProductionYear = this.startDate.year,
PremiereDate = this.GetStartDate(),
ImageUrl = this.GetImageUrl(),
SearchProviderName = ProviderNames.AniList,
ProviderIds = new Dictionary<string, string>() {{ProviderNames.AniList, this.id.ToString()}}
};
}
}
public class Media: MediaSearchResult
{
public int? averageScore { get; set; }
public string bannerImage { get; set; }
public object chapters { get; set; }
public object volumes { get; set; }
public string season { get; set; }
public Characters characters { get; set; }
public string description { get; set; }
public int averageScore { get; set; }
public int meanScore { get; set; }
public int? duration { get; set; }
public ApiDate endDate { get; set; }
public int? episodes { get; set; }
public string format { get; set; }
public List<string> genres { get; set; }
public List<object> synonyms { get; set; }
public object hashtag { get; set; }
public bool isAdult { get; set; }
public int? meanScore { get; set; }
public object nextAiringEpisode { get; set; }
public int? popularity { get; set; }
public string season { get; set; }
public int? seasonYear { get; set; }
public string status { get; set; }
public StudioConnection studios { get; set; }
public List<object> synonyms { get; set; }
public List<Tag> tags { get; set; }
public string type { get; set; }
public object volumes { get; set; }
/// <summary>
/// Get the rating, normalized to 1-10
/// </summary>
/// <returns></returns>
public float GetRating()
{
return (this.averageScore ?? 0) / 10f;
}
/// <summary>
/// Returns the end date as a DateTime object or null if not available
/// </summary>
/// <returns></returns>
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);
}
/// <summary>
/// Returns a list of studio names
/// </summary>
/// <returns></returns>
public List<string> GetStudioNames()
{
List<string> results = new List<string>();
foreach (Studio node in this.studios.nodes)
{
results.Add(node.name);
}
return results;
}
/// <summary>
/// Returns a list of PersonInfo for voice actors
/// </summary>
/// <returns></returns>
public List<PersonInfo> GetPeopleInfo()
{
List<PersonInfo> lpi = new List<PersonInfo>();
foreach (CharacterEdge edge in this.characters.edges)
{
foreach (VoiceActor va in edge.voiceActors)
{
PeopleHelper.AddPerson(lpi, new PersonInfo {
Name = va.name.full,
ImageUrl = va.image.large ?? va.image.medium,
Role = edge.node.name.full,
Type = PersonType.Actor,
ProviderIds = new Dictionary<string, string>() {{ProviderNames.AniList, this.id.ToString()}}
});
}
}
return lpi;
}
/// <summary>
/// Returns a list of tag names
/// </summary>
/// <returns></returns>
public List<string> GetTagNames()
{
List<string> results = new List<string>();
foreach (Tag tag in this.tags)
{
results.Add(tag.name);
}
return results;
}
/// <summary>
/// Convert a Media object to a Series
/// </summary>
/// <returns></returns>
public Series ToSeries()
{
var result = new Series {
Name = this.GetPreferredTitle("en"),
Overview = this.description,
ProductionYear = this.startDate.year,
PremiereDate = this.GetStartDate(),
EndDate = this.GetStartDate(),
CommunityRating = this.GetRating(),
RunTimeTicks = this.duration.HasValue ? TimeSpan.FromMinutes(this.duration.Value).Ticks : (long?)null,
Genres = this.genres.ToArray(),
Tags = this.GetTagNames().ToArray(),
Studios = this.GetStudioNames().ToArray(),
ProviderIds = new Dictionary<string, string>() {{ProviderNames.AniList, this.id.ToString()}}
};
if (this.status == "FINISHED" || this.status == "CANCELLED")
{
result.Status = SeriesStatus.Ended;
}
else if (this.status == "RELEASING")
{
result.Status = SeriesStatus.Continuing;
}
return result;
}
/// <summary>
/// Convert a Media object to a Movie
/// </summary>
/// <returns></returns>
public Movie ToMovie()
{
return new Movie {
Name = this.GetPreferredTitle("en"),
Overview = this.description,
ProductionYear = this.startDate.year,
PremiereDate = this.GetStartDate(),
EndDate = this.GetStartDate(),
CommunityRating = this.GetRating(),
Genres = this.genres.ToArray(),
Tags = this.GetTagNames().ToArray(),
Studios = this.GetStudioNames().ToArray(),
ProviderIds = new Dictionary<string, string>() {{ProviderNames.AniList, this.id.ToString()}}
};
}
}
public class PageInfo
{
@ -117,10 +294,10 @@ namespace Jellyfin.Plugin.Anime.Providers.AniList
public string large { get; set; }
}
public class Node
public class Character
{
public int id { get; set; }
public Name name { get; set; }
public Name2 name { get; set; }
public Image image { get; set; }
}
@ -128,34 +305,48 @@ namespace Jellyfin.Plugin.Anime.Providers.AniList
{
public string first { get; set; }
public string last { get; set; }
public string full { get; set; }
public string native { get; set; }
}
public class Image2
{
public string medium { get; set; }
public string large { get; set; }
}
public class VoiceActor
{
public int id { get; set; }
public Name2 name { get; set; }
public Image2 image { get; set; }
public Image image { get; set; }
public string language { get; set; }
}
public class Edge
public class CharacterEdge
{
public Node node { get; set; }
public Character node { get; set; }
public string role { get; set; }
public List<VoiceActor> voiceActors { get; set; }
}
public class Characters
{
public PageInfo pageInfo { get; set; }
public List<Edge> edges { get; set; }
public List<CharacterEdge> edges { get; set; }
}
public class Tag
{
public int id { get; set; }
public string name { get; set; }
public string description { get; set; }
public string category { get; set; }
}
public class Studio
{
public int id { get; set; }
public string name { get; set; }
public bool isAnimationStudio { get; set; }
}
public class StudioConnection
{
public List<Studio> nodes { get; set; }
}
public class RootObject

View File

@ -1,6 +1,7 @@
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
namespace Jellyfin.Plugin.Anime.Providers.AniSearch
{
@ -9,12 +10,15 @@ namespace Jellyfin.Plugin.Anime.Providers.AniSearch
public bool Supports(IHasProviderIds item)
=> item is Series;
public string Name
public string ProviderName
=> "AniSearch";
public string Key
=> ProviderNames.AniSearch;
public ExternalIdMediaType? Type
=> ExternalIdMediaType.Series;
public string UrlFormatString
=> "https://www.anisearch.com/anime/{0}";
}

View File

@ -18,13 +18,13 @@ namespace Jellyfin.Plugin.Anime.Providers.AniSearch
{
private readonly IHttpClient _httpClient;
private readonly IApplicationPaths _paths;
private readonly ILogger _log;
private readonly ILogger<AniSearchSeriesProvider> _log;
public int Order => -3;
public string Name => "AniSearch";
public AniSearchSeriesProvider(IApplicationPaths appPaths, IHttpClient httpClient, ILoggerFactory loggerFactory)
public AniSearchSeriesProvider(IApplicationPaths appPaths, IHttpClient httpClient, ILogger<AniSearchSeriesProvider> logger)
{
_log = loggerFactory.CreateLogger("AniSearch");
_log = logger;
_httpClient = httpClient;
_paths = appPaths;
}

View File

@ -13,9 +13,9 @@ namespace Jellyfin.Plugin.Anime.Providers
{
internal class Equals_check
{
public readonly ILogger _logger;
public readonly ILogger<Equals_check> _logger;
public Equals_check(ILogger logger)
public Equals_check(ILogger<Equals_check> logger)
{
_logger = logger;
}

View File

@ -1,8 +1,8 @@
---
name: "Anime"
guid: "a4df60c5-6ab4-412a-8f79-2cab93fb2bc5"
version: "8.0.0.0"
targetAbi: "10.5.0.0"
version: "9.0.0.0"
targetAbi: "10.6.0.0"
owner: "jellyfin"
overview: "Manage your anime from Jellyfin"
description: >