You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
700 lines
29 KiB
700 lines
29 KiB
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using FluentValidation.Results;
|
|
using NLog;
|
|
using NzbDrone.Common.Cache;
|
|
using NzbDrone.Common.Disk;
|
|
using NzbDrone.Common.Extensions;
|
|
using NzbDrone.Common.Http;
|
|
using NzbDrone.Core.Blocklisting;
|
|
using NzbDrone.Core.Configuration;
|
|
using NzbDrone.Core.Localization;
|
|
using NzbDrone.Core.MediaFiles.TorrentInfo;
|
|
using NzbDrone.Core.Parser.Model;
|
|
using NzbDrone.Core.RemotePathMappings;
|
|
using NzbDrone.Core.Validation;
|
|
|
|
namespace NzbDrone.Core.Download.Clients.QBittorrent
|
|
{
|
|
public class QBittorrent : TorrentClientBase<QBittorrentSettings>
|
|
{
|
|
private readonly IQBittorrentProxySelector _proxySelector;
|
|
private readonly ICached<SeedingTimeCacheEntry> _seedingTimeCache;
|
|
|
|
private class SeedingTimeCacheEntry
|
|
{
|
|
public DateTime LastFetched { get; set; }
|
|
public long SeedingTime { get; set; }
|
|
}
|
|
|
|
public QBittorrent(IQBittorrentProxySelector proxySelector,
|
|
ITorrentFileInfoReader torrentFileInfoReader,
|
|
IHttpClient httpClient,
|
|
IConfigService configService,
|
|
IDiskProvider diskProvider,
|
|
IRemotePathMappingService remotePathMappingService,
|
|
ICacheManager cacheManager,
|
|
ILocalizationService localizationService,
|
|
IBlocklistService blocklistService,
|
|
Logger logger)
|
|
: base(torrentFileInfoReader, httpClient, configService, diskProvider, remotePathMappingService, localizationService, blocklistService, logger)
|
|
{
|
|
_proxySelector = proxySelector;
|
|
|
|
_seedingTimeCache = cacheManager.GetCache<SeedingTimeCacheEntry>(GetType(), "seedingTime");
|
|
}
|
|
|
|
private IQBittorrentProxy Proxy => _proxySelector.GetProxy(Settings);
|
|
private Version ProxyApiVersion => _proxySelector.GetApiVersion(Settings);
|
|
|
|
public override void MarkItemAsImported(DownloadClientItem downloadClientItem)
|
|
{
|
|
// set post-import category
|
|
if (Settings.MovieImportedCategory.IsNotNullOrWhiteSpace() &&
|
|
Settings.MovieImportedCategory != Settings.MovieCategory)
|
|
{
|
|
try
|
|
{
|
|
Proxy.SetTorrentLabel(downloadClientItem.DownloadId.ToLower(), Settings.MovieImportedCategory, Settings);
|
|
}
|
|
catch (DownloadClientException)
|
|
{
|
|
_logger.Warn("Failed to set post-import torrent label \"{0}\" for {1} in qBittorrent. Does the label exist?", Settings.MovieImportedCategory, downloadClientItem.Title);
|
|
}
|
|
}
|
|
}
|
|
|
|
protected override string AddFromMagnetLink(RemoteMovie remoteMovie, string hash, string magnetLink)
|
|
{
|
|
if (!Proxy.GetConfig(Settings).DhtEnabled && !magnetLink.Contains("&tr="))
|
|
{
|
|
throw new NotSupportedException("Magnet Links without trackers not supported if DHT is disabled");
|
|
}
|
|
|
|
var setShareLimits = remoteMovie.SeedConfiguration != null && (remoteMovie.SeedConfiguration.Ratio.HasValue || remoteMovie.SeedConfiguration.SeedTime.HasValue);
|
|
var addHasSetShareLimits = setShareLimits && ProxyApiVersion >= new Version(2, 8, 1);
|
|
var isRecentMovie = remoteMovie.Movie.MovieMetadata.Value.IsRecentMovie;
|
|
var moveToTop = (isRecentMovie && Settings.RecentMoviePriority == (int)QBittorrentPriority.First) || (!isRecentMovie && Settings.OlderMoviePriority == (int)QBittorrentPriority.First);
|
|
var forceStart = (QBittorrentState)Settings.InitialState == QBittorrentState.ForceStart;
|
|
|
|
Proxy.AddTorrentFromUrl(magnetLink, addHasSetShareLimits && setShareLimits ? remoteMovie.SeedConfiguration : null, Settings);
|
|
|
|
if ((!addHasSetShareLimits && setShareLimits) || moveToTop || forceStart)
|
|
{
|
|
if (!WaitForTorrent(hash))
|
|
{
|
|
return hash;
|
|
}
|
|
|
|
if (!addHasSetShareLimits && setShareLimits)
|
|
{
|
|
try
|
|
{
|
|
Proxy.SetTorrentSeedingConfiguration(hash.ToLower(), remoteMovie.SeedConfiguration, Settings);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.Warn(ex, "Failed to set the torrent seed criteria for {0}.", hash);
|
|
}
|
|
}
|
|
|
|
if (moveToTop)
|
|
{
|
|
try
|
|
{
|
|
Proxy.MoveTorrentToTopInQueue(hash.ToLower(), Settings);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.Warn(ex, "Failed to set the torrent priority for {0}.", hash);
|
|
}
|
|
}
|
|
|
|
if (forceStart)
|
|
{
|
|
try
|
|
{
|
|
Proxy.SetForceStart(hash.ToLower(), true, Settings);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.Warn(ex, "Failed to set ForceStart for {0}.", hash);
|
|
}
|
|
}
|
|
}
|
|
|
|
return hash;
|
|
}
|
|
|
|
protected override string AddFromTorrentFile(RemoteMovie remoteMovie, string hash, string filename, byte[] fileContent)
|
|
{
|
|
var setShareLimits = remoteMovie.SeedConfiguration != null && (remoteMovie.SeedConfiguration.Ratio.HasValue || remoteMovie.SeedConfiguration.SeedTime.HasValue);
|
|
var addHasSetShareLimits = setShareLimits && ProxyApiVersion >= new Version(2, 8, 1);
|
|
var isRecentMovie = remoteMovie.Movie.MovieMetadata.Value.IsRecentMovie;
|
|
var moveToTop = (isRecentMovie && Settings.RecentMoviePriority == (int)QBittorrentPriority.First) || (!isRecentMovie && Settings.OlderMoviePriority == (int)QBittorrentPriority.First);
|
|
var forceStart = (QBittorrentState)Settings.InitialState == QBittorrentState.ForceStart;
|
|
|
|
Proxy.AddTorrentFromFile(filename, fileContent, addHasSetShareLimits ? remoteMovie.SeedConfiguration : null, Settings);
|
|
|
|
if ((!addHasSetShareLimits && setShareLimits) || moveToTop || forceStart)
|
|
{
|
|
if (!WaitForTorrent(hash))
|
|
{
|
|
return hash;
|
|
}
|
|
|
|
if (!addHasSetShareLimits && setShareLimits)
|
|
{
|
|
try
|
|
{
|
|
Proxy.SetTorrentSeedingConfiguration(hash.ToLower(), remoteMovie.SeedConfiguration, Settings);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.Warn(ex, "Failed to set the torrent seed criteria for {0}.", hash);
|
|
}
|
|
}
|
|
|
|
if (moveToTop)
|
|
{
|
|
try
|
|
{
|
|
Proxy.MoveTorrentToTopInQueue(hash.ToLower(), Settings);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.Warn(ex, "Failed to set the torrent priority for {0}.", hash);
|
|
}
|
|
}
|
|
|
|
if (forceStart)
|
|
{
|
|
try
|
|
{
|
|
Proxy.SetForceStart(hash.ToLower(), true, Settings);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.Warn(ex, "Failed to set ForceStart for {0}.", hash);
|
|
}
|
|
}
|
|
}
|
|
|
|
return hash;
|
|
}
|
|
|
|
protected bool WaitForTorrent(string hash)
|
|
{
|
|
var count = 10;
|
|
|
|
while (count != 0)
|
|
{
|
|
try
|
|
{
|
|
if (Proxy.IsTorrentLoaded(hash.ToLower(), Settings))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
|
|
_logger.Trace("Torrent '{0}' not yet visible in qbit, waiting 100ms.", hash);
|
|
System.Threading.Thread.Sleep(100);
|
|
count--;
|
|
}
|
|
|
|
_logger.Warn("Failed to load torrent '{0}' within 500 ms, skipping additional parameters.", hash);
|
|
return false;
|
|
}
|
|
|
|
public override string Name => "qBittorrent";
|
|
|
|
public override IEnumerable<DownloadClientItem> GetItems()
|
|
{
|
|
var version = Proxy.GetApiVersion(Settings);
|
|
var config = Proxy.GetConfig(Settings);
|
|
var torrents = Proxy.GetTorrents(Settings);
|
|
|
|
var queueItems = new List<DownloadClientItem>();
|
|
|
|
foreach (var torrent in torrents)
|
|
{
|
|
var item = new DownloadClientItem()
|
|
{
|
|
DownloadId = torrent.Hash.ToUpper(),
|
|
Category = torrent.Category.IsNotNullOrWhiteSpace() ? torrent.Category : torrent.Label,
|
|
Title = torrent.Name,
|
|
TotalSize = torrent.Size,
|
|
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, Settings.MovieImportedCategory.IsNotNullOrWhiteSpace()),
|
|
RemainingSize = (long)(torrent.Size * (1.0 - torrent.Progress)),
|
|
RemainingTime = GetRemainingTime(torrent),
|
|
SeedRatio = torrent.Ratio
|
|
};
|
|
|
|
// Avoid removing torrents that haven't reached the global max ratio.
|
|
// Removal also requires the torrent to be paused, in case a higher max ratio was set on the torrent itself (which is not exposed by the api).
|
|
item.CanMoveFiles = item.CanBeRemoved = torrent.State == "pausedUP" && HasReachedSeedLimit(torrent, config);
|
|
|
|
switch (torrent.State)
|
|
{
|
|
case "error": // some error occurred, applies to paused torrents, warning so failed download handling isn't triggered
|
|
item.Status = DownloadItemStatus.Warning;
|
|
item.Message = "qBittorrent is reporting an error";
|
|
break;
|
|
|
|
case "pausedDL": // torrent is paused and has NOT finished downloading
|
|
item.Status = DownloadItemStatus.Paused;
|
|
break;
|
|
|
|
case "queuedDL": // queuing is enabled and torrent is queued for download
|
|
case "checkingDL": // same as checkingUP, but torrent has NOT finished downloading
|
|
case "checkingUP": // torrent has finished downloading and is being checked. Set when `recheck torrent on completion` is enabled. In the event the check fails we shouldn't treat it as completed.
|
|
case "checkingResumeData": // torrent is checking resume data on load
|
|
item.Status = DownloadItemStatus.Queued;
|
|
break;
|
|
|
|
case "pausedUP": // torrent is paused and has finished downloading:
|
|
case "uploading": // torrent is being seeded and data is being transferred
|
|
case "stalledUP": // torrent is being seeded, but no connection were made
|
|
case "queuedUP": // queuing is enabled and torrent is queued for upload
|
|
case "forcedUP": // torrent has finished downloading and is being forcibly seeded
|
|
item.Status = DownloadItemStatus.Completed;
|
|
item.RemainingTime = TimeSpan.Zero; // qBittorrent sends eta=8640000 for completed torrents
|
|
break;
|
|
|
|
case "stalledDL": // torrent is being downloaded, but no connection were made
|
|
item.Status = DownloadItemStatus.Warning;
|
|
item.Message = "The download is stalled with no connections";
|
|
break;
|
|
|
|
case "missingFiles": // torrent is being downloaded, but no connection were made
|
|
item.Status = DownloadItemStatus.Warning;
|
|
item.Message = "The download is missing files";
|
|
break;
|
|
|
|
case "metaDL": // torrent magnet is being downloaded
|
|
if (config.DhtEnabled)
|
|
{
|
|
item.Status = DownloadItemStatus.Queued;
|
|
item.Message = "qBittorrent is downloading metadata";
|
|
}
|
|
else
|
|
{
|
|
item.Status = DownloadItemStatus.Warning;
|
|
item.Message = "qBittorrent cannot resolve magnet link with DHT disabled";
|
|
}
|
|
|
|
break;
|
|
|
|
case "forcedDL": // torrent is being downloaded, and was forced started
|
|
case "forcedMetaDL": // torrent metadata is being forcibly downloaded
|
|
case "moving": // torrent is being moved from a folder
|
|
case "downloading": // torrent is being downloaded and data is being transferred
|
|
item.Status = DownloadItemStatus.Downloading;
|
|
break;
|
|
|
|
default: // new status in API? default to downloading
|
|
item.Message = "Unknown download state: " + torrent.State;
|
|
_logger.Info(item.Message);
|
|
item.Status = DownloadItemStatus.Downloading;
|
|
break;
|
|
}
|
|
|
|
if (version >= new Version("2.6.1") && item.Status == DownloadItemStatus.Completed)
|
|
{
|
|
if (torrent.ContentPath != torrent.SavePath)
|
|
{
|
|
item.OutputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(torrent.ContentPath));
|
|
}
|
|
else
|
|
{
|
|
item.Status = DownloadItemStatus.Warning;
|
|
item.Message = "Unable to Import. Path matches client base download directory, it's possible 'Keep top-level folder' is disabled for this torrent or 'Torrent Content Layout' is NOT set to 'Original' or 'Create Subfolder'?";
|
|
}
|
|
}
|
|
|
|
queueItems.Add(item);
|
|
}
|
|
|
|
return queueItems;
|
|
}
|
|
|
|
public override void RemoveItem(DownloadClientItem item, bool deleteData)
|
|
{
|
|
Proxy.RemoveTorrent(item.DownloadId.ToLower(), deleteData, Settings);
|
|
}
|
|
|
|
public override DownloadClientItem GetImportItem(DownloadClientItem item, DownloadClientItem previousImportAttempt)
|
|
{
|
|
// On API version >= 2.6.1 this is already set correctly
|
|
if (!item.OutputPath.IsEmpty)
|
|
{
|
|
return item;
|
|
}
|
|
|
|
var files = Proxy.GetTorrentFiles(item.DownloadId.ToLower(), Settings);
|
|
if (!files.Any())
|
|
{
|
|
_logger.Debug($"No files found for torrent {item.Title} in qBittorrent");
|
|
return item;
|
|
}
|
|
|
|
var properties = Proxy.GetTorrentProperties(item.DownloadId.ToLower(), Settings);
|
|
var savePath = new OsPath(properties.SavePath);
|
|
|
|
var result = item.Clone();
|
|
|
|
// get the first subdirectory - QBittorrent returns `/` path separators even on windows...
|
|
var relativePath = new OsPath(files[0].Name);
|
|
while (!relativePath.Directory.IsEmpty)
|
|
{
|
|
relativePath = relativePath.Directory;
|
|
}
|
|
|
|
var outputPath = savePath + relativePath.FileName;
|
|
|
|
result.OutputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, outputPath);
|
|
|
|
return result;
|
|
}
|
|
|
|
public override DownloadClientInfo GetStatus()
|
|
{
|
|
var version = Proxy.GetApiVersion(Settings);
|
|
var config = Proxy.GetConfig(Settings);
|
|
|
|
var destDir = new OsPath(config.SavePath);
|
|
|
|
if (Settings.MovieCategory.IsNotNullOrWhiteSpace() && version >= Version.Parse("2.0"))
|
|
{
|
|
if (Proxy.GetLabels(Settings).TryGetValue(Settings.MovieCategory, out var label) && label.SavePath.IsNotNullOrWhiteSpace())
|
|
{
|
|
var labelDir = new OsPath(label.SavePath);
|
|
|
|
if (labelDir.IsRooted)
|
|
{
|
|
destDir = labelDir;
|
|
}
|
|
else
|
|
{
|
|
destDir = destDir + labelDir;
|
|
}
|
|
}
|
|
}
|
|
|
|
var minimumRetention = 60 * 24 * 14;
|
|
|
|
return new DownloadClientInfo
|
|
{
|
|
IsLocalhost = Settings.Host == "127.0.0.1" || Settings.Host == "localhost",
|
|
OutputRootFolders = new List<OsPath> { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, destDir) },
|
|
RemovesCompletedDownloads = (config.MaxRatioEnabled || (config.MaxSeedingTimeEnabled && config.MaxSeedingTime < minimumRetention)) && (config.MaxRatioAction == QBittorrentMaxRatioAction.Remove || config.MaxRatioAction == QBittorrentMaxRatioAction.DeleteFiles)
|
|
};
|
|
}
|
|
|
|
protected override void Test(List<ValidationFailure> failures)
|
|
{
|
|
failures.AddIfNotNull(TestConnection());
|
|
if (failures.HasErrors())
|
|
{
|
|
return;
|
|
}
|
|
|
|
failures.AddIfNotNull(TestCategory());
|
|
failures.AddIfNotNull(TestPrioritySupport());
|
|
failures.AddIfNotNull(TestGetTorrents());
|
|
}
|
|
|
|
private ValidationFailure TestConnection()
|
|
{
|
|
try
|
|
{
|
|
var version = _proxySelector.GetProxy(Settings, true).GetApiVersion(Settings);
|
|
if (version < Version.Parse("1.5"))
|
|
{
|
|
// API version 5 introduced the "save_path" property in /query/torrents
|
|
return new NzbDroneValidationFailure("Host", "Unsupported client version")
|
|
{
|
|
DetailedDescription = "Please upgrade to qBittorrent version 3.2.4 or higher."
|
|
};
|
|
}
|
|
else if (version < Version.Parse("1.6"))
|
|
{
|
|
// API version 6 introduced support for labels
|
|
if (Settings.MovieCategory.IsNotNullOrWhiteSpace())
|
|
{
|
|
return new NzbDroneValidationFailure("Category", "Category is not supported")
|
|
{
|
|
DetailedDescription = "Labels are not supported until qBittorrent version 3.3.0. Please upgrade or try again with an empty Category."
|
|
};
|
|
}
|
|
}
|
|
else if (Settings.MovieCategory.IsNullOrWhiteSpace())
|
|
{
|
|
// warn if labels are supported, but category is not provided
|
|
return new NzbDroneValidationFailure("MovieCategory", "Category is recommended")
|
|
{
|
|
IsWarning = true,
|
|
DetailedDescription = "Radarr will not attempt to import completed downloads without a category."
|
|
};
|
|
}
|
|
|
|
// Complain if qBittorrent is configured to remove torrents on max ratio
|
|
var config = Proxy.GetConfig(Settings);
|
|
if ((config.MaxRatioEnabled || config.MaxSeedingTimeEnabled) && (config.MaxRatioAction == QBittorrentMaxRatioAction.Remove || config.MaxRatioAction == QBittorrentMaxRatioAction.DeleteFiles))
|
|
{
|
|
return new NzbDroneValidationFailure(string.Empty, "qBittorrent is configured to remove torrents when they reach their Share Ratio Limit")
|
|
{
|
|
DetailedDescription = "Radarr will be unable to perform Completed Download Handling as configured. You can fix this in qBittorrent ('Tools -> Options...' in the menu) by changing 'Options -> BitTorrent -> Share Ratio Limiting' from 'Remove them' to 'Pause them'."
|
|
};
|
|
}
|
|
}
|
|
catch (DownloadClientAuthenticationException ex)
|
|
{
|
|
_logger.Error(ex, ex.Message);
|
|
return new NzbDroneValidationFailure("Username", "Authentication failure")
|
|
{
|
|
DetailedDescription = "Please verify your username and password."
|
|
};
|
|
}
|
|
catch (WebException ex)
|
|
{
|
|
_logger.Error(ex, "Unable to connect to qBittorrent");
|
|
if (ex.Status == WebExceptionStatus.ConnectFailure)
|
|
{
|
|
return new NzbDroneValidationFailure("Host", "Unable to connect")
|
|
{
|
|
DetailedDescription = "Please verify the hostname and port."
|
|
};
|
|
}
|
|
|
|
return new NzbDroneValidationFailure(string.Empty, "Unknown exception: " + ex.Message);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.Error(ex, "Unable to test qBittorrent");
|
|
|
|
return new NzbDroneValidationFailure("Host", "Unable to connect to qBittorrent")
|
|
{
|
|
DetailedDescription = ex.Message
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private ValidationFailure TestCategory()
|
|
{
|
|
if (Settings.MovieCategory.IsNullOrWhiteSpace() && Settings.MovieImportedCategory.IsNullOrWhiteSpace())
|
|
{
|
|
return null;
|
|
}
|
|
|
|
// api v1 doesn't need to check/add categories as it's done on set
|
|
var version = _proxySelector.GetProxy(Settings, true).GetApiVersion(Settings);
|
|
if (version < Version.Parse("2.0"))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var labels = Proxy.GetLabels(Settings);
|
|
|
|
if (Settings.MovieCategory.IsNotNullOrWhiteSpace() && !labels.ContainsKey(Settings.MovieCategory))
|
|
{
|
|
Proxy.AddLabel(Settings.MovieCategory, Settings);
|
|
labels = Proxy.GetLabels(Settings);
|
|
|
|
if (!labels.ContainsKey(Settings.MovieCategory))
|
|
{
|
|
return new NzbDroneValidationFailure("MovieCategory", "Configuration of label failed")
|
|
{
|
|
DetailedDescription = "Radarr was unable to add the label to qBittorrent."
|
|
};
|
|
}
|
|
}
|
|
|
|
if (Settings.MovieImportedCategory.IsNotNullOrWhiteSpace() && !labels.ContainsKey(Settings.MovieImportedCategory))
|
|
{
|
|
Proxy.AddLabel(Settings.MovieImportedCategory, Settings);
|
|
labels = Proxy.GetLabels(Settings);
|
|
|
|
if (!labels.ContainsKey(Settings.MovieImportedCategory))
|
|
{
|
|
return new NzbDroneValidationFailure("MovieImportedCategory", "Configuration of label failed")
|
|
{
|
|
DetailedDescription = "Radarr was unable to add the label to qBittorrent."
|
|
};
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private ValidationFailure TestPrioritySupport()
|
|
{
|
|
var recentPriorityDefault = Settings.RecentMoviePriority == (int)QBittorrentPriority.Last;
|
|
var olderPriorityDefault = Settings.OlderMoviePriority == (int)QBittorrentPriority.Last;
|
|
|
|
if (olderPriorityDefault && recentPriorityDefault)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
try
|
|
{
|
|
var config = Proxy.GetConfig(Settings);
|
|
|
|
if (!config.QueueingEnabled)
|
|
{
|
|
if (!recentPriorityDefault)
|
|
{
|
|
return new NzbDroneValidationFailure(nameof(Settings.RecentMoviePriority), "Queueing not enabled") { DetailedDescription = "Torrent Queueing is not enabled in your qBittorrent settings. Enable it in qBittorrent or select 'Last' as priority." };
|
|
}
|
|
else if (!olderPriorityDefault)
|
|
{
|
|
return new NzbDroneValidationFailure(nameof(Settings.OlderMoviePriority), "Queueing not enabled") { DetailedDescription = "Torrent Queueing is not enabled in your qBittorrent settings. Enable it in qBittorrent or select 'Last' as priority." };
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.Error(ex, "Failed to test qBittorrent");
|
|
return new NzbDroneValidationFailure(string.Empty, "Unknown exception: " + ex.Message);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private ValidationFailure TestGetTorrents()
|
|
{
|
|
try
|
|
{
|
|
Proxy.GetTorrents(Settings);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.Error(ex, "Failed to get torrents");
|
|
return new NzbDroneValidationFailure(string.Empty, "Failed to get the list of torrents: " + ex.Message);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
protected TimeSpan? GetRemainingTime(QBittorrentTorrent torrent)
|
|
{
|
|
if (torrent.Eta < 0 || torrent.Eta > 365 * 24 * 3600)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
// qBittorrent sends eta=8640000 if unknown such as queued
|
|
if (torrent.Eta == 8640000)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return TimeSpan.FromSeconds((int)torrent.Eta);
|
|
}
|
|
|
|
protected bool HasReachedSeedLimit(QBittorrentTorrent torrent, QBittorrentPreferences config)
|
|
{
|
|
if (torrent.RatioLimit >= 0)
|
|
{
|
|
if (torrent.Ratio >= torrent.RatioLimit)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
else if (torrent.RatioLimit == -2 && config.MaxRatioEnabled)
|
|
{
|
|
if (Math.Round(torrent.Ratio, 2) >= config.MaxRatio)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (HasReachedSeedingTimeLimit(torrent, config))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
protected bool HasReachedSeedingTimeLimit(QBittorrentTorrent torrent, QBittorrentPreferences config)
|
|
{
|
|
long seedingTimeLimit;
|
|
|
|
if (torrent.SeedingTimeLimit >= 0)
|
|
{
|
|
seedingTimeLimit = torrent.SeedingTimeLimit * 60;
|
|
}
|
|
else if (torrent.SeedingTimeLimit == -2 && config.MaxSeedingTimeEnabled)
|
|
{
|
|
seedingTimeLimit = config.MaxSeedingTime * 60;
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (torrent.SeedingTime.HasValue)
|
|
{
|
|
// SeedingTime can't be available here, but use it if the api starts to provide it.
|
|
return torrent.SeedingTime.Value >= seedingTimeLimit;
|
|
}
|
|
|
|
var cacheKey = Settings.Host + Settings.Port + torrent.Hash;
|
|
var cacheSeedingTime = _seedingTimeCache.Find(cacheKey);
|
|
|
|
if (cacheSeedingTime != null)
|
|
{
|
|
var togo = seedingTimeLimit - cacheSeedingTime.SeedingTime;
|
|
var elapsed = (DateTime.UtcNow - cacheSeedingTime.LastFetched).TotalSeconds;
|
|
|
|
if (togo <= 0)
|
|
{
|
|
// Already reached the limit, keep the cache alive
|
|
_seedingTimeCache.Set(cacheKey, cacheSeedingTime, TimeSpan.FromMinutes(5));
|
|
return true;
|
|
}
|
|
else if (togo > elapsed)
|
|
{
|
|
// SeedingTime cannot have reached the required value since the last check, preserve the cache
|
|
_seedingTimeCache.Set(cacheKey, cacheSeedingTime, TimeSpan.FromMinutes(5));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
FetchTorrentDetails(torrent);
|
|
|
|
cacheSeedingTime = new SeedingTimeCacheEntry
|
|
{
|
|
LastFetched = DateTime.UtcNow,
|
|
SeedingTime = torrent.SeedingTime.Value
|
|
};
|
|
|
|
_seedingTimeCache.Set(cacheKey, cacheSeedingTime, TimeSpan.FromMinutes(5));
|
|
|
|
if (cacheSeedingTime.SeedingTime >= seedingTimeLimit)
|
|
{
|
|
// Reached the limit, keep the cache alive
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
protected void FetchTorrentDetails(QBittorrentTorrent torrent)
|
|
{
|
|
var torrentProperties = Proxy.GetTorrentProperties(torrent.Hash, Settings);
|
|
|
|
torrent.SeedingTime = torrentProperties.SeedingTime;
|
|
}
|
|
}
|
|
}
|