parent
d338425951
commit
1335efd487
@ -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
|
||||
}
|
||||
}
|
Loading…
Reference in new issue