qbittorrent: client plugin based heavily on uTorrent

supports a minimum qBittorrent version of 3.2.4, and uses labels for
v3.3.0 and later

Signed-off-by: Casey Bodley <cbodley@gmail.com>
This commit is contained in:
Casey Bodley 2015-09-03 00:28:08 -04:00
parent 3952ee402b
commit 0552b56b71
9 changed files with 871 additions and 0 deletions

View File

@ -0,0 +1,297 @@
using System;
using System.Linq;
using System.Collections.Generic;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Http;
using NzbDrone.Core.MediaFiles.TorrentInfo;
using NzbDrone.Core.Download;
using NzbDrone.Core.Download.Clients.QBittorrent;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.Download.DownloadClientTests.QBittorrentTests
{
[TestFixture]
public class QBittorrentFixture : DownloadClientFixtureBase<QBittorrent>
{
[SetUp]
public void Setup()
{
Subject.Definition = new DownloadClientDefinition();
Subject.Definition.Settings = new QBittorrentSettings
{
Host = "127.0.0.1",
Port = 2222,
Username = "admin",
Password = "pass",
TvCategory = "tv"
};
Mocker.GetMock<ITorrentFileInfoReader>()
.Setup(s => s.GetHashFromTorrentFile(It.IsAny<Byte[]>()))
.Returns("CBC2F069FE8BB2F544EAE707D75BCD3DE9DCF951");
Mocker.GetMock<IHttpClient>()
.Setup(s => s.Get(It.IsAny<HttpRequest>()))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new Byte[0]));
}
protected void GivenRedirectToMagnet()
{
var httpHeader = new HttpHeader();
httpHeader["Location"] = "magnet:?xt=urn:btih:ZPBPA2P6ROZPKRHK44D5OW6NHXU5Z6KR&tr=udp";
Mocker.GetMock<IHttpClient>()
.Setup(s => s.Get(It.IsAny<HttpRequest>()))
.Returns<HttpRequest>(r => new HttpResponse(r, httpHeader, new Byte[0], System.Net.HttpStatusCode.SeeOther));
}
protected void GivenRedirectToTorrent()
{
var httpHeader = new HttpHeader();
httpHeader["Location"] = "http://test.sonarr.tv/not-a-real-torrent.torrent";
Mocker.GetMock<IHttpClient>()
.Setup(s => s.Get(It.Is<HttpRequest>(h => h.Url.AbsoluteUri == _downloadUrl)))
.Returns<HttpRequest>(r => new HttpResponse(r, httpHeader, new Byte[0], System.Net.HttpStatusCode.Found));
}
protected void GivenFailedDownload()
{
Mocker.GetMock<IQBittorrentProxy>()
.Setup(s => s.AddTorrentFromUrl(It.IsAny<string>(), It.IsAny<QBittorrentSettings>()))
.Throws<InvalidOperationException>();
}
protected void GivenSuccessfulDownload()
{
Mocker.GetMock<IQBittorrentProxy>()
.Setup(s => s.AddTorrentFromUrl(It.IsAny<string>(), It.IsAny<QBittorrentSettings>()))
.Callback(() =>
{
var torrent = new QBittorrentTorrent
{
Hash = "HASH",
Name = _title,
Size = 1000,
Progress = 1.0,
Eta = 8640000,
State = "queuedUP",
Label = "",
SavePath = ""
};
GivenTorrents(new List<QBittorrentTorrent> { torrent });
});
}
protected virtual void GivenTorrents(List<QBittorrentTorrent> torrents)
{
if (torrents == null)
torrents = new List<QBittorrentTorrent>();
Mocker.GetMock<IQBittorrentProxy>()
.Setup(s => s.GetTorrents(It.IsAny<QBittorrentSettings>()))
.Returns(torrents);
}
[Test]
public void error_item_should_have_required_properties()
{
var torrent = new QBittorrentTorrent
{
Hash = "HASH",
Name = _title,
Size = 1000,
Progress = 0.7,
Eta = 8640000,
State = "error",
Label = "",
SavePath = ""
};
GivenTorrents(new List<QBittorrentTorrent> { torrent });
var item = Subject.GetItems().Single();
VerifyFailed(item);
}
[Test]
public void paused_item_should_have_required_properties()
{
var torrent = new QBittorrentTorrent
{
Hash = "HASH",
Name = _title,
Size = 1000,
Progress = 0.7,
Eta = 8640000,
State = "pausedDL",
Label = "",
SavePath = ""
};
GivenTorrents(new List<QBittorrentTorrent> { torrent });
var item = Subject.GetItems().Single();
VerifyPaused(item);
item.RemainingTime.Should().NotBe(TimeSpan.Zero);
}
[TestCase("pausedUP")]
[TestCase("queuedUP")]
[TestCase("uploading")]
[TestCase("stalledUP")]
[TestCase("checkingUP")]
public void completed_item_should_have_required_properties(string state)
{
var torrent = new QBittorrentTorrent
{
Hash = "HASH",
Name = _title,
Size = 1000,
Progress = 1.0,
Eta = 8640000,
State = state,
Label = "",
SavePath = ""
};
GivenTorrents(new List<QBittorrentTorrent> { torrent });
var item = Subject.GetItems().Single();
VerifyCompleted(item);
item.RemainingTime.Should().Be(TimeSpan.Zero);
}
[TestCase("queuedDL")]
[TestCase("checkingDL")]
public void queued_item_should_have_required_properties(string state)
{
var torrent = new QBittorrentTorrent
{
Hash = "HASH",
Name = _title,
Size = 1000,
Progress = 0.7,
Eta = 8640000,
State = state,
Label = "",
SavePath = ""
};
GivenTorrents(new List<QBittorrentTorrent> { torrent });
var item = Subject.GetItems().Single();
VerifyQueued(item);
item.RemainingTime.Should().NotBe(TimeSpan.Zero);
}
[Test]
public void downloading_item_should_have_required_properties()
{
var torrent = new QBittorrentTorrent
{
Hash = "HASH",
Name = _title,
Size = 1000,
Progress = 0.7,
Eta = 60,
State = "downloading",
Label = "",
SavePath = ""
};
GivenTorrents(new List<QBittorrentTorrent> { torrent });
var item = Subject.GetItems().Single();
VerifyDownloading(item);
item.RemainingTime.Should().NotBe(TimeSpan.Zero);
}
[Test]
public void stalledDL_item_should_have_required_properties()
{
var torrent = new QBittorrentTorrent
{
Hash = "HASH",
Name = _title,
Size = 1000,
Progress = 0.7,
Eta = 8640000,
State = "stalledDL",
Label = "",
SavePath = ""
};
GivenTorrents(new List<QBittorrentTorrent> { torrent });
var item = Subject.GetItems().Single();
VerifyWarning(item);
item.RemainingTime.Should().NotBe(TimeSpan.Zero);
}
[Test]
public void Download_should_return_unique_id()
{
GivenSuccessfulDownload();
var remoteEpisode = CreateRemoteEpisode();
var id = Subject.Download(remoteEpisode);
id.Should().NotBeNullOrEmpty();
}
[TestCase("magnet:?xt=urn:btih:ZPBPA2P6ROZPKRHK44D5OW6NHXU5Z6KR&tr=udp", "CBC2F069FE8BB2F544EAE707D75BCD3DE9DCF951")]
public void Download_should_get_hash_from_magnet_url(string magnetUrl, string expectedHash)
{
GivenSuccessfulDownload();
var remoteEpisode = CreateRemoteEpisode();
remoteEpisode.Release.DownloadUrl = magnetUrl;
var id = Subject.Download(remoteEpisode);
id.Should().Be(expectedHash);
}
[Test]
public void should_return_status_with_outputdirs()
{
var configItems = new Dictionary<string, Object>();
configItems.Add("save_path", @"C:\Downloads\Finished\QBittorrent".AsOsAgnostic());
Mocker.GetMock<IQBittorrentProxy>()
.Setup(v => v.GetConfig(It.IsAny<QBittorrentSettings>()))
.Returns(configItems);
var result = Subject.GetStatus();
result.IsLocalhost.Should().BeTrue();
result.OutputRootFolders.Should().NotBeNull();
result.OutputRootFolders.First().Should().Be(@"C:\Downloads\Finished\QBittorrent".AsOsAgnostic());
}
[Test]
public void Download_should_handle_http_redirect_to_magnet()
{
GivenRedirectToMagnet();
GivenSuccessfulDownload();
var remoteEpisode = CreateRemoteEpisode();
var id = Subject.Download(remoteEpisode);
id.Should().NotBeNullOrEmpty();
}
[Test]
public void Download_should_handle_http_redirect_to_torrent()
{
GivenRedirectToTorrent();
GivenSuccessfulDownload();
var remoteEpisode = CreateRemoteEpisode();
var id = Subject.Download(remoteEpisode);
id.Should().NotBeNullOrEmpty();
}
}
}

