2017-09-26 02:31:52 +00:00
using System ;
2017-02-13 20:18:35 +00:00
using System.Collections.Generic ;
using System.Linq ;
using System.Net ;
using FluentValidation.Results ;
using NLog ;
using NzbDrone.Common.Disk ;
using NzbDrone.Common.Extensions ;
using NzbDrone.Common.Http ;
using NzbDrone.Core.Configuration ;
using NzbDrone.Core.Download.Clients.DownloadStation.Proxies ;
using NzbDrone.Core.Parser.Model ;
using NzbDrone.Core.RemotePathMappings ;
2018-11-19 03:30:32 +00:00
using NzbDrone.Core.ThingiProvider ;
2017-02-13 20:18:35 +00:00
using NzbDrone.Core.Validation ;
namespace NzbDrone.Core.Download.Clients.DownloadStation
{
public class UsenetDownloadStation : UsenetClientBase < DownloadStationSettings >
{
2017-03-07 19:43:58 +00:00
protected readonly IDownloadStationInfoProxy _dsInfoProxy ;
2021-10-02 18:36:17 +00:00
protected readonly IDownloadStationTaskProxySelector _dsTaskProxySelector ;
2017-02-13 20:18:35 +00:00
protected readonly ISharedFolderResolver _sharedFolderResolver ;
protected readonly ISerialNumberProvider _serialNumberProvider ;
2017-02-15 13:01:19 +00:00
protected readonly IFileStationProxy _fileStationProxy ;
2017-02-13 20:18:35 +00:00
2017-02-15 13:01:19 +00:00
public UsenetDownloadStation ( ISharedFolderResolver sharedFolderResolver ,
ISerialNumberProvider serialNumberProvider ,
IFileStationProxy fileStationProxy ,
2017-03-07 19:43:58 +00:00
IDownloadStationInfoProxy dsInfoProxy ,
2021-10-02 18:36:17 +00:00
IDownloadStationTaskProxySelector dsTaskProxySelector ,
2017-02-13 20:18:35 +00:00
IHttpClient httpClient ,
IConfigService configService ,
IDiskProvider diskProvider ,
IRemotePathMappingService remotePathMappingService ,
2017-11-26 03:55:50 +00:00
IValidateNzbs nzbValidationService ,
2020-01-03 12:49:24 +00:00
Logger logger )
2017-11-26 03:55:50 +00:00
: base ( httpClient , configService , diskProvider , remotePathMappingService , nzbValidationService , logger )
2017-02-13 20:18:35 +00:00
{
2017-03-07 19:43:58 +00:00
_dsInfoProxy = dsInfoProxy ;
2021-10-02 18:36:17 +00:00
_dsTaskProxySelector = dsTaskProxySelector ;
2017-02-15 13:01:19 +00:00
_fileStationProxy = fileStationProxy ;
2017-02-13 20:18:35 +00:00
_sharedFolderResolver = sharedFolderResolver ;
_serialNumberProvider = serialNumberProvider ;
}
public override string Name = > "Download Station" ;
2018-11-19 03:30:32 +00:00
public override ProviderMessage Message = > new ProviderMessage ( "Lidarr is unable to connect to Download Station if 2-Factor Authentication is enabled on your DSM account" , ProviderMessageType . Warning ) ;
2021-10-02 18:36:17 +00:00
private IDownloadStationTaskProxy DsTaskProxy = > _dsTaskProxySelector . GetProxy ( Settings ) ;
2017-02-22 18:07:10 +00:00
protected IEnumerable < DownloadStationTask > GetTasks ( )
{
2021-10-02 18:36:17 +00:00
return DsTaskProxy . GetTasks ( Settings ) . Where ( v = > v . Type . ToLower ( ) = = DownloadStationTaskType . NZB . ToString ( ) . ToLower ( ) ) ;
2017-02-22 18:07:10 +00:00
}
2017-02-13 20:18:35 +00:00
public override IEnumerable < DownloadClientItem > GetItems ( )
{
2017-02-22 18:07:10 +00:00
var nzbTasks = GetTasks ( ) ;
2017-02-13 20:18:35 +00:00
var serialNumber = _serialNumberProvider . GetSerialNumber ( Settings ) ;
var items = new List < DownloadClientItem > ( ) ;
long totalRemainingSize = 0 ;
long globalSpeed = nzbTasks . Where ( t = > t . Status = = DownloadStationTaskStatus . Downloading )
. Select ( GetDownloadSpeed )
. Sum ( ) ;
foreach ( var nzb in nzbTasks )
{
var outputPath = new OsPath ( $"/{nzb.Additional.Detail[" destination "]}" ) ;
var taskRemainingSize = GetRemainingSize ( nzb ) ;
if ( nzb . Status ! = DownloadStationTaskStatus . Paused )
{
totalRemainingSize + = taskRemainingSize ;
}
if ( Settings . TvDirectory . IsNotNullOrWhiteSpace ( ) )
{
if ( ! new OsPath ( $"/{Settings.TvDirectory}" ) . Contains ( outputPath ) )
{
continue ;
}
}
2018-02-16 02:52:15 +00:00
else if ( Settings . MusicCategory . IsNotNullOrWhiteSpace ( ) )
2017-02-13 20:18:35 +00:00
{
var directories = outputPath . FullPath . Split ( '\\' , '/' ) ;
2018-02-16 02:52:15 +00:00
if ( ! directories . Contains ( Settings . MusicCategory ) )
2017-02-13 20:18:35 +00:00
{
continue ;
}
}
var item = new DownloadClientItem ( )
{
2018-02-16 02:52:15 +00:00
Category = Settings . MusicCategory ,
2020-11-16 21:34:49 +00:00
DownloadClientInfo = DownloadClientItemClientInfo . FromDownloadClient ( this ) ,
2017-02-13 20:18:35 +00:00
DownloadId = CreateDownloadId ( nzb . Id , serialNumber ) ,
Title = nzb . Title ,
TotalSize = nzb . Size ,
RemainingSize = taskRemainingSize ,
Status = GetStatus ( nzb ) ,
Message = GetMessage ( nzb ) ,
2017-09-26 02:31:52 +00:00
CanBeRemoved = true ,
CanMoveFiles = true
2017-02-13 20:18:35 +00:00
} ;
if ( item . Status ! = DownloadItemStatus . Paused )
{
item . RemainingTime = GetRemainingTime ( totalRemainingSize , globalSpeed ) ;
}
if ( item . Status = = DownloadItemStatus . Completed | | item . Status = = DownloadItemStatus . Failed )
{
item . OutputPath = GetOutputPath ( outputPath , nzb , serialNumber ) ;
}
items . Add ( item ) ;
}
return items ;
}
protected OsPath GetOutputPath ( OsPath outputPath , DownloadStationTask task , string serialNumber )
{
var fullPath = _sharedFolderResolver . RemapToFullPath ( outputPath , Settings , serialNumber ) ;
var remotePath = _remotePathMappingService . RemapRemoteToLocal ( Settings . Host , fullPath ) ;
var finalPath = remotePath + task . Title ;
return finalPath ;
}
2017-10-08 03:54:13 +00:00
public override DownloadClientInfo GetStatus ( )
2017-02-13 20:18:35 +00:00
{
try
{
2019-05-08 05:06:05 +00:00
var serialNumber = _serialNumberProvider . GetSerialNumber ( Settings ) ;
var sharedFolder = GetDownloadDirectory ( ) ? ? GetDefaultDir ( ) ;
var outputPath = new OsPath ( $"/{sharedFolder.TrimStart('/')}" ) ;
var path = _sharedFolderResolver . RemapToFullPath ( outputPath , Settings , serialNumber ) ;
2017-02-13 20:18:35 +00:00
2017-10-08 03:54:13 +00:00
return new DownloadClientInfo
2017-02-13 20:18:35 +00:00
{
IsLocalhost = Settings . Host = = "127.0.0.1" | | Settings . Host = = "localhost" ,
2019-05-08 05:06:05 +00:00
OutputRootFolders = new List < OsPath > { _remotePathMappingService . RemapRemoteToLocal ( Settings . Host , path ) }
2017-02-13 20:18:35 +00:00
} ;
}
catch ( DownloadClientException e )
{
_logger . Debug ( e , "Failed to get config from Download Station" ) ;
2018-06-02 01:59:54 +00:00
throw ;
2017-02-13 20:18:35 +00:00
}
}
public override void RemoveItem ( string downloadId , bool deleteData )
{
if ( deleteData )
{
DeleteItemData ( downloadId ) ;
}
2021-10-02 18:36:17 +00:00
DsTaskProxy . RemoveTask ( ParseDownloadId ( downloadId ) , Settings ) ;
2017-02-13 20:18:35 +00:00
_logger . Debug ( "{0} removed correctly" , downloadId ) ;
}
2017-08-14 02:58:42 +00:00
protected override string AddFromNzbFile ( RemoteAlbum remoteAlbum , string filename , byte [ ] fileContent )
2017-02-13 20:18:35 +00:00
{
var hashedSerialNumber = _serialNumberProvider . GetSerialNumber ( Settings ) ;
2021-10-02 18:36:17 +00:00
DsTaskProxy . AddTaskFromData ( fileContent , filename , GetDownloadDirectory ( ) , Settings ) ;
2017-02-13 20:18:35 +00:00
2017-02-22 18:07:10 +00:00
var items = GetTasks ( ) . Where ( t = > t . Additional . Detail [ "uri" ] = = filename ) ;
2017-02-13 20:18:35 +00:00
var item = items . SingleOrDefault ( ) ;
if ( item ! = null )
{
2017-08-14 02:58:42 +00:00
_logger . Debug ( "{0} added correctly" , remoteAlbum ) ;
2017-02-13 20:18:35 +00:00
return CreateDownloadId ( item . Id , hashedSerialNumber ) ;
}
_logger . Debug ( "No such task {0} in Download Station" , filename ) ;
throw new DownloadClientException ( "Failed to add NZB task to Download Station" ) ;
}
protected override void Test ( List < ValidationFailure > failures )
{
failures . AddIfNotNull ( TestConnection ( ) ) ;
2020-01-03 12:49:24 +00:00
if ( failures . HasErrors ( ) )
{
return ;
}
2017-02-15 13:01:19 +00:00
failures . AddIfNotNull ( TestOutputPath ( ) ) ;
2017-02-13 20:18:35 +00:00
failures . AddIfNotNull ( TestGetNZB ( ) ) ;
}
2017-02-15 13:01:19 +00:00
protected ValidationFailure TestOutputPath ( )
{
try
{
2017-03-01 17:46:16 +00:00
var downloadDir = GetDefaultDir ( ) ;
if ( downloadDir = = null )
{
return new NzbDroneValidationFailure ( nameof ( Settings . TvDirectory ) , "No default destination" )
{
DetailedDescription = $"You must login into your Diskstation as {Settings.Username} and manually set it up into DownloadStation settings under BT/HTTP/FTP/NZB -> Location."
} ;
}
downloadDir = GetDownloadDirectory ( ) ;
2017-02-15 13:01:19 +00:00
if ( downloadDir ! = null )
{
var sharedFolder = downloadDir . Split ( '\\' , '/' ) [ 0 ] ;
2018-02-16 02:52:15 +00:00
var fieldName = Settings . TvDirectory . IsNotNullOrWhiteSpace ( ) ? nameof ( Settings . TvDirectory ) : nameof ( Settings . MusicCategory ) ;
2017-02-15 13:01:19 +00:00
var folderInfo = _fileStationProxy . GetInfoFileOrDirectory ( $"/{downloadDir}" , Settings ) ;
if ( folderInfo . Additional = = null )
{
return new NzbDroneValidationFailure ( fieldName , $"Shared folder does not exist" )
{
2017-03-01 17:46:16 +00:00
DetailedDescription = $"The Diskstation does not have a Shared Folder with the name '{sharedFolder}', are you sure you specified it correctly?"
2017-02-15 13:01:19 +00:00
} ;
}
if ( ! folderInfo . IsDir )
{
return new NzbDroneValidationFailure ( fieldName , $"Folder does not exist" )
{
2017-02-21 17:19:55 +00:00
DetailedDescription = $"The folder '{downloadDir}' does not exist, it must be created manually inside the Shared Folder '{sharedFolder}'."
2017-02-15 13:01:19 +00:00
} ;
}
}
return null ;
}
2020-01-03 12:49:24 +00:00
catch ( DownloadClientAuthenticationException ex )
2017-03-01 17:46:16 +00:00
{
2020-01-03 12:49:24 +00:00
// User could not have permission to access to downloadstation
2019-03-13 23:10:58 +00:00
_logger . Error ( ex , "Unable to authenticate" ) ;
2017-03-01 17:46:16 +00:00
return new NzbDroneValidationFailure ( string . Empty , ex . Message ) ;
}
2017-02-15 13:01:19 +00:00
catch ( Exception ex )
{
2017-10-08 05:14:26 +00:00
_logger . Error ( ex , "Error testing Usenet Download Station" ) ;
2017-02-15 13:01:19 +00:00
return new NzbDroneValidationFailure ( string . Empty , $"Unknown exception: {ex.Message}" ) ;
}
}
2017-02-13 20:18:35 +00:00
protected ValidationFailure TestConnection ( )
{
try
{
return ValidateVersion ( ) ;
}
catch ( DownloadClientAuthenticationException ex )
{
2019-03-13 23:10:58 +00:00
_logger . Error ( ex , "Unable to authenticate" ) ;
2017-02-13 20:18:35 +00:00
return new NzbDroneValidationFailure ( "Username" , "Authentication failure" )
{
2017-03-30 03:49:38 +00:00
DetailedDescription = $"Please verify your username and password. Also verify if the host running Lidarr isn't blocked from accessing {Name} by WhiteList limitations in the {Name} configuration."
2017-02-13 20:18:35 +00:00
} ;
}
catch ( WebException ex )
{
2017-10-08 05:14:26 +00:00
_logger . Error ( ex , "Unable to connect to Usenet Download Station" ) ;
2017-02-13 20:18:35 +00:00
if ( ex . Status = = WebExceptionStatus . ConnectFailure )
{
return new NzbDroneValidationFailure ( "Host" , "Unable to connect" )
{
DetailedDescription = "Please verify the hostname and port."
} ;
}
2020-01-03 12:49:24 +00:00
2017-02-13 20:18:35 +00:00
return new NzbDroneValidationFailure ( string . Empty , "Unknown exception: " + ex . Message ) ;
}
catch ( Exception ex )
{
2017-10-08 05:14:26 +00:00
_logger . Error ( ex , "Error testing Torrent Download Station" ) ;
2020-10-25 22:59:40 +00:00
return new NzbDroneValidationFailure ( "Host" , "Unable to connect to Usenet Download Station" )
{
DetailedDescription = ex . Message
} ;
2017-02-13 20:18:35 +00:00
}
}
protected ValidationFailure ValidateVersion ( )
{
2021-10-02 18:36:17 +00:00
var info = DsTaskProxy . GetApiInfo ( Settings ) ;
2017-02-13 20:18:35 +00:00
2017-03-07 19:43:58 +00:00
_logger . Debug ( "Download Station api version information: Min {0} - Max {1}" , info . MinVersion , info . MaxVersion ) ;
2017-02-13 20:18:35 +00:00
2017-03-07 19:43:58 +00:00
if ( info . MinVersion > 2 | | info . MaxVersion < 2 )
2017-02-13 20:18:35 +00:00
{
2017-03-07 19:43:58 +00:00
return new ValidationFailure ( string . Empty , $"Download Station API version not supported, should be at least 2. It supports from {info.MinVersion} to {info.MaxVersion}" ) ;
2017-02-13 20:18:35 +00:00
}
return null ;
}
protected string GetMessage ( DownloadStationTask task )
{
if ( task . StatusExtra ! = null )
{
if ( task . Status = = DownloadStationTaskStatus . Extracting )
{
return $"Extracting: {int.Parse(task.StatusExtra[" unzip_progress "])}%" ;
}
if ( task . Status = = DownloadStationTaskStatus . Error )
{
return task . StatusExtra [ "error_detail" ] ;
}
}
return null ;
}
protected DownloadItemStatus GetStatus ( DownloadStationTask task )
{
switch ( task . Status )
{
2018-01-14 19:34:08 +00:00
case DownloadStationTaskStatus . Unknown :
2017-02-13 20:18:35 +00:00
case DownloadStationTaskStatus . Waiting :
2018-01-14 19:34:08 +00:00
case DownloadStationTaskStatus . FilehostingWaiting :
2017-02-13 20:18:35 +00:00
return task . Size = = 0 | | GetRemainingSize ( task ) > 0 ? DownloadItemStatus . Queued : DownloadItemStatus . Completed ;
case DownloadStationTaskStatus . Paused :
return DownloadItemStatus . Paused ;
case DownloadStationTaskStatus . Finished :
case DownloadStationTaskStatus . Seeding :
return DownloadItemStatus . Completed ;
case DownloadStationTaskStatus . Error :
return DownloadItemStatus . Failed ;
}
return DownloadItemStatus . Downloading ;
}
protected long GetRemainingSize ( DownloadStationTask task )
{
var downloadedString = task . Additional . Transfer [ "size_downloaded" ] ;
long downloadedSize ;
if ( downloadedString . IsNullOrWhiteSpace ( ) | | ! long . TryParse ( downloadedString , out downloadedSize ) )
{
_logger . Debug ( "Task {0} has invalid size_downloaded: {1}" , task . Title , downloadedString ) ;
downloadedSize = 0 ;
}
return task . Size - Math . Max ( 0 , downloadedSize ) ;
}
protected long GetDownloadSpeed ( DownloadStationTask task )
{
var speedString = task . Additional . Transfer [ "speed_download" ] ;
long downloadSpeed ;
if ( speedString . IsNullOrWhiteSpace ( ) | | ! long . TryParse ( speedString , out downloadSpeed ) )
{
_logger . Debug ( "Task {0} has invalid speed_download: {1}" , task . Title , speedString ) ;
downloadSpeed = 0 ;
}
return Math . Max ( downloadSpeed , 0 ) ;
}
protected TimeSpan ? GetRemainingTime ( long remainingSize , long downloadSpeed )
{
if ( downloadSpeed > 0 )
{
return TimeSpan . FromSeconds ( remainingSize / downloadSpeed ) ;
}
else
{
return null ;
}
}
protected ValidationFailure TestGetNZB ( )
{
try
{
2017-02-22 18:07:10 +00:00
GetItems ( ) ;
2017-02-13 20:18:35 +00:00
return null ;
}
catch ( Exception ex )
{
return new NzbDroneValidationFailure ( string . Empty , "Failed to get the list of NZBs: " + ex . Message ) ;
}
}
protected string ParseDownloadId ( string id )
{
return id . Split ( ':' ) [ 1 ] ;
}
protected string CreateDownloadId ( string id , string hashedSerialNumber )
{
return $"{hashedSerialNumber}:{id}" ;
}
protected string GetDefaultDir ( )
{
2017-03-07 19:43:58 +00:00
var config = _dsInfoProxy . GetConfig ( Settings ) ;
2017-02-13 20:18:35 +00:00
var path = config [ "default_destination" ] as string ;
return path ;
}
protected string GetDownloadDirectory ( )
{
if ( Settings . TvDirectory . IsNotNullOrWhiteSpace ( ) )
{
return Settings . TvDirectory . TrimStart ( '/' ) ;
}
2021-10-02 18:36:17 +00:00
var destDir = GetDefaultDir ( ) ;
if ( Settings . MusicCategory . IsNotNullOrWhiteSpace ( ) )
{
2018-02-16 02:52:15 +00:00
return $"{destDir.TrimEnd('/')}/{Settings.MusicCategory}" ;
2017-02-13 20:18:35 +00:00
}
2021-10-02 18:36:17 +00:00
return destDir . TrimEnd ( '/' ) ;
2017-02-13 20:18:35 +00:00
}
}
}