diff --git a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs index 058b88ef3..e2c61a2d0 100644 --- a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrent.cs @@ -5,6 +5,7 @@ using FluentValidation.Results; using NLog; using NzbDrone.Common.Disk; +using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; @@ -23,6 +24,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, @@ -31,19 +34,20 @@ public RTorrent(IRTorrentProxy proxy, INamingConfigService namingConfigService, IDiskProvider diskProvider, IRemotePathMappingService remotePathMappingService, + IDownloadSeedConfigProvider downloadSeedConfigProvider, IRTorrentDirectoryValidator rTorrentDirectoryValidator, Logger logger) : base(torrentFileInfoReader, httpClient, configService, namingConfigService, diskProvider, remotePathMappingService, logger) { _proxy = proxy; _rTorrentDirectoryValidator = rTorrentDirectoryValidator; + _downloadSeedConfigProvider = downloadSeedConfigProvider; } public override void MarkItemAsImported(DownloadClientItem downloadClientItem) { - // set post-import category - if (Settings.MovieImportedCategory.IsNotNullOrWhiteSpace() && - Settings.MovieImportedCategory != Settings.MovieCategory) + // Set post-import label + if (Settings.MovieImportedCategory.IsNotNullOrWhiteSpace() && Settings.MovieImportedCategory != Settings.MovieCategory) { try { @@ -51,12 +55,19 @@ public override void MarkItemAsImported(DownloadClientItem downloadClientItem) } catch (Exception ex) { - _logger.Warn(ex, - "Failed to set torrent post-import label \"{0}\" for {1} in rTorrent. Does the label exist?", - Settings.MovieImportedCategory, - downloadClientItem.Title); + _logger.Warn(ex, "Failed to set torrent post-import label \"{0}\" for {1} in rTorrent. Does the label exist?", Settings.MovieImportedCategory, 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(RemoteMovie remoteMovie, string hash, string magnetLink) @@ -99,7 +110,7 @@ protected override string AddFromTorrentFile(RemoteMovie remoteMovie, string has public override string Name => "rTorrent"; - public override ProviderMessage Message => new ProviderMessage("Radarr is unable to remove torrents that have finished seeding when using rTorrent", ProviderMessageType.Warning); + public override ProviderMessage Message => new ProviderMessage($"Radarr 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() { @@ -154,8 +165,15 @@ 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 && seedConfig != null && + ( + (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 c7f72952f..1e5191e54 100644 --- a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentProxy.cs @@ -18,6 +18,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 @@ -46,6 +47,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) _logger.Trace(ret.ToJson()); @@ -109,6 +114,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); } @@ -175,6 +181,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..aba0672f2 --- /dev/null +++ b/src/NzbDrone.Core/Download/DownloadSeedConfigProvider.cs @@ -0,0 +1,89 @@ +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; + + private class CachedSeedConfiguration + { + public int IndexerId { get; set; } + public bool Movie { 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); + + 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; + } + + ParsedMovieInfo parsedMovieInfo = null; + if (historyItem.Release != null) + { + parsedMovieInfo = Parser.Parser.ParseMovieTitle(historyItem.Release.Title); + } + + if (parsedMovieInfo == 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, + }; + } + } +} diff --git a/src/NzbDrone.Core/Indexers/SeedConfigProvider.cs b/src/NzbDrone.Core/Indexers/SeedConfigProvider.cs index e61116feb..52b188e8f 100644 --- a/src/NzbDrone.Core/Indexers/SeedConfigProvider.cs +++ b/src/NzbDrone.Core/Indexers/SeedConfigProvider.cs @@ -1,6 +1,8 @@ using System; +using NzbDrone.Common.Cache; using NzbDrone.Core.Datastore; using NzbDrone.Core.Download.Clients; +using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.Indexers @@ -8,15 +10,18 @@ namespace NzbDrone.Core.Indexers public interface ISeedConfigProvider { TorrentSeedConfiguration GetSeedConfiguration(RemoteMovie release); + TorrentSeedConfiguration GetSeedConfiguration(int indexerId); } - 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(RemoteMovie remoteMovie) @@ -31,33 +36,56 @@ public TorrentSeedConfiguration GetSeedConfiguration(RemoteMovie remoteMovie) return null; } + return GetSeedConfiguration(remoteMovie.Release.IndexerId); + } + + public TorrentSeedConfiguration GetSeedConfiguration(int indexerId) + { + 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 = seedCriteria.SeedTime; + + if (seedTime.HasValue) + { + seedConfig.SeedTime = TimeSpan.FromMinutes(seedTime.Value); + } + + return seedConfig; + } + + private SeedCriteriaSettings FetchSeedCriteria(int indexerId) + { try { - var indexer = _indexerFactory.Get(remoteMovie.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 = 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(); } } }