diff --git a/src/NzbDrone.Core.Test/ImportListTests/Trakt/TraktValidationFixture.cs b/src/NzbDrone.Core.Test/ImportListTests/Trakt/TraktValidationFixture.cs new file mode 100644 index 000000000..8a29e948e --- /dev/null +++ b/src/NzbDrone.Core.Test/ImportListTests/Trakt/TraktValidationFixture.cs @@ -0,0 +1,137 @@ +using System; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.ImportLists.Trakt; +using NzbDrone.Core.ImportLists.Trakt.List; +using NzbDrone.Core.ImportLists.Trakt.Popular; +using NzbDrone.Core.ImportLists.Trakt.User; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.ImportListTests.Trakt +{ + [TestFixture] + public class TraktValidationFixture : CoreTest + { + [TestCase("0-100")] + [TestCase("50-50")] + [TestCase("100-100")] + public void should_accept_supported_rating_ranges(string rating) + { + CreateValidListSettings(rating: rating).Validate().IsValid.Should().BeTrue(); + CreateValidPopularSettings(rating: rating).Validate().IsValid.Should().BeTrue(); + CreateValidUserSettings(rating: rating).Validate().IsValid.Should().BeTrue(); + } + + [TestCase("10")] + [TestCase("10-5")] + [TestCase("00-10")] + [TestCase("10-101")] + [TestCase("1000-1000")] + [TestCase("10 - 20")] + public void should_reject_invalid_rating_ranges(string rating) + { + CreateValidListSettings(rating: rating).Validate().IsValid.Should().BeFalse(); + CreateValidPopularSettings(rating: rating).Validate().IsValid.Should().BeFalse(); + CreateValidUserSettings(rating: rating).Validate().IsValid.Should().BeFalse(); + } + + [TestCase("1990")] + [TestCase("1990-2000")] + public void should_accept_supported_year_filters(string years) + { + CreateValidListSettings(years: years).Validate().IsValid.Should().BeTrue(); + CreateValidPopularSettings(years: years).Validate().IsValid.Should().BeTrue(); + CreateValidUserSettings(years: years).Validate().IsValid.Should().BeTrue(); + } + + [TestCase("234923498237423-234723477")] + [TestCase("199")] + [TestCase("1990-1980")] + [TestCase("1990 - 2000")] + public void should_reject_invalid_year_filters(string years) + { + CreateValidListSettings(years: years).Validate().IsValid.Should().BeFalse(); + CreateValidPopularSettings(years: years).Validate().IsValid.Should().BeFalse(); + CreateValidUserSettings(years: years).Validate().IsValid.Should().BeFalse(); + } + + [TestCase("genres=comedy")] + [TestCase("ratings=80-100")] + [TestCase("years=1990-2000")] + [TestCase("limit=10")] + public void should_reject_reserved_additional_parameters(string additionalParameters) + { + CreateValidListSettings(additionalParameters: additionalParameters).Validate().IsValid.Should().BeFalse(); + CreateValidPopularSettings(additionalParameters: additionalParameters).Validate().IsValid.Should().BeFalse(); + CreateValidUserSettings(additionalParameters: additionalParameters).Validate().IsValid.Should().BeFalse(); + } + + [Test] + public void should_allow_non_reserved_additional_parameters() + { + CreateValidListSettings(additionalParameters: "languages=en").Validate().IsValid.Should().BeTrue(); + CreateValidPopularSettings(additionalParameters: "languages=en").Validate().IsValid.Should().BeTrue(); + CreateValidUserSettings(additionalParameters: "languages=en").Validate().IsValid.Should().BeTrue(); + } + + [Test] + public void should_ignore_reserved_additional_parameters_when_building_filter_parameters() + { + var parameters = TraktQueryHelper.BuildFilterParameters( + "80-100", + "Drama", + "1990-2000", + 25, + "genres=comedy&ratings=10-20&years=2000-2010&limit=10&languages=en"); + + parameters.Should().ContainKey("genres").WhoseValue.Should().Be("drama"); + parameters.Should().ContainKey("ratings").WhoseValue.Should().Be("80-100"); + parameters.Should().ContainKey("years").WhoseValue.Should().Be("1990-2000"); + parameters.Should().ContainKey("limit").WhoseValue.Should().Be("25"); + parameters.Should().ContainKey("languages").WhoseValue.Should().Be("en"); + parameters.Should().HaveCount(5); + } + + private static TraktListSettings CreateValidListSettings(string rating = "80-100", string years = "1990-2000", string additionalParameters = "languages=en") + { + return new TraktListSettings + { + AccessToken = "access-token", + RefreshToken = "refresh-token", + Expires = DateTime.UtcNow.AddDays(1), + Username = "sonarr", + Listname = "watchlist", + Rating = rating, + Years = years, + TraktAdditionalParameters = additionalParameters + }; + } + + private static TraktPopularSettings CreateValidPopularSettings(string rating = "80-100", string years = "1990-2000", string additionalParameters = "languages=en") + { + return new TraktPopularSettings + { + AccessToken = "access-token", + RefreshToken = "refresh-token", + Expires = DateTime.UtcNow.AddDays(1), + Rating = rating, + Years = years, + TraktAdditionalParameters = additionalParameters + }; + } + + private static TraktUserSettings CreateValidUserSettings(string rating = "80-100", string years = "1990-2000", string additionalParameters = "languages=en") + { + return new TraktUserSettings + { + AccessToken = "access-token", + RefreshToken = "refresh-token", + Expires = DateTime.UtcNow.AddDays(1), + AuthUser = "sonarr-user", + Rating = rating, + Years = years, + TraktAdditionalParameters = additionalParameters + }; + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/ImportLists/Trakt/List/TraktListRequestGenerator.cs b/src/NzbDrone.Core/ImportLists/Trakt/List/TraktListRequestGenerator.cs index 08c09d9af..5623e8faa 100644 --- a/src/NzbDrone.Core/ImportLists/Trakt/List/TraktListRequestGenerator.cs +++ b/src/NzbDrone.Core/ImportLists/Trakt/List/TraktListRequestGenerator.cs @@ -20,21 +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?limit={Settings.Limit}"; + requestBuilder + .Resource("/users/{userName}/lists/{listName}/items/show,season,episode") + .SetSegment("userName", Settings.Username.Trim()) + .SetSegment("listName", Settings.Listname.ToUrlSlug()) + .Accept(HttpAccept.Json); - var request = new ImportListRequest(link, HttpAccept.Json); + var filterParams = TraktQueryHelper.BuildFilterParameters(Settings.Rating, Settings.Genres, Settings.Years, Settings.Limit, Settings.TraktAdditionalParameters); - request.HttpRequest.Headers.Add("trakt-api-version", "2"); - request.HttpRequest.Headers.Add("trakt-api-key", ClientId); + foreach (var param in filterParams) + { + requestBuilder.AddQueryParam(param.Key, param.Value); + } + + 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 59ac1acd8..4d3100f24 100644 --- a/src/NzbDrone.Core/ImportLists/Trakt/List/TraktListSettings.cs +++ b/src/NzbDrone.Core/ImportLists/Trakt/List/TraktListSettings.cs @@ -1,4 +1,5 @@ using FluentValidation; +using NzbDrone.Common.Extensions; using NzbDrone.Core.Annotations; using NzbDrone.Core.Validation; @@ -10,6 +11,11 @@ public TraktListSettingsValidator() { RuleFor(c => c.Username).NotEmpty(); RuleFor(c => c.Listname).NotEmpty(); + + RuleFor(c => c.Years) + .Must(BeValidYearRange) + .When(c => c.Years.IsNotNullOrWhiteSpace()) + .WithMessage("Not a valid year or range of years"); } } @@ -23,6 +29,9 @@ public class TraktListSettings : TraktSettingsBase [FieldDefinition(2, Label = "ImportListsTraktSettingsListName", HelpText = "ImportListsTraktSettingsListNameHelpText")] public string Listname { get; set; } + [FieldDefinition(3, Label = "ImportListsTraktSettingsYears", HelpText = "ImportListsTraktSettingsYearsSeriesHelpText")] + public string Years { 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 c9272b3b0..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; } - var filtersAndLimit = $"?years={Settings.Years}&genres={Settings.Genres?.ToLower()}&ratings={Settings.Rating}&limit={Settings.Limit}{Settings.TraktAdditionalParameters}"; - link += filtersAndLimit; + requestBuilder + .Resource(resource) + .Accept(HttpAccept.Json); - var request = new ImportListRequest(link, HttpAccept.Json); + var filterParams = TraktQueryHelper.BuildFilterParameters(Settings.Rating, Settings.Genres, Settings.Years, Settings.Limit, Settings.TraktAdditionalParameters); - request.HttpRequest.Headers.Add("trakt-api-version", "2"); - request.HttpRequest.Headers.Add("trakt-api-key", ClientId); + foreach (var param in filterParams) + { + requestBuilder.AddQueryParam(param.Key, param.Value); + } + + 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..1a8b2f227 100644 --- a/src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularSettings.cs +++ b/src/NzbDrone.Core/ImportLists/Trakt/Popular/TraktPopularSettings.cs @@ -1,4 +1,3 @@ -using System.Text.RegularExpressions; using FluentValidation; using NzbDrone.Common.Extensions; using NzbDrone.Core.Annotations; @@ -19,15 +18,8 @@ public TraktPopularSettingsValidator() .WithMessage("Yearly lists are no longer supported"); #pragma warning restore CS0612 - // Loose validation @TODO - RuleFor(c => c.Rating) - .Matches(@"^\d+\-\d+$", RegexOptions.IgnoreCase) - .When(c => c.Rating.IsNotNullOrWhiteSpace()) - .WithMessage("Not a valid rating"); - - // Loose validation @TODO RuleFor(c => c.Years) - .Matches(@"^\d+(\-\d+)?$", RegexOptions.IgnoreCase) + .Must(BeValidYearRange) .When(c => c.Years.IsNotNullOrWhiteSpace()) .WithMessage("Not a valid year or range of years"); } @@ -45,18 +37,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 new file mode 100644 index 000000000..133068365 --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/Trakt/TraktQueryHelper.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Common.Extensions; + +namespace NzbDrone.Core.ImportLists.Trakt +{ + public static class TraktQueryHelper + { + private static readonly HashSet ReservedFilterParameters = new(StringComparer.OrdinalIgnoreCase) + { + "genres", + "ratings", + "years", + "limit" + }; + + public static Dictionary BuildFilterParameters(string rating, string genres, string years, int limit, string additionalParameters) + { + var parameters = new Dictionary(StringComparer.OrdinalIgnoreCase); + + if (additionalParameters.IsNotNullOrWhiteSpace()) + { + var trimmed = additionalParameters.TrimStart('?').TrimStart('&'); + + foreach (var param in trimmed.Split('&', StringSplitOptions.RemoveEmptyEntries)) + { + var parts = param.Split('=', 2); + + if (parts.Length == 2 && parts[0].IsNotNullOrWhiteSpace()) + { + var key = parts[0].Trim(); + + if (ReservedFilterParameters.Contains(key)) + { + continue; + } + + parameters[key] = parts[1].Trim(); + } + } + } + + if (genres.IsNotNullOrWhiteSpace()) + { + parameters["genres"] = genres.ToLower(); + } + + if (rating.IsNotNullOrWhiteSpace()) + { + parameters["ratings"] = rating; + } + + if (years.IsNotNullOrWhiteSpace()) + { + parameters["years"] = years; + } + + parameters["limit"] = limit.ToString(); + + return parameters; + } + + public static bool ContainsReservedFilterParameters(string additionalParameters) + { + if (additionalParameters.IsNullOrWhiteSpace()) + { + return false; + } + + var trimmed = additionalParameters.TrimStart('?').TrimStart('&'); + + foreach (var param in trimmed.Split('&', StringSplitOptions.RemoveEmptyEntries)) + { + var parts = param.Split('=', 2); + + if (parts.Length == 2 && parts[0].IsNotNullOrWhiteSpace() && ReservedFilterParameters.Contains(parts[0].Trim())) + { + return true; + } + } + + return false; + } + + public static string ToQueryString(this Dictionary parameters) + { + return string.Join("&", parameters.Where(p => p.Value.IsNotNullOrWhiteSpace()).Select(p => $"{p.Key}={p.Value}")); + } + } +} diff --git a/src/NzbDrone.Core/ImportLists/Trakt/TraktSettingsBase.cs b/src/NzbDrone.Core/ImportLists/Trakt/TraktSettingsBase.cs index 03834afa6..c7757fbad 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.Globalization; using FluentValidation; using NzbDrone.Common.Extensions; using NzbDrone.Core.Annotations; @@ -31,6 +32,73 @@ public TraktSettingsBaseValidator() RuleFor(c => c.Limit) .GreaterThan(0) .WithMessage("Must be integer greater than 0"); + + RuleFor(c => c.Rating) + .Must(BeValidRatingRange) + .When(c => c.Rating.IsNotNullOrWhiteSpace()) + .WithMessage("Not a valid rating"); + + RuleFor(c => c.TraktAdditionalParameters) + .Must(additionalParameters => !TraktQueryHelper.ContainsReservedFilterParameters(additionalParameters)) + .When(c => c.TraktAdditionalParameters.IsNotNullOrWhiteSpace()) + .WithMessage("Additional parameters cannot include genres, ratings, years, or limit"); + } + + protected static bool BeValidYearRange(string years) + { + var parts = years.Split('-', StringSplitOptions.None); + + if (parts.Length == 1) + { + return TryParseYear(parts[0], out _); + } + + if (parts.Length != 2) + { + return false; + } + + return TryParseYear(parts[0], out var startYear) && + TryParseYear(parts[1], out var endYear) && + startYear <= endYear; + } + + private static bool BeValidRatingRange(string rating) + { + var parts = rating.Split('-', StringSplitOptions.None); + + if (parts.Length != 2) + { + return false; + } + + return TryParseRating(parts[0], out var minimumRating) && + TryParseRating(parts[1], out var maximumRating) && + minimumRating <= maximumRating; + } + + private static bool TryParseYear(string token, out int year) + { + year = default; + + return token.Length == 4 && + int.TryParse(token, NumberStyles.None, CultureInfo.InvariantCulture, out year) && + year >= 1000; + } + + private static bool TryParseRating(string token, out int rating) + { + if (!int.TryParse(token, NumberStyles.None, CultureInfo.InvariantCulture, out rating)) + { + return false; + } + + if (rating is < 0 or > 100) + { + return false; + } + + return token == rating.ToString(CultureInfo.InvariantCulture); } } @@ -59,6 +127,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 4ed21a84c..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,36 +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(); + var filterParams = TraktQueryHelper.BuildFilterParameters(_settings.Rating, _settings.Genres, _settings.Years, _settings.Limit, _settings.TraktAdditionalParameters); - requestBuilder - .SetSegment("userName", userName) - .Accept(HttpAccept.Json) - .WithRateLimit(4) - .SetHeader("trakt-api-version", "2") - .SetHeader("trakt-api-key", _clientId) - .AddQueryParam("limit", _settings.Limit.ToString()); + // Add extended parameter for watched list + if (_settings.TraktListType == (int)TraktUserListType.UserWatchedList) + { + 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 a6ae19c4d..32e55cf40 100644 --- a/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserSettings.cs +++ b/src/NzbDrone.Core/ImportLists/Trakt/User/TraktUserSettings.cs @@ -1,4 +1,5 @@ using FluentValidation; +using NzbDrone.Common.Extensions; using NzbDrone.Core.Annotations; using NzbDrone.Core.Validation; @@ -11,6 +12,11 @@ public TraktUserSettingsValidator() RuleFor(c => c.TraktListType).NotNull(); RuleFor(c => c.TraktWatchedListType).NotNull(); RuleFor(c => c.AuthUser).NotEmpty(); + + RuleFor(c => c.Years) + .Must(BeValidYearRange) + .When(c => c.Years.IsNotNullOrWhiteSpace()) + .WithMessage("Not a valid year or range of years"); } } @@ -37,6 +43,9 @@ public TraktUserSettings() [FieldDefinition(4, Label = "Username", HelpText = "ImportListsTraktSettingsUserListUsernameHelpText")] public string Username { get; set; } + [FieldDefinition(5, Label = "ImportListsTraktSettingsYears", HelpText = "ImportListsTraktSettingsYearsSeriesHelpText")] + public string Years { 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 5921e10b8..d4a50a186 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -957,7 +957,7 @@ "ImportListsTraktSettingsAdditionalParametersHelpText": "Additional Trakt API parameters", "ImportListsTraktSettingsAuthenticateWithTrakt": "Authenticate with Trakt", "ImportListsTraktSettingsGenres": "Genres", - "ImportListsTraktSettingsGenresSeriesHelpText": "Filter series by Trakt Genre Slug (Comma Separated) Only for Popular Lists", + "ImportListsTraktSettingsGenresSeriesHelpText": "Filter series by Trakt Genre slug, comma separated (action,comedy)", "ImportListsTraktSettingsLimit": "Limit", "ImportListsTraktSettingsLimitSeriesHelpText": "Limit the number of series to get", "ImportListsTraktSettingsListName": "List Name", @@ -992,7 +992,8 @@ "ImportListsTraktSettingsWatchedListTypeCompleted": "100% Watched", "ImportListsTraktSettingsWatchedListTypeInProgress": "In Progress", "ImportListsTraktSettingsYears": "Years", - "ImportListsTraktSettingsYearsSeriesHelpText": "Filter series by year or year range", + "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.",