From c8c9db1452b6599153aa39539f0e701ac2e93f1e Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 3 Sep 2021 21:41:52 -0700 Subject: [PATCH] Aria2 fixes Fixed: Removing completed downloads from Aria2 Fixed: Return correct path for Aria2 downloads in a job folder Fixed: Seeding torrents in Aria2 are treated as finished downloading Closes Sonarr issue 4648 (cherry picked from commit 1d8b711edaa094fb165a90b43f4d9d3534481fa4) --- .../Extensions/PathExtensions.cs | 28 +++++++ .../Download/Clients/Aria2/Aria2.cs | 73 ++++++++++++----- .../Download/Clients/Aria2/Aria2Proxy.cs | 78 +++++++++++-------- 3 files changed, 128 insertions(+), 51 deletions(-) diff --git a/src/NzbDrone.Common/Extensions/PathExtensions.cs b/src/NzbDrone.Common/Extensions/PathExtensions.cs index 335a0502e..17b82d4f9 100644 --- a/src/NzbDrone.Common/Extensions/PathExtensions.cs +++ b/src/NzbDrone.Common/Extensions/PathExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text.RegularExpressions; using NzbDrone.Common.Disk; using NzbDrone.Common.EnsureThat; @@ -239,6 +240,33 @@ public static string GetAncestorPath(this string path, string ancestorName) return null; } + public static string GetLongestCommonPath(this List paths) + { + var firstPath = paths.First(); + var length = firstPath.Length; + + for (int i = 1; i < paths.Count; i++) + { + var path = paths[i]; + + length = Math.Min(length, path.Length); + + for (int characterIndex = 0; characterIndex < length; characterIndex++) + { + if (path[characterIndex] != firstPath[characterIndex]) + { + length = characterIndex; + break; + } + } + } + + var substring = firstPath.Substring(0, length); + var lastSeparatorIndex = substring.LastIndexOfAny(new[] { '/', '\\' }); + + return substring.Substring(0, lastSeparatorIndex); + } + public static string ProcessNameToExe(this string processName, PlatformType runtime) { if (OsInfo.IsWindows || runtime != PlatformType.NetCore) diff --git a/src/NzbDrone.Core/Download/Clients/Aria2/Aria2.cs b/src/NzbDrone.Core/Download/Clients/Aria2/Aria2.cs index 7d914d566..636f0aaa8 100644 --- a/src/NzbDrone.Core/Download/Clients/Aria2/Aria2.cs +++ b/src/NzbDrone.Core/Download/Clients/Aria2/Aria2.cs @@ -85,12 +85,12 @@ public override IEnumerable GetItems() continue; } - long completedLength = long.Parse(torrent.CompletedLength); - long totalLength = long.Parse(torrent.TotalLength); - long uploadedLength = long.Parse(torrent.UploadLength); - long downloadSpeed = long.Parse(torrent.DownloadSpeed); + var completedLength = long.Parse(torrent.CompletedLength); + var totalLength = long.Parse(torrent.TotalLength); + var uploadedLength = long.Parse(torrent.UploadLength); + var downloadSpeed = long.Parse(torrent.DownloadSpeed); - var sta = DownloadItemStatus.Failed; + var status = DownloadItemStatus.Failed; var title = ""; if (torrent.Bittorrent?.ContainsKey("info") == true && ((XmlRpcStruct)torrent.Bittorrent["info"]).ContainsKey("name")) @@ -101,42 +101,52 @@ public override IEnumerable GetItems() switch (torrent.Status) { case "active": - sta = DownloadItemStatus.Downloading; + if (completedLength == totalLength) + { + status = DownloadItemStatus.Completed; + } + else + { + status = DownloadItemStatus.Downloading; + } + break; case "waiting": - sta = DownloadItemStatus.Queued; + status = DownloadItemStatus.Queued; break; case "paused": - sta = DownloadItemStatus.Paused; + status = DownloadItemStatus.Paused; break; case "error": - sta = DownloadItemStatus.Failed; + status = DownloadItemStatus.Failed; break; case "complete": - sta = DownloadItemStatus.Completed; + status = DownloadItemStatus.Completed; break; case "removed": - sta = DownloadItemStatus.Failed; + status = DownloadItemStatus.Failed; break; } - _logger.Debug($"- aria2 getstatus hash:'{torrent.InfoHash}' gid:'{torrent.Gid}' sta:'{sta}' tot:{totalLength} comp:'{completedLength}'"); + _logger.Trace($"- aria2 getstatus hash:'{torrent.InfoHash}' gid:'{torrent.Gid}' status:'{status}' total:{totalLength} completed:'{completedLength}'"); - yield return new DownloadClientItem() + var outputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(GetOutputPath(torrent))); + + yield return new DownloadClientItem { CanMoveFiles = false, - CanBeRemoved = true, + CanBeRemoved = torrent.Status == "complete", Category = null, DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this), DownloadId = torrent.InfoHash?.ToUpper(), IsEncrypted = false, Message = torrent.ErrorMessage, - OutputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(torrent.Dir)), + OutputPath = outputPath, RemainingSize = totalLength - completedLength, RemainingTime = downloadSpeed == 0 ? (TimeSpan?)null : new TimeSpan(0, 0, (int)((totalLength - completedLength) / downloadSpeed)), Removed = torrent.Status == "removed", SeedRatio = totalLength > 0 ? (double)uploadedLength / totalLength : 0, - Status = sta, + Status = status, Title = title, TotalSize = totalLength, }; @@ -145,7 +155,7 @@ public override IEnumerable GetItems() public override void RemoveItem(DownloadClientItem item, bool deleteData) { - //Aria2 doesn't support file deletion: https://github.com/aria2/aria2/issues/728 + // Aria2 doesn't support file deletion: https://github.com/aria2/aria2/issues/728 var hash = item.DownloadId.ToLower(); var aria2Item = _proxy.GetTorrents(Settings).FirstOrDefault(t => t.InfoHash?.ToLower() == hash); @@ -157,10 +167,23 @@ public override void RemoveItem(DownloadClientItem item, bool deleteData) _logger.Debug($"Aria2 removing hash:'{hash}' gid:'{aria2Item.Gid}'"); - if (!_proxy.RemoveTorrent(Settings, aria2Item.Gid)) + if (aria2Item.Status == "complete" || aria2Item.Status == "error" || aria2Item.Status == "removed") { - _logger.Error($"Aria2 error while deleting {hash}."); - return; + if (!_proxy.RemoveCompletedTorrent(Settings, aria2Item.Gid)) + { + _logger.Error($"Aria2 error while deleting {hash}."); + + return; + } + } + else + { + if (!_proxy.RemoveTorrent(Settings, aria2Item.Gid)) + { + _logger.Error($"Aria2 error while deleting {hash}."); + + return; + } } if (deleteData) @@ -232,5 +255,15 @@ private ValidationFailure TestConnection() return null; } + + private string GetOutputPath(Aria2Status torrent) + { + if (torrent.Files.Length == 1) + { + return torrent.Files.First().Path; + } + + return torrent.Files.Select(f => f.Path).ToList().GetLongestCommonPath(); + } } } diff --git a/src/NzbDrone.Core/Download/Clients/Aria2/Aria2Proxy.cs b/src/NzbDrone.Core/Download/Clients/Aria2/Aria2Proxy.cs index bbcb87a5c..3e7b5a6be 100644 --- a/src/NzbDrone.Core/Download/Clients/Aria2/Aria2Proxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Aria2/Aria2Proxy.cs @@ -13,8 +13,9 @@ public interface IAria2Proxy string AddMagnet(Aria2Settings settings, string magnet); string AddTorrent(Aria2Settings settings, byte[] torrent); bool RemoveTorrent(Aria2Settings settings, string gid); + bool RemoveCompletedTorrent(Aria2Settings settings, string gid); Dictionary GetGlobals(Aria2Settings settings); - Aria2Status[] GetTorrents(Aria2Settings settings); + List GetTorrents(Aria2Settings settings); Aria2Status GetFromGID(Aria2Settings settings, string gid); } @@ -32,6 +33,9 @@ public interface IAria2 : IXmlRpcProxy [XmlRpcMethod("aria2.forceRemove")] string Remove(string token, string gid); + [XmlRpcMethod("aria2.removeDownloadResult")] + string RemoveResult(string token, string gid); + [XmlRpcMethod("aria2.tellStatus")] Aria2Status GetFromGid(string token, string gid); @@ -39,13 +43,13 @@ public interface IAria2 : IXmlRpcProxy XmlRpcStruct GetGlobalOption(string token); [XmlRpcMethod("aria2.tellActive")] - Aria2Status[] GetActives(string token); + Aria2Status[] GetActive(string token); [XmlRpcMethod("aria2.tellWaiting")] - Aria2Status[] GetWaitings(string token, int offset, int num); + Aria2Status[] GetWaiting(string token, int offset, int num); [XmlRpcMethod("aria2.tellStopped")] - Aria2Status[] GetStoppeds(string token, int offset, int num); + Aria2Status[] GetStopped(string token, int offset, int num); } public class Aria2Proxy : IAria2Proxy @@ -69,67 +73,67 @@ private string GetURL(Aria2Settings settings) public string GetVersion(Aria2Settings settings) { - _logger.Debug("> aria2.getVersion"); + _logger.Trace("> aria2.getVersion"); var client = BuildClient(settings); var version = ExecuteRequest(() => client.GetVersion(GetToken(settings))); - _logger.Debug("< aria2.getVersion"); + _logger.Trace("< aria2.getVersion"); return version.Version; } public Aria2Status GetFromGID(Aria2Settings settings, string gid) { - _logger.Debug("> aria2.tellStatus"); + _logger.Trace("> aria2.tellStatus"); var client = BuildClient(settings); var found = ExecuteRequest(() => client.GetFromGid(GetToken(settings), gid)); - _logger.Debug("< aria2.tellStatus"); + _logger.Trace("< aria2.tellStatus"); return found; } - public Aria2Status[] GetTorrents(Aria2Settings settings) + public List GetTorrents(Aria2Settings settings) { - _logger.Debug("> aria2.tellActive"); + _logger.Trace("> aria2.tellActive"); var client = BuildClient(settings); - var actives = ExecuteRequest(() => client.GetActives(GetToken(settings))); + var active = ExecuteRequest(() => client.GetActive(GetToken(settings))); - _logger.Debug("< aria2.tellActive"); + _logger.Trace("< aria2.tellActive"); - _logger.Debug("> aria2.tellWaiting"); + _logger.Trace("> aria2.tellWaiting"); - var waitings = ExecuteRequest(() => client.GetWaitings(GetToken(settings), 1, 10 * 1024)); + var waiting = ExecuteRequest(() => client.GetWaiting(GetToken(settings), 0, 10 * 1024)); - _logger.Debug("< aria2.tellWaiting"); + _logger.Trace("< aria2.tellWaiting"); - _logger.Debug("> aria2.tellStopped"); + _logger.Trace("> aria2.tellStopped"); - var stoppeds = ExecuteRequest(() => client.GetStoppeds(GetToken(settings), 1, 10 * 1024)); + var stopped = ExecuteRequest(() => client.GetStopped(GetToken(settings), 0, 10 * 1024)); - _logger.Debug("< aria2.tellStopped"); + _logger.Trace("< aria2.tellStopped"); - var ret = new List(); + var items = new List(); - ret.AddRange(actives); - ret.AddRange(waitings); - ret.AddRange(stoppeds); + items.AddRange(active); + items.AddRange(waiting); + items.AddRange(stopped); - return ret.ToArray(); + return items; } public Dictionary GetGlobals(Aria2Settings settings) { - _logger.Debug("> aria2.getGlobalOption"); + _logger.Trace("> aria2.getGlobalOption"); var client = BuildClient(settings); var options = ExecuteRequest(() => client.GetGlobalOption(GetToken(settings))); - _logger.Debug("< aria2.getGlobalOption"); + _logger.Trace("< aria2.getGlobalOption"); var ret = new Dictionary(); @@ -143,40 +147,52 @@ public Dictionary GetGlobals(Aria2Settings settings) public string AddMagnet(Aria2Settings settings, string magnet) { - _logger.Debug("> aria2.addUri"); + _logger.Trace("> aria2.addUri"); var client = BuildClient(settings); var gid = ExecuteRequest(() => client.AddUri(GetToken(settings), new[] { magnet })); - _logger.Debug("< aria2.addUri"); + _logger.Trace("< aria2.addUri"); return gid; } public string AddTorrent(Aria2Settings settings, byte[] torrent) { - _logger.Debug("> aria2.addTorrent"); + _logger.Trace("> aria2.addTorrent"); var client = BuildClient(settings); var gid = ExecuteRequest(() => client.AddTorrent(GetToken(settings), torrent)); - _logger.Debug("< aria2.addTorrent"); + _logger.Trace("< aria2.addTorrent"); return gid; } public bool RemoveTorrent(Aria2Settings settings, string gid) { - _logger.Debug("> aria2.forceRemove"); + _logger.Trace("> aria2.forceRemove"); var client = BuildClient(settings); var gidres = ExecuteRequest(() => client.Remove(GetToken(settings), gid)); - _logger.Debug("< aria2.forceRemove"); + _logger.Trace("< aria2.forceRemove"); return gid == gidres; } + public bool RemoveCompletedTorrent(Aria2Settings settings, string gid) + { + _logger.Trace("> aria2.removeDownloadResult"); + + var client = BuildClient(settings); + var result = ExecuteRequest(() => client.RemoveResult(GetToken(settings), gid)); + + _logger.Trace("< aria2.removeDownloadResult"); + + return result == "OK"; + } + private IAria2 BuildClient(Aria2Settings settings) { var client = XmlRpcProxyGen.Create();