From 2eb24d494b2681891533b4ae5c50b533d396b3c0 Mon Sep 17 00:00:00 2001 From: Michael Feinbier Date: Fri, 15 Sep 2023 08:20:45 +0200 Subject: [PATCH 01/10] Applied original .patch From original PR https://github.com/Sonarr/Sonarr/pull/1357 --- .../PutioTests/PutioFixture.cs | 338 ++++++++++++++++++ .../Download/Clients/Putio/Putio.cs | 191 ++++++++++ .../Download/Clients/Putio/PutioException.cs | 13 + .../Download/Clients/Putio/PutioFile.cs | 8 + .../Clients/Putio/PutioFileResponse.cs | 8 + .../Clients/Putio/PutioGenericResponse.cs | 12 + .../Download/Clients/Putio/PutioPriority.cs | 8 + .../Download/Clients/Putio/PutioProxy.cs | 107 ++++++ .../Download/Clients/Putio/PutioSettings.cs | 39 ++ .../Download/Clients/Putio/PutioTorrent.cs | 32 ++ .../Clients/Putio/PutioTorrentStatus.cs | 12 + .../Clients/Putio/PutioTransfersResponse.cs | 8 + 12 files changed, 776 insertions(+) create mode 100644 src/NzbDrone.Core.Test/Download/DownloadClientTests/PutioTests/PutioFixture.cs create mode 100644 src/NzbDrone.Core/Download/Clients/Putio/Putio.cs create mode 100644 src/NzbDrone.Core/Download/Clients/Putio/PutioException.cs create mode 100644 src/NzbDrone.Core/Download/Clients/Putio/PutioFile.cs create mode 100644 src/NzbDrone.Core/Download/Clients/Putio/PutioFileResponse.cs create mode 100644 src/NzbDrone.Core/Download/Clients/Putio/PutioGenericResponse.cs create mode 100644 src/NzbDrone.Core/Download/Clients/Putio/PutioPriority.cs create mode 100644 src/NzbDrone.Core/Download/Clients/Putio/PutioProxy.cs create mode 100644 src/NzbDrone.Core/Download/Clients/Putio/PutioSettings.cs create mode 100644 src/NzbDrone.Core/Download/Clients/Putio/PutioTorrent.cs create mode 100644 src/NzbDrone.Core/Download/Clients/Putio/PutioTorrentStatus.cs create mode 100644 src/NzbDrone.Core/Download/Clients/Putio/PutioTransfersResponse.cs diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/PutioTests/PutioFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/PutioTests/PutioFixture.cs new file mode 100644 index 000000000..9292eb45a --- /dev/null +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/PutioTests/PutioFixture.cs @@ -0,0 +1,338 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using NzbDrone.Common.Http; +using NzbDrone.Core.MediaFiles.TorrentInfo; +using NzbDrone.Core.Download; +using NzbDrone.Core.Download.Clients.Putio; + +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; + + [SetUp] + public void Setup() + { + _settings = new PutioSettings + { + }; + + 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" + }; + + _downloading = new PutioTorrent + { + HashString = "HASH", + IsFinished = false, + Status = PutioTorrentStatus.Downloading, + Name = _title, + TotalSize = 1000, + LeftUntilDone = 100, + DownloadDir = "somepath" + }; + + _failed = new PutioTorrent + { + HashString = "HASH", + IsFinished = false, + Status = PutioTorrentStatus.Error, + Name = _title, + TotalSize = 1000, + LeftUntilDone = 100, + ErrorString = "Error", + DownloadDir = "somepath" + }; + + _completed = new PutioTorrent + { + HashString = "HASH", + IsFinished = true, + Status = PutioTorrentStatus.Completed, + Name = _title, + TotalSize = 1000, + LeftUntilDone = 0, + DownloadDir = "somepath" + }; + + _magnet = new PutioTorrent + { + HashString = "HASH", + IsFinished = false, + Status = PutioTorrentStatus.Downloading, + Name = _title, + TotalSize = 0, + LeftUntilDone = 100, + DownloadDir = "somepath" + }; + + Mocker.GetMock() + .Setup(s => s.GetHashFromTorrentFile(It.IsAny())) + .Returns("CBC2F069FE8BB2F544EAE707D75BCD3DE9DCF951"); + + 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); + + Mocker.GetMock() + .Setup(v => v.GetAccountSettings(It.IsAny())) + .Returns(_PutioAccountSettingsItems); + + } + + protected void GivenFailedDownload() + { + Mocker.GetMock() + .Setup(s => s.AddTorrentFromUrl(It.IsAny(), It.IsAny())) + .Throws(); + } + + 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.AddTorrentFromUrl(It.IsAny(), It.IsAny())) + .Callback(PrepareClientToReturnQueuedItem); + + Mocker.GetMock() + .Setup(s => s.AddTorrentFromData(It.IsAny(), It.IsAny())) + .Callback(PrepareClientToReturnQueuedItem); + } + + protected virtual void GivenTorrents(List torrents) + { + if (torrents == null) + { + torrents = new List(); + } + + Mocker.GetMock() + .Setup(s => s.GetTorrents(It.IsAny())) + .Returns(torrents); + } + + protected void PrepareClientToReturnQueuedItem() + { + 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 new file mode 100644 index 000000000..4d086e732 --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/Putio/Putio.cs @@ -0,0 +1,191 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using NzbDrone.Common.Disk; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; +using NzbDrone.Core.Configuration; +using NLog; +using FluentValidation.Results; +using NzbDrone.Core.MediaFiles.TorrentInfo; +using NzbDrone.Core.Validation; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.RemotePathMappings; + +namespace NzbDrone.Core.Download.Clients.Putio +{ + public class Putio : TorrentClientBase + { + private readonly IPutioProxy _proxy; + + public Putio(IPutioProxy proxy, + ITorrentFileInfoReader torrentFileInfoReader, + IHttpClient httpClient, + IConfigService configService, + IDiskProvider diskProvider, + IRemotePathMappingService remotePathMappingService, + Logger logger) + : base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, logger) + { + _proxy = proxy; + } + + public override string Name + { + get + { + return "put.io"; + } + } + protected override string AddFromMagnetLink(RemoteEpisode remoteEpisode, string hash, string magnetLink) + { + _proxy.AddTorrentFromUrl(magnetLink, Settings); + return hash; + } + + protected override string AddFromTorrentFile(RemoteEpisode remoteEpisode, string hash, string filename, byte[] fileContent) + { + _proxy.AddTorrentFromData(fileContent, Settings); + return hash; + } + + public override IEnumerable GetItems() + { + List torrents; + + try + { + torrents = _proxy.GetTorrents(Settings); + } + catch (DownloadClientException ex) + { + _logger.Error(ex, ex.Message); + return Enumerable.Empty(); + } + + var items = new List(); + + foreach (var torrent in torrents) + { + // If totalsize == 0 the torrent is a magnet downloading metadata + if (torrent.Size == 0) + 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; + + try + { + if (torrent.FileId != 0) + { + var file = _proxy.GetFile(torrent.FileId, Settings); + var torrentPath = "/completed/" + file.Name; + + var outputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Url, new OsPath(torrentPath)); + + if (Settings.SaveParentId.IsNotNullOrWhiteSpace()) + { + var directories = outputPath.FullPath.Split('\\', '/'); + if (!directories.Contains(string.Format("{0}", Settings.SaveParentId))) continue; + } + + item.OutputPath = outputPath; // + torrent.Name; + } + } + catch (DownloadClientException ex) + { + _logger.Error(ex, ex.Message); + } + + if (torrent.EstimatedTime >= 0) + { + item.RemainingTime = TimeSpan.FromSeconds(torrent.EstimatedTime); + } + + if (!torrent.ErrorMessage.IsNullOrWhiteSpace()) + { + item.Status = DownloadItemStatus.Warning; + item.Message = torrent.ErrorMessage; + } + 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); + } + + return items; + } + + public override void RemoveItem(string downloadId, bool deleteData) + { + _proxy.RemoveTorrent(downloadId.ToLower(), Settings); + } + + public override DownloadClientStatus GetStatus() + { + var destDir = string.Format("{0}", Settings.SaveParentId); + + return new DownloadClientStatus + { + IsLocalhost = false, + OutputRootFolders = new List { _remotePathMappingService.RemapRemoteToLocal(Settings.Url, new OsPath(destDir)) } + }; + } + + protected override void Test(List failures) + { + failures.AddIfNotNull(TestConnection()); + if (failures.Any()) return; + failures.AddIfNotNull(TestGetTorrents()); + } + + private ValidationFailure TestConnection() + { + try + { + _proxy.GetAccountSettings(Settings); + } + catch (Exception ex) + { + _logger.Error(ex, ex.Message); + return new NzbDroneValidationFailure(string.Empty, "Unknown exception: " + ex.Message); + } + + return null; + } + + private ValidationFailure TestGetTorrents() + { + try + { + _proxy.GetTorrents(Settings); + } + catch (Exception ex) + { + _logger.Error(ex, ex.Message); + return new NzbDroneValidationFailure(string.Empty, "Failed to get the list of torrents: " + ex.Message); + } + + return null; + } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/Putio/PutioException.cs b/src/NzbDrone.Core/Download/Clients/Putio/PutioException.cs new file mode 100644 index 000000000..ecb3002f6 --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/Putio/PutioException.cs @@ -0,0 +1,13 @@ +using System; + +namespace NzbDrone.Core.Download.Clients.Putio +{ + public class PutioException : DownloadClientException + { + public PutioException(string message) + : base(message) + { + + } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/Putio/PutioFile.cs b/src/NzbDrone.Core/Download/Clients/Putio/PutioFile.cs new file mode 100644 index 000000000..f6a49683c --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/Putio/PutioFile.cs @@ -0,0 +1,8 @@ +namespace NzbDrone.Core.Download.Clients.Putio +{ + public class PutioFile + { + public int Id { get; set; } + public string Name { get; set; } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/Putio/PutioFileResponse.cs b/src/NzbDrone.Core/Download/Clients/Putio/PutioFileResponse.cs new file mode 100644 index 000000000..a03ffc152 --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/Putio/PutioFileResponse.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; +namespace NzbDrone.Core.Download.Clients.Putio +{ + public class PutioFileResponse : PutioGenericResponse + { + public PutioFile File { get; set; } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/Putio/PutioGenericResponse.cs b/src/NzbDrone.Core/Download/Clients/Putio/PutioGenericResponse.cs new file mode 100644 index 000000000..4e0624fb9 --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/Putio/PutioGenericResponse.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace NzbDrone.Core.Download.Clients.Putio +{ + public class PutioGenericResponse + { + [JsonProperty(PropertyName = "error_message")] + public string ErrorMessage { get; set; } + + public string Status { get; set; } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/Putio/PutioPriority.cs b/src/NzbDrone.Core/Download/Clients/Putio/PutioPriority.cs new file mode 100644 index 000000000..1a93436c2 --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/Putio/PutioPriority.cs @@ -0,0 +1,8 @@ +namespace NzbDrone.Core.Download.Clients.Putio +{ + public enum PutioPriority + { + Last = 0, + First = 1 + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Download/Clients/Putio/PutioProxy.cs b/src/NzbDrone.Core/Download/Clients/Putio/PutioProxy.cs new file mode 100644 index 000000000..91afd6f52 --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/Putio/PutioProxy.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using NzbDrone.Core.Rest; +using NLog; +using RestSharp; +using RestSharp.Deserializers; +using Newtonsoft.Json; + +namespace NzbDrone.Core.Download.Clients.Putio +{ + public interface IPutioProxy + { + List GetTorrents(PutioSettings settings); + PutioFile GetFile(long fileId, PutioSettings settings); + void AddTorrentFromUrl(string torrentUrl, PutioSettings settings); + void AddTorrentFromData(byte[] torrentData, PutioSettings settings); + void RemoveTorrent(string hash, PutioSettings settings); + void GetAccountSettings(PutioSettings settings); + } + + public class PutioProxy: IPutioProxy + { + private readonly Logger _logger; + + public PutioProxy(Logger logger) + { + _logger = logger; + } + + public List GetTorrents(PutioSettings settings) + { + var result = ProcessRequest(Method.GET, "transfers/list", null, settings); + return result.Transfers; + } + + public PutioFile GetFile(long fileId, PutioSettings settings) + { + var result = ProcessRequest(Method.GET, "files/" + fileId, null, settings); + return result.File; + } + + public void AddTorrentFromUrl(string torrentUrl, PutioSettings settings) + { + var arguments = new Dictionary(); + arguments.Add("url", torrentUrl); + ProcessRequest(Method.POST, "transfers/add", arguments, settings); + } + + public void AddTorrentFromData(byte[] torrentData, PutioSettings settings) + { + var arguments = new Dictionary(); + arguments.Add("metainfo", Convert.ToBase64String(torrentData)); + ProcessRequest(Method.POST, "transfers/add", arguments, settings); + } + + public void RemoveTorrent(string hashString, PutioSettings settings) + { + var arguments = new Dictionary(); + arguments.Add("transfer_ids", new string[] { hashString }); + ProcessRequest(Method.POST, "torrents/cancel", arguments, settings); + } + + public void GetAccountSettings(PutioSettings settings) + { + ProcessRequest(Method.GET, "account/settings", null, settings); + } + + public TResponseType ProcessRequest(Method method, string resource, Dictionary arguments, PutioSettings settings) where TResponseType : PutioGenericResponse + { + var client = BuildClient(settings); + + var request = new RestRequest(resource, method); + request.RequestFormat = DataFormat.Json; + request.AddQueryParameter("oauth_token", settings.OAuthToken); + + if (arguments != null) + { + foreach (KeyValuePair e in arguments) + { + request.AddParameter(e.Key, e.Value); + } + } + + _logger.Debug("Method: {0} Url: {1}", method, client.BuildUri(request)); + + var restResponse = client.Execute(request); + + var json = new JsonDeserializer(); + + TResponseType output = json.Deserialize(restResponse); + + if (output.Status != "OK") + { + throw new PutioException(output.ErrorMessage); + } + + return output; + } + + private IRestClient BuildClient(PutioSettings settings) + { + var restClient = RestClientFactory.BuildClient(settings.Url); + restClient.FollowRedirects = false; + return restClient; + } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/Putio/PutioSettings.cs b/src/NzbDrone.Core/Download/Clients/Putio/PutioSettings.cs new file mode 100644 index 000000000..fcff64d9a --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/Putio/PutioSettings.cs @@ -0,0 +1,39 @@ +using System.Text.RegularExpressions; +using FluentValidation; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.Download.Clients.Putio +{ + public class PutioSettingsValidator : AbstractValidator + { + public PutioSettingsValidator() + { + RuleFor(c => c.SaveParentId).Matches(@"^\.?[0-9]*$", RegexOptions.IgnoreCase).WithMessage("Allowed characters 0-9"); + } + } + + public class PutioSettings : IProviderConfig + { + private static readonly PutioSettingsValidator Validator = new PutioSettingsValidator(); + + public PutioSettings() + { + Url = "https://api.put.io/v2"; + } + + public string Url { get; } + + [FieldDefinition(0, Label = "OAuth Token", Type = FieldType.Textbox)] + 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.")] + public string SaveParentId { 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 new file mode 100644 index 000000000..54c86339b --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/Putio/PutioTorrent.cs @@ -0,0 +1,32 @@ +using Newtonsoft.Json; + +namespace NzbDrone.Core.Download.Clients.Putio +{ + public class PutioTorrent + { + public long Downloaded { get; set; } + + [JsonProperty(PropertyName = "error_message")] + public string ErrorMessage { get; set; } + + [JsonProperty(PropertyName = "estimated_time")] + public long EstimatedTime { get; set; } + + [JsonProperty(PropertyName = "file_id")] + public long FileId { get; set; } + + public int Id { get; set; } + + public string Name { get; set; } + + [JsonProperty(PropertyName = "percent_done")] + public int PercentDone { get; set; } + + [JsonProperty(PropertyName = "seconds_seeding")] + public long SecondsSeeding { get; set; } + + public long Size { get; set; } + + public string Status { get; set; } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/Putio/PutioTorrentStatus.cs b/src/NzbDrone.Core/Download/Clients/Putio/PutioTorrentStatus.cs new file mode 100644 index 000000000..10bd697b0 --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/Putio/PutioTorrentStatus.cs @@ -0,0 +1,12 @@ +using System; + +namespace NzbDrone.Core.Download.Clients.Putio +{ + public sealed 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"; + } +} diff --git a/src/NzbDrone.Core/Download/Clients/Putio/PutioTransfersResponse.cs b/src/NzbDrone.Core/Download/Clients/Putio/PutioTransfersResponse.cs new file mode 100644 index 000000000..d54d6a5ff --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/Putio/PutioTransfersResponse.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; +namespace NzbDrone.Core.Download.Clients.Putio +{ + public class PutioTransfersResponse : PutioGenericResponse + { + public List Transfers { get; set; } + } +} From cd162f83c294f1c905b3371fb204882533b491e3 Mon Sep 17 00:00:00 2001 From: Michael Feinbier Date: Fri, 15 Sep 2023 16:45:50 +0200 Subject: [PATCH 02/10] Make syntax compatible --- .../PutioTests/PutioFixture.cs | 4 + .../Download/Clients/Putio/Putio.cs | 40 +++++--- .../Download/Clients/Putio/PutioException.cs | 3 - .../Clients/Putio/PutioFileResponse.cs | 1 - .../Download/Clients/Putio/PutioProxy.cs | 97 ++++++++++--------- .../Clients/Putio/PutioTorrentStatus.cs | 10 +- 6 files changed, 85 insertions(+), 70 deletions(-) diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/PutioTests/PutioFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/PutioTests/PutioFixture.cs index 9292eb45a..c356268a2 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/PutioTests/PutioFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/PutioTests/PutioFixture.cs @@ -1,3 +1,4 @@ +/* using System; using System.Linq; using System.Collections.Generic; @@ -9,6 +10,7 @@ using NzbDrone.Core.MediaFiles.TorrentInfo; using NzbDrone.Core.Download; using NzbDrone.Core.Download.Clients.Putio; + namespace NzbDrone.Core.Test.Download.DownloadClientTests.PutioTests { [TestFixture] @@ -336,3 +338,5 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.PutioTests } } } + +*/ diff --git a/src/NzbDrone.Core/Download/Clients/Putio/Putio.cs b/src/NzbDrone.Core/Download/Clients/Putio/Putio.cs index 4d086e732..49fab5952 100644 --- a/src/NzbDrone.Core/Download/Clients/Putio/Putio.cs +++ b/src/NzbDrone.Core/Download/Clients/Putio/Putio.cs @@ -1,16 +1,16 @@ -using System; -using System.Linq; +using System; using System.Collections.Generic; +using System.Linq; +using FluentValidation.Results; +using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; -using NLog; -using FluentValidation.Results; using NzbDrone.Core.MediaFiles.TorrentInfo; -using NzbDrone.Core.Validation; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.RemotePathMappings; +using NzbDrone.Core.Validation; namespace NzbDrone.Core.Download.Clients.Putio { @@ -37,6 +37,7 @@ namespace NzbDrone.Core.Download.Clients.Putio return "put.io"; } } + protected override string AddFromMagnetLink(RemoteEpisode remoteEpisode, string hash, string magnetLink) { _proxy.AddTorrentFromUrl(magnetLink, Settings); @@ -69,14 +70,16 @@ namespace NzbDrone.Core.Download.Clients.Putio { // If totalsize == 0 the torrent is a magnet downloading metadata if (torrent.Size == 0) + { continue; + } var item = new DownloadClientItem(); item.DownloadId = "putio-" + torrent.Id; item.Category = Settings.SaveParentId; item.Title = torrent.Name; - item.DownloadClient = Definition.Name; + // item.DownloadClient = Definition.Name; item.TotalSize = torrent.Size; item.RemainingSize = torrent.Size - torrent.Downloaded; @@ -93,7 +96,10 @@ namespace NzbDrone.Core.Download.Clients.Putio if (Settings.SaveParentId.IsNotNullOrWhiteSpace()) { var directories = outputPath.FullPath.Split('\\', '/'); - if (!directories.Contains(string.Format("{0}", Settings.SaveParentId))) continue; + if (!directories.Contains(string.Format("{0}", Settings.SaveParentId))) + { + continue; + } } item.OutputPath = outputPath; // + torrent.Name; @@ -135,16 +141,11 @@ namespace NzbDrone.Core.Download.Clients.Putio return items; } - public override void RemoveItem(string downloadId, bool deleteData) - { - _proxy.RemoveTorrent(downloadId.ToLower(), Settings); - } - - public override DownloadClientStatus GetStatus() + public override DownloadClientInfo GetStatus() { var destDir = string.Format("{0}", Settings.SaveParentId); - return new DownloadClientStatus + return new DownloadClientInfo { IsLocalhost = false, OutputRootFolders = new List { _remotePathMappingService.RemapRemoteToLocal(Settings.Url, new OsPath(destDir)) } @@ -154,7 +155,11 @@ namespace NzbDrone.Core.Download.Clients.Putio protected override void Test(List failures) { failures.AddIfNotNull(TestConnection()); - if (failures.Any()) return; + if (failures.Any()) + { + return; + } + failures.AddIfNotNull(TestGetTorrents()); } @@ -187,5 +192,10 @@ namespace NzbDrone.Core.Download.Clients.Putio return null; } + + public override void RemoveItem(DownloadClientItem item, bool deleteData) + { + throw new NotImplementedException(); + } } } diff --git a/src/NzbDrone.Core/Download/Clients/Putio/PutioException.cs b/src/NzbDrone.Core/Download/Clients/Putio/PutioException.cs index ecb3002f6..69d5e11e7 100644 --- a/src/NzbDrone.Core/Download/Clients/Putio/PutioException.cs +++ b/src/NzbDrone.Core/Download/Clients/Putio/PutioException.cs @@ -1,5 +1,3 @@ -using System; - namespace NzbDrone.Core.Download.Clients.Putio { public class PutioException : DownloadClientException @@ -7,7 +5,6 @@ namespace NzbDrone.Core.Download.Clients.Putio public PutioException(string message) : base(message) { - } } } diff --git a/src/NzbDrone.Core/Download/Clients/Putio/PutioFileResponse.cs b/src/NzbDrone.Core/Download/Clients/Putio/PutioFileResponse.cs index a03ffc152..8bd40351e 100644 --- a/src/NzbDrone.Core/Download/Clients/Putio/PutioFileResponse.cs +++ b/src/NzbDrone.Core/Download/Clients/Putio/PutioFileResponse.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; namespace NzbDrone.Core.Download.Clients.Putio { public class PutioFileResponse : PutioGenericResponse diff --git a/src/NzbDrone.Core/Download/Clients/Putio/PutioProxy.cs b/src/NzbDrone.Core/Download/Clients/Putio/PutioProxy.cs index 91afd6f52..e4da24a7c 100644 --- a/src/NzbDrone.Core/Download/Clients/Putio/PutioProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Putio/PutioProxy.cs @@ -1,10 +1,9 @@ -using System; +using System; using System.Collections.Generic; -using NzbDrone.Core.Rest; +using System.Net; using NLog; -using RestSharp; -using RestSharp.Deserializers; -using Newtonsoft.Json; +using NzbDrone.Common.Http; +using NzbDrone.Common.Serializer; namespace NzbDrone.Core.Download.Clients.Putio { @@ -18,90 +17,98 @@ namespace NzbDrone.Core.Download.Clients.Putio void GetAccountSettings(PutioSettings settings); } - public class PutioProxy: IPutioProxy + public class PutioProxy : IPutioProxy { private readonly Logger _logger; + private readonly IHttpClient _httpClient; - public PutioProxy(Logger logger) + public PutioProxy(Logger logger, IHttpClient client) { _logger = logger; + _httpClient = client; } public List GetTorrents(PutioSettings settings) { - var result = ProcessRequest(Method.GET, "transfers/list", null, settings); - return result.Transfers; + // var result = ProcessRequest(Method.GET, "transfers/list", null, settings); + // return result.Transfers; + return new List(); } public PutioFile GetFile(long fileId, PutioSettings settings) { - var result = ProcessRequest(Method.GET, "files/" + fileId, null, settings); - return result.File; + // var result = ProcessRequest(Method.GET, "files/" + fileId, null, settings); + // return result.File; + return new PutioFile(); } public void AddTorrentFromUrl(string torrentUrl, PutioSettings settings) { - var arguments = new Dictionary(); - arguments.Add("url", torrentUrl); - ProcessRequest(Method.POST, "transfers/add", arguments, settings); + // var arguments = new Dictionary(); + // arguments.Add("url", torrentUrl); + // ProcessRequest(Method.POST, "transfers/add", arguments, settings); } public void AddTorrentFromData(byte[] torrentData, PutioSettings settings) { - var arguments = new Dictionary(); - arguments.Add("metainfo", Convert.ToBase64String(torrentData)); - ProcessRequest(Method.POST, "transfers/add", arguments, settings); + // var arguments = new Dictionary(); + // arguments.Add("metainfo", Convert.ToBase64String(torrentData)); + // ProcessRequest(Method.POST, "transfers/add", arguments, settings); } public void RemoveTorrent(string hashString, PutioSettings settings) { - var arguments = new Dictionary(); - arguments.Add("transfer_ids", new string[] { hashString }); - ProcessRequest(Method.POST, "torrents/cancel", arguments, settings); + // var arguments = new Dictionary(); + // arguments.Add("transfer_ids", new string[] { hashString }); + // ProcessRequest(Method.POST, "torrents/cancel", arguments, settings); } public void GetAccountSettings(PutioSettings settings) { - ProcessRequest(Method.GET, "account/settings", null, settings); + // ProcessRequest(Method.GET, "account/settings", null, settings); } - public TResponseType ProcessRequest(Method method, string resource, Dictionary arguments, PutioSettings settings) where TResponseType : PutioGenericResponse + private HttpRequestBuilder BuildRequest(PutioSettings settings) { - var client = BuildClient(settings); + var requestBuilder = new HttpRequestBuilder("https://api.put.io/v2") + { + LogResponseContent = true + }; + requestBuilder.SetHeader("Authorization", "Bearer " + settings.OAuthToken); + return requestBuilder; + } - var request = new RestRequest(resource, method); - request.RequestFormat = DataFormat.Json; - request.AddQueryParameter("oauth_token", settings.OAuthToken); + private string ProcessRequest(HttpRequestBuilder requestBuilder) + { + var request = requestBuilder.Build(); + request.LogResponseContent = true; + request.SuppressHttpErrorStatusCodes = new[] { HttpStatusCode.Forbidden }; - if (arguments != null) + HttpResponse response; + try { - foreach (KeyValuePair e in arguments) + response = _httpClient.Execute(request); + + if (response.StatusCode == HttpStatusCode.Forbidden) { - request.AddParameter(e.Key, e.Value); + throw new DownloadClientException("Invalid credentials. Check your OAuthToken"); } } - - _logger.Debug("Method: {0} Url: {1}", method, client.BuildUri(request)); - - var restResponse = client.Execute(request); - - var json = new JsonDeserializer(); - - TResponseType output = json.Deserialize(restResponse); - - if (output.Status != "OK") + catch (Exception ex) { - throw new PutioException(output.ErrorMessage); + throw new DownloadClientException("Failed to connect to put.io.", ex); } - return output; + return response.Content; } - private IRestClient BuildClient(PutioSettings settings) + private TResult ProcessRequest(HttpRequestBuilder requestBuilder) + where TResult : new() { - var restClient = RestClientFactory.BuildClient(settings.Url); - restClient.FollowRedirects = false; - return restClient; + var responseContent = ProcessRequest(requestBuilder); + + return Json.Deserialize(responseContent); } + } } diff --git a/src/NzbDrone.Core/Download/Clients/Putio/PutioTorrentStatus.cs b/src/NzbDrone.Core/Download/Clients/Putio/PutioTorrentStatus.cs index 10bd697b0..f5c4007eb 100644 --- a/src/NzbDrone.Core/Download/Clients/Putio/PutioTorrentStatus.cs +++ b/src/NzbDrone.Core/Download/Clients/Putio/PutioTorrentStatus.cs @@ -1,12 +1,10 @@ -using System; - namespace NzbDrone.Core.Download.Clients.Putio { public sealed 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 Completed = "COMPLETED"; + public static readonly string Downloading = "DOWNLOADING"; + public static readonly string Error = "ERROR"; + public static readonly string InQueue = "IN_QUEUE"; } } From b1ec41c99032bf937de54e7505fdc285a25dca16 Mon Sep 17 00:00:00 2001 From: Michael Feinbier Date: Fri, 15 Sep 2023 16:56:33 +0200 Subject: [PATCH 03/10] Fix styles --- src/NzbDrone.Core/Download/Clients/Putio/PutioPriority.cs | 4 ++-- src/NzbDrone.Core/Download/Clients/Putio/PutioProxy.cs | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/NzbDrone.Core/Download/Clients/Putio/PutioPriority.cs b/src/NzbDrone.Core/Download/Clients/Putio/PutioPriority.cs index 1a93436c2..e5e69b121 100644 --- a/src/NzbDrone.Core/Download/Clients/Putio/PutioPriority.cs +++ b/src/NzbDrone.Core/Download/Clients/Putio/PutioPriority.cs @@ -1,8 +1,8 @@ -namespace NzbDrone.Core.Download.Clients.Putio +namespace NzbDrone.Core.Download.Clients.Putio { public enum PutioPriority { Last = 0, First = 1 } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/Download/Clients/Putio/PutioProxy.cs b/src/NzbDrone.Core/Download/Clients/Putio/PutioProxy.cs index e4da24a7c..ca3da1279 100644 --- a/src/NzbDrone.Core/Download/Clients/Putio/PutioProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Putio/PutioProxy.cs @@ -109,6 +109,5 @@ namespace NzbDrone.Core.Download.Clients.Putio return Json.Deserialize(responseContent); } - } } From 4452d1c9bf0d6689e3744cd87d4e3758cad9d4e4 Mon Sep 17 00:00:00 2001 From: Michael Feinbier Date: Sun, 17 Sep 2023 09:58:22 +0200 Subject: [PATCH 04/10] Fix Tests & GetItem Implementation --- .../PutioTests/PutioFixture.cs | 351 +++++------------- .../Download/Clients/Putio/Putio.cs | 67 ++-- .../Download/Clients/Putio/PutioFile.cs | 4 +- .../Download/Clients/Putio/PutioProxy.cs | 36 +- .../Download/Clients/Putio/PutioSettings.cs | 10 +- .../Download/Clients/Putio/PutioTorrent.cs | 7 +- .../Clients/Putio/PutioTorrentStatus.cs | 14 +- 7 files changed, 184 insertions(+), 305 deletions(-) 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() - { - GivenTorrents(new List - { - _queued - }); - } - - protected void PrepareClientToReturnDownloadingItem() + protected virtual void GivenFile(PutioFile file) { - GivenTorrents(new List - { - _downloading - }); - } + file ??= new PutioFile(); - protected void PrepareClientToReturnFailedItem() - { - GivenTorrents(new List - { - _failed - }); + Mocker.GetMock() + .Setup(s => s.GetFile(file.Id, It.IsAny())) + .Returns(file); } - protected void PrepareClientToReturnCompletedItem() + [Test] + public void getItems_contains_all_items() { GivenTorrents(new List - { - _completed - }); - } + { + _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; - 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(); + { + _queued + }); 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"; } } From 16ccff35bc3ec87365fd85f0bc063bacade4438a Mon Sep 17 00:00:00 2001 From: Michael Feinbier Date: Sun, 17 Sep 2023 10:43:05 +0200 Subject: [PATCH 05/10] Filter Items by ParentFolder --- .../PutioTests/PutioFixture.cs | 37 ++++++++++++++++--- .../Download/Clients/Putio/Putio.cs | 8 ++++ 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/PutioTests/PutioFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/PutioTests/PutioFixture.cs index 41bcc9aef..54607677e 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/PutioTests/PutioFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/PutioTests/PutioFixture.cs @@ -24,10 +24,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.PutioTests [SetUp] public void Setup() { - _settings = new PutioSettings - { - SaveParentId = "1", - }; + _settings = new PutioSettings(); Subject.Definition = new DownloadClientDefinition(); Subject.Definition.Settings = _settings; @@ -74,8 +71,16 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.PutioTests FileId = 1 }; - _completed_different_parent = _completed; - _completed_different_parent.SaveParentId = 2; + _completed_different_parent = new PutioTorrent + { + Hash = "HASH", + Status = PutioTorrentStatus.Completed, + Name = _title, + Size = 1000, + Downloaded = 1000, + SaveParentId = 2, + FileId = 1 + }; _seeding = new PutioTorrent { @@ -166,6 +171,26 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.PutioTests items.Should().HaveCount(6); } + [TestCase("1", 5)] + [TestCase("2", 1)] + [TestCase("3", 0)] + public void getItems_contains_only_items_with_matching_parent_id(string configuredParentId, int expectedCount) + { + GivenTorrents(new List + { + _queued, + _downloading, + _failed, + _completed, + _seeding, + _completed_different_parent + }); + + _settings.SaveParentId = configuredParentId; + + Subject.GetItems().Should().HaveCount(expectedCount); + } + [TestCase("WAITING", DownloadItemStatus.Queued)] [TestCase("PREPARING_DOWNLOAD", DownloadItemStatus.Queued)] [TestCase("COMPLETED", DownloadItemStatus.Completed)] diff --git a/src/NzbDrone.Core/Download/Clients/Putio/Putio.cs b/src/NzbDrone.Core/Download/Clients/Putio/Putio.cs index 81540c615..0f43f90d1 100644 --- a/src/NzbDrone.Core/Download/Clients/Putio/Putio.cs +++ b/src/NzbDrone.Core/Download/Clients/Putio/Putio.cs @@ -74,6 +74,12 @@ namespace NzbDrone.Core.Download.Clients.Putio continue; } + if (Settings.SaveParentId.IsNotNullOrWhiteSpace() && torrent.SaveParentId != long.Parse(Settings.SaveParentId)) + { + // torrent is not related to our parent folder + continue; + } + var item = new DownloadClientItem { DownloadId = torrent.Id.ToString(), @@ -88,6 +94,8 @@ namespace NzbDrone.Core.Download.Clients.Putio { if (torrent.FileId != 0) { + // How needs the output path need to look if we have remote files? + /* var file = _proxy.GetFile(torrent.FileId, Settings); var torrentPath = "/completed/" + file.Name; From c77566fc43378a0b6c039d97d567d548182e05e4 Mon Sep 17 00:00:00 2001 From: Michael Feinbier Date: Sun, 17 Sep 2023 11:14:37 +0200 Subject: [PATCH 06/10] Restructure responses and remove File entity File does not exist on Put.io API anymore --- .../PutioTests/PutioFixture.cs | 9 --------- src/NzbDrone.Core/Download/Clients/Putio/Putio.cs | 15 ++++++++------- .../Download/Clients/Putio/PutioFile.cs | 8 -------- .../Download/Clients/Putio/PutioFileResponse.cs | 7 ------- .../Download/Clients/Putio/PutioPriority.cs | 8 -------- .../Download/Clients/Putio/PutioProxy.cs | 14 ++------------ .../{PutioGenericResponse.cs => PutioResponse.cs} | 8 +++++++- .../Clients/Putio/PutioTransfersResponse.cs | 8 -------- 8 files changed, 17 insertions(+), 60 deletions(-) delete mode 100644 src/NzbDrone.Core/Download/Clients/Putio/PutioFile.cs delete mode 100644 src/NzbDrone.Core/Download/Clients/Putio/PutioFileResponse.cs delete mode 100644 src/NzbDrone.Core/Download/Clients/Putio/PutioPriority.cs rename src/NzbDrone.Core/Download/Clients/Putio/{PutioGenericResponse.cs => PutioResponse.cs} (56%) delete mode 100644 src/NzbDrone.Core/Download/Clients/Putio/PutioTransfersResponse.cs diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/PutioTests/PutioFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/PutioTests/PutioFixture.cs index 54607677e..9cad2262a 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/PutioTests/PutioFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/PutioTests/PutioFixture.cs @@ -137,15 +137,6 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.PutioTests .Returns(torrents); } - 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() { diff --git a/src/NzbDrone.Core/Download/Clients/Putio/Putio.cs b/src/NzbDrone.Core/Download/Clients/Putio/Putio.cs index 0f43f90d1..f211bf3bd 100644 --- a/src/NzbDrone.Core/Download/Clients/Putio/Putio.cs +++ b/src/NzbDrone.Core/Download/Clients/Putio/Putio.cs @@ -18,13 +18,14 @@ namespace NzbDrone.Core.Download.Clients.Putio { private readonly IPutioProxy _proxy; - public Putio(IPutioProxy proxy, - ITorrentFileInfoReader torrentFileInfoReader, - IHttpClient httpClient, - IConfigService configService, - IDiskProvider diskProvider, - IRemotePathMappingService remotePathMappingService, - Logger logger) + public Putio( + IPutioProxy proxy, + ITorrentFileInfoReader torrentFileInfoReader, + IHttpClient httpClient, + IConfigService configService, + IDiskProvider diskProvider, + IRemotePathMappingService remotePathMappingService, + Logger logger) : base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, logger) { _proxy = proxy; diff --git a/src/NzbDrone.Core/Download/Clients/Putio/PutioFile.cs b/src/NzbDrone.Core/Download/Clients/Putio/PutioFile.cs deleted file mode 100644 index f8de1733f..000000000 --- a/src/NzbDrone.Core/Download/Clients/Putio/PutioFile.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace NzbDrone.Core.Download.Clients.Putio -{ - public class PutioFile - { - public long Id { get; set; } - public string Name { get; set; } - } -} diff --git a/src/NzbDrone.Core/Download/Clients/Putio/PutioFileResponse.cs b/src/NzbDrone.Core/Download/Clients/Putio/PutioFileResponse.cs deleted file mode 100644 index 8bd40351e..000000000 --- a/src/NzbDrone.Core/Download/Clients/Putio/PutioFileResponse.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace NzbDrone.Core.Download.Clients.Putio -{ - public class PutioFileResponse : PutioGenericResponse - { - public PutioFile File { get; set; } - } -} diff --git a/src/NzbDrone.Core/Download/Clients/Putio/PutioPriority.cs b/src/NzbDrone.Core/Download/Clients/Putio/PutioPriority.cs deleted file mode 100644 index e5e69b121..000000000 --- a/src/NzbDrone.Core/Download/Clients/Putio/PutioPriority.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace NzbDrone.Core.Download.Clients.Putio -{ - public enum PutioPriority - { - Last = 0, - First = 1 - } -} diff --git a/src/NzbDrone.Core/Download/Clients/Putio/PutioProxy.cs b/src/NzbDrone.Core/Download/Clients/Putio/PutioProxy.cs index 26f2f922c..c12878567 100644 --- a/src/NzbDrone.Core/Download/Clients/Putio/PutioProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Putio/PutioProxy.cs @@ -10,7 +10,6 @@ namespace NzbDrone.Core.Download.Clients.Putio public interface IPutioProxy { List GetTorrents(PutioSettings settings); - PutioFile GetFile(long fileId, PutioSettings settings); void AddTorrentFromUrl(string torrentUrl, PutioSettings settings); void AddTorrentFromData(byte[] torrentData, PutioSettings settings); void RemoveTorrent(string hash, PutioSettings settings); @@ -30,16 +29,8 @@ namespace NzbDrone.Core.Download.Clients.Putio public List GetTorrents(PutioSettings settings) { - // var result = ProcessRequest(Method.GET, "transfers/list", null, settings); - // return result.Transfers; - return new List(); - } - - public PutioFile GetFile(long fileId, PutioSettings settings) - { - // var result = ProcessRequest(Method.GET, "files/" + fileId, null, settings); - // return result.File; - return new PutioFile(); + var result = Execute(BuildRequest(HttpMethod.Get, "transfers/list", settings)); + return result.Resource.Transfers; } public void AddTorrentFromUrl(string torrentUrl, PutioSettings settings) @@ -65,7 +56,6 @@ 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)); } diff --git a/src/NzbDrone.Core/Download/Clients/Putio/PutioGenericResponse.cs b/src/NzbDrone.Core/Download/Clients/Putio/PutioResponse.cs similarity index 56% rename from src/NzbDrone.Core/Download/Clients/Putio/PutioGenericResponse.cs rename to src/NzbDrone.Core/Download/Clients/Putio/PutioResponse.cs index 4e0624fb9..b92df4441 100644 --- a/src/NzbDrone.Core/Download/Clients/Putio/PutioGenericResponse.cs +++ b/src/NzbDrone.Core/Download/Clients/Putio/PutioResponse.cs @@ -1,4 +1,5 @@ -using Newtonsoft.Json; +using System.Collections.Generic; +using Newtonsoft.Json; namespace NzbDrone.Core.Download.Clients.Putio { @@ -9,4 +10,9 @@ namespace NzbDrone.Core.Download.Clients.Putio public string Status { get; set; } } + + public class PutioTransfersResponse : PutioGenericResponse + { + public List Transfers { get; set; } + } } diff --git a/src/NzbDrone.Core/Download/Clients/Putio/PutioTransfersResponse.cs b/src/NzbDrone.Core/Download/Clients/Putio/PutioTransfersResponse.cs deleted file mode 100644 index d54d6a5ff..000000000 --- a/src/NzbDrone.Core/Download/Clients/Putio/PutioTransfersResponse.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Collections.Generic; -namespace NzbDrone.Core.Download.Clients.Putio -{ - public class PutioTransfersResponse : PutioGenericResponse - { - public List Transfers { get; set; } - } -} From 9bff767feba0c316d5c8ef81333c979bef54e294 Mon Sep 17 00:00:00 2001 From: Michael Feinbier Date: Sun, 17 Sep 2023 12:18:17 +0200 Subject: [PATCH 07/10] Implement `AddTorrentFromUrl` --- .../Download/Clients/Putio/Putio.cs | 6 +++++ .../Download/Clients/Putio/PutioProxy.cs | 22 +++++++++++++++---- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/NzbDrone.Core/Download/Clients/Putio/Putio.cs b/src/NzbDrone.Core/Download/Clients/Putio/Putio.cs index f211bf3bd..a2ce8bbea 100644 --- a/src/NzbDrone.Core/Download/Clients/Putio/Putio.cs +++ b/src/NzbDrone.Core/Download/Clients/Putio/Putio.cs @@ -227,5 +227,11 @@ namespace NzbDrone.Core.Download.Clients.Putio { throw new NotImplementedException(); } + + public override void MarkItemAsImported(DownloadClientItem downloadClientItem) + { + // What to do here? Maybe delete the file and transfer from put.io? + base.MarkItemAsImported(downloadClientItem); + } } } diff --git a/src/NzbDrone.Core/Download/Clients/Putio/PutioProxy.cs b/src/NzbDrone.Core/Download/Clients/Putio/PutioProxy.cs index c12878567..2fd3b5685 100644 --- a/src/NzbDrone.Core/Download/Clients/Putio/PutioProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Putio/PutioProxy.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Net; using System.Net.Http; using NLog; +using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; namespace NzbDrone.Core.Download.Clients.Putio @@ -35,9 +36,15 @@ namespace NzbDrone.Core.Download.Clients.Putio public void AddTorrentFromUrl(string torrentUrl, PutioSettings settings) { - // var arguments = new Dictionary(); - // arguments.Add("url", torrentUrl); - // ProcessRequest(Method.POST, "transfers/add", arguments, settings); + var request = BuildRequest(HttpMethod.Post, "transfers/add", settings); + request.AddFormParameter("url", torrentUrl); + + if (settings.SaveParentId.IsNotNullOrWhiteSpace()) + { + request.AddFormParameter("save_parent_id", settings.SaveParentId); + } + + Execute(request); } public void AddTorrentFromData(byte[] torrentData, PutioSettings settings) @@ -79,7 +86,14 @@ namespace NzbDrone.Core.Download.Clients.Putio try { - return _httpClient.Get(request); + if (requestBuilder.Method == HttpMethod.Post) + { + return _httpClient.Post(request); + } + else + { + return _httpClient.Get(request); + } } catch (HttpException ex) { From 2e70764dd9a4a3040a6b7c4c3565a96e63101a92 Mon Sep 17 00:00:00 2001 From: Michael Feinbier Date: Mon, 18 Sep 2023 20:43:23 +0200 Subject: [PATCH 08/10] Implement TorrentMetadata --- .../PutioTests/PutioFixture.cs | 10 +-- .../PutioTests/PutioProxyFixtures.cs | 70 +++++++++++++++++++ .../Download/Clients/Putio/Putio.cs | 6 +- .../Download/Clients/Putio/PutioProxy.cs | 34 +++++++++ .../Download/Clients/Putio/PutioResponse.cs | 10 +++ .../Download/Clients/Putio/PutioSettings.cs | 11 ++- .../Download/Clients/Putio/PutioTorrent.cs | 7 ++ 7 files changed, 137 insertions(+), 11 deletions(-) create mode 100644 src/NzbDrone.Core.Test/Download/DownloadClientTests/PutioTests/PutioProxyFixtures.cs diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/PutioTests/PutioFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/PutioTests/PutioFixture.cs index 9cad2262a..763e7ef26 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/PutioTests/PutioFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/PutioTests/PutioFixture.cs @@ -162,10 +162,10 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.PutioTests items.Should().HaveCount(6); } - [TestCase("1", 5)] - [TestCase("2", 1)] - [TestCase("3", 0)] - public void getItems_contains_only_items_with_matching_parent_id(string configuredParentId, int expectedCount) + [TestCase(1, 5)] + [TestCase(2, 1)] + [TestCase(3, 0)] + public void getItems_contains_only_items_with_matching_parent_id(long configuredParentId, int expectedCount) { GivenTorrents(new List { @@ -177,7 +177,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.PutioTests _completed_different_parent }); - _settings.SaveParentId = configuredParentId; + _settings.SaveParentId = configuredParentId.ToString(); Subject.GetItems().Should().HaveCount(expectedCount); } diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/PutioTests/PutioProxyFixtures.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/PutioTests/PutioProxyFixtures.cs new file mode 100644 index 000000000..c945ff53b --- /dev/null +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/PutioTests/PutioProxyFixtures.cs @@ -0,0 +1,70 @@ +using Moq; +using NUnit.Framework; +using NzbDrone.Common.Http; +using NzbDrone.Core.Download.Clients.Putio; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.Download.DownloadClientTests.PutioTests +{ + public class PutioProxyFixtures : CoreTest + { + [Test] + public void test_GetTorrentMetadata_createsNewObject() + { + ClientGetWillReturn("{\"status\":\"OK\",\"value\":null}"); + + var mt = Subject.GetTorrentMetadata(new PutioTorrent { Id = 1 }, new PutioSettings()); + Assert.IsNotNull(mt); + Assert.AreEqual(1, mt.Id); + Assert.IsFalse(mt.Downloaded); + } + + [Test] + public void test_GetTorrentMetadata_returnsExistingObject() + { + ClientGetWillReturn("{\"status\":\"OK\",\"value\":{\"id\":4711,\"downloaded\":true}}"); + + var mt = Subject.GetTorrentMetadata(new PutioTorrent { Id = 1 }, new PutioSettings()); + Assert.IsNotNull(mt); + Assert.AreEqual(4711, mt.Id); + Assert.IsTrue(mt.Downloaded); + } + + [Test] + public void test_GetAllTorrentMetadata_filters_properly() + { + var json = @"{ + ""config"": { + ""sonarr_123"": { + ""downloaded"": true, + ""id"": 123 + }, + ""another_key"": { + ""foo"": ""bar"" + }, + ""sonarr_456"": { + ""downloaded"": true, + ""id"": 456 + } + }, + ""status"": ""OK"" + }"; + ClientGetWillReturn(json); + + var list = Subject.GetAllTorrentMetadata(new PutioSettings()); + Assert.IsTrue(list.ContainsKey("123")); + Assert.IsTrue(list.ContainsKey("456")); + Assert.AreEqual(list.Count, 2); + Assert.IsTrue(list["123"].Downloaded); + Assert.IsTrue(list["456"].Downloaded); + } + + private void ClientGetWillReturn(string obj) + where TResult : new() + { + Mocker.GetMock() + .Setup(s => s.Get(It.IsAny())) + .Returns(r => new HttpResponse(new HttpResponse(r, new HttpHeader(), obj))); + } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/Putio/Putio.cs b/src/NzbDrone.Core/Download/Clients/Putio/Putio.cs index a2ce8bbea..24cafb093 100644 --- a/src/NzbDrone.Core/Download/Clients/Putio/Putio.cs +++ b/src/NzbDrone.Core/Download/Clients/Putio/Putio.cs @@ -84,7 +84,7 @@ namespace NzbDrone.Core.Download.Clients.Putio var item = new DownloadClientItem { DownloadId = torrent.Id.ToString(), - Category = Settings.SaveParentId, + Category = Settings.SaveParentId?.ToString(), Title = torrent.Name, TotalSize = torrent.Size, RemainingSize = torrent.Size - torrent.Downloaded, @@ -165,12 +165,12 @@ namespace NzbDrone.Core.Download.Clients.Putio public override DownloadClientInfo GetStatus() { - var destDir = string.Format("{0}", Settings.SaveParentId); + var destDir = new OsPath(Settings.DownloadPath); return new DownloadClientInfo { IsLocalhost = false, - OutputRootFolders = new List { _remotePathMappingService.RemapRemoteToLocal(Settings.Url, new OsPath(destDir)) } + OutputRootFolders = new List { destDir } }; } diff --git a/src/NzbDrone.Core/Download/Clients/Putio/PutioProxy.cs b/src/NzbDrone.Core/Download/Clients/Putio/PutioProxy.cs index 2fd3b5685..7e071d3c5 100644 --- a/src/NzbDrone.Core/Download/Clients/Putio/PutioProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Putio/PutioProxy.cs @@ -19,6 +19,7 @@ namespace NzbDrone.Core.Download.Clients.Putio public class PutioProxy : IPutioProxy { + private const string _configPrefix = "sonarr_"; private readonly Logger _logger; private readonly IHttpClient _httpClient; @@ -66,6 +67,39 @@ namespace NzbDrone.Core.Download.Clients.Putio Execute(BuildRequest(HttpMethod.Get, "account/settings", settings)); } + public PutioTorrentMetadata GetTorrentMetadata(PutioTorrent torrent, PutioSettings settings) + { + var metadata = Execute(BuildRequest(HttpMethod.Get, "config/" + _configPrefix + torrent.Id, settings)); + if (metadata.Resource.Value != null) + { + _logger.Debug("Found metadata for torrent: {0} {1}", torrent.Id, metadata.Resource.Value); + return metadata.Resource.Value; + } + + return new PutioTorrentMetadata + { + Id = torrent.Id, + Downloaded = false + }; + } + + public Dictionary GetAllTorrentMetadata(PutioSettings settings) + { + var metadata = Execute(BuildRequest(HttpMethod.Get, "config", settings)); + var result = new Dictionary(); + + foreach (var item in metadata.Resource.Config) + { + if (item.Key.StartsWith(_configPrefix)) + { + var torrentId = item.Key.Substring(_configPrefix.Length); + result[torrentId] = item.Value; + } + } + + return result; + } + private HttpRequestBuilder BuildRequest(HttpMethod method, string endpoint, PutioSettings settings) { var requestBuilder = new HttpRequestBuilder("https://api.put.io/v2") diff --git a/src/NzbDrone.Core/Download/Clients/Putio/PutioResponse.cs b/src/NzbDrone.Core/Download/Clients/Putio/PutioResponse.cs index b92df4441..4b33eec93 100644 --- a/src/NzbDrone.Core/Download/Clients/Putio/PutioResponse.cs +++ b/src/NzbDrone.Core/Download/Clients/Putio/PutioResponse.cs @@ -15,4 +15,14 @@ namespace NzbDrone.Core.Download.Clients.Putio { public List Transfers { get; set; } } + + public class PutioConfigResponse : PutioGenericResponse + { + public PutioTorrentMetadata Value { get; set; } + } + + public class PutioAllConfigResponse : PutioGenericResponse + { + public Dictionary Config { get; set; } + } } diff --git a/src/NzbDrone.Core/Download/Clients/Putio/PutioSettings.cs b/src/NzbDrone.Core/Download/Clients/Putio/PutioSettings.cs index a019e6f6d..6077447ad 100644 --- a/src/NzbDrone.Core/Download/Clients/Putio/PutioSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Putio/PutioSettings.cs @@ -3,6 +3,7 @@ using FluentValidation; using NzbDrone.Core.Annotations; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; +using NzbDrone.Core.Validation.Paths; namespace NzbDrone.Core.Download.Clients.Putio { @@ -11,6 +12,7 @@ namespace NzbDrone.Core.Download.Clients.Putio public PutioSettingsValidator() { RuleFor(c => c.OAuthToken).NotEmpty().WithMessage("Please provide an OAuth token"); + RuleFor(c => c.DownloadPath).IsValidPath().WithMessage("Please provide a valid local path"); RuleFor(c => c.SaveParentId).Matches(@"^\.?[0-9]*$", RegexOptions.IgnoreCase).WithMessage("Allowed characters 0-9"); } } @@ -29,11 +31,14 @@ namespace NzbDrone.Core.Download.Clients.Putio [FieldDefinition(0, Label = "OAuth Token", Type = FieldType.Password)] public string OAuthToken { get; set; } - [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")] + [FieldDefinition(1, Label = "Save Parent Folder ID", Type = FieldType.Textbox, HelpText = "Adding a parent folder ID specific to Sonarr avoids conflicts with unrelated non-Sonarr downloads. Using a parent folder is optional, but strongly recommended.")] public string SaveParentId { get; set; } - [FieldDefinition(2, Label = "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; } + [FieldDefinition(2, Label = "Download completed transfers", Type = FieldType.Checkbox, HelpText = "If enabled, Sonarr will download completed files from Put.io. If you manually sync with rclone or similar, disable this")] + public bool DownloadFiles { get; set; } + + [FieldDefinition(3, Label = "Download Path", Type = FieldType.Path, HelpText = "Path were Put.io is downloading to or if downloading is disabled where the mounts are expected")] + public string DownloadPath { get; set; } public NzbDroneValidationResult Validate() { diff --git a/src/NzbDrone.Core/Download/Clients/Putio/PutioTorrent.cs b/src/NzbDrone.Core/Download/Clients/Putio/PutioTorrent.cs index f7f3ddfb6..e15d4bdab 100644 --- a/src/NzbDrone.Core/Download/Clients/Putio/PutioTorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/Putio/PutioTorrent.cs @@ -34,4 +34,11 @@ namespace NzbDrone.Core.Download.Clients.Putio public string Hash { get; set; } } + + public class PutioTorrentMetadata + { + public bool Downloaded { get; set; } + + public long Id { get; set; } + } } From b68a9912f8da9e58415f0b5fda2ded5ace0cdf0b Mon Sep 17 00:00:00 2001 From: Michael Feinbier Date: Sat, 23 Sep 2023 09:54:10 +0200 Subject: [PATCH 09/10] Small refactorings --- .../PutioTests/PutioFixture.cs | 50 ++++++++++++++++--- .../Download/Clients/Putio/Putio.cs | 28 +++++++---- .../Download/Clients/Putio/PutioProxy.cs | 2 + .../Download/Clients/Putio/PutioSettings.cs | 9 ++-- .../Download/Clients/Putio/PutioTorrent.cs | 30 +++++------ 5 files changed, 84 insertions(+), 35 deletions(-) diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/PutioTests/PutioFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/PutioTests/PutioFixture.cs index 763e7ef26..ef849b8c3 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/PutioTests/PutioFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/PutioTests/PutioFixture.cs @@ -32,6 +32,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.PutioTests _queued = new PutioTorrent { Hash = "HASH", + Id = 1, Status = PutioTorrentStatus.InQueue, Name = _title, Size = 1000, @@ -42,6 +43,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.PutioTests _downloading = new PutioTorrent { Hash = "HASH", + Id = 2, Status = PutioTorrentStatus.Downloading, Name = _title, Size = 1000, @@ -52,6 +54,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.PutioTests _failed = new PutioTorrent { Hash = "HASH", + Id = 3, Status = PutioTorrentStatus.Error, ErrorMessage = "Torrent has reached the maximum number of inactive days.", Name = _title, @@ -64,6 +67,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.PutioTests { Hash = "HASH", Status = PutioTorrentStatus.Completed, + Id = 4, Name = _title, Size = 1000, Downloaded = 1000, @@ -74,6 +78,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.PutioTests _completed_different_parent = new PutioTorrent { Hash = "HASH", + Id = 5, Status = PutioTorrentStatus.Completed, Name = _title, Size = 1000, @@ -85,10 +90,12 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.PutioTests _seeding = new PutioTorrent { Hash = "HASH", + Id = 6, Status = PutioTorrentStatus.Seeding, Name = _title, Size = 1000, Downloaded = 1000, + Uploaded = 1300, SaveParentId = 1, FileId = 2 }; @@ -137,17 +144,37 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.PutioTests .Returns(torrents); } + protected virtual void GivenMetadata(List metadata) + { + metadata ??= new List(); + var result = new Dictionary(); + foreach (var item in metadata) + { + result.Add(item.Id.ToString(), item); + } + + Mocker.GetMock() + .Setup(s => s.GetAllTorrentMetadata(It.IsAny())) + .Returns(result); + } + [Test] public void getItems_contains_all_items() { GivenTorrents(new List { - _queued, - _downloading, - _failed, - _completed, - _seeding, - _completed_different_parent + _queued, + _downloading, + _failed, + _completed, + _seeding, + _completed_different_parent + }); + GivenMetadata(new List + { + PutioTorrentMetadata.fromTorrent(_completed, true), + PutioTorrentMetadata.fromTorrent(_seeding, true), + PutioTorrentMetadata.fromTorrent(_completed_different_parent, true), }); var items = Subject.GetItems(); @@ -198,10 +225,21 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.PutioTests { _queued }); + GivenMetadata(new List { PutioTorrentMetadata.fromTorrent(_queued, true) }); var item = Subject.GetItems().Single(); item.Status.Should().Be(expectedItemStatus); } + + [Test] + public void test_getItems_marks_non_existing_local_download_as_downloading() + { + GivenTorrents(new List { _completed }); + GivenMetadata(new List { PutioTorrentMetadata.fromTorrent(_completed, false) }); + + var item = Subject.GetItems().Single(); + VerifyDownloading(item); + } } } diff --git a/src/NzbDrone.Core/Download/Clients/Putio/Putio.cs b/src/NzbDrone.Core/Download/Clients/Putio/Putio.cs index 24cafb093..a385d68d4 100644 --- a/src/NzbDrone.Core/Download/Clients/Putio/Putio.cs +++ b/src/NzbDrone.Core/Download/Clients/Putio/Putio.cs @@ -8,6 +8,7 @@ using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; using NzbDrone.Core.MediaFiles.TorrentInfo; +using NzbDrone.Core.Organizer; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.RemotePathMappings; using NzbDrone.Core.Validation; @@ -54,24 +55,24 @@ namespace NzbDrone.Core.Download.Clients.Putio public override IEnumerable GetItems() { List torrents; + Dictionary metadata; try { torrents = _proxy.GetTorrents(Settings); + metadata = _proxy.GetAllTorrentMetadata(Settings); } catch (DownloadClientException ex) { _logger.Error(ex, ex.Message); - return Enumerable.Empty(); + yield break; } - var items = new List(); - foreach (var torrent in torrents) { - // If totalsize == 0 the torrent is a magnet downloading metadata if (torrent.Size == 0) { + // If totalsize == 0 the torrent is a magnet downloading metadata continue; } @@ -88,7 +89,11 @@ namespace NzbDrone.Core.Download.Clients.Putio Title = torrent.Name, TotalSize = torrent.Size, RemainingSize = torrent.Size - torrent.Downloaded, - DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this) + DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this), + SeedRatio = torrent.Ratio, + + // Initial status, might change later + Status = GetDownloadItemStatus(torrent) }; try @@ -97,6 +102,10 @@ namespace NzbDrone.Core.Download.Clients.Putio { // How needs the output path need to look if we have remote files? + // check if we need to download the torrent from the remote + var title = FileNameBuilder.CleanFileName(torrent.Name); + + // _diskProvider.FileExists(new OsPath()) /* var file = _proxy.GetFile(torrent.FileId, Settings); var torrentPath = "/completed/" + file.Name; @@ -126,21 +135,17 @@ 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; } - items.Add(item); + yield return item; } - - return items; } - private DownloadItemStatus GetStatus(PutioTorrent torrent) + private DownloadItemStatus GetDownloadItemStatus(PutioTorrent torrent) { if (torrent.Status == PutioTorrentStatus.Completed || torrent.Status == PutioTorrentStatus.Seeding) @@ -176,6 +181,7 @@ namespace NzbDrone.Core.Download.Clients.Putio protected override void Test(List failures) { + failures.AddIfNotNull(TestFolder(Settings.DownloadPath, "DownloadPath")); failures.AddIfNotNull(TestConnection()); if (failures.Any()) { diff --git a/src/NzbDrone.Core/Download/Clients/Putio/PutioProxy.cs b/src/NzbDrone.Core/Download/Clients/Putio/PutioProxy.cs index 7e071d3c5..902cc90f1 100644 --- a/src/NzbDrone.Core/Download/Clients/Putio/PutioProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Putio/PutioProxy.cs @@ -15,6 +15,8 @@ namespace NzbDrone.Core.Download.Clients.Putio void AddTorrentFromData(byte[] torrentData, PutioSettings settings); void RemoveTorrent(string hash, PutioSettings settings); void GetAccountSettings(PutioSettings settings); + public PutioTorrentMetadata GetTorrentMetadata(PutioTorrent torrent, PutioSettings settings); + public Dictionary GetAllTorrentMetadata(PutioSettings settings); } public class PutioProxy : IPutioProxy diff --git a/src/NzbDrone.Core/Download/Clients/Putio/PutioSettings.cs b/src/NzbDrone.Core/Download/Clients/Putio/PutioSettings.cs index 6077447ad..8654c91f9 100644 --- a/src/NzbDrone.Core/Download/Clients/Putio/PutioSettings.cs +++ b/src/NzbDrone.Core/Download/Clients/Putio/PutioSettings.cs @@ -24,6 +24,7 @@ namespace NzbDrone.Core.Download.Clients.Putio public PutioSettings() { Url = "https://api.put.io/v2"; + DeleteImported = false; } public string Url { get; } @@ -34,12 +35,12 @@ namespace NzbDrone.Core.Download.Clients.Putio [FieldDefinition(1, Label = "Save Parent Folder ID", Type = FieldType.Textbox, HelpText = "Adding a parent folder ID specific to Sonarr avoids conflicts with unrelated non-Sonarr downloads. Using a parent folder is optional, but strongly recommended.")] public string SaveParentId { get; set; } - [FieldDefinition(2, Label = "Download completed transfers", Type = FieldType.Checkbox, HelpText = "If enabled, Sonarr will download completed files from Put.io. If you manually sync with rclone or similar, disable this")] - public bool DownloadFiles { get; set; } - - [FieldDefinition(3, Label = "Download Path", Type = FieldType.Path, HelpText = "Path were Put.io is downloading to or if downloading is disabled where the mounts are expected")] + [FieldDefinition(2, Label = "Download Path", Type = FieldType.Path, HelpText = "Path were Sonarr will expect the files to get downloaded to. Note: This client does not download finished transfers automatically. Instead make sure that you download them outside of Sonarr e.g. with rclone")] public string DownloadPath { get; set; } + [FieldDefinition(3, Label = "Delete imported files", Type = FieldType.Checkbox, HelpText = "Delete the files on put.io when Sonarr marks them as successfully imported")] + public bool DeleteImported { get; set; } + public NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this)); diff --git a/src/NzbDrone.Core/Download/Clients/Putio/PutioTorrent.cs b/src/NzbDrone.Core/Download/Clients/Putio/PutioTorrent.cs index e15d4bdab..196a1080a 100644 --- a/src/NzbDrone.Core/Download/Clients/Putio/PutioTorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/Putio/PutioTorrent.cs @@ -4,39 +4,41 @@ namespace NzbDrone.Core.Download.Clients.Putio { public class PutioTorrent { - public long Downloaded { get; set; } + public int Id { get; set; } + public string Hash { get; set; } + public string Name { get; set; } + public long Downloaded { get; set; } + public long Uploaded { get; set; } [JsonProperty(PropertyName = "error_message")] public string ErrorMessage { get; set; } - [JsonProperty(PropertyName = "estimated_time")] public long EstimatedTime { get; set; } - [JsonProperty(PropertyName = "file_id")] public long FileId { get; set; } - - public int Id { get; set; } - - public string Name { get; set; } - [JsonProperty(PropertyName = "percent_done")] public int PercentDone { get; set; } - [JsonProperty(PropertyName = "seconds_seeding")] public long SecondsSeeding { get; set; } - public long Size { get; set; } - public string Status { get; set; } - [JsonProperty(PropertyName = "save_parent_id")] public long SaveParentId { get; set; } - - public string Hash { get; set; } + [JsonProperty(PropertyName = "current_ratio")] + public double Ratio { get; set; } } public class PutioTorrentMetadata { + public static PutioTorrentMetadata fromTorrent(PutioTorrent torrent, bool downloaded = false) + { + return new PutioTorrentMetadata + { + Downloaded = downloaded, + Id = torrent.Id + }; + } + public bool Downloaded { get; set; } public long Id { get; set; } From 12e5d1fad1978e8a5741bf97480d890b34e0cbd0 Mon Sep 17 00:00:00 2001 From: Michael Feinbier Date: Fri, 29 Sep 2023 17:14:47 +0200 Subject: [PATCH 10/10] Implement Remote File listing and local mapping --- .../PutioTests/PutioFixture.cs | 98 +++++++++++++------ .../PutioTests/PutioProxyFixtures.cs | 62 ++++++++++++ .../Download/Clients/Putio/Putio.cs | 70 ++++++++----- .../Download/Clients/Putio/PutioFile.cs | 20 ++++ .../Download/Clients/Putio/PutioProxy.cs | 27 +++-- .../Download/Clients/Putio/PutioResponse.cs | 20 ++++ 6 files changed, 237 insertions(+), 60 deletions(-) create mode 100644 src/NzbDrone.Core/Download/Clients/Putio/PutioFile.cs diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/PutioTests/PutioFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/PutioTests/PutioFixture.cs index ef849b8c3..a90a7bf1c 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/PutioTests/PutioFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/PutioTests/PutioFixture.cs @@ -4,6 +4,7 @@ using System.Linq; using FluentAssertions; using Moq; using NUnit.Framework; +using NzbDrone.Common.Disk; using NzbDrone.Common.Http; using NzbDrone.Core.Download; using NzbDrone.Core.Download.Clients.Putio; @@ -24,7 +25,10 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.PutioTests [SetUp] public void Setup() { - _settings = new PutioSettings(); + _settings = new PutioSettings + { + SaveParentId = "1" + }; Subject.Definition = new DownloadClientDefinition(); Subject.Definition.Settings = _settings; @@ -72,7 +76,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.PutioTests Size = 1000, Downloaded = 1000, SaveParentId = 1, - FileId = 1 + FileId = 2 }; _completed_different_parent = new PutioTorrent @@ -84,7 +88,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.PutioTests Size = 1000, Downloaded = 1000, SaveParentId = 2, - FileId = 1 + FileId = 3 }; _seeding = new PutioTorrent @@ -97,7 +101,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.PutioTests Downloaded = 1000, Uploaded = 1300, SaveParentId = 1, - FileId = 2 + FileId = 4 }; Mocker.GetMock() @@ -110,6 +114,10 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.PutioTests Mocker.GetMock() .Setup(v => v.GetAccountSettings(It.IsAny())); + + Mocker.GetMock() + .Setup(v => v.GetFileListingResponse(It.IsAny(), It.IsAny())) + .Returns(PutioFileListingResponse.Empty()); } protected void GivenFailedDownload() @@ -121,18 +129,16 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.PutioTests 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.AddTorrentFromUrl(It.IsAny(), It.IsAny())) - .Callback(PrepareClientToReturnQueuedItem); - - Mocker.GetMock() - .Setup(s => s.AddTorrentFromData(It.IsAny(), It.IsAny())) - .Callback(PrepareClientToReturnQueuedItem); - */ + GivenRemoteFileStructure(new List + { + new PutioFile { Id = _completed.FileId, Name = _title, FileType = PutioFile.FILE_TYPE_VIDEO }, + new PutioFile { Id = _seeding.FileId, Name = _title, FileType = PutioFile.FILE_TYPE_FOLDER }, + }, new PutioFile { Id = 1, Name = "Downloads" }); + + // GivenRemoteFileStructure(new List + // { + // new PutioFile { Id = _completed_different_parent.FileId, Name = _title, FileType = PutioFile.FILE_TYPE_VIDEO }, + // }, new PutioFile { Id = 2, Name = "Downloads_new" }); } protected virtual void GivenTorrents(List torrents) @@ -144,6 +150,20 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.PutioTests .Returns(torrents); } + protected virtual void GivenRemoteFileStructure(List files, PutioFile parentFile) + { + files ??= new List(); + var list = new PutioFileListingResponse { Files = files, Parent = parentFile }; + + Mocker.GetMock() + .Setup(s => s.GetFileListingResponse(parentFile.Id, It.IsAny())) + .Returns(list); + + Mocker.GetMock() + .Setup(s => s.FolderExists(It.IsAny())) + .Returns(true); + } + protected virtual void GivenMetadata(List metadata) { metadata ??= new List(); @@ -170,12 +190,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.PutioTests _seeding, _completed_different_parent }); - GivenMetadata(new List - { - PutioTorrentMetadata.fromTorrent(_completed, true), - PutioTorrentMetadata.fromTorrent(_seeding, true), - PutioTorrentMetadata.fromTorrent(_completed_different_parent, true), - }); + GivenSuccessfulDownload(); var items = Subject.GetItems(); @@ -184,9 +199,8 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.PutioTests VerifyWarning(items.ElementAt(2)); VerifyCompleted(items.ElementAt(3)); VerifyCompleted(items.ElementAt(4)); - VerifyCompleted(items.ElementAt(5)); - items.Should().HaveCount(6); + items.Should().HaveCount(5); } [TestCase(1, 5)] @@ -203,6 +217,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.PutioTests _seeding, _completed_different_parent }); + GivenSuccessfulDownload(); _settings.SaveParentId = configuredParentId.ToString(); @@ -225,7 +240,6 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.PutioTests { _queued }); - GivenMetadata(new List { PutioTorrentMetadata.fromTorrent(_queued, true) }); var item = Subject.GetItems().Single(); @@ -233,13 +247,41 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.PutioTests } [Test] - public void test_getItems_marks_non_existing_local_download_as_downloading() + public void test_getItems_path_for_folders() + { + GivenTorrents(new List { _completed }); + GivenRemoteFileStructure(new List + { + new PutioFile { Id = _completed.FileId, Name = _title, FileType = PutioFile.FILE_TYPE_FOLDER }, + }, new PutioFile { Id = 1, Name = "Downloads" }); + + var item = Subject.GetItems().Single(); + + VerifyCompleted(item); + item.OutputPath.ToString().Should().ContainAll("Downloads", _title); + + Mocker.GetMock() + .Verify(s => s.GetFileListingResponse(1, It.IsAny()), Times.AtLeastOnce()); + } + + [Test] + public void test_getItems_path_for_files() { GivenTorrents(new List { _completed }); - GivenMetadata(new List { PutioTorrentMetadata.fromTorrent(_completed, false) }); + GivenRemoteFileStructure(new List + { + new PutioFile { Id = _completed.FileId, Name = _title, FileType = PutioFile.FILE_TYPE_VIDEO }, + }, new PutioFile { Id = 1, Name = "Downloads" }); var item = Subject.GetItems().Single(); - VerifyDownloading(item); + + VerifyCompleted(item); + + item.OutputPath.ToString().Should().Contain("Downloads"); + item.OutputPath.ToString().Should().NotContain(_title); + + Mocker.GetMock() + .Verify(s => s.GetFileListingResponse(It.IsAny(), It.IsAny()), Times.AtLeastOnce()); } } } diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/PutioTests/PutioProxyFixtures.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/PutioTests/PutioProxyFixtures.cs index c945ff53b..12c8c6d15 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/PutioTests/PutioProxyFixtures.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/PutioTests/PutioProxyFixtures.cs @@ -59,6 +59,68 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.PutioTests Assert.IsTrue(list["456"].Downloaded); } + [Test] + public void test_GetFileListingResponse() + { + var json = @"{ + ""cursor"": null, + ""files"": [ + { + ""file_type"": ""VIDEO"", + ""id"": 111, + ""name"": ""My.download.mkv"", + ""parent_id"": 4711 + }, + { + ""file_type"": ""FOLDER"", + ""id"": 222, + ""name"": ""Another-folder[dth]"", + ""parent_id"": 4711 + } + ], + ""parent"": { + ""file_type"": ""FOLDER"", + ""id"": 4711, + ""name"": ""Incoming"", + ""parent_id"": 0 + }, + ""status"": ""OK"", + ""total"": 2 + }"; + ClientGetWillReturn(json); + + var response = Subject.GetFileListingResponse(4711, new PutioSettings()); + + Assert.That(response, Is.Not.Null); + Assert.AreEqual(response.Files.Count, 2); + Assert.AreEqual(4711, response.Parent.Id); + Assert.AreEqual(111, response.Files[0].Id); + Assert.AreEqual(222, response.Files[1].Id); + } + + [Test] + public void test_GetFileListingResponse_empty() + { + var json = @"{ + ""cursor"": null, + ""files"": [], + ""parent"": { + ""file_type"": ""FOLDER"", + ""id"": 4711, + ""name"": ""Incoming"", + ""parent_id"": 0 + }, + ""status"": ""OK"", + ""total"": 0 + }"; + ClientGetWillReturn(json); + + var response = Subject.GetFileListingResponse(4711, new PutioSettings()); + + Assert.That(response, Is.Not.Null); + Assert.AreEqual(response.Files.Count, 0); + } + private void ClientGetWillReturn(string obj) where TResult : new() { diff --git a/src/NzbDrone.Core/Download/Clients/Putio/Putio.cs b/src/NzbDrone.Core/Download/Clients/Putio/Putio.cs index a385d68d4..d85ea023f 100644 --- a/src/NzbDrone.Core/Download/Clients/Putio/Putio.cs +++ b/src/NzbDrone.Core/Download/Clients/Putio/Putio.cs @@ -8,7 +8,6 @@ using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; using NzbDrone.Core.MediaFiles.TorrentInfo; -using NzbDrone.Core.Organizer; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.RemotePathMappings; using NzbDrone.Core.Validation; @@ -55,12 +54,20 @@ namespace NzbDrone.Core.Download.Clients.Putio public override IEnumerable GetItems() { List torrents; - Dictionary metadata; + PutioFileListingResponse fileListingResponse; try { torrents = _proxy.GetTorrents(Settings); - metadata = _proxy.GetAllTorrentMetadata(Settings); + + if (Settings.SaveParentId.IsNotNullOrWhiteSpace()) + { + fileListingResponse = _proxy.GetFileListingResponse(long.Parse(Settings.SaveParentId), Settings); + } + else + { + fileListingResponse = _proxy.GetFileListingResponse(0, Settings); + } } catch (DownloadClientException ex) { @@ -100,29 +107,31 @@ namespace NzbDrone.Core.Download.Clients.Putio { if (torrent.FileId != 0) { - // How needs the output path need to look if we have remote files? - - // check if we need to download the torrent from the remote - var title = FileNameBuilder.CleanFileName(torrent.Name); - - // _diskProvider.FileExists(new OsPath()) - /* - var file = _proxy.GetFile(torrent.FileId, Settings); - var torrentPath = "/completed/" + file.Name; + // Todo: make configurable? Behaviour might be different for users (rclone mount, vs sync/mv) + item.CanMoveFiles = false; + item.CanBeRemoved = false; - var outputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Url, new OsPath(torrentPath)); + var file = fileListingResponse.Files.FirstOrDefault(f => f.Id == torrent.FileId); + var parent = fileListingResponse.Parent; - if (Settings.SaveParentId.IsNotNullOrWhiteSpace()) + if (file == null || parent == null) { - var directories = outputPath.FullPath.Split('\\', '/'); - if (!directories.Contains(string.Format("{0}", Settings.SaveParentId))) + item.Message = string.Format("Did not find file {0} in remote listing", torrent.FileId); + item.Status = DownloadItemStatus.Warning; + } + else + { + var expectedPath = new OsPath(Settings.DownloadPath) + new OsPath(parent.Name); + if (file.IsFolder()) { - continue; + expectedPath += new OsPath(file.Name); } - } - item.OutputPath = outputPath; // + torrent.Name; - */ + if (_diskProvider.FolderExists(expectedPath.FullPath)) + { + item.OutputPath = expectedPath; + } + } } } catch (DownloadClientException ex) @@ -170,12 +179,9 @@ namespace NzbDrone.Core.Download.Clients.Putio public override DownloadClientInfo GetStatus() { - var destDir = new OsPath(Settings.DownloadPath); - return new DownloadClientInfo { - IsLocalhost = false, - OutputRootFolders = new List { destDir } + IsLocalhost = false }; } @@ -183,6 +189,7 @@ namespace NzbDrone.Core.Download.Clients.Putio { failures.AddIfNotNull(TestFolder(Settings.DownloadPath, "DownloadPath")); failures.AddIfNotNull(TestConnection()); + failures.AddIfNotNull(TestRemoteParentFolder()); if (failures.Any()) { return; @@ -229,6 +236,21 @@ namespace NzbDrone.Core.Download.Clients.Putio return null; } + private ValidationFailure TestRemoteParentFolder() + { + try + { + _proxy.GetFileListingResponse(long.Parse(Settings.SaveParentId), Settings); + } + catch (Exception ex) + { + _logger.Error(ex, ex.Message); + return new NzbDroneValidationFailure("SaveParentId", "This is not a valid folder in your account"); + } + + return null; + } + public override void RemoveItem(DownloadClientItem item, bool deleteData) { throw new NotImplementedException(); diff --git a/src/NzbDrone.Core/Download/Clients/Putio/PutioFile.cs b/src/NzbDrone.Core/Download/Clients/Putio/PutioFile.cs new file mode 100644 index 000000000..973ed8095 --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/Putio/PutioFile.cs @@ -0,0 +1,20 @@ +using Newtonsoft.Json; + +namespace NzbDrone.Core.Download.Clients.Putio +{ + public class PutioFile + { + public static string FILE_TYPE_FOLDER = "FOLDER"; + public static string FILE_TYPE_VIDEO = "VIDEO"; + public long Id { get; set; } + public string Name { get; set; } + [JsonProperty(PropertyName = "parent_id")] + public long ParentId { get; set; } + [JsonProperty(PropertyName = "file_type")] + public string FileType { get; set; } + public bool IsFolder() + { + return FileType == FILE_TYPE_FOLDER; + } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/Putio/PutioProxy.cs b/src/NzbDrone.Core/Download/Clients/Putio/PutioProxy.cs index 902cc90f1..81bbb0216 100644 --- a/src/NzbDrone.Core/Download/Clients/Putio/PutioProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Putio/PutioProxy.cs @@ -13,10 +13,10 @@ namespace NzbDrone.Core.Download.Clients.Putio List GetTorrents(PutioSettings settings); void AddTorrentFromUrl(string torrentUrl, PutioSettings settings); void AddTorrentFromData(byte[] torrentData, PutioSettings settings); - void RemoveTorrent(string hash, PutioSettings settings); void GetAccountSettings(PutioSettings settings); public PutioTorrentMetadata GetTorrentMetadata(PutioTorrent torrent, PutioSettings settings); public Dictionary GetAllTorrentMetadata(PutioSettings settings); + public PutioFileListingResponse GetFileListingResponse(long parentId, PutioSettings settings); } public class PutioProxy : IPutioProxy @@ -57,13 +57,6 @@ namespace NzbDrone.Core.Download.Clients.Putio // ProcessRequest(Method.POST, "transfers/add", arguments, settings); } - public void RemoveTorrent(string hashString, PutioSettings settings) - { - // var arguments = new Dictionary(); - // arguments.Add("transfer_ids", new string[] { hashString }); - // ProcessRequest(Method.POST, "torrents/cancel", arguments, settings); - } - public void GetAccountSettings(PutioSettings settings) { Execute(BuildRequest(HttpMethod.Get, "account/settings", settings)); @@ -85,6 +78,24 @@ namespace NzbDrone.Core.Download.Clients.Putio }; } + public PutioFileListingResponse GetFileListingResponse(long parentId, PutioSettings settings) + { + var request = BuildRequest(HttpMethod.Get, "files/list", settings); + request.AddQueryParam("parent_id", parentId); + request.AddQueryParam("per_page", 1000); + + try + { + var response = Execute(request); + return response.Resource; + } + catch (DownloadClientException ex) + { + _logger.Error(ex, "Failed to get file listing response"); + throw; + } + } + public Dictionary GetAllTorrentMetadata(PutioSettings settings) { var metadata = Execute(BuildRequest(HttpMethod.Get, "config", settings)); diff --git a/src/NzbDrone.Core/Download/Clients/Putio/PutioResponse.cs b/src/NzbDrone.Core/Download/Clients/Putio/PutioResponse.cs index 4b33eec93..e47bd2a33 100644 --- a/src/NzbDrone.Core/Download/Clients/Putio/PutioResponse.cs +++ b/src/NzbDrone.Core/Download/Clients/Putio/PutioResponse.cs @@ -25,4 +25,24 @@ namespace NzbDrone.Core.Download.Clients.Putio { public Dictionary Config { get; set; } } + + public class PutioFileListingResponse : PutioGenericResponse + { + public static PutioFileListingResponse Empty() + { + return new PutioFileListingResponse + { + Files = new List(), + Parent = new PutioFile + { + Id = 0, + Name = "Your Files" + } + }; + } + + public List Files { get; set; } + + public PutioFile Parent { get; set; } + } }