diff --git a/src/NzbDrone.Core/ImportLists/Trakt/List/TraktListRequestGenerator.cs b/src/NzbDrone.Core/ImportLists/Trakt/List/TraktListRequestGenerator.cs index a9f6f7c7d..5623e8faa 100644 --- a/src/NzbDrone.Core/ImportLists/Trakt/List/TraktListRequestGenerator.cs +++ b/src/NzbDrone.Core/ImportLists/Trakt/List/TraktListRequestGenerator.cs @@ -20,24 +20,31 @@ public virtual ImportListPageableRequestChain GetListItems() private IEnumerable GetSeriesRequest() { - var link = Settings.BaseUrl.Trim(); + var requestBuilder = new HttpRequestBuilder(Settings.BaseUrl.Trim()); - link += $"/users/{Settings.Username.Trim()}/lists/{Settings.Listname.ToUrlSlug()}/items/show,season,episode"; + requestBuilder + .Resource("/users/{userName}/lists/{listName}/items/show,season,episode") + .SetSegment("userName", Settings.Username.Trim()) + .SetSegment("listName", Settings.Listname.ToUrlSlug()) + .Accept(HttpAccept.Json); var filterParams = TraktQueryHelper.BuildFilterParameters(Settings.Rating, Settings.Genres, Settings.Years, Settings.Limit, Settings.TraktAdditionalParameters); - link += "?" + filterParams.ToQueryString(); - var request = new ImportListRequest(link, HttpAccept.Json); + foreach (var param in filterParams) + { + requestBuilder.AddQueryParam(param.Key, param.Value); + } - request.HttpRequest.Headers.Add("trakt-api-version", "2"); - request.HttpRequest.Headers.Add("trakt-api-key", ClientId); + requestBuilder + .SetHeader("trakt-api-version", "2") + .SetHeader("trakt-api-key", ClientId); if (Settings.AccessToken.IsNotNullOrWhiteSpace()) { - request.HttpRequest.Headers.Add("Authorization", "Bearer " + Settings.AccessToken); + requestBuilder.SetHeader("Authorization", $"Bearer {Settings.AccessToken}"); } - yield return request; + yield return new ImportListRequest(requestBuilder.Build()); } } } diff --git a/src/NzbDrone.Core/ImportLists/Trakt/List/TraktListSettings.cs b/src/NzbDrone.Core/ImportLists/Trakt/List/TraktListSettings.cs index b51173598..34f0fa223 100644 --- a/src/NzbDrone.Core/ImportLists/Trakt/List/TraktListSettings.cs +++ b/src/NzbDrone.Core/ImportLists/Trakt/List/TraktListSettings.cs @@ -13,11 +13,6 @@ public TraktListSettingsValidator() RuleFor(c => c.Username).NotEmpty(); RuleFor(c => c.Listname).NotEmpty(); - RuleFor(c => c.Rating) - .Matches(@"^\d+\-\d+$", RegexOptions.IgnoreCase) - .When(c => c.Rating.IsNotNullOrWhiteSpace()) - .WithMessage("Not a valid rating"); - RuleFor(c => c.Years) .Matches(@"^\d+(\-\d+)?$", RegexOptions.IgnoreCase) .When(c => c.Years.IsNotNullOrWhiteSpace()) @@ -35,18 +30,9 @@ public class TraktListSettings : TraktSettingsBase [FieldDefinition(2, Label = "ImportListsTraktSettingsListName", HelpText = "ImportListsTraktSettingsListNameHelpText")] public string Listname { get; set; } - [FieldDefinition(3, Label = "ImportListsTraktSettingsRating", HelpText = "ImportListsTraktSettingsRatingSeriesHelpText")] - public string Rating { get; set; } - - [FieldDefinition(4, Label = "ImportListsTraktSettingsGenres", HelpText = "ImportListsTraktSettingsGenresSeriesHelpText")] - public string Genres { get; set; } - - [FieldDefinition(5, Label = "ImportListsTraktSettingsYears", HelpText = "ImportListsTraktSettingsYearsSeriesHelpText")] + [FieldDefinition(3, Label = "ImportListsTraktSettingsYears", HelpText = "ImportListsTraktSettingsYearsSeriesHelpText")] public string Years { get; set; } - [FieldDefinition(6, Label = "ImportListsTraktSettingsAdditionalParameters", HelpText = "ImportListsTraktSettingsAdditionalParametersHelpText", Advanced = true)] - public string TraktAdditionalParameters { get; set; } - public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularRequestGenerator.cs b/src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularRequestGenerator.cs index 3ff86cf57..715fbc475 100644 --- a/src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularRequestGenerator.cs +++ b/src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularRequestGenerator.cs @@ -21,63 +21,72 @@ public virtual ImportListPageableRequestChain GetListItems() private IEnumerable GetSeriesRequest() { - var link = Settings.BaseUrl.Trim(); + var requestBuilder = new HttpRequestBuilder(Settings.BaseUrl.Trim()); + + var resource = "/shows"; switch (Settings.TraktListType) { case (int)TraktPopularListType.Trending: - link += "/shows/trending"; + resource += "/trending"; break; case (int)TraktPopularListType.Popular: - link += "/shows/popular"; + resource += "/popular"; break; case (int)TraktPopularListType.Anticipated: - link += "/shows/anticipated"; + resource += "/anticipated"; break; case (int)TraktPopularListType.TopWatchedByWeek: - link += "/shows/watched/weekly"; + resource += "/watched/weekly"; break; case (int)TraktPopularListType.TopWatchedByMonth: - link += "/shows/watched/monthly"; + resource += "/watched/monthly"; break; #pragma warning disable CS0612 case (int)TraktPopularListType.TopWatchedByYear: #pragma warning restore CS0612 - link += "/shows/watched/yearly"; + resource += "/watched/yearly"; break; case (int)TraktPopularListType.TopWatchedByAllTime: - link += "/shows/watched/all"; + resource += "/watched/all"; break; case (int)TraktPopularListType.RecommendedByWeek: - link += "/shows/recommended/weekly"; + resource += "/recommended/weekly"; break; case (int)TraktPopularListType.RecommendedByMonth: - link += "/shows/recommended/monthly"; + resource += "/recommended/monthly"; break; #pragma warning disable CS0612 case (int)TraktPopularListType.RecommendedByYear: #pragma warning restore CS0612 - link += "/shows/recommended/yearly"; + resource += "/recommended/yearly"; break; case (int)TraktPopularListType.RecommendedByAllTime: - link += "/shows/recommended/all"; + resource += "/recommended/all"; break; } + requestBuilder + .Resource(resource) + .Accept(HttpAccept.Json); + var filterParams = TraktQueryHelper.BuildFilterParameters(Settings.Rating, Settings.Genres, Settings.Years, Settings.Limit, Settings.TraktAdditionalParameters); - link += "?" + filterParams.ToQueryString(); - var request = new ImportListRequest(link, HttpAccept.Json); + foreach (var param in filterParams) + { + requestBuilder.AddQueryParam(param.Key, param.Value); + } - request.HttpRequest.Headers.Add("trakt-api-version", "2"); - request.HttpRequest.Headers.Add("trakt-api-key", ClientId); + requestBuilder + .SetHeader("trakt-api-version", "2") + .SetHeader("trakt-api-key", ClientId); if (Settings.AccessToken.IsNotNullOrWhiteSpace()) { - request.HttpRequest.Headers.Add("Authorization", "Bearer " + Settings.AccessToken); + requestBuilder.SetHeader("Authorization", $"Bearer {Settings.AccessToken}"); } - yield return request; + yield return new ImportListRequest(requestBuilder.Build()); } } } diff --git a/src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularSettings.cs b/src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularSettings.cs index 9694e308c..0dcc34b44 100644 --- a/src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularSettings.cs +++ b/src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularSettings.cs @@ -45,18 +45,9 @@ public TraktPopularSettings() [FieldDefinition(1, Label = "ImportListsTraktSettingsListType", Type = FieldType.Select, SelectOptions = typeof(TraktPopularListType), HelpText = "ImportListsTraktSettingsListTypeHelpText")] public int TraktListType { get; set; } - [FieldDefinition(2, Label = "ImportListsTraktSettingsRating", HelpText = "ImportListsTraktSettingsRatingSeriesHelpText")] - public string Rating { get; set; } - - [FieldDefinition(4, Label = "ImportListsTraktSettingsGenres", HelpText = "ImportListsTraktSettingsGenresSeriesHelpText")] - public string Genres { get; set; } - - [FieldDefinition(5, Label = "ImportListsTraktSettingsYears", HelpText = "ImportListsTraktSettingsYearsSeriesHelpText")] + [FieldDefinition(2, Label = "ImportListsTraktSettingsYears", HelpText = "ImportListsTraktSettingsYearsSeriesHelpTextPopular")] public string Years { get; set; } - [FieldDefinition(6, Label = "ImportListsTraktSettingsAdditionalParameters", HelpText = "ImportListsTraktSettingsAdditionalParametersHelpText", Advanced = true)] - public string TraktAdditionalParameters { get; set; } - public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/ImportLists/Trakt/TraktQueryHelper.cs b/src/NzbDrone.Core/ImportLists/Trakt/TraktQueryHelper.cs index 46cb4e053..003b2872d 100644 --- a/src/NzbDrone.Core/ImportLists/Trakt/TraktQueryHelper.cs +++ b/src/NzbDrone.Core/ImportLists/Trakt/TraktQueryHelper.cs @@ -7,16 +7,6 @@ namespace NzbDrone.Core.ImportLists.Trakt { public static class TraktQueryHelper { - private static readonly HashSet CommaSeparatedParams = new(StringComparer.OrdinalIgnoreCase) - { - "genres", - "certifications", - "networks", - "languages", - "countries", - "status" - }; - public static Dictionary BuildFilterParameters(string rating, string genres, string years, int limit, string additionalParameters) { var parameters = new Dictionary(StringComparer.OrdinalIgnoreCase); @@ -32,38 +22,27 @@ public static Dictionary BuildFilterParameters(string rating, st if (parts.Length == 2 && parts[0].IsNotNullOrWhiteSpace()) { - parameters[parts[0].Trim()] = parts[1].Trim(); + var key = parts[0].Trim(); + + // Skip explicitly handled parameters + if (key.Equals("genres", StringComparison.OrdinalIgnoreCase) || + key.Equals("ratings", StringComparison.OrdinalIgnoreCase) || + key.Equals("years", StringComparison.OrdinalIgnoreCase) || + key.Equals("limit", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + parameters[key] = parts[1].Trim(); } } } - // Apply explicit settings (higher priority) - // For comma-separated params like genres, combine values from both sources if (genres.IsNotNullOrWhiteSpace()) { - if (parameters.TryGetValue("genres", out var existingGenres) && existingGenres.IsNotNullOrWhiteSpace()) - { - var allGenres = new HashSet(StringComparer.OrdinalIgnoreCase); - - foreach (var g in genres.ToLower().Split(',', StringSplitOptions.RemoveEmptyEntries)) - { - allGenres.Add(g.Trim()); - } - - foreach (var g in existingGenres.ToLower().Split(',', StringSplitOptions.RemoveEmptyEntries)) - { - allGenres.Add(g.Trim()); - } - - parameters["genres"] = string.Join(",", allGenres); - } - else - { - parameters["genres"] = genres.ToLower(); - } + parameters["genres"] = genres.ToLower(); } - // For ratings and years, explicit settings override additional parameters if (rating.IsNotNullOrWhiteSpace()) { parameters["ratings"] = rating; diff --git a/src/NzbDrone.Core/ImportLists/Trakt/TraktSettingsBase.cs b/src/NzbDrone.Core/ImportLists/Trakt/TraktSettingsBase.cs index 03834afa6..73aa8d977 100644 --- a/src/NzbDrone.Core/ImportLists/Trakt/TraktSettingsBase.cs +++ b/src/NzbDrone.Core/ImportLists/Trakt/TraktSettingsBase.cs @@ -1,4 +1,5 @@ using System; +using System.Text.RegularExpressions; using FluentValidation; using NzbDrone.Common.Extensions; using NzbDrone.Core.Annotations; @@ -31,6 +32,11 @@ public TraktSettingsBaseValidator() RuleFor(c => c.Limit) .GreaterThan(0) .WithMessage("Must be integer greater than 0"); + + RuleFor(c => c.Rating) + .Matches(@"^\d+\-\d+$", RegexOptions.IgnoreCase) + .When(c => c.Rating.IsNotNullOrWhiteSpace()) + .WithMessage("Not a valid rating"); } } @@ -59,6 +65,15 @@ public TraktSettingsBase() [FieldDefinition(0, Label = "ImportListsSettingsAuthUser", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] public string AuthUser { get; set; } + [FieldDefinition(95, Label = "ImportListsTraktSettingsRating", HelpText = "ImportListsTraktSettingsRatingSeriesHelpText")] + public string Rating { get; set; } + + [FieldDefinition(96, Label = "ImportListsTraktSettingsGenres", HelpText = "ImportListsTraktSettingsGenresSeriesHelpText")] + public string Genres { get; set; } + + [FieldDefinition(97, Label = "ImportListsTraktSettingsAdditionalParameters", HelpText = "ImportListsTraktSettingsAdditionalParametersHelpText", Advanced = true)] + public string TraktAdditionalParameters { get; set; } + [FieldDefinition(98, Label = "ImportListsTraktSettingsLimit", HelpText = "ImportListsTraktSettingsLimitSeriesHelpText")] public int Limit { get; set; } diff --git a/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserRequestGenerator.cs b/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserRequestGenerator.cs index a4a5e2088..d4724c08b 100644 --- a/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserRequestGenerator.cs +++ b/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserRequestGenerator.cs @@ -26,7 +26,9 @@ public virtual ImportListPageableRequestChain GetListItems() private IEnumerable GetSeriesRequest() { - var requestBuilder = new HttpRequestBuilder(_settings.BaseUrl.Trim()); + var link = _settings.BaseUrl.Trim(); + + var userName = _settings.Username.IsNotNullOrWhiteSpace() ? _settings.Username.Trim() : _settings.AuthUser.Trim(); switch (_settings.TraktListType) { @@ -39,46 +41,37 @@ private IEnumerable GetSeriesRequest() _ => "rank" }; - requestBuilder - .Resource("/users/{userName}/watchlist/shows/{sorting}") - .SetSegment("sorting", watchSorting); + link += $"/users/{userName}/watchlist/shows/{watchSorting}"; break; case (int)TraktUserListType.UserWatchedList: - requestBuilder - .Resource("/users/{userName}/watched/shows") - .AddQueryParam("extended", "full"); + link += $"/users/{userName}/watched/shows"; break; case (int)TraktUserListType.UserCollectionList: - requestBuilder.Resource("/users/{userName}/collection/shows"); + link += $"/users/{userName}/collection/shows"; break; } - var userName = _settings.Username.IsNotNullOrWhiteSpace() ? _settings.Username.Trim() : _settings.AuthUser.Trim(); - - requestBuilder - .SetSegment("userName", userName) - .Accept(HttpAccept.Json) - .WithRateLimit(4) - .SetHeader("trakt-api-version", "2") - .SetHeader("trakt-api-key", _clientId) - .AddQueryParam("limit", _settings.Limit.ToString()); - var filterParams = TraktQueryHelper.BuildFilterParameters(_settings.Rating, _settings.Genres, _settings.Years, _settings.Limit, _settings.TraktAdditionalParameters); - foreach (var param in filterParams) + // Add extended parameter for watched list + if (_settings.TraktListType == (int)TraktUserListType.UserWatchedList) { - if (param.Key != "limit") - { - requestBuilder.AddQueryParam(param.Key, param.Value); - } + filterParams["extended"] = "full"; } + link += "?" + filterParams.ToQueryString(); + + var request = new ImportListRequest(link, HttpAccept.Json); + + request.HttpRequest.Headers.Add("trakt-api-version", "2"); + request.HttpRequest.Headers.Add("trakt-api-key", _clientId); + if (_settings.AccessToken.IsNotNullOrWhiteSpace()) { - requestBuilder.SetHeader("Authorization", $"Bearer {_settings.AccessToken}"); + request.HttpRequest.Headers.Add("Authorization", $"Bearer {_settings.AccessToken}"); } - yield return new ImportListRequest(requestBuilder.Build()); + yield return request; } } } diff --git a/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserSettings.cs b/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserSettings.cs index 90b9c2bf7..1309a4e0e 100644 --- a/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserSettings.cs +++ b/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserSettings.cs @@ -49,18 +49,9 @@ public TraktUserSettings() [FieldDefinition(4, Label = "Username", HelpText = "ImportListsTraktSettingsUserListUsernameHelpText")] public string Username { get; set; } - [FieldDefinition(5, Label = "ImportListsTraktSettingsRating", HelpText = "ImportListsTraktSettingsRatingSeriesHelpText")] - public string Rating { get; set; } - - [FieldDefinition(6, Label = "ImportListsTraktSettingsGenres", HelpText = "ImportListsTraktSettingsGenresSeriesHelpText")] - public string Genres { get; set; } - - [FieldDefinition(7, Label = "ImportListsTraktSettingsYears", HelpText = "ImportListsTraktSettingsYearsSeriesHelpText")] + [FieldDefinition(5, Label = "ImportListsTraktSettingsYears", HelpText = "ImportListsTraktSettingsYearsSeriesHelpText")] public string Years { get; set; } - [FieldDefinition(8, Label = "ImportListsTraktSettingsAdditionalParameters", HelpText = "ImportListsTraktSettingsAdditionalParametersHelpText", Advanced = true)] - public string TraktAdditionalParameters { get; set; } - public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index d4d7fe427..75521aede 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -943,7 +943,7 @@ "ImportListsTraktSettingsAdditionalParametersHelpText": "Additional Trakt API parameters", "ImportListsTraktSettingsAuthenticateWithTrakt": "Authenticate with Trakt", "ImportListsTraktSettingsGenres": "Genres", - "ImportListsTraktSettingsGenresSeriesHelpText": "Filter series by Trakt Genre (action,-romance,-anime)", + "ImportListsTraktSettingsGenresSeriesHelpText": "Filter series by Trakt Genre slug (action,comedy)", "ImportListsTraktSettingsLimit": "Limit", "ImportListsTraktSettingsLimitSeriesHelpText": "Limit the number of series to get", "ImportListsTraktSettingsListName": "List Name", @@ -979,6 +979,7 @@ "ImportListsTraktSettingsWatchedListTypeInProgress": "In Progress", "ImportListsTraktSettingsYears": "Years", "ImportListsTraktSettingsYearsSeriesHelpText": "Filter series by minimum year or year range (1990-2000)", + "ImportListsTraktSettingsYearsSeriesHelpTextPopular": "Filter series by a specific year or year range (1990-2000)", "ImportListsValidationInvalidApiKey": "API Key is invalid", "ImportListsValidationTestFailed": "Test was aborted due to an error: {exceptionMessage}", "ImportListsValidationUnableToConnectException": "Unable to connect to import list: {exceptionMessage}. Check the log surrounding this error for details.",