1
0
mirror of https://github.com/Radarr/Radarr.git synced 2026-04-26 22:46:53 -04:00

Added: A Huge Cleanup of old Series Code. (Let's pray nothing breaks :P) (#2589)

This commit is contained in:
Qstick
2018-03-14 16:41:36 -04:00
committed by Leonardo Galli
parent 8a6d67a6d7
commit 25e10e26e3
551 changed files with 2835 additions and 18867 deletions
@@ -0,0 +1,112 @@
using System;
using System.Collections.Generic;
using System.IO;
using NLog;
using NzbDrone.Core.MediaFiles.MediaInfo;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Movies;
namespace NzbDrone.Core.MediaFiles.MovieImport
{
public interface IDetectSample
{
bool IsSample(Movie movie, QualityModel quality, string path, long size, bool isSpecial);
}
public class DetectSample : IDetectSample
{
private readonly IVideoFileInfoReader _videoFileInfoReader;
private readonly Logger _logger;
private static List<Quality> _largeSampleSizeQualities = new List<Quality> { Quality.HDTV1080p, Quality.WEBDL1080p, Quality.Bluray1080p };
public DetectSample(IVideoFileInfoReader videoFileInfoReader, Logger logger)
{
_videoFileInfoReader = videoFileInfoReader;
_logger = logger;
}
public static long SampleSizeLimit => 70.Megabytes();
public bool IsSample(Movie movie, QualityModel quality, string path, long size, bool isSpecial)
{
if (isSpecial)
{
_logger.Debug("Special, skipping sample check");
return false;
}
var extension = Path.GetExtension(path);
if (extension != null && extension.Equals(".flv", StringComparison.InvariantCultureIgnoreCase))
{
_logger.Debug("Skipping sample check for .flv file");
return false;
}
if (extension != null && extension.Equals(".strm", StringComparison.InvariantCultureIgnoreCase))
{
_logger.Debug("Skipping sample check for .strm file");
return false;
}
try
{
var runTime = _videoFileInfoReader.GetRunTime(path);
var minimumRuntime = GetMinimumAllowedRuntime(movie);
if (runTime.TotalMinutes.Equals(0))
{
_logger.Error("[{0}] has a runtime of 0, is it a valid video file?", path);
return true;
}
if (runTime.TotalSeconds < minimumRuntime)
{
_logger.Debug("[{0}] appears to be a sample. Runtime: {1} seconds. Expected at least: {2} seconds", path, runTime, minimumRuntime);
return true;
}
}
catch (DllNotFoundException)
{
_logger.Debug("Falling back to file size detection");
return CheckSize(size, quality);
}
_logger.Debug("Runtime is over 90 seconds");
return false;
}
private bool CheckSize(long size, QualityModel quality)
{
if (_largeSampleSizeQualities.Contains(quality.Quality))
{
if (size < SampleSizeLimit * 2)
{
_logger.Debug("1080p file is less than sample limit");
return true;
}
}
if (size < SampleSizeLimit)
{
_logger.Debug("File is less than sample limit");
return true;
}
return false;
}
private int GetMinimumAllowedRuntime(Movie movie)
{
if (movie.Runtime < 1)
{
return 5 * 60;
}
return movie.Runtime / 5 * 60;
}
}
}
@@ -0,0 +1,11 @@
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Download;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.MediaFiles.MovieImport
{
public interface IImportDecisionEngineSpecification
{
Decision IsSatisfiedBy(LocalMovie localMovie, DownloadClientItem downloadClientItem);
}
}
@@ -0,0 +1,178 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Download;
using NzbDrone.Core.Extras;
namespace NzbDrone.Core.MediaFiles.MovieImport
{
public interface IImportApprovedMovie
{
List<ImportResult> Import(List<ImportDecision> decisions, bool newDownload, DownloadClientItem downloadClientItem = null, ImportMode importMode = ImportMode.Auto);
}
public class ImportApprovedMovie : IImportApprovedMovie
{
private readonly IUpgradeMediaFiles _movieFileUpgrader;
private readonly IMediaFileService _mediaFileService;
private readonly IExtraService _extraService;
private readonly IDiskProvider _diskProvider;
private readonly IEventAggregator _eventAggregator;
private readonly Logger _logger;
public ImportApprovedMovie(IUpgradeMediaFiles movieFileUpgrader,
IMediaFileService mediaFileService,
IExtraService extraService,
IDiskProvider diskProvider,
IEventAggregator eventAggregator,
Logger logger)
{
_movieFileUpgrader = movieFileUpgrader;
_mediaFileService = mediaFileService;
_extraService = extraService;
_diskProvider = diskProvider;
_eventAggregator = eventAggregator;
_logger = logger;
}
public List<ImportResult> Import(List<ImportDecision> decisions, bool newDownload, DownloadClientItem downloadClientItem = null, ImportMode importMode = ImportMode.Auto)
{
_logger.Debug("Decisions: {0}", decisions.Count);
//I added a null op for the rare case that the quality is null. TODO: find out why that would even happen in the first place.
var qualifiedImports = decisions.Where(c => c.Approved)
.GroupBy(c => c.LocalMovie.Movie.Id, (i, s) => s
.OrderByDescending(c => c.LocalMovie.Quality ?? new QualityModel{Quality = Quality.Unknown}, new QualityModelComparer(s.First().LocalMovie.Movie.Profile))
.ThenByDescending(c => c.LocalMovie.Size))
.SelectMany(c => c)
.ToList();
var importResults = new List<ImportResult>();
foreach (var importDecision in qualifiedImports.OrderBy(e => e.LocalMovie.Size)
.ThenByDescending(e => e.LocalMovie.Size))
{
var localMovie = importDecision.LocalMovie;
var oldFiles = new List<MovieFile>();
try
{
//check if already imported
if (importResults.Select(r => r.ImportDecision.LocalMovie.Movie)
.Select(m => m.Id).Contains(localMovie.Movie.Id))
{
importResults.Add(new ImportResult(importDecision, "Movie has already been imported"));
continue;
}
var movieFile = new MovieFile();
movieFile.DateAdded = DateTime.UtcNow;
movieFile.MovieId = localMovie.Movie.Id;
movieFile.Path = localMovie.Path.CleanFilePath();
movieFile.Size = _diskProvider.GetFileSize(localMovie.Path);
movieFile.Quality = localMovie.Quality;
movieFile.MediaInfo = localMovie.MediaInfo;
movieFile.Movie = localMovie.Movie;
movieFile.ReleaseGroup = localMovie.ParsedMovieInfo.ReleaseGroup;
movieFile.Edition = localMovie.ParsedMovieInfo.Edition;
bool copyOnly;
switch (importMode)
{
default:
case ImportMode.Auto:
copyOnly = downloadClientItem != null && downloadClientItem.IsReadOnly;
break;
case ImportMode.Move:
copyOnly = false;
break;
case ImportMode.Copy:
copyOnly = true;
break;
}
if (newDownload)
{
movieFile.SceneName = GetSceneName(downloadClientItem, localMovie);
var moveResult = _movieFileUpgrader.UpgradeMovieFile(movieFile, localMovie, copyOnly); //TODO: Check if this works
oldFiles = moveResult.OldFiles;
}
else
{
movieFile.RelativePath = localMovie.Movie.Path.GetRelativePath(movieFile.Path);
}
_mediaFileService.Add(movieFile);
importResults.Add(new ImportResult(importDecision));
if (newDownload)
{
_extraService.ImportExtraFiles(localMovie, movieFile, copyOnly);
}
if (downloadClientItem != null)
{
_eventAggregator.PublishEvent(new MovieImportedEvent(localMovie, movieFile, newDownload, downloadClientItem.DownloadClient, downloadClientItem.DownloadId, downloadClientItem.IsReadOnly));
}
else
{
_eventAggregator.PublishEvent(new MovieImportedEvent(localMovie, movieFile, newDownload));
}
if (newDownload)
{
_eventAggregator.PublishEvent(new MovieDownloadedEvent(localMovie, movieFile, oldFiles, downloadClientItem));
}
}
catch (Exception e)
{
_logger.Warn(e, "Couldn't import movie " + localMovie);
importResults.Add(new ImportResult(importDecision, "Failed to import movie"));
}
}
//Adding all the rejected decisions
importResults.AddRange(decisions.Where(c => !c.Approved)
.Select(d => new ImportResult(d, d.Rejections.Select(r => r.Reason).ToArray())));
return importResults;
}
private string GetSceneName(DownloadClientItem downloadClientItem, LocalMovie localMovie)
{
if (downloadClientItem != null)
{
var title = Parser.Parser.RemoveFileExtension(downloadClientItem.Title);
var parsedTitle = Parser.Parser.ParseMovieTitle(title, false);
if (parsedTitle != null)
{
return title;
}
}
var fileName = Path.GetFileNameWithoutExtension(localMovie.Path.CleanFilePath());
if (SceneChecker.IsSceneTitle(fileName))
{
return fileName;
}
return null;
}
}
}
@@ -0,0 +1,31 @@
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.MediaFiles.MovieImport
{
public class ImportDecision
{
public LocalMovie LocalMovie { get; private set; }
public IEnumerable<Rejection> Rejections { get; private set; }
public bool Approved => Rejections.Empty();
public ImportDecision(LocalMovie localMovie, params Rejection[] rejections)
{
LocalMovie = localMovie;
Rejections = rejections.ToList();
//LocalMovie = new LocalMovie
//{
// Quality = localMovie.Quality,
// ExistingFile = localMovie.ExistingFile,
// MediaInfo = localMovie.MediaInfo,
// ParsedMovieInfo = localMovie.ParsedMovieInfo,
// Path = localMovie.Path,
// Size = localMovie.Size
//};
}
}
}
@@ -0,0 +1,414 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Download;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Movies;
using NzbDrone.Core.MediaFiles.MediaInfo;
namespace NzbDrone.Core.MediaFiles.MovieImport
{
public interface IMakeImportDecision
{
List<ImportDecision> GetImportDecisions(List<string> videoFiles, Movie movie);
List<ImportDecision> GetImportDecisions(List<string> videoFiles, Movie movie, bool shouldCheckQuality);
List<ImportDecision> GetImportDecisions(List<string> videoFiles, Movie movie, DownloadClientItem downloadClientItem, ParsedMovieInfo folderInfo, bool sceneSource, bool shouldCheckQuality);
List<ImportDecision> GetImportDecisions(List<string> videoFiles, Movie movie, DownloadClientItem downloadClientItem, ParsedMovieInfo folderInfo, bool sceneSource);
}
public class ImportDecisionMaker : IMakeImportDecision
{
private readonly IEnumerable<IImportDecisionEngineSpecification> _specifications;
private readonly IParsingService _parsingService;
private readonly IMediaFileService _mediaFileService;
private readonly IDiskProvider _diskProvider;
private readonly IVideoFileInfoReader _videoFileInfoReader;
private readonly IDetectSample _detectSample;
private readonly IQualityDefinitionService _qualitiesService;
private readonly Logger _logger;
public ImportDecisionMaker(IEnumerable<IImportDecisionEngineSpecification> specifications,
IParsingService parsingService,
IMediaFileService mediaFileService,
IDiskProvider diskProvider,
IVideoFileInfoReader videoFileInfoReader,
IDetectSample detectSample,
IQualityDefinitionService qualitiesService,
Logger logger)
{
_specifications = specifications;
_parsingService = parsingService;
_mediaFileService = mediaFileService;
_diskProvider = diskProvider;
_videoFileInfoReader = videoFileInfoReader;
_detectSample = detectSample;
_qualitiesService = qualitiesService;
_logger = logger;
}
public List<ImportDecision> GetImportDecisions(List<string> videoFiles, Movie movie)
{
return GetImportDecisions(videoFiles, movie, null, null, true, false);
}
public List<ImportDecision> GetImportDecisions(List<string> videoFiles, Movie movie, bool shouldCheckQuality = false)
{
return GetImportDecisions(videoFiles, movie, null, null, true, shouldCheckQuality);
}
public List<ImportDecision> GetImportDecisions(List<string> videoFiles, Movie movie, DownloadClientItem downloadClientItem, ParsedMovieInfo folderInfo, bool sceneSource)
{
var newFiles = _mediaFileService.FilterExistingFiles(videoFiles.ToList(), movie);
_logger.Debug("Analyzing {0}/{1} files.", newFiles.Count, videoFiles.Count());
var shouldUseFolderName = ShouldUseFolderName(videoFiles, movie, folderInfo);
var decisions = new List<ImportDecision>();
foreach (var file in newFiles)
{
decisions.AddIfNotNull(GetDecision(file, movie, downloadClientItem, folderInfo, sceneSource, shouldUseFolderName));
}
return decisions;
}
public List<ImportDecision> GetImportDecisions(List<string> videoFiles, Movie movie, DownloadClientItem downloadClientItem, ParsedMovieInfo folderInfo, bool sceneSource, bool shouldCheckQuality)
{
var newFiles = _mediaFileService.FilterExistingFiles(videoFiles.ToList(), movie);
_logger.Debug("Analyzing {0}/{1} files.", newFiles.Count, videoFiles.Count());
var shouldUseFolderName = ShouldUseFolderName(videoFiles, movie, folderInfo);
var decisions = new List<ImportDecision>();
foreach (var file in newFiles)
{
decisions.AddIfNotNull(GetDecision(file, movie, downloadClientItem, folderInfo, sceneSource, shouldUseFolderName, shouldCheckQuality));
}
return decisions;
}
private ImportDecision GetDecision(string file, Movie movie, DownloadClientItem downloadClientItem, ParsedMovieInfo folderInfo, bool sceneSource, bool shouldUseFolderName, bool shouldCheckQuality = false)
{
ImportDecision decision = null;
try
{
var localMovie = _parsingService.GetLocalMovie(file, movie, shouldUseFolderName ? folderInfo : null, sceneSource);
if (localMovie != null)
{
localMovie.Quality = GetQuality(folderInfo, localMovie.Quality, movie);
localMovie.Size = _diskProvider.GetFileSize(file);
_logger.Debug("Size: {0}", localMovie.Size);
var current = localMovie.Quality;
localMovie.MediaInfo = _videoFileInfoReader.GetMediaInfo(file);
//TODO: make it so media info doesn't ruin the import process of a new movie
if (sceneSource && ShouldCheckQualityForParsedQuality(current.Quality))
{
if (shouldCheckQuality)
{
_logger.Debug("Checking quality for this video file to make sure nothing mismatched.");
var width = localMovie.MediaInfo.Width;
var qualityName = current.Quality.Name.ToLower();
QualityModel updated = null;
if (width > 2000)
{
if (qualityName.Contains("bluray"))
{
updated = new QualityModel(Quality.Bluray2160p);
}
else if (qualityName.Contains("webdl"))
{
updated = new QualityModel(Quality.WEBDL2160p);
}
else if (qualityName.Contains("hdtv"))
{
updated = new QualityModel(Quality.HDTV2160p);
}
else
{
var def = _qualitiesService.Get(Quality.HDTV2160p);
if (localMovie.Size > def.MinSize && def.MaxSize > localMovie.Size)
{
updated = new QualityModel(Quality.HDTV2160p);
}
def = _qualitiesService.Get(Quality.WEBDL2160p);
if (localMovie.Size > def.MinSize && def.MaxSize > localMovie.Size)
{
updated = new QualityModel(Quality.WEBDL2160p);
}
def = _qualitiesService.Get(Quality.Bluray2160p);
if (localMovie.Size > def.MinSize && def.MaxSize > localMovie.Size)
{
updated = new QualityModel(Quality.Bluray2160p);
}
if (updated == null)
{
updated = new QualityModel(Quality.Bluray2160p);
}
}
}
else if (width > 1400)
{
if (qualityName.Contains("bluray"))
{
updated = new QualityModel(Quality.Bluray1080p);
}
else if (qualityName.Contains("webdl"))
{
updated = new QualityModel(Quality.WEBDL1080p);
}
else if (qualityName.Contains("hdtv"))
{
updated = new QualityModel(Quality.HDTV1080p);
}
else
{
var def = _qualitiesService.Get(Quality.HDTV1080p);
if (localMovie.Size > def.MinSize && def.MaxSize > localMovie.Size)
{
updated = new QualityModel(Quality.HDTV1080p);
}
def = _qualitiesService.Get(Quality.WEBDL1080p);
if (localMovie.Size > def.MinSize && def.MaxSize > localMovie.Size)
{
updated = new QualityModel(Quality.WEBDL1080p);
}
def = _qualitiesService.Get(Quality.Bluray1080p);
if (localMovie.Size > def.MinSize && def.MaxSize > localMovie.Size)
{
updated = new QualityModel(Quality.Bluray1080p);
}
if (updated == null)
{
updated = new QualityModel(Quality.Bluray1080p);
}
}
}
else
if (width > 900)
{
if (qualityName.Contains("bluray"))
{
updated = new QualityModel(Quality.Bluray720p);
}
else if (qualityName.Contains("webdl"))
{
updated = new QualityModel(Quality.WEBDL720p);
}
else if (qualityName.Contains("hdtv"))
{
updated = new QualityModel(Quality.HDTV720p);
}
else
{
var def = _qualitiesService.Get(Quality.HDTV720p);
if (localMovie.Size > def.MinSize && def.MaxSize > localMovie.Size)
{
updated = new QualityModel(Quality.HDTV720p);
}
def = _qualitiesService.Get(Quality.WEBDL720p);
if (localMovie.Size > def.MinSize && def.MaxSize > localMovie.Size)
{
updated = new QualityModel(Quality.WEBDL720p);
}
def = _qualitiesService.Get(Quality.Bluray720p);
if (localMovie.Size > def.MinSize && def.MaxSize > localMovie.Size)
{
updated = new QualityModel(Quality.Bluray720p);
}
if (updated == null)
{
updated = new QualityModel(Quality.Bluray720p);
}
}
}
if (updated != null && updated != current)
{
_logger.Debug("Quality ({0}) of the file is different than the one we have ({1})", updated, current);
updated.QualitySource = QualitySource.MediaInfo;
localMovie.Quality = updated;
}
}
decision = GetDecision(localMovie, downloadClientItem);
}
else
{
decision = GetDecision(localMovie, downloadClientItem);
}
}
else
{
localMovie = new LocalMovie();
localMovie.Path = file;
decision = new ImportDecision(localMovie, new Rejection("Unable to parse file"));
}
}
catch (Exception e)
{
_logger.Error(e, "Couldn't import file. " + file);
var localMovie = new LocalMovie { Path = file };
decision = new ImportDecision(localMovie, new Rejection("Unexpected error processing file"));
}
//LocalMovie nullMovie = null;
//decision = new ImportDecision(nullMovie, new Rejection("IMPLEMENTATION MISSING!!!"));
return decision;
}
private ImportDecision GetDecision(LocalMovie localMovie, DownloadClientItem downloadClientItem)
{
var reasons = _specifications.Select(c => EvaluateSpec(c, localMovie, downloadClientItem))
.Where(c => c != null);
return new ImportDecision(localMovie, reasons.ToArray());
}
private Rejection EvaluateSpec(IImportDecisionEngineSpecification spec, LocalMovie localMovie, DownloadClientItem downloadClientItem)
{
try
{
var result = spec.IsSatisfiedBy(localMovie, downloadClientItem);
if (!result.Accepted)
{
return new Rejection(result.Reason);
}
}
catch (NotImplementedException e)
{
_logger.Warn(e, "Spec " + spec.ToString() + " currently does not implement evaluation for movies.");
return null;
}
catch (Exception e)
{
//e.Data.Add("report", remoteEpisode.Report.ToJson());
//e.Data.Add("parsed", remoteEpisode.ParsedEpisodeInfo.ToJson());
_logger.Error(e, "Couldn't evaluate decision on " + localMovie.Path);
return new Rejection(string.Format("{0}: {1}", spec.GetType().Name, e.Message));
}
return null;
}
private bool ShouldUseFolderName(List<string> videoFiles, Movie movie, ParsedMovieInfo folderInfo)
{
if (folderInfo == null)
{
return false;
}
//if (folderInfo.FullSeason)
//{
// return false;
//}
return videoFiles.Count(file =>
{
var size = _diskProvider.GetFileSize(file);
var fileQuality = QualityParser.ParseQuality(file);
//var sample = null;//_detectSample.IsSample(movie, GetQuality(folderInfo, fileQuality, movie), file, size, folderInfo.IsPossibleSpecialEpisode); //Todo to this
return true;
//if (sample)
{
return false;
}
if (SceneChecker.IsSceneTitle(Path.GetFileName(file)))
{
return false;
}
return true;
}) == 1;
}
private QualityModel GetQuality(ParsedMovieInfo folderInfo, QualityModel fileQuality, Movie movie)
{
if (UseFolderQuality(folderInfo, fileQuality, movie))
{
_logger.Debug("Using quality from folder: {0}", folderInfo.Quality);
return folderInfo.Quality;
}
return fileQuality;
}
private bool UseFolderQuality(ParsedMovieInfo folderInfo, QualityModel fileQuality, Movie movie)
{
if (folderInfo == null)
{
return false;
}
if (folderInfo.Quality.Quality == Quality.Unknown)
{
return false;
}
if (fileQuality.QualitySource == QualitySource.Extension)
{
return true;
}
if (new QualityModelComparer(movie.Profile).Compare(folderInfo.Quality, fileQuality) > 0)
{
return true;
}
return false;
}
private bool ShouldCheckQualityForParsedQuality(Quality quality)
{
List<Quality> shouldNotCheck = new List<Quality> { Quality.WORKPRINT, Quality.TELECINE, Quality.TELESYNC,
Quality.DVDSCR, Quality.DVD, Quality.CAM, Quality.DVDR, Quality.Remux1080p, Quality.Remux2160p, Quality.REGIONAL
};
if (shouldNotCheck.Contains(quality))
{
return false;
}
return true;
}
}
}
@@ -0,0 +1,9 @@
namespace NzbDrone.Core.MediaFiles.MovieImport
{
public enum ImportMode
{
Auto = 0,
Move = 1,
Copy = 2
}
}
@@ -0,0 +1,38 @@
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Common.EnsureThat;
namespace NzbDrone.Core.MediaFiles.MovieImport
{
public class ImportResult
{
public ImportDecision ImportDecision { get; private set; }
public List<string> Errors { get; private set; }
public ImportResultType Result
{
get
{
if (Errors.Any())
{
if (ImportDecision.Approved)
{
return ImportResultType.Skipped;
}
return ImportResultType.Rejected;
}
return ImportResultType.Imported;
}
}
public ImportResult(ImportDecision importDecision, params string[] errors)
{
Ensure.That(importDecision, () => importDecision).IsNotNull();
ImportDecision = importDecision;
Errors = errors.ToList();
}
}
}
@@ -0,0 +1,9 @@
namespace NzbDrone.Core.MediaFiles.MovieImport
{
public enum ImportResultType
{
Imported,
Rejected,
Skipped
}
}
@@ -0,0 +1,14 @@
using System.Collections.Generic;
using NzbDrone.Core.Messaging.Commands;
namespace NzbDrone.Core.MediaFiles.MovieImport.Manual
{
public class ManualImportCommand : Command
{
public List<ManualImportFile> Files { get; set; }
public override bool SendUpdatesToClient => true;
public ImportMode ImportMode { get; set; }
}
}
@@ -0,0 +1,15 @@
using System.Collections.Generic;
using NzbDrone.Core.Qualities;
namespace NzbDrone.Core.MediaFiles.MovieImport.Manual
{
public class ManualImportFile
{
public string Path { get; set; }
public int SeriesId { get; set; }
public List<int> EpisodeIds { get; set; }
public QualityModel Quality { get; set; }
public string DownloadId { get; set; }
public int MovieId { get; set; }
}
}
@@ -0,0 +1,19 @@
using System.Collections.Generic;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Movies;
namespace NzbDrone.Core.MediaFiles.MovieImport.Manual
{
public class ManualImportItem
{
public string Path { get; set; }
public string RelativePath { get; set; }
public string Name { get; set; }
public long Size { get; set; }
public QualityModel Quality { get; set; }
public string DownloadId { get; set; }
public IEnumerable<Rejection> Rejections { get; set; }
public Movie Movie { get; set; }
}
}
@@ -0,0 +1,285 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Download;
using NzbDrone.Core.Download.TrackedDownloads;
using NzbDrone.Core.MediaFiles.MediaInfo;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Movies;
namespace NzbDrone.Core.MediaFiles.MovieImport.Manual
{
public interface IManualImportService
{
List<ManualImportItem> GetMediaFiles(string path, string downloadId);
}
public class ManualImportService : IExecute<ManualImportCommand>, IManualImportService
{
private readonly IDiskProvider _diskProvider;
private readonly IParsingService _parsingService;
private readonly IDiskScanService _diskScanService;
private readonly IMakeImportDecision _importDecisionMaker;
private readonly IMovieService _movieService;
private readonly IVideoFileInfoReader _videoFileInfoReader;
private readonly IImportApprovedMovie _importApprovedMovie;
private readonly ITrackedDownloadService _trackedDownloadService;
private readonly IDownloadedMovieImportService _downloadedMovieImportService;
private readonly IEventAggregator _eventAggregator;
private readonly IConfigService _config;
private readonly Logger _logger;
public ManualImportService(IDiskProvider diskProvider,
IParsingService parsingService,
IDiskScanService diskScanService,
IMakeImportDecision importDecisionMaker,
IMovieService movieService,
IVideoFileInfoReader videoFileInfoReader,
IImportApprovedMovie importApprovedMovie,
ITrackedDownloadService trackedDownloadService,
IDownloadedMovieImportService downloadedMovieImportService,
IEventAggregator eventAggregator,
IConfigService config,
Logger logger)
{
_diskProvider = diskProvider;
_parsingService = parsingService;
_diskScanService = diskScanService;
_importDecisionMaker = importDecisionMaker;
_movieService = movieService;
_videoFileInfoReader = videoFileInfoReader;
_importApprovedMovie = importApprovedMovie;
_trackedDownloadService = trackedDownloadService;
_downloadedMovieImportService = downloadedMovieImportService;
_eventAggregator = eventAggregator;
_config = config;
_logger = logger;
}
public List<ManualImportItem> GetMediaFiles(string path, string downloadId)
{
if (downloadId.IsNotNullOrWhiteSpace())
{
var trackedDownload = _trackedDownloadService.Find(downloadId);
if (trackedDownload == null)
{
return new List<ManualImportItem>();
}
path = trackedDownload.DownloadItem.OutputPath.FullPath;
}
if (!_diskProvider.FolderExists(path))
{
if (!_diskProvider.FileExists(path))
{
return new List<ManualImportItem>();
}
return new List<ManualImportItem> { ProcessFile(path, downloadId) };
}
return ProcessFolder(path, downloadId);
}
private List<ManualImportItem> ProcessFolder(string folder, string downloadId)
{
DownloadClientItem downloadClientItem = null;
var directoryInfo = new DirectoryInfo(folder);
var movie = _parsingService.GetMovie(directoryInfo.Name);
if (downloadId.IsNotNullOrWhiteSpace())
{
var trackedDownload = _trackedDownloadService.Find(downloadId);
downloadClientItem = trackedDownload.DownloadItem;
if (movie == null)
{
movie = trackedDownload.RemoteMovie.Movie;
}
}
if (movie == null)
{
var files = _diskScanService.GetVideoFiles(folder);
return files.Select(file => ProcessFile(file, downloadId, folder)).Where(i => i != null).ToList();
}
var folderInfo = Parser.Parser.ParseMovieTitle(directoryInfo.Name, _config.ParsingLeniency > 0);
var movieFiles = _diskScanService.GetVideoFiles(folder).ToList();
var decisions = _importDecisionMaker.GetImportDecisions(movieFiles, movie, downloadClientItem, folderInfo, SceneSource(movie, folder), false);
return decisions.Select(decision => MapItem(decision, folder, downloadId)).ToList();
}
private ManualImportItem ProcessFile(string file, string downloadId, string folder = null)
{
if (folder.IsNullOrWhiteSpace())
{
folder = new FileInfo(file).Directory.FullName;
}
DownloadClientItem downloadClientItem = null;
var relativeFile = folder.GetRelativePath(file);
var movie = _parsingService.GetMovie(relativeFile.Split('\\', '/')[0]);
if (movie == null)
{
movie = _parsingService.GetMovie(relativeFile);
}
if (downloadId.IsNotNullOrWhiteSpace())
{
var trackedDownload = _trackedDownloadService.Find(downloadId);
downloadClientItem = trackedDownload.DownloadItem;
if (movie == null)
{
movie = trackedDownload.RemoteMovie.Movie;
}
}
if (movie == null)
{
var localMovie = new LocalMovie()
{
Path = file,
Quality = QualityParser.ParseQuality(file),
Size = _diskProvider.GetFileSize(file)
};
return MapItem(new ImportDecision(localMovie, new Rejection("Unknown Movie")), folder, downloadId);
}
var importDecisions = _importDecisionMaker.GetImportDecisions(new List<string> { file },
movie, downloadClientItem, null, SceneSource(movie, folder), true);
return importDecisions.Any() ? MapItem(importDecisions.First(), folder, downloadId) : new ManualImportItem
{
DownloadId = downloadId,
Path = file,
RelativePath = folder.GetRelativePath(file),
Name = Path.GetFileNameWithoutExtension(file),
Rejections = new List<Rejection>
{
new Rejection("Unable to process file")
}
};
}
private bool SceneSource(Movie movie, string folder)
{
return !(movie.Path.PathEquals(folder) || movie.Path.IsParentPath(folder));
}
private ManualImportItem MapItem(ImportDecision decision, string folder, string downloadId)
{
var item = new ManualImportItem();
item.Path = decision.LocalMovie.Path;
item.RelativePath = folder.GetRelativePath(decision.LocalMovie.Path);
item.Name = Path.GetFileNameWithoutExtension(decision.LocalMovie.Path);
item.DownloadId = downloadId;
if (decision.LocalMovie.Movie != null)
{
item.Movie = decision.LocalMovie.Movie;
}
item.Quality = decision.LocalMovie.Quality;
item.Size = _diskProvider.GetFileSize(decision.LocalMovie.Path);
item.Rejections = decision.Rejections;
return item;
}
public void Execute(ManualImportCommand message)
{
_logger.ProgressTrace("Manually importing {0} files using mode {1}", message.Files.Count, message.ImportMode);
var imported = new List<ImportResult>();
var importedTrackedDownload = new List<ManuallyImportedFile>();
for (int i = 0; i < message.Files.Count; i++)
{
_logger.ProgressTrace("Processing file {0} of {1}", i + 1, message.Files.Count);
var file = message.Files[i];
var movie = _movieService.GetMovie(file.MovieId);
var parsedMovieInfo = Parser.Parser.ParseMoviePath(file.Path, _config.ParsingLeniency > 0) ?? new ParsedMovieInfo();
var mediaInfo = _videoFileInfoReader.GetMediaInfo(file.Path);
var existingFile = movie.Path.IsParentPath(file.Path);
var localMovie = new LocalMovie
{
ExistingFile = false,
MediaInfo = mediaInfo,
ParsedMovieInfo = parsedMovieInfo,
Path = file.Path,
Quality = file.Quality,
Movie = movie,
Size = 0
};
//TODO: Cleanup non-tracked downloads
var importDecision = new ImportDecision(localMovie);
if (file.DownloadId.IsNullOrWhiteSpace())
{
imported.AddRange(_importApprovedMovie.Import(new List<ImportDecision> { importDecision }, !existingFile, null, message.ImportMode));
}
else
{
var trackedDownload = _trackedDownloadService.Find(file.DownloadId);
var importResult = _importApprovedMovie.Import(new List<ImportDecision> { importDecision }, true, trackedDownload.DownloadItem, message.ImportMode).First();
imported.Add(importResult);
importedTrackedDownload.Add(new ManuallyImportedFile
{
TrackedDownload = trackedDownload,
ImportResult = importResult
});
}
}
_logger.ProgressTrace("Manually imported {0} files", imported.Count);
foreach (var groupedTrackedDownload in importedTrackedDownload.GroupBy(i => i.TrackedDownload.DownloadItem.DownloadId).ToList())
{
var trackedDownload = groupedTrackedDownload.First().TrackedDownload;
if (_diskProvider.FolderExists(trackedDownload.DownloadItem.OutputPath.FullPath))
{
if (_downloadedMovieImportService.ShouldDeleteFolder(
new DirectoryInfo(trackedDownload.DownloadItem.OutputPath.FullPath),
trackedDownload.RemoteMovie.Movie) && !trackedDownload.DownloadItem.IsReadOnly)
{
_diskProvider.DeleteFolder(trackedDownload.DownloadItem.OutputPath.FullPath, true);
}
}
if (groupedTrackedDownload.Select(c => c.ImportResult).Count(c => c.Result == ImportResultType.Imported) >= Math.Max(1, 1)) //TODO: trackedDownload.RemoteMovie.Movie.Count is always 1?
{
trackedDownload.State = TrackedDownloadStage.Imported;
_eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload));
}
}
}
}
}
@@ -0,0 +1,10 @@
using NzbDrone.Core.Download.TrackedDownloads;
namespace NzbDrone.Core.MediaFiles.MovieImport.Manual
{
public class ManuallyImportedFile
{
public TrackedDownload TrackedDownload { get; set; }
public ImportResult ImportResult { get; set; }
}
}
@@ -0,0 +1,68 @@
using System;
using System.IO;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Download;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.MediaFiles.MovieImport.Specifications
{
public class FreeSpaceSpecification : IImportDecisionEngineSpecification
{
private readonly IDiskProvider _diskProvider;
private readonly IConfigService _configService;
private readonly Logger _logger;
public FreeSpaceSpecification(IDiskProvider diskProvider, IConfigService configService, Logger logger)
{
_diskProvider = diskProvider;
_configService = configService;
_logger = logger;
}
public Decision IsSatisfiedBy(LocalMovie localMovie, DownloadClientItem downloadClientItem)
{
if (_configService.SkipFreeSpaceCheckWhenImporting)
{
_logger.Debug("Skipping free space check when importing");
return Decision.Accept();
}
try
{
if (localMovie.ExistingFile)
{
_logger.Debug("Skipping free space check for existing movie");
return Decision.Accept();
}
var path = Directory.GetParent(localMovie.Movie.Path);
var freeSpace = _diskProvider.GetAvailableSpace(path.FullName);
if (!freeSpace.HasValue)
{
_logger.Debug("Free space check returned an invalid result for: {0}", path);
return Decision.Accept();
}
if (freeSpace < localMovie.Size + 100.Megabytes())
{
_logger.Warn("Not enough free space ({0}) to import: {1} ({2})", freeSpace, localMovie, localMovie.Size);
return Decision.Reject("Not enough free space");
}
}
catch (DirectoryNotFoundException ex)
{
_logger.Error("Unable to check free disk space while importing. " + ex.Message);
}
catch (Exception ex)
{
_logger.Error(ex, "Unable to check free disk space while importing: " + localMovie.Path);
}
return Decision.Accept();
}
}
}
@@ -0,0 +1,55 @@
using System.Linq;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Download;
using NzbDrone.Core.History;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Qualities;
namespace NzbDrone.Core.MediaFiles.MovieImport.Specifications
{
public class GrabbedReleaseQualitySpecification : IImportDecisionEngineSpecification
{
private readonly Logger _logger;
private readonly IHistoryService _historyService;
public GrabbedReleaseQualitySpecification(Logger logger, IHistoryService historyService)
{
_logger = logger;
_historyService = historyService;
}
public Decision IsSatisfiedBy(LocalMovie localMovie, DownloadClientItem downloadClientItem)
{
if (downloadClientItem == null)
{
_logger.Debug("No download client item provided, skipping.");
return Decision.Accept();
}
var grabbedHistory = _historyService.FindByDownloadId(downloadClientItem.DownloadId)
.Where(h => h.EventType == HistoryEventType.Grabbed)
.ToList();
if (grabbedHistory.Empty())
{
_logger.Debug("No grabbed history for this download client item");
return Decision.Accept();
}
var parsedReleaseName = Parser.Parser.ParseMovieTitle(grabbedHistory.First().SourceTitle,false);
foreach (var item in grabbedHistory)
{
if (item.Quality.Quality != Quality.Unknown && item.Quality != localMovie.Quality)
{
_logger.Debug("Quality for grabbed release ({0}) does not match the quality of the file ({1})", item.Quality, localMovie.Quality);
return Decision.Reject("File quality does not match quality of the grabbed release");
}
}
return Decision.Accept();
}
}
}
@@ -0,0 +1,44 @@
using System;
using System.IO;
using System.Linq;
using NLog;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Download;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.MediaFiles.MovieImport.Specifications
{
public class MatchesFolderSpecification : IImportDecisionEngineSpecification
{
private readonly Logger _logger;
public MatchesFolderSpecification(Logger logger)
{
_logger = logger;
}
public Decision IsSatisfiedBy(LocalMovie localMovie, DownloadClientItem downloadClientItem)
{
if (localMovie.ExistingFile)
{
return Decision.Accept();
}
var dirInfo = new FileInfo(localMovie.Path).Directory;
if (dirInfo == null)
{
return Decision.Accept();
}
var folderInfo = Parser.Parser.ParseMovieTitle(dirInfo.Name, false);
if (folderInfo == null)
{
return Decision.Accept();
}
return Decision.Accept();
}
}
}
@@ -0,0 +1,36 @@
using NLog;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Download;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.MediaFiles.MovieImport.Specifications
{
public class NotSampleSpecification : IImportDecisionEngineSpecification
{
private readonly IDetectSample _detectSample;
private readonly Logger _logger;
public NotSampleSpecification(IDetectSample detectSample,
Logger logger)
{
_detectSample = detectSample;
_logger = logger;
}
public Decision IsSatisfiedBy(LocalMovie localEpisode, DownloadClientItem downloadClientItem)
{
var sample = _detectSample.IsSample(localEpisode.Movie,
localEpisode.Quality,
localEpisode.Path,
localEpisode.Size,
false);
if (sample)
{
return Decision.Reject("Sample");
}
return Decision.Accept();
}
}
}
@@ -0,0 +1,61 @@
using System;
using System.IO;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Download;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.MediaFiles.MovieImport.Specifications
{
public class NotUnpackingSpecification : IImportDecisionEngineSpecification
{
private readonly IDiskProvider _diskProvider;
private readonly IConfigService _configService;
private readonly Logger _logger;
public NotUnpackingSpecification(IDiskProvider diskProvider, IConfigService configService, Logger logger)
{
_diskProvider = diskProvider;
_configService = configService;
_logger = logger;
}
public Decision IsSatisfiedBy(LocalMovie localMovie, DownloadClientItem downloadClientItem)
{
if (localMovie.ExistingFile)
{
_logger.Debug("{0} is in movie folder, skipping unpacking check", localMovie.Path);
return Decision.Accept();
}
foreach (var workingFolder in _configService.DownloadClientWorkingFolders.Split('|'))
{
DirectoryInfo parent = Directory.GetParent(localMovie.Path);
while (parent != null)
{
if (parent.Name.StartsWith(workingFolder))
{
if (OsInfo.IsNotWindows)
{
_logger.Debug("{0} is still being unpacked", localMovie.Path);
return Decision.Reject("File is still being unpacked");
}
if (_diskProvider.FileGetLastWrite(localMovie.Path) > DateTime.UtcNow.AddMinutes(-5))
{
_logger.Debug("{0} appears to be unpacking still", localMovie.Path);
return Decision.Reject("File is still being unpacked");
}
}
parent = parent.Parent;
}
}
return Decision.Accept();
}
}
}
@@ -0,0 +1,37 @@
using System.Linq;
using NLog;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Download;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.MediaFiles.MovieImport.Specifications
{
public class SameFileSpecification : IImportDecisionEngineSpecification
{
private readonly Logger _logger;
public SameFileSpecification(Logger logger)
{
_logger = logger;
}
public Decision IsSatisfiedBy(LocalMovie localMovie, DownloadClientItem downloadClientItem)
{
var movieFile = localMovie.Movie.MovieFile;
if (localMovie.Movie.MovieFileId == 0)
{
_logger.Debug("No existing movie file, skipping");
return Decision.Accept();
}
if (movieFile.Size == localMovie.Size)
{
_logger.Debug("'{0}' Has the same filesize as existing file", localMovie.Path);
return Decision.Reject("Has the same filesize as existing file");
}
return Decision.Accept();
}
}
}
@@ -0,0 +1,23 @@
using System;
using System.Linq;
using NLog;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Download;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.MediaFiles.MovieImport.Specifications
{
public class UnverifiedSceneNumberingSpecification : IImportDecisionEngineSpecification
{
private readonly Logger _logger;
public UnverifiedSceneNumberingSpecification(Logger logger)
{
_logger = logger;
}
public Decision IsSatisfiedBy(LocalMovie localMovie, DownloadClientItem downloadClientItem)
{
return Decision.Accept();
}
}
}
@@ -0,0 +1,24 @@
using System.Linq;
using NLog;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Download;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Qualities;
namespace NzbDrone.Core.MediaFiles.MovieImport.Specifications
{
public class UpgradeSpecification : IImportDecisionEngineSpecification
{
private readonly Logger _logger;
public UpgradeSpecification(Logger logger)
{
_logger = logger;
}
public Decision IsSatisfiedBy(LocalMovie localMovie, DownloadClientItem downloadClientItem)
{
return Decision.Accept();
}
}
}