diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/PutioTests/PutioFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/PutioTests/PutioFixture.cs index c356268a2..41bcc9aef 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/PutioTests/PutioFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/PutioTests/PutioFixture.cs @@ -1,94 +1,92 @@ -/* using System; -using System.Linq; using System.Collections.Generic; +using System.Linq; using FluentAssertions; using Moq; using NUnit.Framework; using NzbDrone.Common.Http; -using NzbDrone.Core.MediaFiles.TorrentInfo; using NzbDrone.Core.Download; using NzbDrone.Core.Download.Clients.Putio; - +using NzbDrone.Core.MediaFiles.TorrentInfo; namespace NzbDrone.Core.Test.Download.DownloadClientTests.PutioTests { - [TestFixture] public class PutioFixture : DownloadClientFixtureBase { - protected PutioSettings _settings; - protected PutioTorrent _queued; - protected PutioTorrent _downloading; - protected PutioTorrent _failed; - protected PutioTorrent _completed; - protected PutioTorrent _magnet; - protected Dictionary _PutioAccountSettingsItems; + private PutioSettings _settings; + private PutioTorrent _queued; + private PutioTorrent _downloading; + private PutioTorrent _failed; + private PutioTorrent _completed; + private PutioTorrent _completed_different_parent; + private PutioTorrent _seeding; [SetUp] public void Setup() { _settings = new PutioSettings { + SaveParentId = "1", }; Subject.Definition = new DownloadClientDefinition(); Subject.Definition.Settings = _settings; _queued = new PutioTorrent - { - HashString = "HASH", - IsFinished = false, - Status = PutioTorrentStatus.InQueue, - Name = _title, - TotalSize = 1000, - LeftUntilDone = 1000, - DownloadDir = "somepath" - }; + { + Hash = "HASH", + Status = PutioTorrentStatus.InQueue, + Name = _title, + Size = 1000, + Downloaded = 0, + SaveParentId = 1 + }; _downloading = new PutioTorrent { - HashString = "HASH", - IsFinished = false, + Hash = "HASH", Status = PutioTorrentStatus.Downloading, Name = _title, - TotalSize = 1000, - LeftUntilDone = 100, - DownloadDir = "somepath" + Size = 1000, + Downloaded = 980, + SaveParentId = 1, }; _failed = new PutioTorrent - { - HashString = "HASH", - IsFinished = false, - Status = PutioTorrentStatus.Error, - Name = _title, - TotalSize = 1000, - LeftUntilDone = 100, - ErrorString = "Error", - DownloadDir = "somepath" - }; + { + Hash = "HASH", + Status = PutioTorrentStatus.Error, + ErrorMessage = "Torrent has reached the maximum number of inactive days.", + Name = _title, + Size = 1000, + Downloaded = 980, + SaveParentId = 1, + }; _completed = new PutioTorrent - { - HashString = "HASH", - IsFinished = true, - Status = PutioTorrentStatus.Completed, - Name = _title, - TotalSize = 1000, - LeftUntilDone = 0, - DownloadDir = "somepath" - }; + { + Hash = "HASH", + Status = PutioTorrentStatus.Completed, + Name = _title, + Size = 1000, + Downloaded = 1000, + SaveParentId = 1, + FileId = 1 + }; - _magnet = new PutioTorrent - { - HashString = "HASH", - IsFinished = false, - Status = PutioTorrentStatus.Downloading, - Name = _title, - TotalSize = 0, - LeftUntilDone = 100, - DownloadDir = "somepath" - }; + _completed_different_parent = _completed; + _completed_different_parent.SaveParentId = 2; + + _seeding = new PutioTorrent + { + Hash = "HASH", + Status = PutioTorrentStatus.Seeding, + Name = _title, + Size = 1000, + Downloaded = 1000, + SaveParentId = 1, + FileId = 2 + }; Mocker.GetMock() .Setup(s => s.GetHashFromTorrentFile(It.IsAny())) @@ -96,18 +94,10 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.PutioTests Mocker.GetMock() .Setup(s => s.Get(It.IsAny())) - .Returns(r => new HttpResponse(r, new HttpHeader(), new byte[0])); - - _PutioAccountSettingsItems = new Dictionary(); - - _PutioAccountSettingsItems.Add("download-dir", @"C:/Downloads/Finished/Putio"); - _PutioAccountSettingsItems.Add("incomplete-dir", null); - _PutioAccountSettingsItems.Add("incomplete-dir-enabled", false); + .Returns(r => new HttpResponse(r, new HttpHeader(), Array.Empty())); Mocker.GetMock() - .Setup(v => v.GetAccountSettings(It.IsAny())) - .Returns(_PutioAccountSettingsItems); - + .Setup(v => v.GetAccountSettings(It.IsAny())); } protected void GivenFailedDownload() @@ -122,7 +112,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.PutioTests Mocker.GetMock() .Setup(s => s.Get(It.IsAny())) .Returns(r => new HttpResponse(r, new HttpHeader(), new byte[1000])); - + /* Mocker.GetMock() .Setup(s => s.AddTorrentFromUrl(It.IsAny(), It.IsAny())) .Callback(PrepareClientToReturnQueuedItem); @@ -130,213 +120,72 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.PutioTests Mocker.GetMock() .Setup(s => s.AddTorrentFromData(It.IsAny(), It.IsAny())) .Callback(PrepareClientToReturnQueuedItem); + */ } protected virtual void GivenTorrents(List torrents) { - if (torrents == null) - { - torrents = new List(); - } + torrents ??= new List(); Mocker.GetMock() .Setup(s => s.GetTorrents(It.IsAny())) .Returns(torrents); } - protected void PrepareClientToReturnQueuedItem() + protected virtual void GivenFile(PutioFile file) + { + file ??= new PutioFile(); + + Mocker.GetMock() + .Setup(s => s.GetFile(file.Id, It.IsAny())) + .Returns(file); + } + + [Test] + public void getItems_contains_all_items() { GivenTorrents(new List - { + { + _queued, + _downloading, + _failed, + _completed, + _seeding, + _completed_different_parent + }); + + var items = Subject.GetItems(); + + VerifyQueued(items.ElementAt(0)); + VerifyDownloading(items.ElementAt(1)); + VerifyWarning(items.ElementAt(2)); + VerifyCompleted(items.ElementAt(3)); + VerifyCompleted(items.ElementAt(4)); + VerifyCompleted(items.ElementAt(5)); + + items.Should().HaveCount(6); + } + + [TestCase("WAITING", DownloadItemStatus.Queued)] + [TestCase("PREPARING_DOWNLOAD", DownloadItemStatus.Queued)] + [TestCase("COMPLETED", DownloadItemStatus.Completed)] + [TestCase("COMPLETING", DownloadItemStatus.Downloading)] + [TestCase("DOWNLOADING", DownloadItemStatus.Downloading)] + [TestCase("ERROR", DownloadItemStatus.Failed)] + [TestCase("IN_QUEUE", DownloadItemStatus.Queued)] + [TestCase("SEEDING", DownloadItemStatus.Completed)] + public void test_getItems_maps_download_status(string given, DownloadItemStatus expectedItemStatus) + { + _queued.Status = given; + + GivenTorrents(new List + { _queued - }); - } - - protected void PrepareClientToReturnDownloadingItem() - { - GivenTorrents(new List - { - _downloading - }); - } - - protected void PrepareClientToReturnFailedItem() - { - GivenTorrents(new List - { - _failed - }); - } - - protected void PrepareClientToReturnCompletedItem() - { - GivenTorrents(new List - { - _completed - }); - } - - protected void PrepareClientToReturnMagnetItem() - { - GivenTorrents(new List - { - _magnet - }); - } - - [Test] - public void queued_item_should_have_required_properties() - { - PrepareClientToReturnQueuedItem(); - var item = Subject.GetItems().Single(); - VerifyQueued(item); - } - - [Test] - public void downloading_item_should_have_required_properties() - { - PrepareClientToReturnDownloadingItem(); - var item = Subject.GetItems().Single(); - VerifyDownloading(item); - } - - [Test] - public void failed_item_should_have_required_properties() - { - PrepareClientToReturnFailedItem(); - var item = Subject.GetItems().Single(); - VerifyWarning(item); - } - - [Test] - public void completed_download_should_have_required_properties() - { - PrepareClientToReturnCompletedItem(); - var item = Subject.GetItems().Single(); - VerifyCompleted(item); - } - - [Test] - public void magnet_download_should_not_return_the_item() - { - PrepareClientToReturnMagnetItem(); - Subject.GetItems().Count().Should().Be(0); - } - - [Test] - public void Download_should_return_unique_id() - { - GivenSuccessfulDownload(); - - var remoteEpisode = CreateRemoteEpisode(); - - var id = Subject.Download(remoteEpisode); - - id.Should().NotBeNullOrEmpty(); - } - - [TestCase("magnet:?xt=urn:btih:ZPBPA2P6ROZPKRHK44D5OW6NHXU5Z6KR&tr=udp", "CBC2F069FE8BB2F544EAE707D75BCD3DE9DCF951")] - public void Download_should_get_hash_from_magnet_url(string magnetUrl, string expectedHash) - { - GivenSuccessfulDownload(); - - var remoteEpisode = CreateRemoteEpisode(); - remoteEpisode.Release.DownloadUrl = magnetUrl; - - var id = Subject.Download(remoteEpisode); - - id.Should().Be(expectedHash); - } - - [TestCase(PutioTorrentStatus.Stopped, DownloadItemStatus.Downloading)] - [TestCase(PutioTorrentStatus.CheckWait, DownloadItemStatus.Downloading)] - [TestCase(PutioTorrentStatus.Check, DownloadItemStatus.Downloading)] - [TestCase(PutioTorrentStatus.Queued, DownloadItemStatus.Queued)] - [TestCase(PutioTorrentStatus.Downloading, DownloadItemStatus.Downloading)] - [TestCase(PutioTorrentStatus.SeedingWait, DownloadItemStatus.Completed)] - [TestCase(PutioTorrentStatus.Seeding, DownloadItemStatus.Completed)] - public void GetItems_should_return_queued_item_as_downloadItemStatus(PutioTorrentStatus apiStatus, DownloadItemStatus expectedItemStatus) - { - _queued.Status = apiStatus; - - PrepareClientToReturnQueuedItem(); + }); var item = Subject.GetItems().Single(); item.Status.Should().Be(expectedItemStatus); } - - [TestCase(PutioTorrentStatus.Queued, DownloadItemStatus.Queued)] - [TestCase(PutioTorrentStatus.Downloading, DownloadItemStatus.Downloading)] - [TestCase(PutioTorrentStatus.Seeding, DownloadItemStatus.Completed)] - public void GetItems_should_return_downloading_item_as_downloadItemStatus(PutioTorrentStatus apiStatus, DownloadItemStatus expectedItemStatus) - { - _downloading.Status = apiStatus; - - PrepareClientToReturnDownloadingItem(); - - var item = Subject.GetItems().Single(); - - item.Status.Should().Be(expectedItemStatus); - } - - [TestCase(PutioTorrentStatus.Stopped, DownloadItemStatus.Completed, false)] - [TestCase(PutioTorrentStatus.CheckWait, DownloadItemStatus.Downloading, true)] - [TestCase(PutioTorrentStatus.Check, DownloadItemStatus.Downloading, true)] - [TestCase(PutioTorrentStatus.Queued, DownloadItemStatus.Completed, true)] - [TestCase(PutioTorrentStatus.SeedingWait, DownloadItemStatus.Completed, true)] - [TestCase(PutioTorrentStatus.Seeding, DownloadItemStatus.Completed, true)] - public void GetItems_should_return_completed_item_as_downloadItemStatus(PutioTorrentStatus apiStatus, DownloadItemStatus expectedItemStatus, bool expectedReadOnly) - { - _completed.Status = apiStatus; - - PrepareClientToReturnCompletedItem(); - - var item = Subject.GetItems().Single(); - - item.Status.Should().Be(expectedItemStatus); - item.IsReadOnly.Should().Be(expectedReadOnly); - } - - [Test] - public void should_return_status_with_outputdirs() - { - var result = Subject.GetStatus(); - - result.IsLocalhost.Should().BeTrue(); - result.OutputRootFolders.Should().NotBeNull(); - result.OutputRootFolders.First().Should().Be(@"C:\Downloads\Finished\Putio"); - } - - [Test] - public void should_fix_forward_slashes() - { - WindowsOnly(); - - _downloading.DownloadDir = @"C:/Downloads/Finished/Putio"; - - GivenTorrents(new List - { - _downloading - }); - - var items = Subject.GetItems().ToList(); - - items.Should().HaveCount(1); - items.First().OutputPath.Should().Be(@"C:\Downloads\Finished\Putio\" + _title); - } - - [TestCase(-1)] // Infinite/Unknown - [TestCase(-2)] // Magnet Downloading - public void should_ignore_negative_eta(int eta) - { - _completed.Eta = eta; - - PrepareClientToReturnCompletedItem(); - var item = Subject.GetItems().Single(); - item.RemainingTime.Should().NotHaveValue(); - } } } - -*/ diff --git a/src/NzbDrone.Core/Download/Clients/Putio/Putio.cs b/src/NzbDrone.Core/Download/Clients/Putio/Putio.cs index 49fab5952..81540c615 100644 --- a/src/NzbDrone.Core/Download/Clients/Putio/Putio.cs +++ b/src/NzbDrone.Core/Download/Clients/Putio/Putio.cs @@ -74,20 +74,21 @@ namespace NzbDrone.Core.Download.Clients.Putio continue; } - var item = new DownloadClientItem(); - item.DownloadId = "putio-" + torrent.Id; - item.Category = Settings.SaveParentId; - item.Title = torrent.Name; - - // item.DownloadClient = Definition.Name; - - item.TotalSize = torrent.Size; - item.RemainingSize = torrent.Size - torrent.Downloaded; + var item = new DownloadClientItem + { + DownloadId = torrent.Id.ToString(), + Category = Settings.SaveParentId, + Title = torrent.Name, + TotalSize = torrent.Size, + RemainingSize = torrent.Size - torrent.Downloaded, + DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this) + }; try { if (torrent.FileId != 0) { + /* var file = _proxy.GetFile(torrent.FileId, Settings); var torrentPath = "/completed/" + file.Name; @@ -103,6 +104,7 @@ namespace NzbDrone.Core.Download.Clients.Putio } item.OutputPath = outputPath; // + torrent.Name; + */ } } catch (DownloadClientException ex) @@ -115,25 +117,13 @@ namespace NzbDrone.Core.Download.Clients.Putio item.RemainingTime = TimeSpan.FromSeconds(torrent.EstimatedTime); } + item.Status = GetStatus(torrent); + if (!torrent.ErrorMessage.IsNullOrWhiteSpace()) { item.Status = DownloadItemStatus.Warning; item.Message = torrent.ErrorMessage; } - else if (torrent.Status == PutioTorrentStatus.Completed) - { - item.Status = DownloadItemStatus.Completed; - } - else if (torrent.Status == PutioTorrentStatus.InQueue) - { - item.Status = DownloadItemStatus.Queued; - } - else - { - item.Status = DownloadItemStatus.Downloading; - } - - // item.IsReadOnly = torrent.Status != PutioTorrentStatus.Error; items.Add(item); } @@ -141,6 +131,29 @@ namespace NzbDrone.Core.Download.Clients.Putio return items; } + private DownloadItemStatus GetStatus(PutioTorrent torrent) + { + if (torrent.Status == PutioTorrentStatus.Completed || + torrent.Status == PutioTorrentStatus.Seeding) + { + return DownloadItemStatus.Completed; + } + + if (torrent.Status == PutioTorrentStatus.InQueue || + torrent.Status == PutioTorrentStatus.Waiting || + torrent.Status == PutioTorrentStatus.PrepareDownload) + { + return DownloadItemStatus.Queued; + } + + if (torrent.Status == PutioTorrentStatus.Error) + { + return DownloadItemStatus.Failed; + } + + return DownloadItemStatus.Downloading; + } + public override DownloadClientInfo GetStatus() { var destDir = string.Format("{0}", Settings.SaveParentId); @@ -169,6 +182,14 @@ namespace NzbDrone.Core.Download.Clients.Putio { _proxy.GetAccountSettings(Settings); } + catch (DownloadClientAuthenticationException ex) + { + _logger.Error(ex, ex.Message); + return new NzbDroneValidationFailure("OAuthToken", "Authentication failed") + { + DetailedDescription = "See the wiki for more details on how to obtain an OAuthToken" + }; + } catch (Exception ex) { _logger.Error(ex, ex.Message); diff --git a/src/NzbDrone.Core/Download/Clients/Putio/PutioFile.cs b/src/NzbDrone.Core/Download/Clients/Putio/PutioFile.cs index f6a49683c..f8de1733f 100644 --- a/src/NzbDrone.Core/Download/Clients/Putio/PutioFile.cs +++ b/src/NzbDrone.Core/Download/Clients/Putio/PutioFile.cs @@ -1,8 +1,8 @@ -namespace NzbDrone.Core.Download.Clients.Putio +namespace NzbDrone.Core.Download.Clients.Putio { public class PutioFile { - public int Id { get; set; } + public long Id { get; set; } public string Name { get; set; } } } diff --git a/src/NzbDrone.Core/Download/Clients/Putio/PutioProxy.cs b/src/NzbDrone.Core/Download/Clients/Putio/PutioProxy.cs index ca3da1279..26f2f922c 100644 --- a/src/NzbDrone.Core/Download/Clients/Putio/PutioProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Putio/PutioProxy.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; using System.Net; +using System.Net.Http; using NLog; using NzbDrone.Common.Http; -using NzbDrone.Common.Serializer; namespace NzbDrone.Core.Download.Clients.Putio { @@ -66,48 +66,44 @@ namespace NzbDrone.Core.Download.Clients.Putio public void GetAccountSettings(PutioSettings settings) { // ProcessRequest(Method.GET, "account/settings", null, settings); + Execute(BuildRequest(HttpMethod.Get, "account/settings", settings)); } - private HttpRequestBuilder BuildRequest(PutioSettings settings) + private HttpRequestBuilder BuildRequest(HttpMethod method, string endpoint, PutioSettings settings) { var requestBuilder = new HttpRequestBuilder("https://api.put.io/v2") { LogResponseContent = true }; + requestBuilder.Method = method; + requestBuilder.Resource(endpoint); requestBuilder.SetHeader("Authorization", "Bearer " + settings.OAuthToken); return requestBuilder; } - private string ProcessRequest(HttpRequestBuilder requestBuilder) + private HttpResponse Execute(HttpRequestBuilder requestBuilder) + where TResult : new() { var request = requestBuilder.Build(); request.LogResponseContent = true; - request.SuppressHttpErrorStatusCodes = new[] { HttpStatusCode.Forbidden }; - HttpResponse response; try { - response = _httpClient.Execute(request); - - if (response.StatusCode == HttpStatusCode.Forbidden) + return _httpClient.Get(request); + } + catch (HttpException ex) + { + if (ex.Response.StatusCode == HttpStatusCode.Unauthorized) { - throw new DownloadClientException("Invalid credentials. Check your OAuthToken"); + throw new DownloadClientAuthenticationException("Invalid credentials. Check your OAuthToken"); } + + throw new DownloadClientException("Failed to connect to put.io API", ex); } catch (Exception ex) { - throw new DownloadClientException("Failed to connect to put.io.", ex); + throw new DownloadClientException("Failed to connect to put.io API", ex); } - - return response.Content; - } - - private TResult ProcessRequest(HttpRequestBuilder requestBuilder) - where TResult : new() - { - var responseContent = ProcessRequest(requestBuilder); - - return Json.Deserialize(responseContent); } } } diff --git a/src/NzbDrone.Core/Download/Clients/Putio/PutioSettings.cs b/src/NzbDrone.Core/Download/Clients/Putio/PutioSettings.cs index fcff64d9a..a019e6f6d 100644 --- a/src/NzbDrone.Core/Download/Clients/Putio/PutioSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Putio/PutioSettings.cs @@ -1,4 +1,4 @@ -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; using FluentValidation; using NzbDrone.Core.Annotations; using NzbDrone.Core.ThingiProvider; @@ -10,6 +10,7 @@ namespace NzbDrone.Core.Download.Clients.Putio { public PutioSettingsValidator() { + RuleFor(c => c.OAuthToken).NotEmpty().WithMessage("Please provide an OAuth token"); RuleFor(c => c.SaveParentId).Matches(@"^\.?[0-9]*$", RegexOptions.IgnoreCase).WithMessage("Allowed characters 0-9"); } } @@ -25,12 +26,15 @@ namespace NzbDrone.Core.Download.Clients.Putio public string Url { get; } - [FieldDefinition(0, Label = "OAuth Token", Type = FieldType.Textbox)] + [FieldDefinition(0, Label = "OAuth Token", Type = FieldType.Password)] public string OAuthToken { get; set; } - [FieldDefinition(1, Label = "Save Parent ID", Type = FieldType.Textbox, HelpText = "Adding a save parent ID specific to Sonarr avoids conflicts with unrelated downloads, but it's optional. Creates a .[SaveParentId] subdirectory in the output directory.")] + [FieldDefinition(1, Label = "Save Parent ID", Type = FieldType.Textbox, HelpText = "If you provide a folder id here the torrents will be saved in that directory")] public string SaveParentId { get; set; } + [FieldDefinition(2, Label = "Disable Download", Type = FieldType.Checkbox, HelpText = "If enabled, Sonarr will not download completed files from Put.io. Useful if you manually sync with rclone or similar")] + public bool DisableDownload { get; set; } + public NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/Download/Clients/Putio/PutioTorrent.cs b/src/NzbDrone.Core/Download/Clients/Putio/PutioTorrent.cs index 54c86339b..f7f3ddfb6 100644 --- a/src/NzbDrone.Core/Download/Clients/Putio/PutioTorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/Putio/PutioTorrent.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; namespace NzbDrone.Core.Download.Clients.Putio { @@ -28,5 +28,10 @@ namespace NzbDrone.Core.Download.Clients.Putio public long Size { get; set; } public string Status { get; set; } + + [JsonProperty(PropertyName = "save_parent_id")] + public long SaveParentId { get; set; } + + public string Hash { get; set; } } } diff --git a/src/NzbDrone.Core/Download/Clients/Putio/PutioTorrentStatus.cs b/src/NzbDrone.Core/Download/Clients/Putio/PutioTorrentStatus.cs index f5c4007eb..9abd8d480 100644 --- a/src/NzbDrone.Core/Download/Clients/Putio/PutioTorrentStatus.cs +++ b/src/NzbDrone.Core/Download/Clients/Putio/PutioTorrentStatus.cs @@ -1,10 +1,14 @@ namespace NzbDrone.Core.Download.Clients.Putio { - public sealed class PutioTorrentStatus + public static class PutioTorrentStatus { - public static readonly string Completed = "COMPLETED"; - public static readonly string Downloading = "DOWNLOADING"; - public static readonly string Error = "ERROR"; - public static readonly string InQueue = "IN_QUEUE"; + public static readonly string Waiting = "WAITING"; + public static readonly string PrepareDownload = "PREPARING_DOWNLOAD"; + public static readonly string Completed = "COMPLETED"; + public static readonly string Completing = "COMPLETING"; + public static readonly string Downloading = "DOWNLOADING"; + public static readonly string Error = "ERROR"; + public static readonly string InQueue = "IN_QUEUE"; + public static readonly string Seeding = "SEEDING"; } }