2017-08-06 10:09:16 +00:00
using System ;
2015-09-03 04:28:08 +00:00
using System.Collections.Generic ;
2020-11-14 05:27:07 +00:00
using System.Linq ;
2019-12-22 22:08:53 +00:00
using System.Net ;
using FluentValidation.Results ;
using NLog ;
using NzbDrone.Common.Cache ;
2015-09-03 04:28:08 +00:00
using NzbDrone.Common.Disk ;
using NzbDrone.Common.Extensions ;
using NzbDrone.Common.Http ;
using NzbDrone.Core.Configuration ;
using NzbDrone.Core.MediaFiles.TorrentInfo ;
2019-12-22 22:08:53 +00:00
using NzbDrone.Core.Organizer ;
2015-09-03 04:28:08 +00:00
using NzbDrone.Core.Parser.Model ;
using NzbDrone.Core.RemotePathMappings ;
2019-12-22 22:08:53 +00:00
using NzbDrone.Core.Validation ;
2015-09-03 04:28:08 +00:00
namespace NzbDrone.Core.Download.Clients.QBittorrent
{
public class QBittorrent : TorrentClientBase < QBittorrentSettings >
{
2019-06-14 03:54:25 +00:00
private readonly IQBittorrentProxySelector _proxySelector ;
2019-07-14 09:34:03 +00:00
private readonly ICached < SeedingTimeCacheEntry > _seedingTimeCache ;
private class SeedingTimeCacheEntry
{
public DateTime LastFetched { get ; set ; }
public long SeedingTime { get ; set ; }
}
2015-09-03 04:28:08 +00:00
2019-06-14 03:54:25 +00:00
public QBittorrent ( IQBittorrentProxySelector proxySelector ,
2015-09-03 04:28:08 +00:00
ITorrentFileInfoReader torrentFileInfoReader ,
IHttpClient httpClient ,
IConfigService configService ,
2018-04-16 22:22:54 +00:00
INamingConfigService namingConfigService ,
2015-09-03 04:28:08 +00:00
IDiskProvider diskProvider ,
IRemotePathMappingService remotePathMappingService ,
2019-07-14 09:34:03 +00:00
ICacheManager cacheManager ,
2015-09-03 04:28:08 +00:00
Logger logger )
2018-04-16 22:22:54 +00:00
: base ( torrentFileInfoReader , httpClient , configService , namingConfigService , diskProvider , remotePathMappingService , logger )
2015-09-03 04:28:08 +00:00
{
2019-06-14 03:54:25 +00:00
_proxySelector = proxySelector ;
2019-07-14 09:34:03 +00:00
_seedingTimeCache = cacheManager . GetCache < SeedingTimeCacheEntry > ( GetType ( ) , "seedingTime" ) ;
2015-09-03 04:28:08 +00:00
}
2019-06-14 03:54:25 +00:00
private IQBittorrentProxy Proxy = > _proxySelector . GetProxy ( Settings ) ;
2021-03-15 01:50:18 +00:00
private Version ProxyApiVersion = > _proxySelector . GetApiVersion ( Settings ) ;
2019-06-14 03:54:25 +00:00
2019-07-01 01:50:01 +00:00
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 )
{
2019-12-22 22:08:53 +00:00
_logger . Warn ( "Failed to set post-import torrent label \"{0}\" for {1} in qBittorrent. Does the label exist?" , Settings . MovieImportedCategory , downloadClientItem . Title ) ;
2019-07-01 01:50:01 +00:00
}
}
}
2017-01-13 16:25:59 +00:00
protected override string AddFromMagnetLink ( RemoteMovie remoteMovie , string hash , string magnetLink )
2017-01-02 18:20:32 +00:00
{
2019-06-14 03:54:25 +00:00
if ( ! Proxy . GetConfig ( Settings ) . DhtEnabled & & ! magnetLink . Contains ( "&tr=" ) )
{
throw new NotSupportedException ( "Magnet Links without trackers not supported if DHT is disabled" ) ;
}
2021-03-15 01:50:18 +00:00
var setShareLimits = remoteMovie . SeedConfiguration ! = null & & ( remoteMovie . SeedConfiguration . Ratio . HasValue | | remoteMovie . SeedConfiguration . SeedTime . HasValue ) ;
var addHasSetShareLimits = setShareLimits & & ProxyApiVersion > = new Version ( 2 , 8 , 1 ) ;
2022-03-20 15:55:47 +00:00
var isRecentMovie = remoteMovie . Movie . MovieMetadata . Value . IsRecentMovie ;
2021-03-15 01:50:18 +00:00
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 ) ;
2018-04-15 03:03:32 +00:00
2021-03-15 01:50:18 +00:00
if ( ( ! addHasSetShareLimits & & setShareLimits ) | | moveToTop | | forceStart )
2018-04-15 03:03:32 +00:00
{
2021-03-15 01:50:18 +00:00
if ( ! WaitForTorrent ( hash ) )
{
return hash ;
}
2018-04-15 03:03:32 +00:00
2021-03-15 01:50:18 +00:00
if ( ! addHasSetShareLimits & & setShareLimits )
{
2021-03-21 21:47:33 +00:00
try
{
Proxy . SetTorrentSeedingConfiguration ( hash . ToLower ( ) , remoteMovie . SeedConfiguration , Settings ) ;
}
catch ( Exception ex )
{
_logger . Warn ( ex , "Failed to set the torrent seed criteria for {0}." , hash ) ;
}
2021-03-15 01:50:18 +00:00
}
2017-10-15 10:35:53 +00:00
2021-03-15 01:50:18 +00:00
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 ) ;
}
}
2019-06-14 03:54:25 +00:00
}
2017-01-02 18:20:32 +00:00
return hash ;
}
2019-12-22 22:08:53 +00:00
protected override string AddFromTorrentFile ( RemoteMovie remoteMovie , string hash , string filename , byte [ ] fileContent )
2017-01-02 18:20:32 +00:00
{
2021-03-15 01:50:18 +00:00
var setShareLimits = remoteMovie . SeedConfiguration ! = null & & ( remoteMovie . SeedConfiguration . Ratio . HasValue | | remoteMovie . SeedConfiguration . SeedTime . HasValue ) ;
var addHasSetShareLimits = setShareLimits & & ProxyApiVersion > = new Version ( 2 , 8 , 1 ) ;
2022-03-20 15:55:47 +00:00
var isRecentMovie = remoteMovie . Movie . MovieMetadata . Value . IsRecentMovie ;
2021-03-15 01:50:18 +00:00
var moveToTop = ( isRecentMovie & & Settings . RecentMoviePriority = = ( int ) QBittorrentPriority . First ) | | ( ! isRecentMovie & & Settings . OlderMoviePriority = = ( int ) QBittorrentPriority . First ) ;
var forceStart = ( QBittorrentState ) Settings . InitialState = = QBittorrentState . ForceStart ;
2017-01-02 18:20:32 +00:00
2021-03-15 01:50:18 +00:00
Proxy . AddTorrentFromFile ( filename , fileContent , addHasSetShareLimits ? remoteMovie . SeedConfiguration : null , Settings ) ;
if ( ( ! addHasSetShareLimits & & setShareLimits ) | | moveToTop | | forceStart )
2018-04-15 03:03:32 +00:00
{
2021-03-15 01:50:18 +00:00
if ( ! WaitForTorrent ( hash ) )
{
return hash ;
}
2018-04-15 03:03:32 +00:00
2021-03-15 01:50:18 +00:00
if ( ! addHasSetShareLimits & & setShareLimits )
2018-04-15 03:03:32 +00:00
{
2021-03-21 21:47:33 +00:00
try
{
Proxy . SetTorrentSeedingConfiguration ( hash . ToLower ( ) , remoteMovie . SeedConfiguration , Settings ) ;
}
catch ( Exception ex )
{
_logger . Warn ( ex , "Failed to set the torrent seed criteria for {0}." , hash ) ;
}
2021-04-20 05:00:33 +00:00
}
2021-03-15 01:50:18 +00:00
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 ) ;
}
2018-04-15 03:03:32 +00:00
}
2017-01-02 18:20:32 +00:00
}
2021-03-15 01:50:18 +00:00
return hash ;
}
protected bool WaitForTorrent ( string hash )
{
2021-03-21 21:47:33 +00:00
var count = 10 ;
2017-10-15 10:35:53 +00:00
2021-03-15 01:50:18 +00:00
while ( count ! = 0 )
2019-06-14 03:54:25 +00:00
{
2021-03-15 01:50:18 +00:00
try
{
2021-03-21 21:47:33 +00:00
if ( Proxy . IsTorrentLoaded ( hash . ToLower ( ) , Settings ) )
{
return true ;
}
2021-03-15 01:50:18 +00:00
}
catch
{
}
_logger . Trace ( "Torrent '{0}' not yet visible in qbit, waiting 100ms." , hash ) ;
System . Threading . Thread . Sleep ( 100 ) ;
count - - ;
2019-06-14 03:54:25 +00:00
}
2021-03-15 01:50:18 +00:00
_logger . Warn ( "Failed to load torrent '{0}' within 500 ms, skipping additional parameters." , hash ) ;
return false ;
2017-01-02 18:20:32 +00:00
}
2016-12-09 06:54:15 +00:00
public override string Name = > "qBittorrent" ;
2015-09-03 04:28:08 +00:00
public override IEnumerable < DownloadClientItem > GetItems ( )
{
2020-11-14 05:27:07 +00:00
var version = Proxy . GetApiVersion ( Settings ) ;
2019-06-14 03:54:25 +00:00
var config = Proxy . GetConfig ( Settings ) ;
var torrents = Proxy . GetTorrents ( Settings ) ;
2015-09-03 04:28:08 +00:00
var queueItems = new List < DownloadClientItem > ( ) ;
foreach ( var torrent in torrents )
{
2019-06-14 03:54:25 +00:00
var item = new DownloadClientItem ( )
{
DownloadId = torrent . Hash . ToUpper ( ) ,
Category = torrent . Category . IsNotNullOrWhiteSpace ( ) ? torrent . Category : torrent . Label ,
Title = torrent . Name ,
TotalSize = torrent . Size ,
2020-03-29 20:05:48 +00:00
DownloadClientInfo = DownloadClientItemClientInfo . FromDownloadClient ( this ) ,
2019-06-14 03:54:25 +00:00
RemainingSize = ( long ) ( torrent . Size * ( 1.0 - torrent . Progress ) ) ,
RemainingTime = GetRemainingTime ( torrent ) ,
2020-11-14 05:27:07 +00:00
SeedRatio = torrent . Ratio
2019-06-14 03:54:25 +00:00
} ;
2016-10-28 03:07:42 +00:00
// 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).
2019-12-22 22:08:53 +00:00
item . CanMoveFiles = item . CanBeRemoved = torrent . State = = "pausedUP" & & HasReachedSeedLimit ( torrent , config ) ;
2015-09-03 04:28:08 +00:00
switch ( torrent . State )
{
2021-10-03 07:44:13 +00:00
case "error" : // some error occurred, applies to paused torrents, warning so failed download handling isn't triggered
item . Status = DownloadItemStatus . Warning ;
2017-08-06 10:09:16 +00:00
item . Message = "qBittorrent is reporting an error" ;
2015-09-03 04:28:08 +00:00
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
2020-04-03 00:23:25 +00:00
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.
2015-09-03 04:28:08 +00:00
item . Status = DownloadItemStatus . Queued ;
break ;
2019-06-14 03:54:25 +00:00
case "pausedUP" : // torrent is paused and has finished downloading:
2020-04-03 00:23:25 +00:00
case "uploading" : // torrent is being seeded and data is being transferred
2015-09-03 04:28:08 +00:00
case "stalledUP" : // torrent is being seeded, but no connection were made
case "queuedUP" : // queuing is enabled and torrent is queued for upload
2019-06-14 03:54:25 +00:00
case "forcedUP" : // torrent has finished downloading and is being forcibly seeded
2015-09-03 04:28:08 +00:00
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 ;
2020-09-10 01:18:38 +00:00
case "missingFiles" : // torrent is being downloaded, but no connection were made
item . Status = DownloadItemStatus . Warning ;
item . Message = "The download is missing files" ;
break ;
2019-06-14 03:54:25 +00:00
case "metaDL" : // torrent magnet is being downloaded
if ( config . DhtEnabled )
{
item . Status = DownloadItemStatus . Queued ;
}
else
{
item . Status = DownloadItemStatus . Warning ;
item . Message = "qBittorrent cannot resolve magnet link with DHT disabled" ;
}
2019-12-22 22:08:53 +00:00
2018-12-30 13:17:03 +00:00
break ;
2022-04-24 18:14:58 +00:00
case "forcedDL" : // torrent is being downloaded, and was forced started
case "forcedMetaDL" : // torrent metadata is being forcibly downloaded
2019-12-04 17:02:39 +00:00
case "moving" : // torrent is being moved from a folder
2020-04-03 00:23:25 +00:00
case "downloading" : // torrent is being downloaded and data is being transferred
2019-12-04 17:02:39 +00:00
item . Status = DownloadItemStatus . Downloading ;
break ;
2015-09-03 04:28:08 +00:00
default : // new status in API? default to downloading
2019-12-04 17:02:39 +00:00
item . Message = "Unknown download state: " + torrent . State ;
_logger . Info ( item . Message ) ;
2015-09-03 04:28:08 +00:00
item . Status = DownloadItemStatus . Downloading ;
break ;
}
2020-11-14 17:49:58 +00:00
if ( version > = new Version ( "2.6.1" ) )
{
if ( torrent . ContentPath ! = torrent . SavePath )
{
item . OutputPath = _remotePathMappingService . RemapRemoteToLocal ( Settings . Host , new OsPath ( torrent . ContentPath ) ) ;
}
else if ( item . Status = = DownloadItemStatus . Completed )
{
item . Status = DownloadItemStatus . Warning ;
2022-04-07 01:53:52 +00:00
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'?" ;
2020-11-14 17:49:58 +00:00
}
}
2015-09-03 04:28:08 +00:00
queueItems . Add ( item ) ;
}
return queueItems ;
}
2021-04-20 05:00:33 +00:00
public override void RemoveItem ( DownloadClientItem item , bool deleteData )
2015-09-03 04:28:08 +00:00
{
2021-04-20 05:00:33 +00:00
Proxy . RemoveTorrent ( item . DownloadId . ToLower ( ) , deleteData , Settings ) ;
2015-09-03 04:28:08 +00:00
}
2020-11-14 05:27:07 +00:00
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 ( ) ;
2020-11-15 21:01:20 +00:00
// get the first subdirectory - QBittorrent returns `/` path separators even on windows...
var relativePath = new OsPath ( files [ 0 ] . Name ) ;
while ( ! relativePath . Directory . IsEmpty )
2020-11-14 05:27:07 +00:00
{
2020-11-15 21:01:20 +00:00
relativePath = relativePath . Directory ;
2020-11-14 05:27:07 +00:00
}
2020-11-15 21:01:20 +00:00
var outputPath = savePath + relativePath . FileName ;
2020-11-14 05:27:07 +00:00
result . OutputPath = _remotePathMappingService . RemapRemoteToLocal ( Settings . Host , outputPath ) ;
return result ;
}
2019-06-14 03:54:25 +00:00
public override DownloadClientInfo GetStatus ( )
2015-09-03 04:28:08 +00:00
{
2020-12-01 21:20:21 +00:00
var version = Proxy . GetApiVersion ( Settings ) ;
2019-06-14 03:54:25 +00:00
var config = Proxy . GetConfig ( Settings ) ;
2015-09-03 04:28:08 +00:00
2016-10-28 03:07:42 +00:00
var destDir = new OsPath ( config . SavePath ) ;
2015-09-03 04:28:08 +00:00
2020-12-01 21:20:21 +00:00
if ( Settings . MovieCategory . IsNotNullOrWhiteSpace ( ) & & version > = Version . Parse ( "2.0" ) )
{
2022-01-24 21:13:20 +00:00
if ( Proxy . GetLabels ( Settings ) . TryGetValue ( Settings . MovieCategory , out var label ) & & label . SavePath . IsNotNullOrWhiteSpace ( ) )
2020-12-01 21:20:21 +00:00
{
var labelDir = new OsPath ( label . SavePath ) ;
if ( labelDir . IsRooted )
{
destDir = labelDir ;
}
else
{
destDir = destDir + labelDir ;
}
}
}
2019-06-14 03:54:25 +00:00
return new DownloadClientInfo
2015-09-03 04:28:08 +00:00
{
IsLocalhost = Settings . Host = = "127.0.0.1" | | Settings . Host = = "localhost" ,
OutputRootFolders = new List < OsPath > { _remotePathMappingService . RemapRemoteToLocal ( Settings . Host , destDir ) }
} ;
}
protected override void Test ( List < ValidationFailure > failures )
{
failures . AddIfNotNull ( TestConnection ( ) ) ;
2019-12-22 22:08:53 +00:00
if ( failures . HasErrors ( ) )
{
return ;
}
2019-07-01 01:50:01 +00:00
failures . AddIfNotNull ( TestCategory ( ) ) ;
2018-04-15 03:03:32 +00:00
failures . AddIfNotNull ( TestPrioritySupport ( ) ) ;
2015-09-03 04:28:08 +00:00
failures . AddIfNotNull ( TestGetTorrents ( ) ) ;
}
private ValidationFailure TestConnection ( )
{
try
{
2019-06-14 03:54:25 +00:00
var version = _proxySelector . GetProxy ( Settings , true ) . GetApiVersion ( Settings ) ;
if ( version < Version . Parse ( "1.5" ) )
2015-09-03 04:28:08 +00:00
{
// 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."
} ;
}
2019-06-14 03:54:25 +00:00
else if ( version < Version . Parse ( "1.6" ) )
2015-09-03 04:28:08 +00:00
{
// API version 6 introduced support for labels
2017-01-13 16:25:59 +00:00
if ( Settings . MovieCategory . IsNotNullOrWhiteSpace ( ) )
2015-09-03 04:28:08 +00:00
{
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."
} ;
}
}
2017-01-13 16:25:59 +00:00
else if ( Settings . MovieCategory . IsNullOrWhiteSpace ( ) )
2015-09-03 04:28:08 +00:00
{
// warn if labels are supported, but category is not provided
2019-07-01 01:50:01 +00:00
return new NzbDroneValidationFailure ( "MovieCategory" , "Category is recommended" )
2015-09-03 04:28:08 +00:00
{
IsWarning = true ,
2017-01-03 16:35:31 +00:00
DetailedDescription = "Radarr will not attempt to import completed downloads without a category."
2015-09-03 04:28:08 +00:00
} ;
}
2016-10-28 03:07:42 +00:00
// Complain if qBittorrent is configured to remove torrents on max ratio
2019-06-14 03:54:25 +00:00
var config = Proxy . GetConfig ( Settings ) ;
2021-01-26 09:58:27 +00:00
if ( ( config . MaxRatioEnabled | | config . MaxSeedingTimeEnabled ) & & ( config . MaxRatioAction = = QBittorrentMaxRatioAction . Remove | | config . MaxRatioAction = = QBittorrentMaxRatioAction . DeleteFiles ) )
2016-10-28 03:07:42 +00:00
{
2019-12-22 22:08:53 +00:00
return new NzbDroneValidationFailure ( string . Empty , "qBittorrent is configured to remove torrents when they reach their Share Ratio Limit" )
2016-10-28 03:07:42 +00:00
{
2017-01-03 16:35:31 +00:00
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'."
2016-10-28 03:07:42 +00:00
} ;
}
2015-09-03 04:28:08 +00:00
}
catch ( DownloadClientAuthenticationException ex )
{
2016-02-11 21:13:42 +00:00
_logger . Error ( ex , ex . Message ) ;
2015-09-03 04:28:08 +00:00
return new NzbDroneValidationFailure ( "Username" , "Authentication failure" )
{
DetailedDescription = "Please verify your username and password."
} ;
}
catch ( WebException ex )
{
2019-06-14 03:54:25 +00:00
_logger . Error ( ex , "Unable to connect to qBittorrent" ) ;
2015-09-03 04:28:08 +00:00
if ( ex . Status = = WebExceptionStatus . ConnectFailure )
{
return new NzbDroneValidationFailure ( "Host" , "Unable to connect" )
{
DetailedDescription = "Please verify the hostname and port."
} ;
}
2019-12-22 22:08:53 +00:00
return new NzbDroneValidationFailure ( string . Empty , "Unknown exception: " + ex . Message ) ;
2015-09-03 04:28:08 +00:00
}
catch ( Exception ex )
{
2019-06-14 03:54:25 +00:00
_logger . Error ( ex , "Unable to test qBittorrent" ) ;
2020-10-25 22:59:40 +00:00
return new NzbDroneValidationFailure ( "Host" , "Unable to connect to qBittorrent" )
2021-03-21 21:47:33 +00:00
{
DetailedDescription = ex . Message
} ;
2015-09-03 04:28:08 +00:00
}
return null ;
}
2019-07-01 01:50:01 +00:00
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 ;
}
2023-05-23 10:52:39 +00:00
var labels = Proxy . GetLabels ( Settings ) ;
2019-07-01 01:50:01 +00:00
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 ;
}
2018-04-15 03:03:32 +00:00
private ValidationFailure TestPrioritySupport ( )
{
var recentPriorityDefault = Settings . RecentMoviePriority = = ( int ) QBittorrentPriority . Last ;
var olderPriorityDefault = Settings . OlderMoviePriority = = ( int ) QBittorrentPriority . Last ;
if ( olderPriorityDefault & & recentPriorityDefault )
{
return null ;
}
try
{
2019-06-14 03:54:25 +00:00
var config = Proxy . GetConfig ( Settings ) ;
2018-04-15 03:03:32 +00:00
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" ) ;
2019-12-22 22:08:53 +00:00
return new NzbDroneValidationFailure ( string . Empty , "Unknown exception: " + ex . Message ) ;
2018-04-15 03:03:32 +00:00
}
return null ;
}
2015-09-03 04:28:08 +00:00
private ValidationFailure TestGetTorrents ( )
{
try
{
2019-06-14 03:54:25 +00:00
Proxy . GetTorrents ( Settings ) ;
2015-09-03 04:28:08 +00:00
}
catch ( Exception ex )
{
2019-06-14 03:54:25 +00:00
_logger . Error ( ex , "Failed to get torrents" ) ;
2019-12-22 22:08:53 +00:00
return new NzbDroneValidationFailure ( string . Empty , "Failed to get the list of torrents: " + ex . Message ) ;
2015-09-03 04:28:08 +00:00
}
return null ;
}
2017-10-15 10:35:53 +00:00
2018-05-21 08:43:06 +00:00
protected TimeSpan ? GetRemainingTime ( QBittorrentTorrent torrent )
{
2019-06-14 03:54:25 +00:00
if ( torrent . Eta < 0 | | torrent . Eta > 365 * 24 * 3600 )
2018-05-21 08:43:06 +00:00
{
return null ;
}
2019-06-14 03:54:25 +00:00
// 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 )
{
2019-12-22 22:08:53 +00:00
if ( torrent . Ratio > = torrent . RatioLimit )
{
return true ;
}
2019-06-14 03:54:25 +00:00
}
else if ( torrent . RatioLimit = = - 2 & & config . MaxRatioEnabled )
{
2019-12-22 22:08:53 +00:00
if ( torrent . Ratio > = config . MaxRatio )
{
return true ;
}
2019-06-14 03:54:25 +00:00
}
2019-12-22 22:08:53 +00:00
if ( HasReachedSeedingTimeLimit ( torrent , config ) )
{
return true ;
}
2019-07-14 09:34:03 +00:00
return false ;
}
protected bool HasReachedSeedingTimeLimit ( QBittorrentTorrent torrent , QBittorrentPreferences config )
{
long seedingTimeLimit ;
2019-06-14 03:54:25 +00:00
if ( torrent . SeedingTimeLimit > = 0 )
{
2019-07-14 09:34:03 +00:00
seedingTimeLimit = torrent . SeedingTimeLimit ;
2019-06-14 03:54:25 +00:00
}
else if ( torrent . SeedingTimeLimit = = - 2 & & config . MaxSeedingTimeEnabled )
{
2019-07-14 09:34:03 +00:00
seedingTimeLimit = config . MaxSeedingTime ;
}
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 )
2019-06-14 03:54:25 +00:00
{
2019-07-14 09:34:03 +00:00
// SeedingTime cannot have reached the required value since the last check, preserve the cache
_seedingTimeCache . Set ( cacheKey , cacheSeedingTime , TimeSpan . FromMinutes ( 5 ) ) ;
return false ;
2019-06-14 03:54:25 +00:00
}
2019-07-14 09:34:03 +00:00
}
FetchTorrentDetails ( torrent ) ;
cacheSeedingTime = new SeedingTimeCacheEntry
{
LastFetched = DateTime . UtcNow ,
SeedingTime = torrent . SeedingTime . Value
} ;
2019-06-14 03:54:25 +00:00
2019-07-14 09:34:03 +00:00
_seedingTimeCache . Set ( cacheKey , cacheSeedingTime , TimeSpan . FromMinutes ( 5 ) ) ;
if ( cacheSeedingTime . SeedingTime > = seedingTimeLimit )
{
// Reached the limit, keep the cache alive
return true ;
2019-06-14 03:54:25 +00:00
}
return false ;
}
protected void FetchTorrentDetails ( QBittorrentTorrent torrent )
{
var torrentProperties = Proxy . GetTorrentProperties ( torrent . Hash , Settings ) ;
torrent . SeedingTime = torrentProperties . SeedingTime ;
2018-05-21 08:43:06 +00:00
}
2015-09-03 04:28:08 +00:00
}
}