New: Trakt List Organization, User Collection List Support

This commit is contained in:
Qstick 2020-01-26 17:42:44 -05:00
parent 7adfe65f65
commit 770e3379fb
24 changed files with 1096 additions and 267 deletions

View File

@ -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<fix_trakt_list_config>
{
[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<ListDefinition169>("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<ListDefinition169>("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<TraktUserSettings170>();
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<ListDefinition169>("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<TraktPopularSettings170>();
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<ListDefinition169>("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<TraktListSettings170>();
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<int> Tags { get; set; }
}
public class RadarrListSettings169
{
public string APIURL { get; set; }
public string Path { get; set; }
}
}

View File

@ -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<ProviderDefinition169>($"SELECT Id, Implementation, ConfigContract, Settings FROM NetImport WHERE Implementation = 'RadarrLists'");
var corrected = new List<ProviderDefinition169>();
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<string, string>();
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<ProviderDefinition169>($"SELECT Id, Implementation, ConfigContract, Settings FROM NetImport WHERE Implementation = 'TraktImport'");
var corrected = new List<ProviderDefinition169>();
foreach (var row in rows)
{
var settings = JsonSerializer.Deserialize<TraktSettings169>(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<T>(Dictionary<string, string> 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
}
}

View File

@ -1,4 +1,4 @@
using NLog;
using NLog;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Parser;
@ -9,7 +9,7 @@ namespace NzbDrone.Core.NetImport.CouchPotato
{
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;

View File

@ -1,8 +1,10 @@
namespace NzbDrone.Core.NetImport
namespace NzbDrone.Core.NetImport
{
public enum NetImportType
{
Program,
TMDB,
Trakt,
Other
}
}

View File

@ -1,4 +1,4 @@
using NLog;
using NLog;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Parser;
@ -11,7 +11,7 @@ namespace NzbDrone.Core.NetImport.Radarr
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,

View File

@ -8,7 +8,7 @@ namespace NzbDrone.Core.NetImport.TMDb.Popular
public TMDbPopularSettingsValidator()
: base()
{
RuleFor(c => c.TMDbListType).NotEmpty();
RuleFor(c => c.TMDbListType).NotNull();
RuleFor(c => c.FilterCriteria).SetValidator(_ => new TMDbFilterSettingsValidator());
}

View File

@ -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<TraktListSettings>
{
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
};
}
}
}

View File

@ -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<NetImportRequest> 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;
}
}
}

View File

@ -0,0 +1,28 @@
using FluentValidation;
using NzbDrone.Core.Annotations;
namespace NzbDrone.Core.NetImport.Trakt.List
{
public class TraktListSettingsValidator : TraktSettingsBaseValidator<TraktListSettings>
{
public TraktListSettingsValidator()
: base()
{
}
}
public class TraktListSettings : TraktSettingsBase<TraktListSettings>
{
protected override AbstractValidator<TraktListSettings> 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; }
}
}

View File

@ -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<TraktPopularSettings>
{
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
};
}
}
}

View File

@ -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
}
}

View File

@ -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<Movie> ParseResponse(NetImportResponse importResponse)
{
_importResponse = importResponse;
var movies = new List<Movie>();
if (!PreProcess(_importResponse))
{
return movies;
}
var jsonResponse = new List<TraktMovieResource>();
if (_settings.TraktListType == (int)TraktPopularListType.Popular)
{
jsonResponse = JsonConvert.DeserializeObject<List<TraktMovieResource>>(_importResponse.Content);
}
else
{
jsonResponse = JsonConvert.DeserializeObject<List<TraktResponse>>(_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;
}
}
}

View File

@ -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<NetImportRequest> 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;
}
}
}

View File

