diff --git a/src/NzbDrone.Core/Notifications/Trakt/Trakt.cs b/src/NzbDrone.Core/Notifications/Trakt/Trakt.cs index cd3077dbe..c3723e357 100644 --- a/src/NzbDrone.Core/Notifications/Trakt/Trakt.cs +++ b/src/NzbDrone.Core/Notifications/Trakt/Trakt.cs @@ -1,22 +1,28 @@ using System; using System.Collections.Generic; +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.Notifications.Trakt.Resource; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Tv; using NzbDrone.Core.Validation; namespace NzbDrone.Core.Notifications.Trakt { public class Trakt : NotificationBase { - private readonly ITraktService _traktService; + private readonly ITraktProxy _proxy; private readonly INotificationRepository _notificationRepository; private readonly Logger _logger; - public Trakt(ITraktService traktService, INotificationRepository notificationRepository, Logger logger) + public Trakt(ITraktProxy proxy, INotificationRepository notificationRepository, Logger logger) { - _traktService = traktService; + _proxy = proxy; _notificationRepository = notificationRepository; _logger = logger; } @@ -26,24 +32,53 @@ namespace NzbDrone.Core.Notifications.Trakt public override void OnDownload(DownloadMessage message) { - _traktService.AddEpisodeToCollection(Settings, message.Series, message.EpisodeFile); + RefreshTokenIfNecessary(); + AddEpisodeToCollection(Settings, message.Series, message.EpisodeFile); } public override void OnEpisodeFileDelete(EpisodeDeleteMessage deleteMessage) { - _traktService.RemoveEpisodeFromCollection(Settings, deleteMessage.Series, deleteMessage.EpisodeFile); + RefreshTokenIfNecessary(); + RemoveEpisodeFromCollection(Settings, deleteMessage.Series, deleteMessage.EpisodeFile); } public override void OnSeriesDelete(SeriesDeleteMessage deleteMessage) { - _traktService.RemoveSeriesFromCollection(Settings, deleteMessage.Series); + RefreshTokenIfNecessary(); + RemoveSeriesFromCollection(Settings, deleteMessage.Series); } public override ValidationResult Test() { var failures = new List(); - failures.AddIfNotNull(_traktService.Test(Settings)); + RefreshTokenIfNecessary(); + + try + { + _proxy.GetUserName(Settings.AccessToken); + } + catch (HttpException ex) + { + if (ex.Response.StatusCode == HttpStatusCode.Unauthorized) + { + _logger.Error(ex, "Access Token is invalid: " + ex.Message); + + failures.Add(new ValidationFailure("Token", "Access Token is invalid")); + } + else + { + _logger.Error(ex, "Unable to send test message: " + ex.Message); + + failures.Add(new ValidationFailure("Token", "Unable to send test message")); + } + } + catch (Exception ex) + { + _logger.Error(ex, "Unable to send test message: " + ex.Message); + + failures.Add(new ValidationFailure("", "Unable to send test message")); + } return new ValidationResult(failures); } @@ -52,7 +87,7 @@ namespace NzbDrone.Core.Notifications.Trakt { if (action == "startOAuth") { - var request = _traktService.GetOAuthRequest(query["callbackUrl"]); + var request = _proxy.GetOAuthRequest(query["callbackUrl"]); return new { @@ -66,14 +101,22 @@ namespace NzbDrone.Core.Notifications.Trakt accessToken = query["access_token"], expires = DateTime.UtcNow.AddSeconds(int.Parse(query["expires_in"])), refreshToken = query["refresh_token"], - authUser = _traktService.GetUserName(query["access_token"]) + authUser = _proxy.GetUserName(query["access_token"]) }; } return new { }; } - public void RefreshToken() + private void RefreshTokenIfNecessary() + { + if (Settings.Expires < DateTime.UtcNow.AddMinutes(5)) + { + RefreshToken(); + } + } + + private void RefreshToken() { _logger.Trace("Refreshing Token"); @@ -81,11 +124,12 @@ namespace NzbDrone.Core.Notifications.Trakt try { - var response = _traktService.RefreshAuthToken(Settings.RefreshToken); + var response = _proxy.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; @@ -96,10 +140,246 @@ namespace NzbDrone.Core.Notifications.Trakt } } } - catch (HttpException) + catch (HttpException ex) + { + _logger.Warn(ex, "Error refreshing trakt access token"); + } + } + + private void AddEpisodeToCollection(TraktSettings settings, Series series, EpisodeFile episodeFile) + { + var payload = new TraktCollectShowsResource { - _logger.Warn($"Error refreshing trakt access token"); + 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); + } + + private 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); + } + + private 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/TraktProxy.cs b/src/NzbDrone.Core/Notifications/Trakt/TraktProxy.cs index fc6bee59b..c9768fc69 100644 --- a/src/NzbDrone.Core/Notifications/Trakt/TraktProxy.cs +++ b/src/NzbDrone.Core/Notifications/Trakt/TraktProxy.cs @@ -14,7 +14,6 @@ namespace NzbDrone.Core.Notifications.Trakt 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 @@ -36,60 +35,30 @@ namespace NzbDrone.Core.Notifications.Trakt public void AddToCollection(TraktCollectShowsResource payload, string accessToken) { - var request = BuildTraktRequest("sync/collection", HttpMethod.Post, accessToken); + var request = BuildRequest("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); - } + MakeRequest(request); } public void RemoveFromCollection(TraktCollectShowsResource payload, string accessToken) { - var request = BuildTraktRequest("sync/collection/remove", HttpMethod.Post, accessToken); + var request = BuildRequest("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); - } + MakeRequest(request); } public string GetUserName(string accessToken) { - var request = BuildTraktRequest("users/settings", HttpMethod.Get, accessToken); + var request = BuildRequest("users/settings", HttpMethod.Get, accessToken); + var response = _httpClient.Get(request); - 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; + return response?.Resource?.User?.Ids?.Slug; } public HttpRequest GetOAuthRequest(string callbackUrl) @@ -111,7 +80,7 @@ namespace NzbDrone.Core.Notifications.Trakt return _httpClient.Get(request)?.Resource ?? null; } - public HttpRequest BuildTraktRequest(string resource, HttpMethod method, string accessToken) + private HttpRequest BuildRequest(string resource, HttpMethod method, string accessToken) { var request = new HttpRequestBuilder(URL).Resource(resource).Build(); request.Method = method; @@ -127,5 +96,17 @@ namespace NzbDrone.Core.Notifications.Trakt return request; } + + private void MakeRequest(HttpRequest request) + { + try + { + _httpClient.Execute(request); + } + catch (HttpException ex) + { + throw new TraktException("Unable to send payload", ex); + } + } } } diff --git a/src/NzbDrone.Core/Notifications/Trakt/TraktService.cs b/src/NzbDrone.Core/Notifications/Trakt/TraktService.cs deleted file mode 100644 index ee6d26f49..000000000 --- a/src/NzbDrone.Core/Notifications/Trakt/TraktService.cs +++ /dev/null @@ -1,319 +0,0 @@ -using System; -using System.Collections.Generic; -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.Notifications.Trakt.Resource; -using NzbDrone.Core.Qualities; -using NzbDrone.Core.Tv; - -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; - } - } -}