using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Text.RegularExpressions; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Common.Instrumentation.Extensions; using NzbDrone.Core.Configuration; using NzbDrone.Core.MediaFiles.Commands; using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.MediaFiles.MediaInfo; using NzbDrone.Core.MediaFiles.MovieImport; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Movies; using NzbDrone.Core.RootFolders; namespace NzbDrone.Core.MediaFiles { public interface IDiskScanService { void Scan(Movie movie); string[] GetVideoFiles(string path, bool allDirectories = true); string[] GetNonVideoFiles(string path, bool allDirectories = true); List FilterPaths(string basePath, IEnumerable paths, bool filterExtras = true); } public class DiskScanService : IDiskScanService, IExecute { private readonly IDiskProvider _diskProvider; private readonly IMakeImportDecision _importDecisionMaker; private readonly IImportApprovedMovie _importApprovedMovies; private readonly IConfigService _configService; private readonly IMovieService _movieService; private readonly IMediaFileService _mediaFileService; private readonly IMediaFileTableCleanupService _mediaFileTableCleanupService; private readonly IRootFolderService _rootFolderService; private readonly IUpdateMediaInfo _updateMediaInfoService; private readonly IEventAggregator _eventAggregator; private readonly Logger _logger; public DiskScanService(IDiskProvider diskProvider, IMakeImportDecision importDecisionMaker, IImportApprovedMovie importApprovedMovies, IConfigService configService, IMovieService movieService, IMediaFileService mediaFileService, IMediaFileTableCleanupService mediaFileTableCleanupService, IRootFolderService rootFolderService, IUpdateMediaInfo updateMediaInfoService, IEventAggregator eventAggregator, Logger logger) { _diskProvider = diskProvider; _importDecisionMaker = importDecisionMaker; _importApprovedMovies = importApprovedMovies; _configService = configService; _movieService = movieService; _mediaFileService = mediaFileService; _mediaFileTableCleanupService = mediaFileTableCleanupService; _rootFolderService = rootFolderService; _updateMediaInfoService = updateMediaInfoService; _eventAggregator = eventAggregator; _logger = logger; } private static readonly Regex ExcludedExtrasSubFolderRegex = new Regex(@"(?:\\|\/|^)(?:extras|extrafanart|behind the scenes|deleted scenes|featurettes|interviews|scenes|sample[s]?|shorts|trailers)(?:\\|\/)", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex ExcludedSubFoldersRegex = new Regex(@"(?:\\|\/|^)(?:@eadir|\.@__thumb|plex versions|\.[^\\/]+)(?:\\|\/)", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex ExcludedExtraFilesRegex = new Regex(@"(-(trailer|other|behindthescenes|deleted|featurette|interview|scene|short)\.[^.]+$)", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex ExcludedFilesRegex = new Regex(@"^\._|^Thumbs\.db$", RegexOptions.Compiled | RegexOptions.IgnoreCase); public void Scan(Movie movie) { var rootFolder = _rootFolderService.GetBestRootFolderPath(movie.Path); var movieFolderExists = _diskProvider.FolderExists(movie.Path); if (!movieFolderExists) { if (!_diskProvider.FolderExists(rootFolder)) { _logger.Warn("Movie's root folder ({0}) doesn't exist.", rootFolder); _eventAggregator.PublishEvent(new MovieScanSkippedEvent(movie, MovieScanSkippedReason.RootFolderDoesNotExist)); return; } if (_diskProvider.FolderEmpty(rootFolder)) { _logger.Warn("Movie's root folder ({0}) is empty.", rootFolder); _eventAggregator.PublishEvent(new MovieScanSkippedEvent(movie, MovieScanSkippedReason.RootFolderIsEmpty)); return; } } _logger.ProgressInfo("Scanning disk for {0}", movie.Title); if (!movieFolderExists) { if (_configService.CreateEmptyMovieFolders) { if (_configService.DeleteEmptyFolders) { _logger.Debug("Not creating missing movie folder: {0} because delete empty movie folders is enabled", movie.Path); } else { _logger.Debug("Creating missing movie folder: {0}", movie.Path); _diskProvider.CreateFolder(movie.Path); SetPermissions(movie.Path); } } else { _logger.Debug("Movie's folder doesn't exist: {0}", movie.Path); } CleanMediaFiles(movie, new List()); CompletedScanning(movie); return; } var videoFilesStopwatch = Stopwatch.StartNew(); var mediaFileList = FilterPaths(movie.Path, GetVideoFiles(movie.Path)).ToList(); videoFilesStopwatch.Stop(); _logger.Trace("Finished getting movie files for: {0} [{1}]", movie, videoFilesStopwatch.Elapsed); CleanMediaFiles(movie, mediaFileList); var movieFiles = _mediaFileService.GetFilesByMovie(movie.Id); var unmappedFiles = MediaFileService.FilterExistingFiles(mediaFileList, movieFiles, movie); var decisionsStopwatch = Stopwatch.StartNew(); var decisions = _importDecisionMaker.GetImportDecisions(unmappedFiles, movie, false); decisionsStopwatch.Stop(); _logger.Trace("Import decisions complete for: {0} [{1}]", movie, decisionsStopwatch.Elapsed); _importApprovedMovies.Import(decisions, false); // Update existing files that have a different file size var fileInfoStopwatch = Stopwatch.StartNew(); var filesToUpdate = new List(); foreach (var file in movieFiles) { var path = Path.Combine(movie.Path, file.RelativePath); var fileSize = _diskProvider.GetFileSize(path); if (file.Size == fileSize) { continue; } file.Size = fileSize; if (!_updateMediaInfoService.Update(file, movie)) { filesToUpdate.Add(file); } } // Update any files that had a file size change, but didn't get media info updated. if (filesToUpdate.Any()) { _mediaFileService.Update(filesToUpdate); } fileInfoStopwatch.Stop(); _logger.Trace("Reprocessing existing files complete for: {0} [{1}]", movie, decisionsStopwatch.Elapsed); RemoveEmptyMovieFolder(movie.Path); CompletedScanning(movie); } private void CleanMediaFiles(Movie movie, List mediaFileList) { _logger.Debug("{0} Cleaning up media files in DB", movie); _mediaFileTableCleanupService.Clean(movie, mediaFileList); } private void CompletedScanning(Movie movie) { _logger.Info("Completed scanning disk for {0}", movie.Title); _eventAggregator.PublishEvent(new MovieScannedEvent(movie)); } public string[] GetVideoFiles(string path, bool allDirectories = true) { _logger.Debug("Scanning '{0}' for video files", path); var searchOption = allDirectories ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; var filesOnDisk = _diskProvider.GetFiles(path, searchOption).ToList(); var mediaFileList = filesOnDisk.Where(file => MediaFileExtensions.Extensions.Contains(Path.GetExtension(file))) .ToList(); _logger.Trace("{0} files were found in {1}", filesOnDisk.Count, path); _logger.Debug("{0} video files were found in {1}", mediaFileList.Count, path); return mediaFileList.ToArray(); } public string[] GetNonVideoFiles(string path, bool allDirectories = true) { _logger.Debug("Scanning '{0}' for non-video files", path); var searchOption = allDirectories ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; var filesOnDisk = _diskProvider.GetFiles(path, searchOption).ToList(); var mediaFileList = filesOnDisk.Where(file => !MediaFileExtensions.Extensions.Contains(Path.GetExtension(file))) .ToList(); _logger.Trace("{0} files were found in {1}", filesOnDisk.Count, path); _logger.Debug("{0} non-video files were found in {1}", mediaFileList.Count, path); return mediaFileList.ToArray(); } public List FilterPaths(string basePath, IEnumerable paths, bool filterExtras = true) { var filteredPaths = paths.Where(path => !ExcludedSubFoldersRegex.IsMatch(basePath.GetRelativePath(path))) .Where(path => !ExcludedFilesRegex.IsMatch(Path.GetFileName(path))) .ToList(); if (filterExtras) { filteredPaths = filteredPaths.Where(path => !ExcludedExtrasSubFolderRegex.IsMatch(basePath.GetRelativePath(path))) .Where(path => !ExcludedExtraFilesRegex.IsMatch(Path.GetFileName(path))) .ToList(); } return filteredPaths; } private void SetPermissions(string path) { if (!_configService.SetPermissionsLinux) { return; } try { _diskProvider.SetPermissions(path, _configService.ChmodFolder, _configService.ChownGroup); } catch (Exception ex) { _logger.Warn(ex, "Unable to apply permissions to: " + path); _logger.Debug(ex, ex.Message); } } private void RemoveEmptyMovieFolder(string path) { if (_configService.DeleteEmptyFolders) { _diskProvider.RemoveEmptySubfolders(path); if (_diskProvider.FolderEmpty(path)) { _diskProvider.DeleteFolder(path, true); } } } public void Execute(RescanMovieCommand message) { if (message.MovieId.HasValue) { var movie = _movieService.GetMovie(message.MovieId.Value); Scan(movie); } else { var allMovies = _movieService.GetAllMovies(); foreach (var movie in allMovies) { Scan(movie); } } } } }