View File

@ -163,6 +163,7 @@
<Compile Include="Download\DownloadClientTests\NzbgetTests\NzbgetFixture.cs" />
<Compile Include="Download\DownloadClientTests\PneumaticProviderFixture.cs" />
<Compile Include="Download\DownloadClientTests\RTorrentTests\RTorrentFixture.cs" />
<Compile Include="Download\DownloadClientTests\QBittorrentTests\QBittorrentFixture.cs" />
<Compile Include="Download\DownloadClientTests\SabnzbdTests\SabnzbdFixture.cs" />
<Compile Include="Download\DownloadClientTests\TransmissionTests\TransmissionFixture.cs" />
<Compile Include="Download\DownloadClientTests\UTorrentTests\UTorrentFixture.cs" />

View File

@ -0,0 +1,22 @@
using RestSharp;
using System.Net;
namespace NzbDrone.Core.Download.Clients.QBittorrent
{
public class DigestAuthenticator : IAuthenticator
{
private readonly string _user;
private readonly string _pass;
public DigestAuthenticator(string user, string pass)
{
_user = user;
_pass = pass;
}
public void Authenticate(IRestClient client, IRestRequest request)
{
request.Credentials = new NetworkCredential(_user, _pass);
}
}
}

View File

@ -0,0 +1,261 @@
using System;
using System.Linq;
using System.Collections.Generic;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.MediaFiles.TorrentInfo;
using NLog;
using NzbDrone.Core.Validation;
using FluentValidation.Results;
using System.Net;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.RemotePathMappings;
namespace NzbDrone.Core.Download.Clients.QBittorrent
{
public class QBittorrent : TorrentClientBase<QBittorrentSettings>
{
private readonly IQBittorrentProxy _proxy;
public QBittorrent(IQBittorrentProxy proxy,
ITorrentFileInfoReader torrentFileInfoReader,
IHttpClient httpClient,
IConfigService configService,
IDiskProvider diskProvider,
IRemotePathMappingService remotePathMappingService,
Logger logger)
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, logger)
{
_proxy = proxy;
}
protected override string AddFromMagnetLink(RemoteEpisode remoteEpisode, string hash, string magnetLink)
{
_proxy.AddTorrentFromUrl(magnetLink, Settings);
if (Settings.TvCategory.IsNotNullOrWhiteSpace())
{
_proxy.SetTorrentLabel(hash.ToLower(), Settings.TvCategory, Settings);
}
var isRecentEpisode = remoteEpisode.IsRecentEpisode();
if (isRecentEpisode && Settings.RecentTvPriority == (int)QBittorrentPriority.First ||
!isRecentEpisode && Settings.OlderTvPriority == (int)QBittorrentPriority.First)
{
_proxy.MoveTorrentToTopInQueue(hash.ToLower(), Settings);
}
return hash;
}
protected override string AddFromTorrentFile(RemoteEpisode remoteEpisode, string hash, string filename, Byte[] fileContent)
{
_proxy.AddTorrentFromFile(filename, fileContent, Settings);
if (Settings.TvCategory.IsNotNullOrWhiteSpace())
{
_proxy.SetTorrentLabel(hash.ToLower(), Settings.TvCategory, Settings);
}
var isRecentEpisode = remoteEpisode.IsRecentEpisode();
if (isRecentEpisode && Settings.RecentTvPriority == (int)QBittorrentPriority.First ||
!isRecentEpisode && Settings.OlderTvPriority == (int)QBittorrentPriority.First)
{
_proxy.MoveTorrentToTopInQueue(hash.ToLower(), Settings);
}
return hash;
}
public override string Name
{
get
{
return "qBittorrent";
}
}
public override IEnumerable<DownloadClientItem> GetItems()
{
List<QBittorrentTorrent> torrents;
try
{
torrents = _proxy.GetTorrents(Settings);
}
catch (DownloadClientException ex)
{
_logger.ErrorException(ex.Message, ex);
return Enumerable.Empty<DownloadClientItem>();
}
var queueItems = new List<DownloadClientItem>();
foreach (var torrent in torrents)
{
var item = new DownloadClientItem();
item.DownloadId = torrent.Hash.ToUpper();
item.Category = torrent.Label;
item.Title = torrent.Name;
item.TotalSize = torrent.Size;
item.DownloadClient = Definition.Name;
item.RemainingSize = (long)(torrent.Size * (1.0 - torrent.Progress));
item.RemainingTime = TimeSpan.FromSeconds(torrent.Eta);
item.OutputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(torrent.SavePath));
if (!item.OutputPath.IsEmpty && item.OutputPath.FileName != torrent.Name)
{
item.OutputPath += torrent.Name;
}
switch (torrent.State)
{
case "error": // some error occurred, applies to paused torrents
item.Status = DownloadItemStatus.Failed;
item.Message = "QBittorrent is reporting an error";
break;
case "pausedDL": // torrent is paused and has NOT finished downloading
item.Status = DownloadItemStatus.Paused;
break;
case "queuedDL": // queuing is enabled and torrent is queued for download
case "checkingDL": // same as checkingUP, but torrent has NOT finished downloading
item.Status = DownloadItemStatus.Queued;
break;
case "pausedUP": // torrent is paused and has finished downloading
case "uploading": // torrent is being seeded and data is being transfered
case "stalledUP": // torrent is being seeded, but no connection were made
case "queuedUP": // queuing is enabled and torrent is queued for upload
case "checkingUP": // torrent has finished downloading and is being checked
item.Status = DownloadItemStatus.Completed;
item.RemainingTime = TimeSpan.Zero; // qBittorrent sends eta=8640000 for completed torrents
break;
case "stalledDL": // torrent is being downloaded, but no connection were made
item.Status = DownloadItemStatus.Warning;
item.Message = "The download is stalled with no connections";
break;
case "downloading": // torrent is being downloaded and data is being transfered
default: // new status in API? default to downloading
item.Status = DownloadItemStatus.Downloading;
break;
}
queueItems.Add(item);
}
return queueItems;
}
public override void RemoveItem(string hash, bool deleteData)
{
_proxy.RemoveTorrent(hash.ToLower(), deleteData, Settings);
}
public override DownloadClientStatus GetStatus()
{
var config = _proxy.GetConfig(Settings);
var destDir = new OsPath((string)config.GetValueOrDefault("save_path"));
return new DownloadClientStatus
{
IsLocalhost = Settings.Host == "127.0.0.1" || Settings.Host == "localhost",
OutputRootFolders = new List<OsPath> { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, destDir) }
};
}
protected override void Test(List<ValidationFailure> failures)
{
failures.AddIfNotNull(TestConnection());
if (failures.Any()) return;
failures.AddIfNotNull(TestGetTorrents());
}
private ValidationFailure TestConnection()
{
try
{
var version = _proxy.GetVersion(Settings);
if (version < 5)
{
// API version 5 introduced the "save_path" property in /query/torrents
return new NzbDroneValidationFailure("Host", "Unsupported client version")
{
DetailedDescription = "Please upgrade to qBittorrent version 3.2.4 or higher."
};
}
else if (version < 6)
{
// API version 6 introduced support for labels
if (Settings.TvCategory.IsNotNullOrWhiteSpace())
{
return new NzbDroneValidationFailure("Category", "Category is not supported")
{
DetailedDescription = "Labels are not supported until qBittorrent version 3.3.0. Please upgrade or try again with an empty Category."
};
}
}
else if (Settings.TvCategory.IsNullOrWhiteSpace())
{
// warn if labels are supported, but category is not provided
return new NzbDroneValidationFailure("TvCategory", "Category is recommended")
{
IsWarning = true,
DetailedDescription = "Sonarr will not attempt to import completed downloads without a category."
};
}
}
catch (DownloadClientAuthenticationException ex)
{
_logger.ErrorException(ex.Message, ex);
return new NzbDroneValidationFailure("Username", "Authentication failure")
{
DetailedDescription = "Please verify your username and password."
};
}
catch (WebException ex)
{
_logger.ErrorException(ex.Message, ex);
if (ex.Status == WebExceptionStatus.ConnectFailure)
{
return new NzbDroneValidationFailure("Host", "Unable to connect")
{
DetailedDescription = "Please verify the hostname and port."
};
}
return new NzbDroneValidationFailure(String.Empty, "Unknown exception: " + ex.Message);
}
catch (Exception ex)
{
_logger.ErrorException(ex.Message, ex);
return new NzbDroneValidationFailure(String.Empty, "Unknown exception: " + ex.Message);
}
return null;
}
private ValidationFailure TestGetTorrents()
{
try
{
_proxy.GetTorrents(Settings);
}
catch (Exception ex)
{
_logger.ErrorException(ex.Message, ex);
return new NzbDroneValidationFailure(String.Empty, "Failed to get the list of torrents: " + ex.Message);
}
return null;
}
}
}

