From 731e607666a03b062e3f1f9ebf50283ab2f68ec8 Mon Sep 17 00:00:00 2001 From: Devin Buhl Date: Wed, 22 Feb 2017 18:42:11 -0500 Subject: [PATCH] Add NZB Station for Synology (#841) --- .../TorrentDownloadStationFixture.cs | 154 +++--- .../UsenetDownloadStationFixture.cs | 452 ++++++++++++++++++ .../NzbDrone.Core.Test.csproj | 1 + .../DownloadStationSettings.cs | 2 +- ...ationTorrent.cs => DownloadStationTask.cs} | 12 +- ...al.cs => DownloadStationTaskAdditional.cs} | 4 +- ...rentFile.cs => DownloadStationTaskFile.cs} | 4 +- .../DownloadStation/Proxies/DSMInfoProxy.cs | 2 +- .../Proxies/DiskStationProxyBase.cs | 4 +- .../Proxies/DownloadStationProxy.cs | 33 +- .../Proxies/FileStationProxy.cs | 2 +- .../Responses/DiskStationError.cs | 9 +- .../DownloadStationTaskInfoResponse.cs | 2 +- .../DownloadStation/TorrentDownloadStation.cs | 85 ++-- .../DownloadStation/UsenetDownloadStation.cs | 410 ++++++++++++++++ src/NzbDrone.Core/NzbDrone.Core.csproj | 35 +- 16 files changed, 1064 insertions(+), 147 deletions(-) create mode 100644 src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadStationTests/UsenetDownloadStationFixture.cs rename src/NzbDrone.Core/Download/Clients/DownloadStation/{DownloadStationTorrent.cs => DownloadStationTask.cs} (78%) rename src/NzbDrone.Core/Download/Clients/DownloadStation/{DownloadStationTorrentAdditional.cs => DownloadStationTaskAdditional.cs} (71%) rename src/NzbDrone.Core/Download/Clients/DownloadStation/{DownloadStationTorrentFile.cs => DownloadStationTaskFile.cs} (90%) create mode 100644 src/NzbDrone.Core/Download/Clients/DownloadStation/UsenetDownloadStation.cs diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadStationTests/TorrentDownloadStationFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadStationTests/TorrentDownloadStationFixture.cs index 546a603fa..d48b29e11 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadStationTests/TorrentDownloadStationFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadStationTests/TorrentDownloadStationFixture.cs @@ -20,16 +20,16 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests { 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 DownloadStationTask _queued; + protected DownloadStationTask _downloading; + protected DownloadStationTask _failed; + protected DownloadStationTask _completed; + protected DownloadStationTask _seeding; + protected DownloadStationTask _magnet; + protected DownloadStationTask _singleFile; + protected DownloadStationTask _multipleFiles; + protected DownloadStationTask _singleFileCompleted; + protected DownloadStationTask _multipleFilesCompleted; protected string _serialNumber = "SERIALNUMBER"; protected string _category = "sonarr"; @@ -55,15 +55,15 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests Subject.Definition = new DownloadClientDefinition(); Subject.Definition.Settings = _settings; - _queued = new DownloadStationTorrent() + _queued = new DownloadStationTask() { Id = "id1", Size = 1000, Status = DownloadStationTaskStatus.Waiting, - Type = DownloadStationTaskType.BT, + Type = DownloadStationTaskType.BT.ToString(), Username = "admin", Title = "title", - Additional = new DownloadStationTorrentAdditional + Additional = new DownloadStationTaskAdditional { Detail = new Dictionary { @@ -78,15 +78,15 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests } }; - _completed = new DownloadStationTorrent() + _completed = new DownloadStationTask() { Id = "id2", Size = 1000, Status = DownloadStationTaskStatus.Finished, - Type = DownloadStationTaskType.BT, + Type = DownloadStationTaskType.BT.ToString(), Username = "admin", Title = "title", - Additional = new DownloadStationTorrentAdditional + Additional = new DownloadStationTaskAdditional { Detail = new Dictionary { @@ -101,15 +101,15 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests } }; - _seeding = new DownloadStationTorrent() + _seeding = new DownloadStationTask() { Id = "id2", Size = 1000, Status = DownloadStationTaskStatus.Seeding, - Type = DownloadStationTaskType.BT, + Type = DownloadStationTaskType.BT.ToString(), Username = "admin", Title = "title", - Additional = new DownloadStationTorrentAdditional + Additional = new DownloadStationTaskAdditional { Detail = new Dictionary { @@ -124,15 +124,15 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests } }; - _downloading = new DownloadStationTorrent() + _downloading = new DownloadStationTask() { Id = "id3", Size = 1000, Status = DownloadStationTaskStatus.Downloading, - Type = DownloadStationTaskType.BT, + Type = DownloadStationTaskType.BT.ToString(), Username = "admin", Title = "title", - Additional = new DownloadStationTorrentAdditional + Additional = new DownloadStationTaskAdditional { Detail = new Dictionary { @@ -147,15 +147,15 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests } }; - _failed = new DownloadStationTorrent() + _failed = new DownloadStationTask() { Id = "id4", Size = 1000, Status = DownloadStationTaskStatus.Error, - Type = DownloadStationTaskType.BT, + Type = DownloadStationTaskType.BT.ToString(), Username = "admin", Title = "title", - Additional = new DownloadStationTorrentAdditional + Additional = new DownloadStationTaskAdditional { Detail = new Dictionary { @@ -170,15 +170,15 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests } }; - _singleFile = new DownloadStationTorrent() + _singleFile = new DownloadStationTask() { Id = "id5", Size = 1000, Status = DownloadStationTaskStatus.Seeding, - Type = DownloadStationTaskType.BT, + Type = DownloadStationTaskType.BT.ToString(), Username = "admin", Title = "a.mkv", - Additional = new DownloadStationTorrentAdditional + Additional = new DownloadStationTaskAdditional { Detail = new Dictionary { @@ -193,15 +193,15 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests } }; - _multipleFiles = new DownloadStationTorrent() + _multipleFiles = new DownloadStationTask() { Id = "id6", Size = 1000, Status = DownloadStationTaskStatus.Seeding, - Type = DownloadStationTaskType.BT, + Type = DownloadStationTaskType.BT.ToString(), Username = "admin", Title = "title", - Additional = new DownloadStationTorrentAdditional + Additional = new DownloadStationTaskAdditional { Detail = new Dictionary { @@ -216,15 +216,15 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests } }; - _singleFileCompleted = new DownloadStationTorrent() + _singleFileCompleted = new DownloadStationTask() { Id = "id6", Size = 1000, Status = DownloadStationTaskStatus.Finished, - Type = DownloadStationTaskType.BT, + Type = DownloadStationTaskType.BT.ToString(), Username = "admin", Title = "a.mkv", - Additional = new DownloadStationTorrentAdditional + Additional = new DownloadStationTaskAdditional { Detail = new Dictionary { @@ -239,15 +239,15 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests } }; - _multipleFilesCompleted = new DownloadStationTorrent() + _multipleFilesCompleted = new DownloadStationTask() { Id = "id6", Size = 1000, Status = DownloadStationTaskStatus.Finished, - Type = DownloadStationTaskType.BT, + Type = DownloadStationTaskType.BT.ToString(), Username = "admin", Title = "title", - Additional = new DownloadStationTorrentAdditional + Additional = new DownloadStationTaskAdditional { Detail = new Dictionary { @@ -304,21 +304,21 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests _settings.TvDirectory = _tvDirectory; } - protected virtual void GivenTorrents(List torrents) + protected virtual void GivenTasks(List torrents) { if (torrents == null) { - torrents = new List(); + torrents = new List(); } Mocker.GetMock() - .Setup(s => s.GetTorrents(It.IsAny())) + .Setup(s => s.GetTasks(It.IsAny())) .Returns(torrents); } protected void PrepareClientToReturnQueuedItem() { - GivenTorrents(new List + GivenTasks(new List { _queued }); @@ -331,11 +331,11 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests .Returns(r => new HttpResponse(r, new HttpHeader(), new byte[1000])); Mocker.GetMock() - .Setup(s => s.AddTorrentFromUrl(It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(s => s.AddTaskFromUrl(It.IsAny(), It.IsAny(), It.IsAny())) .Callback(PrepareClientToReturnQueuedItem); Mocker.GetMock() - .Setup(s => s.AddTorrentFromData(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(s => s.AddTaskFromData(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Callback(PrepareClientToReturnQueuedItem); } @@ -350,10 +350,10 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests protected int GivenAllKindOfTasks() { - var tasks = new List() { _queued, _completed, _failed, _downloading, _seeding }; + var tasks = new List() { _queued, _completed, _failed, _downloading, _seeding }; Mocker.GetMock() - .Setup(d => d.GetTorrents(_settings)) + .Setup(d => d.GetTasks(_settings)) .Returns(tasks); return tasks.Count; @@ -373,7 +373,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests id.Should().NotBeNullOrEmpty(); Mocker.GetMock() - .Verify(v => v.AddTorrentFromUrl(It.IsAny(), _tvDirectory, It.IsAny()), Times.Once()); + .Verify(v => v.AddTaskFromUrl(It.IsAny(), _tvDirectory, It.IsAny()), Times.Once()); } [Test] @@ -390,7 +390,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests id.Should().NotBeNullOrEmpty(); Mocker.GetMock() - .Verify(v => v.AddTorrentFromUrl(It.IsAny(), $"{_defaultDestination}/{_category}", It.IsAny()), Times.Once()); + .Verify(v => v.AddTaskFromUrl(It.IsAny(), $"{_defaultDestination}/{_category}", It.IsAny()), Times.Once()); } [Test] @@ -406,7 +406,29 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests id.Should().NotBeNullOrEmpty(); Mocker.GetMock() - .Verify(v => v.AddTorrentFromUrl(It.IsAny(), null, It.IsAny()), Times.Once()); + .Verify(v => v.AddTaskFromUrl(It.IsAny(), null, It.IsAny()), Times.Once()); + } + + [Test] + public void GetItems_should_return_empty_list_if_no_tasks_available() + { + GivenSerialNumber(); + GivenSharedFolder(); + GivenTasks(new List()); + + Subject.GetItems().Should().BeEmpty(); + } + + [Test] + public void GetItems_should_return_ignore_tasks_of_unknown_type() + { + GivenSerialNumber(); + GivenSharedFolder(); + GivenTasks(new List { _completed }); + + _completed.Type = "ipfs"; + + Subject.GetItems().Should().BeEmpty(); } [Test] @@ -416,7 +438,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests GivenSerialNumber(); GivenSharedFolder(); - GivenTorrents(new List { _completed }); + GivenTasks(new List { _completed }); Subject.GetItems().Should().BeEmpty(); } @@ -450,7 +472,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests } [Test] - public void Download_should_throw_and_not_add_torrent_if_cannot_get_serial_number() + public void Download_should_throw_and_not_add_task_if_cannot_get_serial_number() { var remoteEpisode = CreateRemoteEpisode(); @@ -461,16 +483,16 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests Assert.Throws(Is.InstanceOf(), () => Subject.Download(remoteEpisode)); Mocker.GetMock() - .Verify(v => v.AddTorrentFromUrl(It.IsAny(), null, _settings), Times.Never()); + .Verify(v => v.AddTaskFromUrl(It.IsAny(), null, _settings), Times.Never()); } [Test] - public void GetItems_should_set_outputPath_to_base_folder_when_single_file_non_finished_torrent() + public void GetItems_should_set_outputPath_to_base_folder_when_single_file_non_finished_tasks() { GivenSerialNumber(); GivenSharedFolder(); - GivenTorrents(new List() { _singleFile }); + GivenTasks(new List() { _singleFile }); var items = Subject.GetItems(); @@ -479,12 +501,12 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests } [Test] - public void GetItems_should_set_outputPath_to_torrent_folder_when_multiple_files_non_finished_torrent() + public void GetItems_should_set_outputPath_to_torrent_folder_when_multiple_files_non_finished_tasks() { GivenSerialNumber(); GivenSharedFolder(); - GivenTorrents(new List() { _multipleFiles }); + GivenTasks(new List() { _multipleFiles }); var items = Subject.GetItems(); @@ -493,12 +515,12 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests } [Test] - public void GetItems_should_set_outputPath_to_base_folder_when_single_file_finished_torrent() + public void GetItems_should_set_outputPath_to_base_folder_when_single_file_finished_tasks() { GivenSerialNumber(); GivenSharedFolder(); - GivenTorrents(new List() { _singleFileCompleted }); + GivenTasks(new List() { _singleFileCompleted }); var items = Subject.GetItems(); @@ -507,12 +529,12 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests } [Test] - public void GetItems_should_set_outputPath_to_torrent_folder_when_multiple_files_finished_torrent() + public void GetItems_should_set_outputPath_to_torrent_folder_when_multiple_files_finished_tasks() { GivenSerialNumber(); GivenSharedFolder(); - GivenTorrents(new List() { _multipleFilesCompleted }); + GivenTasks(new List() { _multipleFilesCompleted }); var items = Subject.GetItems(); @@ -521,12 +543,12 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests } [Test] - public void GetItems_should_not_map_outputpath_for_queued_or_downloading_torrents() + public void GetItems_should_not_map_outputpath_for_queued_or_downloading_tasks() { GivenSerialNumber(); GivenSharedFolder(); - GivenTorrents(new List + GivenTasks(new List { _queued, _downloading }); @@ -538,12 +560,12 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests } [Test] - public void GetItems_should_map_outputpath_for_completed_or_failed_torrents() + public void GetItems_should_map_outputpath_for_completed_or_failed_tasks() { GivenSerialNumber(); GivenSharedFolder(); - GivenTorrents(new List + GivenTasks(new List { _completed, _failed, _seeding }); @@ -565,7 +587,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests _queued.Status = apiStatus; - GivenTorrents(new List() { _queued }); + GivenTasks(new List() { _queued }); var items = Subject.GetItems(); @@ -589,7 +611,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests _queued.Status = apiStatus; - GivenTorrents(new List() { _queued }); + GivenTasks(new List() { _queued }); var items = Subject.GetItems(); items.Should().HaveCount(1); @@ -597,4 +619,4 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests items.First().Status.Should().Be(expectedItemStatus); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadStationTests/UsenetDownloadStationFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadStationTests/UsenetDownloadStationFixture.cs new file mode 100644 index 000000000..2e44c60ba --- /dev/null +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadStationTests/UsenetDownloadStationFixture.cs @@ -0,0 +1,452 @@ +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; +using NzbDrone.Core.Organizer; + +namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests +{ + [TestFixture] + public class UsenetDownloadStationFixture : DownloadClientFixtureBase + { + protected DownloadStationSettings _settings; + + protected DownloadStationTask _queued; + protected DownloadStationTask _downloading; + protected DownloadStationTask _failed; + protected DownloadStationTask _completed; + protected DownloadStationTask _seeding; + + 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 RemoteEpisode _remoteEpisode; + + protected Dictionary _downloadStationConfigItems; + + [SetUp] + public void Setup() + { + _remoteEpisode = CreateRemoteEpisode(); + + _settings = new DownloadStationSettings() + { + Host = "127.0.0.1", + Port = 5000, + Username = "admin", + Password = "pass" + }; + + Subject.Definition = new DownloadClientDefinition(); + Subject.Definition.Settings = _settings; + + _queued = new DownloadStationTask() + { + Id = "id1", + Size = 1000, + Status = DownloadStationTaskStatus.Waiting, + Type = DownloadStationTaskType.NZB.ToString(), + Username = "admin", + Title = "title", + Additional = new DownloadStationTaskAdditional + { + Detail = new Dictionary + { + { "destination","shared/folder" }, + { "uri", FileNameBuilder.CleanFileName(_remoteEpisode.Release.Title) + ".nzb" } + }, + Transfer = new Dictionary + { + { "size_downloaded", "0"}, + { "speed_download", "0" } + } + } + }; + + _completed = new DownloadStationTask() + { + Id = "id2", + Size = 1000, + Status = DownloadStationTaskStatus.Finished, + Type = DownloadStationTaskType.NZB.ToString(), + Username = "admin", + Title = "title", + Additional = new DownloadStationTaskAdditional + { + Detail = new Dictionary + { + { "destination","shared/folder" }, + { "uri", FileNameBuilder.CleanFileName(_remoteEpisode.Release.Title) + ".nzb" } + }, + Transfer = new Dictionary + { + { "size_downloaded", "1000"}, + { "speed_download", "0" } + }, + } + }; + + _seeding = new DownloadStationTask() + { + Id = "id2", + Size = 1000, + Status = DownloadStationTaskStatus.Seeding, + Type = DownloadStationTaskType.NZB.ToString(), + Username = "admin", + Title = "title", + Additional = new DownloadStationTaskAdditional + { + Detail = new Dictionary + { + { "destination","shared/folder" }, + { "uri", FileNameBuilder.CleanFileName(_remoteEpisode.Release.Title) + ".nzb" } + }, + Transfer = new Dictionary + { + { "size_downloaded", "1000"}, + { "speed_download", "0" } + } + } + }; + + _downloading = new DownloadStationTask() + { + Id = "id3", + Size = 1000, + Status = DownloadStationTaskStatus.Downloading, + Type = DownloadStationTaskType.NZB.ToString(), + Username = "admin", + Title = "title", + Additional = new DownloadStationTaskAdditional + { + Detail = new Dictionary + { + { "destination","shared/folder" }, + { "uri", FileNameBuilder.CleanFileName(_remoteEpisode.Release.Title) + ".nzb" } + }, + Transfer = new Dictionary + { + { "size_downloaded", "100"}, + { "speed_download", "50" } + } + } + }; + + _failed = new DownloadStationTask() + { + Id = "id4", + Size = 1000, + Status = DownloadStationTaskStatus.Error, + Type = DownloadStationTaskType.NZB.ToString(), + Username = "admin", + Title = "title", + Additional = new DownloadStationTaskAdditional + { + Detail = new Dictionary + { + { "destination","shared/folder" }, + { "uri", FileNameBuilder.CleanFileName(_remoteEpisode.Release.Title) + ".nzb" } + }, + Transfer = new Dictionary + { + { "size_downloaded", "10"}, + { "speed_download", "0" } + } + } + }; + + Mocker.GetMock() + .Setup(s => s.Get(It.IsAny())) + .Returns(r => new HttpResponse(r, new HttpHeader(), new byte[0])); + + _downloadStationConfigItems = new Dictionary + { + { "default_destination", _defaultDestination }, + }; + + Mocker.GetMock() + .Setup(v => v.GetConfig(It.IsAny())) + .Returns(_downloadStationConfigItems); + } + + protected void GivenSharedFolder() + { + Mocker.GetMock() + .Setup(s => s.RemapToFullPath(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((path, setttings, serial) => _physicalPath); + } + + protected void GivenSerialNumber() + { + Mocker.GetMock() + .Setup(s => s.GetSerialNumber(It.IsAny())) + .Returns(_serialNumber); + } + + protected void GivenTvCategory() + { + _settings.TvCategory = _category; + } + + protected void GivenTvDirectory() + { + _settings.TvDirectory = _tvDirectory; + } + + protected virtual void GivenTasks(List nzbs) + { + if (nzbs == null) + { + nzbs = new List(); + } + + Mocker.GetMock() + .Setup(s => s.GetTasks(It.IsAny())) + .Returns(nzbs); + } + + protected void PrepareClientToReturnQueuedItem() + { + GivenTasks(new List + { + _queued + }); + } + + protected void GivenSuccessfulDownload() + {/* + Mocker.GetMock() + .Setup(s => s.Get(It.IsAny())) + .Returns(r => new HttpResponse(r, new HttpHeader(), new byte[1000])); + */ + + Mocker.GetMock() + .Setup(s => s.AddTaskFromData(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(PrepareClientToReturnQueuedItem); + } + + protected void GivenAllKindOfTasks() + { + var tasks = new List() { _queued, _completed, _failed, _downloading, _seeding }; + + Mocker.GetMock() + .Setup(d => d.GetTasks(_settings)) + .Returns(tasks); + } + + [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() + .Verify(v => v.AddTaskFromData(It.IsAny(), It.IsAny(), _tvDirectory, It.IsAny()), 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() + .Verify(v => v.AddTaskFromData(It.IsAny(), It.IsAny(), $"{_defaultDestination}/{_category}", It.IsAny()), 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() + .Verify(v => v.AddTaskFromData(It.IsAny(), It.IsAny(), null, It.IsAny()), Times.Once()); + } + + [Test] + public void GetItems_should_return_empty_list_if_no_tasks_available() + { + GivenSerialNumber(); + GivenSharedFolder(); + GivenTasks(new List()); + + Subject.GetItems().Should().BeEmpty(); + } + + [Test] + public void GetItems_should_return_ignore_tasks_of_unknown_type() + { + GivenSerialNumber(); + GivenSharedFolder(); + GivenTasks(new List { _completed }); + + _completed.Type = "ipfs"; + + Subject.GetItems().Should().BeEmpty(); + } + + [Test] + public void GetItems_should_ignore_downloads_in_wrong_folder() + { + _settings.TvDirectory = @"/shared/folder/sub"; + + GivenSerialNumber(); + GivenSharedFolder(); + GivenTasks(new List { _completed }); + + Subject.GetItems().Should().BeEmpty(); + } + + [Test] + public void GetItems_should_throw_if_shared_folder_resolve_fails() + { + Mocker.GetMock() + .Setup(s => s.RemapToFullPath(It.IsAny(), It.IsAny(), It.IsAny())) + .Throws(new ApplicationException("Some unknown exception, HttpException or DownloadClientException")); + + GivenSerialNumber(); + GivenAllKindOfTasks(); + + Assert.Throws(Is.InstanceOf(), () => Subject.GetItems()); + ExceptionVerification.ExpectedErrors(0); + } + + [Test] + public void GetItems_should_throw_if_serial_number_unavailable() + { + Mocker.GetMock() + .Setup(s => s.GetSerialNumber(_settings)) + .Throws(new ApplicationException("Some unknown exception, HttpException or DownloadClientException")); + + GivenSharedFolder(); + GivenAllKindOfTasks(); + + Assert.Throws(Is.InstanceOf(), () => Subject.GetItems()); + ExceptionVerification.ExpectedErrors(0); + } + + [Test] + public void Download_should_throw_and_not_add_task_if_cannot_get_serial_number() + { + var remoteEpisode = CreateRemoteEpisode(); + + Mocker.GetMock() + .Setup(s => s.GetSerialNumber(_settings)) + .Throws(new ApplicationException("Some unknown exception, HttpException or DownloadClientException")); + + Assert.Throws(Is.InstanceOf(), () => Subject.Download(remoteEpisode)); + + Mocker.GetMock() + .Verify(v => v.AddTaskFromUrl(It.IsAny(), null, _settings), Times.Never()); + } + + [Test] + public void GetItems_should_not_map_outputpath_for_queued_or_downloading_tasks() + { + GivenSerialNumber(); + GivenSharedFolder(); + + GivenTasks(new List + { + _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_tasks() + { + GivenSerialNumber(); + GivenSharedFolder(); + + GivenTasks(new List + { + _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.Waiting, DownloadItemStatus.Queued, true)] + public void GetItems_should_return_readonly_expected(DownloadStationTaskStatus apiStatus, DownloadItemStatus expectedItemStatus, bool readOnlyExpected) + { + GivenSerialNumber(); + GivenSharedFolder(); + + _queued.Status = apiStatus; + + GivenTasks(new List() { _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.Waiting, DownloadItemStatus.Queued)] + public void GetItems_should_return_item_as_downloadItemStatus(DownloadStationTaskStatus apiStatus, DownloadItemStatus expectedItemStatus) + { + GivenSerialNumber(); + GivenSharedFolder(); + + _queued.Status = apiStatus; + + GivenTasks(new List() { _queued }); + + var items = Subject.GetItems(); + items.Should().HaveCount(1); + + items.First().Status.Should().Be(expectedItemStatus); + } + } +} diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 10b01b539..b16274137 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -186,6 +186,7 @@ + diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationSettings.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationSettings.cs index ab2bbf06e..a1e12d899 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationSettings.cs @@ -42,7 +42,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation [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 Radarr avoids conflicts with unrelated downloads, but it's optional. Creates a [category] subdirectory in the output directory.")] + [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")] diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationTorrent.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationTask.cs similarity index 78% rename from src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationTorrent.cs rename to src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationTask.cs index e840fddb0..a22cc7296 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationTorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationTask.cs @@ -5,7 +5,7 @@ using Newtonsoft.Json.Converters; namespace NzbDrone.Core.Download.Clients.DownloadStation { - public class DownloadStationTorrent + public class DownloadStationTask { public string Username { get; set; } @@ -15,8 +15,10 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation public long Size { get; set; } - [JsonConverter(typeof(StringEnumConverter))] - public DownloadStationTaskType Type { get; set; } + /// + /// /// Possible values are: BT, NZB, http, ftp, eMule and https + /// + public string Type { get; set; } [JsonProperty(PropertyName = "status_extra")] public Dictionary StatusExtra { get; set; } @@ -24,7 +26,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation [JsonConverter(typeof(StringEnumConverter))] public DownloadStationTaskStatus Status { get; set; } - public DownloadStationTorrentAdditional Additional { get; set; } + public DownloadStationTaskAdditional Additional { get; set; } public override string ToString() { @@ -34,7 +36,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation public enum DownloadStationTaskType { - BT, NZB, http, ftp, eMule + BT, NZB, http, ftp, eMule, https } public enum DownloadStationTaskStatus diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationTorrentAdditional.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationTaskAdditional.cs similarity index 71% rename from src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationTorrentAdditional.cs rename to src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationTaskAdditional.cs index f10c84b00..45c9fd137 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationTorrentAdditional.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationTaskAdditional.cs @@ -3,13 +3,13 @@ using System.Collections.Generic; namespace NzbDrone.Core.Download.Clients.DownloadStation { - public class DownloadStationTorrentAdditional + public class DownloadStationTaskAdditional { public Dictionary Detail { get; set; } public Dictionary Transfer { get; set; } [JsonProperty("File")] - public List Files { get; set; } + public List Files { get; set; } } } diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationTorrentFile.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationTaskFile.cs similarity index 90% rename from src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationTorrentFile.cs rename to src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationTaskFile.cs index 186911c77..d49c5d7dd 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationTorrentFile.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/DownloadStationTaskFile.cs @@ -4,11 +4,11 @@ using System; using System.Collections.Generic; using System.Linq; using System.Text; -using static NzbDrone.Core.Download.Clients.DownloadStation.DownloadStationTorrent; +using static NzbDrone.Core.Download.Clients.DownloadStation.DownloadStationTask; namespace NzbDrone.Core.Download.Clients.DownloadStation { - public class DownloadStationTorrentFile + public class DownloadStationTaskFile { public string FileName { get; set; } diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DSMInfoProxy.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DSMInfoProxy.cs index 2d8bc75e4..b5687a0e0 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DSMInfoProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DSMInfoProxy.cs @@ -30,4 +30,4 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation.Proxies return response.Data.SerialNumber; } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DiskStationProxyBase.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DiskStationProxyBase.cs index e64ccf4a7..7a6538970 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DiskStationProxyBase.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DiskStationProxyBase.cs @@ -77,7 +77,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation.Proxies _authenticated = false; return ProcessRequest(api, arguments, settings, operation, method, retries++); } - + var msg = $"Failed to {operation}. Reason: {responseContent.Error.GetMessage(api)}"; _logger.Error(msg); @@ -210,4 +210,4 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation.Proxies } } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DownloadStationProxy.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DownloadStationProxy.cs index bf14d1899..427cd3d5b 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DownloadStationProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/DownloadStationProxy.cs @@ -10,11 +10,11 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation.Proxies { public interface IDownloadStationProxy { - IEnumerable GetTorrents(DownloadStationSettings settings); + IEnumerable GetTasks(DownloadStationSettings settings); Dictionary GetConfig(DownloadStationSettings settings); - void RemoveTorrent(string downloadId, DownloadStationSettings settings); - void AddTorrentFromUrl(string url, string downloadDirectory, DownloadStationSettings settings); - void AddTorrentFromData(byte[] torrentData, string filename, string downloadDirectory, DownloadStationSettings settings); + void RemoveTask(string downloadId, DownloadStationSettings settings); + void AddTaskFromUrl(string url, string downloadDirectory, DownloadStationSettings settings); + void AddTaskFromData(byte[] data, string filename, string downloadDirectory, DownloadStationSettings settings); IEnumerable GetApiVersion(DownloadStationSettings settings); } @@ -25,7 +25,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation.Proxies { } - public void AddTorrentFromData(byte[] torrentData, string filename, string downloadDirectory, DownloadStationSettings settings) + public void AddTaskFromData(byte[] data, string filename, string downloadDirectory, DownloadStationSettings settings) { var arguments = new Dictionary { @@ -39,19 +39,19 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation.Proxies arguments.Add("destination", downloadDirectory); } - arguments.Add("file", new Dictionary() { { "name", filename }, { "data", torrentData } }); + arguments.Add("file", new Dictionary() { { "name", filename }, { "data", data } }); - var response = ProcessRequest(DiskStationApi.DownloadStationTask, arguments, settings, $"add torrent from data {filename}", HttpMethod.POST); + var response = ProcessRequest(DiskStationApi.DownloadStationTask, arguments, settings, $"add task from data {filename}", HttpMethod.POST); } - public void AddTorrentFromUrl(string torrentUrl, string downloadDirectory, DownloadStationSettings settings) + public void AddTaskFromUrl(string url, string downloadDirectory, DownloadStationSettings settings) { var arguments = new Dictionary { { "api", "SYNO.DownloadStation.Task" }, { "version", "3" }, { "method", "create" }, - { "uri", torrentUrl } + { "uri", url } }; if (downloadDirectory.IsNotNullOrWhiteSpace()) @@ -59,10 +59,10 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation.Proxies arguments.Add("destination", downloadDirectory); } - var response = ProcessRequest(DiskStationApi.DownloadStationTask, arguments, settings, $"add torrent from url {torrentUrl}", HttpMethod.GET); + var response = ProcessRequest(DiskStationApi.DownloadStationTask, arguments, settings, $"add task from url {url}"); } - public IEnumerable GetTorrents(DownloadStationSettings settings) + public IEnumerable GetTasks(DownloadStationSettings settings) { var arguments = new Dictionary { @@ -74,14 +74,14 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation.Proxies try { - var response = ProcessRequest(DiskStationApi.DownloadStationTask, arguments, settings, "get torrents"); + var response = ProcessRequest(DiskStationApi.DownloadStationTask, arguments, settings, "get tasks"); - return response.Data.Tasks.Where(t => t.Type == DownloadStationTaskType.BT); + return response.Data.Tasks; } catch (DownloadClientException e) { _logger.Error(e); - return new List(); + return new List(); } } @@ -99,7 +99,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation.Proxies return response.Data; } - public void RemoveTorrent(string downloadId, DownloadStationSettings settings) + public void RemoveTask(string downloadId, DownloadStationSettings settings) { var arguments = new Dictionary { @@ -111,7 +111,6 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation.Proxies }; var response = ProcessRequest(DiskStationApi.DownloadStationTask, arguments, settings, $"remove item {downloadId}"); - _logger.Trace("Item {0} removed from Download Station", downloadId); } public IEnumerable GetApiVersion(DownloadStationSettings settings) @@ -119,4 +118,4 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation.Proxies return base.GetApiVersion(settings, DiskStationApi.DownloadStationInfo); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/FileStationProxy.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/FileStationProxy.cs index e3476df87..4beacd8e9 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/FileStationProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/Proxies/FileStationProxy.cs @@ -52,4 +52,4 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation.Proxies return response.Data.Files.First(); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/Responses/DiskStationError.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/Responses/DiskStationError.cs index af818313e..d8ce31d71 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/Responses/DiskStationError.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/Responses/DiskStationError.cs @@ -83,16 +83,23 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation.Responses { 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]; + if (CommonMessages.ContainsKey(Code)) + { + return CommonMessages[Code]; + } + + return $"{ Code } - Unknown error"; } } } diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/Responses/DownloadStationTaskInfoResponse.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/Responses/DownloadStationTaskInfoResponse.cs index 1da16621c..1d974b913 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/Responses/DownloadStationTaskInfoResponse.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/Responses/DownloadStationTaskInfoResponse.cs @@ -5,7 +5,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation.Responses public class DownloadStationTaskInfoResponse { public int Offset { get; set; } - public List Tasks {get;set;} + public List Tasks {get;set;} public int Total { get; set; } } } diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/TorrentDownloadStation.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/TorrentDownloadStation.cs index c236115b0..57b1d389c 100644 --- a/src/NzbDrone.Core/Download/Clients/DownloadStation/TorrentDownloadStation.cs +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/TorrentDownloadStation.cs @@ -25,30 +25,34 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation protected readonly ISerialNumberProvider _serialNumberProvider; protected readonly IFileStationProxy _fileStationProxy; - public TorrentDownloadStation(IDownloadStationProxy proxy, - ITorrentFileInfoReader torrentFileInfoReader, - IHttpClient httpClient, - IConfigService configService, - IDiskProvider diskProvider, - IRemotePathMappingService remotePathMappingService, - Logger logger, - ICacheManager cacheManager, - ISharedFolderResolver sharedFolderResolver, - ISerialNumberProvider serialNumberProvider, - IFileStationProxy fileStationProxy) + public TorrentDownloadStation(ISharedFolderResolver sharedFolderResolver, + ISerialNumberProvider serialNumberProvider, + IFileStationProxy fileStationProxy, + IDownloadStationProxy proxy, + ITorrentFileInfoReader torrentFileInfoReader, + IHttpClient httpClient, + IConfigService configService, + IDiskProvider diskProvider, + IRemotePathMappingService remotePathMappingService, + Logger logger) : base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, logger) { _proxy = proxy; + _fileStationProxy = fileStationProxy; _sharedFolderResolver = sharedFolderResolver; _serialNumberProvider = serialNumberProvider; - _fileStationProxy = fileStationProxy; } public override string Name => "Download Station"; + protected IEnumerable GetTasks() + { + return _proxy.GetTasks(Settings).Where(v => v.Type == DownloadStationTaskType.BT.ToString()); + } + public override IEnumerable GetItems() { - var torrents = _proxy.GetTorrents(Settings); + var torrents = GetTasks(); var serialNumber = _serialNumberProvider.GetSerialNumber(Settings); var items = new List(); @@ -125,11 +129,11 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation DeleteItemData(downloadId); } - _proxy.RemoveTorrent(ParseDownloadId(downloadId), Settings); + _proxy.RemoveTask(ParseDownloadId(downloadId), Settings); _logger.Debug("{0} removed correctly", downloadId); } - protected OsPath GetOutputPath(OsPath outputPath, DownloadStationTorrent torrent, string serialNumber) + protected OsPath GetOutputPath(OsPath outputPath, DownloadStationTask torrent, string serialNumber) { var fullPath = _sharedFolderResolver.RemapToFullPath(outputPath, Settings, serialNumber); @@ -154,9 +158,9 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation { var hashedSerialNumber = _serialNumberProvider.GetSerialNumber(Settings); - _proxy.AddTorrentFromUrl(magnetLink, GetDownloadDirectory(), Settings); + _proxy.AddTaskFromUrl(magnetLink, GetDownloadDirectory(), Settings); - var item = _proxy.GetTorrents(Settings).SingleOrDefault(t => t.Additional.Detail["uri"] == magnetLink); + var item = GetTasks().SingleOrDefault(t => t.Additional.Detail["uri"] == magnetLink); if (item != null) { @@ -173,9 +177,9 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation { var hashedSerialNumber = _serialNumberProvider.GetSerialNumber(Settings); - _proxy.AddTorrentFromData(fileContent, filename, GetDownloadDirectory(), Settings); + _proxy.AddTaskFromData(fileContent, filename, GetDownloadDirectory(), Settings); - var items = _proxy.GetTorrents(Settings).Where(t => t.Additional.Detail["uri"] == Path.GetFileNameWithoutExtension(filename)); + var items = GetTasks().Where(t => t.Additional.Detail["uri"] == Path.GetFileNameWithoutExtension(filename)); var item = items.SingleOrDefault(); @@ -195,16 +199,15 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation failures.AddIfNotNull(TestConnection()); if (failures.Any()) return; failures.AddIfNotNull(TestOutputPath()); - if (failures.Any()) return; failures.AddIfNotNull(TestGetTorrents()); } - protected bool IsFinished(DownloadStationTorrent torrent) + protected bool IsFinished(DownloadStationTask torrent) { return torrent.Status == DownloadStationTaskStatus.Finished; } - protected string GetMessage(DownloadStationTorrent torrent) + protected string GetMessage(DownloadStationTask torrent) { if (torrent.StatusExtra != null) { @@ -222,7 +225,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation return null; } - protected DownloadItemStatus GetStatus(DownloadStationTorrent torrent) + protected DownloadItemStatus GetStatus(DownloadStationTask torrent) { switch (torrent.Status) { @@ -240,7 +243,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation return DownloadItemStatus.Downloading; } - protected long GetRemainingSize(DownloadStationTorrent torrent) + protected long GetRemainingSize(DownloadStationTask torrent) { var downloadedString = torrent.Additional.Transfer["size_downloaded"]; long downloadedSize; @@ -254,7 +257,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation return torrent.Size - Math.Max(0, downloadedSize); } - protected TimeSpan? GetRemainingTime(DownloadStationTorrent torrent) + protected TimeSpan? GetRemainingTime(DownloadStationTask torrent) { var speedString = torrent.Additional.Transfer["speed_download"]; long downloadSpeed; @@ -279,18 +282,38 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation { try { - var folderInfo = _fileStationProxy.GetInfoFileOrDirectory($"/{GetDownloadDirectory()}", Settings); + var downloadDir = GetDownloadDirectory(); - if (!folderInfo.IsDir || folderInfo.Additional == null) + if (downloadDir != null) { - throw new Exception($"{folderInfo.Path} is not a shared folder or it doesn't exist"); + var sharedFolder = downloadDir.Split('\\', '/')[0]; + var fieldName = Settings.TvDirectory.IsNotNullOrWhiteSpace() ? nameof(Settings.TvDirectory) : nameof(Settings.TvCategory); + + var folderInfo = _fileStationProxy.GetInfoFileOrDirectory($"/{downloadDir}", Settings); + + if (folderInfo.Additional == null) + { + return new NzbDroneValidationFailure(fieldName, $"Shared folder does not exist") + { + DetailedDescription = $"The DownloadStation does not have a Shared Folder with the name '{sharedFolder}', are you sure you specified it correctly?" + }; + } + + if (!folderInfo.IsDir) + { + return new NzbDroneValidationFailure(fieldName, $"Folder does not exist") + { + DetailedDescription = $"The folder '{downloadDir}' does not exist, it must be created manually inside the Shared Folder '{sharedFolder}'." + }; + } } return null; } catch (Exception ex) { - return new NzbDroneValidationFailure(string.Empty, $"Failed to get output path: {ex.Message}"); + _logger.Error(ex); + return new NzbDroneValidationFailure(string.Empty, $"Unknown exception: {ex.Message}"); } } @@ -305,7 +328,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation _logger.Error(ex, ex.Message); return new NzbDroneValidationFailure("Username", "Authentication failure") { - DetailedDescription = $"Please verify your username and password. Also verify if the host running Radarr isn't blocked from accessing {Name} by WhiteList limitations in the {Name} configuration." + 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) @@ -346,7 +369,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation { try { - _proxy.GetTorrents(Settings); + GetItems(); return null; } catch (Exception ex) diff --git a/src/NzbDrone.Core/Download/Clients/DownloadStation/UsenetDownloadStation.cs b/src/NzbDrone.Core/Download/Clients/DownloadStation/UsenetDownloadStation.cs new file mode 100644 index 000000000..f080a17d0 --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/DownloadStation/UsenetDownloadStation.cs @@ -0,0 +1,410 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using FluentValidation.Results; +using NLog; +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.Parser.Model; +using NzbDrone.Core.RemotePathMappings; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.Download.Clients.DownloadStation +{ + public class UsenetDownloadStation : UsenetClientBase + { + protected readonly IDownloadStationProxy _proxy; + protected readonly ISharedFolderResolver _sharedFolderResolver; + protected readonly ISerialNumberProvider _serialNumberProvider; + protected readonly IFileStationProxy _fileStationProxy; + + public UsenetDownloadStation(ISharedFolderResolver sharedFolderResolver, + ISerialNumberProvider serialNumberProvider, + IFileStationProxy fileStationProxy, + IDownloadStationProxy proxy, + IHttpClient httpClient, + IConfigService configService, + IDiskProvider diskProvider, + IRemotePathMappingService remotePathMappingService, + Logger logger + ) + : base(httpClient, configService, diskProvider, remotePathMappingService, logger) + { + _proxy = proxy; + _fileStationProxy = fileStationProxy; + _sharedFolderResolver = sharedFolderResolver; + _serialNumberProvider = serialNumberProvider; + } + + public override string Name => "Download Station"; + + protected IEnumerable GetTasks() + { + return _proxy.GetTasks(Settings).Where(v => v.Type == DownloadStationTaskType.NZB.ToString()); + } + + public override IEnumerable GetItems() + { + var nzbTasks = GetTasks(); + var serialNumber = _serialNumberProvider.GetSerialNumber(Settings); + + var items = new List(); + + long totalRemainingSize = 0; + long globalSpeed = nzbTasks.Where(t => t.Status == DownloadStationTaskStatus.Downloading) + .Select(GetDownloadSpeed) + .Sum(); + + foreach (var nzb in nzbTasks) + { + var outputPath = new OsPath($"/{nzb.Additional.Detail["destination"]}"); + + var taskRemainingSize = GetRemainingSize(nzb); + + if (nzb.Status != DownloadStationTaskStatus.Paused) + { + totalRemainingSize += taskRemainingSize; + } + + 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(nzb.Id, serialNumber), + Title = nzb.Title, + TotalSize = nzb.Size, + RemainingSize = taskRemainingSize, + Status = GetStatus(nzb), + Message = GetMessage(nzb), + IsReadOnly = !IsFinished(nzb) + }; + + if (item.Status != DownloadItemStatus.Paused) + { + item.RemainingTime = GetRemainingTime(totalRemainingSize, globalSpeed); + } + + if (item.Status == DownloadItemStatus.Completed || item.Status == DownloadItemStatus.Failed) + { + item.OutputPath = GetOutputPath(outputPath, nzb, serialNumber); + } + + items.Add(item); + } + + return items; + } + + protected OsPath GetOutputPath(OsPath outputPath, DownloadStationTask task, string serialNumber) + { + var fullPath = _sharedFolderResolver.RemapToFullPath(outputPath, Settings, serialNumber); + + var remotePath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, fullPath); + + var finalPath = remotePath + task.Title; + + return finalPath; + } + + public override DownloadClientStatus GetStatus() + { + try + { + var path = GetDownloadDirectory(); + + return new DownloadClientStatus + { + IsLocalhost = Settings.Host == "127.0.0.1" || Settings.Host == "localhost", + OutputRootFolders = new List { _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 (deleteData) + { + DeleteItemData(downloadId); + } + + _proxy.RemoveTask(ParseDownloadId(downloadId), Settings); + _logger.Debug("{0} removed correctly", downloadId); + } + + protected override string AddFromNzbFile(RemoteEpisode remoteEpisode, string filename, byte[] fileContent) + { + throw new DownloadClientException("Episodes are not working with Radarr"); + } + + protected override string AddFromNzbFile(RemoteMovie remoteEpisode, string filename, byte[] fileContent) + { + var hashedSerialNumber = _serialNumberProvider.GetSerialNumber(Settings); + + _proxy.AddTaskFromData(fileContent, filename, GetDownloadDirectory(), Settings); + + var items = GetTasks().Where(t => t.Additional.Detail["uri"] == 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 NZB task to Download Station"); + } + + protected override void Test(List failures) + { + failures.AddIfNotNull(TestConnection()); + if (failures.Any()) return; + failures.AddIfNotNull(TestOutputPath()); + failures.AddIfNotNull(TestGetNZB()); + } + + protected ValidationFailure TestOutputPath() + { + try + { + var downloadDir = GetDownloadDirectory(); + + if (downloadDir != null) + { + var sharedFolder = downloadDir.Split('\\', '/')[0]; + var fieldName = Settings.TvDirectory.IsNotNullOrWhiteSpace() ? nameof(Settings.TvDirectory) : nameof(Settings.TvCategory); + + var folderInfo = _fileStationProxy.GetInfoFileOrDirectory($"/{downloadDir}", Settings); + + if (folderInfo.Additional == null) + { + return new NzbDroneValidationFailure(fieldName, $"Shared folder does not exist") + { + DetailedDescription = $"The DownloadStation does not have a Shared Folder with the name '{sharedFolder}', are you sure you specified it correctly?" + }; + } + + if (!folderInfo.IsDir) + { + return new NzbDroneValidationFailure(fieldName, $"Folder does not exist") + { + DetailedDescription = $"The folder '{downloadDir}' does not exist, it must be created manually inside the Shared Folder '{sharedFolder}'." + }; + } + } + + return null; + } + catch (Exception ex) + { + _logger.Error(ex); + return new NzbDroneValidationFailure(string.Empty, $"Unknown exception: {ex.Message}"); + } + } + + 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(DownloadStationTask task) + { + return task.Status == DownloadStationTaskStatus.Finished; + } + + protected string GetMessage(DownloadStationTask task) + { + if (task.StatusExtra != null) + { + if (task.Status == DownloadStationTaskStatus.Extracting) + { + return $"Extracting: {int.Parse(task.StatusExtra["unzip_progress"])}%"; + } + + if (task.Status == DownloadStationTaskStatus.Error) + { + return task.StatusExtra["error_detail"]; + } + } + + return null; + } + + protected DownloadItemStatus GetStatus(DownloadStationTask task) + { + switch (task.Status) + { + case DownloadStationTaskStatus.Waiting: + return task.Size == 0 || GetRemainingSize(task) > 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(DownloadStationTask task) + { + var downloadedString = task.Additional.Transfer["size_downloaded"]; + long downloadedSize; + + if (downloadedString.IsNullOrWhiteSpace() || !long.TryParse(downloadedString, out downloadedSize)) + { + _logger.Debug("Task {0} has invalid size_downloaded: {1}", task.Title, downloadedString); + downloadedSize = 0; + } + + return task.Size - Math.Max(0, downloadedSize); + } + + protected long GetDownloadSpeed(DownloadStationTask task) + { + var speedString = task.Additional.Transfer["speed_download"]; + long downloadSpeed; + + if (speedString.IsNullOrWhiteSpace() || !long.TryParse(speedString, out downloadSpeed)) + { + _logger.Debug("Task {0} has invalid speed_download: {1}", task.Title, speedString); + downloadSpeed = 0; + } + + return Math.Max(downloadSpeed, 0); + } + + protected TimeSpan? GetRemainingTime(long remainingSize, long downloadSpeed) + { + if (downloadSpeed > 0) + { + return TimeSpan.FromSeconds(remainingSize / downloadSpeed); + } + else + { + return null; + } + } + + protected ValidationFailure TestGetNZB() + { + try + { + GetItems(); + return null; + } + catch (Exception ex) + { + return new NzbDroneValidationFailure(string.Empty, "Failed to get the list of NZBs: " + 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; + } + } +} diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 1c110c948..3cebfefbd 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -406,30 +406,16 @@ - - - - - - - - - - - - - - - - + + - + @@ -442,6 +428,21 @@ + + + + + + + + + + + + + + +