using System; using System.Collections.Generic; using System.Linq; using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Common.Instrumentation.Extensions; using NzbDrone.Common.Serializer; using NzbDrone.Core.Configuration; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.DecisionEngine { public interface IMakeDownloadDecision { List GetRssDecision(List reports); List GetSearchDecision(List reports, SearchCriteriaBase searchCriteriaBase); } public class DownloadDecisionMaker : IMakeDownloadDecision { private readonly IEnumerable _specifications; private readonly IParsingService _parsingService; private readonly IConfigService _configService; private readonly Logger _logger; public DownloadDecisionMaker(IEnumerable specifications, IParsingService parsingService, IConfigService configService, Logger logger) { _specifications = specifications; _parsingService = parsingService; _configService = configService; _logger = logger; } public List GetRssDecision(List reports) { return GetMovieDecisions(reports).ToList(); } public List GetSearchDecision(List reports, SearchCriteriaBase searchCriteriaBase) { if (searchCriteriaBase.Movie != null) { return GetMovieDecisions(reports, searchCriteriaBase).ToList(); } return GetDecisions(reports, searchCriteriaBase).ToList(); } private IEnumerable GetMovieDecisions(List reports, SearchCriteriaBase searchCriteria = null) { if (reports.Any()) { _logger.ProgressInfo("Processing {0} releases", reports.Count); } else { _logger.ProgressInfo("No results found"); } var reportNumber = 1; foreach (var report in reports) { DownloadDecision decision = null; _logger.ProgressTrace("Processing release {0}/{1}", reportNumber, reports.Count); try { var parsedMovieInfo = Parser.Parser.ParseMovieTitle(report.Title); if (parsedMovieInfo != null && !parsedMovieInfo.MovieTitle.IsNullOrWhiteSpace()) { RemoteMovie remoteMovie = _parsingService.Map(parsedMovieInfo, report.ImdbId.ToString(), searchCriteria); remoteMovie.Release = report; if (remoteMovie.Movie == null) { decision = new DownloadDecision(remoteMovie, new Rejection("Unknown movie. Movie found does not match wanted movie.")); } else { if (parsedMovieInfo.Quality.HardcodedSubs.IsNotNullOrWhiteSpace()) { remoteMovie.DownloadAllowed = true; if (_configService.AllowHardcodedSubs) { decision = GetDecisionForReport(remoteMovie, searchCriteria); } else { var whitelisted = _configService.WhitelistedHardcodedSubs.Split(','); _logger.Debug("Testing: {0}", whitelisted); if (whitelisted != null && whitelisted.Any(t => (parsedMovieInfo.Quality.HardcodedSubs.ToLower().Contains(t.ToLower()) && t.IsNotNullOrWhiteSpace()))) { decision = GetDecisionForReport(remoteMovie, searchCriteria); } else { decision = new DownloadDecision(remoteMovie, new Rejection("Hardcoded subs found: " + parsedMovieInfo.Quality.HardcodedSubs)); } } } else { remoteMovie.DownloadAllowed = true; decision = GetDecisionForReport(remoteMovie, searchCriteria); } } } else { _logger.Trace("{0} could not be parsed :(.", report.Title); } } catch (Exception e) { _logger.Error(e, "Couldn't process release."); var remoteMovie = new RemoteMovie { Release = report }; decision = new DownloadDecision(remoteMovie, new Rejection("Unexpected error processing release")); } reportNumber++; if (decision != null) { if (decision.Rejections.Any()) { _logger.Debug("Release rejected for the following reasons: {0}", string.Join(", ", decision.Rejections)); } else { _logger.Debug("Release accepted"); } yield return decision; } } } private IEnumerable GetDecisions(List reports, SearchCriteriaBase searchCriteria = null) { if (reports.Any()) { _logger.ProgressInfo("Processing {0} releases", reports.Count); } else { _logger.ProgressInfo("No results found"); } var reportNumber = 1; foreach (var report in reports) { DownloadDecision decision = null; _logger.ProgressTrace("Processing release {0}/{1}", reportNumber, reports.Count); try { var parsedEpisodeInfo = Parser.Parser.ParseTitle(report.Title); if (parsedEpisodeInfo == null || parsedEpisodeInfo.IsPossibleSpecialEpisode) { var specialEpisodeInfo = _parsingService.ParseSpecialEpisodeTitle(report.Title, report.TvdbId, report.TvRageId, searchCriteria); if (specialEpisodeInfo != null) { parsedEpisodeInfo = specialEpisodeInfo; } } if (parsedEpisodeInfo != null && !parsedEpisodeInfo.SeriesTitle.IsNullOrWhiteSpace()) { var remoteEpisode = _parsingService.Map(parsedEpisodeInfo, report.TvdbId, report.TvRageId, searchCriteria); remoteEpisode.Release = report; if (remoteEpisode.Series == null) { //remoteEpisode.DownloadAllowed = true; //Fuck you :) //decision = GetDecisionForReport(remoteEpisode, searchCriteria); decision = new DownloadDecision(remoteEpisode, new Rejection("Unknown release. Series not Found.")); } else if (remoteEpisode.Episodes.Empty()) { decision = new DownloadDecision(remoteEpisode, new Rejection("Unable to parse episodes from release name")); } else { remoteEpisode.DownloadAllowed = remoteEpisode.Episodes.Any(); decision = GetDecisionForReport(remoteEpisode, searchCriteria); } } } catch (Exception e) { _logger.Error(e, "Couldn't process release."); var remoteEpisode = new RemoteEpisode { Release = report }; decision = new DownloadDecision(remoteEpisode, new Rejection("Unexpected error processing release")); } reportNumber++; if (decision != null) { if (decision.Rejections.Any()) { _logger.Debug("Release rejected for the following reasons: {0}", string.Join(", ", decision.Rejections)); } else { _logger.Debug("Release accepted"); } yield return decision; } } } private DownloadDecision GetDecisionForReport(RemoteMovie remoteEpisode, SearchCriteriaBase searchCriteria = null) { var reasons = _specifications.Select(c => EvaluateSpec(c, remoteEpisode, searchCriteria)) .Where(c => c != null); return new DownloadDecision(remoteEpisode, reasons.ToArray()); } private DownloadDecision GetDecisionForReport(RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteria = null) { var reasons = _specifications.Select(c => EvaluateSpec(c, remoteEpisode, searchCriteria)) .Where(c => c != null); return new DownloadDecision(remoteEpisode, reasons.ToArray()); } private Rejection EvaluateSpec(IDecisionEngineSpecification spec, RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteriaBase = null) { try { var result = spec.IsSatisfiedBy(remoteEpisode, searchCriteriaBase); if (!result.Accepted) { return new Rejection(result.Reason, spec.Type); } } catch (Exception e) { e.Data.Add("report", remoteEpisode.Release.ToJson()); e.Data.Add("parsed", remoteEpisode.ParsedEpisodeInfo.ToJson()); _logger.Error(e, "Couldn't evaluate decision on " + remoteEpisode.Release.Title + ", with spec: " + spec.GetType().Name); //return new Rejection(string.Format("{0}: {1}", spec.GetType().Name, e.Message));//TODO UPDATE SPECS! //return null; } return null; } private Rejection EvaluateSpec(IDecisionEngineSpecification spec, RemoteMovie remoteMovie, SearchCriteriaBase searchCriteriaBase = null) { try { var result = spec.IsSatisfiedBy(remoteMovie, searchCriteriaBase); if (!result.Accepted) { return new Rejection(result.Reason, spec.Type); } } catch (NotImplementedException e) { _logger.Trace("Spec " + spec.GetType().Name + " does not care about movies."); } catch (Exception e) { e.Data.Add("report", remoteMovie.Release.ToJson()); e.Data.Add("parsed", remoteMovie.ParsedMovieInfo.ToJson()); _logger.Error(e, "Couldn't evaluate decision on " + remoteMovie.Release.Title + ", with spec: " + spec.GetType().Name); return new Rejection(string.Format("{0}: {1}", spec.GetType().Name, e.Message));//TODO UPDATE SPECS! } return null; } } }