From 00f631c623e08699ed1c8dbb7e5566a1ab5c4152 Mon Sep 17 00:00:00 2001 From: Qstick Date: Sat, 8 Aug 2020 23:55:55 -0400 Subject: [PATCH] New: Trakt connection for adding movies to collection --- .../TraktTests/TraktServiceFixture.cs | 97 +++++++ .../TraktSettingsValidatorFixture.cs | 62 +++++ .../Extras/Files/ExtraFileRepository.cs | 1 - .../RadarrList/RadarrListRequestGenerator.cs | 4 +- .../NetImport/Trakt/List/TraktListImport.cs | 9 +- .../Trakt/List/TraktListRequestGenerator.cs | 21 +- .../Trakt/Popular/TraktPopularImport.cs | 19 +- .../Trakt/Popular/TraktPopularParser.cs | 3 +- .../Popular/TraktPopularRequestGenerator.cs | 36 +-- src/NzbDrone.Core/NetImport/Trakt/TraktAPI.cs | 59 ----- .../NetImport/Trakt/TraktImportBase.cs | 68 +---- .../NetImport/Trakt/TraktParser.cs | 3 +- .../NetImport/Trakt/User/TraktUserImport.cs | 9 +- .../Trakt/User/TraktUserRequestGenerator.cs | 26 +- .../Notifications/NotificationRepository.cs | 8 +- .../Resource/TraktAuthRefreshResource.cs | 17 ++ .../Resource/TraktCollectMovieResource.cs | 17 ++ .../Resource/TraktCollectMoviesResource.cs | 9 + .../Trakt/Resource/TraktListResource.cs | 24 ++ .../Trakt/Resource/TraktMovieIdsResource.cs | 10 + .../Trakt/Resource/TraktMovieResource.cs | 9 + .../Trakt/Resource/TraktUserIdsResource.cs | 7 + .../Trakt/Resource/TraktUserResource.cs | 10 + .../Resource/TraktUserSettingsResource.cs | 7 + .../Notifications/Trakt/Trakt.cs | 95 +++++++ .../Notifications/Trakt/TraktException.cs | 18 ++ .../Notifications/Trakt/TraktProxy.cs | 114 ++++++++ .../Notifications/Trakt/TraktService.cs | 250 ++++++++++++++++++ .../Notifications/Trakt/TraktSettings.cs | 48 ++++ 29 files changed, 869 insertions(+), 191 deletions(-) create mode 100644 src/NzbDrone.Core.Test/NotificationTests/TraktTests/TraktServiceFixture.cs create mode 100644 src/NzbDrone.Core.Test/NotificationTests/TraktTests/TraktSettingsValidatorFixture.cs delete mode 100644 src/NzbDrone.Core/NetImport/Trakt/TraktAPI.cs create mode 100644 src/NzbDrone.Core/Notifications/Trakt/Resource/TraktAuthRefreshResource.cs create mode 100644 src/NzbDrone.Core/Notifications/Trakt/Resource/TraktCollectMovieResource.cs create mode 100644 src/NzbDrone.Core/Notifications/Trakt/Resource/TraktCollectMoviesResource.cs create mode 100644 src/NzbDrone.Core/Notifications/Trakt/Resource/TraktListResource.cs create mode 100644 src/NzbDrone.Core/Notifications/Trakt/Resource/TraktMovieIdsResource.cs create mode 100644 src/NzbDrone.Core/Notifications/Trakt/Resource/TraktMovieResource.cs create mode 100644 src/NzbDrone.Core/Notifications/Trakt/Resource/TraktUserIdsResource.cs create mode 100644 src/NzbDrone.Core/Notifications/Trakt/Resource/TraktUserResource.cs create mode 100644 src/NzbDrone.Core/Notifications/Trakt/Resource/TraktUserSettingsResource.cs create mode 100644 src/NzbDrone.Core/Notifications/Trakt/Trakt.cs create mode 100644 src/NzbDrone.Core/Notifications/Trakt/TraktException.cs create mode 100644 src/NzbDrone.Core/Notifications/Trakt/TraktProxy.cs create mode 100644 src/NzbDrone.Core/Notifications/Trakt/TraktService.cs create mode 100644 src/NzbDrone.Core/Notifications/Trakt/TraktSettings.cs diff --git a/src/NzbDrone.Core.Test/NotificationTests/TraktTests/TraktServiceFixture.cs b/src/NzbDrone.Core.Test/NotificationTests/TraktTests/TraktServiceFixture.cs new file mode 100644 index 000000000..a6503b5ec --- /dev/null +++ b/src/NzbDrone.Core.Test/NotificationTests/TraktTests/TraktServiceFixture.cs @@ -0,0 +1,97 @@ +using System.Linq; +using Moq; +using NUnit.Framework; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.MediaFiles.MediaInfo; +using NzbDrone.Core.Movies; +using NzbDrone.Core.Notifications; +using NzbDrone.Core.Notifications.Trakt; +using NzbDrone.Core.Notifications.Trakt.Resource; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.NotificationTests +{ + [TestFixture] + public class TraktServiceFixture : CoreTest + { + private DownloadMessage _downloadMessage; + private TraktSettings _traktSettings; + + [SetUp] + public void Setup() + { + _downloadMessage = new DownloadMessage + { + Movie = new Movie(), + MovieFile = new MovieFile + { + MediaInfo = null, + Quality = new QualityModel + { + Quality = Quality.Unknown + } + } + }; + + _traktSettings = new TraktSettings + { + AccessToken = "", + RefreshToken = "" + }; + } + + private void GiventValidMediaInfo(Quality quality, string audioChannels, string audioFormat, string scanType) + { + _downloadMessage.MovieFile.MediaInfo = new MediaInfoModel + { + AudioChannelPositions = audioChannels, + AudioFormat = audioFormat, + ScanType = scanType + }; + + _downloadMessage.MovieFile.Quality.Quality = quality; + } + + [Test] + public void should_add_collection_movie_if_null_mediainfo() + { + Subject.AddMovieToCollection(_traktSettings, _downloadMessage.Movie, _downloadMessage.MovieFile); + + Mocker.GetMock() + .Verify(v => v.AddToCollection(It.IsAny(), It.IsAny()), Times.Once()); + } + + [Test] + public void should_add_collection_movie_if_valid_mediainfo() + { + GiventValidMediaInfo(Quality.Bluray1080p, "3/2/0.1", "DTS", "Interlaced"); + + Subject.AddMovieToCollection(_traktSettings, _downloadMessage.Movie, _downloadMessage.MovieFile); + + Mocker.GetMock() + .Verify(v => v.AddToCollection(It.Is(t => + t.Movies.First().Audio == "dts" && + t.Movies.First().AudioChannels == "5.1" && + t.Movies.First().Resolution == "hd_1080i" && + t.Movies.First().MediaType == "bluray"), + It.IsAny()), Times.Once()); + } + + [Test] + public void should_format_audio_channels_to_one_decimal_when_adding_collection_movie() + { + GiventValidMediaInfo(Quality.Bluray1080p, "2/0/0", "DTS", "Interlaced"); + + Subject.AddMovieToCollection(_traktSettings, _downloadMessage.Movie, _downloadMessage.MovieFile); + + Mocker.GetMock() + .Verify(v => v.AddToCollection(It.Is(t => + t.Movies.First().Audio == "dts" && + t.Movies.First().AudioChannels == "2.0" && + t.Movies.First().Resolution == "hd_1080i" && + t.Movies.First().MediaType == "bluray"), + It.IsAny()), Times.Once()); + } + } +} diff --git a/src/NzbDrone.Core.Test/NotificationTests/TraktTests/TraktSettingsValidatorFixture.cs b/src/NzbDrone.Core.Test/NotificationTests/TraktTests/TraktSettingsValidatorFixture.cs new file mode 100644 index 000000000..5baf8ab46 --- /dev/null +++ b/src/NzbDrone.Core.Test/NotificationTests/TraktTests/TraktSettingsValidatorFixture.cs @@ -0,0 +1,62 @@ +using System; +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Notifications.Trakt; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.NotificationTests.TraktTests +{ + [TestFixture] + public class TraktSettingsValidatorFixture : CoreTest + { + private TraktSettings _traktSettings; + private TestValidator _validator; + + [SetUp] + public void Setup() + { + _validator = new TestValidator + { + v => v.RuleFor(s => s).SetValidator(Subject) + }; + + _traktSettings = Builder.CreateNew() + .With(s => s.AccessToken = "sometoken") + .With(s => s.RefreshToken = "sometoken") + .With(s => s.Expires = DateTime.Now.AddDays(2)) + .Build(); + } + + [Test] + public void should_be_valid_if_all_settings_valid() + { + _validator.Validate(_traktSettings).IsValid.Should().BeTrue(); + } + + [Test] + public void should_not_be_valid_if_port_is_out_of_range() + { + _traktSettings.AccessToken = ""; + + _validator.Validate(_traktSettings).IsValid.Should().BeFalse(); + } + + [Test] + public void should_not_be_valid_if_server_is_empty() + { + _traktSettings.RefreshToken = ""; + + _validator.Validate(_traktSettings).IsValid.Should().BeFalse(); + } + + [Test] + public void should_not_be_valid_if_from_is_empty() + { + _traktSettings.Expires = default(DateTime); + + _validator.Validate(_traktSettings).IsValid.Should().BeFalse(); + } + } +} diff --git a/src/NzbDrone.Core/Extras/Files/ExtraFileRepository.cs b/src/NzbDrone.Core/Extras/Files/ExtraFileRepository.cs index e3390bc1c..36a7bddc6 100644 --- a/src/NzbDrone.Core/Extras/Files/ExtraFileRepository.cs +++ b/src/NzbDrone.Core/Extras/Files/ExtraFileRepository.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Linq; using NzbDrone.Core.Datastore; using NzbDrone.Core.Messaging.Events; diff --git a/src/NzbDrone.Core/NetImport/RadarrList/RadarrListRequestGenerator.cs b/src/NzbDrone.Core/NetImport/RadarrList/RadarrListRequestGenerator.cs index 4ea030688..18643db27 100644 --- a/src/NzbDrone.Core/NetImport/RadarrList/RadarrListRequestGenerator.cs +++ b/src/NzbDrone.Core/NetImport/RadarrList/RadarrListRequestGenerator.cs @@ -1,7 +1,5 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using NLog; -using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; namespace NzbDrone.Core.NetImport.RadarrList diff --git a/src/NzbDrone.Core/NetImport/Trakt/List/TraktListImport.cs b/src/NzbDrone.Core/NetImport/Trakt/List/TraktListImport.cs index 690c97dbe..958febba5 100644 --- a/src/NzbDrone.Core/NetImport/Trakt/List/TraktListImport.cs +++ b/src/NzbDrone.Core/NetImport/Trakt/List/TraktListImport.cs @@ -1,6 +1,7 @@ using NLog; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Notifications.Trakt; using NzbDrone.Core.Parser; namespace NzbDrone.Core.NetImport.Trakt.List @@ -8,12 +9,13 @@ namespace NzbDrone.Core.NetImport.Trakt.List public class TraktListImport : TraktImportBase { public TraktListImport(INetImportRepository netImportRepository, + ITraktProxy traktProxy, IHttpClient httpClient, INetImportStatusService netImportStatusService, IConfigService configService, IParsingService parsingService, Logger logger) - : base(netImportRepository, httpClient, netImportStatusService, configService, parsingService, logger) + : base(netImportRepository, traktProxy, httpClient, netImportStatusService, configService, parsingService, logger) { } @@ -23,10 +25,9 @@ public TraktListImport(INetImportRepository netImportRepository, public override INetImportRequestGenerator GetRequestGenerator() { - return new TraktListRequestGenerator() + return new TraktListRequestGenerator(_traktProxy) { - Settings = Settings, - ClientId = ClientId + Settings = Settings }; } } diff --git a/src/NzbDrone.Core/NetImport/Trakt/List/TraktListRequestGenerator.cs b/src/NzbDrone.Core/NetImport/Trakt/List/TraktListRequestGenerator.cs index ab80c3829..b5d8a94cb 100644 --- a/src/NzbDrone.Core/NetImport/Trakt/List/TraktListRequestGenerator.cs +++ b/src/NzbDrone.Core/NetImport/Trakt/List/TraktListRequestGenerator.cs @@ -1,16 +1,17 @@ using System.Collections.Generic; -using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; +using NzbDrone.Core.Notifications.Trakt; namespace NzbDrone.Core.NetImport.Trakt.List { public class TraktListRequestGenerator : INetImportRequestGenerator { + private readonly ITraktProxy _traktProxy; public TraktListSettings Settings { get; set; } - public string ClientId { get; set; } - public TraktListRequestGenerator() + public TraktListRequestGenerator(ITraktProxy traktProxy) { + _traktProxy = traktProxy; } public virtual NetImportPageableRequestChain GetMovies() @@ -24,20 +25,12 @@ public virtual NetImportPageableRequestChain GetMovies() private IEnumerable GetMoviesRequest() { - var link = Settings.Link.Trim(); + var link = string.Empty; var listName = Parser.Parser.ToUrlSlug(Settings.Listname.Trim()); - link += $"/users/{Settings.Username.Trim()}/lists/{listName}/items/movies?limit={Settings.Limit}"; + 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", ClientId); //aeon - - if (Settings.AccessToken.IsNotNullOrWhiteSpace()) - { - request.HttpRequest.Headers.Add("Authorization", "Bearer " + Settings.AccessToken); - } + var request = new NetImportRequest(_traktProxy.BuildTraktRequest(link, HttpMethod.GET, Settings.AccessToken)); yield return request; } diff --git a/src/NzbDrone.Core/NetImport/Trakt/Popular/TraktPopularImport.cs b/src/NzbDrone.Core/NetImport/Trakt/Popular/TraktPopularImport.cs index b1bde3f41..b552ee374 100644 --- a/src/NzbDrone.Core/NetImport/Trakt/Popular/TraktPopularImport.cs +++ b/src/NzbDrone.Core/NetImport/Trakt/Popular/TraktPopularImport.cs @@ -1,6 +1,7 @@ using NLog; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Notifications.Trakt; using NzbDrone.Core.Parser; namespace NzbDrone.Core.NetImport.Trakt.Popular @@ -8,12 +9,13 @@ namespace NzbDrone.Core.NetImport.Trakt.Popular public class TraktPopularImport : TraktImportBase { public TraktPopularImport(INetImportRepository netImportRepository, - IHttpClient httpClient, - INetImportStatusService netImportStatusService, - IConfigService configService, - IParsingService parsingService, - Logger logger) - : base(netImportRepository, httpClient, netImportStatusService, configService, parsingService, logger) + ITraktProxy traktProxy, + IHttpClient httpClient, + INetImportStatusService netImportStatusService, + IConfigService configService, + IParsingService parsingService, + Logger logger) + : base(netImportRepository, traktProxy, httpClient, netImportStatusService, configService, parsingService, logger) { } @@ -28,10 +30,9 @@ public override IParseNetImportResponse GetParser() public override INetImportRequestGenerator GetRequestGenerator() { - return new TraktPopularRequestGenerator() + return new TraktPopularRequestGenerator(_traktProxy) { - Settings = Settings, - ClientId = ClientId + Settings = Settings }; } } diff --git a/src/NzbDrone.Core/NetImport/Trakt/Popular/TraktPopularParser.cs b/src/NzbDrone.Core/NetImport/Trakt/Popular/TraktPopularParser.cs index 905633bd3..da563f26d 100644 --- a/src/NzbDrone.Core/NetImport/Trakt/Popular/TraktPopularParser.cs +++ b/src/NzbDrone.Core/NetImport/Trakt/Popular/TraktPopularParser.cs @@ -2,6 +2,7 @@ using Newtonsoft.Json; using NzbDrone.Common.Extensions; using NzbDrone.Core.Movies; +using NzbDrone.Core.Notifications.Trakt.Resource; namespace NzbDrone.Core.NetImport.Trakt.Popular { @@ -34,7 +35,7 @@ public override IList ParseResponse(NetImportResponse importResponse) } else { - jsonResponse = JsonConvert.DeserializeObject>(_importResponse.Content).SelectList(c => c.Movie); + jsonResponse = JsonConvert.DeserializeObject>(_importResponse.Content).SelectList(c => c.Movie); } // no movies were return diff --git a/src/NzbDrone.Core/NetImport/Trakt/Popular/TraktPopularRequestGenerator.cs b/src/NzbDrone.Core/NetImport/Trakt/Popular/TraktPopularRequestGenerator.cs index 2a547b246..aa10daee9 100644 --- a/src/NzbDrone.Core/NetImport/Trakt/Popular/TraktPopularRequestGenerator.cs +++ b/src/NzbDrone.Core/NetImport/Trakt/Popular/TraktPopularRequestGenerator.cs @@ -1,17 +1,17 @@ using System.Collections.Generic; -using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; +using NzbDrone.Core.Notifications.Trakt; namespace NzbDrone.Core.NetImport.Trakt.Popular { public class TraktPopularRequestGenerator : INetImportRequestGenerator { + private readonly ITraktProxy _traktProxy; public TraktPopularSettings Settings { get; set; } - public string ClientId { get; set; } - - public TraktPopularRequestGenerator() + public TraktPopularRequestGenerator(ITraktProxy traktProxy) { + _traktProxy = traktProxy; } public virtual NetImportPageableRequestChain GetMovies() @@ -25,47 +25,39 @@ public virtual NetImportPageableRequestChain GetMovies() private IEnumerable GetMoviesRequest() { - var link = Settings.Link.Trim(); + var link = string.Empty; 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; + link += "movies/trending" + filtersAndLimit; break; case (int)TraktPopularListType.Popular: - link += "/movies/popular" + filtersAndLimit; + link += "movies/popular" + filtersAndLimit; break; case (int)TraktPopularListType.Anticipated: - link += "/movies/anticipated" + filtersAndLimit; + link += "movies/anticipated" + filtersAndLimit; break; case (int)TraktPopularListType.BoxOffice: - link += "/movies/boxoffice" + filtersAndLimit; + link += "movies/boxoffice" + filtersAndLimit; break; case (int)TraktPopularListType.TopWatchedByWeek: - link += "/movies/watched/weekly" + filtersAndLimit; + link += "movies/watched/weekly" + filtersAndLimit; break; case (int)TraktPopularListType.TopWatchedByMonth: - link += "/movies/watched/monthly" + filtersAndLimit; + link += "movies/watched/monthly" + filtersAndLimit; break; case (int)TraktPopularListType.TopWatchedByYear: - link += "/movies/watched/yearly" + filtersAndLimit; + link += "movies/watched/yearly" + filtersAndLimit; break; case (int)TraktPopularListType.TopWatchedByAllTime: - link += "/movies/watched/all" + filtersAndLimit; + 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", ClientId); //aeon - - if (Settings.AccessToken.IsNotNullOrWhiteSpace()) - { - request.HttpRequest.Headers.Add("Authorization", "Bearer " + Settings.AccessToken); - } + var request = new NetImportRequest(_traktProxy.BuildTraktRequest(link, HttpMethod.GET, Settings.AccessToken)); yield return request; } diff --git a/src/NzbDrone.Core/NetImport/Trakt/TraktAPI.cs b/src/NzbDrone.Core/NetImport/Trakt/TraktAPI.cs deleted file mode 100644 index e8363c097..000000000 --- a/src/NzbDrone.Core/NetImport/Trakt/TraktAPI.cs +++ /dev/null @@ -1,59 +0,0 @@ -namespace NzbDrone.Core.NetImport.Trakt -{ - public class TraktMovieIdsResource - { - public int Trakt { get; set; } - public string Slug { get; set; } - public string Imdb { get; set; } - public int Tmdb { get; set; } - } - - public class TraktMovieResource - { - 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? Watchers { 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 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; } - public TraktUserIdsResource Ids { get; set; } - } - - public class TraktUserIdsResource - { - public string Slug { get; set; } - } -} diff --git a/src/NzbDrone.Core/NetImport/Trakt/TraktImportBase.cs b/src/NzbDrone.Core/NetImport/Trakt/TraktImportBase.cs index a339ed6de..77f27f79d 100644 --- a/src/NzbDrone.Core/NetImport/Trakt/TraktImportBase.cs +++ b/src/NzbDrone.Core/NetImport/Trakt/TraktImportBase.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; using NLog; -using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Notifications.Trakt; using NzbDrone.Core.Parser; using NzbDrone.Core.Validation; @@ -12,16 +12,12 @@ namespace NzbDrone.Core.NetImport.Trakt public abstract class TraktImportBase : HttpNetImportBase where TSettings : TraktSettingsBase, new() { + public ITraktProxy _traktProxy; + private readonly INetImportRepository _netImportRepository; public override NetImportType ListType => NetImportType.Trakt; - public const string OAuthUrl = "https://api.trakt.tv/oauth/authorize"; - public const string RedirectUri = "https://auth.servarr.com/v1/trakt/auth"; - public const string RenewUri = "https://auth.servarr.com/v1/trakt/renew"; - public const string ClientId = "64508a8bf370cee550dde4806469922fd7cd70afb2d5690e3ee7f75ae784b70e"; - - private INetImportRepository _netImportRepository; - protected TraktImportBase(INetImportRepository netImportRepository, + ITraktProxy traktProxy, IHttpClient httpClient, INetImportStatusService netImportStatusService, IConfigService configService, @@ -30,6 +26,7 @@ protected TraktImportBase(INetImportRepository netImportRepository, : base(httpClient, netImportStatusService, configService, parsingService, logger) { _netImportRepository = netImportRepository; + _traktProxy = traktProxy; } public override NetImportFetchResult Fetch() @@ -55,12 +52,7 @@ public override object RequestAction(string action, IDictionary { if (action == "startOAuth") { - var request = new HttpRequestBuilder(OAuthUrl) - .AddQueryParam("client_id", ClientId) - .AddQueryParam("response_type", "code") - .AddQueryParam("redirect_uri", RedirectUri) - .AddQueryParam("state", query["callbackUrl"]) - .Build(); + var request = _traktProxy.GetOAuthRequest(query["callbackUrl"]); return new { @@ -74,63 +66,29 @@ public override object RequestAction(string action, IDictionary accessToken = query["access_token"], expires = DateTime.UtcNow.AddSeconds(int.Parse(query["expires_in"])), refreshToken = query["refresh_token"], - authUser = GetUserName(query["access_token"]) + authUser = _traktProxy.GetUserName(query["access_token"]) }; } 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", ClientId); - - if (accessToken.IsNotNullOrWhiteSpace()) - { - request.Headers.Add("Authorization", "Bearer " + accessToken); - } - - try - { - var response = _httpClient.Get(request); - - if (response != null && response.Resource != null) - { - return response.Resource.User.Ids.Slug; - } - } - catch (HttpException) - { - _logger.Warn($"Error refreshing trakt access token"); - } - - return null; - } - private void RefreshToken() { _logger.Trace("Refreshing Token"); Settings.Validate().Filter("RefreshToken").ThrowOnError(); - var request = new HttpRequestBuilder(RenewUri) - .AddQueryParam("refresh_token", Settings.RefreshToken) - .Build(); - try { - var response = _httpClient.Get(request); + var response = _traktProxy.RefreshAuthToken(Settings.RefreshToken); - if (response != null && response.Resource != null) + if (response != 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; + var token = response; + Settings.AccessToken = token.AccessToken; + Settings.Expires = DateTime.UtcNow.AddSeconds(token.ExpiresIn); + Settings.RefreshToken = token.RefreshToken ?? Settings.RefreshToken; if (Definition.Id > 0) { diff --git a/src/NzbDrone.Core/NetImport/Trakt/TraktParser.cs b/src/NzbDrone.Core/NetImport/Trakt/TraktParser.cs index 370232626..c7dc6ffdf 100644 --- a/src/NzbDrone.Core/NetImport/Trakt/TraktParser.cs +++ b/src/NzbDrone.Core/NetImport/Trakt/TraktParser.cs @@ -3,6 +3,7 @@ using Newtonsoft.Json; using NzbDrone.Common.Extensions; using NzbDrone.Core.NetImport.Exceptions; +using NzbDrone.Core.Notifications.Trakt.Resource; namespace NzbDrone.Core.NetImport.Trakt { @@ -25,7 +26,7 @@ public TraktParser() return movies; } - var jsonResponse = JsonConvert.DeserializeObject>(_importResponse.Content); + var jsonResponse = JsonConvert.DeserializeObject>(_importResponse.Content); // no movies were return if (jsonResponse == null) diff --git a/src/NzbDrone.Core/NetImport/Trakt/User/TraktUserImport.cs b/src/NzbDrone.Core/NetImport/Trakt/User/TraktUserImport.cs index 687391083..608aebc3c 100644 --- a/src/NzbDrone.Core/NetImport/Trakt/User/TraktUserImport.cs +++ b/src/NzbDrone.Core/NetImport/Trakt/User/TraktUserImport.cs @@ -1,6 +1,7 @@ using NLog; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Notifications.Trakt; using NzbDrone.Core.Parser; namespace NzbDrone.Core.NetImport.Trakt.User @@ -8,12 +9,13 @@ namespace NzbDrone.Core.NetImport.Trakt.User public class TraktUserImport : TraktImportBase { public TraktUserImport(INetImportRepository netImportRepository, + ITraktProxy traktProxy, IHttpClient httpClient, INetImportStatusService netImportStatusService, IConfigService configService, IParsingService parsingService, Logger logger) - : base(netImportRepository, httpClient, netImportStatusService, configService, parsingService, logger) + : base(netImportRepository, traktProxy, httpClient, netImportStatusService, configService, parsingService, logger) { } @@ -23,10 +25,9 @@ public TraktUserImport(INetImportRepository netImportRepository, public override INetImportRequestGenerator GetRequestGenerator() { - return new TraktUserRequestGenerator() + return new TraktUserRequestGenerator(_traktProxy) { - Settings = Settings, - ClientId = ClientId + Settings = Settings }; } } diff --git a/src/NzbDrone.Core/NetImport/Trakt/User/TraktUserRequestGenerator.cs b/src/NzbDrone.Core/NetImport/Trakt/User/TraktUserRequestGenerator.cs index 3d042dc89..e3d664788 100644 --- a/src/NzbDrone.Core/NetImport/Trakt/User/TraktUserRequestGenerator.cs +++ b/src/NzbDrone.Core/NetImport/Trakt/User/TraktUserRequestGenerator.cs @@ -1,17 +1,17 @@ using System.Collections.Generic; -using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; +using NzbDrone.Core.Notifications.Trakt; namespace NzbDrone.Core.NetImport.Trakt.User { public class TraktUserRequestGenerator : INetImportRequestGenerator { + private readonly ITraktProxy _traktProxy; public TraktUserSettings Settings { get; set; } - public string ClientId { get; set; } - - public TraktUserRequestGenerator() + public TraktUserRequestGenerator(ITraktProxy traktProxy) { + _traktProxy = traktProxy; } public virtual NetImportPageableRequestChain GetMovies() @@ -25,30 +25,22 @@ public virtual NetImportPageableRequestChain GetMovies() private IEnumerable GetMoviesRequest() { - var link = Settings.Link.Trim(); + var link = string.Empty; switch (Settings.TraktListType) { case (int)TraktUserListType.UserWatchList: - link += $"/users/{Settings.AuthUser.Trim()}/watchlist/movies?limit={Settings.Limit}"; + 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}"; + 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}"; + 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", ClientId); - - if (Settings.AccessToken.IsNotNullOrWhiteSpace()) - { - request.HttpRequest.Headers.Add("Authorization", "Bearer " + Settings.AccessToken); - } + var request = new NetImportRequest(_traktProxy.BuildTraktRequest(link, HttpMethod.GET, Settings.AccessToken)); yield return request; } diff --git a/src/NzbDrone.Core/Notifications/NotificationRepository.cs b/src/NzbDrone.Core/Notifications/NotificationRepository.cs index ccdaaf541..96ff55b52 100644 --- a/src/NzbDrone.Core/Notifications/NotificationRepository.cs +++ b/src/NzbDrone.Core/Notifications/NotificationRepository.cs @@ -1,4 +1,4 @@ -using NzbDrone.Core.Datastore; +using NzbDrone.Core.Datastore; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.ThingiProvider; @@ -6,6 +6,7 @@ namespace NzbDrone.Core.Notifications { public interface INotificationRepository : IProviderRepository { + void UpdateSettings(NotificationDefinition model); } public class NotificationRepository : ProviderRepository, INotificationRepository @@ -14,5 +15,10 @@ public NotificationRepository(IMainDatabase database, IEventAggregator eventAggr : base(database, eventAggregator) { } + + public void UpdateSettings(NotificationDefinition model) + { + SetFields(model, m => m.Settings); + } } } diff --git a/src/NzbDrone.Core/Notifications/Trakt/Resource/TraktAuthRefreshResource.cs b/src/NzbDrone.Core/Notifications/Trakt/Resource/TraktAuthRefreshResource.cs new file mode 100644 index 000000000..e1583c17d --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Trakt/Resource/TraktAuthRefreshResource.cs @@ -0,0 +1,17 @@ +using Newtonsoft.Json; + +namespace NzbDrone.Core.Notifications.Trakt.Resource +{ + public class TraktAuthRefreshResource + { + [JsonProperty(PropertyName = "access_token")] + public string AccessToken { get; set; } + [JsonProperty(PropertyName = "token_type")] + public string TokenType { get; set; } + [JsonProperty(PropertyName = "expires_in")] + public int ExpiresIn { get; set; } + [JsonProperty(PropertyName = "refresh_token")] + public string RefreshToken { get; set; } + public string Scope { get; set; } + } +} diff --git a/src/NzbDrone.Core/Notifications/Trakt/Resource/TraktCollectMovieResource.cs b/src/NzbDrone.Core/Notifications/Trakt/Resource/TraktCollectMovieResource.cs new file mode 100644 index 000000000..01b7aa64c --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Trakt/Resource/TraktCollectMovieResource.cs @@ -0,0 +1,17 @@ +using System; +using Newtonsoft.Json; + +namespace NzbDrone.Core.Notifications.Trakt.Resource +{ + public class TraktCollectMovie : TraktMovieResource + { + [JsonProperty(PropertyName = "collected_at")] + public DateTime CollectedAt { get; set; } + public string Resolution { get; set; } + [JsonProperty(PropertyName = "audio_channels")] + public string AudioChannels { get; set; } + public string Audio { get; set; } + [JsonProperty(PropertyName = "media_type")] + public string MediaType { get; set; } + } +} diff --git a/src/NzbDrone.Core/Notifications/Trakt/Resource/TraktCollectMoviesResource.cs b/src/NzbDrone.Core/Notifications/Trakt/Resource/TraktCollectMoviesResource.cs new file mode 100644 index 000000000..50fb40e20 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Trakt/Resource/TraktCollectMoviesResource.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace NzbDrone.Core.Notifications.Trakt.Resource +{ + public class TraktCollectMoviesResource + { + public List Movies { get; set; } + } +} diff --git a/src/NzbDrone.Core/Notifications/Trakt/Resource/TraktListResource.cs b/src/NzbDrone.Core/Notifications/Trakt/Resource/TraktListResource.cs new file mode 100644 index 000000000..99b1d2792 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Trakt/Resource/TraktListResource.cs @@ -0,0 +1,24 @@ +using Newtonsoft.Json; + +namespace NzbDrone.Core.Notifications.Trakt.Resource +{ + public class TraktListResource + { + public int? Rank { get; set; } + [JsonProperty(PropertyName = "listed_at")] + public string ListedAt { get; set; } + public string Type { get; set; } + + public int? Watchers { get; set; } + + public long? Revenue { get; set; } + [JsonProperty(PropertyName = "watcher_count")] + public long? WatcherCount { get; set; } + [JsonProperty(PropertyName = "play_count")] + public long? PlayCount { get; set; } + [JsonProperty(PropertyName = "collected_count")] + public long? CollectedCount { get; set; } + + public TraktMovieResource Movie { get; set; } + } +} diff --git a/src/NzbDrone.Core/Notifications/Trakt/Resource/TraktMovieIdsResource.cs b/src/NzbDrone.Core/Notifications/Trakt/Resource/TraktMovieIdsResource.cs new file mode 100644 index 000000000..078ff717d --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Trakt/Resource/TraktMovieIdsResource.cs @@ -0,0 +1,10 @@ +namespace NzbDrone.Core.Notifications.Trakt.Resource +{ + public class TraktMovieIdsResource + { + public int Trakt { get; set; } + public string Slug { get; set; } + public string Imdb { get; set; } + public int Tmdb { get; set; } + } +} diff --git a/src/NzbDrone.Core/Notifications/Trakt/Resource/TraktMovieResource.cs b/src/NzbDrone.Core/Notifications/Trakt/Resource/TraktMovieResource.cs new file mode 100644 index 000000000..fd202ba58 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Trakt/Resource/TraktMovieResource.cs @@ -0,0 +1,9 @@ +namespace NzbDrone.Core.Notifications.Trakt.Resource +{ + public class TraktMovieResource + { + public string Title { get; set; } + public int? Year { get; set; } + public TraktMovieIdsResource Ids { get; set; } + } +} diff --git a/src/NzbDrone.Core/Notifications/Trakt/Resource/TraktUserIdsResource.cs b/src/NzbDrone.Core/Notifications/Trakt/Resource/TraktUserIdsResource.cs new file mode 100644 index 000000000..3890ae8e1 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Trakt/Resource/TraktUserIdsResource.cs @@ -0,0 +1,7 @@ +namespace NzbDrone.Core.Notifications.Trakt.Resource +{ + public class TraktUserIdsResource + { + public string Slug { get; set; } + } +} diff --git a/src/NzbDrone.Core/Notifications/Trakt/Resource/TraktUserResource.cs b/src/NzbDrone.Core/Notifications/Trakt/Resource/TraktUserResource.cs new file mode 100644 index 000000000..21bf61e2a --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Trakt/Resource/TraktUserResource.cs @@ -0,0 +1,10 @@ +using NzbDrone.Core.Notifications.Trakt.Resource; + +namespace NzbDrone.Core.Notifications.Trakt +{ + public class TraktUserResource + { + public string Username { get; set; } + public TraktUserIdsResource Ids { get; set; } + } +} diff --git a/src/NzbDrone.Core/Notifications/Trakt/Resource/TraktUserSettingsResource.cs b/src/NzbDrone.Core/Notifications/Trakt/Resource/TraktUserSettingsResource.cs new file mode 100644 index 000000000..8ef53f7f2 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Trakt/Resource/TraktUserSettingsResource.cs @@ -0,0 +1,7 @@ +namespace NzbDrone.Core.Notifications.Trakt.Resource +{ + public class TraktUserSettingsResource + { + public TraktUserResource User { get; set; } + } +} diff --git a/src/NzbDrone.Core/Notifications/Trakt/Trakt.cs b/src/NzbDrone.Core/Notifications/Trakt/Trakt.cs new file mode 100644 index 000000000..e846bb676 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Trakt/Trakt.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using FluentValidation.Results; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.Notifications.Trakt +{ + public class Trakt : NotificationBase + { + private readonly ITraktService _traktService; + private readonly INotificationRepository _notificationRepository; + private readonly Logger _logger; + + public Trakt(ITraktService traktService, INotificationRepository notificationRepository, Logger logger) + { + _traktService = traktService; + _notificationRepository = notificationRepository; + _logger = logger; + } + + public override string Link => "https://trakt.tv/"; + public override string Name => "Trakt"; + + public override void OnDownload(DownloadMessage message) + { + _traktService.AddMovieToCollection(Settings, message.Movie, message.MovieFile); + } + + public override ValidationResult Test() + { + var failures = new List(); + + failures.AddIfNotNull(_traktService.Test(Settings)); + + return new ValidationResult(failures); + } + + public override object RequestAction(string action, IDictionary query) + { + if (action == "startOAuth") + { + var request = _traktService.GetOAuthRequest(query["callbackUrl"]); + + return new + { + OauthUrl = request.Url.ToString() + }; + } + else if (action == "getOAuthToken") + { + return new + { + accessToken = query["access_token"], + expires = DateTime.UtcNow.AddSeconds(int.Parse(query["expires_in"])), + refreshToken = query["refresh_token"], + authUser = _traktService.GetUserName(query["access_token"]) + }; + } + + return new { }; + } + + public void RefreshToken() + { + _logger.Trace("Refreshing Token"); + + Settings.Validate().Filter("RefreshToken").ThrowOnError(); + + try + { + var response = _traktService.RefreshAuthToken(Settings.RefreshToken); + + if (response != null) + { + var token = response; + Settings.AccessToken = token.AccessToken; + Settings.Expires = DateTime.UtcNow.AddSeconds(token.ExpiresIn); + Settings.RefreshToken = token.RefreshToken ?? Settings.RefreshToken; + + if (Definition.Id > 0) + { + _notificationRepository.UpdateSettings((NotificationDefinition)Definition); + } + } + } + catch (HttpException) + { + _logger.Warn($"Error refreshing trakt access token"); + } + } + } +} diff --git a/src/NzbDrone.Core/Notifications/Trakt/TraktException.cs b/src/NzbDrone.Core/Notifications/Trakt/TraktException.cs new file mode 100644 index 000000000..9015d474d --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Trakt/TraktException.cs @@ -0,0 +1,18 @@ +using System; +using NzbDrone.Common.Exceptions; + +namespace NzbDrone.Core.Notifications.Trakt +{ + public class TraktException : NzbDroneException + { + public TraktException(string message) + : base(message) + { + } + + public TraktException(string message, Exception innerException, params object[] args) + : base(message, innerException, args) + { + } + } +} diff --git a/src/NzbDrone.Core/Notifications/Trakt/TraktProxy.cs b/src/NzbDrone.Core/Notifications/Trakt/TraktProxy.cs new file mode 100644 index 000000000..ed05a36ee --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Trakt/TraktProxy.cs @@ -0,0 +1,114 @@ +using System; +using System.Net; +using FluentValidation.Results; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Notifications.Trakt.Resource; + +namespace NzbDrone.Core.Notifications.Trakt +{ + public interface ITraktProxy + { + string GetUserName(string accessToken); + HttpRequest GetOAuthRequest(string callbackUrl); + TraktAuthRefreshResource RefreshAuthToken(string refreshToken); + void AddToCollection(TraktCollectMoviesResource payload, string accessToken); + HttpRequest BuildTraktRequest(string resource, HttpMethod method, string accessToken); + } + + public class TraktProxy : ITraktProxy + { + private const string URL = "https://api.trakt.tv"; + private const string OAuthUrl = "https://api.trakt.tv/oauth/authorize"; + private const string RedirectUri = "https://auth.servarr.com/v1/trakt/auth"; + private const string RenewUri = "https://auth.servarr.com/v1/trakt/renew"; + private const string ClientId = "64508a8bf370cee550dde4806469922fd7cd70afb2d5690e3ee7f75ae784b70e"; + + private readonly IHttpClient _httpClient; + private readonly Logger _logger; + + public TraktProxy(IHttpClient httpClient, Logger logger) + { + _httpClient = httpClient; + _logger = logger; + } + + public void AddToCollection(TraktCollectMoviesResource payload, string accessToken) + { + var request = BuildTraktRequest("sync/collection", HttpMethod.POST, accessToken); + + request.Headers.ContentType = "application/json"; + request.SetContent(payload.ToJson()); + + try + { + _httpClient.Execute(request); + } + catch (HttpException ex) + { + _logger.Error(ex, "Unable to post payload {0}", payload); + throw new TraktException("Unable to post payload", ex); + } + } + + public string GetUserName(string accessToken) + { + var request = BuildTraktRequest("users/settings", HttpMethod.GET, accessToken); + + try + { + var response = _httpClient.Get(request); + + if (response != null && response.Resource != null) + { + return response.Resource.User.Ids.Slug; + } + } + catch (HttpException) + { + _logger.Warn($"Error refreshing trakt access token"); + } + + return null; + } + + public HttpRequest GetOAuthRequest(string callbackUrl) + { + return new HttpRequestBuilder(OAuthUrl) + .AddQueryParam("client_id", ClientId) + .AddQueryParam("response_type", "code") + .AddQueryParam("redirect_uri", RedirectUri) + .AddQueryParam("state", callbackUrl) + .Build(); + } + + public TraktAuthRefreshResource RefreshAuthToken(string refreshToken) + { + var request = new HttpRequestBuilder(RenewUri) + .AddQueryParam("refresh_token", refreshToken) + .Build(); + + return _httpClient.Get(request)?.Resource ?? null; + } + + public HttpRequest BuildTraktRequest(string resource, HttpMethod method, string accessToken) + { + var request = new HttpRequestBuilder(URL).Resource(resource).Build(); + + request.Headers.Accept = HttpAccept.Json.Value; + request.Method = method; + + request.Headers.Add("trakt-api-version", "2"); + request.Headers.Add("trakt-api-key", ClientId); + + if (accessToken.IsNotNullOrWhiteSpace()) + { + request.Headers.Add("Authorization", "Bearer " + accessToken); + } + + return request; + } + } +} diff --git a/src/NzbDrone.Core/Notifications/Trakt/TraktService.cs b/src/NzbDrone.Core/Notifications/Trakt/TraktService.cs new file mode 100644 index 000000000..9090191d6 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Trakt/TraktService.cs @@ -0,0 +1,250 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using FluentValidation.Results; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.MediaFiles.MediaInfo; +using NzbDrone.Core.Movies; +using NzbDrone.Core.Notifications.Trakt.Resource; +using NzbDrone.Core.Qualities; + +namespace NzbDrone.Core.Notifications.Trakt +{ + public interface ITraktService + { + HttpRequest GetOAuthRequest(string callbackUrl); + TraktAuthRefreshResource RefreshAuthToken(string refreshToken); + void AddMovieToCollection(TraktSettings settings, Movie movie, MovieFile movieFile); + string GetUserName(string accessToken); + ValidationFailure Test(TraktSettings settings); + } + + public class TraktService : ITraktService + { + private readonly ITraktProxy _proxy; + private readonly Logger _logger; + + public TraktService(ITraktProxy proxy, + Logger logger) + { + _proxy = proxy; + _logger = logger; + } + + public string GetUserName(string accessToken) + { + return _proxy.GetUserName(accessToken); + } + + public HttpRequest GetOAuthRequest(string callbackUrl) + { + return _proxy.GetOAuthRequest(callbackUrl); + } + + public TraktAuthRefreshResource RefreshAuthToken(string refreshToken) + { + return _proxy.RefreshAuthToken(refreshToken); + } + + public ValidationFailure Test(TraktSettings settings) + { + try + { + GetUserName(settings.AccessToken); + return null; + } + catch (HttpException ex) + { + if (ex.Response.StatusCode == HttpStatusCode.Unauthorized) + { + _logger.Error(ex, "Access Token is invalid: " + ex.Message); + return new ValidationFailure("Token", "Access Token is invalid"); + } + + _logger.Error(ex, "Unable to send test message: " + ex.Message); + return new ValidationFailure("Token", "Unable to send test message"); + } + catch (Exception ex) + { + _logger.Error(ex, "Unable to send test message: " + ex.Message); + return new ValidationFailure("", "Unable to send test message"); + } + } + + public void AddMovieToCollection(TraktSettings settings, Movie movie, MovieFile movieFile) + { + var payload = new TraktCollectMoviesResource + { + Movies = new List() + }; + + var traktResolution = MapResolution(movieFile.Quality.Quality.Resolution, movieFile.MediaInfo?.ScanType); + var mediaType = MapMediaType(movieFile.Quality.Quality.Source); + var audio = MapAudio(movieFile); + var audioChannels = MapAudioChannels(movieFile, audio); + + payload.Movies.Add(new TraktCollectMovie + { + Title = movie.Title, + Year = movie.Year, + CollectedAt = DateTime.Now, + Resolution = traktResolution, + MediaType = mediaType, + AudioChannels = audioChannels, + Audio = audio, + Ids = new TraktMovieIdsResource + { + Tmdb = movie.TmdbId, + Imdb = movie.ImdbId ?? "", + } + }); + + _proxy.AddToCollection(payload, settings.AccessToken); + } + + private string MapMediaType(Source source) + { + var traktSource = string.Empty; + + switch (source) + { + case Source.BLURAY: + traktSource = "bluray"; + break; + case Source.WEBDL: + traktSource = "digital"; + break; + case Source.WEBRIP: + traktSource = "digital"; + break; + case Source.DVD: + traktSource = "dvd"; + break; + case Source.TV: + traktSource = "dvd"; + break; + } + + return traktSource; + } + + private string MapResolution(int resolution, string scanType) + { + var traktResolution = string.Empty; + var interlacedTypes = new string[] { "Interlaced", "MBAFF", "PAFF" }; + + var scanIdentifier = scanType.IsNotNullOrWhiteSpace() && interlacedTypes.Contains(scanType) ? "i" : "p"; + + switch (resolution) + { + case 2160: + traktResolution = "uhd_4k"; + break; + case 1080: + traktResolution = string.Format("hd_1080{0}", scanIdentifier); + break; + case 720: + traktResolution = "hd_720p"; + break; + case 576: + traktResolution = string.Format("sd_576{0}", scanIdentifier); + break; + case 480: + traktResolution = string.Format("sd_480{0}", scanIdentifier); + break; + } + + return traktResolution; + } + + private string MapAudio(MovieFile movieFile) + { + var traktAudioFormat = string.Empty; + + var audioCodec = movieFile.MediaInfo != null ? MediaInfoFormatter.FormatAudioCodec(movieFile.MediaInfo, movieFile.SceneName) : string.Empty; + + switch (audioCodec) + { + case "AC3": + traktAudioFormat = "dolby_digital"; + break; + case "EAC3": + traktAudioFormat = "dolby_digital_plus"; + break; + case "TrueHD": + traktAudioFormat = "dolby_truehd"; + break; + case "EAC3 Atmos": + case "TrueHD Atmos": + traktAudioFormat = "dolby_atmos"; + break; + case "DTS": + case "DTS-ES": + traktAudioFormat = "dts"; + break; + case "DTS-HD MA": + traktAudioFormat = "dts_ma"; + break; + case "DTS-HD HRA": + traktAudioFormat = "dts_hr"; + break; + case "DTS-X": + traktAudioFormat = "dts_x"; + break; + case "MP3": + case "MP2": + traktAudioFormat = "mp3"; + break; + case "Vorbis": + traktAudioFormat = "ogg"; + break; + case "WMA": + traktAudioFormat = "wma"; + break; + case "AAC": + traktAudioFormat = "aac"; + break; + case "PCM": + traktAudioFormat = "lpcm"; + break; + case "FLAC": + traktAudioFormat = "flac"; + break; + case "Opus": + traktAudioFormat = "ogg"; + break; + } + + return traktAudioFormat; + } + + private string MapAudioChannels(MovieFile movieFile, string audioFormat) + { + var audioChannels = movieFile.MediaInfo != null ? MediaInfoFormatter.FormatAudioChannels(movieFile.MediaInfo).ToString("0.0") : string.Empty; + + // Map cases where Radarr doesn't handle MI correctly, can purge once mediainfo handling is improved + if (audioChannels == "8.0") + { + audioChannels = "7.1"; + } + else if (audioChannels == "6.0" && audioFormat == "dts_ma") + { + audioChannels = "7.1"; + } + else if (audioChannels == "6.0" && audioFormat != "dts_ma") + { + audioChannels = "5.1"; + } + else if (audioChannels == "0.0") + { + audioChannels = string.Empty; + } + + return audioChannels; + } + } +} diff --git a/src/NzbDrone.Core/Notifications/Trakt/TraktSettings.cs b/src/NzbDrone.Core/Notifications/Trakt/TraktSettings.cs new file mode 100644 index 000000000..1d5c7b47d --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Trakt/TraktSettings.cs @@ -0,0 +1,48 @@ +using System; +using FluentValidation; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.Notifications.Trakt +{ + public class TraktSettingsValidator : AbstractValidator + { + public TraktSettingsValidator() + { + RuleFor(c => c.AccessToken).NotEmpty(); + RuleFor(c => c.RefreshToken).NotEmpty(); + RuleFor(c => c.Expires).NotEmpty(); + } + } + + public class TraktSettings : IProviderConfig + { + private static readonly TraktSettingsValidator Validator = new TraktSettingsValidator(); + + public TraktSettings() + { + SignIn = "startOAuth"; + } + + [FieldDefinition(0, Label = "Access Token", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] + public string AccessToken { get; set; } + + [FieldDefinition(0, Label = "Refresh Token", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] + public string RefreshToken { get; set; } + + [FieldDefinition(0, Label = "Expires", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] + public DateTime Expires { get; set; } + + [FieldDefinition(0, Label = "Auth User", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] + public string AuthUser { get; set; } + + [FieldDefinition(99, Label = "Authenticate with Trakt", Type = FieldType.OAuth)] + public string SignIn { get; set; } + + public NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } + } +}