mirror of
https://github.com/Sonarr/Sonarr.git
synced 2024-12-16 11:37:58 +02:00
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:
parent
3952ee402b
commit
0552b56b71
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -163,6 +163,7 @@
|
|||||||
<Compile Include="Download\DownloadClientTests\NzbgetTests\NzbgetFixture.cs" />
|
<Compile Include="Download\DownloadClientTests\NzbgetTests\NzbgetFixture.cs" />
|
||||||
<Compile Include="Download\DownloadClientTests\PneumaticProviderFixture.cs" />
|
<Compile Include="Download\DownloadClientTests\PneumaticProviderFixture.cs" />
|
||||||
<Compile Include="Download\DownloadClientTests\RTorrentTests\RTorrentFixture.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\SabnzbdTests\SabnzbdFixture.cs" />
|
||||||
<Compile Include="Download\DownloadClientTests\TransmissionTests\TransmissionFixture.cs" />
|
<Compile Include="Download\DownloadClientTests\TransmissionTests\TransmissionFixture.cs" />
|
||||||
<Compile Include="Download\DownloadClientTests\UTorrentTests\UTorrentFixture.cs" />
|
<Compile Include="Download\DownloadClientTests\UTorrentTests\UTorrentFixture.cs" />
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
261
src/NzbDrone.Core/Download/Clients/qBittorrent/QBittorrent.cs
Normal file
261
src/NzbDrone.Core/Download/Clients/qBittorrent/QBittorrent.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
namespace NzbDrone.Core.Download.Clients.QBittorrent
|
||||||
|
{
|
||||||
|
public enum QBittorrentPriority
|
||||||
|
{
|
||||||
|
Last = 0,
|
||||||
|
First = 1
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -356,7 +356,13 @@
|
|||||||
<Compile Include="Download\Clients\Nzbget\NzbgetSettings.cs" />
|
<Compile Include="Download\Clients\Nzbget\NzbgetSettings.cs" />
|
||||||
<Compile Include="Download\Clients\Pneumatic\Pneumatic.cs" />
|
<Compile Include="Download\Clients\Pneumatic\Pneumatic.cs" />
|
||||||
<Compile Include="Download\Clients\Pneumatic\PneumaticSettings.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\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\SabnzbdPriorityTypeConverter.cs" />
|
||||||
<Compile Include="Download\Clients\Sabnzbd\JsonConverters\SabnzbdQueueTimeConverter.cs" />
|
<Compile Include="Download\Clients\Sabnzbd\JsonConverters\SabnzbdQueueTimeConverter.cs" />
|
||||||
<Compile Include="Download\Clients\Sabnzbd\Responses\SabnzbdRetryResponse.cs" />
|
<Compile Include="Download\Clients\Sabnzbd\Responses\SabnzbdRetryResponse.cs" />
|
||||||
|
Loading…
Reference in New Issue
Block a user