From 9aa805062749209564fcba4a55ddeebfd1e654d9 Mon Sep 17 00:00:00 2001 From: Leonardo Galli Date: Fri, 6 Jan 2017 15:05:30 +0100 Subject: [PATCH] Decision Engine is now mostly working with movies :) --- .../DecisionEngine/DownloadDecisionMaker.cs | 39 ++++++++++- .../IDecisionEngineSpecification.cs | 2 + .../AcceptableSizeSpecification.cs | 53 +++++++++++++++ .../AnimeVersionUpgradeSpecification.cs | 6 ++ .../Specifications/BlacklistSpecification.cs | 9 +++ .../Specifications/CutoffSpecification.cs | 14 ++++ .../Specifications/FullSeasonSpecification.cs | 5 ++ .../Specifications/LanguageSpecification.cs | 15 +++++ .../Specifications/MinimumAgeSpecification.cs | 31 +++++++++ .../Specifications/NotSampleSpecification.cs | 14 +++- .../Specifications/ProtocolSpecification.cs | 19 ++++++ .../QualityAllowedByProfileSpecification.cs | 12 ++++ .../Specifications/QueueSpecification.cs | 27 ++++++++ .../Specifications/RawDiskSpecification.cs | 22 +++++++ .../ReleaseRestrictionsSpecification.cs | 40 ++++++++++++ .../Specifications/RetentionSpecification.cs | 21 ++++++ .../RssSync/DelaySpecification.cs | 65 +++++++++++++++++++ .../RssSync/HistorySpecification.cs | 50 ++++++++++++++ .../RssSync/MonitoredEpisodeSpecification.cs | 6 ++ .../RssSync/ProperSpecification.cs | 33 ++++++++++ .../SameEpisodesGrabSpecification.cs | 6 ++ .../Search/DailyEpisodeMatchSpecification.cs | 6 ++ .../Search/EpisodeRequestedSpecification.cs | 8 ++- .../Search/SeasonMatchSpecification.cs | 6 ++ .../Search/SeriesSpecification.cs | 18 +++++ .../SingleEpisodeSearchMatchSpecification.cs | 6 ++ .../Search/TorrentSeedingSpecification.cs | 18 +++++ .../UpgradeDiskSpecification.cs | 19 ++++++ 28 files changed, 566 insertions(+), 4 deletions(-) diff --git a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs index 5d8ee991b..e2d171ee4 100644 --- a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs +++ b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs @@ -89,8 +89,8 @@ namespace NzbDrone.Core.DecisionEngine else { remoteEpisode.DownloadAllowed = true; - //decision = GetDecisionForReport(remoteEpisode, searchCriteria); TODO: Rewrite this for movies! - decision = new DownloadDecision(remoteEpisode); + decision = GetDecisionForReport(remoteEpisode, searchCriteria); + //decision = new DownloadDecision(remoteEpisode); } } @@ -205,6 +205,14 @@ namespace NzbDrone.Core.DecisionEngine } } + 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)) @@ -235,5 +243,32 @@ namespace NzbDrone.Core.DecisionEngine return null; } + + private Rejection EvaluateSpec(IDecisionEngineSpecification spec, RemoteMovie remoteEpisode, SearchCriteriaBase searchCriteriaBase = null) + { + try + { + var result = spec.IsSatisfiedBy(remoteEpisode, searchCriteriaBase); + + if (!result.Accepted) + { + return new Rejection(result.Reason, spec.Type); + } + } + catch (NotImplementedException e) + { + _logger.Info("Spec " + spec.GetType().Name + " does not care about movies."); + } + 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; + } } } diff --git a/src/NzbDrone.Core/DecisionEngine/IDecisionEngineSpecification.cs b/src/NzbDrone.Core/DecisionEngine/IDecisionEngineSpecification.cs index 199984734..e98a6977f 100644 --- a/src/NzbDrone.Core/DecisionEngine/IDecisionEngineSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/IDecisionEngineSpecification.cs @@ -8,5 +8,7 @@ namespace NzbDrone.Core.DecisionEngine RejectionType Type { get; } Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria); + + Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria); } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs index 4ab566d2e..e4d771cae 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs @@ -107,5 +107,58 @@ namespace NzbDrone.Core.DecisionEngine.Specifications _logger.Debug("Item: {0}, meets size constraints.", subject); return Decision.Accept(); } + + public Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria) + { + _logger.Debug("Beginning size check for: {0}", subject); + + var quality = subject.ParsedMovieInfo.Quality.Quality; + + if (subject.Release.Size == 0) + { + _logger.Debug("Release has unknown size, skipping size check."); + return Decision.Accept(); + } + + var qualityDefinition = _qualityDefinitionService.Get(quality); + if (qualityDefinition.MinSize.HasValue) + { + var minSize = qualityDefinition.MinSize.Value.Megabytes(); + + //Multiply maxSize by Series.Runtime + minSize = minSize * subject.Movie.Runtime; + + //If the parsed size is smaller than minSize we don't want it + if (subject.Release.Size < minSize) + { + var runtimeMessage = subject.Movie.Title; + + _logger.Debug("Item: {0}, Size: {1} is smaller than minimum allowed size ({2} bytes for {3}), rejecting.", subject, subject.Release.Size, minSize, runtimeMessage); + return Decision.Reject("{0} is smaller than minimum allowed {1} (for {2})", subject.Release.Size.SizeSuffix(), minSize.SizeSuffix(), runtimeMessage); + } + } + if (!qualityDefinition.MaxSize.HasValue || qualityDefinition.MaxSize.Value == 0) + { + _logger.Debug("Max size is unlimited - skipping check."); + } + else + { + var maxSize = qualityDefinition.MaxSize.Value.Megabytes(); + + //Multiply maxSize by Series.Runtime + maxSize = maxSize * subject.Movie.Runtime; + + //If the parsed size is greater than maxSize we don't want it + if (subject.Release.Size > maxSize) + {; + + _logger.Debug("Item: {0}, Size: {1} is greater than maximum allowed size ({2} for {3}), rejecting.", subject, subject.Release.Size, maxSize, subject.Movie.Title); + return Decision.Reject("{0} is larger than maximum allowed {1} (for {2})", subject.Release.Size.SizeSuffix(), maxSize.SizeSuffix(), subject.Movie.Title); + } + } + + _logger.Debug("Item: {0}, meets size constraints.", subject); + return Decision.Accept(); + } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/AnimeVersionUpgradeSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/AnimeVersionUpgradeSpecification.cs index c2f93f7c0..33f6cff9f 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/AnimeVersionUpgradeSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/AnimeVersionUpgradeSpecification.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using NLog; using NzbDrone.Common.Extensions; @@ -55,5 +56,10 @@ namespace NzbDrone.Core.DecisionEngine.Specifications return Decision.Accept(); } + + public Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria) + { + throw new NotImplementedException(); + } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/BlacklistSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/BlacklistSpecification.cs index 18b216263..82bc2d83c 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/BlacklistSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/BlacklistSpecification.cs @@ -2,6 +2,7 @@ using NLog; using NzbDrone.Core.Blacklisting; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; +using System; namespace NzbDrone.Core.DecisionEngine.Specifications { @@ -28,5 +29,13 @@ namespace NzbDrone.Core.DecisionEngine.Specifications return Decision.Accept(); } + + public Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria) + { + + throw new NotImplementedException(); + + return Decision.Accept(); + } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/CutoffSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/CutoffSpecification.cs index 6dfdbc64c..1ac882632 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/CutoffSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/CutoffSpecification.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using NLog; using NzbDrone.Core.IndexerSearch.Definitions; @@ -34,5 +35,18 @@ namespace NzbDrone.Core.DecisionEngine.Specifications return Decision.Accept(); } + + public Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria) + { + if (subject.Movie.MovieFile.Value != null) + { + if (!_qualityUpgradableSpecification.CutoffNotMet(subject.Movie.Profile, subject.Movie.MovieFile.Value.Quality, subject.ParsedMovieInfo.Quality)) + { + return Decision.Reject("Existing file meets cutoff: {0}", subject.Movie.Profile.Value.Cutoff); + } + } + + return Decision.Accept(); + } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/FullSeasonSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/FullSeasonSpecification.cs index c2d86afe8..7d2f89d26 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/FullSeasonSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/FullSeasonSpecification.cs @@ -36,5 +36,10 @@ namespace NzbDrone.Core.DecisionEngine.Specifications return Decision.Accept(); } + + public Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria) + { + throw new NotImplementedException(); + } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/LanguageSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/LanguageSpecification.cs index 9f7f75038..fe244d057 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/LanguageSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/LanguageSpecification.cs @@ -29,5 +29,20 @@ namespace NzbDrone.Core.DecisionEngine.Specifications return Decision.Accept(); } + + public virtual Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria) + { + var wantedLanguage = subject.Movie.Profile.Value.Language; + + _logger.Debug("Checking if report meets language requirements. {0}", subject.ParsedMovieInfo.Language); + + if (subject.ParsedMovieInfo.Language != wantedLanguage) + { + _logger.Debug("Report Language: {0} rejected because it is not wanted, wanted {1}", subject.ParsedMovieInfo.Language, wantedLanguage); + return Decision.Reject("{0} is wanted, but found {1}", wantedLanguage, subject.ParsedMovieInfo.Language); + } + + return Decision.Accept(); + } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/MinimumAgeSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/MinimumAgeSpecification.cs index 449d7be76..bcec8d6cd 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/MinimumAgeSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/MinimumAgeSpecification.cs @@ -36,6 +36,37 @@ namespace NzbDrone.Core.DecisionEngine.Specifications } + _logger.Debug("Checking if report meets minimum age requirements. {0}", age); + + if (age < minimumAge) + { + _logger.Debug("Only {0} minutes old, minimum age is {1} minutes", age, minimumAge); + return Decision.Reject("Only {0} minutes old, minimum age is {1} minutes", age, minimumAge); + } + + _logger.Debug("Release is {0} minutes old, greater than minimum age of {1} minutes", age, minimumAge); + + return Decision.Accept(); + } + + public virtual Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria) + { + if (subject.Release.DownloadProtocol != Indexers.DownloadProtocol.Usenet) + { + _logger.Debug("Not checking minimum age requirement for non-usenet report"); + return Decision.Accept(); + } + + var age = subject.Release.AgeMinutes; + var minimumAge = _configService.MinimumAge; + + if (minimumAge == 0) + { + _logger.Debug("Minimum age is not set."); + return Decision.Accept(); + } + + _logger.Debug("Checking if report meets minimum age requirements. {0}", age); if (age < minimumAge) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/NotSampleSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/NotSampleSpecification.cs index 02ff7653a..eba2566e2 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/NotSampleSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/NotSampleSpecification.cs @@ -1,4 +1,5 @@ -using NLog; +using System; +using NLog; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; @@ -25,5 +26,16 @@ namespace NzbDrone.Core.DecisionEngine.Specifications return Decision.Accept(); } + + public Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria) + { + if (subject.Release.Title.ToLower().Contains("sample") && subject.Release.Size < 70.Megabytes()) + { + _logger.Debug("Sample release, rejecting."); + return Decision.Reject("Sample"); + } + + return Decision.Accept(); + } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/ProtocolSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/ProtocolSpecification.cs index 008e58812..93846789c 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/ProtocolSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/ProtocolSpecification.cs @@ -38,5 +38,24 @@ namespace NzbDrone.Core.DecisionEngine.Specifications return Decision.Accept(); } + + public virtual Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria) + { + var delayProfile = _delayProfileService.BestForTags(subject.Movie.Tags); + + if (subject.Release.DownloadProtocol == DownloadProtocol.Usenet && !delayProfile.EnableUsenet) + { + _logger.Debug("[{0}] Usenet is not enabled for this series", subject.Release.Title); + return Decision.Reject("Usenet is not enabled for this series"); + } + + if (subject.Release.DownloadProtocol == DownloadProtocol.Torrent && !delayProfile.EnableTorrent) + { + _logger.Debug("[{0}] Torrent is not enabled for this series", subject.Release.Title); + return Decision.Reject("Torrent is not enabled for this series"); + } + + return Decision.Accept(); + } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/QualityAllowedByProfileSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/QualityAllowedByProfileSpecification.cs index 7913e0e7e..dcba1ef41 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/QualityAllowedByProfileSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/QualityAllowedByProfileSpecification.cs @@ -26,5 +26,17 @@ namespace NzbDrone.Core.DecisionEngine.Specifications return Decision.Accept(); } + + public virtual Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria) + { + _logger.Debug("Checking if report meets quality requirements. {0}", subject.ParsedMovieInfo.Quality); + if (!subject.Movie.Profile.Value.Items.Exists(v => v.Allowed && v.Quality == subject.ParsedMovieInfo.Quality.Quality)) + { + _logger.Debug("Quality {0} rejected by Series' quality profile", subject.ParsedMovieInfo.Quality); + return Decision.Reject("{0} is not wanted in profile", subject.ParsedMovieInfo.Quality.Quality); + } + + return Decision.Accept(); + } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs index 6f3ec1bea..8ff6105d7 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs @@ -50,5 +50,32 @@ namespace NzbDrone.Core.DecisionEngine.Specifications return Decision.Accept(); } + + public Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria) + { + var queue = _queueService.GetQueue() + .Select(q => q.RemoteMovie).ToList(); + + var matchingSeries = queue.Where(q => q.Movie.Id == subject.Movie.Id); + + foreach (var remoteEpisode in matchingSeries) + { + _logger.Debug("Checking if existing release in queue meets cutoff. Queued quality is: {0}", remoteEpisode.ParsedEpisodeInfo.Quality); + + if (!_qualityUpgradableSpecification.CutoffNotMet(subject.Movie.Profile, remoteEpisode.ParsedMovieInfo.Quality, subject.ParsedMovieInfo.Quality)) + { + return Decision.Reject("Quality for release in queue already meets cutoff: {0}", remoteEpisode.ParsedEpisodeInfo.Quality); + } + + _logger.Debug("Checking if release is higher quality than queued release. Queued quality is: {0}", remoteEpisode.ParsedMovieInfo.Quality); + + if (!_qualityUpgradableSpecification.IsUpgradable(subject.Movie.Profile, remoteEpisode.ParsedMovieInfo.Quality, subject.ParsedMovieInfo.Quality)) + { + return Decision.Reject("Quality for release in queue is of equal or higher preference: {0}", remoteEpisode.ParsedMovieInfo.Quality); + } + } + + return Decision.Accept(); + } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RawDiskSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RawDiskSpecification.cs index 7f278cb7e..7a11bacfe 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RawDiskSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RawDiskSpecification.cs @@ -42,5 +42,27 @@ namespace NzbDrone.Core.DecisionEngine.Specifications return Decision.Accept(); } + + public virtual Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria) + { + if (subject.Release == null || subject.Release.Container.IsNullOrWhiteSpace()) + { + return Decision.Accept(); + } + + if (_dvdContainerTypes.Contains(subject.Release.Container.ToLower())) + { + _logger.Debug("Release contains raw DVD, rejecting."); + return Decision.Reject("Raw DVD release"); + } + + if (_blurayContainerTypes.Contains(subject.Release.Container.ToLower())) + { + _logger.Debug("Release contains raw Bluray, rejecting."); + return Decision.Reject("Raw Bluray release"); + } + + return Decision.Accept(); + } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/ReleaseRestrictionsSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/ReleaseRestrictionsSpecification.cs index 9fb8c13f5..42735995f 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/ReleaseRestrictionsSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/ReleaseRestrictionsSpecification.cs @@ -62,6 +62,46 @@ namespace NzbDrone.Core.DecisionEngine.Specifications return Decision.Accept(); } + public virtual Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria) + { + _logger.Debug("Checking if release meets restrictions: {0}", subject); + + var title = subject.Release.Title; + var restrictions = _restrictionService.AllForTags(subject.Movie.Tags); + + var required = restrictions.Where(r => r.Required.IsNotNullOrWhiteSpace()); + var ignored = restrictions.Where(r => r.Ignored.IsNotNullOrWhiteSpace()); + + foreach (var r in required) + { + var requiredTerms = r.Required.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(); + + var foundTerms = ContainsAny(requiredTerms, title); + if (foundTerms.Empty()) + { + var terms = string.Join(", ", requiredTerms); + _logger.Debug("[{0}] does not contain one of the required terms: {1}", title, terms); + return Decision.Reject("Does not contain one of the required terms: {0}", terms); + } + } + + foreach (var r in ignored) + { + var ignoredTerms = r.Ignored.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(); + + var foundTerms = ContainsAny(ignoredTerms, title); + if (foundTerms.Any()) + { + var terms = string.Join(", ", foundTerms); + _logger.Debug("[{0}] contains these ignored terms: {1}", title, terms); + return Decision.Reject("Contains these ignored terms: {0}", terms); + } + } + + _logger.Debug("[{0}] No restrictions apply, allowing", subject); + return Decision.Accept(); + } + private static List ContainsAny(List terms, string title) { return terms.Where(t => title.ToLowerInvariant().Contains(t.ToLowerInvariant())).ToList(); diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RetentionSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RetentionSpecification.cs index 97802f871..7663c3fb1 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RetentionSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RetentionSpecification.cs @@ -38,5 +38,26 @@ namespace NzbDrone.Core.DecisionEngine.Specifications return Decision.Accept(); } + + public virtual Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria) + { + if (subject.Release.DownloadProtocol != Indexers.DownloadProtocol.Usenet) + { + _logger.Debug("Not checking retention requirement for non-usenet report"); + return Decision.Accept(); + } + + var age = subject.Release.Age; + var retention = _configService.Retention; + + _logger.Debug("Checking if report meets retention requirements. {0}", age); + if (retention > 0 && age > retention) + { + _logger.Debug("Report age: {0} rejected by user's retention limit", age); + return Decision.Reject("Older than configured retention"); + } + + return Decision.Accept(); + } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DelaySpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DelaySpecification.cs index 68551c66c..f75ac84af 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DelaySpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DelaySpecification.cs @@ -28,6 +28,71 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync public RejectionType Type => RejectionType.Temporary; + public virtual Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria) + { + if (searchCriteria != null && searchCriteria.UserInvokedSearch) + { + _logger.Debug("Ignoring delay for user invoked search"); + return Decision.Accept(); + } + + var profile = subject.Movie.Profile.Value; + var delayProfile = _delayProfileService.BestForTags(subject.Movie.Tags); + var delay = delayProfile.GetProtocolDelay(subject.Release.DownloadProtocol); + var isPreferredProtocol = subject.Release.DownloadProtocol == delayProfile.PreferredProtocol; + + if (delay == 0) + { + _logger.Debug("Profile does not require a waiting period before download for {0}.", subject.Release.DownloadProtocol); + return Decision.Accept(); + } + + var comparer = new QualityModelComparer(profile); + + if (isPreferredProtocol) + { + var upgradable = _qualityUpgradableSpecification.IsUpgradable(profile, subject.Movie.MovieFile.Value.Quality, subject.ParsedMovieInfo.Quality); + + if (upgradable) + { + var revisionUpgrade = _qualityUpgradableSpecification.IsRevisionUpgrade(subject.Movie.MovieFile.Value.Quality, subject.ParsedMovieInfo.Quality); + + if (revisionUpgrade) + { + _logger.Debug("New quality is a better revision for existing quality, skipping delay"); + return Decision.Accept(); + } + } + + } + + // If quality meets or exceeds the best allowed quality in the profile accept it immediately + var bestQualityInProfile = new QualityModel(profile.LastAllowedQuality()); + var isBestInProfile = comparer.Compare(subject.ParsedEpisodeInfo.Quality, bestQualityInProfile) >= 0; + + if (isBestInProfile && isPreferredProtocol) + { + _logger.Debug("Quality is highest in profile for preferred protocol, will not delay"); + return Decision.Accept(); + } + + /* + var oldest = _pendingReleaseService.OldestPendingRelease(subject.Series.Id, episodeIds); + + if (oldest != null && oldest.Release.AgeMinutes > delay) + { + return Decision.Accept(); + } + + if (subject.Release.AgeMinutes < delay) + { + _logger.Debug("Waiting for better quality release, There is a {0} minute delay on {1}", delay, subject.Release.DownloadProtocol); + return Decision.Reject("Waiting for better quality release"); + }*/ //TODO: Update for movies! + + return Decision.Accept(); + } + public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { if (searchCriteria != null && searchCriteria.UserInvokedSearch) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs index 9aa4fabf1..2a312fbdd 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs @@ -28,6 +28,56 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync public RejectionType Type => RejectionType.Permanent; + public virtual Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria) + { + if (searchCriteria != null) + { + _logger.Debug("Skipping history check during search"); + return Decision.Accept(); + } + + var cdhEnabled = _configService.EnableCompletedDownloadHandling; + + _logger.Debug("Performing history status check on report"); + _logger.Debug("Checking current status of episode [{0}] in history", subject.Movie.Id); + var mostRecent = _historyService.MostRecentForMovie(subject.Movie.Id); + + if (mostRecent != null && mostRecent.EventType == HistoryEventType.Grabbed) + { + var recent = mostRecent.Date.After(DateTime.UtcNow.AddHours(-12)); + var cutoffUnmet = _qualityUpgradableSpecification.CutoffNotMet(subject.Movie.Profile, mostRecent.Quality, subject.ParsedMovieInfo.Quality); + var upgradeable = _qualityUpgradableSpecification.IsUpgradable(subject.Movie.Profile, mostRecent.Quality, subject.ParsedMovieInfo.Quality); + + if (!recent && cdhEnabled) + { + return Decision.Accept(); + } + + if (!cutoffUnmet) + { + if (recent) + { + return Decision.Reject("Recent grab event in history already meets cutoff: {0}", mostRecent.Quality); + } + + return Decision.Reject("CDH is disabled and grab event in history already meets cutoff: {0}", mostRecent.Quality); + } + + if (!upgradeable) + { + if (recent) + { + return Decision.Reject("Recent grab event in history is of equal or higher quality: {0}", mostRecent.Quality); + } + + return Decision.Reject("CDH is disabled and grab event in history is of equal or higher quality: {0}", mostRecent.Quality); + } + } + + + return Decision.Accept(); + } + public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { if (searchCriteria != null) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/MonitoredEpisodeSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/MonitoredEpisodeSpecification.cs index f56f26478..ccb87c414 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/MonitoredEpisodeSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/MonitoredEpisodeSpecification.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using NLog; using NzbDrone.Core.IndexerSearch.Definitions; @@ -16,6 +17,11 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync public RejectionType Type => RejectionType.Permanent; + public Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria) + { + throw new NotImplementedException(); + } + public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { if (searchCriteria != null) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/ProperSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/ProperSpecification.cs index 0c6632d25..d029e929c 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/ProperSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/ProperSpecification.cs @@ -47,6 +47,39 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync } } + return Decision.Accept(); + } + + public virtual Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria) + { + if (searchCriteria != null) + { + return Decision.Accept(); + } + + if (subject.Movie.MovieFile.Value == null) + { + return Decision.Accept(); + } + + var file = subject.Movie.MovieFile.Value; + + if (_qualityUpgradableSpecification.IsRevisionUpgrade(file.Quality, subject.ParsedMovieInfo.Quality)) + { + if (file.DateAdded < DateTime.Today.AddDays(-7)) + { + _logger.Debug("Proper for old file, rejecting: {0}", subject); + return Decision.Reject("Proper for old file"); + } + + if (!_configService.AutoDownloadPropers) + { + _logger.Debug("Auto downloading of propers is disabled"); + return Decision.Reject("Proper downloading is disabled"); + } + } + + return Decision.Accept(); } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/SameEpisodesGrabSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/SameEpisodesGrabSpecification.cs index 1a8c5db5b..bd3c2f908 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/SameEpisodesGrabSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/SameEpisodesGrabSpecification.cs @@ -1,3 +1,4 @@ +using System; using NLog; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; @@ -27,5 +28,10 @@ namespace NzbDrone.Core.DecisionEngine.Specifications _logger.Debug("Episode file on disk contains more episodes than this release contains"); return Decision.Reject("Episode file on disk contains more episodes than this release contains"); } + + public Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria) + { + throw new NotImplementedException(); + } } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/DailyEpisodeMatchSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/DailyEpisodeMatchSpecification.cs index 50fd9b3cc..285dc5b5e 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/DailyEpisodeMatchSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/DailyEpisodeMatchSpecification.cs @@ -1,3 +1,4 @@ +using System; using NLog; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; @@ -39,5 +40,10 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search return Decision.Accept(); } + + public Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria) + { + throw new NotImplementedException(); + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/EpisodeRequestedSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/EpisodeRequestedSpecification.cs index 60640442f..d8c0065a5 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/EpisodeRequestedSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/EpisodeRequestedSpecification.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using NLog; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; @@ -17,6 +18,11 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search public RejectionType Type => RejectionType.Permanent; + public Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria) + { + throw new NotImplementedException(); + } + public Decision IsSatisfiedBy(RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteria) { if (searchCriteria == null) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SeasonMatchSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SeasonMatchSpecification.cs index 56e986e19..c7f96303d 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SeasonMatchSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SeasonMatchSpecification.cs @@ -1,3 +1,4 @@ +using System; using NLog; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; @@ -15,6 +16,11 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search public RejectionType Type => RejectionType.Permanent; + public Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria) + { + throw new NotImplementedException(); + } + public Decision IsSatisfiedBy(RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteria) { if (searchCriteria == null) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SeriesSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SeriesSpecification.cs index 7f1201b33..dde54155f 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SeriesSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SeriesSpecification.cs @@ -32,5 +32,23 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search return Decision.Accept(); } + + public Decision IsSatisfiedBy(RemoteMovie remoteEpisode, SearchCriteriaBase searchCriteria) + { + if (searchCriteria == null) + { + return Decision.Accept(); + } + + _logger.Debug("Checking if movie matches searched movie"); + + if (remoteEpisode.Movie.Id != searchCriteria.Movie.Id) + { + _logger.Debug("Series {0} does not match {1}", remoteEpisode.Movie, searchCriteria.Series); + return Decision.Reject("Wrong movie"); + } + + return Decision.Accept(); + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SingleEpisodeSearchMatchSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SingleEpisodeSearchMatchSpecification.cs index dfaa711ff..18162d3f5 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SingleEpisodeSearchMatchSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SingleEpisodeSearchMatchSpecification.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using NLog; using NzbDrone.Core.IndexerSearch.Definitions; @@ -16,6 +17,11 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search public RejectionType Type => RejectionType.Permanent; + public Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria) + { + throw new NotImplementedException(); + } + public Decision IsSatisfiedBy(RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteria) { if (searchCriteria == null) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/TorrentSeedingSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/TorrentSeedingSpecification.cs index 87c244b53..142e2009a 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/TorrentSeedingSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/TorrentSeedingSpecification.cs @@ -33,5 +33,23 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search return Decision.Accept(); } + + public Decision IsSatisfiedBy(RemoteMovie remoteEpisode, SearchCriteriaBase searchCriteria) + { + var torrentInfo = remoteEpisode.Release as TorrentInfo; + + if (torrentInfo == null) + { + return Decision.Accept(); + } + + if (torrentInfo.Seeders != null && torrentInfo.Seeders < 1) + { + _logger.Debug("Not enough seeders. ({0})", torrentInfo.Seeders); + return Decision.Reject("Not enough seeders. ({0})", torrentInfo.Seeders); + } + + return Decision.Accept(); + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs index 5a24b6305..e30d0fc92 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs @@ -30,6 +30,25 @@ namespace NzbDrone.Core.DecisionEngine.Specifications } } + return Decision.Accept(); + } + + public virtual Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria) + { + if (subject.Movie.MovieFile.Value == null) + { + return Decision.Accept(); + } + + var file = subject.Movie.MovieFile.Value; + _logger.Debug("Comparing file quality with report. Existing file is {0}", file.Quality); + + if (!_qualityUpgradableSpecification.IsUpgradable(subject.Movie.Profile, file.Quality, subject.ParsedMovieInfo.Quality)) + { + return Decision.Reject("Quality for existing file on disk is of equal or higher preference: {0}", file.Quality); + } + + return Decision.Accept(); } }