View File

@ -0,0 +1,8 @@
namespace NzbDrone.Core.Download.Clients.QBittorrent
{
public enum QBittorrentPriority
{
Last = 0,
First = 1
}
}

View File

@ -0,0 +1,192 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Rest;
using RestSharp;
using NzbDrone.Common.Cache;
namespace NzbDrone.Core.Download.Clients.QBittorrent
{
// API https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-Documentation
public interface IQBittorrentProxy
{
int GetVersion(QBittorrentSettings settings);
Dictionary<string, Object> GetConfig(QBittorrentSettings settings);
List<QBittorrentTorrent> GetTorrents(QBittorrentSettings settings);
void AddTorrentFromUrl(string torrentUrl, QBittorrentSettings settings);
void AddTorrentFromFile(string fileName, Byte[] fileContent, QBittorrentSettings settings);
void RemoveTorrent(string hash, Boolean removeData, QBittorrentSettings settings);
void SetTorrentLabel(string hash, string label, QBittorrentSettings settings);
void MoveTorrentToTopInQueue(string hash, QBittorrentSettings settings);
}
public class QBittorrentProxy : IQBittorrentProxy
{
private readonly Logger _logger;
private readonly CookieContainer _cookieContainer;
private readonly ICached<bool> _logins;
private readonly TimeSpan _loginTimeout = TimeSpan.FromSeconds(10);
public QBittorrentProxy(ICacheManager cacheManager, Logger logger)
{
_logger = logger;
_cookieContainer = new CookieContainer();
_logins = cacheManager.GetCache<bool>(GetType(), "logins");
}
public int GetVersion(QBittorrentSettings settings)
{
var request = new RestRequest("/version/api", Method.GET);
var client = BuildClient(settings);
var response = ProcessRequest(client, request, settings);
response.ValidateResponse(client);
return Convert.ToInt32(response.Content);
}
public Dictionary<string, Object> GetConfig(QBittorrentSettings settings)
{
var request = new RestRequest("/query/preferences", Method.GET);
request.RequestFormat = DataFormat.Json;
var client = BuildClient(settings);
var response = ProcessRequest(client, request, settings);
response.ValidateResponse(client);
return response.Read<Dictionary<string, Object>>(client);
}
public List<QBittorrentTorrent> GetTorrents(QBittorrentSettings settings)
{
var request = new RestRequest("/query/torrents", Method.GET);
request.RequestFormat = DataFormat.Json;
request.AddParameter("label", settings.TvCategory);
var client = BuildClient(settings);
var response = ProcessRequest(client, request, settings);
response.ValidateResponse(client);
return response.Read<List<QBittorrentTorrent>>(client);
}
public void AddTorrentFromUrl(string torrentUrl, QBittorrentSettings settings)
{
var request = new RestRequest("/command/download", Method.POST);
request.AddParameter("urls", torrentUrl);
var client = BuildClient(settings);
var response = ProcessRequest(client, request, settings);
response.ValidateResponse(client);
}
public void AddTorrentFromFile(string fileName, Byte[] fileContent, QBittorrentSettings settings)
{
var request = new RestRequest("/command/upload", Method.POST);
request.AddFile("torrents", fileContent, fileName);
var client = BuildClient(settings);
var response = ProcessRequest(client, request, settings);
response.ValidateResponse(client);
}
public void RemoveTorrent(string hash, Boolean removeData, QBittorrentSettings settings)
{
var cmd = removeData ? "/command/deletePerm" : "/command/delete";
var request = new RestRequest(cmd, Method.POST);
request.AddParameter("hashes", hash);
var client = BuildClient(settings);
var response = ProcessRequest(client, request, settings);
response.ValidateResponse(client);
}
public void SetTorrentLabel(string hash, string label, QBittorrentSettings settings)
{
var request = new RestRequest("/command/setLabel", Method.POST);
request.AddParameter("hashes", hash);
request.AddParameter("label", label);
var client = BuildClient(settings);
var response = ProcessRequest(client, request, settings);
response.ValidateResponse(client);
}
public void MoveTorrentToTopInQueue(string hash, QBittorrentSettings settings)
{
var request = new RestRequest("/command/topPrio", Method.POST);
request.AddParameter("hashes", hash);
var client = BuildClient(settings);
var response = ProcessRequest(client, request, settings);
// qBittorrent rejects all Prio commands with 403: Forbidden if Options -> BitTorrent -> Torrent Queueing is not enabled
if (response.StatusCode == HttpStatusCode.Forbidden)
{
return;
}
response.ValidateResponse(client);
}
private IRestResponse ProcessRequest(IRestClient client, IRestRequest request, QBittorrentSettings settings)
{
var response = client.Execute(request);
if (response.StatusCode == HttpStatusCode.Forbidden)
{
_logger.Info("Authentication required, logging in.");
var loggedIn = _logins.Get(settings.Username + settings.Password, () => Login(client, settings), _loginTimeout);
if (!loggedIn)
{
throw new DownloadClientAuthenticationException("Failed to authenticate");
}
// success! retry the original request
response = client.Execute(request);
}
return response;
}
private bool Login(IRestClient client, QBittorrentSettings settings)
{
var request = new RestRequest("/login", Method.POST);
request.AddParameter("username", settings.Username);
request.AddParameter("password", settings.Password);
var response = client.Execute(request);
if (response.StatusCode != HttpStatusCode.OK)
{
_logger.Warn("Login failed with {0}.", response.StatusCode);
return false;
}
if (response.Content != "Ok.") // returns "Fails." on bad login
{
_logger.Warn("Login failed, incorrect username or password.");
return false;
}
response.ValidateResponse(client);
return true;
}
private IRestClient BuildClient(QBittorrentSettings settings)
{
var protocol = settings.UseSsl ? "https" : "http";
var url = String.Format(@"{0}://{1}:{2}", protocol, settings.Host, settings.Port);
var client = RestClientFactory.BuildClient(url);
client.Authenticator = new DigestAuthenticator(settings.Username, settings.Password);
client.CookieContainer = _cookieContainer;
return client;
}
}
}

