Standardize trakt settings (#1)

* enforce consistency between generators

* separate additional parameters from manually added parameters. update years text between popular and user lists
This commit is contained in:
Andrew Ukkonen 2026-02-16 17:06:29 -06:00 committed by GitHub
parent 4a0cce15db
commit a410d0d57e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 93 additions and 121 deletions

View file

@ -20,24 +20,31 @@ public virtual ImportListPageableRequestChain GetListItems()
private IEnumerable<ImportListRequest> 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());
}
}
}

View file

@ -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<TraktListSettings>
[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));

View file

@ -21,63 +21,72 @@ public virtual ImportListPageableRequestChain GetListItems()
private IEnumerable<ImportListRequest> 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());
}
}
}

View file

@ -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));

View file

@ -7,16 +7,6 @@ namespace NzbDrone.Core.ImportLists.Trakt
{
public static class TraktQueryHelper
{
private static readonly HashSet<string> CommaSeparatedParams = new(StringComparer.OrdinalIgnoreCase)
{
"genres",
"certifications",
"networks",
"languages",
"countries",
"status"
};
public static Dictionary<string, string> BuildFilterParameters(string rating, string genres, string years, int limit, string additionalParameters)
{
var parameters = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
@ -32,38 +22,27 @@ public static Dictionary<string, string> 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<string>(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;

View file

@ -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; }

View file

@ -26,7 +26,9 @@ public virtual ImportListPageableRequestChain GetListItems()
private IEnumerable<ImportListRequest> 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<ImportListRequest> 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;
}
}
}

View file

@ -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));

View file

@ -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.",