From 1d8b711edaa094fb165a90b43f4d9d3534481fa4 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 #4648 --- .../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 f8d9a160e..d02e1b15d 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; @@ -232,6 +233,33 @@ namespace NzbDrone.Common.Extensions 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 GetAppDataPath(this IAppFolderInfo appFolderInfo) { return appFolderInfo.AppDataFolder; diff --git a/src/NzbDrone.Core/Download/Clients/Aria2/Aria2.cs b/src/NzbDrone.Core/Download/Clients/Aria2/Aria2.cs index 1ebcb0437..fb8ae5f21 100644 --- a/src/NzbDrone.Core/Download/Clients/Aria2/Aria2.cs +++ b/src/NzbDrone.Core/Download/Clients/Aria2/Aria2.cs @@ -83,12 +83,12 @@ namespace NzbDrone.Core.Download.Clients.Aria2 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")) @@ -99,42 +99,52 @@ namespace NzbDrone.Core.Download.Clients.Aria2 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, }; @@ -143,7 +153,7 @@ namespace NzbDrone.Core.Download.Clients.Aria2 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); @@ -155,10 +165,23 @@ namespace NzbDrone.Core.Download.Clients.Aria2 _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) @@ -230,5 +253,15 @@ namespace NzbDrone.Core.Download.Clients.Aria2 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 f2fed61dc..e67e5062b 100644 --- a/src/NzbDrone.Core/Download/Clients/Aria2/Aria2Proxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Aria2/Aria2Proxy.cs @@ -13,8 +13,9 @@ namespace NzbDrone.Core.Download.Clients.Aria2 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); } @@ -31,6 +32,9 @@ namespace NzbDrone.Core.Download.Clients.Aria2 [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 @@ namespace NzbDrone.Core.Download.Clients.Aria2 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,68 +73,68 @@ namespace NzbDrone.Core.Download.Clients.Aria2 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(); @@ -144,39 +148,51 @@ namespace NzbDrone.Core.Download.Clients.Aria2 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) {