From 411be4d0116f0739bb9c71235312d0c5a26dd3a2 Mon Sep 17 00:00:00 2001 From: leaty Date: Thu, 19 Mar 2020 15:47:25 +0100 Subject: [PATCH] New: Removing rtorrent downloads when seeding criteria have been met --- .../Download/Clients/rTorrent/RTorrent.cs | 32 ++++++- .../Clients/rTorrent/RTorrentProxy.cs | 20 ++++- .../Clients/rTorrent/RTorrentTorrent.cs | 1 + .../Download/DownloadSeedConfigProvider.cs | 84 +++++++++++++++++++ .../Indexers/SeedConfigProvider.cs | 60 ++++++++----- 5 files changed, 172 insertions(+), 25 deletions(-) create mode 100644 src/NzbDrone.Core/Download/DownloadSeedConfigProvider.cs diff --git a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs index 2d13b251e..a9a2594b4 100644 --- a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs @@ -4,6 +4,7 @@ using System.Threading; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; +using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; using NzbDrone.Core.MediaFiles.TorrentInfo; @@ -22,6 +23,8 @@ public class RTorrent : TorrentClientBase { private readonly IRTorrentProxy _proxy; private readonly IRTorrentDirectoryValidator _rTorrentDirectoryValidator; + private readonly IDownloadSeedConfigProvider _downloadSeedConfigProvider; + private readonly string _imported_view = String.Concat(BuildInfo.AppName.ToLower(), "_imported"); public RTorrent(IRTorrentProxy proxy, ITorrentFileInfoReader torrentFileInfoReader, @@ -29,17 +32,19 @@ public RTorrent(IRTorrentProxy proxy, IConfigService configService, IDiskProvider diskProvider, IRemotePathMappingService remotePathMappingService, + IDownloadSeedConfigProvider downloadSeedConfigProvider, IRTorrentDirectoryValidator rTorrentDirectoryValidator, Logger logger) : base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, logger) { _proxy = proxy; _rTorrentDirectoryValidator = rTorrentDirectoryValidator; + _downloadSeedConfigProvider = downloadSeedConfigProvider; } public override void MarkItemAsImported(DownloadClientItem downloadClientItem) { - // set post-import category + // Set post-import label if (Settings.TvImportedCategory.IsNotNullOrWhiteSpace() && Settings.TvImportedCategory != Settings.TvCategory) { @@ -53,6 +58,17 @@ public override void MarkItemAsImported(DownloadClientItem downloadClientItem) Settings.TvImportedCategory, downloadClientItem.Title); } } + + // Set post-import view + try + { + _proxy.PushTorrentUniqueView(downloadClientItem.DownloadId.ToLower(), _imported_view, Settings); + } + catch (Exception ex) + { + _logger.Warn(ex, "Failed to set torrent post-import view \"{0}\" for {1} in rTorrent.", + _imported_view, downloadClientItem.Title); + } } protected override string AddFromMagnetLink(RemoteEpisode remoteEpisode, string hash, string magnetLink) @@ -95,7 +111,7 @@ protected override string AddFromTorrentFile(RemoteEpisode remoteEpisode, string public override string Name => "rTorrent"; - public override ProviderMessage Message => new ProviderMessage("Sonarr is unable to remove torrents that have finished seeding when using rTorrent", ProviderMessageType.Warning); + public override ProviderMessage Message => new ProviderMessage($"Sonarr will handle automatic removal of torrents based on the current seed criteria in Settings->Indexers. After importing it will also set \"{_imported_view}\" as an rTorrent view, which can be used in rTorrent scripts to customize behavior.", ProviderMessageType.Info); public override IEnumerable GetItems() { @@ -147,8 +163,16 @@ public override IEnumerable GetItems() item.Status = DownloadItemStatus.Paused; } - // No stop ratio data is present, so do not delete - item.CanMoveFiles = item.CanBeRemoved = false; + // Grab cached seedConfig + var seedConfig = _downloadSeedConfigProvider.GetSeedConfiguration(torrent.Hash); + + // Check if torrent is finished and if it exceeds cached seedConfig + item.CanMoveFiles = item.CanBeRemoved = + torrent.IsFinished && + ( + (torrent.Ratio / 1000.0) >= seedConfig.Ratio || + (DateTimeOffset.Now - DateTimeOffset.FromUnixTimeSeconds(torrent.FinishedTime)) >= seedConfig.SeedTime + ); items.Add(item); } diff --git a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentProxy.cs b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentProxy.cs index 00b03ebf5..91721f213 100644 --- a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentProxy.cs @@ -20,6 +20,7 @@ public interface IRTorrentProxy void RemoveTorrent(string hash, RTorrentSettings settings); void SetTorrentLabel(string hash, string label, RTorrentSettings settings); bool HasHashTorrent(string hash, RTorrentSettings settings); + void PushTorrentUniqueView(string hash, string view, RTorrentSettings settings); } public interface IRTorrent : IXmlRpcProxy @@ -48,6 +49,9 @@ public interface IRTorrent : IXmlRpcProxy [XmlRpcMethod("d.custom1.set")] string SetLabel(string hash, string label); + [XmlRpcMethod("d.views.push_back_unique")] + int PushUniqueView(string hash, string view); + [XmlRpcMethod("system.client_version")] string GetVersion(); } @@ -87,7 +91,8 @@ public List GetTorrents(RTorrentSettings settings) "d.ratio=", // long "d.is_open=", // long "d.is_active=", // long - "d.complete=") //long + "d.complete=", // long + "d.timestamp.finished=") // long (unix timestamp) ); var items = new List(); @@ -108,6 +113,7 @@ public List GetTorrents(RTorrentSettings settings) item.IsOpen = Convert.ToBoolean((long)torrent[8]); item.IsActive = Convert.ToBoolean((long)torrent[9]); item.IsFinished = Convert.ToBoolean((long)torrent[10]); + item.FinishedTime = (long)torrent[11]; items.Add(item); } @@ -174,6 +180,18 @@ public void SetTorrentLabel(string hash, string label, RTorrentSettings settings } } + public void PushTorrentUniqueView(string hash, string view, RTorrentSettings settings) + { + _logger.Debug("Executing remote method: d.views.push_back_unique"); + + var client = BuildClient(settings); + var response = ExecuteRequest(() => client.PushUniqueView(hash, view)); + if (response != 0) + { + throw new DownloadClientException("Could not push unique view {0} for torrent: {1}.", view, hash); + } + } + public void RemoveTorrent(string hash, RTorrentSettings settings) { _logger.Debug("Executing remote method: d.erase"); diff --git a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentTorrent.cs b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentTorrent.cs index d00df188f..14cd0b346 100644 --- a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentTorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentTorrent.cs @@ -10,6 +10,7 @@ public class RTorrentTorrent public long RemainingSize { get; set; } public long DownRate { get; set; } public long Ratio { get; set; } + public long FinishedTime { get; set; } public bool IsFinished { get; set; } public bool IsOpen { get; set; } public bool IsActive { get; set; } diff --git a/src/NzbDrone.Core/Download/DownloadSeedConfigProvider.cs b/src/NzbDrone.Core/Download/DownloadSeedConfigProvider.cs new file mode 100644 index 000000000..4c6513a42 --- /dev/null +++ b/src/NzbDrone.Core/Download/DownloadSeedConfigProvider.cs @@ -0,0 +1,84 @@ +using System; +using NLog; +using NzbDrone.Common.Cache; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Download.Clients; +using NzbDrone.Core.Download.History; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.Download +{ + public interface IDownloadSeedConfigProvider + { + TorrentSeedConfiguration GetSeedConfiguration(string infoHash); + } + + public class DownloadSeedConfigProvider : IDownloadSeedConfigProvider + { + private readonly Logger _logger; + private readonly ISeedConfigProvider _indexerSeedConfigProvider; + private readonly IDownloadHistoryService _downloadHistoryService; + + class CachedSeedConfiguration + { + public int IndexerId { get; set; } + public bool FullSeason { get; set; } + } + + private readonly ICached _cacheDownloads; + + public DownloadSeedConfigProvider(IDownloadHistoryService downloadHistoryService, ISeedConfigProvider indexerSeedConfigProvider, ICacheManager cacheManager, Logger logger) + { + _logger = logger; + _indexerSeedConfigProvider = indexerSeedConfigProvider; + _downloadHistoryService = downloadHistoryService; + + _cacheDownloads = cacheManager.GetRollingCache(GetType(), "indexerByHash", TimeSpan.FromHours(1)); + } + + public TorrentSeedConfiguration GetSeedConfiguration(string infoHash) + { + if (infoHash.IsNullOrWhiteSpace()) return null; + + infoHash = infoHash.ToUpper(); + + var cachedConfig = _cacheDownloads.Get(infoHash, () => FetchIndexer(infoHash)); + + if (cachedConfig == null) return null; + + var seedConfig = _indexerSeedConfigProvider.GetSeedConfiguration(cachedConfig.IndexerId, cachedConfig.FullSeason); + + return seedConfig; + } + + private CachedSeedConfiguration FetchIndexer(string infoHash) + { + var historyItem = _downloadHistoryService.GetLatestGrab(infoHash); + + if (historyItem == null) + { + _logger.Debug("No download history item for infohash {0}, unable to provide seed configuration", infoHash); + return null; + } + + ParsedEpisodeInfo parsedEpisodeInfo = null; + if (historyItem.Release != null) + { + parsedEpisodeInfo = Parser.Parser.ParseTitle(historyItem.Release.Title); + } + + if (parsedEpisodeInfo == null) + { + _logger.Debug("No parsed title in download history item for infohash {0}, unable to provide seed configuration", infoHash); + return null; + } + + return new CachedSeedConfiguration + { + IndexerId = historyItem.IndexerId, + FullSeason = parsedEpisodeInfo.FullSeason + }; + } + } +} diff --git a/src/NzbDrone.Core/Indexers/SeedConfigProvider.cs b/src/NzbDrone.Core/Indexers/SeedConfigProvider.cs index ff07ec2b3..e99659a4e 100644 --- a/src/NzbDrone.Core/Indexers/SeedConfigProvider.cs +++ b/src/NzbDrone.Core/Indexers/SeedConfigProvider.cs @@ -1,9 +1,10 @@ using System; using System.Linq; using NzbDrone.Common.Extensions; +using NzbDrone.Common.Cache; using NzbDrone.Core.Datastore; using NzbDrone.Core.Download.Clients; -using NzbDrone.Core.Indexers; +using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.Indexers @@ -11,15 +12,18 @@ namespace NzbDrone.Core.Indexers public interface ISeedConfigProvider { TorrentSeedConfiguration GetSeedConfiguration(RemoteEpisode release); + TorrentSeedConfiguration GetSeedConfiguration(int indexerId, bool fullSeason); } - public class SeedConfigProvider : ISeedConfigProvider + public class SeedConfigProvider : ISeedConfigProvider, IHandle { private readonly IIndexerFactory _indexerFactory; + private readonly ICached _cache; - public SeedConfigProvider(IIndexerFactory indexerFactory) + public SeedConfigProvider(IIndexerFactory indexerFactory, ICacheManager cacheManager) { _indexerFactory = indexerFactory; + _cache = cacheManager.GetRollingCache(GetType(), "criteriaByIndexer", TimeSpan.FromHours(1)); } public TorrentSeedConfiguration GetSeedConfiguration(RemoteEpisode remoteEpisode) @@ -27,33 +31,49 @@ public TorrentSeedConfiguration GetSeedConfiguration(RemoteEpisode remoteEpisode if (remoteEpisode.Release.DownloadProtocol != DownloadProtocol.Torrent) return null; if (remoteEpisode.Release.IndexerId == 0) return null; + return GetSeedConfiguration(remoteEpisode.Release.IndexerId, remoteEpisode.ParsedEpisodeInfo.FullSeason); + } + + public TorrentSeedConfiguration GetSeedConfiguration(int indexerId, bool fullSeason) + { + if (indexerId == 0) return null; + + var seedCriteria = _cache.Get(indexerId.ToString(), () => FetchSeedCriteria(indexerId)); + + if (seedCriteria == null) return null; + + var seedConfig = new TorrentSeedConfiguration + { + Ratio = seedCriteria.SeedRatio + }; + + var seedTime = fullSeason ? seedCriteria.SeasonPackSeedTime : seedCriteria.SeedTime; + if (seedTime.HasValue) + { + seedConfig.SeedTime = TimeSpan.FromMinutes(seedTime.Value); + } + + return seedConfig; + } + + private SeedCriteriaSettings FetchSeedCriteria(int indexerId) + { try { - var indexer = _indexerFactory.Get(remoteEpisode.Release.IndexerId); + var indexer = _indexerFactory.Get(indexerId); var torrentIndexerSettings = indexer.Settings as ITorrentIndexerSettings; - if (torrentIndexerSettings != null && torrentIndexerSettings.SeedCriteria != null) - { - var seedConfig = new TorrentSeedConfiguration - { - Ratio = torrentIndexerSettings.SeedCriteria.SeedRatio - }; - - var seedTime = remoteEpisode.ParsedEpisodeInfo.FullSeason ? torrentIndexerSettings.SeedCriteria.SeasonPackSeedTime : torrentIndexerSettings.SeedCriteria.SeedTime; - if (seedTime.HasValue) - { - seedConfig.SeedTime = TimeSpan.FromMinutes(seedTime.Value); - } - - return seedConfig; - } + return torrentIndexerSettings?.SeedCriteria; } catch (ModelNotFoundException) { return null; } + } - return null; + public void Handle(IndexerSettingUpdatedEvent message) + { + _cache.Clear(); } } }