using System ;
using System.Collections.Generic ;
using System.Globalization ;
using System.IO ;
using System.Linq ;
using FluentValidation.Results ;
using NLog ;
using NzbDrone.Common.Disk ;
using NzbDrone.Common.Extensions ;
using NzbDrone.Common.Http ;
using NzbDrone.Core.Configuration ;
using NzbDrone.Core.Exceptions ;
using NzbDrone.Core.Localization ;
using NzbDrone.Core.Parser.Model ;
using NzbDrone.Core.RemotePathMappings ;
using NzbDrone.Core.Validation ;
namespace NzbDrone.Core.Download.Clients.Nzbget
{
public class Nzbget : UsenetClientBase < NzbgetSettings >
{
private readonly INzbgetProxy _proxy ;
private readonly string [ ] _successStatus = { "SUCCESS" , "NONE" } ;
private readonly string [ ] _deleteFailedStatus = { "HEALTH" , "DUPE" , "SCAN" , "COPY" , "BAD" } ;
public Nzbget ( INzbgetProxy proxy ,
IHttpClient httpClient ,
IConfigService configService ,
IDiskProvider diskProvider ,
IRemotePathMappingService remotePathMappingService ,
IValidateNzbs nzbValidationService ,
Logger logger ,
ILocalizationService localizationService )
: base ( httpClient , configService , diskProvider , remotePathMappingService , nzbValidationService , logger , localizationService )
{
_proxy = proxy ;
}
protected override string AddFromNzbFile ( RemoteMovie remoteMovie , string filename , byte [ ] fileContent )
{
var category = Settings . MovieCategory ;
var priority = remoteMovie . Movie . MovieMetadata . Value . IsRecentMovie ? Settings . RecentMoviePriority : Settings . OlderMoviePriority ;
var addpaused = Settings . AddPaused ;
var response = _proxy . DownloadNzb ( fileContent , filename , category , priority , addpaused , Settings ) ;
if ( response = = null )
{
throw new DownloadClientRejectedReleaseException ( remoteMovie . Release , "NZBGet rejected the NZB for an unknown reason" ) ;
}
return response ;
}
private IEnumerable < DownloadClientItem > GetQueue ( )
{
var globalStatus = _proxy . GetGlobalStatus ( Settings ) ;
var queue = _proxy . GetQueue ( Settings ) ;
var queueItems = new List < DownloadClientItem > ( ) ;
long totalRemainingSize = 0 ;
foreach ( var item in queue )
{
var totalSize = MakeInt64 ( item . FileSizeHi , item . FileSizeLo ) ;
var pausedSize = MakeInt64 ( item . PausedSizeHi , item . PausedSizeLo ) ;
var remainingSize = MakeInt64 ( item . RemainingSizeHi , item . RemainingSizeLo ) ;
var droneParameter = item . Parameters . SingleOrDefault ( p = > p . Name = = "drone" ) ;
var queueItem = new DownloadClientItem ( ) ;
queueItem . DownloadId = droneParameter = = null ? item . NzbId . ToString ( ) : droneParameter . Value . ToString ( ) ;
queueItem . Title = item . NzbName ;
queueItem . TotalSize = totalSize ;
queueItem . Category = item . Category ;
queueItem . DownloadClientInfo = DownloadClientItemClientInfo . FromDownloadClient ( this , false ) ;
queueItem . CanMoveFiles = true ;
queueItem . CanBeRemoved = true ;
if ( globalStatus . DownloadPaused | | ( remainingSize = = pausedSize & & remainingSize ! = 0 ) )
{
queueItem . Status = DownloadItemStatus . Paused ;
queueItem . RemainingSize = remainingSize ;
}
else
{
if ( item . ActiveDownloads = = 0 & & remainingSize ! = 0 )
{
queueItem . Status = DownloadItemStatus . Queued ;
}
else
{
queueItem . Status = DownloadItemStatus . Downloading ;
}
queueItem . RemainingSize = remainingSize - pausedSize ;
if ( globalStatus . DownloadRate ! = 0 )
{
queueItem . RemainingTime = TimeSpan . FromSeconds ( ( totalRemainingSize + queueItem . RemainingSize ) / globalStatus . DownloadRate ) ;
totalRemainingSize + = queueItem . RemainingSize ;
}
}
queueItems . Add ( queueItem ) ;
}
return queueItems ;
}
private IEnumerable < DownloadClientItem > GetHistory ( )
{
var history = _proxy . GetHistory ( Settings ) . Take ( _configService . DownloadClientHistoryLimit ) . ToList ( ) ;
var historyItems = new List < DownloadClientItem > ( ) ;
foreach ( var item in history )
{
var droneParameter = item . Parameters . SingleOrDefault ( p = > p . Name = = "drone" ) ;
var historyItem = new DownloadClientItem ( ) ;
var itemDir = item . FinalDir . IsNullOrWhiteSpace ( ) ? item . DestDir : item . FinalDir ;
historyItem . DownloadClientInfo = DownloadClientItemClientInfo . FromDownloadClient ( this , false ) ;
historyItem . DownloadId = droneParameter = = null ? item . Id . ToString ( ) : droneParameter . Value . ToString ( ) ;
historyItem . Title = item . Name ;
historyItem . TotalSize = MakeInt64 ( item . FileSizeHi , item . FileSizeLo ) ;
historyItem . OutputPath = _remotePathMappingService . RemapRemoteToLocal ( Settings . Host , new OsPath ( itemDir ) ) ;
historyItem . Category = item . Category ;
historyItem . Message = $"PAR Status: {item.ParStatus} - Unpack Status: {item.UnpackStatus} - Move Status: {item.MoveStatus} - Script Status: {item.ScriptStatus} - Delete Status: {item.DeleteStatus} - Mark Status: {item.MarkStatus}" ;
historyItem . Status = DownloadItemStatus . Completed ;
historyItem . RemainingTime = TimeSpan . Zero ;
historyItem . CanMoveFiles = true ;
historyItem . CanBeRemoved = true ;
if ( item . DeleteStatus = = "MANUAL" )
{
if ( item . MarkStatus = = "BAD" )
{
historyItem . Status = DownloadItemStatus . Failed ;
historyItems . Add ( historyItem ) ;
}
continue ;
}
if ( ! _successStatus . Contains ( item . ParStatus ) )
{
historyItem . Status = DownloadItemStatus . Failed ;
}
if ( item . UnpackStatus = = "SPACE" )
{
historyItem . Status = DownloadItemStatus . Warning ;
}
else if ( ! _successStatus . Contains ( item . UnpackStatus ) )
{
historyItem . Status = DownloadItemStatus . Failed ;
}
if ( ! _successStatus . Contains ( item . MoveStatus ) )
{
historyItem . Status = DownloadItemStatus . Warning ;
}
if ( ! _successStatus . Contains ( item . ScriptStatus ) )
{
historyItem . Status = DownloadItemStatus . Failed ;
}
if ( ! _successStatus . Contains ( item . DeleteStatus ) & & item . DeleteStatus . IsNotNullOrWhiteSpace ( ) )
{
if ( _deleteFailedStatus . Contains ( item . DeleteStatus ) )
{
historyItem . Status = DownloadItemStatus . Failed ;
}
else
{
historyItem . Status = DownloadItemStatus . Warning ;
}
}
historyItems . Add ( historyItem ) ;
}
return historyItems ;
}
public override string Name = > "NZBGet" ;
public override IEnumerable < DownloadClientItem > GetItems ( )
{
return GetQueue ( ) . Concat ( GetHistory ( ) ) . Where ( downloadClientItem = > downloadClientItem . Category = = Settings . MovieCategory ) ;
}
public override void RemoveItem ( DownloadClientItem item , bool deleteData )
{
if ( deleteData )
{
DeleteItemData ( item ) ;
}
_proxy . RemoveItem ( item . DownloadId , Settings ) ;
}
public override DownloadClientInfo GetStatus ( )
{
var config = _proxy . GetConfig ( Settings ) ;
var category = GetCategories ( config ) . FirstOrDefault ( v = > v . Name = = Settings . MovieCategory ) ;
var status = new DownloadClientInfo
{
IsLocalhost = Settings . Host = = "127.0.0.1" | | Settings . Host = = "localhost"
} ;
if ( category ! = null )
{
status . OutputRootFolders = new List < OsPath > { _remotePathMappingService . RemapRemoteToLocal ( Settings . Host , new OsPath ( category . DestDir ) ) } ;
}
return status ;
}
protected IEnumerable < NzbgetCategory > GetCategories ( Dictionary < string , string > config )
{
for ( var i = 1 ; i < 100 ; i + + )
{
var name = config . GetValueOrDefault ( "Category" + i + ".Name" ) ;
if ( name = = null )
{
yield break ;
}
var destDir = config . GetValueOrDefault ( "Category" + i + ".DestDir" ) ;
if ( destDir . IsNullOrWhiteSpace ( ) )
{
var mainDir = config . GetValueOrDefault ( "MainDir" ) ;
destDir = config . GetValueOrDefault ( "DestDir" , string . Empty ) . Replace ( "${MainDir}" , mainDir ) ;
if ( config . GetValueOrDefault ( "AppendCategoryDir" , "yes" ) = = "yes" )
{
destDir = Path . Combine ( destDir , name ) ;
}
}
yield return new NzbgetCategory
{
Name = name ,
DestDir = destDir ,
Unpack = config . GetValueOrDefault ( "Category" + i + ".Unpack" ) = = "yes" ,
DefScript = config . GetValueOrDefault ( "Category" + i + ".DefScript" ) ,
Aliases = config . GetValueOrDefault ( "Category" + i + ".Aliases" ) ,
} ;
}
}
protected override void Test ( List < ValidationFailure > failures )
{
failures . AddIfNotNull ( TestConnection ( ) ) ;
failures . AddIfNotNull ( TestCategory ( ) ) ;
failures . AddIfNotNull ( TestSettings ( ) ) ;
}
private ValidationFailure TestConnection ( )
{
try
{
var version = _proxy . GetVersion ( Settings ) . Split ( '-' ) [ 0 ] ;
if ( Version . Parse ( version ) < Version . Parse ( "12.0" ) )
{
return new ValidationFailure ( string . Empty , "NZBGet version too low, need 12.0 or higher" ) ;
}
}
catch ( Exception ex )
{
if ( ex . Message . ContainsIgnoreCase ( "Authentication failed" ) )
{
return new ValidationFailure ( "Username" , "Authentication failed" ) ;
}
_logger . Error ( ex , "Unable to connect to NZBGet" ) ;
return new ValidationFailure ( "Host" , "Unable to connect to NZBGet" ) ;
}
return null ;
}
private ValidationFailure TestCategory ( )
{
var config = _proxy . GetConfig ( Settings ) ;
var categories = GetCategories ( config ) ;
if ( ! Settings . MovieCategory . IsNullOrWhiteSpace ( ) & & ! categories . Any ( v = > v . Name = = Settings . MovieCategory ) )
{
return new NzbDroneValidationFailure ( "MovieCategory" , "Category does not exist" )
{
InfoLink = _proxy . GetBaseUrl ( Settings ) ,
DetailedDescription = "The category you entered doesn't exist in NZBGet. Go to NZBGet to create it."
} ;
}
return null ;
}
private ValidationFailure TestSettings ( )
{
var config = _proxy . GetConfig ( Settings ) ;
var keepHistory = config . GetValueOrDefault ( "KeepHistory" , "7" ) ;
if ( ! int . TryParse ( keepHistory , NumberStyles . None , CultureInfo . InvariantCulture , out var value ) | | value = = 0 )
{
return new NzbDroneValidationFailure ( string . Empty , "NzbGet setting KeepHistory should be greater than 0" )
{
InfoLink = _proxy . GetBaseUrl ( Settings ) ,
DetailedDescription = "NzbGet setting KeepHistory is set to 0. Which prevents Radarr from seeing completed downloads."
} ;
}
else if ( value > 25000 )
{
return new NzbDroneValidationFailure ( string . Empty , "NzbGet setting KeepHistory should be less than 25000" )
{
InfoLink = _proxy . GetBaseUrl ( Settings ) ,
DetailedDescription = "NzbGet setting KeepHistory is set too high."
} ;
}
return null ;
}
// Javascript doesn't support 64 bit integers natively so json officially doesn't either.
// NzbGet api thus sends it in two 32 bit chunks. Here we join the two chunks back together.
// Simplified decimal example: "42" splits into "4" and "2". To join them I shift (<<) the "4" 1 digit to the left = "40". combine it with "2". which becomes "42" again.
private long MakeInt64 ( uint high , uint low )
{
long result = high ;
result = ( result < < 32 ) | ( long ) low ;
return result ;
}
}
}