From af5166e95dbc458ffbd0a8fba4cfdbfbe12b7238 Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Wed, 29 May 2019 23:56:37 +0200 Subject: [PATCH] Fixed: Transmission seeding idle time handling --- .../TransmissionTests/TransmissionFixture.cs | 140 +++++++++++++++++- .../TransmissionFixtureBase.cs | 58 +++++++- .../VuzeTests/VuzeFixture.cs | 4 +- .../Clients/Transmission/TransmissionBase.cs | 50 ++++++- .../Transmission/TransmissionConfig.cs | 27 ++++ .../Clients/Transmission/TransmissionProxy.cs | 21 +-- .../Transmission/TransmissionTorrent.cs | 6 +- src/NzbDrone.Core/NzbDrone.Core.csproj | 1 + 8 files changed, 282 insertions(+), 25 deletions(-) create mode 100644 src/NzbDrone.Core/Download/Clients/Transmission/TransmissionConfig.cs diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/TransmissionTests/TransmissionFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/TransmissionTests/TransmissionFixture.cs index 032fe15ff..1cea84d48 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/TransmissionTests/TransmissionFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/TransmissionTests/TransmissionFixture.cs @@ -42,8 +42,8 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.TransmissionTests var item = Subject.GetItems().Single(); VerifyCompleted(item); - item.CanBeRemoved.Should().BeTrue(); - item.CanMoveFiles.Should().BeTrue(); + item.CanBeRemoved.Should().BeFalse(); + item.CanMoveFiles.Should().BeFalse(); } [Test] @@ -175,7 +175,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.TransmissionTests item.Status.Should().Be(expectedItemStatus); } - [TestCase(TransmissionTorrentStatus.Stopped, DownloadItemStatus.Completed, true)] + [TestCase(TransmissionTorrentStatus.Stopped, DownloadItemStatus.Completed, false)] [TestCase(TransmissionTorrentStatus.CheckWait, DownloadItemStatus.Downloading, false)] [TestCase(TransmissionTorrentStatus.Check, DownloadItemStatus.Downloading, false)] [TestCase(TransmissionTorrentStatus.Queued, DownloadItemStatus.Completed, false)] @@ -283,5 +283,139 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.TransmissionTests var item = Subject.GetItems().Single(); item.RemainingTime.Should().NotHaveValue(); } + + + [Test] + public void should_not_be_removable_and_should_not_allow_move_files_if_max_ratio_reached_and_not_stopped() + { + GivenGlobalSeedLimits(1.0); + PrepareClientToReturnCompletedItem(false, ratio: 1.0); + + var item = Subject.GetItems().Single(); + item.CanBeRemoved.Should().BeFalse(); + item.CanMoveFiles.Should().BeFalse(); + } + + [Test] + public void should_not_be_removable_and_should_not_allow_move_files_if_max_ratio_is_not_set() + { + GivenGlobalSeedLimits(); + PrepareClientToReturnCompletedItem(true, ratio: 1.0); + + var item = Subject.GetItems().Single(); + item.CanBeRemoved.Should().BeFalse(); + item.CanMoveFiles.Should().BeFalse(); + } + + [Test] + public void should_be_removable_and_should_allow_move_files_if_max_ratio_reached_and_paused() + { + GivenGlobalSeedLimits(1.0); + PrepareClientToReturnCompletedItem(true, ratio: 1.0); + + var item = Subject.GetItems().Single(); + item.CanBeRemoved.Should().BeTrue(); + item.CanMoveFiles.Should().BeTrue(); + } + + [Test] + public void should_be_removable_and_should_allow_move_files_if_overridden_max_ratio_reached_and_paused() + { + GivenGlobalSeedLimits(2.0); + PrepareClientToReturnCompletedItem(true, ratio: 1.0, ratioLimit: 0.8); + + var item = Subject.GetItems().Single(); + item.CanBeRemoved.Should().BeTrue(); + item.CanMoveFiles.Should().BeTrue(); + } + + [Test] + public void should_not_be_removable_if_overridden_max_ratio_not_reached_and_paused() + { + GivenGlobalSeedLimits(0.2); + PrepareClientToReturnCompletedItem(true, ratio: 0.5, ratioLimit: 0.8); + + var item = Subject.GetItems().Single(); + item.CanBeRemoved.Should().BeFalse(); + item.CanMoveFiles.Should().BeFalse(); + } + + + [Test] + public void should_not_be_removable_and_should_not_allow_move_files_if_max_idletime_reached_and_not_paused() + { + GivenGlobalSeedLimits(null, 20); + PrepareClientToReturnCompletedItem(false, ratio: 2.0, seedingTime: 30); + + var item = Subject.GetItems().Single(); + item.CanBeRemoved.Should().BeFalse(); + item.CanMoveFiles.Should().BeFalse(); + } + + [Test] + public void should_be_removable_and_should_allow_move_files_if_max_idletime_reached_and_paused() + { + GivenGlobalSeedLimits(null, 20); + PrepareClientToReturnCompletedItem(true, ratio: 2.0, seedingTime: 20); + + var item = Subject.GetItems().Single(); + item.CanBeRemoved.Should().BeTrue(); + item.CanMoveFiles.Should().BeTrue(); + } + + [Test] + public void should_be_removable_and_should_allow_move_files_if_overridden_max_idletime_reached_and_paused() + { + GivenGlobalSeedLimits(null, 40); + PrepareClientToReturnCompletedItem(true, ratio: 2.0, seedingTime: 20, idleLimit: 10); + + var item = Subject.GetItems().Single(); + item.CanBeRemoved.Should().BeTrue(); + item.CanMoveFiles.Should().BeTrue(); + } + + [Test] + public void should_be_removable_and_should_not_allow_move_files_if_overridden_max_idletime_reached_and_not_paused() + { + GivenGlobalSeedLimits(null, 40); + PrepareClientToReturnCompletedItem(false, ratio: 2.0, seedingTime: 20, idleLimit: 10); + + var item = Subject.GetItems().Single(); + item.CanBeRemoved.Should().BeTrue(); + item.CanMoveFiles.Should().BeFalse(); + } + + [Test] + public void should_not_be_removable_if_overridden_max_idletime_not_reached_and_paused() + { + GivenGlobalSeedLimits(null, 20); + PrepareClientToReturnCompletedItem(true, ratio: 2.0, seedingTime: 30, idleLimit: 40); + + var item = Subject.GetItems().Single(); + item.CanBeRemoved.Should().BeFalse(); + item.CanMoveFiles.Should().BeFalse(); + } + + [Test] + public void should_not_be_removable_if_max_idletime_reached_but_ratio_not_and_not_paused() + { + GivenGlobalSeedLimits(2.0, 20); + PrepareClientToReturnCompletedItem(false, ratio: 1.0, seedingTime: 30); + + var item = Subject.GetItems().Single(); + item.CanBeRemoved.Should().BeFalse(); + item.CanMoveFiles.Should().BeFalse(); + } + + [Test] + public void should_be_removable_and_should_allow_move_files_if_max_idletime_configured_and_paused() + { + GivenGlobalSeedLimits(2.0, 20); + PrepareClientToReturnCompletedItem(true, ratio: 1.0, seedingTime: 30); + + var item = Subject.GetItems().Single(); + item.CanBeRemoved.Should().BeTrue(); + item.CanMoveFiles.Should().BeTrue(); + } } } diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/TransmissionTests/TransmissionFixtureBase.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/TransmissionTests/TransmissionFixtureBase.cs index d46f9a30e..5dded73ea 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/TransmissionTests/TransmissionFixtureBase.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/TransmissionTests/TransmissionFixtureBase.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Moq; using NUnit.Framework; using NzbDrone.Common.Http; +using NzbDrone.Common.Serializer; using NzbDrone.Core.Download; using NzbDrone.Core.Download.Clients.Transmission; using NzbDrone.Core.MediaFiles.TorrentInfo; @@ -72,11 +73,13 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.TransmissionTests { HashString = "HASH", IsFinished = true, - Status = TransmissionTorrentStatus.Stopped, + Status = TransmissionTorrentStatus.Seeding, Name = _title, TotalSize = 1000, LeftUntilDone = 0, - DownloadDir = "somepath" + DownloadDir = "somepath", + DownloadedEver = 1000, + UploadedEver = 900 }; _magnet = new TransmissionTorrent @@ -106,7 +109,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.TransmissionTests Mocker.GetMock() .Setup(v => v.GetConfig(It.IsAny())) - .Returns(_transmissionConfigItems); + .Returns(() => Json.Deserialize(_transmissionConfigItems.ToJson())); } @@ -178,8 +181,40 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.TransmissionTests }); } - protected void PrepareClientToReturnCompletedItem() + protected void PrepareClientToReturnCompletedItem(bool stopped = false, double ratio = 0.9, int seedingTime = 60, double? ratioLimit = null, int? idleLimit = null) { + if (stopped) + _completed.Status = TransmissionTorrentStatus.Stopped; + _completed.UploadedEver = (int)(_completed.DownloadedEver * ratio); + _completed.SecondsSeeding = seedingTime * 60; + + if (ratioLimit.HasValue) + { + if (double.IsPositiveInfinity(ratioLimit.Value)) + { + _completed.SeedRatioMode = 2; + } + else + { + _completed.SeedRatioMode = 1; + _completed.SeedRatioLimit = ratioLimit.Value; + } + } + + if (idleLimit.HasValue) + { + if (double.IsPositiveInfinity(idleLimit.Value)) + { + _completed.SeedIdleMode = 2; + } + else + { + _completed.SeedIdleMode = 1; + _completed.SeedIdleLimit = idleLimit.Value; + } + } + + GivenTorrents(new List { _completed @@ -193,5 +228,20 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.TransmissionTests _magnet }); } + + protected void GivenGlobalSeedLimits(double? ratioLimit = null, int? idleLimit = null) + { + _transmissionConfigItems["seedRatioLimited"] = ratioLimit.HasValue; + if (ratioLimit.HasValue) + { + _transmissionConfigItems["seedRatioLimit"] = ratioLimit.Value; + } + + _transmissionConfigItems["idle-seeding-limit-enabled"] = idleLimit.HasValue; + if (idleLimit.HasValue) + { + _transmissionConfigItems["idle-seeding-limit"] = idleLimit.Value; + } + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/VuzeTests/VuzeFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/VuzeTests/VuzeFixture.cs index 406e70e15..91f9e701a 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/VuzeTests/VuzeFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/VuzeTests/VuzeFixture.cs @@ -47,7 +47,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.VuzeTests [Test] public void completed_download_should_have_required_properties() { - PrepareClientToReturnCompletedItem(); + PrepareClientToReturnCompletedItem(true, ratioLimit: 0.5); var item = Subject.GetItems().Single(); VerifyCompleted(item); @@ -184,7 +184,7 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.VuzeTests item.Status.Should().Be(expectedItemStatus); } - [TestCase(TransmissionTorrentStatus.Stopped, DownloadItemStatus.Completed, true)] + [TestCase(TransmissionTorrentStatus.Stopped, DownloadItemStatus.Completed, false)] [TestCase(TransmissionTorrentStatus.CheckWait, DownloadItemStatus.Downloading, false)] [TestCase(TransmissionTorrentStatus.Check, DownloadItemStatus.Downloading, false)] [TestCase(TransmissionTorrentStatus.Queued, DownloadItemStatus.Queued, false)] diff --git a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionBase.cs b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionBase.cs index e583edaa7..c1fd61180 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionBase.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionBase.cs @@ -32,6 +32,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission public override IEnumerable GetItems() { + var configFunc = new Lazy(() => _proxy.GetConfig(Settings)); var torrents = _proxy.GetTorrents(Settings); var items = new List(); @@ -98,9 +99,8 @@ namespace NzbDrone.Core.Download.Clients.Transmission item.Status = DownloadItemStatus.Downloading; } - item.CanMoveFiles = item.CanBeRemoved = - torrent.Status == TransmissionTorrentStatus.Stopped && - item.SeedRatio >= torrent.SeedRatioLimit; + item.CanBeRemoved = HasReachedSeedLimit(torrent, item.SeedRatio, configFunc); + item.CanMoveFiles = item.CanBeRemoved && torrent.Status == TransmissionTorrentStatus.Stopped; items.Add(item); } @@ -108,6 +108,46 @@ namespace NzbDrone.Core.Download.Clients.Transmission return items; } + protected bool HasReachedSeedLimit(TransmissionTorrent torrent, double? ratio, Lazy config) + { + var isStopped = torrent.Status == TransmissionTorrentStatus.Stopped; + var isSeeding = torrent.Status == TransmissionTorrentStatus.Seeding; + + if (torrent.SeedRatioMode == 1) + { + if (isStopped && ratio.HasValue && ratio >= torrent.SeedRatioLimit) + { + return true; + } + } + else if (torrent.SeedRatioMode == 0) + { + if (isStopped && config.Value.SeedRatioLimited && ratio >= config.Value.SeedRatioLimit) + { + return true; + } + } + + // Transmission doesn't support SeedTimeLimit, use/abuse seed idle limit, but only if it was set per-torrent. + if (torrent.SeedIdleMode == 1) + { + if ((isStopped || isSeeding) && torrent.SecondsSeeding > torrent.SeedIdleLimit * 60) + { + return true; + } + } + else if (torrent.SeedIdleMode == 0) + { + // The global idle limit is a real idle limit, if it's configured then 'Stopped' is enough. + if (isStopped && config.Value.IdleSeedingLimitEnabled) + { + return true; + } + } + + return false; + } + public override void RemoveItem(string downloadId, bool deleteData) { _proxy.RemoveTorrent(downloadId.ToLower(), deleteData, Settings); @@ -116,7 +156,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission public override DownloadClientInfo GetStatus() { var config = _proxy.GetConfig(Settings); - var destDir = config.GetValueOrDefault("download-dir") as string; + var destDir = config.DownloadDir; if (Settings.TvCategory.IsNotNullOrWhiteSpace()) { @@ -184,7 +224,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission if (!Settings.TvCategory.IsNotNullOrWhiteSpace()) return null; var config = _proxy.GetConfig(Settings); - var destDir = (string)config.GetValueOrDefault("download-dir"); + var destDir = config.DownloadDir; return $"{destDir.TrimEnd('/')}/{Settings.TvCategory}"; } diff --git a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionConfig.cs b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionConfig.cs new file mode 100644 index 000000000..194df1e5e --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionConfig.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +namespace NzbDrone.Core.Download.Clients.Transmission +{ + public class TransmissionConfig + { + [JsonProperty("rpc-version")] + public string RpcVersion { get; set; } + public string Version { get; set; } + + [JsonProperty("download-dir")] + public string DownloadDir { get; set; } + + public double SeedRatioLimit { get; set; } + public bool SeedRatioLimited { get; set; } + + [JsonProperty("idle-seeding-limit")] + public long IdleSeedingLimit { get; set; } + [JsonProperty("idle-seeding-limit-enabled")] + public bool IdleSeedingLimitEnabled { get; set; } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionProxy.cs b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionProxy.cs index a461a5fa9..393ecf914 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionProxy.cs @@ -16,7 +16,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission void AddTorrentFromUrl(string torrentUrl, string downloadDirectory, TransmissionSettings settings); void AddTorrentFromData(byte[] torrentData, string downloadDirectory, TransmissionSettings settings); void SetTorrentSeedingConfiguration(string hash, TorrentSeedConfiguration seedConfiguration, TransmissionSettings settings); - Dictionary GetConfig(TransmissionSettings settings); + TransmissionConfig GetConfig(TransmissionSettings settings); string GetProtocolVersion(TransmissionSettings settings); string GetClientVersion(TransmissionSettings settings); void RemoveTorrent(string hash, bool removeData, TransmissionSettings settings); @@ -101,26 +101,22 @@ namespace NzbDrone.Core.Download.Clients.Transmission { var config = GetConfig(settings); - var version = config["rpc-version"]; - - return version.ToString(); + return config.RpcVersion; } public string GetClientVersion(TransmissionSettings settings) { var config = GetConfig(settings); - var version = config["version"]; - - return version.ToString(); + return config.Version; } - public Dictionary GetConfig(TransmissionSettings settings) + public TransmissionConfig GetConfig(TransmissionSettings settings) { // Gets the transmission version. var result = GetSessionVariables(settings); - return result.Arguments; + return Json.Deserialize(result.Arguments.ToJson()); } public void RemoveTorrent(string hashString, bool removeData, TransmissionSettings settings) @@ -164,15 +160,20 @@ namespace NzbDrone.Core.Download.Clients.Transmission "hashString", // Unique torrent ID. Use this instead of the client id? "name", "downloadDir", - "status", "totalSize", "leftUntilDone", "isFinished", "eta", + "status", + "secondsDownloading", + "secondsSeeding", "errorString", "uploadedEver", "downloadedEver", "seedRatioLimit", + "seedRatioMode", + "seedIdleLimit", + "seedIdleMode", "fileCount" }; diff --git a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionTorrent.cs b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionTorrent.cs index 377cc01f2..3abb5d4e8 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionTorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/TransmissionTorrent.cs @@ -12,10 +12,14 @@ public int Eta { get; set; } public TransmissionTorrentStatus Status { get; set; } public int SecondsDownloading { get; set; } + public int SecondsSeeding { get; set; } public string ErrorString { get; set; } public long DownloadedEver { get; set; } public long UploadedEver { get; set; } - public long SeedRatioLimit { get; set; } + public double SeedRatioLimit { get; set; } + public int SeedRatioMode { get; set; } + public long SeedIdleLimit { get; set; } + public int SeedIdleMode { get; set; } public int FileCount { get; set; } } } diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index b71e5e1f1..458de42e0 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -151,6 +151,7 @@ +