New: API key support for qBittorrent

This commit is contained in:
Bogdan
2026-01-07 05:13:25 +02:00
committed by Auggie
parent b106629afd
commit 498de3fc51
3 changed files with 60 additions and 20 deletions
@@ -474,8 +474,9 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
} }
catch (DownloadClientAuthenticationException ex) catch (DownloadClientAuthenticationException ex)
{ {
_logger.Error(ex, "Unable to authenticate"); _logger.Error(ex, ex.Message);
return new NzbDroneValidationFailure("Username", "Authentication failure")
return new NzbDroneValidationFailure(Settings.ApiKey.IsNotNullOrWhiteSpace() ? "ApiKey" : "Username", "Authentication failure")
{ {
DetailedDescription = "Please verify your username and password." DetailedDescription = "Please verify your username and password."
}; };
@@ -337,13 +337,19 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
ProcessRequest(request, settings); ProcessRequest(request, settings);
} }
private HttpRequestBuilder BuildRequest(QBittorrentSettings settings) private static HttpRequestBuilder BuildRequest(QBittorrentSettings settings)
{ {
var requestBuilder = new HttpRequestBuilder(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase) var requestBuilder = new HttpRequestBuilder(settings.UseSsl, settings.Host, settings.Port, settings.UrlBase)
{ {
LogResponseContent = true, LogResponseContent = true,
StoreRequestCookie = false StoreRequestCookie = false
}; };
if (settings.ApiKey.IsNotNullOrWhiteSpace())
{
requestBuilder.Headers["Authorization"] = $"Bearer {settings.ApiKey}";
}
return requestBuilder; return requestBuilder;
} }
@@ -357,16 +363,39 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
private string ProcessRequest(HttpRequestBuilder requestBuilder, QBittorrentSettings settings) private string ProcessRequest(HttpRequestBuilder requestBuilder, QBittorrentSettings settings)
{ {
AuthenticateClient(requestBuilder, settings);
var request = requestBuilder.Build(); var request = requestBuilder.Build();
request.LogResponseContent = true; request.LogResponseContent = true;
request.SuppressHttpErrorStatusCodes = new[] { HttpStatusCode.Forbidden };
HttpResponse response; if (settings.ApiKey.IsNotNullOrWhiteSpace())
{
try try
{ {
response = _httpClient.Execute(request); return _httpClient.Execute(request).Content;
}
catch (HttpException ex)
{
if (ex.Response.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden)
{
_logger.Debug(ex, "qbitTorrent authentication failed.");
throw new DownloadClientAuthenticationException("Failed to authenticate with qBittorrent.", ex);
}
throw new DownloadClientException("Failed to connect to qBittorrent, check your settings.", ex);
}
catch (Exception ex)
{
throw new DownloadClientException("Failed to connect to qBittorrent, please check your settings.", ex);
}
}
AuthenticateClient(requestBuilder, settings);
request.SuppressHttpErrorStatusCodes = new[] { HttpStatusCode.Forbidden };
try
{
var response = _httpClient.Execute(request);
if (response.StatusCode == HttpStatusCode.Forbidden) if (response.StatusCode == HttpStatusCode.Forbidden)
{ {
@@ -378,17 +407,17 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
response = _httpClient.Execute(request); response = _httpClient.Execute(request);
} }
return response.Content;
} }
catch (HttpException ex) catch (HttpException ex)
{ {
throw new DownloadClientException("Failed to connect to qBittorrent, check your settings.", ex); throw new DownloadClientException("Failed to connect to qBittorrent, check your settings.", ex);
} }
catch (WebException ex) catch (Exception ex)
{ {
throw new DownloadClientException("Failed to connect to qBittorrent, please check your settings.", ex); throw new DownloadClientException("Failed to connect to qBittorrent, please check your settings.", ex);
} }
return response.Content;
} }
private void AuthenticateClient(HttpRequestBuilder requestBuilder, QBittorrentSettings settings, bool reauthenticate = false) private void AuthenticateClient(HttpRequestBuilder requestBuilder, QBittorrentSettings settings, bool reauthenticate = false)
@@ -14,6 +14,13 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
RuleFor(c => c.Port).InclusiveBetween(1, 65535); RuleFor(c => c.Port).InclusiveBetween(1, 65535);
RuleFor(c => c.UrlBase).ValidUrlBase().When(c => c.UrlBase.IsNotNullOrWhiteSpace()); RuleFor(c => c.UrlBase).ValidUrlBase().When(c => c.UrlBase.IsNotNullOrWhiteSpace());
RuleFor(c => c.Username).Empty()
.WithMessage("Username must be empty when using API Key.")
.When(c => c.ApiKey.IsNotNullOrWhiteSpace());
RuleFor(c => c.Password).Empty()
.WithMessage("Password must be empty when using API Key.")
.When(c => c.ApiKey.IsNotNullOrWhiteSpace());
RuleFor(c => c.MusicCategory).Matches(@"^([^\\\/](\/?[^\\\/])*)?$").WithMessage(@"Can not contain '\', '//', or start/end with '/'"); RuleFor(c => c.MusicCategory).Matches(@"^([^\\\/](\/?[^\\\/])*)?$").WithMessage(@"Can not contain '\', '//', or start/end with '/'");
RuleFor(c => c.MusicImportedCategory).Matches(@"^([^\\\/](\/?[^\\\/])*)?$").WithMessage(@"Can not contain '\', '//', or start/end with '/'"); RuleFor(c => c.MusicImportedCategory).Matches(@"^([^\\\/](\/?[^\\\/])*)?$").WithMessage(@"Can not contain '\', '//', or start/end with '/'");
} }
@@ -42,34 +49,37 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
[FieldDefinition(3, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the qBittorrent url, e.g. http://[host]:[port]/[urlBase]/api")] [FieldDefinition(3, Label = "Url Base", Type = FieldType.Textbox, Advanced = true, HelpText = "Adds a prefix to the qBittorrent url, e.g. http://[host]:[port]/[urlBase]/api")]
public string UrlBase { get; set; } public string UrlBase { get; set; }
[FieldDefinition(4, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)] [FieldDefinition(4, Label = "ApiKey", Type = FieldType.Textbox, Privacy = PrivacyLevel.ApiKey)]
public string ApiKey { get; set; }
[FieldDefinition(5, Label = "Username", Type = FieldType.Textbox, Privacy = PrivacyLevel.UserName)]
public string Username { get; set; } public string Username { get; set; }
[FieldDefinition(5, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)] [FieldDefinition(6, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
public string Password { get; set; } public string Password { get; set; }
[FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Lidarr avoids conflicts with unrelated downloads, but it's optional")] [FieldDefinition(7, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Lidarr avoids conflicts with unrelated downloads, but it's optional")]
public string MusicCategory { get; set; } public string MusicCategory { get; set; }
[FieldDefinition(7, Label = "PostImportCategory", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientSettingsPostImportCategoryHelpText")] [FieldDefinition(8, Label = "PostImportCategory", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientSettingsPostImportCategoryHelpText")]
public string MusicImportedCategory { get; set; } public string MusicImportedCategory { get; set; }
[FieldDefinition(8, Label = "DownloadClientSettingsRecentPriority", Type = FieldType.Select, SelectOptions = typeof(QBittorrentPriority), HelpText = "DownloadClientSettingsRecentPriorityAlbumHelpText")] [FieldDefinition(9, Label = "DownloadClientSettingsRecentPriority", Type = FieldType.Select, SelectOptions = typeof(QBittorrentPriority), HelpText = "DownloadClientSettingsRecentPriorityAlbumHelpText")]
public int RecentMusicPriority { get; set; } public int RecentMusicPriority { get; set; }
[FieldDefinition(9, Label = "DownloadClientSettingsOlderPriority", Type = FieldType.Select, SelectOptions = typeof(QBittorrentPriority), HelpText = "DownloadClientSettingsOlderPriorityAlbumHelpText")] [FieldDefinition(10, Label = "DownloadClientSettingsOlderPriority", Type = FieldType.Select, SelectOptions = typeof(QBittorrentPriority), HelpText = "DownloadClientSettingsOlderPriorityAlbumHelpText")]
public int OlderMusicPriority { get; set; } public int OlderMusicPriority { get; set; }
[FieldDefinition(10, Label = "Initial State", Type = FieldType.Select, SelectOptions = typeof(QBittorrentState), HelpText = "Initial state for torrents added to qBittorrent")] [FieldDefinition(11, Label = "Initial State", Type = FieldType.Select, SelectOptions = typeof(QBittorrentState), HelpText = "Initial state for torrents added to qBittorrent")]
public int InitialState { get; set; } public int InitialState { get; set; }
[FieldDefinition(11, Label = "Sequential Order", Type = FieldType.Checkbox, HelpText = "Download in sequential order (qBittorrent 4.1.0+)")] [FieldDefinition(12, Label = "Sequential Order", Type = FieldType.Checkbox, HelpText = "Download in sequential order (qBittorrent 4.1.0+)")]
public bool SequentialOrder { get; set; } public bool SequentialOrder { get; set; }
[FieldDefinition(12, Label = "First and Last First", Type = FieldType.Checkbox, HelpText = "Download first and last pieces first (qBittorrent 4.1.0+)")] [FieldDefinition(13, Label = "First and Last First", Type = FieldType.Checkbox, HelpText = "Download first and last pieces first (qBittorrent 4.1.0+)")]
public bool FirstAndLast { get; set; } public bool FirstAndLast { get; set; }
[FieldDefinition(13, Label = "DownloadClientQbittorrentSettingsContentLayout", Type = FieldType.Select, SelectOptions = typeof(QBittorrentContentLayout), HelpText = "DownloadClientQbittorrentSettingsContentLayoutHelpText")] [FieldDefinition(14, Label = "DownloadClientQbittorrentSettingsContentLayout", Type = FieldType.Select, SelectOptions = typeof(QBittorrentContentLayout), HelpText = "DownloadClientQbittorrentSettingsContentLayoutHelpText")]
public int ContentLayout { get; set; } public int ContentLayout { get; set; }
public NzbDroneValidationResult Validate() public NzbDroneValidationResult Validate()