View File

@ -0,0 +1,58 @@
using System;
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Download.Clients.QBittorrent
{
public class QBittorrentSettingsValidator : AbstractValidator<QBittorrentSettings>
{
public QBittorrentSettingsValidator()
{
RuleFor(c => c.Host).ValidHost();
RuleFor(c => c.Port).InclusiveBetween(0, 65535);
}
}
public class QBittorrentSettings : IProviderConfig
{
private static readonly QBittorrentSettingsValidator Validator = new QBittorrentSettingsValidator();
public QBittorrentSettings()
{
Host = "localhost";
Port = 9091;
TvCategory = "tv-sonarr";
}
[FieldDefinition(0, Label = "Host", Type = FieldType.Textbox)]
public string Host { get; set; }
[FieldDefinition(1, Label = "Port", Type = FieldType.Textbox)]
public int Port { get; set; }
[FieldDefinition(2, Label = "Username", Type = FieldType.Textbox)]
public string Username { get; set; }
[FieldDefinition(3, Label = "Password", Type = FieldType.Password)]
public string Password { get; set; }
[FieldDefinition(4, Label = "Category", Type = FieldType.Textbox, HelpText = "Adding a category specific to Sonarr avoids conflicts with unrelated downloads, but it's optional")]
public string TvCategory { get; set; }
[FieldDefinition(5, Label = "Recent Priority", Type = FieldType.Select, SelectOptions = typeof(QBittorrentPriority), HelpText = "Priority to use when grabbing episodes that aired within the last 14 days")]
public int RecentTvPriority { get; set; }
[FieldDefinition(6, Label = "Older Priority", Type = FieldType.Select, SelectOptions = typeof(QBittorrentPriority), HelpText = "Priority to use when grabbing episodes that aired over 14 days ago")]
public int OlderTvPriority { get; set; }
[FieldDefinition(7, Label = "Use SSL", Type = FieldType.Checkbox, HelpText = "Use a secure connection. See Options -> Web UI -> 'Use HTTPS instead of HTTP' in qBittorrent.")]
public bool UseSsl { get; set; }
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}

View File

@ -0,0 +1,26 @@
using Newtonsoft.Json;
using System;
namespace NzbDrone.Core.Download.Clients.QBittorrent
{
// torrent properties from the list returned by /query/torrents
public class QBittorrentTorrent
{
public string Hash { get; set; } // Torrent hash
public string Name { get; set; } // Torrent name
public long Size { get; set; } // Torrent size (bytes)
public double Progress { get; set; } // Torrent progress (%/100)
public int Eta { get; set; } // Torrent ETA (seconds)
public string State { get; set; } // Torrent state. See possible values here below
public string Label { get; set; } // Label of the torrent
[JsonProperty(PropertyName = "save_path")]
public string SavePath { get; set; } // Torrent save path
}
}

View File

@ -356,7 +356,13 @@
<Compile Include="Download\Clients\Nzbget\NzbgetSettings.cs" />
<Compile Include="Download\Clients\Pneumatic\Pneumatic.cs" />
<Compile Include="Download\Clients\Pneumatic\PneumaticSettings.cs" />
<Compile Include="Download\Clients\qBittorrent\DigestAuthenticator.cs" />
<Compile Include="Download\Clients\rTorrent\RTorrentDirectoryValidator.cs" />
<Compile Include="Download\Clients\qBittorrent\QBittorrent.cs" />
<Compile Include="Download\Clients\qBittorrent\QBittorrentPriority.cs" />
<Compile Include="Download\Clients\qBittorrent\QBittorrentProxy.cs" />
<Compile Include="Download\Clients\qBittorrent\QBittorrentSettings.cs" />
<Compile Include="Download\Clients\qBittorrent\QBittorrentTorrent.cs" />
<Compile Include="Download\Clients\Sabnzbd\JsonConverters\SabnzbdPriorityTypeConverter.cs" />
<Compile Include="Download\Clients\Sabnzbd\JsonConverters\SabnzbdQueueTimeConverter.cs" />
<Compile Include="Download\Clients\Sabnzbd\Responses\SabnzbdRetryResponse.cs" />