using System; using System.Collections.Generic; using System.Linq; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Core.CustomFormats; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; using NzbDrone.Core.MediaFiles.EpisodeImport.Aggregation; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Tv; namespace NzbDrone.Core.MediaFiles.EpisodeImport { public interface IMakeImportDecision { List GetImportDecisions(List videoFiles, Series series); List GetImportDecisions(List videoFiles, Series series, bool filterExistingFiles); List GetImportDecisions(List videoFiles, Series series, DownloadClientItem downloadClientItem, ParsedEpisodeInfo folderInfo, bool sceneSource); List GetImportDecisions(List videoFiles, Series series, DownloadClientItem downloadClientItem, ParsedEpisodeInfo folderInfo, bool sceneSource, bool filterExistingFiles); ImportDecision GetDecision(LocalEpisode localEpisode, DownloadClientItem downloadClientItem); } public class ImportDecisionMaker : IMakeImportDecision { private readonly IEnumerable _specifications; private readonly IMediaFileService _mediaFileService; private readonly IAggregationService _aggregationService; private readonly IDiskProvider _diskProvider; private readonly IDetectSample _detectSample; private readonly ICustomFormatCalculationService _formatCalculator; private readonly Logger _logger; public ImportDecisionMaker(IEnumerable specifications, IMediaFileService mediaFileService, IAggregationService aggregationService, IDiskProvider diskProvider, IDetectSample detectSample, ICustomFormatCalculationService formatCalculator, Logger logger) { _specifications = specifications; _mediaFileService = mediaFileService; _aggregationService = aggregationService; _diskProvider = diskProvider; _detectSample = detectSample; _formatCalculator = formatCalculator; _logger = logger; } public List GetImportDecisions(List videoFiles, Series series) { return GetImportDecisions(videoFiles, series, false); } public List GetImportDecisions(List videoFiles, Series series, bool filterExistingFiles) { return GetImportDecisions(videoFiles, series, null, null, false, filterExistingFiles); } public List GetImportDecisions(List videoFiles, Series series, DownloadClientItem downloadClientItem, ParsedEpisodeInfo folderInfo, bool sceneSource) { return GetImportDecisions(videoFiles, series, downloadClientItem, folderInfo, sceneSource, true); } public List GetImportDecisions(List videoFiles, Series series, DownloadClientItem downloadClientItem, ParsedEpisodeInfo folderInfo, bool sceneSource, bool filterExistingFiles) { var newFiles = filterExistingFiles ? _mediaFileService.FilterExistingFiles(videoFiles.ToList(), series) : videoFiles.ToList(); _logger.Debug("Analyzing {0}/{1} files.", newFiles.Count, videoFiles.Count()); ParsedEpisodeInfo downloadClientItemInfo = null; if (downloadClientItem != null) { downloadClientItemInfo = Parser.Parser.ParseTitle(downloadClientItem.Title); } // If not importing from a scene source (series folder for example), then assume all files are not samples // to avoid using media info on every file needlessly (especially if Analyse Media Files is disabled). var nonSampleVideoFileCount = sceneSource ? GetNonSampleVideoFileCount(newFiles, series, downloadClientItemInfo, folderInfo) : videoFiles.Count; var decisions = new List(); foreach (var file in newFiles) { var localEpisode = new LocalEpisode { Series = series, DownloadClientEpisodeInfo = downloadClientItemInfo, FolderEpisodeInfo = folderInfo, Path = file, SceneSource = sceneSource, ExistingFile = series.Path.IsParentPath(file), OtherVideoFiles = nonSampleVideoFileCount > 1 }; decisions.AddIfNotNull(GetDecision(localEpisode, downloadClientItem, nonSampleVideoFileCount > 1)); } return decisions; } public ImportDecision GetDecision(LocalEpisode localEpisode, DownloadClientItem downloadClientItem) { var reasons = _specifications.Select(c => EvaluateSpec(c, localEpisode, downloadClientItem)) .Where(c => c != null); return new ImportDecision(localEpisode, reasons.ToArray()); } private ImportDecision GetDecision(LocalEpisode localEpisode, DownloadClientItem downloadClientItem, bool otherFiles) { ImportDecision decision = null; var fileEpisodeInfo = Parser.Parser.ParsePath(localEpisode.Path); localEpisode.FileEpisodeInfo = fileEpisodeInfo; localEpisode.Size = _diskProvider.GetFileSize(localEpisode.Path); try { _aggregationService.Augment(localEpisode, downloadClientItem); if (localEpisode.Episodes.Empty()) { if (IsPartialSeason(localEpisode)) { decision = new ImportDecision(localEpisode, new Rejection("Partial season packs are not supported")); } else if (IsSeasonExtra(localEpisode)) { decision = new ImportDecision(localEpisode, new Rejection("Extras are not supported")); } else { decision = new ImportDecision(localEpisode, new Rejection("Invalid season or episode")); } } else { decision = GetDecision(localEpisode, downloadClientItem); } } catch (AugmentingFailedException) { decision = new ImportDecision(localEpisode, new Rejection("Unable to parse file")); } catch (Exception ex) { _logger.Error(ex, "Couldn't import file. {0}", localEpisode.Path); decision = new ImportDecision(localEpisode, new Rejection("Unexpected error processing file")); } if (decision == null) { _logger.Error("Unable to make a decision on {0}", localEpisode.Path); } else if (decision.Rejections.Any()) { _logger.Debug("File rejected for the following reasons: {0}", string.Join(", ", decision.Rejections)); } else { _logger.Debug("File accepted"); } return decision; } private Rejection EvaluateSpec(IImportDecisionEngineSpecification spec, LocalEpisode localEpisode, DownloadClientItem downloadClientItem) { try { var result = spec.IsSatisfiedBy(localEpisode, downloadClientItem); if (!result.Accepted) { return new Rejection(result.Reason); } } 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 {0}", localEpisode.Path); return new Rejection($"{spec.GetType().Name}: {e.Message}"); } return null; } private int GetNonSampleVideoFileCount(List videoFiles, Series series, ParsedEpisodeInfo downloadClientItemInfo, ParsedEpisodeInfo folderInfo) { var isPossibleSpecialEpisode = downloadClientItemInfo?.IsPossibleSpecialEpisode ?? false; // If we might already have a special, don't try to get it from the folder info. isPossibleSpecialEpisode = isPossibleSpecialEpisode || (folderInfo?.IsPossibleSpecialEpisode ?? false); return videoFiles.Count(file => { var sample = _detectSample.IsSample(series, file, isPossibleSpecialEpisode); if (sample == DetectSampleResult.Sample) { return false; } return true; }); } private bool IsPartialSeason(LocalEpisode localEpisode) { var downloadClientEpisodeInfo = localEpisode.DownloadClientEpisodeInfo; var folderEpisodeInfo = localEpisode.FolderEpisodeInfo; var fileEpisodeInfo = localEpisode.FileEpisodeInfo; if (downloadClientEpisodeInfo != null && downloadClientEpisodeInfo.IsPartialSeason) { return true; } if (folderEpisodeInfo != null && folderEpisodeInfo.IsPartialSeason) { return true; } if (fileEpisodeInfo != null && fileEpisodeInfo.IsPartialSeason) { return true; } return false; } private bool IsSeasonExtra(LocalEpisode localEpisode) { var downloadClientEpisodeInfo = localEpisode.DownloadClientEpisodeInfo; var folderEpisodeInfo = localEpisode.FolderEpisodeInfo; var fileEpisodeInfo = localEpisode.FileEpisodeInfo; if (downloadClientEpisodeInfo != null && downloadClientEpisodeInfo.IsSeasonExtra) { return true; } if (folderEpisodeInfo != null && folderEpisodeInfo.IsSeasonExtra) { return true; } if (fileEpisodeInfo != null && fileEpisodeInfo.IsSeasonExtra) { return true; } return false; } } }