@ -0,0 +1,27 @@
using FluentValidation;
using NzbDrone.Core.Annotations;
namespace NzbDrone.Core.NetImport.Trakt.Popular
{
public class TraktPopularSettingsValidator : TraktSettingsBaseValidator<TraktPopularSettings>
{
public TraktPopularSettingsValidator()
: base()
{
RuleFor(c => c.TraktListType).NotNull();
}
}
public class TraktPopularSettings : TraktSettingsBase<TraktPopularSettings>
{
protected override AbstractValidator<TraktPopularSettings> 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; }
}
}

View File

@ -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; }
}
}

View File

@ -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 @@ using NzbDrone.Core.Validation;
namespace NzbDrone.Core.NetImport.Trakt
{
public class TraktImport : HttpNetImportBase<TraktSettings>
public abstract class TraktImportBase<TSettings> : HttpNetImportBase<TSettings>
where TSettings : TraktSettingsBase<TSettings>, 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 @@ namespace NzbDrone.Core.NetImport.Trakt
_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<RefreshRequestResponse>(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 @@ namespace NzbDrone.Core.NetImport.Trakt
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<string, string> query)
@ -99,10 +65,74 @@ namespace NzbDrone.Core.NetImport.Trakt
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<UserSettingsResponse>(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<RefreshRequestResponse>(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");
}
}
}
}

View File

@ -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
}
}

View File

@ -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<Movies.Movie> ParseResponse(NetImportResponse importResponse)
public virtual IList<Movies.Movie> ParseResponse(NetImportResponse importResponse)
{
_importResponse = importResponse;
@ -27,41 +25,23 @@ namespace NzbDrone.Core.NetImport.Trakt
return movies;
}
if (_settings.TraktListType == (int)TraktListType.Popular)
{
var jsonResponse = JsonConvert.DeserializeObject<List<Movie>>(_importResponse.Content);
var jsonResponse = JsonConvert.DeserializeObject<List<TraktResponse>>(_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<List<TraktResponse>>(_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;

View File

@ -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<NetImportRequest> 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;
}
}
}

View File

@ -1,4 +1,4 @@
using System;
using System;
using System.Text.RegularExpressions;
using FluentValidation;
using NzbDrone.Common.Extensions;
@ -8,27 +8,16 @@ using NzbDrone.Core.Validation;
namespace NzbDrone.Core.NetImport.Trakt
{
public class TraktSettingsValidator : AbstractValidator<TraktSettings>
public class TraktSettingsBaseValidator<TSettings> : AbstractValidator<TSettings>
where TSettings : TraktSettingsBase<TSettings>
{
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 @@ namespace NzbDrone.Core.NetImport.Trakt
}
}
public class TraktSettings : IProviderConfig
public class TraktSettingsBase<TSettings> : IProviderConfig
where TSettings : TraktSettingsBase<TSettings>
{
private static readonly TraktSettingsValidator Validator = new TraktSettingsValidator();
protected virtual AbstractValidator<TSettings> Validator => new TraktSettingsBaseValidator<TSettings>();
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 @@ namespace NzbDrone.Core.NetImport.Trakt
[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 @@ namespace NzbDrone.Core.NetImport.Trakt
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
return new NzbDroneValidationResult(Validator.Validate((TSettings)this));
}
}
}

View File

@ -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<TraktUserSettings>
{
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
};
}
}
}

View File

@ -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
}
}

View File

@ -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<NetImportRequest> 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;
}
}
}

View File

@ -0,0 +1,28 @@
using FluentValidation;
using NzbDrone.Core.Annotations;
namespace NzbDrone.Core.NetImport.Trakt.User
{
public class TraktUserSettingsValidator : TraktSettingsBaseValidator<TraktUserSettings>
{
public TraktUserSettingsValidator()
: base()
{
RuleFor(c => c.TraktListType).NotNull();
RuleFor(c => c.AuthUser).NotEmpty();
}
}
public class TraktUserSettings : TraktSettingsBase<TraktUserSettings>
{
protected override AbstractValidator<TraktUserSettings> 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; }
}
}