Radarr/src/NzbDrone.Core/MediaFiles/DiskScanService.cs

290 lines
12 KiB
C#

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<string> FilterPaths(string basePath, IEnumerable<string> paths, bool filterExtras = true);
}
public class DiskScanService :
IDiskScanService,
IExecute<RescanMovieCommand>
{
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<string>());
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<MovieFile>();
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<string> 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<string> FilterPaths(string basePath, IEnumerable<string> 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);
}
}
}
}
}