From fa006d85fd7e8beb19cc5196e655dcaffd3f7413 Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Sat, 11 Mar 2017 19:49:32 +0100 Subject: [PATCH] New: Check whether an existing episode file was deleted before grabbing an upgrade, to avoid timing issues in combination with Ignore Deleted Episodes. --- .../DownloadDecisionMakerFixture.cs | 38 ++++- .../DeletedEpisodeFileSpecificationFixture.cs | 139 ++++++++++++++++++ .../NzbDrone.Core.Test.csproj | 1 + .../DecisionEngine/DownloadDecisionMaker.cs | 12 +- .../IDecisionEngineSpecification.cs | 2 + .../DecisionEngine/SpecificationPriority.cs | 10 ++ .../AcceptableSizeSpecification.cs | 1 + .../AnimeVersionUpgradeSpecification.cs | 1 + .../Specifications/BlacklistSpecification.cs | 3 +- .../Specifications/CutoffSpecification.cs | 3 +- .../Specifications/FullSeasonSpecification.cs | 1 + .../Specifications/LanguageSpecification.cs | 3 +- .../Specifications/MinimumAgeSpecification.cs | 1 + .../Specifications/NotSampleSpecification.cs | 1 + .../Specifications/ProtocolSpecification.cs | 1 + .../QualityAllowedByProfileSpecification.cs | 1 + .../Specifications/QueueSpecification.cs | 1 + .../Specifications/RawDiskSpecification.cs | 1 + .../ReleaseRestrictionsSpecification.cs | 1 + .../Specifications/RetentionSpecification.cs | 1 + .../RssSync/DelaySpecification.cs | 1 + .../DeletedEpisodeFileSpecification.cs | 71 +++++++++ .../RssSync/HistorySpecification.cs | 3 +- .../RssSync/MonitoredEpisodeSpecification.cs | 1 + .../RssSync/ProperSpecification.cs | 1 + .../SameEpisodesGrabSpecification.cs | 1 + .../Search/DailyEpisodeMatchSpecification.cs | 3 +- .../Search/EpisodeRequestedSpecification.cs | 1 + .../Search/SeasonMatchSpecification.cs | 3 +- .../Search/SeriesSpecification.cs | 3 +- .../SingleEpisodeSearchMatchSpecification.cs | 3 +- .../Search/TorrentSeedingSpecification.cs | 3 +- .../UpgradeDiskSpecification.cs | 1 + src/NzbDrone.Core/NzbDrone.Core.csproj | 2 + 34 files changed, 302 insertions(+), 17 deletions(-) create mode 100644 src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DeletedEpisodeFileSpecificationFixture.cs create mode 100644 src/NzbDrone.Core/DecisionEngine/SpecificationPriority.cs create mode 100644 src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DeletedEpisodeFileSpecification.cs diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/DownloadDecisionMakerFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/DownloadDecisionMakerFixture.cs index 0206abbd2..c9225540f 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/DownloadDecisionMakerFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/DownloadDecisionMakerFixture.cs @@ -28,6 +28,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests private Mock _fail2; private Mock _fail3; + private Mock _failDelayed1; + [SetUp] public void Setup() { @@ -39,14 +41,19 @@ namespace NzbDrone.Core.Test.DecisionEngineTests _fail2 = new Mock(); _fail3 = new Mock(); + _failDelayed1 = new Mock(); + _pass1.Setup(c => c.IsSatisfiedBy(It.IsAny(), null)).Returns(Decision.Accept); _pass2.Setup(c => c.IsSatisfiedBy(It.IsAny(), null)).Returns(Decision.Accept); _pass3.Setup(c => c.IsSatisfiedBy(It.IsAny(), null)).Returns(Decision.Accept); - + _fail1.Setup(c => c.IsSatisfiedBy(It.IsAny(), null)).Returns(Decision.Reject("fail1")); _fail2.Setup(c => c.IsSatisfiedBy(It.IsAny(), null)).Returns(Decision.Reject("fail2")); _fail3.Setup(c => c.IsSatisfiedBy(It.IsAny(), null)).Returns(Decision.Reject("fail3")); + _failDelayed1.Setup(c => c.IsSatisfiedBy(It.IsAny(), null)).Returns(Decision.Reject("failDelayed1")); + _failDelayed1.SetupGet(c => c.Priority).Returns(SpecificationPriority.Disk); + _reports = new List { new ReleaseInfo { Title = "The.Office.S03E115.DVDRip.XviD-OSiTV" } }; _remoteEpisode = new RemoteEpisode { Series = new Series(), @@ -78,6 +85,25 @@ namespace NzbDrone.Core.Test.DecisionEngineTests _pass3.Verify(c => c.IsSatisfiedBy(_remoteEpisode, null), Times.Once()); } + [Test] + public void should_call_delayed_specifications_if_non_delayed_passed() + { + GivenSpecifications(_pass1, _failDelayed1); + + Subject.GetRssDecision(_reports).ToList(); + _failDelayed1.Verify(c => c.IsSatisfiedBy(_remoteEpisode, null), Times.Once()); + } + + [Test] + public void should_not_call_delayed_specifications_if_non_delayed_failed() + { + GivenSpecifications(_fail1, _failDelayed1); + + Subject.GetRssDecision(_reports).ToList(); + + _failDelayed1.Verify(c => c.IsSatisfiedBy(_remoteEpisode, null), Times.Never()); + } + [Test] public void should_return_rejected_if_single_specs_fail() { @@ -214,10 +240,10 @@ namespace NzbDrone.Core.Test.DecisionEngineTests var criteria = new SeasonSearchCriteria { Episodes = episodes.Take(1).ToList(), SeasonNumber = 1 }; - var reports = episodes.Select(v => - new ReleaseInfo() - { - Title = string.Format("{0}.S{1:00}E{2:00}.720p.WEB-DL-DRONE", series.Title, v.SceneSeasonNumber, v.SceneEpisodeNumber) + var reports = episodes.Select(v => + new ReleaseInfo() + { + Title = string.Format("{0}.S{1:00}E{2:00}.720p.WEB-DL-DRONE", series.Title, v.SceneSeasonNumber, v.SceneEpisodeNumber) }).ToList(); Mocker.GetMock() @@ -289,4 +315,4 @@ namespace NzbDrone.Core.Test.DecisionEngineTests ExceptionVerification.ExpectedErrors(1); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DeletedEpisodeFileSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DeletedEpisodeFileSpecificationFixture.cs new file mode 100644 index 000000000..a1e3691a7 --- /dev/null +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DeletedEpisodeFileSpecificationFixture.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.DecisionEngine.Specifications.RssSync; +using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Profiles; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Tv; +using NzbDrone.Core.DecisionEngine; + +using NzbDrone.Core.Test.Framework; +using NzbDrone.Common.Disk; +using Moq; +using NzbDrone.Test.Common; +using System.IO; + +namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync +{ + [TestFixture] + public class DeletedEpisodeFileSpecificationFixture : CoreTest + { + private RemoteEpisode _parseResultMulti; + private RemoteEpisode _parseResultSingle; + private EpisodeFile _firstFile; + private EpisodeFile _secondFile; + + [SetUp] + public void Setup() + { + _firstFile = new EpisodeFile + { + Id = 1, + RelativePath = "My.Series.S01E01.mkv", + Quality = new QualityModel(Quality.Bluray1080p, new Revision(version: 1)), + DateAdded = DateTime.Now + }; + _secondFile = new EpisodeFile + { + Id = 2, + RelativePath = "My.Series.S01E02.mkv", + Quality = new QualityModel(Quality.Bluray1080p, new Revision(version: 1)), + DateAdded = DateTime.Now + }; + + var singleEpisodeList = new List { new Episode { EpisodeFile = _firstFile, EpisodeFileId = 1 } }; + var doubleEpisodeList = new List { + new Episode { EpisodeFile = _firstFile, EpisodeFileId = 1 }, + new Episode { EpisodeFile = _secondFile, EpisodeFileId = 2 } + }; + + var fakeSeries = Builder.CreateNew() + .With(c => c.Profile = new Profile { Cutoff = Quality.Bluray1080p }) + .With(c => c.Path = @"C:\Series\My.Series".AsOsAgnostic()) + .Build(); + + _parseResultMulti = new RemoteEpisode + { + Series = fakeSeries, + ParsedEpisodeInfo = new ParsedEpisodeInfo { Quality = new QualityModel(Quality.DVD, new Revision(version: 2)) }, + Episodes = doubleEpisodeList + }; + + _parseResultSingle = new RemoteEpisode + { + Series = fakeSeries, + ParsedEpisodeInfo = new ParsedEpisodeInfo { Quality = new QualityModel(Quality.DVD, new Revision(version: 2)) }, + Episodes = singleEpisodeList + }; + + GivenUnmonitorDeletedEpisodes(true); + } + + private void GivenUnmonitorDeletedEpisodes(bool enabled) + { + Mocker.GetMock() + .SetupGet(v => v.AutoUnmonitorPreviouslyDownloadedEpisodes) + .Returns(enabled); + } + + private void WithExistingFile(EpisodeFile episodeFile) + { + var path = Path.Combine(@"C:\Series\My.Series".AsOsAgnostic(), episodeFile.RelativePath); + + Mocker.GetMock() + .Setup(v => v.FileExists(path)) + .Returns(true); + } + + [Test] + public void should_return_true_when_unmonitor_deleted_episdes_is_off() + { + GivenUnmonitorDeletedEpisodes(false); + + Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeTrue(); + } + + [Test] + public void should_return_true_when_searching() + { + Subject.IsSatisfiedBy(_parseResultSingle, new SeasonSearchCriteria()).Accepted.Should().BeTrue(); + } + + [Test] + public void should_return_true_if_file_exists() + { + WithExistingFile(_firstFile); + + Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeTrue(); + } + + [Test] + public void should_return_false_if_file_is_missing() + { + Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeFalse(); + } + + [Test] + public void should_return_true_if_both_of_multiple_episode_exist() + { + WithExistingFile(_firstFile); + WithExistingFile(_secondFile); + + Subject.IsSatisfiedBy(_parseResultMulti, null).Accepted.Should().BeTrue(); + } + + [Test] + public void should_return_false_if_one_of_multiple_episode_is_missing() + { + WithExistingFile(_firstFile); + + Subject.IsSatisfiedBy(_parseResultMulti, null).Accepted.Should().BeFalse(); + } + } +} diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 61dff5f73..c716524ec 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -159,6 +159,7 @@ + diff --git a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs index bb1a70873..343280208 100644 --- a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs +++ b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs @@ -122,8 +122,16 @@ namespace NzbDrone.Core.DecisionEngine private DownloadDecision GetDecisionForReport(RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteria = null) { - var reasons = _specifications.Select(c => EvaluateSpec(c, remoteEpisode, searchCriteria)) - .Where(c => c != null); + var reasons = new Rejection[0]; + + foreach (var specifications in _specifications.GroupBy(v => v.Priority).OrderBy(v => v.Key)) + { + reasons = specifications.Select(c => EvaluateSpec(c, remoteEpisode, searchCriteria)) + .Where(c => c != null) + .ToArray(); + + if (reasons.Any()) break; + } return new DownloadDecision(remoteEpisode, reasons.ToArray()); } diff --git a/src/NzbDrone.Core/DecisionEngine/IDecisionEngineSpecification.cs b/src/NzbDrone.Core/DecisionEngine/IDecisionEngineSpecification.cs index 199984734..08ea4c012 100644 --- a/src/NzbDrone.Core/DecisionEngine/IDecisionEngineSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/IDecisionEngineSpecification.cs @@ -7,6 +7,8 @@ namespace NzbDrone.Core.DecisionEngine { RejectionType Type { get; } + SpecificationPriority Priority { get; } + Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria); } } diff --git a/src/NzbDrone.Core/DecisionEngine/SpecificationPriority.cs b/src/NzbDrone.Core/DecisionEngine/SpecificationPriority.cs new file mode 100644 index 000000000..e3eb0b9d7 --- /dev/null +++ b/src/NzbDrone.Core/DecisionEngine/SpecificationPriority.cs @@ -0,0 +1,10 @@ +namespace NzbDrone.Core.DecisionEngine +{ + public enum SpecificationPriority + { + Default = 0, + Parsing = 0, + Database = 0, + Disk = 1 + } +} diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs index 4ab566d2e..d8af6bbce 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs @@ -22,6 +22,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications _logger = logger; } + public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; public Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/AnimeVersionUpgradeSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/AnimeVersionUpgradeSpecification.cs index c2f93f7c0..3adb65df0 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/AnimeVersionUpgradeSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/AnimeVersionUpgradeSpecification.cs @@ -18,6 +18,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications _logger = logger; } + public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/BlacklistSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/BlacklistSpecification.cs index 18b216263..1b2c3d05e 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/BlacklistSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/BlacklistSpecification.cs @@ -16,10 +16,11 @@ namespace NzbDrone.Core.DecisionEngine.Specifications _logger = logger; } + public SpecificationPriority Priority => SpecificationPriority.Database; public RejectionType Type => RejectionType.Permanent; public Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) - { + { if (_blacklistService.Blacklisted(subject.Series.Id, subject.Release)) { _logger.Debug("{0} is blacklisted, rejecting.", subject.Release.Title); diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/CutoffSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/CutoffSpecification.cs index c5d52a48a..39102b0fc 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/CutoffSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/CutoffSpecification.cs @@ -16,6 +16,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications _logger = logger; } + public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) @@ -29,7 +30,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications } _logger.Debug("Comparing file quality with report. Existing file is {0}", file.Quality); - + if (!_qualityUpgradableSpecification.CutoffNotMet(subject.Series.Profile, file.Quality, subject.ParsedEpisodeInfo.Quality)) { _logger.Debug("Cutoff already met, rejecting."); diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/FullSeasonSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/FullSeasonSpecification.cs index 023b6be60..bea83b303 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/FullSeasonSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/FullSeasonSpecification.cs @@ -19,6 +19,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications _episodeService = episodeService; } + public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/LanguageSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/LanguageSpecification.cs index 9f7f75038..edef58757 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/LanguageSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/LanguageSpecification.cs @@ -13,12 +13,13 @@ namespace NzbDrone.Core.DecisionEngine.Specifications _logger = logger; } + public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { var wantedLanguage = subject.Series.Profile.Value.Language; - + _logger.Debug("Checking if report meets language requirements. {0}", subject.ParsedEpisodeInfo.Language); if (subject.ParsedEpisodeInfo.Language != wantedLanguage) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/MinimumAgeSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/MinimumAgeSpecification.cs index 449d7be76..57f8a2487 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/MinimumAgeSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/MinimumAgeSpecification.cs @@ -16,6 +16,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications _logger = logger; } + public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Temporary; public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/NotSampleSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/NotSampleSpecification.cs index 02ff7653a..091efb948 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/NotSampleSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/NotSampleSpecification.cs @@ -8,6 +8,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications { private readonly Logger _logger; + public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; public NotSampleSpecification(Logger logger) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/ProtocolSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/ProtocolSpecification.cs index 008e58812..ba956ecbd 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/ProtocolSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/ProtocolSpecification.cs @@ -18,6 +18,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications _logger = logger; } + public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/QualityAllowedByProfileSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/QualityAllowedByProfileSpecification.cs index 7913e0e7e..d8dcd9afb 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/QualityAllowedByProfileSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/QualityAllowedByProfileSpecification.cs @@ -13,6 +13,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications _logger = logger; } + public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs index 6f3ec1bea..838d8a80d 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs @@ -21,6 +21,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications _logger = logger; } + public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; public Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RawDiskSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RawDiskSpecification.cs index 7f278cb7e..77b9c006f 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RawDiskSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RawDiskSpecification.cs @@ -19,6 +19,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications _logger = logger; } + public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/ReleaseRestrictionsSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/ReleaseRestrictionsSpecification.cs index 9fb8c13f5..39a286af9 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/ReleaseRestrictionsSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/ReleaseRestrictionsSpecification.cs @@ -20,6 +20,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications _logger = logger; } + public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RetentionSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RetentionSpecification.cs index 97802f871..99cf93f67 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RetentionSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RetentionSpecification.cs @@ -16,6 +16,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications _logger = logger; } + public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DelaySpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DelaySpecification.cs index 68551c66c..da6285942 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DelaySpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DelaySpecification.cs @@ -26,6 +26,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync _logger = logger; } + public SpecificationPriority Priority => SpecificationPriority.Database; public RejectionType Type => RejectionType.Temporary; public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DeletedEpisodeFileSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DeletedEpisodeFileSpecification.cs new file mode 100644 index 000000000..bf4ab6ae5 --- /dev/null +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DeletedEpisodeFileSpecification.cs @@ -0,0 +1,71 @@ +using System.IO; +using System.Linq; +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync +{ + public class DeletedEpisodeFileSpecification : IDecisionEngineSpecification + { + private readonly IDiskProvider _diskProvider; + private readonly IConfigService _configService; + private readonly Logger _logger; + + public DeletedEpisodeFileSpecification(IDiskProvider diskProvider, IConfigService configService, Logger logger) + { + _diskProvider = diskProvider; + _configService = configService; + _logger = logger; + } + + public SpecificationPriority Priority => SpecificationPriority.Disk; + public RejectionType Type => RejectionType.Temporary; + + public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) + { + if (!_configService.AutoUnmonitorPreviouslyDownloadedEpisodes) + { + return Decision.Accept(); + } + + if (searchCriteria != null) + { + _logger.Debug("Skipping deleted episodefile check during search"); + return Decision.Accept(); + } + + var missingEpisodeFiles = subject.Episodes + .Where(v => v.EpisodeFileId != 0) + .Select(v => v.EpisodeFile.Value) + .DistinctBy(v => v.Id) + .Where(v => IsEpisodeFileMissing(subject.Series, v)) + .ToArray(); + + if (missingEpisodeFiles.Any()) + { + foreach (var missingEpisodeFile in missingEpisodeFiles) + { + _logger.Trace("Episode file {0} is missing from disk.", missingEpisodeFile.RelativePath); + } + + _logger.Debug("Files for this episode exist in the database but not on disk, will be unmonitored on next diskscan. skipping."); + return Decision.Reject("Series is not monitored"); + } + + return Decision.Accept(); + } + + private bool IsEpisodeFileMissing(Series series, EpisodeFile episodeFile) + { + var fullPath = Path.Combine(series.Path, episodeFile.RelativePath); + + return !_diskProvider.FileExists(fullPath); + } + } +} diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs index 9aa4fabf1..8ce71848c 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs @@ -26,6 +26,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync _logger = logger; } + public SpecificationPriority Priority => SpecificationPriority.Database; public RejectionType Type => RejectionType.Permanent; public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) @@ -59,7 +60,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync { if (recent) { - return Decision.Reject("Recent grab event in history already meets cutoff: {0}", mostRecent.Quality); + 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); diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/MonitoredEpisodeSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/MonitoredEpisodeSpecification.cs index f56f26478..ca0cb5d32 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/MonitoredEpisodeSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/MonitoredEpisodeSpecification.cs @@ -14,6 +14,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync _logger = logger; } + public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/ProperSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/ProperSpecification.cs index 0c6632d25..f3f5d4e8d 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/ProperSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/ProperSpecification.cs @@ -20,6 +20,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync _logger = logger; } + public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/SameEpisodesGrabSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/SameEpisodesGrabSpecification.cs index 1a8c5db5b..0ef769d01 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/SameEpisodesGrabSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/SameEpisodesGrabSpecification.cs @@ -15,6 +15,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications _logger = logger; } + public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/DailyEpisodeMatchSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/DailyEpisodeMatchSpecification.cs index 50fd9b3cc..49136428f 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/DailyEpisodeMatchSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/DailyEpisodeMatchSpecification.cs @@ -16,6 +16,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search _episodeService = episodeService; } + public SpecificationPriority Priority => SpecificationPriority.Database; public RejectionType Type => RejectionType.Permanent; public Decision IsSatisfiedBy(RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteria) @@ -40,4 +41,4 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search return Decision.Accept(); } } -} \ 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..e4bd7b2c6 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/EpisodeRequestedSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/EpisodeRequestedSpecification.cs @@ -15,6 +15,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search _logger = logger; } + public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; public Decision IsSatisfiedBy(RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteria) diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SeasonMatchSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SeasonMatchSpecification.cs index b09d888ec..8516b08e7 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SeasonMatchSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SeasonMatchSpecification.cs @@ -13,6 +13,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search _logger = logger; } + public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; public Decision IsSatisfiedBy(RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteria) @@ -34,4 +35,4 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search return Decision.Accept(); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SeriesSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SeriesSpecification.cs index 7f1201b33..07afbaada 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SeriesSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SeriesSpecification.cs @@ -13,6 +13,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search _logger = logger; } + public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; public Decision IsSatisfiedBy(RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteria) @@ -33,4 +34,4 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search 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 fb056734f..4d4f76d05 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SingleEpisodeSearchMatchSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SingleEpisodeSearchMatchSpecification.cs @@ -14,6 +14,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search _logger = logger; } + public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; public Decision IsSatisfiedBy(RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteria) @@ -47,4 +48,4 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search return Decision.Accept(); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/TorrentSeedingSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/TorrentSeedingSpecification.cs index 87c244b53..65230ef12 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/TorrentSeedingSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/TorrentSeedingSpecification.cs @@ -13,6 +13,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search _logger = logger; } + public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; @@ -34,4 +35,4 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search 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 427a4cd4f..8554c0c40 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs @@ -16,6 +16,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications _logger = logger; } + public SpecificationPriority Priority => SpecificationPriority.Default; public RejectionType Type => RejectionType.Permanent; public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 9e238632b..735d1a73f 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -315,6 +315,7 @@ + @@ -330,6 +331,7 @@ +