2018-02-15 12:39:01 +00:00
using System ;
using System.Collections.Generic ;
2017-01-11 02:12:47 +00:00
using System.IO ;
using System.Linq ;
using NLog ;
using NzbDrone.Common.Disk ;
2019-07-01 01:50:01 +00:00
using NzbDrone.Common.EnvironmentInfo ;
2022-08-08 02:54:39 +00:00
using NzbDrone.Common.Extensions ;
2017-06-17 13:02:58 +00:00
using NzbDrone.Core.Configuration ;
2017-01-11 02:12:47 +00:00
using NzbDrone.Core.DecisionEngine ;
using NzbDrone.Core.Download ;
2018-08-05 14:28:05 +00:00
using NzbDrone.Core.History ;
2019-12-22 22:08:53 +00:00
using NzbDrone.Core.MediaFiles.MovieImport ;
using NzbDrone.Core.Movies ;
using NzbDrone.Core.Parser ;
2017-01-11 02:12:47 +00:00
using NzbDrone.Core.Parser.Model ;
namespace NzbDrone.Core.MediaFiles
{
public interface IDownloadedMovieImportService
{
List < ImportResult > ProcessRootFolder ( DirectoryInfo directoryInfo ) ;
List < ImportResult > ProcessPath ( string path , ImportMode importMode = ImportMode . Auto , Movie movie = null , DownloadClientItem downloadClientItem = null ) ;
bool ShouldDeleteFolder ( DirectoryInfo directoryInfo , Movie movie ) ;
}
public class DownloadedMovieImportService : IDownloadedMovieImportService
{
private readonly IDiskProvider _diskProvider ;
private readonly IDiskScanService _diskScanService ;
private readonly IMovieService _movieService ;
private readonly IParsingService _parsingService ;
private readonly IMakeImportDecision _importDecisionMaker ;
private readonly IImportApprovedMovie _importApprovedMovie ;
private readonly IDetectSample _detectSample ;
2019-07-01 01:50:01 +00:00
private readonly IRuntimeInfo _runtimeInfo ;
2017-06-17 13:02:58 +00:00
private readonly IConfigService _config ;
2018-08-05 14:28:05 +00:00
private readonly IHistoryService _historyService ;
2017-01-11 02:12:47 +00:00
private readonly Logger _logger ;
public DownloadedMovieImportService ( IDiskProvider diskProvider ,
IDiskScanService diskScanService ,
IMovieService movieService ,
IParsingService parsingService ,
IMakeImportDecision importDecisionMaker ,
IImportApprovedMovie importApprovedMovie ,
IDetectSample detectSample ,
2019-07-01 01:50:01 +00:00
IRuntimeInfo runtimeInfo ,
2017-06-17 13:02:58 +00:00
IConfigService config ,
2018-08-05 14:28:05 +00:00
IHistoryService historyService ,
2017-01-11 02:12:47 +00:00
Logger logger )
{
_diskProvider = diskProvider ;
_diskScanService = diskScanService ;
_movieService = movieService ;
_parsingService = parsingService ;
_importDecisionMaker = importDecisionMaker ;
_importApprovedMovie = importApprovedMovie ;
_detectSample = detectSample ;
2019-07-01 01:50:01 +00:00
_runtimeInfo = runtimeInfo ;
2017-06-17 13:02:58 +00:00
_config = config ;
2018-08-05 14:28:05 +00:00
_historyService = historyService ;
2017-01-11 02:12:47 +00:00
_logger = logger ;
}
public List < ImportResult > ProcessRootFolder ( DirectoryInfo directoryInfo )
{
var results = new List < ImportResult > ( ) ;
foreach ( var subFolder in _diskProvider . GetDirectories ( directoryInfo . FullName ) )
{
var folderResults = ProcessFolder ( new DirectoryInfo ( subFolder ) , ImportMode . Auto , null ) ;
results . AddRange ( folderResults ) ;
}
foreach ( var videoFile in _diskScanService . GetVideoFiles ( directoryInfo . FullName , false ) )
{
var fileResults = ProcessFile ( new FileInfo ( videoFile ) , ImportMode . Auto , null ) ;
results . AddRange ( fileResults ) ;
}
return results ;
}
public List < ImportResult > ProcessPath ( string path , ImportMode importMode = ImportMode . Auto , Movie movie = null , DownloadClientItem downloadClientItem = null )
{
2020-03-29 17:48:34 +00:00
_logger . Debug ( "Processing path: {0}" , path ) ;
2017-01-11 02:12:47 +00:00
if ( _diskProvider . FolderExists ( path ) )
{
var directoryInfo = new DirectoryInfo ( path ) ;
if ( movie = = null )
{
return ProcessFolder ( directoryInfo , importMode , downloadClientItem ) ;
}
return ProcessFolder ( directoryInfo , importMode , movie , downloadClientItem ) ;
}
if ( _diskProvider . FileExists ( path ) )
{
var fileInfo = new FileInfo ( path ) ;
if ( movie = = null )
{
return ProcessFile ( fileInfo , importMode , downloadClientItem ) ;
}
return ProcessFile ( fileInfo , importMode , movie , downloadClientItem ) ;
}
2019-07-01 01:50:01 +00:00
LogInaccessiblePathError ( path ) ;
2017-01-11 02:12:47 +00:00
return new List < ImportResult > ( ) ;
}
public bool ShouldDeleteFolder ( DirectoryInfo directoryInfo , Movie movie )
{
2020-03-29 17:48:34 +00:00
try
{
var videoFiles = _diskScanService . GetVideoFiles ( directoryInfo . FullName ) ;
2021-02-14 06:19:25 +00:00
var rarFiles = _diskProvider . GetFiles ( directoryInfo . FullName , SearchOption . AllDirectories )
. Where ( f = > Path . GetExtension ( f )
. Equals ( ".rar" , StringComparison . OrdinalIgnoreCase ) ) ;
2017-01-11 02:12:47 +00:00
2020-03-29 17:48:34 +00:00
foreach ( var videoFile in videoFiles )
2017-01-11 02:12:47 +00:00
{
2020-03-29 17:48:34 +00:00
var movieParseResult =
2020-04-08 16:54:05 +00:00
Parser . Parser . ParseMovieTitle ( Path . GetFileName ( videoFile ) ) ;
2020-03-29 17:48:34 +00:00
if ( movieParseResult = = null )
{
_logger . Warn ( "Unable to parse file on import: [{0}]" , videoFile ) ;
return false ;
}
2022-03-20 15:55:47 +00:00
if ( _detectSample . IsSample ( movie . MovieMetadata , videoFile ) ! = DetectSampleResult . Sample )
2020-03-29 17:48:34 +00:00
{
_logger . Warn ( "Non-sample file detected: [{0}]" , videoFile ) ;
return false ;
}
2017-01-11 02:12:47 +00:00
}
2020-03-29 17:48:34 +00:00
if ( rarFiles . Any ( f = > _diskProvider . GetFileSize ( f ) > 10. Megabytes ( ) ) )
2017-01-11 02:12:47 +00:00
{
2020-03-29 17:48:34 +00:00
_logger . Warn ( "RAR file detected, will require manual cleanup" ) ;
2017-01-11 02:12:47 +00:00
return false ;
}
2020-03-29 17:48:34 +00:00
return true ;
}
catch ( DirectoryNotFoundException e )
2017-01-11 02:12:47 +00:00
{
2020-03-29 17:48:34 +00:00
_logger . Debug ( e , "Folder {0} has already been removed" , directoryInfo . FullName ) ;
2017-01-11 02:12:47 +00:00
return false ;
}
2021-02-14 06:19:25 +00:00
catch ( Exception e )
{
_logger . Debug ( e , "Unable to determine whether folder {0} should be removed" , directoryInfo . FullName ) ;
return false ;
}
2017-01-11 02:12:47 +00:00
}
private List < ImportResult > ProcessFolder ( DirectoryInfo directoryInfo , ImportMode importMode , DownloadClientItem downloadClientItem )
{
var cleanedUpName = GetCleanedUpFolderName ( directoryInfo . Name ) ;
var movie = _parsingService . GetMovie ( cleanedUpName ) ;
if ( movie = = null )
{
_logger . Debug ( "Unknown Movie {0}" , cleanedUpName ) ;
return new List < ImportResult >
{
UnknownMovieResult ( "Unknown Movie" )
} ;
}
return ProcessFolder ( directoryInfo , importMode , movie , downloadClientItem ) ;
}
private List < ImportResult > ProcessFolder ( DirectoryInfo directoryInfo , ImportMode importMode , Movie movie , DownloadClientItem downloadClientItem )
{
if ( _movieService . MoviePathExists ( directoryInfo . FullName ) )
{
2021-04-06 16:29:34 +00:00
_logger . Warn ( "Unable to process folder that is mapped to an existing movie" ) ;
2022-12-13 07:05:01 +00:00
return new List < ImportResult >
{
RejectionResult ( "Import path is mapped to a movie folder" )
} ;
2017-01-11 02:12:47 +00:00
}
var cleanedUpName = GetCleanedUpFolderName ( directoryInfo . Name ) ;
2018-08-05 14:28:05 +00:00
var historyItems = _historyService . FindByDownloadId ( downloadClientItem ? . DownloadId ? ? "" ) ;
var firstHistoryItem = historyItems ? . OrderByDescending ( h = > h . Date ) . FirstOrDefault ( ) ;
2019-12-22 22:08:53 +00:00
var folderInfo = _parsingService . ParseMovieInfo ( cleanedUpName , new List < object > { firstHistoryItem } ) ;
2017-01-11 02:12:47 +00:00
if ( folderInfo ! = null )
{
_logger . Debug ( "{0} folder quality: {1}" , cleanedUpName , folderInfo . Quality ) ;
}
2020-09-05 05:23:24 +00:00
var videoFiles = _diskScanService . FilterPaths ( directoryInfo . FullName , _diskScanService . GetVideoFiles ( directoryInfo . FullName ) ) ;
2017-01-11 02:12:47 +00:00
if ( downloadClientItem = = null )
{
foreach ( var videoFile in videoFiles )
{
if ( _diskProvider . IsFileLocked ( videoFile ) )
{
return new List < ImportResult >
{
FileIsLockedResult ( videoFile )
} ;
}
}
}
2019-07-16 02:27:35 +00:00
var decisions = _importDecisionMaker . GetImportDecisions ( videoFiles . ToList ( ) , movie , downloadClientItem , folderInfo , true ) ;
2017-01-11 02:12:47 +00:00
var importResults = _importApprovedMovie . Import ( decisions , true , downloadClientItem , importMode ) ;
2020-05-10 16:34:16 +00:00
if ( importMode = = ImportMode . Auto )
{
importMode = ( downloadClientItem = = null | | downloadClientItem . CanMoveFiles ) ? ImportMode . Move : ImportMode . Copy ;
}
if ( importMode = = ImportMode . Move & &
2017-01-11 02:12:47 +00:00
importResults . Any ( i = > i . Result = = ImportResultType . Imported ) & &
ShouldDeleteFolder ( directoryInfo , movie ) )
{
_logger . Debug ( "Deleting folder after importing valid files" ) ;
_diskProvider . DeleteFolder ( directoryInfo . FullName , true ) ;
}
2022-12-13 07:05:01 +00:00
else if ( importResults . Empty ( ) )
{
importResults . AddIfNotNull ( CheckEmptyResultForIssue ( directoryInfo . FullName ) ) ;
}
2017-01-11 02:12:47 +00:00
return importResults ;
}
private List < ImportResult > ProcessFile ( FileInfo fileInfo , ImportMode importMode , DownloadClientItem downloadClientItem )
{
var movie = _parsingService . GetMovie ( Path . GetFileNameWithoutExtension ( fileInfo . Name ) ) ;
if ( movie = = null )
{
_logger . Debug ( "Unknown Movie for file: {0}" , fileInfo . Name ) ;
return new List < ImportResult >
{
UnknownMovieResult ( string . Format ( "Unknown Movie for file: {0}" , fileInfo . Name ) , fileInfo . FullName )
} ;
}
return ProcessFile ( fileInfo , importMode , movie , downloadClientItem ) ;
}
private List < ImportResult > ProcessFile ( FileInfo fileInfo , ImportMode importMode , Movie movie , DownloadClientItem downloadClientItem )
{
if ( Path . GetFileNameWithoutExtension ( fileInfo . Name ) . StartsWith ( "._" ) )
{
_logger . Debug ( "[{0}] starts with '._', skipping" , fileInfo . FullName ) ;
return new List < ImportResult >
{
2018-03-14 20:41:36 +00:00
new ImportResult ( new ImportDecision ( new LocalMovie { Path = fileInfo . FullName } , new Rejection ( "Invalid video file, filename starts with '._'" ) ) , "Invalid video file, filename starts with '._'" )
2017-01-11 02:12:47 +00:00
} ;
2022-08-08 02:54:39 +00:00
}
var extension = Path . GetExtension ( fileInfo . Name ) ;
if ( extension . IsNullOrWhiteSpace ( ) | | ! MediaFileExtensions . Extensions . Contains ( extension ) )
{
_logger . Debug ( "[{0}] has an unsupported extension: '{1}'" , fileInfo . FullName , extension ) ;
return new List < ImportResult >
{
new ImportResult ( new ImportDecision ( new LocalMovie { Path = fileInfo . FullName } ,
new Rejection ( $"Invalid video file, unsupported extension: '{extension}'" ) ) ,
$"Invalid video file, unsupported extension: '{extension}'" )
} ;
2017-01-11 02:12:47 +00:00
}
if ( downloadClientItem = = null )
{
if ( _diskProvider . IsFileLocked ( fileInfo . FullName ) )
{
return new List < ImportResult >
{
FileIsLockedResult ( fileInfo . FullName )
} ;
}
}
2019-07-16 02:27:35 +00:00
var decisions = _importDecisionMaker . GetImportDecisions ( new List < string > ( ) { fileInfo . FullName } , movie , downloadClientItem , null , true ) ;
2017-01-11 02:12:47 +00:00
return _importApprovedMovie . Import ( decisions , true , downloadClientItem , importMode ) ;
}
private string GetCleanedUpFolderName ( string folder )
{
folder = folder . Replace ( "_UNPACK_" , "" )
. Replace ( "_FAILED_" , "" ) ;
return folder ;
}
private ImportResult FileIsLockedResult ( string videoFile )
{
_logger . Debug ( "[{0}] is currently locked by another process, skipping" , videoFile ) ;
2018-03-14 20:41:36 +00:00
return new ImportResult ( new ImportDecision ( new LocalMovie { Path = videoFile } , new Rejection ( "Locked file, try again later" ) ) , "Locked file, try again later" ) ;
2017-01-11 02:12:47 +00:00
}
private ImportResult UnknownMovieResult ( string message , string videoFile = null )
{
2018-03-14 20:41:36 +00:00
var localMovie = videoFile = = null ? null : new LocalMovie { Path = videoFile } ;
2017-01-11 02:12:47 +00:00
2018-03-14 20:41:36 +00:00
return new ImportResult ( new ImportDecision ( localMovie , new Rejection ( "Unknown Movie" ) ) , message ) ;
2017-01-11 02:12:47 +00:00
}
2019-07-01 01:50:01 +00:00
2022-12-13 07:05:01 +00:00
private ImportResult RejectionResult ( string message )
{
return new ImportResult ( new ImportDecision ( null , new Rejection ( message ) ) , message ) ;
}
private ImportResult CheckEmptyResultForIssue ( string folder )
{
var files = _diskProvider . GetFiles ( folder , SearchOption . AllDirectories ) ;
if ( files . Any ( file = > FileExtensions . ExecutableExtensions . Contains ( Path . GetExtension ( file ) ) ) )
{
return RejectionResult ( "Caution: Found executable file" ) ;
}
if ( files . Any ( file = > FileExtensions . ArchiveExtensions . Contains ( Path . GetExtension ( file ) ) ) )
{
return RejectionResult ( "Found archive file, might need to be extracted" ) ;
}
return null ;
}
2019-07-01 01:50:01 +00:00
private void LogInaccessiblePathError ( string path )
{
if ( _runtimeInfo . IsWindowsService )
{
var mounts = _diskProvider . GetMounts ( ) ;
var mount = mounts . FirstOrDefault ( m = > m . RootDirectory = = Path . GetPathRoot ( path ) ) ;
2020-03-29 17:48:34 +00:00
if ( mount = = null )
{
_logger . Error ( "Import failed, path does not exist or is not accessible by Radarr: {0}. Unable to find a volume mounted for the path. If you're using a mapped network drive see the FAQ for more info" , path ) ;
return ;
}
2019-07-01 01:50:01 +00:00
if ( mount . DriveType = = DriveType . Network )
{
2019-10-26 19:58:30 +00:00
_logger . Error ( "Import failed, path does not exist or is not accessible by Radarr: {0}. It's recommended to avoid mapped network drives when running as a Windows service. See the FAQ for more info" , path ) ;
2019-07-01 01:50:01 +00:00
return ;
}
}
if ( OsInfo . IsWindows )
{
if ( path . StartsWith ( @"\\" ) )
{
2019-10-26 19:58:30 +00:00
_logger . Error ( "Import failed, path does not exist or is not accessible by Radarr: {0}. Ensure the user running Radarr has access to the network share" , path ) ;
2019-07-01 01:50:01 +00:00
return ;
}
}
2019-10-26 19:58:30 +00:00
_logger . Error ( "Import failed, path does not exist or is not accessible by Radarr: {0}. Ensure the path exists and the user running Radarr has the correct permissions to access this file/folder" , path ) ;
2019-07-01 01:50:01 +00:00
}
2017-01-11 02:12:47 +00:00
}
}