diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs index 9ecf3d471..b7d7b2c40 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs @@ -474,8 +474,9 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent } catch (DownloadClientAuthenticationException ex) { - _logger.Error(ex, "Unable to authenticate"); - return new NzbDroneValidationFailure("Username", "Authentication failure") + _logger.Error(ex, ex.Message); + + return new NzbDroneValidationFailure(Settings.ApiKey.IsNotNullOrWhiteSpace() ? "ApiKey" : "Username", "Authentication failure") { DetailedDescription = "Please verify your username and password." }; diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs index 0940d1231..186566361 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentProxyV2.cs @@ -337,13 +337,19 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent 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) { LogResponseContent = true, StoreRequestCookie = false }; + + if (settings.ApiKey.IsNotNullOrWhiteSpace()) + { + requestBuilder.Headers["Authorization"] = $"Bearer {settings.ApiKey}"; + } + return requestBuilder; } @@ -357,16 +363,39 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent private string ProcessRequest(HttpRequestBuilder requestBuilder, QBittorrentSettings settings) { - AuthenticateClient(requestBuilder, settings); - var request = requestBuilder.Build(); request.LogResponseContent = true; + + if (settings.ApiKey.IsNotNullOrWhiteSpace()) + { + try + { + 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 }; - HttpResponse response; try { - response = _httpClient.Execute(request); + var response = _httpClient.Execute(request); if (response.StatusCode == HttpStatusCode.Forbidden) { @@ -378,17 +407,17 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent response = _httpClient.Execute(request); } + + return response.Content; } catch (HttpException 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); } - - return response.Content; } private void AuthenticateClient(HttpRequestBuilder requestBuilder, QBittorrentSettings settings, bool reauthenticate = false) diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs index c2881508a..07efd8179 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrentSettings.cs @@ -14,6 +14,13 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent RuleFor(c => c.Port).InclusiveBetween(1, 65535); 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.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")] 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; } - [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; } - [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; } - [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; } - [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; } - [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; } - [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; } - [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; } - [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; } - [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 NzbDroneValidationResult Validate()