New: Added support for Synology Download Station as torrent client.
This commit is contained in:
parent
2f6d9e191e
commit
82a99b7f80
|
@ -0,0 +1,602 @@
|
|||
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.DownloadStation;
|
||||
using NzbDrone.Core.Download.Clients.DownloadStation.Proxies;
|
||||
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Test.Common;
|
||||
|
||||
namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class DownloadStationFixture : DownloadClientFixtureBase<DownloadStation>
|
||||
{
|
||||
protected DownloadStationSettings _settings;
|
||||
|
||||
protected DownloadStationTorrent _queued;
|
||||
protected DownloadStationTorrent _downloading;
|
||||
protected DownloadStationTorrent _failed;
|
||||
protected DownloadStationTorrent _completed;
|
||||
protected DownloadStationTorrent _seeding;
|
||||
protected DownloadStationTorrent _magnet;
|
||||
protected DownloadStationTorrent _singleFile;
|
||||
protected DownloadStationTorrent _multipleFiles;
|
||||
protected DownloadStationTorrent _singleFileCompleted;
|
||||
protected DownloadStationTorrent _multipleFilesCompleted;
|
||||
|
||||
protected string _serialNumber = "SERIALNUMBER";
|
||||
protected string _category = "sonarr";
|
||||
protected string _tvDirectory = @"video/Series";
|
||||
protected string _defaultDestination = "somepath";
|
||||
protected OsPath _physicalPath = new OsPath("/mnt/sdb1/mydata");
|
||||
|
||||
protected Dictionary<string, object> _downloadStationConfigItems;
|
||||
|
||||
protected string DownloadURL => "magnet:?xt=urn:btih:5dee65101db281ac9c46344cd6b175cdcad53426&dn=download";
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_settings = new DownloadStationSettings()
|
||||
{
|
||||
Host = "127.0.0.1",
|
||||
Port = 5000,
|
||||
Username = "admin",
|
||||
Password = "pass"
|
||||
};
|
||||
|
||||
Subject.Definition = new DownloadClientDefinition();
|
||||
Subject.Definition.Settings = _settings;
|
||||
|
||||
_queued = new DownloadStationTorrent()
|
||||
{
|
||||
Id = "id1",
|
||||
Size = 1000,
|
||||
Status = DownloadStationTaskStatus.Waiting,
|
||||
Type = DownloadStationTaskType.BT,
|
||||
Username = "admin",
|
||||
Title = "title",
|
||||
Additional = new DownloadStationTorrentAdditional
|
||||
{
|
||||
Detail = new Dictionary<string, string>
|
||||
{
|
||||
{ "destination","shared/folder" },
|
||||
{ "uri", DownloadURL }
|
||||
},
|
||||
Transfer = new Dictionary<string, string>
|
||||
{
|
||||
{ "size_downloaded", "0"},
|
||||
{ "speed_download", "0" }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_completed = new DownloadStationTorrent()
|
||||
{
|
||||
Id = "id2",
|
||||
Size = 1000,
|
||||
Status = DownloadStationTaskStatus.Finished,
|
||||
Type = DownloadStationTaskType.BT,
|
||||
Username = "admin",
|
||||
Title = "title",
|
||||
Additional = new DownloadStationTorrentAdditional
|
||||
{
|
||||
Detail = new Dictionary<string, string>
|
||||
{
|
||||
{ "destination","shared/folder" },
|
||||
{ "uri", DownloadURL }
|
||||
},
|
||||
Transfer = new Dictionary<string, string>
|
||||
{
|
||||
{ "size_downloaded", "1000"},
|
||||
{ "speed_download", "0" }
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
_seeding = new DownloadStationTorrent()
|
||||
{
|
||||
Id = "id2",
|
||||
Size = 1000,
|
||||
Status = DownloadStationTaskStatus.Seeding,
|
||||
Type = DownloadStationTaskType.BT,
|
||||
Username = "admin",
|
||||
Title = "title",
|
||||
Additional = new DownloadStationTorrentAdditional
|
||||
{
|
||||
Detail = new Dictionary<string, string>
|
||||
{
|
||||
{ "destination","shared/folder" },
|
||||
{ "uri", DownloadURL }
|
||||
},
|
||||
Transfer = new Dictionary<string, string>
|
||||
{
|
||||
{ "size_downloaded", "1000"},
|
||||
{ "speed_download", "0" }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_downloading = new DownloadStationTorrent()
|
||||
{
|
||||
Id = "id3",
|
||||
Size = 1000,
|
||||
Status = DownloadStationTaskStatus.Downloading,
|
||||
Type = DownloadStationTaskType.BT,
|
||||
Username = "admin",
|
||||
Title = "title",
|
||||
Additional = new DownloadStationTorrentAdditional
|
||||
{
|
||||
Detail = new Dictionary<string, string>
|
||||
{
|
||||
{ "destination","shared/folder" },
|
||||
{ "uri", DownloadURL }
|
||||
},
|
||||
Transfer = new Dictionary<string, string>
|
||||
{
|
||||
{ "size_downloaded", "100"},
|
||||
{ "speed_download", "50" }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_failed = new DownloadStationTorrent()
|
||||
{
|
||||
Id = "id4",
|
||||
Size = 1000,
|
||||
Status = DownloadStationTaskStatus.Error,
|
||||
Type = DownloadStationTaskType.BT,
|
||||
Username = "admin",
|
||||
Title = "title",
|
||||
Additional = new DownloadStationTorrentAdditional
|
||||
{
|
||||
Detail = new Dictionary<string, string>
|
||||
{
|
||||
{ "destination","shared/folder" },
|
||||
{ "uri", DownloadURL }
|
||||
},
|
||||
Transfer = new Dictionary<string, string>
|
||||
{
|
||||
{ "size_downloaded", "10"},
|
||||
{ "speed_download", "0" }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_singleFile = new DownloadStationTorrent()
|
||||
{
|
||||
Id = "id5",
|
||||
Size = 1000,
|
||||
Status = DownloadStationTaskStatus.Seeding,
|
||||
Type = DownloadStationTaskType.BT,
|
||||
Username = "admin",
|
||||
Title = "a.mkv",
|
||||
Additional = new DownloadStationTorrentAdditional
|
||||
{
|
||||
Detail = new Dictionary<string, string>
|
||||
{
|
||||
{ "destination","shared/folder" },
|
||||
{ "uri", DownloadURL }
|
||||
},
|
||||
Transfer = new Dictionary<string, string>
|
||||
{
|
||||
{ "size_downloaded", "1000"},
|
||||
{ "speed_download", "0" }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_multipleFiles = new DownloadStationTorrent()
|
||||
{
|
||||
Id = "id6",
|
||||
Size = 1000,
|
||||
Status = DownloadStationTaskStatus.Seeding,
|
||||
Type = DownloadStationTaskType.BT,
|
||||
Username = "admin",
|
||||
Title = "title",
|
||||
Additional = new DownloadStationTorrentAdditional
|
||||
{
|
||||
Detail = new Dictionary<string, string>
|
||||
{
|
||||
{ "destination","shared/folder" },
|
||||
{ "uri", DownloadURL }
|
||||
},
|
||||
Transfer = new Dictionary<string, string>
|
||||
{
|
||||
{ "size_downloaded", "1000"},
|
||||
{ "speed_download", "0" }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_singleFileCompleted = new DownloadStationTorrent()
|
||||
{
|
||||
Id = "id6",
|
||||
Size = 1000,
|
||||
Status = DownloadStationTaskStatus.Finished,
|
||||
Type = DownloadStationTaskType.BT,
|
||||
Username = "admin",
|
||||
Title = "a.mkv",
|
||||
Additional = new DownloadStationTorrentAdditional
|
||||
{
|
||||
Detail = new Dictionary<string, string>
|
||||
{
|
||||
{ "destination","shared/folder" },
|
||||
{ "uri", DownloadURL }
|
||||
},
|
||||
Transfer = new Dictionary<string, string>
|
||||
{
|
||||
{ "size_downloaded", "1000"},
|
||||
{ "speed_download", "0" }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_multipleFilesCompleted = new DownloadStationTorrent()
|
||||
{
|
||||
Id = "id6",
|
||||
Size = 1000,
|
||||
Status = DownloadStationTaskStatus.Finished,
|
||||
Type = DownloadStationTaskType.BT,
|
||||
Username = "admin",
|
||||
Title = "title",
|
||||
Additional = new DownloadStationTorrentAdditional
|
||||
{
|
||||
Detail = new Dictionary<string, string>
|
||||
{
|
||||
{ "destination","shared/folder" },
|
||||
{ "uri", DownloadURL }
|
||||
},
|
||||
Transfer = new Dictionary<string, string>
|
||||
{
|
||||
{ "size_downloaded", "1000"},
|
||||
{ "speed_download", "0" }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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]));
|
||||
|
||||
_downloadStationConfigItems = new Dictionary<string, object>
|
||||
{
|
||||
{ "default_destination", _defaultDestination },
|
||||
};
|
||||
|
||||
Mocker.GetMock<IDownloadStationProxy>()
|
||||
.Setup(v => v.GetConfig(It.IsAny<DownloadStationSettings>()))
|
||||
.Returns(_downloadStationConfigItems);
|
||||
}
|
||||
|
||||
protected void GivenSharedFolder()
|
||||
{
|
||||
Mocker.GetMock<ISharedFolderResolver>()
|
||||
.Setup(s => s.RemapToFullPath(It.IsAny<OsPath>(), It.IsAny<DownloadStationSettings>(), It.IsAny<string>()))
|
||||
.Returns<OsPath, DownloadStationSettings, string>((path, setttings, serial) => _physicalPath);
|
||||
}
|
||||
|
||||
protected void GivenSerialNumber()
|
||||
{
|
||||
Mocker.GetMock<ISerialNumberProvider>()
|
||||
.Setup(s => s.GetSerialNumber(It.IsAny<DownloadStationSettings>()))
|
||||
.Returns(_serialNumber);
|
||||
}
|
||||
|
||||
protected void GivenTvCategory()
|
||||
{
|
||||
_settings.TvCategory = _category;
|
||||
}
|
||||
|
||||
protected void GivenTvDirectory()
|
||||
{
|
||||
_settings.TvDirectory = _tvDirectory;
|
||||
}
|
||||
|
||||
protected virtual void GivenTorrents(List<DownloadStationTorrent> torrents)
|
||||
{
|
||||
if (torrents == null)
|
||||
{
|
||||
torrents = new List<DownloadStationTorrent>();
|
||||
}
|
||||
|
||||
Mocker.GetMock<IDownloadStationProxy>()
|
||||
.Setup(s => s.GetTorrents(It.IsAny<DownloadStationSettings>()))
|
||||
.Returns(torrents);
|
||||
}
|
||||
|
||||
protected void PrepareClientToReturnQueuedItem()
|
||||
{
|
||||
GivenTorrents(new List<DownloadStationTorrent>
|
||||
{
|
||||
_queued
|
||||
});
|
||||
}
|
||||
|
||||
protected void GivenSuccessfulDownload()
|
||||
{
|
||||
Mocker.GetMock<IHttpClient>()
|
||||
.Setup(s => s.Get(It.IsAny<HttpRequest>()))
|
||||
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), new byte[1000]));
|
||||
|
||||
Mocker.GetMock<IDownloadStationProxy>()
|
||||
.Setup(s => s.AddTorrentFromUrl(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<DownloadStationSettings>()))
|
||||
.Returns(true)
|
||||
.Callback(PrepareClientToReturnQueuedItem);
|
||||
|
||||
Mocker.GetMock<IDownloadStationProxy>()
|
||||
.Setup(s => s.AddTorrentFromData(It.IsAny<byte[]>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<DownloadStationSettings>()))
|
||||
.Returns(true)
|
||||
.Callback(PrepareClientToReturnQueuedItem);
|
||||
}
|
||||
|
||||
protected override RemoteEpisode CreateRemoteEpisode()
|
||||
{
|
||||
var episode = base.CreateRemoteEpisode();
|
||||
|
||||
episode.Release.DownloadUrl = DownloadURL;
|
||||
|
||||
return episode;
|
||||
}
|
||||
|
||||
protected int GivenAllKindOfTasks()
|
||||
{
|
||||
var tasks = new List<DownloadStationTorrent>() { _queued, _completed, _failed, _downloading, _seeding };
|
||||
|
||||
Mocker.GetMock<IDownloadStationProxy>()
|
||||
.Setup(d => d.GetTorrents(_settings))
|
||||
.Returns(tasks);
|
||||
|
||||
return tasks.Count;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Download_with_TvDirectory_should_force_directory()
|
||||
{
|
||||
GivenSerialNumber();
|
||||
GivenTvDirectory();
|
||||
GivenSuccessfulDownload();
|
||||
|
||||
var remoteEpisode = CreateRemoteEpisode();
|
||||
|
||||
var id = Subject.Download(remoteEpisode);
|
||||
|
||||
id.Should().NotBeNullOrEmpty();
|
||||
|
||||
Mocker.GetMock<IDownloadStationProxy>()
|
||||
.Verify(v => v.AddTorrentFromUrl(It.IsAny<string>(), _tvDirectory, It.IsAny<DownloadStationSettings>()), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Download_with_category_should_force_directory()
|
||||
{
|
||||
GivenSerialNumber();
|
||||
GivenTvCategory();
|
||||
GivenSuccessfulDownload();
|
||||
|
||||
var remoteEpisode = CreateRemoteEpisode();
|
||||
|
||||
var id = Subject.Download(remoteEpisode);
|
||||
|
||||
id.Should().NotBeNullOrEmpty();
|
||||
|
||||
Mocker.GetMock<IDownloadStationProxy>()
|
||||
.Verify(v => v.AddTorrentFromUrl(It.IsAny<string>(), $"{_defaultDestination}/{_category}", It.IsAny<DownloadStationSettings>()), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Download_without_TvDirectory_and_Category_should_use_default()
|
||||
{
|
||||
GivenSerialNumber();
|
||||
GivenSuccessfulDownload();
|
||||
|
||||
var remoteEpisode = CreateRemoteEpisode();
|
||||
|
||||
var id = Subject.Download(remoteEpisode);
|
||||
|
||||
id.Should().NotBeNullOrEmpty();
|
||||
|
||||
Mocker.GetMock<IDownloadStationProxy>()
|
||||
.Verify(v => v.AddTorrentFromUrl(It.IsAny<string>(), null, It.IsAny<DownloadStationSettings>()), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetItems_should_ignore_downloads_in_wrong_folder()
|
||||
{
|
||||
_settings.TvDirectory = @"/shared/folder/sub";
|
||||
|
||||
GivenSerialNumber();
|
||||
GivenSharedFolder();
|
||||
GivenTorrents(new List<DownloadStationTorrent> { _completed });
|
||||
|
||||
Subject.GetItems().Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetItems_should_throw_if_shared_folder_resolve_fails()
|
||||
{
|
||||
Mocker.GetMock<ISharedFolderResolver>()
|
||||
.Setup(s => s.RemapToFullPath(It.IsAny<OsPath>(), It.IsAny<DownloadStationSettings>(), It.IsAny<string>()))
|
||||
.Throws(new ApplicationException("Some unknown exception, HttpException or DownloadClientException"));
|
||||
|
||||
GivenSerialNumber();
|
||||
GivenAllKindOfTasks();
|
||||
|
||||
Assert.Throws(Is.InstanceOf<Exception>(), () => Subject.GetItems());
|
||||
ExceptionVerification.ExpectedErrors(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetItems_should_throw_if_serial_number_unavailable()
|
||||
{
|
||||
Mocker.GetMock<ISerialNumberProvider>()
|
||||
.Setup(s => s.GetSerialNumber(_settings))
|
||||
.Throws(new ApplicationException("Some unknown exception, HttpException or DownloadClientException"));
|
||||
|
||||
GivenSharedFolder();
|
||||
GivenAllKindOfTasks();
|
||||
|
||||
Assert.Throws(Is.InstanceOf<Exception>(), () => Subject.GetItems());
|
||||
ExceptionVerification.ExpectedErrors(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Download_should_throw_and_not_add_torrent_if_cannot_get_serial_number()
|
||||
{
|
||||
var remoteEpisode = CreateRemoteEpisode();
|
||||
|
||||
Mocker.GetMock<ISerialNumberProvider>()
|
||||
.Setup(s => s.GetSerialNumber(_settings))
|
||||
.Throws(new ApplicationException("Some unknown exception, HttpException or DownloadClientException"));
|
||||
|
||||
Assert.Throws(Is.InstanceOf<Exception>(), () => Subject.Download(remoteEpisode));
|
||||
|
||||
Mocker.GetMock<IDownloadStationProxy>()
|
||||
.Verify(v => v.AddTorrentFromUrl(It.IsAny<string>(), null, _settings), Times.Never());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetItems_should_set_outputhPath_to_base_folder_when_single_file_non_finished_torrent()
|
||||
{
|
||||
GivenSerialNumber();
|
||||
GivenSharedFolder();
|
||||
|
||||
GivenTorrents(new List<DownloadStationTorrent>() { _singleFile });
|
||||
|
||||
var items = Subject.GetItems();
|
||||
|
||||
items.Should().HaveCount(1);
|
||||
items.First().OutputPath.Should().Be(_physicalPath + _singleFile.Title);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetItems_should_set_outputhPath_to_torrent_folder_when_multiple_files_non_finished_torrent()
|
||||
{
|
||||
GivenSerialNumber();
|
||||
GivenSharedFolder();
|
||||
|
||||
GivenTorrents(new List<DownloadStationTorrent>() { _multipleFiles });
|
||||
|
||||
var items = Subject.GetItems();
|
||||
|
||||
items.Should().HaveCount(1);
|
||||
items.First().OutputPath.Should().Be(_physicalPath + _multipleFiles.Title);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetItems_should_set_outputhPath_to_base_folder_when_single_file_finished_torrent()
|
||||
{
|
||||
GivenSerialNumber();
|
||||
GivenSharedFolder();
|
||||
|
||||
GivenTorrents(new List<DownloadStationTorrent>() { _singleFileCompleted });
|
||||
|
||||
var items = Subject.GetItems();
|
||||
|
||||
items.Should().HaveCount(1);
|
||||
items.First().OutputPath.Should().Be(_physicalPath + _singleFileCompleted.Title);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetItems_should_set_outputhPath_to_torrent_folder_when_multiple_files_finished_torrent()
|
||||
{
|
||||
GivenSerialNumber();
|
||||
GivenSharedFolder();
|
||||
|
||||
GivenTorrents(new List<DownloadStationTorrent>() { _multipleFilesCompleted });
|
||||
|
||||
var items = Subject.GetItems();
|
||||
|
||||
items.Should().HaveCount(1);
|
||||
items.First().OutputPath.Should().Be($"{_physicalPath}/{_multipleFiles.Title}");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetItems_should_not_map_outputpath_for_queued_or_downloading_torrents()
|
||||
{
|
||||
GivenSerialNumber();
|
||||
GivenSharedFolder();
|
||||
|
||||
GivenTorrents(new List<DownloadStationTorrent>
|
||||
{
|
||||
_queued, _downloading
|
||||
});
|
||||
|
||||
var items = Subject.GetItems();
|
||||
|
||||
items.Should().HaveCount(2);
|
||||
items.Should().OnlyContain(v => v.OutputPath.IsEmpty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetItems_should_map_outputpath_for_completed_or_failed_torrents()
|
||||
{
|
||||
GivenSerialNumber();
|
||||
GivenSharedFolder();
|
||||
|
||||
GivenTorrents(new List<DownloadStationTorrent>
|
||||
{
|
||||
_completed, _failed, _seeding
|
||||
});
|
||||
|
||||
var items = Subject.GetItems();
|
||||
|
||||
items.Should().HaveCount(3);
|
||||
items.Should().OnlyContain(v => !v.OutputPath.IsEmpty);
|
||||
}
|
||||
|
||||
[TestCase(DownloadStationTaskStatus.Downloading, DownloadItemStatus.Downloading, true)]
|
||||
[TestCase(DownloadStationTaskStatus.Finished, DownloadItemStatus.Completed, false)]
|
||||
[TestCase(DownloadStationTaskStatus.Seeding, DownloadItemStatus.Completed, true)]
|
||||
[TestCase(DownloadStationTaskStatus.Waiting, DownloadItemStatus.Queued, true)]
|
||||
public void GetItems_should_return_readonly_expected(DownloadStationTaskStatus apiStatus, DownloadItemStatus expectedItemStatus, bool readOnlyExpected)
|
||||
{
|
||||
GivenSerialNumber();
|
||||
GivenSharedFolder();
|
||||
|
||||
_queued.Status = apiStatus;
|
||||
|
||||
GivenTorrents(new List<DownloadStationTorrent>() { _queued });
|
||||
|
||||
var items = Subject.GetItems();
|
||||
|
||||
items.Should().HaveCount(1);
|
||||
items.First().IsReadOnly.Should().Be(readOnlyExpected);
|
||||
}
|
||||
|
||||
[TestCase(DownloadStationTaskStatus.Downloading, DownloadItemStatus.Downloading)]
|
||||
[TestCase(DownloadStationTaskStatus.Error, DownloadItemStatus.Failed)]
|
||||
[TestCase(DownloadStationTaskStatus.Extracting, DownloadItemStatus.Downloading)]
|
||||
[TestCase(DownloadStationTaskStatus.Finished, DownloadItemStatus.Completed)]
|
||||
[TestCase(DownloadStationTaskStatus.Finishing, DownloadItemStatus.Downloading)]
|
||||
[TestCase(DownloadStationTaskStatus.HashChecking, DownloadItemStatus.Downloading)]
|
||||
[TestCase(DownloadStationTaskStatus.Paused, DownloadItemStatus.Paused)]
|
||||
[TestCase(DownloadStationTaskStatus.Seeding, DownloadItemStatus.Completed)]
|
||||
[TestCase(DownloadStationTaskStatus.Waiting, DownloadItemStatus.Queued)]
|
||||
public void GetItems_should_return_item_as_downloadItemStatus(DownloadStationTaskStatus apiStatus, DownloadItemStatus expectedItemStatus)
|
||||
{
|
||||
GivenSerialNumber();
|
||||
GivenSharedFolder();
|
||||
|
||||
_queued.Status = apiStatus;
|
||||
|
||||
GivenTorrents(new List<DownloadStationTorrent>() { _queued });
|
||||
|
||||
var items = Subject.GetItems();
|
||||
items.Should().HaveCount(1);
|
||||
|
||||
items.First().Status.Should().Be(expectedItemStatus);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
using System;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Download.Clients;
|
||||
using NzbDrone.Core.Download.Clients.DownloadStation;
|
||||
using NzbDrone.Core.Download.Clients.DownloadStation.Proxies;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Test.Common;
|
||||
|
||||
namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class SerialNumberProviderFixture : CoreTest<SerialNumberProvider>
|
||||
{
|
||||
protected DownloadStationSettings _settings;
|
||||
|
||||
[SetUp]
|
||||
protected void Setup()
|
||||
{
|
||||
_settings = new DownloadStationSettings();
|
||||
}
|
||||
|
||||
private void GivenValidResponse()
|
||||
{
|
||||
Mocker.GetMock<IDSMInfoProxy>()
|
||||
.Setup(d => d.GetSerialNumber(It.IsAny<DownloadStationSettings>()))
|
||||
.Returns("serial");
|
||||
}
|
||||
|
||||
private void GivenInvalidResponse()
|
||||
{
|
||||
Mocker.GetMock<IDSMInfoProxy>()
|
||||
.Setup(d => d.GetSerialNumber(It.IsAny<DownloadStationSettings>()))
|
||||
.Throws(new DownloadClientException("Serial response invalid"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_hashedserialnumber()
|
||||
{
|
||||
GivenValidResponse();
|
||||
|
||||
var serial = Subject.GetSerialNumber(_settings);
|
||||
|
||||
// This hash should remain the same for 'serial', so don't update the test if you change HashConverter, fix the code instead.
|
||||
serial.Should().Be("50DE66B735D30738618568294742FCF1DFA52A47");
|
||||
|
||||
Mocker.GetMock<IDSMInfoProxy>()
|
||||
.Verify(d => d.GetSerialNumber(It.IsAny<DownloadStationSettings>()), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_cache_serialnumber()
|
||||
{
|
||||
GivenValidResponse();
|
||||
|
||||
var serial1 = Subject.GetSerialNumber(_settings);
|
||||
var serial2 = Subject.GetSerialNumber(_settings);
|
||||
|
||||
serial2.Should().Be(serial1);
|
||||
|
||||
Mocker.GetMock<IDSMInfoProxy>()
|
||||
.Verify(d => d.GetSerialNumber(It.IsAny<DownloadStationSettings>()), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_throw_if_serial_number_unavailable()
|
||||
{
|
||||
Assert.Throws(Is.InstanceOf<Exception>(), () => Subject.GetSerialNumber(_settings));
|
||||
|
||||
ExceptionVerification.ExpectedWarns(1);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
using System;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Core.Download.Clients;
|
||||
using NzbDrone.Core.Download.Clients.DownloadStation;
|
||||
using NzbDrone.Core.Download.Clients.DownloadStation.Proxies;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Test.Common;
|
||||
|
||||
namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class SharedFolderResolverFixture : CoreTest<SharedFolderResolver>
|
||||
{
|
||||
protected string _serialNumber = "SERIALNUMBER";
|
||||
protected OsPath _sharedFolder;
|
||||
protected OsPath _physicalPath;
|
||||
protected DownloadStationSettings _settings;
|
||||
|
||||
[SetUp]
|
||||
protected void Setup()
|
||||
{
|
||||
_sharedFolder = new OsPath("/myFolder");
|
||||
_physicalPath = new OsPath("/mnt/sda1/folder");
|
||||
_settings = new DownloadStationSettings();
|
||||
|
||||
Mocker.GetMock<IFileStationProxy>()
|
||||
.Setup(f => f.GetSharedFolderMapping(It.IsAny<string>(), It.IsAny<DownloadStationSettings>()))
|
||||
.Throws(new DownloadClientException("There is no shared folder"));
|
||||
|
||||
Mocker.GetMock<IFileStationProxy>()
|
||||
.Setup(f => f.GetSharedFolderMapping(_sharedFolder.FullPath, It.IsAny<DownloadStationSettings>()))
|
||||
.Returns(new SharedFolderMapping(_sharedFolder.FullPath, _physicalPath.FullPath));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_throw_when_cannot_resolve_shared_folder()
|
||||
{
|
||||
Assert.Throws(Is.InstanceOf<Exception>(), () => Subject.RemapToFullPath(new OsPath("/unknownFolder"), _settings, _serialNumber));
|
||||
|
||||
ExceptionVerification.ExpectedWarns(1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_valid_sharedfolder()
|
||||
{
|
||||
var mapping = Subject.RemapToFullPath(_sharedFolder, _settings, "abc");
|
||||
|
||||
mapping.Should().Be(_physicalPath);
|
||||
|
||||
Mocker.GetMock<IFileStationProxy>()
|
||||
.Verify(f => f.GetSharedFolderMapping(It.IsAny<string>(), It.IsAny<DownloadStationSettings>()), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_cache_mapping()
|
||||
{
|
||||
Subject.RemapToFullPath(_sharedFolder, _settings, "abc");
|
||||
Subject.RemapToFullPath(_sharedFolder, _settings, "abc");
|
||||
|
||||
Mocker.GetMock<IFileStationProxy>()
|
||||
.Verify(f => f.GetSharedFolderMapping(It.IsAny<string>(), It.IsAny<DownloadStationSettings>()), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_remap_subfolder()
|
||||
{
|
||||
var mapping = Subject.RemapToFullPath(_sharedFolder + "sub", _settings, "abc");
|
||||
|
||||
mapping.Should().Be(_physicalPath + "sub");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -173,6 +173,9 @@
|
|||
<Compile Include="Download\DownloadClientTests\Blackhole\UsenetBlackholeFixture.cs" />
|
||||
<Compile Include="Download\DownloadClientTests\DelugeTests\DelugeFixture.cs" />
|
||||
<Compile Include="Download\DownloadClientTests\DownloadClientFixtureBase.cs" />
|
||||
<Compile Include="Download\DownloadClientTests\DownloadStationTests\DownloadStationFixture.cs" />
|
||||
<Compile Include="Download\DownloadClientTests\DownloadStationTests\SerialNumberProviderFixture.cs" />
|
||||
<Compile Include="Download\DownloadClientTests\DownloadStationTests\SharedFolderResolverFixture.cs" />
|
||||
<Compile Include="Download\DownloadClientTests\HadoukenTests\HadoukenFixture.cs" />
|
||||
<Compile Include="Download\DownloadClientTests\NzbgetTests\NzbgetFixture.cs" />
|
||||
<Compile Include="Download\DownloadClientTests\NzbVortexTests\NzbVortexFixture.cs" />
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
namespace NzbDrone.Core.Download.Clients.DownloadStation
|
||||
{
|
||||
public enum DiskStationApi
|
||||
{
|
||||
Info,
|
||||
Auth,
|
||||
DownloadStationInfo,
|
||||
DownloadStationTask,
|
||||
FileStationList,
|
||||
DSMInfo,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
namespace NzbDrone.Core.Download.Clients.DownloadStation
|
||||
{
|
||||
public class DiskStationApiInfo
|
||||
{
|
||||
private string _path;
|
||||
|
||||
public int MaxVersion { get; set; }
|
||||
|
||||
public int MinVersion { get; set; }
|
||||
|
||||
public string Path
|
||||
{
|
||||
get { return _path; }
|
||||
|
||||
set
|
||||
{
|
||||
if (!string.IsNullOrEmpty(value))
|
||||
{
|
||||
_path = value.TrimStart(new char[] { '/', '\\' });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,361 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using FluentValidation.Results;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Download.Clients.DownloadStation.Proxies;
|
||||
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.RemotePathMappings;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.DownloadStation
|
||||
{
|
||||
public class DownloadStation : TorrentClientBase<DownloadStationSettings>
|
||||
{
|
||||
protected readonly IDownloadStationProxy _proxy;
|
||||
protected readonly ISharedFolderResolver _sharedFolderResolver;
|
||||
protected readonly ISerialNumberProvider _serialNumberProvider;
|
||||
|
||||
public DownloadStation(IDownloadStationProxy proxy,
|
||||
ITorrentFileInfoReader torrentFileInfoReader,
|
||||
IHttpClient httpClient,
|
||||
IConfigService configService,
|
||||
IDiskProvider diskProvider,
|
||||
IRemotePathMappingService remotePathMappingService,
|
||||
Logger logger,
|
||||
ICacheManager cacheManager,
|
||||
ISharedFolderResolver sharedFolderResolver,
|
||||
ISerialNumberProvider serialNumberProvider)
|
||||
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, logger)
|
||||
{
|
||||
_proxy = proxy;
|
||||
_sharedFolderResolver = sharedFolderResolver;
|
||||
_serialNumberProvider = serialNumberProvider;
|
||||
}
|
||||
|
||||
public override string Name => "Download Station";
|
||||
|
||||
public override IEnumerable<DownloadClientItem> GetItems()
|
||||
{
|
||||
var torrents = _proxy.GetTorrents(Settings);
|
||||
var serialNumber = _serialNumberProvider.GetSerialNumber(Settings);
|
||||
|
||||
var items = new List<DownloadClientItem>();
|
||||
|
||||
foreach (var torrent in torrents)
|
||||
{
|
||||
var outputPath = new OsPath($"/{torrent.Additional.Detail["destination"]}");
|
||||
|
||||
if (Settings.TvDirectory.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
if (!new OsPath($"/{Settings.TvDirectory}").Contains(outputPath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if (Settings.TvCategory.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
var directories = outputPath.FullPath.Split('\\', '/');
|
||||
if (!directories.Contains(Settings.TvCategory))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
var item = new DownloadClientItem()
|
||||
{
|
||||
Category = Settings.TvCategory,
|
||||
DownloadClient = Definition.Name,
|
||||
DownloadId = CreateDownloadId(torrent.Id, serialNumber),
|
||||
Title = torrent.Title,
|
||||
TotalSize = torrent.Size,
|
||||
RemainingSize = GetRemainingSize(torrent),
|
||||
RemainingTime = GetRemainingTime(torrent),
|
||||
Status = GetStatus(torrent),
|
||||
Message = GetMessage(torrent),
|
||||
IsReadOnly = !IsFinished(torrent)
|
||||
};
|
||||
|
||||
if (item.Status == DownloadItemStatus.Completed || item.Status == DownloadItemStatus.Failed)
|
||||
{
|
||||
item.OutputPath = GetOutputPath(outputPath, torrent, serialNumber);
|
||||
}
|
||||
|
||||
items.Add(item);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
public override DownloadClientStatus GetStatus()
|
||||
{
|
||||
try
|
||||
{
|
||||
var path = GetDownloadDirectory();
|
||||
|
||||
return new DownloadClientStatus
|
||||
{
|
||||
IsLocalhost = Settings.Host == "127.0.0.1" || Settings.Host == "localhost",
|
||||
OutputRootFolders = new List<OsPath> { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(path)) }
|
||||
};
|
||||
}
|
||||
catch (DownloadClientException e)
|
||||
{
|
||||
_logger.Debug(e, "Failed to get config from Download Station");
|
||||
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public override void RemoveItem(string downloadId, bool deleteData)
|
||||
{
|
||||
if (_proxy.RemoveTorrent(ParseDownloadId(downloadId), deleteData, Settings))
|
||||
{
|
||||
_logger.Debug("{0} removed correctly", downloadId);
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.Error("Failed to remove {0}", downloadId);
|
||||
}
|
||||
|
||||
protected OsPath GetOutputPath(OsPath outputPath, DownloadStationTorrent torrent, string serialNumber)
|
||||
{
|
||||
var fullPath = _sharedFolderResolver.RemapToFullPath(outputPath, Settings, serialNumber);
|
||||
|
||||
var remotePath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, fullPath);
|
||||
|
||||
var finalPath = remotePath + torrent.Title;
|
||||
|
||||
return finalPath;
|
||||
}
|
||||
|
||||
protected override string AddFromMagnetLink(RemoteEpisode remoteEpisode, string hash, string magnetLink)
|
||||
{
|
||||
var hashedSerialNumber = _serialNumberProvider.GetSerialNumber(Settings);
|
||||
|
||||
if (_proxy.AddTorrentFromUrl(magnetLink, GetDownloadDirectory(), Settings))
|
||||
{
|
||||
var item = _proxy.GetTorrents(Settings).Where(t => t.Additional.Detail["uri"] == magnetLink).SingleOrDefault();
|
||||
|
||||
if (item != null)
|
||||
{
|
||||
_logger.Debug("{0} added correctly", remoteEpisode);
|
||||
return CreateDownloadId(item.Id, hashedSerialNumber);
|
||||
}
|
||||
|
||||
_logger.Debug("No such task {0} in Download Station", magnetLink);
|
||||
}
|
||||
|
||||
throw new DownloadClientException("Failed to add magnet task to Download Station");
|
||||
}
|
||||
|
||||
protected override string AddFromTorrentFile(RemoteEpisode remoteEpisode, string hash, string filename, byte[] fileContent)
|
||||
{
|
||||
var hashedSerialNumber = _serialNumberProvider.GetSerialNumber(Settings);
|
||||
|
||||
if (_proxy.AddTorrentFromData(fileContent, filename, GetDownloadDirectory(), Settings))
|
||||
{
|
||||
var items = _proxy.GetTorrents(Settings).Where(t => t.Additional.Detail["uri"] == Path.GetFileNameWithoutExtension(filename));
|
||||
|
||||
var item = items.SingleOrDefault();
|
||||
|
||||
if (item != null)
|
||||
{
|
||||
_logger.Debug("{0} added correctly", remoteEpisode);
|
||||
return CreateDownloadId(item.Id, hashedSerialNumber);
|
||||
}
|
||||
|
||||
_logger.Debug("No such task {0} in Download Station", filename);
|
||||
}
|
||||
|
||||
throw new DownloadClientException("Failed to add torrent task to Download Station");
|
||||
}
|
||||
|
||||
protected override void Test(List<ValidationFailure> failures)
|
||||
{
|
||||
failures.AddIfNotNull(TestConnection());
|
||||
if (failures.Any()) return;
|
||||
failures.AddIfNotNull(TestGetTorrents());
|
||||
}
|
||||
|
||||
protected ValidationFailure TestConnection()
|
||||
{
|
||||
try
|
||||
{
|
||||
return ValidateVersion();
|
||||
}
|
||||
catch (DownloadClientAuthenticationException ex)
|
||||
{
|
||||
_logger.Error(ex, ex.Message);
|
||||
return new NzbDroneValidationFailure("Username", "Authentication failure")
|
||||
{
|
||||
DetailedDescription = $"Please verify your username and password. Also verify if the host running Sonarr isn't blocked from accessing {Name} by WhiteList limitations in the {Name} configuration."
|
||||
};
|
||||
}
|
||||
catch (WebException ex)
|
||||
{
|
||||
_logger.Error(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.Error(ex);
|
||||
return new NzbDroneValidationFailure(string.Empty, "Unknown exception: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
protected ValidationFailure ValidateVersion()
|
||||
{
|
||||
var versionRange = _proxy.GetApiVersion(Settings);
|
||||
|
||||
_logger.Debug("Download Station api version information: Min {0} - Max {1}", versionRange.Min(), versionRange.Max());
|
||||
|
||||
if (!versionRange.Contains(2))
|
||||
{
|
||||
return new ValidationFailure(string.Empty, $"Download Station API version not supported, should be at least 2. It supports from {versionRange.Min()} to {versionRange.Max()}");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected bool IsFinished(DownloadStationTorrent torrent)
|
||||
{
|
||||
return torrent.Status == DownloadStationTaskStatus.Finished;
|
||||
}
|
||||
|
||||
protected string GetMessage(DownloadStationTorrent torrent)
|
||||
{
|
||||
if (torrent.StatusExtra != null)
|
||||
{
|
||||
if (torrent.Status == DownloadStationTaskStatus.Extracting)
|
||||
{
|
||||
return $"Extracting: {int.Parse(torrent.StatusExtra["unzip_progress"])}%";
|
||||
}
|
||||
|
||||
if (torrent.Status == DownloadStationTaskStatus.Error)
|
||||
{
|
||||
return torrent.StatusExtra["error_detail"];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected DownloadItemStatus GetStatus(DownloadStationTorrent torrent)
|
||||
{
|
||||
switch (torrent.Status)
|
||||
{
|
||||
case DownloadStationTaskStatus.Waiting:
|
||||
return torrent.Size == 0 || GetRemainingSize(torrent) > 0 ? DownloadItemStatus.Queued : DownloadItemStatus.Completed;
|
||||
case DownloadStationTaskStatus.Paused:
|
||||
return DownloadItemStatus.Paused;
|
||||
case DownloadStationTaskStatus.Finished:
|
||||
case DownloadStationTaskStatus.Seeding:
|
||||
return DownloadItemStatus.Completed;
|
||||
case DownloadStationTaskStatus.Error:
|
||||
return DownloadItemStatus.Failed;
|
||||
}
|
||||
|
||||
return DownloadItemStatus.Downloading;
|
||||
}
|
||||
|
||||
protected long GetRemainingSize(DownloadStationTorrent torrent)
|
||||
{
|
||||
var downloadedString = torrent.Additional.Transfer["size_downloaded"];
|
||||
long downloadedSize;
|
||||
|
||||
if (downloadedString.IsNullOrWhiteSpace() || !long.TryParse(downloadedString, out downloadedSize))
|
||||
{
|
||||
_logger.Debug("Torrent {0} has invalid size_downloaded: {1}", torrent.Title, downloadedString);
|
||||
downloadedSize = 0;
|
||||
}
|
||||
|
||||
return torrent.Size - Math.Max(0, downloadedSize);
|
||||
}
|
||||
|
||||
protected TimeSpan? GetRemainingTime(DownloadStationTorrent torrent)
|
||||
{
|
||||
var speedString = torrent.Additional.Transfer["speed_download"];
|
||||
long downloadSpeed;
|
||||
|
||||
if (speedString.IsNullOrWhiteSpace() || !long.TryParse(speedString, out downloadSpeed))
|
||||
{
|
||||
_logger.Debug("Torrent {0} has invalid speed_download: {1}", torrent.Title, speedString);
|
||||
downloadSpeed = 0;
|
||||
}
|
||||
|
||||
if (downloadSpeed <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var remainingSize = GetRemainingSize(torrent);
|
||||
|
||||
return TimeSpan.FromSeconds(remainingSize / downloadSpeed);
|
||||
}
|
||||
|
||||
protected ValidationFailure TestGetTorrents()
|
||||
{
|
||||
try
|
||||
{
|
||||
_proxy.GetTorrents(Settings);
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new NzbDroneValidationFailure(string.Empty, "Failed to get the list of torrents: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
protected string ParseDownloadId(string id)
|
||||
{
|
||||
return id.Split(':')[1];
|
||||
}
|
||||
|
||||
protected string CreateDownloadId(string id, string hashedSerialNumber)
|
||||
{
|
||||
return $"{hashedSerialNumber}:{id}";
|
||||
}
|
||||
|
||||
protected string GetDefaultDir()
|
||||
{
|
||||
var config = _proxy.GetConfig(Settings);
|
||||
|
||||
var path = config["default_destination"] as string;
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
protected string GetDownloadDirectory()
|
||||
{
|
||||
if (Settings.TvDirectory.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
return Settings.TvDirectory.TrimStart('/');
|
||||
}
|
||||
else if (Settings.TvCategory.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
var destDir = GetDefaultDir();
|
||||
|
||||
return $"{destDir.TrimEnd('/')}/{Settings.TvCategory}";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
using System.Text.RegularExpressions;
|
||||
using FluentValidation;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.DownloadStation
|
||||
{
|
||||
public class DownloadStationSettingsValidator : AbstractValidator<DownloadStationSettings>
|
||||
{
|
||||
public DownloadStationSettingsValidator()
|
||||
{
|
||||
RuleFor(c => c.Host).ValidHost();
|
||||
RuleFor(c => c.Port).InclusiveBetween(1, 65535);
|
||||
|
||||
RuleFor(c => c.TvDirectory).Matches(@"^(?!/).+")
|
||||
.When(c => c.TvDirectory.IsNotNullOrWhiteSpace())
|
||||
.WithMessage("Cannot start with /");
|
||||
|
||||
RuleFor(c => c.TvCategory).Matches(@"^\.?[-a-z]*$", RegexOptions.IgnoreCase).WithMessage("Allowed characters a-z and -");
|
||||
|
||||
RuleFor(c => c.TvCategory).Empty()
|
||||
.When(c => c.TvDirectory.IsNotNullOrWhiteSpace())
|
||||
.WithMessage("Cannot use Category and Directory");
|
||||
}
|
||||
}
|
||||
|
||||
public class DownloadStationSettings : IProviderConfig
|
||||
{
|
||||
private static readonly DownloadStationSettingsValidator Validator = new DownloadStationSettingsValidator();
|
||||
|
||||
[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. Creates a [category] subdirectory in the output directory.")]
|
||||
public string TvCategory { get; set; }
|
||||
|
||||
[FieldDefinition(5, Label = "Directory", Type = FieldType.Textbox, HelpText = "Optional shared folder to put downloads into, leave blank to use the default Download Station location")]
|
||||
public string TvDirectory { get; set; }
|
||||
|
||||
[FieldDefinition(6, Label = "Use SSL", Type = FieldType.Checkbox)]
|
||||
public bool UseSsl { get; set; }
|
||||
|
||||
public DownloadStationSettings()
|
||||
{
|
||||
this.Host = "127.0.0.1";
|
||||
this.Port = 5000;
|
||||
}
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.DownloadStation
|
||||
{
|
||||
public class DownloadStationTorrent
|
||||
{
|
||||
public string Username { get; set; }
|
||||
|
||||
public string Id { get; set; }
|
||||
|
||||
public string Title { get; set; }
|
||||
|
||||
public long Size { get; set; }
|
||||
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public DownloadStationTaskType Type { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "status_extra")]
|
||||
public Dictionary<string, string> StatusExtra { get; set; }
|
||||
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public DownloadStationTaskStatus Status { get; set; }
|
||||
|
||||
public DownloadStationTorrentAdditional Additional { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return this.Title;
|
||||
}
|
||||
}
|
||||
|
||||
public enum DownloadStationTaskType
|
||||
{
|
||||
BT, NZB, http, ftp, eMule
|
||||
}
|
||||
|
||||
public enum DownloadStationTaskStatus
|
||||
{
|
||||
Waiting,
|
||||
Downloading,
|
||||
Paused,
|
||||
Finishing,
|
||||
Finished,
|
||||
HashChecking,
|
||||
Seeding,
|
||||
FileHostingWaiting,
|
||||
Extracting,
|
||||
Error
|
||||
}
|
||||
|
||||
public enum DownloadStationPriority
|
||||
{
|
||||
Auto,
|
||||
Low,
|
||||
Normal,
|
||||
High
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
using Newtonsoft.Json;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.DownloadStation
|
||||
{
|
||||
public class DownloadStationTorrentAdditional
|
||||
{
|
||||
public Dictionary<string, string> Detail { get; set; }
|
||||
|
||||
public Dictionary<string, string> Transfer { get; set; }
|
||||
|
||||
[JsonProperty("File")]
|
||||
public List<DownloadStationTorrentFile> Files { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using static NzbDrone.Core.Download.Clients.DownloadStation.DownloadStationTorrent;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.DownloadStation
|
||||
{
|
||||
public class DownloadStationTorrentFile
|
||||
{
|
||||
public string FileName { get; set; }
|
||||
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public DownloadStationPriority Priority { get; set; }
|
||||
|
||||
[JsonProperty("size")]
|
||||
public long TotalSize { get; set; }
|
||||
|
||||
[JsonProperty("size_downloaded")]
|
||||
public long BytesDownloaded { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Download.Clients.DownloadStation.Responses;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.DownloadStation.Proxies
|
||||
{
|
||||
public interface IDSMInfoProxy
|
||||
{
|
||||
string GetSerialNumber(DownloadStationSettings settings);
|
||||
}
|
||||
|
||||
public class DSMInfoProxy : DiskStationProxyBase, IDSMInfoProxy
|
||||
{
|
||||
public DSMInfoProxy(IHttpClient httpClient, Logger logger) :
|
||||
base(httpClient, logger)
|
||||
{
|
||||
}
|
||||
|
||||
public string GetSerialNumber(DownloadStationSettings settings)
|
||||
{
|
||||
var arguments = new Dictionary<string, object>() {
|
||||
{ "api", "SYNO.DSM.Info" },
|
||||
{ "version", "2" },
|
||||
{ "method", "getinfo" }
|
||||
};
|
||||
|
||||
var response = ProcessRequest<DSMInfoResponse>(DiskStationApi.DSMInfo, arguments, settings);
|
||||
|
||||
if (response.Success == true)
|
||||
{
|
||||
return response.Data.SerialNumber;
|
||||
}
|
||||
_logger.Debug("Failed to get Download Station serial number");
|
||||
throw new DownloadClientException("Failed to get Download Station serial number");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,208 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Download.Clients.DownloadStation.Responses;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.DownloadStation.Proxies
|
||||
{
|
||||
public abstract class DiskStationProxyBase
|
||||
{
|
||||
private static readonly Dictionary<DiskStationApi, string> Resources;
|
||||
|
||||
private readonly IHttpClient _httpClient;
|
||||
protected readonly Logger _logger;
|
||||
private bool _authenticated;
|
||||
|
||||
static DiskStationProxyBase()
|
||||
{
|
||||
Resources = new Dictionary<DiskStationApi, string>
|
||||
{
|
||||
{ DiskStationApi.Info, "query.cgi" }
|
||||
};
|
||||
}
|
||||
|
||||
public DiskStationProxyBase(IHttpClient httpClient, Logger logger)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
|
||||
protected DiskStationResponse<object> ProcessRequest(DiskStationApi api,
|
||||
Dictionary<string, object> arguments,
|
||||
DownloadStationSettings settings,
|
||||
HttpMethod method = HttpMethod.GET)
|
||||
{
|
||||
return ProcessRequest<object>(api, arguments, settings, method);
|
||||
}
|
||||
|
||||
protected DiskStationResponse<T> ProcessRequest<T>(DiskStationApi api,
|
||||
Dictionary<string, object> arguments,
|
||||
DownloadStationSettings settings,
|
||||
HttpMethod method = HttpMethod.GET,
|
||||
int retries = 0) where T : new()
|
||||
{
|
||||
if (retries == 5)
|
||||
{
|
||||
throw new DownloadClientException("Try to process same request more than 5 times");
|
||||
}
|
||||
|
||||
if (!_authenticated && api != DiskStationApi.Info && api != DiskStationApi.DSMInfo)
|
||||
{
|
||||
AuthenticateClient(settings);
|
||||
}
|
||||
|
||||
var request = BuildRequest(settings, api, arguments, method);
|
||||
var response = _httpClient.Execute(request);
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.OK)
|
||||
{
|
||||
var responseContent = Json.Deserialize<DiskStationResponse<T>>(response.Content);
|
||||
|
||||
if (!responseContent.Success && responseContent.Error.SessionError)
|
||||
{
|
||||
_authenticated = false;
|
||||
return ProcessRequest<T>(api, arguments, settings, method, retries++);
|
||||
}
|
||||
else
|
||||
{
|
||||
return responseContent;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new HttpException(request, response);
|
||||
}
|
||||
}
|
||||
|
||||
private void AuthenticateClient(DownloadStationSettings settings)
|
||||
{
|
||||
var arguments = new Dictionary<string, object>
|
||||
{
|
||||
{ "api", "SYNO.API.Auth" },
|
||||
{ "version", "1" },
|
||||
{ "method", "login" },
|
||||
{ "account", settings.Username },
|
||||
{ "passwd", settings.Password },
|
||||
{ "format", "cookie" },
|
||||
{ "session", "DownloadStation" },
|
||||
};
|
||||
|
||||
var authLoginRequest = BuildRequest(settings, DiskStationApi.Auth, arguments, HttpMethod.GET);
|
||||
authLoginRequest.StoreResponseCookie = true;
|
||||
|
||||
var response = _httpClient.Execute(authLoginRequest);
|
||||
|
||||
var downloadStationResponse = Json.Deserialize<DiskStationResponse<DiskStationAuthResponse>>(response.Content);
|
||||
|
||||
var authResponse = Json.Deserialize<DiskStationResponse<DiskStationAuthResponse>>(response.Content);
|
||||
|
||||
_authenticated = authResponse.Success;
|
||||
|
||||
if (!_authenticated)
|
||||
{
|
||||
throw new DownloadClientAuthenticationException(downloadStationResponse.Error.GetMessage(DiskStationApi.Auth));
|
||||
}
|
||||
}
|
||||
|
||||
private HttpRequest BuildRequest(DownloadStationSettings settings, DiskStationApi api, Dictionary<string, object> arguments, HttpMethod method)
|
||||
{
|
||||
if (!Resources.ContainsKey(api))
|
||||
{
|
||||
GetApiVersion(settings, api);
|
||||
}
|
||||
|
||||
var requestBuilder = new HttpRequestBuilder(settings.UseSsl, settings.Host, settings.Port).Resource($"webapi/{Resources[api]}");
|
||||
requestBuilder.Method = method;
|
||||
requestBuilder.LogResponseContent = true;
|
||||
requestBuilder.SuppressHttpError = true;
|
||||
requestBuilder.AllowAutoRedirect = false;
|
||||
|
||||
if (requestBuilder.Method == HttpMethod.POST)
|
||||
{
|
||||
if (api == DiskStationApi.DownloadStationTask && arguments.ContainsKey("file"))
|
||||
{
|
||||
requestBuilder.Headers.ContentType = "multipart/form-data";
|
||||
|
||||
foreach (var arg in arguments)
|
||||
{
|
||||
if (arg.Key == "file")
|
||||
{
|
||||
Dictionary<string, object> file = (Dictionary<string, object>)arg.Value;
|
||||
requestBuilder.AddFormUpload(arg.Key, file["name"].ToString(), (byte[])file["data"]);
|
||||
}
|
||||
else
|
||||
{
|
||||
requestBuilder.AddFormParameter(arg.Key, arg.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
requestBuilder.Headers.ContentType = "application/json";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var arg in arguments)
|
||||
{
|
||||
requestBuilder.AddQueryParam(arg.Key, arg.Value);
|
||||
}
|
||||
}
|
||||
|
||||
return requestBuilder.Build();
|
||||
}
|
||||
|
||||
protected IEnumerable<int> GetApiVersion(DownloadStationSettings settings, DiskStationApi api)
|
||||
{
|
||||
var arguments = new Dictionary<string, object>
|
||||
{
|
||||
{ "api", "SYNO.API.Info" },
|
||||
{ "version", "1" },
|
||||
{ "method", "query" },
|
||||
{ "query", "SYNO.API.Auth, SYNO.DownloadStation.Info, SYNO.DownloadStation.Task, SYNO.FileStation.List, SYNO.DSM.Info" },
|
||||
};
|
||||
|
||||
var infoResponse = ProcessRequest<DiskStationApiInfoResponse>(DiskStationApi.Info, arguments, settings);
|
||||
|
||||
if (infoResponse.Success == true)
|
||||
{
|
||||
//TODO: Refactor this into more elegant code
|
||||
var infoResponeDSAuth = infoResponse.Data["SYNO.API.Auth"];
|
||||
var infoResponeDSInfo = infoResponse.Data["SYNO.DownloadStation.Info"];
|
||||
var infoResponeDSTask = infoResponse.Data["SYNO.DownloadStation.Task"];
|
||||
var infoResponseFSList = infoResponse.Data["SYNO.FileStation.List"];
|
||||
var infoResponseDSMInfo = infoResponse.Data["SYNO.DSM.Info"];
|
||||
|
||||
Resources[DiskStationApi.Auth] = infoResponeDSAuth.Path;
|
||||
Resources[DiskStationApi.DownloadStationInfo] = infoResponeDSInfo.Path;
|
||||
Resources[DiskStationApi.DownloadStationTask] = infoResponeDSTask.Path;
|
||||
Resources[DiskStationApi.FileStationList] = infoResponseFSList.Path;
|
||||
Resources[DiskStationApi.DSMInfo] = infoResponseDSMInfo.Path;
|
||||
|
||||
switch (api)
|
||||
{
|
||||
case DiskStationApi.Auth:
|
||||
return Enumerable.Range(infoResponeDSAuth.MinVersion, infoResponeDSAuth.MaxVersion - infoResponeDSAuth.MinVersion + 1);
|
||||
case DiskStationApi.DownloadStationInfo:
|
||||
return Enumerable.Range(infoResponeDSInfo.MinVersion, infoResponeDSInfo.MaxVersion - infoResponeDSInfo.MinVersion + 1);
|
||||
case DiskStationApi.DownloadStationTask:
|
||||
return Enumerable.Range(infoResponeDSTask.MinVersion, infoResponeDSTask.MaxVersion - infoResponeDSTask.MinVersion + 1);
|
||||
case DiskStationApi.FileStationList:
|
||||
return Enumerable.Range(infoResponseFSList.MinVersion, infoResponseFSList.MaxVersion - infoResponseFSList.MinVersion + 1);
|
||||
case DiskStationApi.DSMInfo:
|
||||
return Enumerable.Range(infoResponseDSMInfo.MinVersion, infoResponseDSMInfo.MaxVersion - infoResponseDSMInfo.MinVersion + 1);
|
||||
default:
|
||||
throw new DownloadClientException("Api not implemented");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new DownloadClientException(infoResponse.Error.GetMessage(DiskStationApi.Info));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,146 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Download.Clients.DownloadStation.Responses;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.DownloadStation.Proxies
|
||||
{
|
||||
public interface IDownloadStationProxy
|
||||
{
|
||||
IEnumerable<DownloadStationTorrent> GetTorrents(DownloadStationSettings settings);
|
||||
Dictionary<string, object> GetConfig(DownloadStationSettings settings);
|
||||
bool RemoveTorrent(string downloadId, bool deleteData, DownloadStationSettings settings);
|
||||
bool AddTorrentFromUrl(string url, string downloadDirectory, DownloadStationSettings settings);
|
||||
bool AddTorrentFromData(byte[] torrentData, string filename, string downloadDirectory, DownloadStationSettings settings);
|
||||
IEnumerable<int> GetApiVersion(DownloadStationSettings settings);
|
||||
}
|
||||
|
||||
public class DownloadStationProxy : DiskStationProxyBase, IDownloadStationProxy
|
||||
{
|
||||
public DownloadStationProxy(IHttpClient httpClient, Logger logger)
|
||||
: base(httpClient, logger)
|
||||
{
|
||||
}
|
||||
|
||||
public bool AddTorrentFromData(byte[] torrentData, string filename, string downloadDirectory, DownloadStationSettings settings)
|
||||
{
|
||||
var arguments = new Dictionary<string, object>
|
||||
{
|
||||
{ "api", "SYNO.DownloadStation.Task" },
|
||||
{ "version", "2" },
|
||||
{ "method", "create" }
|
||||
};
|
||||
|
||||
if (downloadDirectory.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
arguments.Add("destination", downloadDirectory);
|
||||
}
|
||||
|
||||
arguments.Add("file", new Dictionary<string, object>() { { "name", filename }, { "data", torrentData } });
|
||||
|
||||
var response = ProcessRequest(DiskStationApi.DownloadStationTask, arguments, settings, HttpMethod.POST);
|
||||
|
||||
return response.Success;
|
||||
}
|
||||
|
||||
public bool AddTorrentFromUrl(string torrentUrl, string downloadDirectory, DownloadStationSettings settings)
|
||||
{
|
||||
var arguments = new Dictionary<string, object>
|
||||
{
|
||||
{ "api", "SYNO.DownloadStation.Task" },
|
||||
{ "version", "3" },
|
||||
{ "method", "create" },
|
||||
{ "uri", torrentUrl }
|
||||
};
|
||||
|
||||
if (downloadDirectory.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
arguments.Add("destination", downloadDirectory);
|
||||
}
|
||||
|
||||
var response = ProcessRequest(DiskStationApi.DownloadStationTask, arguments, settings, HttpMethod.GET);
|
||||
|
||||
return response.Success;
|
||||
}
|
||||
|
||||
public IEnumerable<DownloadStationTorrent> GetTorrents(DownloadStationSettings settings)
|
||||
{
|
||||
var arguments = new Dictionary<string, object>
|
||||
{
|
||||
{ "api", "SYNO.DownloadStation.Task" },
|
||||
{ "version", "1" },
|
||||
{ "method", "list" },
|
||||
{ "additional", "detail,transfer" }
|
||||
};
|
||||
|
||||
var response = ProcessRequest<DownloadStationTaskInfoResponse>(DiskStationApi.DownloadStationTask, arguments, settings);
|
||||
|
||||
if (response.Success)
|
||||
{
|
||||
return response.Data.Tasks.Where(t => t.Type == DownloadStationTaskType.BT);
|
||||
}
|
||||
|
||||
return new List<DownloadStationTorrent>();
|
||||
}
|
||||
|
||||
public Dictionary<string, object> GetConfig(DownloadStationSettings settings)
|
||||
{
|
||||
var arguments = new Dictionary<string, object>
|
||||
{
|
||||
{ "api", "SYNO.DownloadStation.Info" },
|
||||
{ "version", "1" },
|
||||
{ "method", "getconfig" }
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var response = ProcessRequest<Dictionary<string, object>>(DiskStationApi.DownloadStationInfo, arguments, settings);
|
||||
return response.Data;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Failed to get config from Download Station");
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public bool RemoveTorrent(string downloadId, bool deleteData, DownloadStationSettings settings)
|
||||
{
|
||||
var arguments = new Dictionary<string, object>
|
||||
{
|
||||
{ "api", "SYNO.DownloadStation.Task" },
|
||||
{ "version", "1" },
|
||||
{ "method", "delete" },
|
||||
{ "id", downloadId },
|
||||
{ "force_complete", false }
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var response = ProcessRequest(DiskStationApi.DownloadStationTask, arguments, settings);
|
||||
|
||||
if (response.Success)
|
||||
{
|
||||
_logger.Trace("Item {0} removed from Download Station", downloadId);
|
||||
}
|
||||
|
||||
return response.Success;
|
||||
}
|
||||
catch (DownloadClientException e)
|
||||
{
|
||||
_logger.Debug(e, "Failed to remove item {0} from Download Station", downloadId);
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<int> GetApiVersion(DownloadStationSettings settings)
|
||||
{
|
||||
return base.GetApiVersion(settings, DiskStationApi.DownloadStationInfo);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Download.Clients.DownloadStation.Responses;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.DownloadStation.Proxies
|
||||
{
|
||||
public interface IFileStationProxy
|
||||
{
|
||||
SharedFolderMapping GetSharedFolderMapping(string sharedFolder, DownloadStationSettings settings);
|
||||
IEnumerable<int> GetApiVersion(DownloadStationSettings settings);
|
||||
FileStationListFileInfoResponse GetInfoFileOrDirectory(string path, DownloadStationSettings settings);
|
||||
}
|
||||
|
||||
public class FileStationProxy : DiskStationProxyBase, IFileStationProxy
|
||||
{
|
||||
public FileStationProxy(IHttpClient httpClient, Logger logger)
|
||||
: base(httpClient, logger)
|
||||
{
|
||||
}
|
||||
|
||||
public IEnumerable<int> GetApiVersion(DownloadStationSettings settings)
|
||||
{
|
||||
return base.GetApiVersion(settings, DiskStationApi.FileStationList);
|
||||
}
|
||||
|
||||
public SharedFolderMapping GetSharedFolderMapping(string sharedFolder, DownloadStationSettings settings)
|
||||
{
|
||||
var info = GetInfoFileOrDirectory(sharedFolder, settings);
|
||||
|
||||
var physicalPath = info.Additional["real_path"].ToString();
|
||||
|
||||
return new SharedFolderMapping(sharedFolder, physicalPath);
|
||||
}
|
||||
|
||||
public FileStationListFileInfoResponse GetInfoFileOrDirectory(string path, DownloadStationSettings settings)
|
||||
{
|
||||
var arguments = new Dictionary<string, object>
|
||||
{
|
||||
{ "api", "SYNO.FileStation.List" },
|
||||
{ "version", "2" },
|
||||
{ "method", "getinfo" },
|
||||
{ "path", new [] { path }.ToJson() },
|
||||
{ "additional", $"[\"real_path\"]" }
|
||||
};
|
||||
|
||||
var response = ProcessRequest<FileStationListResponse>(DiskStationApi.FileStationList, arguments, settings);
|
||||
|
||||
if (response.Success == true)
|
||||
{
|
||||
return response.Data.Files.First();
|
||||
}
|
||||
|
||||
throw new DownloadClientException($"Failed to get info of {0}", path);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
using Newtonsoft.Json;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.DownloadStation.Responses
|
||||
{
|
||||
public class DSMInfoResponse
|
||||
{
|
||||
[JsonProperty("serial")]
|
||||
public string SerialNumber { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
namespace NzbDrone.Core.Download.Clients.DownloadStation.Responses
|
||||
{
|
||||
public class DiskStationAuthResponse
|
||||
{
|
||||
public string SId { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.DownloadStation.Responses
|
||||
{
|
||||
public class DiskStationError
|
||||
{
|
||||
private static readonly Dictionary<int, string> CommonMessages;
|
||||
private static readonly Dictionary<int, string> AuthMessages;
|
||||
private static readonly Dictionary<int, string> DownloadStationTaskMessages;
|
||||
private static readonly Dictionary<int, string> FileStationMessages;
|
||||
|
||||
static DiskStationError()
|
||||
{
|
||||
CommonMessages = new Dictionary<int, string>
|
||||
{
|
||||
{ 100, "Unknown error" },
|
||||
{ 101, "Invalid parameter" },
|
||||
{ 102, "The requested API does not exist" },
|
||||
{ 103, "The requested method does not exist" },
|
||||
{ 104, "The requested version does not support the functionality" },
|
||||
{ 105, "The logged in session does not have permission" },
|
||||
{ 106, "Session timeout" },
|
||||
{ 107, "Session interrupted by duplicate login" }
|
||||
};
|
||||
|
||||
AuthMessages = new Dictionary<int, string>
|
||||
{
|
||||
{ 400, "No such account or incorrect password" },
|
||||
{ 401, "Account disabled" },
|
||||
{ 402, "Permission denied" },
|
||||
{ 403, "2-step verification code required" },
|
||||
{ 404, "Failed to authenticate 2-step verification code" }
|
||||
};
|
||||
|
||||
DownloadStationTaskMessages = new Dictionary<int, string>
|
||||
{
|
||||
{ 400, "File upload failed" },
|
||||
{ 401, "Max number of tasks reached" },
|
||||
{ 402, "Destination denied" },
|
||||
{ 403, "Destination does not exist" },
|
||||
{ 404, "Invalid task id" },
|
||||
{ 405, "Invalid task action" },
|
||||
{ 406, "No default destination" },
|
||||
{ 407, "Set destination failed" },
|
||||
{ 408, "File does not exist" }
|
||||
};
|
||||
|
||||
FileStationMessages = new Dictionary<int, string>
|
||||
{
|
||||
{ 400, "Invalid parameter of file operation" },
|
||||
{ 401, "Unknown error of file operation" },
|
||||
{ 402, "System is too busy" },
|
||||
{ 403, "Invalid user does this file operation" },
|
||||
{ 404, "Invalid group does this file operation" },
|
||||
{ 405, "Invalid user and group does this file operation" },
|
||||
{ 406, "Can’t get user/group information from the account server" },
|
||||
{ 407, "Operation not permitted" },
|
||||
{ 408, "No such file or directory" },
|
||||
{ 409, "Non-supported file system" },
|
||||
{ 410, "Failed to connect internet-based file system (ex: CIFS)" },
|
||||
{ 411, "Read-only file system" },
|
||||
{ 412, "Filename too long in the non-encrypted file system" },
|
||||
{ 413, "Filename too long in the encrypted file system" },
|
||||
{ 414, "File already exists" },
|
||||
{ 415, "Disk quota exceeded" },
|
||||
{ 416, "No space left on device" },
|
||||
{ 417, "Input/output error" },
|
||||
{ 418, "Illegal name or path" },
|
||||
{ 419, "Illegal file name" },
|
||||
{ 420, "Illegal file name on FAT file system" },
|
||||
{ 421, "Device or resource busy" },
|
||||
{ 599, "No such task of the file operation" },
|
||||
};
|
||||
}
|
||||
|
||||
public int Code { get; set; }
|
||||
|
||||
public bool SessionError => Code == 105 || Code == 106 || Code == 107;
|
||||
|
||||
public string GetMessage(DiskStationApi api)
|
||||
{
|
||||
if (api == DiskStationApi.Auth && AuthMessages.ContainsKey(Code))
|
||||
{
|
||||
return AuthMessages[Code];
|
||||
}
|
||||
if (api == DiskStationApi.DownloadStationTask && DownloadStationTaskMessages.ContainsKey(Code))
|
||||
{
|
||||
return DownloadStationTaskMessages[Code];
|
||||
}
|
||||
if (api == DiskStationApi.FileStationList && FileStationMessages.ContainsKey(Code))
|
||||
{
|
||||
return FileStationMessages[Code];
|
||||
}
|
||||
|
||||
return CommonMessages[Code];
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.DownloadStation.Responses
|
||||
{
|
||||
public class DiskStationApiInfoResponse : Dictionary<string, DiskStationApiInfo>
|
||||
{
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
namespace NzbDrone.Core.Download.Clients.DownloadStation.Responses
|
||||
{
|
||||
public class DiskStationResponse<T> where T:new()
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
|
||||
public DiskStationError Error { get; set; }
|
||||
|
||||
public T Data { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.DownloadStation.Responses
|
||||
{
|
||||
public class DownloadStationTaskInfoResponse
|
||||
{
|
||||
public int Offset { get; set; }
|
||||
public List<DownloadStationTorrent> Tasks {get;set;}
|
||||
public int Total { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.DownloadStation.Responses
|
||||
{
|
||||
public class FileStationListFileInfoResponse
|
||||
{
|
||||
public bool IsDir { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Path { get; set; }
|
||||
public Dictionary <string, object> Additional { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.DownloadStation.Responses
|
||||
{
|
||||
public class FileStationListResponse
|
||||
{
|
||||
public List<FileStationListFileInfoResponse> Files { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
using System;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Common.Crypto;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Download.Clients.DownloadStation.Proxies;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.DownloadStation
|
||||
{
|
||||
public interface ISerialNumberProvider
|
||||
{
|
||||
string GetSerialNumber(DownloadStationSettings settings);
|
||||
}
|
||||
|
||||
public class SerialNumberProvider : ISerialNumberProvider
|
||||
{
|
||||
private readonly IDSMInfoProxy _proxy;
|
||||
private ICached<string> _cache;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public SerialNumberProvider(ICacheManager cacheManager,
|
||||
IDSMInfoProxy proxy,
|
||||
Logger logger)
|
||||
{
|
||||
_proxy = proxy;
|
||||
_cache = cacheManager.GetCache<string>(GetType());
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public string GetSerialNumber(DownloadStationSettings settings)
|
||||
{
|
||||
try
|
||||
{
|
||||
return _cache.Get(settings.Host, () => GetHashedSerialNumber(settings), TimeSpan.FromMinutes(5));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warn(ex, "Could not get the serial number from Download Station {0}:{1}", settings.Host, settings.Port);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private string GetHashedSerialNumber(DownloadStationSettings settings)
|
||||
{
|
||||
var serialNumber = _proxy.GetSerialNumber(settings);
|
||||
return HashConverter.GetHash(serialNumber).ToHexString();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
using NzbDrone.Common.Disk;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.DownloadStation
|
||||
{
|
||||
public class SharedFolderMapping
|
||||
{
|
||||
public OsPath PhysicalPath { get; private set; }
|
||||
public OsPath SharedFolder { get; private set; }
|
||||
|
||||
public SharedFolderMapping(string sharedFolder, string physicalPath)
|
||||
{
|
||||
SharedFolder = new OsPath(sharedFolder);
|
||||
PhysicalPath = new OsPath(physicalPath);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{SharedFolder} -> {PhysicalPath}";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
using System;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Core.Download.Clients.DownloadStation.Proxies;
|
||||
|
||||
namespace NzbDrone.Core.Download.Clients.DownloadStation
|
||||
{
|
||||
public interface ISharedFolderResolver
|
||||
{
|
||||
OsPath RemapToFullPath(OsPath sharedFolderPath, DownloadStationSettings settings, string serialNumber);
|
||||
}
|
||||
|
||||
public class SharedFolderResolver : ISharedFolderResolver
|
||||
{
|
||||
private readonly IFileStationProxy _proxy;
|
||||
private ICached<SharedFolderMapping> _cache;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public SharedFolderResolver(ICacheManager cacheManager,
|
||||
IFileStationProxy proxy,
|
||||
Logger logger)
|
||||
{
|
||||
_proxy = proxy;
|
||||
_cache = cacheManager.GetCache<SharedFolderMapping>(GetType());
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
private SharedFolderMapping GetPhysicalPath(OsPath sharedFolder, DownloadStationSettings settings)
|
||||
{
|
||||
try
|
||||
{
|
||||
return _proxy.GetSharedFolderMapping(sharedFolder.FullPath, settings);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warn(ex, "Failed to get shared folder {0} from Disk Station {1}:{2}", sharedFolder, settings.Host, settings.Port);
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public OsPath RemapToFullPath(OsPath sharedFolderPath, DownloadStationSettings settings, string serialNumber)
|
||||
{
|
||||
var index = sharedFolderPath.FullPath.IndexOf('/', 1);
|
||||
var sharedFolder = index == -1 ? sharedFolderPath : new OsPath(sharedFolderPath.FullPath.Substring(0, index));
|
||||
|
||||
var mapping = _cache.Get($"{serialNumber}:{sharedFolder}", () => GetPhysicalPath(sharedFolder, settings), TimeSpan.FromHours(1));
|
||||
|
||||
var fullPath = mapping.PhysicalPath + (sharedFolderPath - mapping.SharedFolder);
|
||||
|
||||
return fullPath;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -351,6 +351,28 @@
|
|||
<Compile Include="Download\Clients\Deluge\DelugeUpdateUIResult.cs" />
|
||||
<Compile Include="Download\Clients\DownloadClientAuthenticationException.cs" />
|
||||
<Compile Include="Download\Clients\DownloadClientException.cs" />
|
||||
<Compile Include="Download\Clients\DownloadStation\DownloadStation.cs" />
|
||||
<Compile Include="Download\Clients\DownloadStation\Proxies\DownloadStationProxy.cs" />
|
||||
<Compile Include="Download\Clients\DownloadStation\DownloadStationSettings.cs" />
|
||||
<Compile Include="Download\Clients\DownloadStation\DownloadStationTorrent.cs" />
|
||||
<Compile Include="Download\Clients\DownloadStation\DownloadStationTorrentAdditional.cs" />
|
||||
<Compile Include="Download\Clients\DownloadStation\Proxies\DSMInfoProxy.cs" />
|
||||
<Compile Include="Download\Clients\DownloadStation\Proxies\FileStationProxy.cs" />
|
||||
<Compile Include="Download\Clients\DownloadStation\Proxies\DiskStationProxyBase.cs" />
|
||||
<Compile Include="Download\Clients\DownloadStation\Responses\DiskStationAuthResponse.cs" />
|
||||
<Compile Include="Download\Clients\DownloadStation\DownloadStationTorrentFile.cs" />
|
||||
<Compile Include="Download\Clients\DownloadStation\Responses\DSMInfoResponse.cs" />
|
||||
<Compile Include="Download\Clients\DownloadStation\Responses\FileStationListFileInfoResponse.cs" />
|
||||
<Compile Include="Download\Clients\DownloadStation\Responses\FileStationListResponse.cs" />
|
||||
<Compile Include="Download\Clients\DownloadStation\Responses\DiskStationError.cs" />
|
||||
<Compile Include="Download\Clients\DownloadStation\Responses\DiskStationInfoResponse.cs" />
|
||||
<Compile Include="Download\Clients\DownloadStation\Responses\DiskStationResponse.cs" />
|
||||
<Compile Include="Download\Clients\DownloadStation\Responses\DownloadStationTaskInfoResponse.cs" />
|
||||
<Compile Include="Download\Clients\DownloadStation\DiskStationApiInfo.cs" />
|
||||
<Compile Include="Download\Clients\DownloadStation\SerialNumberProvider.cs" />
|
||||
<Compile Include="Download\Clients\DownloadStation\SharedFolderMapping.cs" />
|
||||
<Compile Include="Download\Clients\DownloadStation\SharedFolderResolver.cs" />
|
||||
<Compile Include="Download\Clients\DownloadStation\DiskStationApi.cs" />
|
||||
<Compile Include="Download\Clients\Hadouken\Hadouken.cs" />
|
||||
<Compile Include="Download\Clients\Hadouken\HadoukenProxy.cs" />
|
||||
<Compile Include="Download\Clients\Hadouken\HadoukenSettings.cs" />
|
||||
|
|
Loading…
Reference in New Issue