Merge b8007391b8
into 2f04b037a1
This commit is contained in:
commit
23bb02b48f
|
@ -0,0 +1,287 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using FluentAssertions;
|
||||||
|
using Moq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Common.Disk;
|
||||||
|
using NzbDrone.Common.Http;
|
||||||
|
using NzbDrone.Core.Download;
|
||||||
|
using NzbDrone.Core.Download.Clients.Putio;
|
||||||
|
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Test.Download.DownloadClientTests.PutioTests
|
||||||
|
{
|
||||||
|
public class PutioFixture : DownloadClientFixtureBase<Putio>
|
||||||
|
{
|
||||||
|
private PutioSettings _settings;
|
||||||
|
private PutioTorrent _queued;
|
||||||
|
private PutioTorrent _downloading;
|
||||||
|
private PutioTorrent _failed;
|
||||||
|
private PutioTorrent _completed;
|
||||||
|
private PutioTorrent _completed_different_parent;
|
||||||
|
private PutioTorrent _seeding;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
_settings = new PutioSettings
|
||||||
|
{
|
||||||
|
SaveParentId = "1"
|
||||||
|
};
|
||||||
|
|
||||||
|
Subject.Definition = new DownloadClientDefinition();
|
||||||
|
Subject.Definition.Settings = _settings;
|
||||||
|
|
||||||
|
_queued = new PutioTorrent
|
||||||
|
{
|
||||||
|
Hash = "HASH",
|
||||||
|
Id = 1,
|
||||||
|
Status = PutioTorrentStatus.InQueue,
|
||||||
|
Name = _title,
|
||||||
|
Size = 1000,
|
||||||
|
Downloaded = 0,
|
||||||
|
SaveParentId = 1
|
||||||
|
};
|
||||||
|
|
||||||
|
_downloading = new PutioTorrent
|
||||||
|
{
|
||||||
|
Hash = "HASH",
|
||||||
|
Id = 2,
|
||||||
|
Status = PutioTorrentStatus.Downloading,
|
||||||
|
Name = _title,
|
||||||
|
Size = 1000,
|
||||||
|
Downloaded = 980,
|
||||||
|
SaveParentId = 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
_failed = new PutioTorrent
|
||||||
|
{
|
||||||
|
Hash = "HASH",
|
||||||
|
Id = 3,
|
||||||
|
Status = PutioTorrentStatus.Error,
|
||||||
|
ErrorMessage = "Torrent has reached the maximum number of inactive days.",
|
||||||
|
Name = _title,
|
||||||
|
Size = 1000,
|
||||||
|
Downloaded = 980,
|
||||||
|
SaveParentId = 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
_completed = new PutioTorrent
|
||||||
|
{
|
||||||
|
Hash = "HASH",
|
||||||
|
Status = PutioTorrentStatus.Completed,
|
||||||
|
Id = 4,
|
||||||
|
Name = _title,
|
||||||
|
Size = 1000,
|
||||||
|
Downloaded = 1000,
|
||||||
|
SaveParentId = 1,
|
||||||
|
FileId = 2
|
||||||
|
};
|
||||||
|
|
||||||
|
_completed_different_parent = new PutioTorrent
|
||||||
|
{
|
||||||
|
Hash = "HASH",
|
||||||
|
Id = 5,
|
||||||
|
Status = PutioTorrentStatus.Completed,
|
||||||
|
Name = _title,
|
||||||
|
Size = 1000,
|
||||||
|
Downloaded = 1000,
|
||||||
|
SaveParentId = 2,
|
||||||
|
FileId = 3
|
||||||
|
};
|
||||||
|
|
||||||
|
_seeding = new PutioTorrent
|
||||||
|
{
|
||||||
|
Hash = "HASH",
|
||||||
|
Id = 6,
|
||||||
|
Status = PutioTorrentStatus.Seeding,
|
||||||
|
Name = _title,
|
||||||
|
Size = 1000,
|
||||||
|
Downloaded = 1000,
|
||||||
|
Uploaded = 1300,
|
||||||
|
SaveParentId = 1,
|
||||||
|
FileId = 4
|
||||||
|
};
|
||||||
|
|
||||||
|
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(), Array.Empty<byte>()));
|
||||||
|
|
||||||
|
Mocker.GetMock<IPutioProxy>()
|
||||||
|
.Setup(v => v.GetAccountSettings(It.IsAny<PutioSettings>()));
|
||||||
|
|
||||||
|
Mocker.GetMock<IPutioProxy>()
|
||||||
|
.Setup(v => v.GetFileListingResponse(It.IsAny<long>(), It.IsAny<PutioSettings>()))
|
||||||
|
.Returns(PutioFileListingResponse.Empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void GivenFailedDownload()
|
||||||
|
{
|
||||||
|
Mocker.GetMock<IPutioProxy>()
|
||||||
|
.Setup(s => s.AddTorrentFromUrl(It.IsAny<string>(), It.IsAny<PutioSettings>()))
|
||||||
|
.Throws<InvalidOperationException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void GivenSuccessfulDownload()
|
||||||
|
{
|
||||||
|
GivenRemoteFileStructure(new List<PutioFile>
|
||||||
|
{
|
||||||
|
new PutioFile { Id = _completed.FileId, Name = _title, FileType = PutioFile.FILE_TYPE_VIDEO },
|
||||||
|
new PutioFile { Id = _seeding.FileId, Name = _title, FileType = PutioFile.FILE_TYPE_FOLDER },
|
||||||
|
}, new PutioFile { Id = 1, Name = "Downloads" });
|
||||||
|
|
||||||
|
// GivenRemoteFileStructure(new List<PutioFile>
|
||||||
|
// {
|
||||||
|
// new PutioFile { Id = _completed_different_parent.FileId, Name = _title, FileType = PutioFile.FILE_TYPE_VIDEO },
|
||||||
|
// }, new PutioFile { Id = 2, Name = "Downloads_new" });
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void GivenTorrents(List<PutioTorrent> torrents)
|
||||||
|
{
|
||||||
|
torrents ??= new List<PutioTorrent>();
|
||||||
|
|
||||||
|
Mocker.GetMock<IPutioProxy>()
|
||||||
|
.Setup(s => s.GetTorrents(It.IsAny<PutioSettings>()))
|
||||||
|
.Returns(torrents);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void GivenRemoteFileStructure(List<PutioFile> files, PutioFile parentFile)
|
||||||
|
{
|
||||||
|
files ??= new List<PutioFile>();
|
||||||
|
var list = new PutioFileListingResponse { Files = files, Parent = parentFile };
|
||||||
|
|
||||||
|
Mocker.GetMock<IPutioProxy>()
|
||||||
|
.Setup(s => s.GetFileListingResponse(parentFile.Id, It.IsAny<PutioSettings>()))
|
||||||
|
.Returns(list);
|
||||||
|
|
||||||
|
Mocker.GetMock<IDiskProvider>()
|
||||||
|
.Setup(s => s.FolderExists(It.IsAny<string>()))
|
||||||
|
.Returns(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void GivenMetadata(List<PutioTorrentMetadata> metadata)
|
||||||
|
{
|
||||||
|
metadata ??= new List<PutioTorrentMetadata>();
|
||||||
|
var result = new Dictionary<string, PutioTorrentMetadata>();
|
||||||
|
foreach (var item in metadata)
|
||||||
|
{
|
||||||
|
result.Add(item.Id.ToString(), item);
|
||||||
|
}
|
||||||
|
|
||||||
|
Mocker.GetMock<IPutioProxy>()
|
||||||
|
.Setup(s => s.GetAllTorrentMetadata(It.IsAny<PutioSettings>()))
|
||||||
|
.Returns(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void getItems_contains_all_items()
|
||||||
|
{
|
||||||
|
GivenTorrents(new List<PutioTorrent>
|
||||||
|
{
|
||||||
|
_queued,
|
||||||
|
_downloading,
|
||||||
|
_failed,
|
||||||
|
_completed,
|
||||||
|
_seeding,
|
||||||
|
_completed_different_parent
|
||||||
|
});
|
||||||
|
GivenSuccessfulDownload();
|
||||||
|
|
||||||
|
var items = Subject.GetItems();
|
||||||
|
|
||||||
|
VerifyQueued(items.ElementAt(0));
|
||||||
|
VerifyDownloading(items.ElementAt(1));
|
||||||
|
VerifyWarning(items.ElementAt(2));
|
||||||
|
VerifyCompleted(items.ElementAt(3));
|
||||||
|
VerifyCompleted(items.ElementAt(4));
|
||||||
|
|
||||||
|
items.Should().HaveCount(5);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(1, 5)]
|
||||||
|
[TestCase(2, 1)]
|
||||||
|
[TestCase(3, 0)]
|
||||||
|
public void getItems_contains_only_items_with_matching_parent_id(long configuredParentId, int expectedCount)
|
||||||
|
{
|
||||||
|
GivenTorrents(new List<PutioTorrent>
|
||||||
|
{
|
||||||
|
_queued,
|
||||||
|
_downloading,
|
||||||
|
_failed,
|
||||||
|
_completed,
|
||||||
|
_seeding,
|
||||||
|
_completed_different_parent
|
||||||
|
});
|
||||||
|
GivenSuccessfulDownload();
|
||||||
|
|
||||||
|
_settings.SaveParentId = configuredParentId.ToString();
|
||||||
|
|
||||||
|
Subject.GetItems().Should().HaveCount(expectedCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase("WAITING", DownloadItemStatus.Queued)]
|
||||||
|
[TestCase("PREPARING_DOWNLOAD", DownloadItemStatus.Queued)]
|
||||||
|
[TestCase("COMPLETED", DownloadItemStatus.Completed)]
|
||||||
|
[TestCase("COMPLETING", DownloadItemStatus.Downloading)]
|
||||||
|
[TestCase("DOWNLOADING", DownloadItemStatus.Downloading)]
|
||||||
|
[TestCase("ERROR", DownloadItemStatus.Failed)]
|
||||||
|
[TestCase("IN_QUEUE", DownloadItemStatus.Queued)]
|
||||||
|
[TestCase("SEEDING", DownloadItemStatus.Completed)]
|
||||||
|
public void test_getItems_maps_download_status(string given, DownloadItemStatus expectedItemStatus)
|
||||||
|
{
|
||||||
|
_queued.Status = given;
|
||||||
|
|
||||||
|
GivenTorrents(new List<PutioTorrent>
|
||||||
|
{
|
||||||
|
_queued
|
||||||
|
});
|
||||||
|
|
||||||
|
var item = Subject.GetItems().Single();
|
||||||
|
|
||||||
|
item.Status.Should().Be(expectedItemStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void test_getItems_path_for_folders()
|
||||||
|
{
|
||||||
|
GivenTorrents(new List<PutioTorrent> { _completed });
|
||||||
|
GivenRemoteFileStructure(new List<PutioFile>
|
||||||
|
{
|
||||||
|
new PutioFile { Id = _completed.FileId, Name = _title, FileType = PutioFile.FILE_TYPE_FOLDER },
|
||||||
|
}, new PutioFile { Id = 1, Name = "Downloads" });
|
||||||
|
|
||||||
|
var item = Subject.GetItems().Single();
|
||||||
|
|
||||||
|
VerifyCompleted(item);
|
||||||
|
item.OutputPath.ToString().Should().ContainAll("Downloads", _title);
|
||||||
|
|
||||||
|
Mocker.GetMock<IPutioProxy>()
|
||||||
|
.Verify(s => s.GetFileListingResponse(1, It.IsAny<PutioSettings>()), Times.AtLeastOnce());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void test_getItems_path_for_files()
|
||||||
|
{
|
||||||
|
GivenTorrents(new List<PutioTorrent> { _completed });
|
||||||
|
GivenRemoteFileStructure(new List<PutioFile>
|
||||||
|
{
|
||||||
|
new PutioFile { Id = _completed.FileId, Name = _title, FileType = PutioFile.FILE_TYPE_VIDEO },
|
||||||
|
}, new PutioFile { Id = 1, Name = "Downloads" });
|
||||||
|
|
||||||
|
var item = Subject.GetItems().Single();
|
||||||
|
|
||||||
|
VerifyCompleted(item);
|
||||||
|
|
||||||
|
item.OutputPath.ToString().Should().Contain("Downloads");
|
||||||
|
item.OutputPath.ToString().Should().NotContain(_title);
|
||||||
|
|
||||||
|
Mocker.GetMock<IPutioProxy>()
|
||||||
|
.Verify(s => s.GetFileListingResponse(It.IsAny<long>(), It.IsAny<PutioSettings>()), Times.AtLeastOnce());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,132 @@
|
||||||
|
using Moq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Common.Http;
|
||||||
|
using NzbDrone.Core.Download.Clients.Putio;
|
||||||
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Test.Download.DownloadClientTests.PutioTests
|
||||||
|
{
|
||||||
|
public class PutioProxyFixtures : CoreTest<PutioProxy>
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void test_GetTorrentMetadata_createsNewObject()
|
||||||
|
{
|
||||||
|
ClientGetWillReturn<PutioConfigResponse>("{\"status\":\"OK\",\"value\":null}");
|
||||||
|
|
||||||
|
var mt = Subject.GetTorrentMetadata(new PutioTorrent { Id = 1 }, new PutioSettings());
|
||||||
|
Assert.IsNotNull(mt);
|
||||||
|
Assert.AreEqual(1, mt.Id);
|
||||||
|
Assert.IsFalse(mt.Downloaded);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void test_GetTorrentMetadata_returnsExistingObject()
|
||||||
|
{
|
||||||
|
ClientGetWillReturn<PutioConfigResponse>("{\"status\":\"OK\",\"value\":{\"id\":4711,\"downloaded\":true}}");
|
||||||
|
|
||||||
|
var mt = Subject.GetTorrentMetadata(new PutioTorrent { Id = 1 }, new PutioSettings());
|
||||||
|
Assert.IsNotNull(mt);
|
||||||
|
Assert.AreEqual(4711, mt.Id);
|
||||||
|
Assert.IsTrue(mt.Downloaded);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void test_GetAllTorrentMetadata_filters_properly()
|
||||||
|
{
|
||||||
|
var json = @"{
|
||||||
|
""config"": {
|
||||||
|
""sonarr_123"": {
|
||||||
|
""downloaded"": true,
|
||||||
|
""id"": 123
|
||||||
|
},
|
||||||
|
""another_key"": {
|
||||||
|
""foo"": ""bar""
|
||||||
|
},
|
||||||
|
""sonarr_456"": {
|
||||||
|
""downloaded"": true,
|
||||||
|
""id"": 456
|
||||||
|
}
|
||||||
|
},
|
||||||
|
""status"": ""OK""
|
||||||
|
}";
|
||||||
|
ClientGetWillReturn<PutioAllConfigResponse>(json);
|
||||||
|
|
||||||
|
var list = Subject.GetAllTorrentMetadata(new PutioSettings());
|
||||||
|
Assert.IsTrue(list.ContainsKey("123"));
|
||||||
|
Assert.IsTrue(list.ContainsKey("456"));
|
||||||
|
Assert.AreEqual(list.Count, 2);
|
||||||
|
Assert.IsTrue(list["123"].Downloaded);
|
||||||
|
Assert.IsTrue(list["456"].Downloaded);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void test_GetFileListingResponse()
|
||||||
|
{
|
||||||
|
var json = @"{
|
||||||
|
""cursor"": null,
|
||||||
|
""files"": [
|
||||||
|
{
|
||||||
|
""file_type"": ""VIDEO"",
|
||||||
|
""id"": 111,
|
||||||
|
""name"": ""My.download.mkv"",
|
||||||
|
""parent_id"": 4711
|
||||||
|
},
|
||||||
|
{
|
||||||
|
""file_type"": ""FOLDER"",
|
||||||
|
""id"": 222,
|
||||||
|
""name"": ""Another-folder[dth]"",
|
||||||
|
""parent_id"": 4711
|
||||||
|
}
|
||||||
|
],
|
||||||
|
""parent"": {
|
||||||
|
""file_type"": ""FOLDER"",
|
||||||
|
""id"": 4711,
|
||||||
|
""name"": ""Incoming"",
|
||||||
|
""parent_id"": 0
|
||||||
|
},
|
||||||
|
""status"": ""OK"",
|
||||||
|
""total"": 2
|
||||||
|
}";
|
||||||
|
ClientGetWillReturn<PutioFileListingResponse>(json);
|
||||||
|
|
||||||
|
var response = Subject.GetFileListingResponse(4711, new PutioSettings());
|
||||||
|
|
||||||
|
Assert.That(response, Is.Not.Null);
|
||||||
|
Assert.AreEqual(response.Files.Count, 2);
|
||||||
|
Assert.AreEqual(4711, response.Parent.Id);
|
||||||
|
Assert.AreEqual(111, response.Files[0].Id);
|
||||||
|
Assert.AreEqual(222, response.Files[1].Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void test_GetFileListingResponse_empty()
|
||||||
|
{
|
||||||
|
var json = @"{
|
||||||
|
""cursor"": null,
|
||||||
|
""files"": [],
|
||||||
|
""parent"": {
|
||||||
|
""file_type"": ""FOLDER"",
|
||||||
|
""id"": 4711,
|
||||||
|
""name"": ""Incoming"",
|
||||||
|
""parent_id"": 0
|
||||||
|
},
|
||||||
|
""status"": ""OK"",
|
||||||
|
""total"": 0
|
||||||
|
}";
|
||||||
|
ClientGetWillReturn<PutioFileListingResponse>(json);
|
||||||
|
|
||||||
|
var response = Subject.GetFileListingResponse(4711, new PutioSettings());
|
||||||
|
|
||||||
|
Assert.That(response, Is.Not.Null);
|
||||||
|
Assert.AreEqual(response.Files.Count, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClientGetWillReturn<TResult>(string obj)
|
||||||
|
where TResult : new()
|
||||||
|
{
|
||||||
|
Mocker.GetMock<IHttpClient>()
|
||||||
|
.Setup(s => s.Get<TResult>(It.IsAny<HttpRequest>()))
|
||||||
|
.Returns<HttpRequest>(r => new HttpResponse<TResult>(new HttpResponse(r, new HttpHeader(), obj)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,265 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using FluentValidation.Results;
|
||||||
|
using NLog;
|
||||||
|
using NzbDrone.Common.Disk;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
|
using NzbDrone.Common.Http;
|
||||||
|
using NzbDrone.Core.Configuration;
|
||||||
|
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
||||||
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
using NzbDrone.Core.RemotePathMappings;
|
||||||
|
using NzbDrone.Core.Validation;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Download.Clients.Putio
|
||||||
|
{
|
||||||
|
public class Putio : TorrentClientBase<PutioSettings>
|
||||||
|
{
|
||||||
|
private readonly IPutioProxy _proxy;
|
||||||
|
|
||||||
|
public Putio(
|
||||||
|
IPutioProxy proxy,
|
||||||
|
ITorrentFileInfoReader torrentFileInfoReader,
|
||||||
|
IHttpClient httpClient,
|
||||||
|
IConfigService configService,
|
||||||
|
IDiskProvider diskProvider,
|
||||||
|
IRemotePathMappingService remotePathMappingService,
|
||||||
|
Logger logger)
|
||||||
|
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, logger)
|
||||||
|
{
|
||||||
|
_proxy = proxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string Name
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return "put.io";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override string AddFromMagnetLink(RemoteEpisode remoteEpisode, string hash, string magnetLink)
|
||||||
|
{
|
||||||
|
_proxy.AddTorrentFromUrl(magnetLink, Settings);
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override string AddFromTorrentFile(RemoteEpisode remoteEpisode, string hash, string filename, byte[] fileContent)
|
||||||
|
{
|
||||||
|
_proxy.AddTorrentFromData(fileContent, Settings);
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override IEnumerable<DownloadClientItem> GetItems()
|
||||||
|
{
|
||||||
|
List<PutioTorrent> torrents;
|
||||||
|
PutioFileListingResponse fileListingResponse;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
torrents = _proxy.GetTorrents(Settings);
|
||||||
|
|
||||||
|
if (Settings.SaveParentId.IsNotNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
fileListingResponse = _proxy.GetFileListingResponse(long.Parse(Settings.SaveParentId), Settings);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
fileListingResponse = _proxy.GetFileListingResponse(0, Settings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (DownloadClientException ex)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, ex.Message);
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var torrent in torrents)
|
||||||
|
{
|
||||||
|
if (torrent.Size == 0)
|
||||||
|
{
|
||||||
|
// If totalsize == 0 the torrent is a magnet downloading metadata
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Settings.SaveParentId.IsNotNullOrWhiteSpace() && torrent.SaveParentId != long.Parse(Settings.SaveParentId))
|
||||||
|
{
|
||||||
|
// torrent is not related to our parent folder
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var item = new DownloadClientItem
|
||||||
|
{
|
||||||
|
DownloadId = torrent.Id.ToString(),
|
||||||
|
Category = Settings.SaveParentId?.ToString(),
|
||||||
|
Title = torrent.Name,
|
||||||
|
TotalSize = torrent.Size,
|
||||||
|
RemainingSize = torrent.Size - torrent.Downloaded,
|
||||||
|
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this),
|
||||||
|
SeedRatio = torrent.Ratio,
|
||||||
|
|
||||||
|
// Initial status, might change later
|
||||||
|
Status = GetDownloadItemStatus(torrent)
|
||||||
|
};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (torrent.FileId != 0)
|
||||||
|
{
|
||||||
|
// Todo: make configurable? Behaviour might be different for users (rclone mount, vs sync/mv)
|
||||||
|
item.CanMoveFiles = false;
|
||||||
|
item.CanBeRemoved = false;
|
||||||
|
|
||||||
|
var file = fileListingResponse.Files.FirstOrDefault(f => f.Id == torrent.FileId);
|
||||||
|
var parent = fileListingResponse.Parent;
|
||||||
|
|
||||||
|
if (file == null || parent == null)
|
||||||
|
{
|
||||||
|
item.Message = string.Format("Did not find file {0} in remote listing", torrent.FileId);
|
||||||
|
item.Status = DownloadItemStatus.Warning;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var expectedPath = new OsPath(Settings.DownloadPath) + new OsPath(parent.Name);
|
||||||
|
if (file.IsFolder())
|
||||||
|
{
|
||||||
|
expectedPath += new OsPath(file.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_diskProvider.FolderExists(expectedPath.FullPath))
|
||||||
|
{
|
||||||
|
item.OutputPath = expectedPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (DownloadClientException ex)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (torrent.EstimatedTime >= 0)
|
||||||
|
{
|
||||||
|
item.RemainingTime = TimeSpan.FromSeconds(torrent.EstimatedTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!torrent.ErrorMessage.IsNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
item.Status = DownloadItemStatus.Warning;
|
||||||
|
item.Message = torrent.ErrorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield return item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private DownloadItemStatus GetDownloadItemStatus(PutioTorrent torrent)
|
||||||
|
{
|
||||||
|
if (torrent.Status == PutioTorrentStatus.Completed ||
|
||||||
|
torrent.Status == PutioTorrentStatus.Seeding)
|
||||||
|
{
|
||||||
|
return DownloadItemStatus.Completed;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (torrent.Status == PutioTorrentStatus.InQueue ||
|
||||||
|
torrent.Status == PutioTorrentStatus.Waiting ||
|
||||||
|
torrent.Status == PutioTorrentStatus.PrepareDownload)
|
||||||
|
{
|
||||||
|
return DownloadItemStatus.Queued;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (torrent.Status == PutioTorrentStatus.Error)
|
||||||
|
{
|
||||||
|
return DownloadItemStatus.Failed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DownloadItemStatus.Downloading;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override DownloadClientInfo GetStatus()
|
||||||
|
{
|
||||||
|
return new DownloadClientInfo
|
||||||
|
{
|
||||||
|
IsLocalhost = false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Test(List<ValidationFailure> failures)
|
||||||
|
{
|
||||||
|
failures.AddIfNotNull(TestFolder(Settings.DownloadPath, "DownloadPath"));
|
||||||
|
failures.AddIfNotNull(TestConnection());
|
||||||
|
failures.AddIfNotNull(TestRemoteParentFolder());
|
||||||
|
if (failures.Any())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
failures.AddIfNotNull(TestGetTorrents());
|
||||||
|
}
|
||||||
|
|
||||||
|
private ValidationFailure TestConnection()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_proxy.GetAccountSettings(Settings);
|
||||||
|
}
|
||||||
|
catch (DownloadClientAuthenticationException ex)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, ex.Message);
|
||||||
|
return new NzbDroneValidationFailure("OAuthToken", "Authentication failed")
|
||||||
|
{
|
||||||
|
DetailedDescription = "See the wiki for more details on how to obtain an OAuthToken"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, ex.Message);
|
||||||
|
return new NzbDroneValidationFailure(string.Empty, "Unknown exception: " + ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ValidationFailure TestGetTorrents()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_proxy.GetTorrents(Settings);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, ex.Message);
|
||||||
|
return new NzbDroneValidationFailure(string.Empty, "Failed to get the list of torrents: " + ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ValidationFailure TestRemoteParentFolder()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_proxy.GetFileListingResponse(long.Parse(Settings.SaveParentId), Settings);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, ex.Message);
|
||||||
|
return new NzbDroneValidationFailure("SaveParentId", "This is not a valid folder in your account");
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void RemoveItem(DownloadClientItem item, bool deleteData)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void MarkItemAsImported(DownloadClientItem downloadClientItem)
|
||||||
|
{
|
||||||
|
// What to do here? Maybe delete the file and transfer from put.io?
|
||||||
|
base.MarkItemAsImported(downloadClientItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
namespace NzbDrone.Core.Download.Clients.Putio
|
||||||
|
{
|
||||||
|
public class PutioException : DownloadClientException
|
||||||
|
{
|
||||||
|
public PutioException(string message)
|
||||||
|
: base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Download.Clients.Putio
|
||||||
|
{
|
||||||
|
public class PutioFile
|
||||||
|
{
|
||||||
|
public static string FILE_TYPE_FOLDER = "FOLDER";
|
||||||
|
public static string FILE_TYPE_VIDEO = "VIDEO";
|
||||||
|
public long Id { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
[JsonProperty(PropertyName = "parent_id")]
|
||||||
|
public long ParentId { get; set; }
|
||||||
|
[JsonProperty(PropertyName = "file_type")]
|
||||||
|
public string FileType { get; set; }
|
||||||
|
public bool IsFolder()
|
||||||
|
{
|
||||||
|
return FileType == FILE_TYPE_FOLDER;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,160 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using NLog;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
|
using NzbDrone.Common.Http;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Download.Clients.Putio
|
||||||
|
{
|
||||||
|
public interface IPutioProxy
|
||||||
|
{
|
||||||
|
List<PutioTorrent> GetTorrents(PutioSettings settings);
|
||||||
|
void AddTorrentFromUrl(string torrentUrl, PutioSettings settings);
|
||||||
|
void AddTorrentFromData(byte[] torrentData, PutioSettings settings);
|
||||||
|
void GetAccountSettings(PutioSettings settings);
|
||||||
|
public PutioTorrentMetadata GetTorrentMetadata(PutioTorrent torrent, PutioSettings settings);
|
||||||
|
public Dictionary<string, PutioTorrentMetadata> GetAllTorrentMetadata(PutioSettings settings);
|
||||||
|
public PutioFileListingResponse GetFileListingResponse(long parentId, PutioSettings settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PutioProxy : IPutioProxy
|
||||||
|
{
|
||||||
|
private const string _configPrefix = "sonarr_";
|
||||||
|
private readonly Logger _logger;
|
||||||
|
private readonly IHttpClient _httpClient;
|
||||||
|
|
||||||
|
public PutioProxy(Logger logger, IHttpClient client)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_httpClient = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<PutioTorrent> GetTorrents(PutioSettings settings)
|
||||||
|
{
|
||||||
|
var result = Execute<PutioTransfersResponse>(BuildRequest(HttpMethod.Get, "transfers/list", settings));
|
||||||
|
return result.Resource.Transfers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddTorrentFromUrl(string torrentUrl, PutioSettings settings)
|
||||||
|
{
|
||||||
|
var request = BuildRequest(HttpMethod.Post, "transfers/add", settings);
|
||||||
|
request.AddFormParameter("url", torrentUrl);
|
||||||
|
|
||||||
|
if (settings.SaveParentId.IsNotNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
request.AddFormParameter("save_parent_id", settings.SaveParentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Execute<PutioGenericResponse>(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddTorrentFromData(byte[] torrentData, PutioSettings settings)
|
||||||
|
{
|
||||||
|
// var arguments = new Dictionary<string, object>();
|
||||||
|
// arguments.Add("metainfo", Convert.ToBase64String(torrentData));
|
||||||
|
// ProcessRequest<PutioGenericResponse>(Method.POST, "transfers/add", arguments, settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void GetAccountSettings(PutioSettings settings)
|
||||||
|
{
|
||||||
|
Execute<PutioGenericResponse>(BuildRequest(HttpMethod.Get, "account/settings", settings));
|
||||||
|
}
|
||||||
|
|
||||||
|
public PutioTorrentMetadata GetTorrentMetadata(PutioTorrent torrent, PutioSettings settings)
|
||||||
|
{
|
||||||
|
var metadata = Execute<PutioConfigResponse>(BuildRequest(HttpMethod.Get, "config/" + _configPrefix + torrent.Id, settings));
|
||||||
|
if (metadata.Resource.Value != null)
|
||||||
|
{
|
||||||
|
_logger.Debug("Found metadata for torrent: {0} {1}", torrent.Id, metadata.Resource.Value);
|
||||||
|
return metadata.Resource.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PutioTorrentMetadata
|
||||||
|
{
|
||||||
|
Id = torrent.Id,
|
||||||
|
Downloaded = false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public PutioFileListingResponse GetFileListingResponse(long parentId, PutioSettings settings)
|
||||||
|
{
|
||||||
|
var request = BuildRequest(HttpMethod.Get, "files/list", settings);
|
||||||
|
request.AddQueryParam("parent_id", parentId);
|
||||||
|
request.AddQueryParam("per_page", 1000);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = Execute<PutioFileListingResponse>(request);
|
||||||
|
return response.Resource;
|
||||||
|
}
|
||||||
|
catch (DownloadClientException ex)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, "Failed to get file listing response");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Dictionary<string, PutioTorrentMetadata> GetAllTorrentMetadata(PutioSettings settings)
|
||||||
|
{
|
||||||
|
var metadata = Execute<PutioAllConfigResponse>(BuildRequest(HttpMethod.Get, "config", settings));
|
||||||
|
var result = new Dictionary<string, PutioTorrentMetadata>();
|
||||||
|
|
||||||
|
foreach (var item in metadata.Resource.Config)
|
||||||
|
{
|
||||||
|
if (item.Key.StartsWith(_configPrefix))
|
||||||
|
{
|
||||||
|
var torrentId = item.Key.Substring(_configPrefix.Length);
|
||||||
|
result[torrentId] = item.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpRequestBuilder BuildRequest(HttpMethod method, string endpoint, PutioSettings settings)
|
||||||
|
{
|
||||||
|
var requestBuilder = new HttpRequestBuilder("https://api.put.io/v2")
|
||||||
|
{
|
||||||
|
LogResponseContent = true
|
||||||
|
};
|
||||||
|
requestBuilder.Method = method;
|
||||||
|
requestBuilder.Resource(endpoint);
|
||||||
|
requestBuilder.SetHeader("Authorization", "Bearer " + settings.OAuthToken);
|
||||||
|
return requestBuilder;
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpResponse<TResult> Execute<TResult>(HttpRequestBuilder requestBuilder)
|
||||||
|
where TResult : new()
|
||||||
|
{
|
||||||
|
var request = requestBuilder.Build();
|
||||||
|
request.LogResponseContent = true;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (requestBuilder.Method == HttpMethod.Post)
|
||||||
|
{
|
||||||
|
return _httpClient.Post<TResult>(request);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return _httpClient.Get<TResult>(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (HttpException ex)
|
||||||
|
{
|
||||||
|
if (ex.Response.StatusCode == HttpStatusCode.Unauthorized)
|
||||||
|
{
|
||||||
|
throw new DownloadClientAuthenticationException("Invalid credentials. Check your OAuthToken");
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new DownloadClientException("Failed to connect to put.io API", ex);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new DownloadClientException("Failed to connect to put.io API", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Download.Clients.Putio
|
||||||
|
{
|
||||||
|
public class PutioGenericResponse
|
||||||
|
{
|
||||||
|
[JsonProperty(PropertyName = "error_message")]
|
||||||
|
public string ErrorMessage { get; set; }
|
||||||
|
|
||||||
|
public string Status { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PutioTransfersResponse : PutioGenericResponse
|
||||||
|
{
|
||||||
|
public List<PutioTorrent> Transfers { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PutioConfigResponse : PutioGenericResponse
|
||||||
|
{
|
||||||
|
public PutioTorrentMetadata Value { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PutioAllConfigResponse : PutioGenericResponse
|
||||||
|
{
|
||||||
|
public Dictionary<string, PutioTorrentMetadata> Config { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PutioFileListingResponse : PutioGenericResponse
|
||||||
|
{
|
||||||
|
public static PutioFileListingResponse Empty()
|
||||||
|
{
|
||||||
|
return new PutioFileListingResponse
|
||||||
|
{
|
||||||
|
Files = new List<PutioFile>(),
|
||||||
|
Parent = new PutioFile
|
||||||
|
{
|
||||||
|
Id = 0,
|
||||||
|
Name = "Your Files"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<PutioFile> Files { get; set; }
|
||||||
|
|
||||||
|
public PutioFile Parent { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using FluentValidation;
|
||||||
|
using NzbDrone.Core.Annotations;
|
||||||
|
using NzbDrone.Core.ThingiProvider;
|
||||||
|
using NzbDrone.Core.Validation;
|
||||||
|
using NzbDrone.Core.Validation.Paths;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Download.Clients.Putio
|
||||||
|
{
|
||||||
|
public class PutioSettingsValidator : AbstractValidator<PutioSettings>
|
||||||
|
{
|
||||||
|
public PutioSettingsValidator()
|
||||||
|
{
|
||||||
|
RuleFor(c => c.OAuthToken).NotEmpty().WithMessage("Please provide an OAuth token");
|
||||||
|
RuleFor(c => c.DownloadPath).IsValidPath().WithMessage("Please provide a valid local path");
|
||||||
|
RuleFor(c => c.SaveParentId).Matches(@"^\.?[0-9]*$", RegexOptions.IgnoreCase).WithMessage("Allowed characters 0-9");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PutioSettings : IProviderConfig
|
||||||
|
{
|
||||||
|
private static readonly PutioSettingsValidator Validator = new PutioSettingsValidator();
|
||||||
|
|
||||||
|
public PutioSettings()
|
||||||
|
{
|
||||||
|
Url = "https://api.put.io/v2";
|
||||||
|
DeleteImported = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Url { get; }
|
||||||
|
|
||||||
|
[FieldDefinition(0, Label = "OAuth Token", Type = FieldType.Password)]
|
||||||
|
public string OAuthToken { get; set; }
|
||||||
|
|
||||||
|
[FieldDefinition(1, Label = "Save Parent Folder ID", Type = FieldType.Textbox, HelpText = "Adding a parent folder ID specific to Sonarr avoids conflicts with unrelated non-Sonarr downloads. Using a parent folder is optional, but strongly recommended.")]
|
||||||
|
public string SaveParentId { get; set; }
|
||||||
|
|
||||||
|
[FieldDefinition(2, Label = "Download Path", Type = FieldType.Path, HelpText = "Path were Sonarr will expect the files to get downloaded to. Note: This client does not download finished transfers automatically. Instead make sure that you download them outside of Sonarr e.g. with rclone")]
|
||||||
|
public string DownloadPath { get; set; }
|
||||||
|
|
||||||
|
[FieldDefinition(3, Label = "Delete imported files", Type = FieldType.Checkbox, HelpText = "Delete the files on put.io when Sonarr marks them as successfully imported")]
|
||||||
|
public bool DeleteImported { get; set; }
|
||||||
|
|
||||||
|
public NzbDroneValidationResult Validate()
|
||||||
|
{
|
||||||
|
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Download.Clients.Putio
|
||||||
|
{
|
||||||
|
public class PutioTorrent
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Hash { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
public long Downloaded { get; set; }
|
||||||
|
public long Uploaded { get; set; }
|
||||||
|
[JsonProperty(PropertyName = "error_message")]
|
||||||
|
public string ErrorMessage { get; set; }
|
||||||
|
[JsonProperty(PropertyName = "estimated_time")]
|
||||||
|
public long EstimatedTime { get; set; }
|
||||||
|
[JsonProperty(PropertyName = "file_id")]
|
||||||
|
public long FileId { get; set; }
|
||||||
|
[JsonProperty(PropertyName = "percent_done")]
|
||||||
|
public int PercentDone { get; set; }
|
||||||
|
[JsonProperty(PropertyName = "seconds_seeding")]
|
||||||
|
public long SecondsSeeding { get; set; }
|
||||||
|
public long Size { get; set; }
|
||||||
|
public string Status { get; set; }
|
||||||
|
[JsonProperty(PropertyName = "save_parent_id")]
|
||||||
|
public long SaveParentId { get; set; }
|
||||||
|
[JsonProperty(PropertyName = "current_ratio")]
|
||||||
|
public double Ratio { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PutioTorrentMetadata
|
||||||
|
{
|
||||||
|
public static PutioTorrentMetadata fromTorrent(PutioTorrent torrent, bool downloaded = false)
|
||||||
|
{
|
||||||
|
return new PutioTorrentMetadata
|
||||||
|
{
|
||||||
|
Downloaded = downloaded,
|
||||||
|
Id = torrent.Id
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Downloaded { get; set; }
|
||||||
|
|
||||||
|
public long Id { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
namespace NzbDrone.Core.Download.Clients.Putio
|
||||||
|
{
|
||||||
|
public static class PutioTorrentStatus
|
||||||
|
{
|
||||||
|
public static readonly string Waiting = "WAITING";
|
||||||
|
public static readonly string PrepareDownload = "PREPARING_DOWNLOAD";
|
||||||
|
public static readonly string Completed = "COMPLETED";
|
||||||
|
public static readonly string Completing = "COMPLETING";
|
||||||
|
public static readonly string Downloading = "DOWNLOADING";
|
||||||
|
public static readonly string Error = "ERROR";
|
||||||
|
public static readonly string InQueue = "IN_QUEUE";
|
||||||
|
public static readonly string Seeding = "SEEDING";
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue