parent
d338425951
commit
1335efd487
|
@ -190,6 +190,23 @@ namespace NzbDrone.Core.ImportLists
|
||||||
item.Title = mappedSeries.Title;
|
item.Title = mappedSeries.Title;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Map by MyAniList ID if we have it
|
||||||
|
if (item.TvdbId <= 0 && item.MalId > 0)
|
||||||
|
{
|
||||||
|
var mappedSeries = _seriesSearchService.SearchForNewSeriesByMyAnimeListId(item.MalId)
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
if (mappedSeries == null)
|
||||||
|
{
|
||||||
|
_logger.Debug("Rejected, unable to find matching TVDB ID for MAL ID: {0} [{1}]", item.MalId, item.Title);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
item.TvdbId = mappedSeries.TvdbId;
|
||||||
|
item.Title = mappedSeries.Title;
|
||||||
|
}
|
||||||
|
|
||||||
if (item.TvdbId == 0)
|
if (item.TvdbId == 0)
|
||||||
{
|
{
|
||||||
_logger.Debug("[{0}] Rejected, unable to find TVDB ID", item.Title);
|
_logger.Debug("[{0}] Rejected, unable to find TVDB ID", item.Title);
|
||||||
|
|
|
@ -0,0 +1,121 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net.Http;
|
||||||
|
using NLog;
|
||||||
|
using NzbDrone.Common.Cloud;
|
||||||
|
using NzbDrone.Common.Http;
|
||||||
|
using NzbDrone.Core.Configuration;
|
||||||
|
using NzbDrone.Core.Localization;
|
||||||
|
using NzbDrone.Core.Parser;
|
||||||
|
using NzbDrone.Core.Validation;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.ImportLists.MyAnimeList
|
||||||
|
{
|
||||||
|
public class MyAnimeListImport : HttpImportListBase<MyAnimeListSettings>
|
||||||
|
{
|
||||||
|
public const string OAuthPath = "oauth/myanimelist/authorize";
|
||||||
|
public const string RedirectUriPath = "oauth/myanimelist/auth";
|
||||||
|
public const string RenewUriPath = "oauth/myanimelist/renew";
|
||||||
|
|
||||||
|
public override string Name => "MyAnimeList";
|
||||||
|
public override ImportListType ListType => ImportListType.Other;
|
||||||
|
public override TimeSpan MinRefreshInterval => TimeSpan.FromHours(6);
|
||||||
|
|
||||||
|
private readonly IImportListRepository _importListRepository;
|
||||||
|
private readonly IHttpRequestBuilderFactory _requestBuilder;
|
||||||
|
|
||||||
|
// This constructor the first thing that is called when sonarr creates a button
|
||||||
|
public MyAnimeListImport(IImportListRepository netImportRepository, IHttpClient httpClient, IImportListStatusService importListStatusService, IConfigService configService, IParsingService parsingService, ILocalizationService localizationService, ISonarrCloudRequestBuilder requestBuilder, Logger logger)
|
||||||
|
: base(httpClient, importListStatusService, configService, parsingService, localizationService, logger)
|
||||||
|
{
|
||||||
|
_importListRepository = netImportRepository;
|
||||||
|
_requestBuilder = requestBuilder.Services;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override ImportListFetchResult Fetch()
|
||||||
|
{
|
||||||
|
if (Settings.Expires < DateTime.UtcNow.AddMinutes(5))
|
||||||
|
{
|
||||||
|
RefreshToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
return FetchItems(g => g.GetListItems());
|
||||||
|
}
|
||||||
|
|
||||||
|
// MAL OAuth info: https://myanimelist.net/blog.php?eid=835707
|
||||||
|
// The whole process is handled through Sonarr's services.
|
||||||
|
public override object RequestAction(string action, IDictionary<string, string> query)
|
||||||
|
{
|
||||||
|
if (action == "startOAuth")
|
||||||
|
{
|
||||||
|
var request = _requestBuilder.Create()
|
||||||
|
.Resource(OAuthPath)
|
||||||
|
.AddQueryParam("state", query["callbackUrl"])
|
||||||
|
.AddQueryParam("redirect_uri", _requestBuilder.Create().Resource(RedirectUriPath).Build().Url.ToString())
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
OauthUrl = request.Url.ToString()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else if (action == "getOAuthToken")
|
||||||
|
{
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
accessToken = query["access_token"],
|
||||||
|
expires = DateTime.UtcNow.AddSeconds(int.Parse(query["expires_in"])),
|
||||||
|
refreshToken = query["refresh_token"]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return new { };
|
||||||
|
}
|
||||||
|
|
||||||
|
public override IParseImportListResponse GetParser()
|
||||||
|
{
|
||||||
|
return new MyAnimeListParser();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override IImportListRequestGenerator GetRequestGenerator()
|
||||||
|
{
|
||||||
|
return new MyAnimeListRequestGenerator()
|
||||||
|
{
|
||||||
|
Settings = Settings,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshToken()
|
||||||
|
{
|
||||||
|
_logger.Trace("Refreshing Token");
|
||||||
|
|
||||||
|
Settings.Validate().Filter("RefreshToken").ThrowOnError();
|
||||||
|
|
||||||
|
var httpReq = _requestBuilder.Create()
|
||||||
|
.Resource(RenewUriPath)
|
||||||
|
.AddQueryParam("refresh_token", Settings.RefreshToken)
|
||||||
|
.Build();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var httpResp = _httpClient.Get<MyAnimeListAuthToken>(httpReq);
|
||||||
|
|
||||||
|
if (httpResp?.Resource != null)
|
||||||
|
{
|
||||||
|
var token = httpResp.Resource;
|
||||||
|
Settings.AccessToken = token.AccessToken;
|
||||||
|
Settings.Expires = DateTime.UtcNow.AddSeconds(token.ExpiresIn);
|
||||||
|
Settings.RefreshToken = token.RefreshToken ?? Settings.RefreshToken;
|
||||||
|
|
||||||
|
if (Definition.Id > 0)
|
||||||
|
{
|
||||||
|
_importListRepository.UpdateSettings((ImportListDefinition)Definition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (HttpRequestException)
|
||||||
|
{
|
||||||
|
_logger.Error("Error trying to refresh MAL access token.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using NLog;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
|
using NzbDrone.Common.Instrumentation;
|
||||||
|
using NzbDrone.Common.Serializer;
|
||||||
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.ImportLists.MyAnimeList
|
||||||
|
{
|
||||||
|
public class MyAnimeListParser : IParseImportListResponse
|
||||||
|
{
|
||||||
|
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(MyAnimeListParser));
|
||||||
|
|
||||||
|
public IList<ImportListItemInfo> ParseResponse(ImportListResponse importListResponse)
|
||||||
|
{
|
||||||
|
var jsonResponse = Json.Deserialize<MyAnimeListResponse>(importListResponse.Content);
|
||||||
|
var series = new List<ImportListItemInfo>();
|
||||||
|
|
||||||
|
foreach (var show in jsonResponse.Animes)
|
||||||
|
{
|
||||||
|
series.AddIfNotNull(new ImportListItemInfo
|
||||||
|
{
|
||||||
|
Title = show.AnimeListInfo.Title,
|
||||||
|
MalId = show.AnimeListInfo.Id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return series;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using NzbDrone.Common.Http;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.ImportLists.MyAnimeList
|
||||||
|
{
|
||||||
|
public class MyAnimeListRequestGenerator : IImportListRequestGenerator
|
||||||
|
{
|
||||||
|
public MyAnimeListSettings Settings { get; set; }
|
||||||
|
|
||||||
|
private static readonly Dictionary<MyAnimeListStatus, string> StatusMapping = new Dictionary<MyAnimeListStatus, string>
|
||||||
|
{
|
||||||
|
{ MyAnimeListStatus.Watching, "watching" },
|
||||||
|
{ MyAnimeListStatus.Completed, "completed" },
|
||||||
|
{ MyAnimeListStatus.OnHold, "on_hold" },
|
||||||
|
{ MyAnimeListStatus.Dropped, "dropped" },
|
||||||
|
{ MyAnimeListStatus.PlanToWatch, "plan_to_watch" },
|
||||||
|
};
|
||||||
|
|
||||||
|
public virtual ImportListPageableRequestChain GetListItems()
|
||||||
|
{
|
||||||
|
var pageableReq = new ImportListPageableRequestChain();
|
||||||
|
|
||||||
|
pageableReq.Add(GetSeriesRequest());
|
||||||
|
|
||||||
|
return pageableReq;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<ImportListRequest> GetSeriesRequest()
|
||||||
|
{
|
||||||
|
var status = (MyAnimeListStatus)Settings.ListStatus;
|
||||||
|
var requestBuilder = new HttpRequestBuilder(Settings.BaseUrl.Trim());
|
||||||
|
|
||||||
|
requestBuilder.Resource("users/@me/animelist");
|
||||||
|
requestBuilder.AddQueryParam("fields", "list_status");
|
||||||
|
requestBuilder.AddQueryParam("limit", "1000");
|
||||||
|
requestBuilder.Accept(HttpAccept.Json);
|
||||||
|
|
||||||
|
if (status != MyAnimeListStatus.All && StatusMapping.TryGetValue(status, out var statusName))
|
||||||
|
{
|
||||||
|
requestBuilder.AddQueryParam("status", statusName);
|
||||||
|
}
|
||||||
|
|
||||||
|
var httpReq = new ImportListRequest(requestBuilder.Build());
|
||||||
|
|
||||||
|
httpReq.HttpRequest.Headers.Add("Authorization", $"Bearer {Settings.AccessToken}");
|
||||||
|
|
||||||
|
yield return httpReq;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.ImportLists.MyAnimeList
|
||||||
|
{
|
||||||
|
public class MyAnimeListResponse
|
||||||
|
{
|
||||||
|
[JsonProperty("data")]
|
||||||
|
public List<MyAnimeListItem> Animes { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MyAnimeListItem
|
||||||
|
{
|
||||||
|
[JsonProperty("node")]
|
||||||
|
public MyAnimeListItemInfo AnimeListInfo { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("list_status")]
|
||||||
|
public MyAnimeListStatusResult ListStatus { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MyAnimeListStatusResult
|
||||||
|
{
|
||||||
|
public string Status { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MyAnimeListItemInfo
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Title { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MyAnimeListIds
|
||||||
|
{
|
||||||
|
[JsonProperty("mal_id")]
|
||||||
|
public int MalId { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("thetvdb_id")]
|
||||||
|
public int TvdbId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MyAnimeListAuthToken
|
||||||
|
{
|
||||||
|
[JsonProperty("token_type")]
|
||||||
|
public string TokenType { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("expires_in")]
|
||||||
|
public int ExpiresIn { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("access_token")]
|
||||||
|
public string AccessToken { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("refresh_token")]
|
||||||
|
public string RefreshToken { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
using System;
|
||||||
|
using FluentValidation;
|
||||||
|
using NzbDrone.Core.Annotations;
|
||||||
|
using NzbDrone.Core.Validation;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.ImportLists.MyAnimeList
|
||||||
|
{
|
||||||
|
public class MalSettingsValidator : AbstractValidator<MyAnimeListSettings>
|
||||||
|
{
|
||||||
|
public MalSettingsValidator()
|
||||||
|
{
|
||||||
|
RuleFor(c => c.BaseUrl).ValidRootUrl();
|
||||||
|
RuleFor(c => c.AccessToken).NotEmpty()
|
||||||
|
.OverridePropertyName("SignIn")
|
||||||
|
.WithMessage("Must authenticate with MyAnimeList");
|
||||||
|
|
||||||
|
RuleFor(c => c.ListStatus).Custom((status, context) =>
|
||||||
|
{
|
||||||
|
if (!Enum.IsDefined(typeof(MyAnimeListStatus), status))
|
||||||
|
{
|
||||||
|
context.AddFailure($"Invalid status: {status}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MyAnimeListSettings : IImportListSettings
|
||||||
|
{
|
||||||
|
public string BaseUrl { get; set; }
|
||||||
|
|
||||||
|
protected AbstractValidator<MyAnimeListSettings> Validator => new MalSettingsValidator();
|
||||||
|
|
||||||
|
public MyAnimeListSettings()
|
||||||
|
{
|
||||||
|
BaseUrl = "https://api.myanimelist.net/v2";
|
||||||
|
}
|
||||||
|
|
||||||
|
[FieldDefinition(0, Label = "ImportListsMyAnimeListSettingsListStatus", Type = FieldType.Select, SelectOptions = typeof(MyAnimeListStatus), HelpText = "ImportListsMyAnimeListSettingsListStatusHelpText")]
|
||||||
|
public int ListStatus { get; set; }
|
||||||
|
|
||||||
|
[FieldDefinition(0, Label = "ImportListsSettingsAccessToken", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)]
|
||||||
|
public string AccessToken { get; set; }
|
||||||
|
|
||||||
|
[FieldDefinition(0, Label = "ImportListsSettingsRefreshToken", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)]
|
||||||
|
public string RefreshToken { get; set; }
|
||||||
|
|
||||||
|
[FieldDefinition(0, Label = "ImportListsSettingsExpires", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)]
|
||||||
|
public DateTime Expires { get; set; }
|
||||||
|
|
||||||
|
[FieldDefinition(99, Label = "ImportListsMyAnimeListSettingsAuthenticateWithMyAnimeList", Type = FieldType.OAuth)]
|
||||||
|
public string SignIn { get; set; }
|
||||||
|
|
||||||
|
public NzbDroneValidationResult Validate()
|
||||||
|
{
|
||||||
|
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
using NzbDrone.Core.Annotations;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.ImportLists.MyAnimeList
|
||||||
|
{
|
||||||
|
public enum MyAnimeListStatus
|
||||||
|
{
|
||||||
|
[FieldOption(label: "All")]
|
||||||
|
All = 0,
|
||||||
|
|
||||||
|
[FieldOption(label: "Watching")]
|
||||||
|
Watching = 1,
|
||||||
|
|
||||||
|
[FieldOption(label: "Completed")]
|
||||||
|
Completed = 2,
|
||||||
|
|
||||||
|
[FieldOption(label: "On Hold")]
|
||||||
|
OnHold = 3,
|
||||||
|
|
||||||
|
[FieldOption(label: "Dropped")]
|
||||||
|
Dropped = 4,
|
||||||
|
|
||||||
|
[FieldOption(label: "Plan to Watch")]
|
||||||
|
PlanToWatch = 5
|
||||||
|
}
|
||||||
|
}
|
|
@ -838,6 +838,9 @@
|
||||||
"ImportListsImdbSettingsListId": "List ID",
|
"ImportListsImdbSettingsListId": "List ID",
|
||||||
"ImportListsImdbSettingsListIdHelpText": "IMDb list ID (e.g ls12345678)",
|
"ImportListsImdbSettingsListIdHelpText": "IMDb list ID (e.g ls12345678)",
|
||||||
"ImportListsLoadError": "Unable to load Import Lists",
|
"ImportListsLoadError": "Unable to load Import Lists",
|
||||||
|
"ImportListsMyAnimeListSettingsAuthenticateWithMyAnimeList": "Authenticate with MyAnimeList",
|
||||||
|
"ImportListsMyAnimeListSettingsListStatus": "List Status",
|
||||||
|
"ImportListsMyAnimeListSettingsListStatusHelpText": "Type of list you want to import from, set to 'All' for all lists",
|
||||||
"ImportListsPlexSettingsAuthenticateWithPlex": "Authenticate with Plex.tv",
|
"ImportListsPlexSettingsAuthenticateWithPlex": "Authenticate with Plex.tv",
|
||||||
"ImportListsPlexSettingsWatchlistName": "Plex Watchlist",
|
"ImportListsPlexSettingsWatchlistName": "Plex Watchlist",
|
||||||
"ImportListsPlexSettingsWatchlistRSSName": "Plex Watchlist RSS",
|
"ImportListsPlexSettingsWatchlistRSSName": "Plex Watchlist RSS",
|
||||||
|
|
|
@ -9,5 +9,6 @@ namespace NzbDrone.Core.MetadataSource
|
||||||
List<Series> SearchForNewSeriesByImdbId(string imdbId);
|
List<Series> SearchForNewSeriesByImdbId(string imdbId);
|
||||||
List<Series> SearchForNewSeriesByAniListId(int aniListId);
|
List<Series> SearchForNewSeriesByAniListId(int aniListId);
|
||||||
List<Series> SearchForNewSeriesByTmdbId(int tmdbId);
|
List<Series> SearchForNewSeriesByTmdbId(int tmdbId);
|
||||||
|
List<Series> SearchForNewSeriesByMyAnimeListId(int malId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,6 +90,13 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<Series> SearchForNewSeriesByMyAnimeListId(int malId)
|
||||||
|
{
|
||||||
|
var results = SearchForNewSeries($"mal:{malId}");
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
public List<Series> SearchForNewSeriesByTmdbId(int tmdbId)
|
public List<Series> SearchForNewSeriesByTmdbId(int tmdbId)
|
||||||
{
|
{
|
||||||
var results = SearchForNewSeries($"tmdb:{tmdbId}");
|
var results = SearchForNewSeries($"tmdb:{tmdbId}");
|
||||||
|
|
Loading…
Reference in New Issue