diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/170_fix_trakt_list_configFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/170_fix_trakt_list_configFixture.cs new file mode 100644 index 0000000000..74f58043e3 --- /dev/null +++ b/src/NzbDrone.Core.Test/Datastore/Migration/170_fix_trakt_list_configFixture.cs @@ -0,0 +1,185 @@ +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Newtonsoft.Json.Linq; +using NUnit.Framework; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Datastore.Migration; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.Datastore.Migration +{ + [TestFixture] + public class fix_trakt_list_configFixture : MigrationTest + { + [Test] + public void should_change_implementation_contract_on_radarr_lists() + { + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("NetImport").Row(new + { + Enabled = 1, + EnableAuto = 1, + RootFolderPath = "D:\\Movies", + ProfileId = 1, + MinimumAvailability = 1, + ShouldMonitor = 1, + Name = "IMDB List", + Implementation = "RadarrLists", + Settings = new RadarrListSettings169 + { + APIURL = "https://api.radarr.video/v2", + Path = "/imdb/list?listId=ls000199717", + }.ToJson(), + ConfigContract = "RadarrSettings" + }); + }); + + var items = db.Query("SELECT * FROM NetImport"); + + items.Should().HaveCount(1); + items.First().Implementation.Should().Be("RadarrListImport"); + items.First().ConfigContract.Should().Be("RadarrListSettings"); + items.First().Settings.Count.Should().Be(2); + items.First().Settings.First.Should().NotBeEmpty(); + } + + [Test] + public void should_change_implementation_contract_type_on_trakt_user() + { + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("NetImport").Row(new + { + Enabled = 1, + EnableAuto = 1, + RootFolderPath = "D:\\Movies", + ProfileId = 1, + MinimumAvailability = 1, + ShouldMonitor = 1, + Name = "TraktImport", + Implementation = "TraktImport", + Settings = new TraktSettings169 + { + AccessToken = "123456798", + RefreshToken = "987654321", + Rating = "0-100", + TraktListType = (int)TraktListType169.UserWatchList, + Username = "someuser", + }.ToJson(), + ConfigContract = "TraktSettings" + }); + }); + + var items = db.Query("SELECT * FROM NetImport"); + + items.Should().HaveCount(1); + items.First().Implementation.Should().Be("TraktUserImport"); + items.First().ConfigContract.Should().Be("TraktUserSettings"); + + var firstSettings = items.First().Settings.ToObject(); + firstSettings.AccessToken.Should().NotBeEmpty(); + firstSettings.TraktListType.Should().Be((int)TraktUserListType170.UserWatchList); + } + + [Test] + public void should_change_implementation_contract_type_on_trakt_popular() + { + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("NetImport").Row(new + { + Enabled = 1, + EnableAuto = 1, + RootFolderPath = "D:\\Movies", + ProfileId = 1, + MinimumAvailability = 1, + ShouldMonitor = 1, + Name = "TraktImport", + Implementation = "TraktImport", + Settings = new TraktSettings169 + { + AccessToken = "123456798", + RefreshToken = "987654321", + Rating = "0-100", + TraktListType = (int)TraktListType169.Popular, + Username = "someuser", + }.ToJson(), + ConfigContract = "TraktSettings" + }); + }); + + var items = db.Query("SELECT * FROM NetImport"); + + items.Should().HaveCount(1); + items.First().Implementation.Should().Be("TraktPopularImport"); + items.First().ConfigContract.Should().Be("TraktPopularSettings"); + + var firstSettings = items.First().Settings.ToObject(); + firstSettings.AccessToken.Should().NotBeEmpty(); + firstSettings.TraktListType.Should().Be((int)TraktPopularListType170.Popular); + } + + [Test] + public void should_change_implementation_contract_type_on_trakt_list() + { + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("NetImport").Row(new + { + Enabled = 1, + EnableAuto = 1, + RootFolderPath = "D:\\Movies", + ProfileId = 1, + MinimumAvailability = 1, + ShouldMonitor = 1, + Name = "TraktImport", + Implementation = "TraktImport", + Settings = new TraktSettings169 + { + AccessToken = "123456798", + RefreshToken = "987654321", + Rating = "0-100", + TraktListType = (int)TraktListType169.UserCustomList, + Username = "someuser", + Listname = "mylist" + }.ToJson(), + ConfigContract = "TraktSettings" + }); + }); + + var items = db.Query("SELECT * FROM NetImport"); + + items.Should().HaveCount(1); + items.First().Implementation.Should().Be("TraktListImport"); + items.First().ConfigContract.Should().Be("TraktListSettings"); + + var firstSettings = items.First().Settings.ToObject(); + firstSettings.AccessToken.Should().NotBeEmpty(); + firstSettings.Listname.Should().Be("mylist"); + } + } + + public class ListDefinition169 + { + public int Id { get; set; } + public bool Enabled { get; set; } + public bool EnableAuto { get; set; } + public bool ShouldMonitor { get; set; } + public string Name { get; set; } + public string Implementation { get; set; } + public JObject Settings { get; set; } + public string ConfigContract { get; set; } + public string RootFolderPath { get; set; } + public int ProfileId { get; set; } + public int MinimumAvailability { get; set; } + public List Tags { get; set; } + } + + public class RadarrListSettings169 + { + public string APIURL { get; set; } + public string Path { get; set; } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/170_fix_trakt_list_config.cs b/src/NzbDrone.Core/Datastore/Migration/170_fix_trakt_list_config.cs new file mode 100644 index 0000000000..afc0c00497 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/170_fix_trakt_list_config.cs @@ -0,0 +1,308 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Text.Json; +using Dapper; +using FluentMigrator; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(170)] + public class fix_trakt_list_config : NzbDroneMigrationBase + { + private readonly JsonSerializerOptions _serializerSettings; + + public fix_trakt_list_config() + { + _serializerSettings = new JsonSerializerOptions + { + AllowTrailingCommas = true, + IgnoreNullValues = false, + PropertyNameCaseInsensitive = true, + DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = true + }; + } + + protected override void MainDbUpgrade() + { + Execute.WithConnection(FixTraktConfig); + Execute.WithConnection(RenameRadarrListType); + Execute.Sql("DELETE FROM Config WHERE[KEY] IN ('TraktAuthToken', 'TraktRefreshToken', 'TraktTokenExpiry', 'NewTraktAuthToken', 'NewTraktRefreshToken', 'NewTraktTokenExpiry')"); + } + + private void RenameRadarrListType(IDbConnection conn, IDbTransaction tran) + { + var rows = conn.Query($"SELECT Id, Implementation, ConfigContract, Settings FROM NetImport WHERE Implementation = 'RadarrLists'"); + + var corrected = new List(); + + foreach (var row in rows) + { + corrected.Add(new ProviderDefinition169 + { + Id = row.Id, + Implementation = "RadarrListImport", + ConfigContract = "RadarrListSettings" + }); + } + + var updateSql = "UPDATE NetImport SET Implementation = @Implementation, ConfigContract = @ConfigContract WHERE Id = @Id"; + conn.Execute(updateSql, corrected, transaction: tran); + } + + private void FixTraktConfig(IDbConnection conn, IDbTransaction tran) + { + var config = new Dictionary(); + + using (IDbCommand configCmd = conn.CreateCommand()) + { + configCmd.Transaction = tran; + configCmd.CommandText = @"SELECT * FROM Config"; + using (IDataReader configReader = configCmd.ExecuteReader()) + { + var keyIndex = configReader.GetOrdinal("Key"); + var valueIndex = configReader.GetOrdinal("Value"); + + while (configReader.Read()) + { + var key = configReader.GetString(keyIndex); + var value = configReader.GetString(valueIndex); + + config.Add(key.ToLowerInvariant(), value); + } + } + } + + var rows = conn.Query($"SELECT Id, Implementation, ConfigContract, Settings FROM NetImport WHERE Implementation = 'TraktImport'"); + + var corrected = new List(); + + foreach (var row in rows) + { + var settings = JsonSerializer.Deserialize(row.Settings, _serializerSettings); + + if (settings.TraktListType == (int)TraktListType169.UserCustomList) + { + var newSettings = new TraktListSettings170 + { + Listname = settings.Listname, + Username = settings.Username, + AuthUser = settings.Username, + + OAuthUrl = "http://radarr.aeonlucid.com/v1/trakt/redirect", + RenewUri = "http://radarr.aeonlucid.com/v1/trakt/refresh", + ClientId = "964f67b126ade0112c4ae1f0aea3a8fb03190f71117bd83af6a0560a99bc52e6", + Scope = settings.Scope, + AccessToken = settings.AccessToken.IsNotNullOrWhiteSpace() ? settings.AccessToken : GetConfigValue(config, "TraktAuthToken", "localhost") ?? "", + RefreshToken = settings.RefreshToken.IsNotNullOrWhiteSpace() ? settings.RefreshToken : GetConfigValue(config, "TraktRefreshToken", "localhost") ?? "", + Expires = settings.Expires > DateTime.UtcNow ? settings.Expires : DateTime.UtcNow, + Link = settings.Link, + Rating = settings.Rating, + Certification = settings.Certification, + Genres = settings.Genres, + Years = settings.Years, + Limit = settings.Limit, + TraktAdditionalParameters = settings.TraktAdditionalParameters, + SignIn = settings.SignIn + }; + + corrected.Add(new ProviderDefinition169 + { + Id = row.Id, + Implementation = "TraktListImport", + ConfigContract = "TraktListSettings", + Settings = JsonSerializer.Serialize(newSettings, _serializerSettings) + }); + } + else if (settings.TraktListType == (int)TraktListType169.UserWatchedList || settings.TraktListType == (int)TraktListType169.UserWatchList) + { + var newSettings = new TraktUserSettings170 + { + TraktListType = settings.TraktListType, + AuthUser = settings.Username, + + OAuthUrl = "http://radarr.aeonlucid.com/v1/trakt/redirect", + RenewUri = "http://radarr.aeonlucid.com/v1/trakt/refresh", + ClientId = "964f67b126ade0112c4ae1f0aea3a8fb03190f71117bd83af6a0560a99bc52e6", + Scope = settings.Scope, + AccessToken = settings.AccessToken.IsNotNullOrWhiteSpace() ? settings.AccessToken : GetConfigValue(config, "TraktAuthToken", "localhost") ?? "", + RefreshToken = settings.RefreshToken.IsNotNullOrWhiteSpace() ? settings.RefreshToken : GetConfigValue(config, "TraktRefreshToken", "localhost") ?? "", + Expires = settings.Expires > DateTime.UtcNow ? settings.Expires : DateTime.UtcNow, + Link = settings.Link, + Rating = settings.Rating, + Certification = settings.Certification, + Genres = settings.Genres, + Years = settings.Years, + Limit = settings.Limit, + TraktAdditionalParameters = settings.TraktAdditionalParameters, + SignIn = settings.SignIn + }; + + corrected.Add(new ProviderDefinition169 + { + Id = row.Id, + Implementation = "TraktUserImport", + ConfigContract = "TraktUserSettings", + Settings = JsonSerializer.Serialize(newSettings, _serializerSettings) + }); + } + else + { + var newSettings = new TraktPopularSettings170 + { + TraktListType = (int)Enum.Parse(typeof(TraktPopularListType170), Enum.GetName(typeof(TraktListType169), settings.TraktListType)), + AuthUser = settings.Username, + + OAuthUrl = "http://radarr.aeonlucid.com/v1/trakt/redirect", + RenewUri = "http://radarr.aeonlucid.com/v1/trakt/refresh", + ClientId = "964f67b126ade0112c4ae1f0aea3a8fb03190f71117bd83af6a0560a99bc52e6", + Scope = settings.Scope, + AccessToken = settings.AccessToken.IsNotNullOrWhiteSpace() ? settings.AccessToken : GetConfigValue(config, "TraktAuthToken", "localhost") ?? "", + RefreshToken = settings.RefreshToken.IsNotNullOrWhiteSpace() ? settings.RefreshToken : GetConfigValue(config, "TraktRefreshToken", "localhost") ?? "", + Expires = settings.Expires > DateTime.UtcNow ? settings.Expires : DateTime.UtcNow, + Link = settings.Link, + Rating = settings.Rating, + Certification = settings.Certification, + Genres = settings.Genres, + Years = settings.Years, + Limit = settings.Limit, + TraktAdditionalParameters = settings.TraktAdditionalParameters, + SignIn = settings.SignIn + }; + + corrected.Add(new ProviderDefinition169 + { + Id = row.Id, + Implementation = "TraktPopularImport", + ConfigContract = "TraktPopularSettings", + Settings = JsonSerializer.Serialize(newSettings, _serializerSettings) + }); + } + } + + Console.WriteLine(corrected.ToJson()); + + var updateSql = "UPDATE NetImport SET Implementation = @Implementation, ConfigContract = @ConfigContract, Settings = @Settings WHERE Id = @Id"; + conn.Execute(updateSql, corrected, transaction: tran); + } + + private T GetConfigValue(Dictionary config, string key, T defaultValue) + { + key = key.ToLowerInvariant(); + + if (config.ContainsKey(key)) + { + return (T)Convert.ChangeType(config[key], typeof(T)); + } + + return defaultValue; + } + } + + public class ProviderDefinition169 : ModelBase + { + public string Implementation { get; set; } + public string ConfigContract { get; set; } + public string Settings { get; set; } + } + + public class TraktBaseSettings170 + { + public string OAuthUrl { get; set; } + public string RenewUri { get; set; } + public string ClientId { get; set; } + public string Scope { get; set; } + public string AuthUser { get; set; } + public string AccessToken { get; set; } + public string RefreshToken { get; set; } + public DateTime Expires { get; set; } + public string Link { get; set; } + public string Rating { get; set; } + public string Certification { get; set; } + public string Genres { get; set; } + public string Years { get; set; } + public int Limit { get; set; } + public string TraktAdditionalParameters { get; set; } + public string SignIn { get; set; } + } + + public class TraktListSettings170 : TraktBaseSettings170 + { + public string Username { get; set; } + public string Listname { get; set; } + } + + public class TraktPopularSettings170 : TraktBaseSettings170 + { + public int TraktListType { get; set; } + } + + public class TraktUserSettings170 : TraktBaseSettings170 + { + public int TraktListType { get; set; } + } + + public class TraktSettings169 + { + public string OAuthUrl { get; set; } + public string RenewUri { get; set; } + public string ClientId { get; set; } + public virtual string Scope { get; set; } + public string AccessToken { get; set; } + public string RefreshToken { get; set; } + public DateTime Expires { get; set; } + public string Link { get; set; } + public int TraktListType { get; set; } + public string Username { get; set; } + public string Listname { get; set; } + public string Rating { get; set; } + public string Certification { get; set; } + public string Genres { get; set; } + public string Years { get; set; } + public int Limit { get; set; } + public string TraktAdditionalParameters { get; set; } + public string SignIn { get; set; } + } + + public enum TraktListType169 + { + UserWatchList = 0, + UserWatchedList = 1, + UserCustomList = 2, + + Trending = 3, + Popular = 4, + Anticipated = 5, + BoxOffice = 6, + + TopWatchedByWeek = 7, + TopWatchedByMonth = 8, + TopWatchedByYear = 9, + TopWatchedByAllTime = 10 + } + + public enum TraktUserListType170 + { + UserWatchList = 0, + UserWatchedList = 1, + UserCollectionList = 2, + } + + public enum TraktPopularListType170 + { + Trending = 0, + Popular = 1, + Anticipated = 2, + BoxOffice = 3, + + TopWatchedByWeek = 4, + TopWatchedByMonth = 5, + TopWatchedByYear = 6, + TopWatchedByAllTime = 7 + } +} diff --git a/src/NzbDrone.Core/NetImport/CouchPotato/CouchPotatoImport.cs b/src/NzbDrone.Core/NetImport/CouchPotato/CouchPotatoImport.cs index 3a51f256db..3642ee3973 100644 --- a/src/NzbDrone.Core/NetImport/CouchPotato/CouchPotatoImport.cs +++ b/src/NzbDrone.Core/NetImport/CouchPotato/CouchPotatoImport.cs @@ -1,4 +1,4 @@ -using NLog; +using NLog; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; using NzbDrone.Core.Parser; @@ -9,7 +9,7 @@ public class CouchPotatoImport : HttpNetImportBase { public override string Name => "CouchPotato"; - public override NetImportType ListType => NetImportType.Other; + public override NetImportType ListType => NetImportType.Program; public override bool Enabled => true; public override bool EnableAuto => false; diff --git a/src/NzbDrone.Core/NetImport/NetImportType.cs b/src/NzbDrone.Core/NetImport/NetImportType.cs index 6ac07af0b7..7b75c288bb 100644 --- a/src/NzbDrone.Core/NetImport/NetImportType.cs +++ b/src/NzbDrone.Core/NetImport/NetImportType.cs @@ -1,8 +1,10 @@ -namespace NzbDrone.Core.NetImport +namespace NzbDrone.Core.NetImport { public enum NetImportType { + Program, TMDB, + Trakt, Other } } diff --git a/src/NzbDrone.Core/NetImport/Radarr/RadarrImport.cs b/src/NzbDrone.Core/NetImport/Radarr/RadarrImport.cs index 43bbb8e546..683eb29631 100644 --- a/src/NzbDrone.Core/NetImport/Radarr/RadarrImport.cs +++ b/src/NzbDrone.Core/NetImport/Radarr/RadarrImport.cs @@ -1,4 +1,4 @@ -using NLog; +using NLog; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; using NzbDrone.Core.Parser; @@ -11,7 +11,7 @@ public class RadarrImport : HttpNetImportBase public override bool Enabled => true; public override bool EnableAuto => false; - public override NetImportType ListType => NetImportType.Other; + public override NetImportType ListType => NetImportType.Program; public RadarrImport(IHttpClient httpClient, IConfigService configService, diff --git a/src/NzbDrone.Core/NetImport/TMDb/Popular/TMDbPopularSettings.cs b/src/NzbDrone.Core/NetImport/TMDb/Popular/TMDbPopularSettings.cs index 1de77f9153..c12cad65f0 100644 --- a/src/NzbDrone.Core/NetImport/TMDb/Popular/TMDbPopularSettings.cs +++ b/src/NzbDrone.Core/NetImport/TMDb/Popular/TMDbPopularSettings.cs @@ -8,7 +8,7 @@ public class TMDbPopularSettingsValidator : TMDbSettingsBaseValidator c.TMDbListType).NotEmpty(); + RuleFor(c => c.TMDbListType).NotNull(); RuleFor(c => c.FilterCriteria).SetValidator(_ => new TMDbFilterSettingsValidator()); } diff --git a/src/NzbDrone.Core/NetImport/Trakt/List/TraktListImport.cs b/src/NzbDrone.Core/NetImport/Trakt/List/TraktListImport.cs new file mode 100644 index 0000000000..52d838bb2d --- /dev/null +++ b/src/NzbDrone.Core/NetImport/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.NetImport.Trakt.List +{ + public class TraktListImport : TraktImportBase + { + public TraktListImport(INetImportRepository netImportRepository, + IHttpClient httpClient, + IConfigService configService, + IParsingService parsingService, + Logger logger) + : base(netImportRepository, httpClient, configService, parsingService, logger) + { + } + + public override string Name => "Trakt List"; + public override bool Enabled => true; + public override bool EnableAuto => false; + + public override INetImportRequestGenerator GetRequestGenerator() + { + return new TraktListRequestGenerator() + { + Settings = Settings + }; + } + } +} diff --git a/src/NzbDrone.Core/NetImport/Trakt/List/TraktListRequestGenerator.cs b/src/NzbDrone.Core/NetImport/Trakt/List/TraktListRequestGenerator.cs new file mode 100644 index 0000000000..59c6c17377 --- /dev/null +++ b/src/NzbDrone.Core/NetImport/Trakt/List/TraktListRequestGenerator.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; + +namespace NzbDrone.Core.NetImport.Trakt.List +{ + public class TraktListRequestGenerator : INetImportRequestGenerator + { + public TraktListSettings Settings { get; set; } + + public TraktListRequestGenerator() + { + } + + public virtual NetImportPageableRequestChain GetMovies() + { + var pageableRequests = new NetImportPageableRequestChain(); + + pageableRequests.Add(GetMoviesRequest()); + + return pageableRequests; + } + + private IEnumerable GetMoviesRequest() + { + var link = Settings.Link.Trim(); + + var listName = Parser.Parser.ToUrlSlug(Settings.Listname.Trim()); + link += $"/users/{Settings.Username.Trim()}/lists/{listName}/items/movies?limit={Settings.Limit}"; + + var request = new NetImportRequest($"{link}", HttpAccept.Json); + + request.HttpRequest.Headers.Add("trakt-api-version", "2"); + request.HttpRequest.Headers.Add("trakt-api-key", Settings.ClientId); //aeon + + if (Settings.AccessToken.IsNotNullOrWhiteSpace()) + { + request.HttpRequest.Headers.Add("Authorization", "Bearer " + Settings.AccessToken); + } + + yield return request; + } + } +} diff --git a/src/NzbDrone.Core/NetImport/Trakt/List/TraktListSettings.cs b/src/NzbDrone.Core/NetImport/Trakt/List/TraktListSettings.cs new file mode 100644 index 0000000000..f6f5609105 --- /dev/null +++ b/src/NzbDrone.Core/NetImport/Trakt/List/TraktListSettings.cs @@ -0,0 +1,28 @@ +using FluentValidation; +using NzbDrone.Core.Annotations; + +namespace NzbDrone.Core.NetImport.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/NetImport/Trakt/Popular/TraktPopularImport.cs b/src/NzbDrone.Core/NetImport/Trakt/Popular/TraktPopularImport.cs new file mode 100644 index 0000000000..9a46ad0e81 --- /dev/null +++ b/src/NzbDrone.Core/NetImport/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.NetImport.Trakt.Popular +{ + public class TraktPopularImport : TraktImportBase + { + public TraktPopularImport(INetImportRepository netImportRepository, + IHttpClient httpClient, + IConfigService configService, + IParsingService parsingService, + Logger logger) + : base(netImportRepository, httpClient, configService, parsingService, logger) + { + } + + public override string Name => "Trakt Popular List"; + public override bool Enabled => true; + public override bool EnableAuto => false; + + public override IParseNetImportResponse GetParser() + { + return new TraktPopularParser(Settings); + } + + public override INetImportRequestGenerator GetRequestGenerator() + { + return new TraktPopularRequestGenerator() + { + Settings = Settings + }; + } + } +} diff --git a/src/NzbDrone.Core/NetImport/Trakt/Popular/TraktPopularListType.cs b/src/NzbDrone.Core/NetImport/Trakt/Popular/TraktPopularListType.cs new file mode 100644 index 0000000000..d380952a17 --- /dev/null +++ b/src/NzbDrone.Core/NetImport/Trakt/Popular/TraktPopularListType.cs @@ -0,0 +1,25 @@ +using System.Runtime.Serialization; + +namespace NzbDrone.Core.NetImport.Trakt.Popular +{ + public enum TraktPopularListType + { + [EnumMember(Value = "Trending Movies")] + Trending = 0, + [EnumMember(Value = "Popular Movies")] + Popular = 1, + [EnumMember(Value = "Top Anticipated Movies")] + Anticipated = 2, + [EnumMember(Value = "Top Box Office Movies")] + BoxOffice = 3, + + [EnumMember(Value = "Top Watched Movies By Week")] + TopWatchedByWeek = 4, + [EnumMember(Value = "Top Watched Movies By Month")] + TopWatchedByMonth = 5, + [EnumMember(Value = "Top Watched Movies By Year")] + TopWatchedByYear = 6, + [EnumMember(Value = "Top Watched Movies Of All Time")] + TopWatchedByAllTime = 7 + } +} diff --git a/src/NzbDrone.Core/NetImport/Trakt/Popular/TraktPopularParser.cs b/src/NzbDrone.Core/NetImport/Trakt/Popular/TraktPopularParser.cs new file mode 100644 index 0000000000..905633bd3a --- /dev/null +++ b/src/NzbDrone.Core/NetImport/Trakt/Popular/TraktPopularParser.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using Newtonsoft.Json; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Movies; + +namespace NzbDrone.Core.NetImport.Trakt.Popular +{ + public class TraktPopularParser : TraktParser + { + private readonly TraktPopularSettings _settings; + private NetImportResponse _importResponse; + + public TraktPopularParser(TraktPopularSettings settings) + { + _settings = settings; + } + + public override IList ParseResponse(NetImportResponse importResponse) + { + _importResponse = importResponse; + + var movies = new List(); + + if (!PreProcess(_importResponse)) + { + return movies; + } + + var jsonResponse = new List(); + + if (_settings.TraktListType == (int)TraktPopularListType.Popular) + { + jsonResponse = JsonConvert.DeserializeObject>(_importResponse.Content); + } + else + { + jsonResponse = JsonConvert.DeserializeObject>(_importResponse.Content).SelectList(c => c.Movie); + } + + // no movies were return + if (jsonResponse == null) + { + return movies; + } + + foreach (var movie in jsonResponse) + { + movies.AddIfNotNull(new Movies.Movie() + { + Title = movie.Title, + ImdbId = movie.Ids.Imdb, + TmdbId = movie.Ids.Tmdb, + Year = movie.Year ?? 0 + }); + } + + return movies; + } + } +} diff --git a/src/NzbDrone.Core/NetImport/Trakt/Popular/TraktPopularRequestGenerator.cs b/src/NzbDrone.Core/NetImport/Trakt/Popular/TraktPopularRequestGenerator.cs new file mode 100644 index 0000000000..f8215a61a8 --- /dev/null +++ b/src/NzbDrone.Core/NetImport/Trakt/Popular/TraktPopularRequestGenerator.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; + +namespace NzbDrone.Core.NetImport.Trakt.Popular +{ + public class TraktPopularRequestGenerator : INetImportRequestGenerator + { + public TraktPopularSettings Settings { get; set; } + + public TraktPopularRequestGenerator() + { + } + + public virtual NetImportPageableRequestChain GetMovies() + { + var pageableRequests = new NetImportPageableRequestChain(); + + pageableRequests.Add(GetMoviesRequest()); + + return pageableRequests; + } + + private IEnumerable GetMoviesRequest() + { + var link = Settings.Link.Trim(); + + var filtersAndLimit = $"?years={Settings.Years}&genres={Settings.Genres.ToLower()}&ratings={Settings.Rating}&certifications={Settings.Certification.ToLower()}&limit={Settings.Limit}{Settings.TraktAdditionalParameters}"; + + switch (Settings.TraktListType) + { + case (int)TraktPopularListType.Trending: + link += "/movies/trending" + filtersAndLimit; + break; + case (int)TraktPopularListType.Popular: + link += "/movies/popular" + filtersAndLimit; + break; + case (int)TraktPopularListType.Anticipated: + link += "/movies/anticipated" + filtersAndLimit; + break; + case (int)TraktPopularListType.BoxOffice: + link += "/movies/boxoffice" + filtersAndLimit; + break; + case (int)TraktPopularListType.TopWatchedByWeek: + link += "/movies/watched/weekly" + filtersAndLimit; + break; + case (int)TraktPopularListType.TopWatchedByMonth: + link += "/movies/watched/monthly" + filtersAndLimit; + break; + case (int)TraktPopularListType.TopWatchedByYear: + link += "/movies/watched/yearly" + filtersAndLimit; + break; + case (int)TraktPopularListType.TopWatchedByAllTime: + link += "/movies/watched/all" + filtersAndLimit; + break; + } + + var request = new NetImportRequest($"{link}", HttpAccept.Json); + + request.HttpRequest.Headers.Add("trakt-api-version", "2"); + request.HttpRequest.Headers.Add("trakt-api-key", Settings.ClientId); //aeon + + if (Settings.AccessToken.IsNotNullOrWhiteSpace()) + { + request.HttpRequest.Headers.Add("Authorization", "Bearer " + Settings.AccessToken); + } + + yield return request; + } + } +} diff --git a/src/NzbDrone.Core/NetImport/Trakt/Popular/TraktPopularSettings.cs b/src/NzbDrone.Core/NetImport/Trakt/Popular/TraktPopularSettings.cs new file mode 100644 index 0000000000..39ba6d5020 --- /dev/null +++ b/src/NzbDrone.Core/NetImport/Trakt/Popular/TraktPopularSettings.cs @@ -0,0 +1,27 @@ +using FluentValidation; +using NzbDrone.Core.Annotations; + +namespace NzbDrone.Core.NetImport.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/NetImport/Trakt/TraktAPI.cs b/src/NzbDrone.Core/NetImport/Trakt/TraktAPI.cs index fb9734c26a..62b759bc9b 100644 --- a/src/NzbDrone.Core/NetImport/Trakt/TraktAPI.cs +++ b/src/NzbDrone.Core/NetImport/Trakt/TraktAPI.cs @@ -1,34 +1,53 @@ -namespace NzbDrone.Core.NetImport.Trakt +namespace NzbDrone.Core.NetImport.Trakt { - public class Ids + public class TraktMovieIdsResource { - public int trakt { get; set; } - public string slug { get; set; } - public string imdb { get; set; } - public int tmdb { get; set; } + public int Trakt { get; set; } + public string Slug { get; set; } + public string Imdb { get; set; } + public int Tmdb { get; set; } } - public class Movie + public class TraktMovieResource { - public string title { get; set; } - public int? year { get; set; } - public Ids ids { get; set; } + public string Title { get; set; } + public int? Year { get; set; } + public TraktMovieIdsResource Ids { get; set; } } public class TraktResponse { - public int? rank { get; set; } - public string listed_at { get; set; } - public string type { get; set; } + public int? Rank { get; set; } + public string Listed_at { get; set; } + public string Type { get; set; } - public int? watchers { get; set; } + public int? Watchers { get; set; } - public long? revenue { get; set; } + public long? Revenue { get; set; } - public long? watcher_count { get; set; } - public long? play_count { get; set; } - public long? collected_count { get; set; } + public long? Watcher_count { get; set; } + public long? Play_count { get; set; } + public long? Collected_count { get; set; } - public Movie movie { get; set; } + public TraktMovieResource Movie { get; set; } + } + + public class RefreshRequestResponse + { + public string Access_token { get; set; } + public string Token_type { get; set; } + public int Expires_in { get; set; } + public string Refresh_token { get; set; } + public string Scope { get; set; } + } + + public class UserSettingsResponse + { + public TraktUserResource User { get; set; } + } + + public class TraktUserResource + { + public string Username { get; set; } } } diff --git a/src/NzbDrone.Core/NetImport/Trakt/TraktImport.cs b/src/NzbDrone.Core/NetImport/Trakt/TraktImportBase.cs similarity index 63% rename from src/NzbDrone.Core/NetImport/Trakt/TraktImport.cs rename to src/NzbDrone.Core/NetImport/Trakt/TraktImportBase.cs index 82c398586e..45ed2dc7b0 100644 --- a/src/NzbDrone.Core/NetImport/Trakt/TraktImport.cs +++ b/src/NzbDrone.Core/NetImport/Trakt/TraktImportBase.cs @@ -1,6 +1,7 @@ -using System; +using System; using System.Collections.Generic; using NLog; +using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; using NzbDrone.Core.Parser; @@ -8,17 +9,14 @@ namespace NzbDrone.Core.NetImport.Trakt { - public class TraktImport : HttpNetImportBase + public abstract class TraktImportBase : HttpNetImportBase + where TSettings : TraktSettingsBase, new() { - public override string Name => "Trakt List"; - - public override NetImportType ListType => NetImportType.Other; - public override bool Enabled => true; - public override bool EnableAuto => false; + public override NetImportType ListType => NetImportType.Trakt; private INetImportRepository _netImportRepository; - public TraktImport(INetImportRepository netImportRepository, + protected TraktImportBase(INetImportRepository netImportRepository, IHttpClient httpClient, IConfigService configService, IParsingService parsingService, @@ -28,40 +26,7 @@ public TraktImport(INetImportRepository netImportRepository, _netImportRepository = netImportRepository; } - private void RefreshToken() - { - _logger.Trace("Refreshing Token"); - - Settings.Validate().Filter("RefreshToken").ThrowOnError(); - - var request = new HttpRequestBuilder(Settings.RenewUri) - .AddQueryParam("refresh", Settings.RefreshToken) - .Build(); - - try - { - var response = _httpClient.Get(request); - - if (response != null && response.Resource != null) - { - var token = response.Resource; - Settings.AccessToken = token.access_token; - Settings.Expires = DateTime.UtcNow.AddSeconds(token.expires_in); - Settings.RefreshToken = token.refresh_token != null ? token.refresh_token : Settings.RefreshToken; - - if (Definition.Id > 0) - { - _netImportRepository.UpdateSettings((NetImportDefinition)Definition); - } - } - } - catch (HttpException) - { - _logger.Warn($"Error refreshing trakt access token"); - } - } - - public override INetImportRequestGenerator GetRequestGenerator() + public override NetImportFetchResult Fetch() { Settings.Validate().Filter("AccessToken", "RefreshToken").ThrowOnError(); _logger.Trace($"Access token expires at {Settings.Expires}"); @@ -71,12 +36,13 @@ public override INetImportRequestGenerator GetRequestGenerator() RefreshToken(); } - return new TraktRequestGenerator() { Settings = Settings, _configService = _configService, HttpClient = _httpClient, }; + var generator = GetRequestGenerator(); + return FetchMovies(generator.GetMovies()); } public override IParseNetImportResponse GetParser() { - return new TraktParser(Settings); + return new TraktParser(); } public override object RequestAction(string action, IDictionary query) @@ -99,10 +65,74 @@ public override object RequestAction(string action, IDictionary accessToken = query["access"], expires = DateTime.UtcNow.AddSeconds(4838400), refreshToken = query["refresh"], + authUser = GetUserName(query["access"]) }; } return new { }; } + + private string GetUserName(string accessToken) + { + var request = new HttpRequestBuilder(string.Format("{0}/users/settings", Settings.Link)) + .Build(); + + request.Headers.Add("trakt-api-version", "2"); + request.Headers.Add("trakt-api-key", Settings.ClientId); //aeon + + 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.Username; + } + } + 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(Settings.RenewUri) + .AddQueryParam("refresh", Settings.RefreshToken) + .Build(); + + try + { + var response = _httpClient.Get(request); + + if (response != null && response.Resource != null) + { + var token = response.Resource; + Settings.AccessToken = token.Access_token; + Settings.Expires = DateTime.UtcNow.AddSeconds(token.Expires_in); + Settings.RefreshToken = token.Refresh_token != null ? token.Refresh_token : Settings.RefreshToken; + + if (Definition.Id > 0) + { + _netImportRepository.UpdateSettings((NetImportDefinition)Definition); + } + } + } + catch (HttpException) + { + _logger.Warn($"Error refreshing trakt access token"); + } + } } } diff --git a/src/NzbDrone.Core/NetImport/Trakt/TraktListType.cs b/src/NzbDrone.Core/NetImport/Trakt/TraktListType.cs deleted file mode 100644 index 2aca1cdffe..0000000000 --- a/src/NzbDrone.Core/NetImport/Trakt/TraktListType.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Runtime.Serialization; - -namespace NzbDrone.Core.NetImport.Trakt -{ - public enum TraktListType - { - [EnumMember(Value = "User Watch List")] - UserWatchList = 0, - [EnumMember(Value = "User Watched List")] - UserWatchedList = 1, - [EnumMember(Value = "User Custom List")] - UserCustomList = 2, - - [EnumMember(Value = "Trending Movies")] - Trending = 3, - [EnumMember(Value = "Popular Movies")] - Popular = 4, - [EnumMember(Value = "Top Anticipated Movies")] - Anticipated = 5, - [EnumMember(Value = "Top Box Office Movies")] - BoxOffice = 6, - - [EnumMember(Value = "Top Watched Movies By Week")] - TopWatchedByWeek = 7, - [EnumMember(Value = "Top Watched Movies By Month")] - TopWatchedByMonth = 8, - [EnumMember(Value = "Top Watched Movies By Year")] - TopWatchedByYear = 9, - [EnumMember(Value = "Top Watched Movies Of All Time")] - TopWatchedByAllTime = 10 - } -} diff --git a/src/NzbDrone.Core/NetImport/Trakt/TraktParser.cs b/src/NzbDrone.Core/NetImport/Trakt/TraktParser.cs index a0f26ec9cc..3702326269 100644 --- a/src/NzbDrone.Core/NetImport/Trakt/TraktParser.cs +++ b/src/NzbDrone.Core/NetImport/Trakt/TraktParser.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Net; using Newtonsoft.Json; using NzbDrone.Common.Extensions; @@ -8,15 +8,13 @@ namespace NzbDrone.Core.NetImport.Trakt { public class TraktParser : IParseNetImportResponse { - private readonly TraktSettings _settings; private NetImportResponse _importResponse; - public TraktParser(TraktSettings settings) + public TraktParser() { - _settings = settings; } - public IList ParseResponse(NetImportResponse importResponse) + public virtual IList ParseResponse(NetImportResponse importResponse) { _importResponse = importResponse; @@ -27,41 +25,23 @@ public TraktParser(TraktSettings settings) return movies; } - if (_settings.TraktListType == (int)TraktListType.Popular) - { - var jsonResponse = JsonConvert.DeserializeObject>(_importResponse.Content); + var jsonResponse = JsonConvert.DeserializeObject>(_importResponse.Content); - foreach (var movie in jsonResponse) - { - movies.AddIfNotNull(new Movies.Movie() - { - Title = movie.title, - ImdbId = movie.ids.imdb, - TmdbId = movie.ids.tmdb, - Year = movie.year ?? 0 - }); - } + // no movies were return + if (jsonResponse == null) + { + return movies; } - else + + foreach (var movie in jsonResponse) { - var jsonResponse = JsonConvert.DeserializeObject>(_importResponse.Content); - - // no movies were return - if (jsonResponse == null) + movies.AddIfNotNull(new Movies.Movie() { - return movies; - } - - foreach (var movie in jsonResponse) - { - movies.AddIfNotNull(new Movies.Movie() - { - Title = movie.movie.title, - ImdbId = movie.movie.ids.imdb, - TmdbId = movie.movie.ids.tmdb, - Year = movie.movie.year ?? 0 - }); - } + Title = movie.Movie.Title, + ImdbId = movie.Movie.Ids.Imdb, + TmdbId = movie.Movie.Ids.Tmdb, + Year = movie.Movie.Year ?? 0 + }); } return movies; diff --git a/src/NzbDrone.Core/NetImport/Trakt/TraktRequestGenerator.cs b/src/NzbDrone.Core/NetImport/Trakt/TraktRequestGenerator.cs deleted file mode 100644 index 0a253881db..0000000000 --- a/src/NzbDrone.Core/NetImport/Trakt/TraktRequestGenerator.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System.Collections.Generic; -using NzbDrone.Common.Extensions; -using NzbDrone.Common.Http; -using NzbDrone.Core.Configuration; - -namespace NzbDrone.Core.NetImport.Trakt -{ - public class RefreshRequestResponse - { - public string access_token { get; set; } - public string token_type { get; set; } - public int expires_in { get; set; } - public string refresh_token { get; set; } - public string scope { get; set; } - } - - public class TraktRequestGenerator : INetImportRequestGenerator - { - public IConfigService _configService; - public IHttpClient HttpClient { get; set; } - public TraktSettings Settings { get; set; } - - public TraktRequestGenerator() - { - } - - public virtual NetImportPageableRequestChain GetMovies() - { - var pageableRequests = new NetImportPageableRequestChain(); - - pageableRequests.Add(GetMovies(null)); - - return pageableRequests; - } - - private IEnumerable GetMovies(string searchParameters) - { - var link = Settings.Link.Trim(); - - var filtersAndLimit = $"?years={Settings.Years}&genres={Settings.Genres.ToLower()}&ratings={Settings.Rating}&certifications={Settings.Certification.ToLower()}&limit={Settings.Limit}{Settings.TraktAdditionalParameters}"; - - switch (Settings.TraktListType) - { - case (int)TraktListType.UserCustomList: - var listName = Parser.Parser.ToUrlSlug(Settings.Listname.Trim()); - link = link + $"/users/{Settings.Username.Trim()}/lists/{listName}/items/movies?limit={Settings.Limit}"; - break; - case (int)TraktListType.UserWatchList: - link = link + $"/users/{Settings.Username.Trim()}/watchlist/movies?limit={Settings.Limit}"; - break; - case (int)TraktListType.UserWatchedList: - link = link + $"/users/{Settings.Username.Trim()}/watched/movies?limit={Settings.Limit}"; - break; - case (int)TraktListType.Trending: - link = link + "/movies/trending" + filtersAndLimit; - break; - case (int)TraktListType.Popular: - link = link + "/movies/popular" + filtersAndLimit; - break; - case (int)TraktListType.Anticipated: - link = link + "/movies/anticipated" + filtersAndLimit; - break; - case (int)TraktListType.BoxOffice: - link = link + "/movies/boxoffice" + filtersAndLimit; - break; - case (int)TraktListType.TopWatchedByWeek: - link = link + "/movies/watched/weekly" + filtersAndLimit; - break; - case (int)TraktListType.TopWatchedByMonth: - link = link + "/movies/watched/monthly" + filtersAndLimit; - break; - case (int)TraktListType.TopWatchedByYear: - link = link + "/movies/watched/yearly" + filtersAndLimit; - break; - case (int)TraktListType.TopWatchedByAllTime: - link = link + "/movies/watched/all" + filtersAndLimit; - break; - } - - var request = new NetImportRequest($"{link}", HttpAccept.Json); - - request.HttpRequest.Headers.Add("trakt-api-version", "2"); - request.HttpRequest.Headers.Add("trakt-api-key", Settings.ClientId); //aeon - - if (Settings.AccessToken.IsNotNullOrWhiteSpace()) - { - request.HttpRequest.Headers.Add("Authorization", "Bearer " + Settings.AccessToken); - } - - yield return request; - } - } -} diff --git a/src/NzbDrone.Core/NetImport/Trakt/TraktSettings.cs b/src/NzbDrone.Core/NetImport/Trakt/TraktSettingsBase.cs similarity index 63% rename from src/NzbDrone.Core/NetImport/Trakt/TraktSettings.cs rename to src/NzbDrone.Core/NetImport/Trakt/TraktSettingsBase.cs index 90be3f0f1a..66322b5383 100644 --- a/src/NzbDrone.Core/NetImport/Trakt/TraktSettings.cs +++ b/src/NzbDrone.Core/NetImport/Trakt/TraktSettingsBase.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Text.RegularExpressions; using FluentValidation; using NzbDrone.Common.Extensions; @@ -8,27 +8,16 @@ namespace NzbDrone.Core.NetImport.Trakt { - public class TraktSettingsValidator : AbstractValidator + public class TraktSettingsBaseValidator : AbstractValidator + where TSettings : TraktSettingsBase { - public TraktSettingsValidator() + public TraktSettingsBaseValidator() { RuleFor(c => c.Link).ValidRootUrl(); RuleFor(c => c.AccessToken).NotEmpty(); RuleFor(c => c.RefreshToken).NotEmpty(); RuleFor(c => c.Expires).NotEmpty(); - // List name required for UserCustomList - RuleFor(c => c.Listname) - .Matches(@"^[A-Za-z0-9\-_]+$", RegexOptions.IgnoreCase) - .When(c => c.TraktListType == (int)TraktListType.UserCustomList) - .WithMessage("List name is required when using Custom Trakt Lists"); - - // Username required for UserWatchedList/UserWatchList - RuleFor(c => c.Username) - .Matches(@"^[A-Za-z0-9\-_]+$", RegexOptions.IgnoreCase) - .When(c => c.TraktListType == (int)TraktListType.UserWatchedList || c.TraktListType == (int)TraktListType.UserWatchList) - .WithMessage("Username is required when using User Trakt Lists"); - // Loose validation @TODO RuleFor(c => c.Rating) .Matches(@"^\d+\-\d+$", RegexOptions.IgnoreCase) @@ -56,17 +45,15 @@ public TraktSettingsValidator() } } - public class TraktSettings : IProviderConfig + public class TraktSettingsBase : IProviderConfig + where TSettings : TraktSettingsBase { - private static readonly TraktSettingsValidator Validator = new TraktSettingsValidator(); + protected virtual AbstractValidator Validator => new TraktSettingsBaseValidator(); - public TraktSettings() + public TraktSettingsBase() { Link = "https://api.trakt.tv"; SignIn = "startOAuth"; - TraktListType = (int)Trakt.TraktListType.Popular; - Username = ""; - Listname = ""; Rating = "0-100"; Certification = "NR,G,PG,PG-13,R,NC-17"; Genres = ""; @@ -88,34 +75,28 @@ public TraktSettings() [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(0, Label = "Trakt API URL", HelpText = "Link to to Trakt API URL, do not change unless you know what you are doing.")] public string Link { get; set; } - [FieldDefinition(1, Label = "List Type", Type = FieldType.Select, SelectOptions = typeof(TraktListType), HelpText = "Trakt list type")] - public int TraktListType { get; set; } - - [FieldDefinition(2, Label = "Username", HelpText = "Required for User List (Ignores Filtering Options)")] - public string Username { get; set; } - - [FieldDefinition(3, Label = "List Name", HelpText = "Required for Custom List (Ignores Filtering Options)")] - public string Listname { get; set; } - - [FieldDefinition(4, Label = "Rating", HelpText = "Filter movies by rating range (0-100)")] + [FieldDefinition(1, Label = "Rating", HelpText = "Filter movies by rating range (0-100)")] public string Rating { get; set; } - [FieldDefinition(5, Label = "Certification", HelpText = "Filter movies by a certification (NR,G,PG,PG-13,R,NC-17), (Comma Separated)")] + [FieldDefinition(2, Label = "Certification", HelpText = "Filter movies by a certification (NR,G,PG,PG-13,R,NC-17), (Comma Separated)")] public string Certification { get; set; } - [FieldDefinition(6, Label = "Genres", HelpText = "Filter movies by Trakt Genre Slug (Comma Separated)")] + [FieldDefinition(3, Label = "Genres", HelpText = "Filter movies by Trakt Genre Slug (Comma Separated)")] public string Genres { get; set; } - [FieldDefinition(7, Label = "Years", HelpText = "Filter movies by year or year range")] + [FieldDefinition(4, Label = "Years", HelpText = "Filter movies by year or year range")] public string Years { get; set; } - [FieldDefinition(8, Label = "Limit", HelpText = "Limit the number of movies to get")] + [FieldDefinition(5, Label = "Limit", HelpText = "Limit the number of movies to get")] public int Limit { get; set; } - [FieldDefinition(9, Label = "Additional Parameters", HelpText = "Additional Trakt API parameters", Advanced = true)] + [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)] @@ -123,7 +104,7 @@ public TraktSettings() public NzbDroneValidationResult Validate() { - return new NzbDroneValidationResult(Validator.Validate(this)); + return new NzbDroneValidationResult(Validator.Validate((TSettings)this)); } } } diff --git a/src/NzbDrone.Core/NetImport/Trakt/User/TraktUserImport.cs b/src/NzbDrone.Core/NetImport/Trakt/User/TraktUserImport.cs new file mode 100644 index 0000000000..9a4a90f8c6 --- /dev/null +++ b/src/NzbDrone.Core/NetImport/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.NetImport.Trakt.User +{ + public class TraktUserImport : TraktImportBase + { + public TraktUserImport(INetImportRepository netImportRepository, + IHttpClient httpClient, + IConfigService configService, + IParsingService parsingService, + Logger logger) + : base(netImportRepository, httpClient, configService, parsingService, logger) + { + } + + public override string Name => "Trakt User"; + public override bool Enabled => true; + public override bool EnableAuto => false; + + public override INetImportRequestGenerator GetRequestGenerator() + { + return new TraktUserRequestGenerator() + { + Settings = Settings + }; + } + } +} diff --git a/src/NzbDrone.Core/NetImport/Trakt/User/TraktUserListType.cs b/src/NzbDrone.Core/NetImport/Trakt/User/TraktUserListType.cs new file mode 100644 index 0000000000..24c147fa29 --- /dev/null +++ b/src/NzbDrone.Core/NetImport/Trakt/User/TraktUserListType.cs @@ -0,0 +1,14 @@ +using System.Runtime.Serialization; + +namespace NzbDrone.Core.NetImport.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/NetImport/Trakt/User/TraktUserRequestGenerator.cs b/src/NzbDrone.Core/NetImport/Trakt/User/TraktUserRequestGenerator.cs new file mode 100644 index 0000000000..044a266756 --- /dev/null +++ b/src/NzbDrone.Core/NetImport/Trakt/User/TraktUserRequestGenerator.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; + +namespace NzbDrone.Core.NetImport.Trakt.User +{ + public class TraktUserRequestGenerator : INetImportRequestGenerator + { + public TraktUserSettings Settings { get; set; } + + public TraktUserRequestGenerator() + { + } + + public virtual NetImportPageableRequestChain GetMovies() + { + var pageableRequests = new NetImportPageableRequestChain(); + + pageableRequests.Add(GetMoviesRequest()); + + return pageableRequests; + } + + private IEnumerable GetMoviesRequest() + { + var link = Settings.Link.Trim(); + + switch (Settings.TraktListType) + { + case (int)TraktUserListType.UserWatchList: + link += $"/users/{Settings.AuthUser.Trim()}/watchlist/movies?limit={Settings.Limit}"; + break; + case (int)TraktUserListType.UserWatchedList: + link += $"/users/{Settings.AuthUser.Trim()}/watched/movies?limit={Settings.Limit}"; + break; + case (int)TraktUserListType.UserCollectionList: + link += $"/users/{Settings.AuthUser.Trim()}/collection/movies?limit={Settings.Limit}"; + break; + } + + var request = new NetImportRequest($"{link}", HttpAccept.Json); + + request.HttpRequest.Headers.Add("trakt-api-version", "2"); + request.HttpRequest.Headers.Add("trakt-api-key", Settings.ClientId); + + if (Settings.AccessToken.IsNotNullOrWhiteSpace()) + { + request.HttpRequest.Headers.Add("Authorization", "Bearer " + Settings.AccessToken); + } + + yield return request; + } + } +} diff --git a/src/NzbDrone.Core/NetImport/Trakt/User/TraktUserSettings.cs b/src/NzbDrone.Core/NetImport/Trakt/User/TraktUserSettings.cs new file mode 100644 index 0000000000..a487db0c85 --- /dev/null +++ b/src/NzbDrone.Core/NetImport/Trakt/User/TraktUserSettings.cs @@ -0,0 +1,28 @@ +using FluentValidation; +using NzbDrone.Core.Annotations; + +namespace NzbDrone.Core.NetImport.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; } + } +}