From 86fa6036d0f455b31abaf8cf663143c3de021a73 Mon Sep 17 00:00:00 2001 From: geogolem <55031547+geogolem@users.noreply.github.com> Date: Sun, 21 Nov 2021 13:08:56 -0500 Subject: [PATCH] New: Trakt Connection Closes #3906 --- .../Notifications/NotificationRepository.cs | 6 +- .../Resource/TraktAuthRefreshResource.cs | 20 ++ .../Resource/TraktCollectShowResource.cs | 9 + .../Resource/TraktCollectShowsResource.cs | 9 + .../Trakt/Resource/TraktEpisodeResource.cs | 20 ++ .../Trakt/Resource/TraktListResource.cs | 25 ++ .../Trakt/Resource/TraktSeasonResource.cs | 10 + .../Trakt/Resource/TraktShowIdsResource.cs | 12 + .../Trakt/Resource/TraktShowResource.cs | 11 + .../Trakt/Resource/TraktUserIdsResource.cs | 7 + .../Trakt/Resource/TraktUserResource.cs | 10 + .../Resource/TraktUserSettingsResource.cs | 7 + .../Notifications/Trakt/Trakt.cs | 105 ++++++ .../Notifications/Trakt/TraktException.cs | 18 + .../Trakt/TraktInterlacedTypes.cs | 20 ++ .../Notifications/Trakt/TraktProxy.cs | 130 +++++++ .../Notifications/Trakt/TraktService.cs | 323 ++++++++++++++++++ .../Notifications/Trakt/TraktSettings.cs | 48 +++ 18 files changed, 789 insertions(+), 1 deletion(-) create mode 100644 src/NzbDrone.Core/Notifications/Trakt/Resource/TraktAuthRefreshResource.cs create mode 100644 src/NzbDrone.Core/Notifications/Trakt/Resource/TraktCollectShowResource.cs create mode 100644 src/NzbDrone.Core/Notifications/Trakt/Resource/TraktCollectShowsResource.cs create mode 100644 src/NzbDrone.Core/Notifications/Trakt/Resource/TraktEpisodeResource.cs create mode 100644 src/NzbDrone.Core/Notifications/Trakt/Resource/TraktListResource.cs create mode 100644 src/NzbDrone.Core/Notifications/Trakt/Resource/TraktSeasonResource.cs create mode 100644 src/NzbDrone.Core/Notifications/Trakt/Resource/TraktShowIdsResource.cs create mode 100644 src/NzbDrone.Core/Notifications/Trakt/Resource/TraktShowResource.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/TraktInterlacedTypes.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/Notifications/NotificationRepository.cs b/src/NzbDrone.Core/Notifications/NotificationRepository.cs index b012bd620..4dddebba9 100644 --- a/src/NzbDrone.Core/Notifications/NotificationRepository.cs +++ b/src/NzbDrone.Core/Notifications/NotificationRepository.cs @@ -7,7 +7,7 @@ namespace NzbDrone.Core.Notifications { public interface INotificationRepository : IProviderRepository { - + void UpdateSettings(NotificationDefinition model); } public class NotificationRepository : ProviderRepository, INotificationRepository @@ -16,5 +16,9 @@ namespace NzbDrone.Core.Notifications : base(database, eventAggregator) { } + public void UpdateSettings(NotificationDefinition model) + { + SetFields(model, m => m.Settings); + } } } \ No newline at end of file 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..514108cf8 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Trakt/Resource/TraktAuthRefreshResource.cs @@ -0,0 +1,20 @@ +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/TraktCollectShowResource.cs b/src/NzbDrone.Core/Notifications/Trakt/Resource/TraktCollectShowResource.cs new file mode 100644 index 000000000..6ed9cb275 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Trakt/Resource/TraktCollectShowResource.cs @@ -0,0 +1,9 @@ +using System; +using Newtonsoft.Json; + +namespace NzbDrone.Core.Notifications.Trakt.Resource +{ + public class TraktCollectShow : TraktShowResource + { + } +} diff --git a/src/NzbDrone.Core/Notifications/Trakt/Resource/TraktCollectShowsResource.cs b/src/NzbDrone.Core/Notifications/Trakt/Resource/TraktCollectShowsResource.cs new file mode 100644 index 000000000..df1dce362 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Trakt/Resource/TraktCollectShowsResource.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace NzbDrone.Core.Notifications.Trakt.Resource +{ + public class TraktCollectShowsResource + { + public List Shows { get; set; } + } +} diff --git a/src/NzbDrone.Core/Notifications/Trakt/Resource/TraktEpisodeResource.cs b/src/NzbDrone.Core/Notifications/Trakt/Resource/TraktEpisodeResource.cs new file mode 100644 index 000000000..5634f9056 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Trakt/Resource/TraktEpisodeResource.cs @@ -0,0 +1,20 @@ +using System; +using Newtonsoft.Json; +namespace NzbDrone.Core.Notifications.Trakt.Resource +{ + public class TraktEpisodeResource + { + public int Number { get; set; } + + [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/TraktListResource.cs b/src/NzbDrone.Core/Notifications/Trakt/Resource/TraktListResource.cs new file mode 100644 index 000000000..6fbb72b03 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Trakt/Resource/TraktListResource.cs @@ -0,0 +1,25 @@ +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; } + + [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 string Type { get; set; } + public int? Watchers { get; set; } + public long? Revenue { get; set; } + public TraktEpisodeResource Episode { get; set; } + } +} diff --git a/src/NzbDrone.Core/Notifications/Trakt/Resource/TraktSeasonResource.cs b/src/NzbDrone.Core/Notifications/Trakt/Resource/TraktSeasonResource.cs new file mode 100644 index 000000000..9387e39ae --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Trakt/Resource/TraktSeasonResource.cs @@ -0,0 +1,10 @@ +using NzbDrone.Core.Tv; +using System.Collections.Generic; +namespace NzbDrone.Core.Notifications.Trakt.Resource +{ + public class TraktSeasonResource + { + public int Number { get; set; } + public List Episodes {get; set;} + } +} diff --git a/src/NzbDrone.Core/Notifications/Trakt/Resource/TraktShowIdsResource.cs b/src/NzbDrone.Core/Notifications/Trakt/Resource/TraktShowIdsResource.cs new file mode 100644 index 000000000..669a4e979 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Trakt/Resource/TraktShowIdsResource.cs @@ -0,0 +1,12 @@ +using NzbDrone.Core.Tv; +using System.Collections.Generic; +namespace NzbDrone.Core.Notifications.Trakt.Resource +{ + public class TraktShowIdsResource + { + public int Trakt { get; set; } + public string Slug { get; set; } + public string Imdb { get; set; } + public int Tvdb { get; set; } + } +} diff --git a/src/NzbDrone.Core/Notifications/Trakt/Resource/TraktShowResource.cs b/src/NzbDrone.Core/Notifications/Trakt/Resource/TraktShowResource.cs new file mode 100644 index 000000000..fcd29a2c0 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Trakt/Resource/TraktShowResource.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +namespace NzbDrone.Core.Notifications.Trakt.Resource +{ + public class TraktShowResource + { + public string Title { get; set; } + public int? Year { get; set; } + public TraktShowIdsResource Ids { get; set; } + public List Seasons { 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..cd3077dbe --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Trakt/Trakt.cs @@ -0,0 +1,105 @@ +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.AddEpisodeToCollection(Settings, message.Series, message.EpisodeFile); + } + + public override void OnEpisodeFileDelete(EpisodeDeleteMessage deleteMessage) + { + _traktService.RemoveEpisodeFromCollection(Settings, deleteMessage.Series, deleteMessage.EpisodeFile); + } + + public override void OnSeriesDelete(SeriesDeleteMessage deleteMessage) + { + _traktService.RemoveSeriesFromCollection(Settings, deleteMessage.Series); + } + + 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/TraktInterlacedTypes.cs b/src/NzbDrone.Core/Notifications/Trakt/TraktInterlacedTypes.cs new file mode 100644 index 000000000..4d76d5895 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Trakt/TraktInterlacedTypes.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; + +namespace NzbDrone.Core.Notifications.Trakt +{ + public static class TraktInterlacedTypes + { + private static HashSet _interlacedTypes; + + static TraktInterlacedTypes() + { + _interlacedTypes = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "Interlaced", "MBAFF", "PAFF" + }; + } + + public static HashSet interlacedTypes => _interlacedTypes; + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Notifications/Trakt/TraktProxy.cs b/src/NzbDrone.Core/Notifications/Trakt/TraktProxy.cs new file mode 100644 index 000000000..bbfc230bb --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Trakt/TraktProxy.cs @@ -0,0 +1,130 @@ +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(TraktCollectShowsResource payload, string accessToken); + void RemoveFromCollection(TraktCollectShowsResource 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://trakt.tv/oauth/authorize"; + private const string RedirectUri = "https://auth.servarr.com/v1/trakt_sonarr/auth"; + private const string RenewUri = "https://auth.servarr.com/v1/trakt_sonarr/renew"; + private const string ClientId = "d44ba57cab40c31eb3f797dcfccd203500796539125b333883ec1d94aa62ed4c"; + + private readonly IHttpClient _httpClient; + private readonly Logger _logger; + + public TraktProxy(IHttpClient httpClient, Logger logger) + { + _httpClient = httpClient; + _logger = logger; + } + + public void AddToCollection(TraktCollectShowsResource 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 void RemoveFromCollection(TraktCollectShowsResource payload, string accessToken) + { + var request = BuildTraktRequest("sync/collection/remove", HttpMethod.POST, accessToken); + + request.Headers.ContentType = "application/json"; + var temp = payload.ToJson(); + 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.Method = method; + + request.Headers.Accept = HttpAccept.Json.Value; + 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..4a3a14190 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Trakt/TraktService.cs @@ -0,0 +1,323 @@ +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.Tv; +using NzbDrone.Core.Notifications.Trakt.Resource; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.MetadataSource.SkyHook.Resource; +using NzbDrone.Core.Indexers.HDBits; +using NzbDrone.Core.IndexerSearch; + +namespace NzbDrone.Core.Notifications.Trakt +{ + public interface ITraktService + { + HttpRequest GetOAuthRequest(string callbackUrl); + TraktAuthRefreshResource RefreshAuthToken(string refreshToken); + void AddEpisodeToCollection(TraktSettings settings, Series series, EpisodeFile episodeFile); + void RemoveEpisodeFromCollection(TraktSettings settings, Series series, EpisodeFile episodeFile); + void RemoveSeriesFromCollection(TraktSettings settings, Series series); + 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 AddEpisodeToCollection(TraktSettings settings, Series series, EpisodeFile episodeFile) + { + var payload = new TraktCollectShowsResource + { + Shows = new List() + }; + + var traktResolution = MapResolution(episodeFile.Quality.Quality.Resolution, episodeFile.MediaInfo?.ScanType); + var mediaType = MapMediaType(episodeFile.Quality.Quality.Source); + var audio = MapAudio(episodeFile); + var audioChannels = MapAudioChannels(episodeFile, audio); + + var payloadEpisodes = new List(); + + foreach (var episode in episodeFile.Episodes.Value) + { + payloadEpisodes.Add(new TraktEpisodeResource + { + Number = episode.EpisodeNumber, + CollectedAt = DateTime.Now, + Resolution = traktResolution, + MediaType = mediaType, + AudioChannels = audioChannels, + Audio = audio, + }); + } + + var payloadSeasons = new List(); + payloadSeasons.Add(new TraktSeasonResource + { + Number = episodeFile.SeasonNumber, + Episodes = payloadEpisodes + }); + + payload.Shows.Add(new TraktCollectShow + { + Title = series.Title, + Year = series.Year, + Ids = new TraktShowIdsResource + { + Tvdb = series.TvdbId, + Imdb = series.ImdbId ?? "", + }, + Seasons = payloadSeasons, + }); ; + + _proxy.AddToCollection(payload, settings.AccessToken); + } + + public void RemoveEpisodeFromCollection(TraktSettings settings, Series series, EpisodeFile episodeFile) + { + var payload = new TraktCollectShowsResource + { + Shows = new List() + }; + + var payloadEpisodes = new List(); + + foreach (var episode in episodeFile.Episodes.Value) + { + payloadEpisodes.Add(new TraktEpisodeResource + { + Number = episode.EpisodeNumber + }); + } + + var payloadSeasons = new List(); + payloadSeasons.Add(new TraktSeasonResource + { + Number = episodeFile.SeasonNumber, + Episodes = payloadEpisodes + }); + + payload.Shows.Add(new TraktCollectShow + { + Title = series.Title, + Year = series.Year, + Ids = new TraktShowIdsResource + { + Tvdb = series.TvdbId, + Imdb = series.ImdbId ?? "", + }, + Seasons = payloadSeasons, + }); ; + + _proxy.RemoveFromCollection(payload, settings.AccessToken); + } + + public void RemoveSeriesFromCollection(TraktSettings settings, Series series) + { + var payload = new TraktCollectShowsResource + { + Shows = new List() + }; + + payload.Shows.Add(new TraktCollectShow + { + Title = series.Title, + Year = series.Year, + Ids = new TraktShowIdsResource + { + Tvdb = series.TvdbId, + Imdb = series.ImdbId ?? "", + }, + }); ; + + _proxy.RemoveFromCollection(payload, settings.AccessToken); + } + + private string MapMediaType(QualitySource source) + { + var traktSource = string.Empty; + + switch (source) + { + case QualitySource.Web: + case QualitySource.WebRip: + traktSource = "digital"; + break; + case QualitySource.BlurayRaw: + case QualitySource.Bluray: + traktSource = "bluray"; + break; + case QualitySource.Television: + case QualitySource.TelevisionRaw: + traktSource = "vhs"; + break; + case QualitySource.DVD: + 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() && TraktInterlacedTypes.interlacedTypes.Contains(scanType) ? "i" : "p"; + + switch (resolution) + { + case 2160: + traktResolution = "uhd_4k"; + break; + case 1080: + traktResolution = $"hd_1080{scanIdentifier}"; + break; + case 720: + traktResolution = "hd_720p"; + break; + case 576: + traktResolution = $"sd_576{scanIdentifier}"; + break; + case 480: + traktResolution = $"sd_480{scanIdentifier}"; + break; + } + + return traktResolution; + } + + private string MapAudio(EpisodeFile episodeFile) + { + var traktAudioFormat = string.Empty; + + var audioCodec = episodeFile.MediaInfo != null ? MediaInfoFormatter.FormatAudioCodec(episodeFile.MediaInfo, episodeFile.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": + traktAudioFormat = "dolby_digital_plus_atmos"; + break; + 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": + traktAudioFormat = "mp3"; + break; + case "MP2": + traktAudioFormat = "mp2"; + 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_opus"; + break; + } + + return traktAudioFormat; + } + + private string MapAudioChannels(EpisodeFile episodeFile, string audioFormat) + { + var audioChannels = episodeFile.MediaInfo != null ? MediaInfoFormatter.FormatAudioChannels(episodeFile.MediaInfo).ToString("0.0") : 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..99dfaf9fc --- /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(1, Label = "Refresh Token", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] + public string RefreshToken { get; set; } + + [FieldDefinition(2, Label = "Expires", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] + public DateTime Expires { get; set; } + + [FieldDefinition(3, Label = "Auth User", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] + public string AuthUser { get; set; } + + [FieldDefinition(4, Label = "Authenticate with Trakt", Type = FieldType.OAuth)] + public string SignIn { get; set; } + + public NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } + } +}