From 9ed2b4e10b3a78752c88250f3e03247899a83d66 Mon Sep 17 00:00:00 2001 From: Qstick Date: Wed, 5 Aug 2020 16:13:35 -0400 Subject: [PATCH] New: Trakt.tv List Options --- .../ImportLists/Trakt/List/TraktListImport.cs | 31 ++++ .../Trakt/List/TraktListRequestGenerator.cs | 46 ++++++ .../Trakt/List/TraktListSettings.cs | 28 ++++ .../Trakt/Popular/TraktPopularImport.cs | 36 +++++ .../Trakt/Popular/TraktPopularListType.cs | 23 +++ .../Trakt/Popular/TraktPopularParser.cs | 58 +++++++ .../Popular/TraktPopularRequestGenerator.cs | 70 ++++++++ .../Trakt/Popular/TraktPopularSettings.cs | 27 ++++ .../ImportLists/Trakt/TraktAPI.cs | 51 ++++++ .../ImportLists/Trakt/TraktImportBase.cs | 149 ++++++++++++++++++ .../ImportLists/Trakt/TraktParser.cs | 65 ++++++++ .../ImportLists/Trakt/TraktSettingsBase.cs | 92 +++++++++++ .../ImportLists/Trakt/User/TraktUserImport.cs | 31 ++++ .../Trakt/User/TraktUserListType.cs | 14 ++ .../Trakt/User/TraktUserRequestGenerator.cs | 56 +++++++ .../Trakt/User/TraktUserSettings.cs | 28 ++++ 16 files changed, 805 insertions(+) create mode 100644 src/NzbDrone.Core/ImportLists/Trakt/List/TraktListImport.cs create mode 100644 src/NzbDrone.Core/ImportLists/Trakt/List/TraktListRequestGenerator.cs create mode 100644 src/NzbDrone.Core/ImportLists/Trakt/List/TraktListSettings.cs create mode 100644 src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularImport.cs create mode 100644 src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularListType.cs create mode 100644 src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularParser.cs create mode 100644 src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularRequestGenerator.cs create mode 100644 src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularSettings.cs create mode 100644 src/NzbDrone.Core/ImportLists/Trakt/TraktAPI.cs create mode 100644 src/NzbDrone.Core/ImportLists/Trakt/TraktImportBase.cs create mode 100644 src/NzbDrone.Core/ImportLists/Trakt/TraktParser.cs create mode 100644 src/NzbDrone.Core/ImportLists/Trakt/TraktSettingsBase.cs create mode 100644 src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserImport.cs create mode 100644 src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserListType.cs create mode 100644 src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserRequestGenerator.cs create mode 100644 src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserSettings.cs diff --git a/src/NzbDrone.Core/ImportLists/Trakt/List/TraktListImport.cs b/src/NzbDrone.Core/ImportLists/Trakt/List/TraktListImport.cs new file mode 100644 index 000000000..3d67b7ead --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/Trakt/List/TraktListImport.cs @@ -0,0 +1,31 @@ +using NLog; +using NzbDrone.Common.Http; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Parser; + +namespace NzbDrone.Core.ImportLists.Trakt.List +{ + public class TraktListImport : TraktImportBase + { + public TraktListImport(IImportListRepository netImportRepository, + IHttpClient httpClient, + IImportListStatusService netImportStatusService, + IConfigService configService, + IParsingService parsingService, + Logger logger) + : base(netImportRepository, httpClient, netImportStatusService, configService, parsingService, logger) + { + } + + public override string Name => "Trakt List"; + + public override IImportListRequestGenerator GetRequestGenerator() + { + return new TraktListRequestGenerator() + { + Settings = Settings, + ClientId = ClientId + }; + } + } +} diff --git a/src/NzbDrone.Core/ImportLists/Trakt/List/TraktListRequestGenerator.cs b/src/NzbDrone.Core/ImportLists/Trakt/List/TraktListRequestGenerator.cs new file mode 100644 index 000000000..d268c25c0 --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/Trakt/List/TraktListRequestGenerator.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; + +namespace NzbDrone.Core.ImportLists.Trakt.List +{ + public class TraktListRequestGenerator : IImportListRequestGenerator + { + public TraktListSettings Settings { get; set; } + public string ClientId { get; set; } + + public TraktListRequestGenerator() + { + } + + public virtual ImportListPageableRequestChain GetListItems() + { + var pageableRequests = new ImportListPageableRequestChain(); + + pageableRequests.Add(GetSeriesRequest()); + + return pageableRequests; + } + + private IEnumerable GetSeriesRequest() + { + var link = Settings.BaseUrl.Trim(); + + var listName = Settings.Listname.Trim(); + + link += $"/users/{Settings.Username.Trim()}/lists/{listName}/items/shows?limit={Settings.Limit}"; + + var request = new ImportListRequest($"{link}", HttpAccept.Json); + + request.HttpRequest.Headers.Add("trakt-api-version", "2"); + request.HttpRequest.Headers.Add("trakt-api-key", ClientId); + + if (Settings.AccessToken.IsNotNullOrWhiteSpace()) + { + request.HttpRequest.Headers.Add("Authorization", "Bearer " + Settings.AccessToken); + } + + yield return request; + } + } +} diff --git a/src/NzbDrone.Core/ImportLists/Trakt/List/TraktListSettings.cs b/src/NzbDrone.Core/ImportLists/Trakt/List/TraktListSettings.cs new file mode 100644 index 000000000..194c27a79 --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/Trakt/List/TraktListSettings.cs @@ -0,0 +1,28 @@ +using FluentValidation; +using NzbDrone.Core.Annotations; + +namespace NzbDrone.Core.ImportLists.Trakt.List +{ + public class TraktListSettingsValidator : TraktSettingsBaseValidator + { + public TraktListSettingsValidator() + : base() + { + } + } + + public class TraktListSettings : TraktSettingsBase + { + protected override AbstractValidator Validator => new TraktListSettingsValidator(); + + public TraktListSettings() + { + } + + [FieldDefinition(1, Label = "Username", HelpText = "Username for the List to import from")] + public string Username { get; set; } + + [FieldDefinition(2, Label = "List Name", HelpText = "List name for import, list must be public or you must have access to the list")] + public string Listname { get; set; } + } +} diff --git a/src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularImport.cs b/src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularImport.cs new file mode 100644 index 000000000..b31cc8322 --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularImport.cs @@ -0,0 +1,36 @@ +using NLog; +using NzbDrone.Common.Http; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Parser; + +namespace NzbDrone.Core.ImportLists.Trakt.Popular +{ + public class TraktPopularImport : TraktImportBase + { + public TraktPopularImport(IImportListRepository netImportRepository, + IHttpClient httpClient, + IImportListStatusService netImportStatusService, + IConfigService configService, + IParsingService parsingService, + Logger logger) + : base(netImportRepository, httpClient, netImportStatusService, configService, parsingService, logger) + { + } + + public override string Name => "Trakt Popular List"; + + public override IParseImportListResponse GetParser() + { + return new TraktPopularParser(Settings); + } + + public override IImportListRequestGenerator GetRequestGenerator() + { + return new TraktPopularRequestGenerator() + { + Settings = Settings, + ClientId = ClientId + }; + } + } +} diff --git a/src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularListType.cs b/src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularListType.cs new file mode 100644 index 000000000..99ad454c4 --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularListType.cs @@ -0,0 +1,23 @@ +using System.Runtime.Serialization; + +namespace NzbDrone.Core.ImportLists.Trakt.Popular +{ + public enum TraktPopularListType + { + [EnumMember(Value = "Trending Shows")] + Trending = 0, + [EnumMember(Value = "Popular Shows")] + Popular = 1, + [EnumMember(Value = "Anticipated Shows")] + Anticipated = 2, + + [EnumMember(Value = "Top Watched Shows By Week")] + TopWatchedByWeek = 3, + [EnumMember(Value = "Top Watched Shows By Month")] + TopWatchedByMonth = 4, + [EnumMember(Value = "Top Watched Shows By Year")] + TopWatchedByYear = 5, + [EnumMember(Value = "Top Watched Shows Of All Time")] + TopWatchedByAllTime = 6 + } +} diff --git a/src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularParser.cs b/src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularParser.cs new file mode 100644 index 000000000..e25d39b53 --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularParser.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using Newtonsoft.Json; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.ImportLists.Trakt.Popular +{ + public class TraktPopularParser : TraktParser + { + private readonly TraktPopularSettings _settings; + private ImportListResponse _importResponse; + + public TraktPopularParser(TraktPopularSettings settings) + { + _settings = settings; + } + + public override IList ParseResponse(ImportListResponse importResponse) + { + _importResponse = importResponse; + + var listItems = new List(); + + if (!PreProcess(_importResponse)) + { + return listItems; + } + + var jsonResponse = new List(); + + if (_settings.TraktListType == (int)TraktPopularListType.Popular) + { + jsonResponse = JsonConvert.DeserializeObject>(_importResponse.Content); + } + else + { + jsonResponse = JsonConvert.DeserializeObject>(_importResponse.Content).SelectList(c => c.Show); + } + + // no movies were return + if (jsonResponse == null) + { + return listItems; + } + + foreach (var series in jsonResponse) + { + listItems.AddIfNotNull(new ImportListItemInfo() + { + Title = series.Title, + TvdbId = series.Ids.Tvdb.GetValueOrDefault(), + }); + } + + return listItems; + } + } +} diff --git a/src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularRequestGenerator.cs b/src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularRequestGenerator.cs new file mode 100644 index 000000000..439e4a984 --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularRequestGenerator.cs @@ -0,0 +1,70 @@ +using System.Collections.Generic; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; + +namespace NzbDrone.Core.ImportLists.Trakt.Popular +{ + public class TraktPopularRequestGenerator : IImportListRequestGenerator + { + public TraktPopularSettings Settings { get; set; } + + public string ClientId { get; set; } + + public TraktPopularRequestGenerator() + { + } + + public virtual ImportListPageableRequestChain GetListItems() + { + var pageableRequests = new ImportListPageableRequestChain(); + + pageableRequests.Add(GetSeriesRequest()); + + return pageableRequests; + } + + private IEnumerable GetSeriesRequest() + { + var link = Settings.BaseUrl.Trim(); + + var filtersAndLimit = $"?years={Settings.Years}&genres={Settings.Genres.ToLower()}&ratings={Settings.Rating}&limit={Settings.Limit}{Settings.TraktAdditionalParameters}"; + + switch (Settings.TraktListType) + { + case (int)TraktPopularListType.Trending: + link += "/shows/trending" + filtersAndLimit; + break; + case (int)TraktPopularListType.Popular: + link += "/shows/popular" + filtersAndLimit; + break; + case (int)TraktPopularListType.Anticipated: + link += "/shows/anticipated" + filtersAndLimit; + break; + case (int)TraktPopularListType.TopWatchedByWeek: + link += "/shows/watched/weekly" + filtersAndLimit; + break; + case (int)TraktPopularListType.TopWatchedByMonth: + link += "/shows/watched/monthly" + filtersAndLimit; + break; + case (int)TraktPopularListType.TopWatchedByYear: + link += "/shows/watched/yearly" + filtersAndLimit; + break; + case (int)TraktPopularListType.TopWatchedByAllTime: + link += "/shows/watched/all" + filtersAndLimit; + break; + } + + var request = new ImportListRequest($"{link}", HttpAccept.Json); + + request.HttpRequest.Headers.Add("trakt-api-version", "2"); + request.HttpRequest.Headers.Add("trakt-api-key", ClientId); + + if (Settings.AccessToken.IsNotNullOrWhiteSpace()) + { + request.HttpRequest.Headers.Add("Authorization", "Bearer " + Settings.AccessToken); + } + + yield return request; + } + } +} diff --git a/src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularSettings.cs b/src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularSettings.cs new file mode 100644 index 000000000..60c0fa8e7 --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularSettings.cs @@ -0,0 +1,27 @@ +using FluentValidation; +using NzbDrone.Core.Annotations; + +namespace NzbDrone.Core.ImportLists.Trakt.Popular +{ + public class TraktPopularSettingsValidator : TraktSettingsBaseValidator + { + public TraktPopularSettingsValidator() + : base() + { + RuleFor(c => c.TraktListType).NotNull(); + } + } + + public class TraktPopularSettings : TraktSettingsBase + { + protected override AbstractValidator Validator => new TraktPopularSettingsValidator(); + + public TraktPopularSettings() + { + TraktListType = (int)TraktPopularListType.Popular; + } + + [FieldDefinition(1, Label = "List Type", Type = FieldType.Select, SelectOptions = typeof(TraktPopularListType), HelpText = "Type of list your seeking to import from")] + public int TraktListType { get; set; } + } +} diff --git a/src/NzbDrone.Core/ImportLists/Trakt/TraktAPI.cs b/src/NzbDrone.Core/ImportLists/Trakt/TraktAPI.cs new file mode 100644 index 000000000..dcaf98f20 --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/Trakt/TraktAPI.cs @@ -0,0 +1,51 @@ +using Newtonsoft.Json; + +namespace NzbDrone.Core.ImportLists.Trakt +{ + public class TraktSeriesIdsResource + { + public int Trakt { get; set; } + public string Slug { get; set; } + public string Imdb { get; set; } + public int? Tmdb { get; set; } + public int? Tvdb { get; set; } + } + + public class TraktSeriesResource + { + public string Title { get; set; } + public int? Year { get; set; } + public TraktSeriesIdsResource Ids { get; set; } + } + + public class TraktResponse + { + public TraktSeriesResource Show { get; set; } + } + + public class RefreshRequestResponse + { + [JsonProperty("access_token")] + public string AccessToken { get; set; } + [JsonProperty("expires_in")] + public int ExpiresIn { get; set; } + [JsonProperty("refresh_token")] + public string RefreshToken { get; set; } + } + + public class UserSettingsResponse + { + public TraktUserResource User { get; set; } + } + + public class TraktUserResource + { + public string Username { get; set; } + public TraktUserIdsResource Ids { get; set; } + } + + public class TraktUserIdsResource + { + public string Slug { get; set; } + } +} diff --git a/src/NzbDrone.Core/ImportLists/Trakt/TraktImportBase.cs b/src/NzbDrone.Core/ImportLists/Trakt/TraktImportBase.cs new file mode 100644 index 000000000..3ae3f6593 --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/Trakt/TraktImportBase.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.ImportLists.Trakt +{ + public abstract class TraktImportBase : HttpImportListBase + where TSettings : TraktSettingsBase, new() + { + public override ImportListType ListType => ImportListType.Trakt; + + public const string OAuthUrl = "https://api.trakt.tv/oauth/authorize"; + public const string RedirectUri = "https://auth.servarr.com/v1/trakt_sonarr/auth"; + public const string RenewUri = "https://auth.servarr.com/v1/trakt_sonarr/renew"; + public const string ClientId = "d44ba57cab40c31eb3f797dcfccd203500796539125b333883ec1d94aa62ed4c"; + + + private IImportListRepository _importListRepository; + + protected TraktImportBase(IImportListRepository netImportRepository, + IHttpClient httpClient, + IImportListStatusService importListStatusService, + IConfigService configService, + IParsingService parsingService, + Logger logger) + : base(httpClient, importListStatusService, configService, parsingService, logger) + { + _importListRepository = netImportRepository; + } + + public override IList Fetch() + { + Settings.Validate().Filter("AccessToken", "RefreshToken").ThrowOnError(); + _logger.Trace($"Access token expires at {Settings.Expires}"); + + if (Settings.Expires < DateTime.UtcNow.AddMinutes(5)) + { + RefreshToken(); + } + + var generator = GetRequestGenerator(); + return FetchItems(g => g.GetListItems(), true); + } + + public override IParseImportListResponse GetParser() + { + return new TraktParser(); + } + + public override object RequestAction(string action, IDictionary query) + { + if (action == "startOAuth") + { + var request = new HttpRequestBuilder(OAuthUrl) + .AddQueryParam("client_id", ClientId) + .AddQueryParam("response_type", "code") + .AddQueryParam("redirect_uri", RedirectUri) + .AddQueryParam("state", query["callbackUrl"]) + .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"], + authUser = GetUserName(query["access_token"]) + }; + } + + return new { }; + } + + private string GetUserName(string accessToken) + { + var request = new HttpRequestBuilder(string.Format("{0}/users/settings", Settings.BaseUrl)) + .Build(); + + request.Headers.Add("trakt-api-version", "2"); + request.Headers.Add("trakt-api-key", ClientId); + + if (accessToken.IsNotNullOrWhiteSpace()) + { + request.Headers.Add("Authorization", "Bearer " + accessToken); + } + + try + { + var response = _httpClient.Get(request); + + if (response != null && response.Resource != null) + { + return response.Resource.User.Ids.Slug; + } + } + catch (HttpException) + { + _logger.Warn($"Error refreshing trakt access token"); + } + + return null; + } + + private void RefreshToken() + { + _logger.Trace("Refreshing Token"); + + Settings.Validate().Filter("RefreshToken").ThrowOnError(); + + var request = new HttpRequestBuilder(RenewUri) + .AddQueryParam("refresh_token", Settings.RefreshToken) + .Build(); + + try + { + var response = _httpClient.Get(request); + + if (response != null && response.Resource != null) + { + var token = response.Resource; + Settings.AccessToken = token.AccessToken; + Settings.Expires = DateTime.UtcNow.AddSeconds(token.ExpiresIn); + Settings.RefreshToken = token.RefreshToken != null ? token.RefreshToken : Settings.RefreshToken; + + if (Definition.Id > 0) + { + _importListRepository.UpdateSettings((ImportListDefinition)Definition); + } + } + } + catch (HttpException) + { + _logger.Warn($"Error refreshing trakt access token"); + } + } + } +} diff --git a/src/NzbDrone.Core/ImportLists/Trakt/TraktParser.cs b/src/NzbDrone.Core/ImportLists/Trakt/TraktParser.cs new file mode 100644 index 000000000..9a51ae377 --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/Trakt/TraktParser.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; +using System.Net; +using Newtonsoft.Json; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.ImportLists.Exceptions; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.ImportLists.Trakt +{ + public class TraktParser : IParseImportListResponse + { + private ImportListResponse _importResponse; + + public TraktParser() + { + } + + public virtual IList ParseResponse(ImportListResponse importResponse) + { + _importResponse = importResponse; + + var series = new List(); + + if (!PreProcess(_importResponse)) + { + return series; + } + + var jsonResponse = JsonConvert.DeserializeObject>(_importResponse.Content); + + // no movies were return + if (jsonResponse == null) + { + return series; + } + + foreach (var movie in jsonResponse) + { + series.AddIfNotNull(new ImportListItemInfo() + { + Title = movie.Show.Title, + TvdbId = movie.Show.Ids.Tvdb.GetValueOrDefault() + }); + } + + return series; + } + + protected virtual bool PreProcess(ImportListResponse netImportResponse) + { + if (netImportResponse.HttpResponse.StatusCode != HttpStatusCode.OK) + { + throw new ImportListException(netImportResponse, "Trakt API call resulted in an unexpected StatusCode [{0}]", netImportResponse.HttpResponse.StatusCode); + } + + if (netImportResponse.HttpResponse.Headers.ContentType != null && netImportResponse.HttpResponse.Headers.ContentType.Contains("text/json") && + netImportResponse.HttpRequest.Headers.Accept != null && !netImportResponse.HttpRequest.Headers.Accept.Contains("text/json")) + { + throw new ImportListException(netImportResponse, "Trakt API responded with html content. Site is likely blocked or unavailable."); + } + + return true; + } + } +} diff --git a/src/NzbDrone.Core/ImportLists/Trakt/TraktSettingsBase.cs b/src/NzbDrone.Core/ImportLists/Trakt/TraktSettingsBase.cs new file mode 100644 index 000000000..eea675a97 --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/Trakt/TraktSettingsBase.cs @@ -0,0 +1,92 @@ +using System; +using System.Text.RegularExpressions; +using FluentValidation; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.ImportLists.Trakt +{ + public class TraktSettingsBaseValidator : AbstractValidator + where TSettings : TraktSettingsBase + { + public TraktSettingsBaseValidator() + { + RuleFor(c => c.BaseUrl).ValidRootUrl(); + RuleFor(c => c.AccessToken).NotEmpty(); + RuleFor(c => c.RefreshToken).NotEmpty(); + RuleFor(c => c.Expires).NotEmpty(); + + // Loose validation @TODO + RuleFor(c => c.Rating) + .Matches(@"^\d+\-\d+$", RegexOptions.IgnoreCase) + .When(c => c.Rating.IsNotNullOrWhiteSpace()) + .WithMessage("Not a valid rating"); + + // Loose validation @TODO + RuleFor(c => c.Years) + .Matches(@"^\d+(\-\d+)?$", RegexOptions.IgnoreCase) + .When(c => c.Years.IsNotNullOrWhiteSpace()) + .WithMessage("Not a valid year or range of years"); + + // Limit not smaller than 1 and not larger than 100 + RuleFor(c => c.Limit) + .GreaterThan(0) + .WithMessage("Must be integer greater than 0"); + } + } + + public class TraktSettingsBase : IImportListSettings + where TSettings : TraktSettingsBase + { + protected virtual AbstractValidator Validator => new TraktSettingsBaseValidator(); + + public TraktSettingsBase() + { + BaseUrl = "https://api.trakt.tv"; + SignIn = "startOAuth"; + Rating = "0-100"; + Genres = ""; + Years = ""; + Limit = 100; + } + + public string BaseUrl { get; set; } + + [FieldDefinition(0, Label = "Access Token", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] + public string AccessToken { get; set; } + + [FieldDefinition(0, Label = "Refresh Token", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] + public string RefreshToken { get; set; } + + [FieldDefinition(0, Label = "Expires", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] + public DateTime Expires { get; set; } + + [FieldDefinition(0, Label = "Auth User", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] + public string AuthUser { get; set; } + + [FieldDefinition(1, Label = "Rating", HelpText = "Filter series by rating range (0-100)")] + public string Rating { get; set; } + + [FieldDefinition(3, Label = "Genres", HelpText = "Filter series by Trakt Genre Slug (Comma Separated)")] + public string Genres { get; set; } + + [FieldDefinition(4, Label = "Years", HelpText = "Filter series by year or year range")] + public string Years { get; set; } + + [FieldDefinition(5, Label = "Limit", HelpText = "Limit the number of series to get")] + public int Limit { get; set; } + + [FieldDefinition(6, Label = "Additional Parameters", HelpText = "Additional Trakt API parameters", Advanced = true)] + public string TraktAdditionalParameters { get; set; } + + [FieldDefinition(99, Label = "Authenticate with Trakt", Type = FieldType.OAuth)] + public string SignIn { get; set; } + + public NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate((TSettings)this)); + } + } +} diff --git a/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserImport.cs b/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserImport.cs new file mode 100644 index 000000000..03fde5d08 --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserImport.cs @@ -0,0 +1,31 @@ +using NLog; +using NzbDrone.Common.Http; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Parser; + +namespace NzbDrone.Core.ImportLists.Trakt.User +{ + public class TraktUserImport : TraktImportBase + { + public TraktUserImport(IImportListRepository netImportRepository, + IHttpClient httpClient, + IImportListStatusService netImportStatusService, + IConfigService configService, + IParsingService parsingService, + Logger logger) + : base(netImportRepository, httpClient, netImportStatusService, configService, parsingService, logger) + { + } + + public override string Name => "Trakt User"; + + public override IImportListRequestGenerator GetRequestGenerator() + { + return new TraktUserRequestGenerator() + { + Settings = Settings, + ClientId = ClientId + }; + } + } +} diff --git a/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserListType.cs b/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserListType.cs new file mode 100644 index 000000000..b90508d14 --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserListType.cs @@ -0,0 +1,14 @@ +using System.Runtime.Serialization; + +namespace NzbDrone.Core.ImportLists.Trakt.User +{ + public enum TraktUserListType + { + [EnumMember(Value = "User Watch List")] + UserWatchList = 0, + [EnumMember(Value = "User Watched List")] + UserWatchedList = 1, + [EnumMember(Value = "User Collection List")] + UserCollectionList = 2 + } +} diff --git a/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserRequestGenerator.cs b/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserRequestGenerator.cs new file mode 100644 index 000000000..6602c950e --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserRequestGenerator.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; + +namespace NzbDrone.Core.ImportLists.Trakt.User +{ + public class TraktUserRequestGenerator : IImportListRequestGenerator + { + public TraktUserSettings Settings { get; set; } + + public string ClientId { get; set; } + + public TraktUserRequestGenerator() + { + } + + public virtual ImportListPageableRequestChain GetListItems() + { + var pageableRequests = new ImportListPageableRequestChain(); + + pageableRequests.Add(GetSeriesRequest()); + + return pageableRequests; + } + + private IEnumerable GetSeriesRequest() + { + var link = Settings.BaseUrl.Trim(); + + switch (Settings.TraktListType) + { + case (int)TraktUserListType.UserWatchList: + link += $"/users/{Settings.AuthUser.Trim()}/watchlist/shows?limit={Settings.Limit}"; + break; + case (int)TraktUserListType.UserWatchedList: + link += $"/users/{Settings.AuthUser.Trim()}/watched/shows?limit={Settings.Limit}"; + break; + case (int)TraktUserListType.UserCollectionList: + link += $"/users/{Settings.AuthUser.Trim()}/collection/shows?limit={Settings.Limit}"; + break; + } + + var request = new ImportListRequest($"{link}", HttpAccept.Json); + + request.HttpRequest.Headers.Add("trakt-api-version", "2"); + request.HttpRequest.Headers.Add("trakt-api-key", ClientId); + + if (Settings.AccessToken.IsNotNullOrWhiteSpace()) + { + request.HttpRequest.Headers.Add("Authorization", "Bearer " + Settings.AccessToken); + } + + yield return request; + } + } +} diff --git a/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserSettings.cs b/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserSettings.cs new file mode 100644 index 000000000..da8a74557 --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserSettings.cs @@ -0,0 +1,28 @@ +using FluentValidation; +using NzbDrone.Core.Annotations; + +namespace NzbDrone.Core.ImportLists.Trakt.User +{ + public class TraktUserSettingsValidator : TraktSettingsBaseValidator + { + public TraktUserSettingsValidator() + : base() + { + RuleFor(c => c.TraktListType).NotNull(); + RuleFor(c => c.AuthUser).NotEmpty(); + } + } + + public class TraktUserSettings : TraktSettingsBase + { + protected override AbstractValidator Validator => new TraktUserSettingsValidator(); + + public TraktUserSettings() + { + TraktListType = (int)TraktUserListType.UserWatchList; + } + + [FieldDefinition(1, Label = "List Type", Type = FieldType.Select, SelectOptions = typeof(TraktUserListType), HelpText = "Type of list your seeking to import from")] + public int TraktListType { get; set; } + } +}