1
0
mirror of https://github.com/Radarr/Radarr.git synced 2024-11-05 02:22:31 +01:00

New: Check whether an existing episode file was deleted before grabbing an upgrade, to avoid timing issues in combination with Ignore Deleted Episodes.

This commit is contained in:
Taloth Saldono 2017-03-11 19:49:32 +01:00
parent 413ce1d9a7
commit fa006d85fd
34 changed files with 302 additions and 17 deletions

View File

@ -28,6 +28,8 @@ public class DownloadDecisionMakerFixture : CoreTest<DownloadDecisionMaker>
private Mock<IDecisionEngineSpecification> _fail2;
private Mock<IDecisionEngineSpecification> _fail3;
private Mock<IDecisionEngineSpecification> _failDelayed1;
[SetUp]
public void Setup()
{
@ -39,14 +41,19 @@ public void Setup()
_fail2 = new Mock<IDecisionEngineSpecification>();
_fail3 = new Mock<IDecisionEngineSpecification>();
_failDelayed1 = new Mock<IDecisionEngineSpecification>();
_pass1.Setup(c => c.IsSatisfiedBy(It.IsAny<RemoteEpisode>(), null)).Returns(Decision.Accept);
_pass2.Setup(c => c.IsSatisfiedBy(It.IsAny<RemoteEpisode>(), null)).Returns(Decision.Accept);
_pass3.Setup(c => c.IsSatisfiedBy(It.IsAny<RemoteEpisode>(), null)).Returns(Decision.Accept);
_fail1.Setup(c => c.IsSatisfiedBy(It.IsAny<RemoteEpisode>(), null)).Returns(Decision.Reject("fail1"));
_fail2.Setup(c => c.IsSatisfiedBy(It.IsAny<RemoteEpisode>(), null)).Returns(Decision.Reject("fail2"));
_fail3.Setup(c => c.IsSatisfiedBy(It.IsAny<RemoteEpisode>(), null)).Returns(Decision.Reject("fail3"));
_failDelayed1.Setup(c => c.IsSatisfiedBy(It.IsAny<RemoteEpisode>(), null)).Returns(Decision.Reject("failDelayed1"));
_failDelayed1.SetupGet(c => c.Priority).Returns(SpecificationPriority.Disk);
_reports = new List<ReleaseInfo> { new ReleaseInfo { Title = "The.Office.S03E115.DVDRip.XviD-OSiTV" } };
_remoteEpisode = new RemoteEpisode {
Series = new Series(),
@ -78,6 +85,25 @@ public void should_call_all_specifications()
_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 @@ public void should_only_include_reports_for_requested_episodes()
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<IParsingService>()
@ -289,4 +315,4 @@ public void should_return_a_decision_when_exception_is_caught()
ExceptionVerification.ExpectedErrors(1);
}
}
}
}

View File

@ -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<DeletedEpisodeFileSpecification>
{
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<Episode> { new Episode { EpisodeFile = _firstFile, EpisodeFileId = 1 } };
var doubleEpisodeList = new List<Episode> {
new Episode { EpisodeFile = _firstFile, EpisodeFileId = 1 },
new Episode { EpisodeFile = _secondFile, EpisodeFileId = 2 }
};
var fakeSeries = Builder<Series>.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<IConfigService>()
.SetupGet(v => v.AutoUnmonitorPreviouslyDownloadedEpisodes)
.Returns(enabled);
}
private void WithExistingFile(EpisodeFile episodeFile)
{
var path = Path.Combine(@"C:\Series\My.Series".AsOsAgnostic(), episodeFile.RelativePath);
Mocker.GetMock<IDiskProvider>()
.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();
}
}
}

View File

@ -159,6 +159,7 @@
<Compile Include="DecisionEngineTests\MinimumAgeSpecificationFixture.cs" />
<Compile Include="DecisionEngineTests\RetentionSpecificationFixture.cs" />
<Compile Include="DecisionEngineTests\RssSync\DelaySpecificationFixture.cs" />
<Compile Include="DecisionEngineTests\RssSync\DeletedEpisodeFileSpecificationFixture.cs" />
<Compile Include="DecisionEngineTests\RssSync\ProperSpecificationFixture.cs" />
<Compile Include="DecisionEngineTests\Search\SeriesSpecificationFixture.cs" />
<Compile Include="DecisionEngineTests\SameEpisodesSpecificationFixture.cs" />

View File

@ -122,8 +122,16 @@ private IEnumerable<DownloadDecision> GetDecisions(List<ReleaseInfo> reports, Se
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());
}

View File

@ -7,6 +7,8 @@ public interface IDecisionEngineSpecification
{
RejectionType Type { get; }
SpecificationPriority Priority { get; }
Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria);
}
}

View File

@ -0,0 +1,10 @@
namespace NzbDrone.Core.DecisionEngine
{
public enum SpecificationPriority
{
Default = 0,
Parsing = 0,
Database = 0,
Disk = 1
}
}

View File

@ -22,6 +22,7 @@ public AcceptableSizeSpecification(IQualityDefinitionService qualityDefinitionSe
_logger = logger;
}
public SpecificationPriority Priority => SpecificationPriority.Default;
public RejectionType Type => RejectionType.Permanent;
public Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria)

View File

@ -18,6 +18,7 @@ public AnimeVersionUpgradeSpecification(QualityUpgradableSpecification qualityUp
_logger = logger;
}
public SpecificationPriority Priority => SpecificationPriority.Default;
public RejectionType Type => RejectionType.Permanent;
public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria)

View File

@ -16,10 +16,11 @@ public BlacklistSpecification(IBlacklistService blacklistService, Logger logger)
_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);

View File

@ -16,6 +16,7 @@ public CutoffSpecification(QualityUpgradableSpecification qualityUpgradableSpeci
_logger = logger;
}
public SpecificationPriority Priority => SpecificationPriority.Default;
public RejectionType Type => RejectionType.Permanent;
public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria)
@ -29,7 +30,7 @@ public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase
}
_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.");

View File

@ -19,6 +19,7 @@ public FullSeasonSpecification(Logger logger, IEpisodeService episodeService)
_episodeService = episodeService;
}
public SpecificationPriority Priority => SpecificationPriority.Default;
public RejectionType Type => RejectionType.Permanent;
public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria)

View File

@ -13,12 +13,13 @@ public LanguageSpecification(Logger logger)
_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)

View File

@ -16,6 +16,7 @@ public MinimumAgeSpecification(IConfigService configService, Logger logger)
_logger = logger;
}
public SpecificationPriority Priority => SpecificationPriority.Default;
public RejectionType Type => RejectionType.Temporary;
public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria)

View File

@ -8,6 +8,7 @@ public class NotSampleSpecification : IDecisionEngineSpecification
{
private readonly Logger _logger;
public SpecificationPriority Priority => SpecificationPriority.Default;
public RejectionType Type => RejectionType.Permanent;
public NotSampleSpecification(Logger logger)

View File

@ -18,6 +18,7 @@ public ProtocolSpecification(IDelayProfileService delayProfileService,
_logger = logger;
}
public SpecificationPriority Priority => SpecificationPriority.Default;
public RejectionType Type => RejectionType.Permanent;
public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria)

View File

@ -13,6 +13,7 @@ public QualityAllowedByProfileSpecification(Logger logger)
_logger = logger;
}
public SpecificationPriority Priority => SpecificationPriority.Default;
public RejectionType Type => RejectionType.Permanent;
public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria)

View File

@ -21,6 +21,7 @@ public QueueSpecification(IQueueService queueService,
_logger = logger;
}
public SpecificationPriority Priority => SpecificationPriority.Default;
public RejectionType Type => RejectionType.Permanent;
public Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria)

View File

@ -19,6 +19,7 @@ public RawDiskSpecification(Logger logger)
_logger = logger;
}
public SpecificationPriority Priority => SpecificationPriority.Default;
public RejectionType Type => RejectionType.Permanent;
public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria)

View File

@ -20,6 +20,7 @@ public ReleaseRestrictionsSpecification(IRestrictionService restrictionService,
_logger = logger;
}
public SpecificationPriority Priority => SpecificationPriority.Default;
public RejectionType Type => RejectionType.Permanent;
public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria)

View File

@ -16,6 +16,7 @@ public RetentionSpecification(IConfigService configService, Logger logger)
_logger = logger;
}
public SpecificationPriority Priority => SpecificationPriority.Default;
public RejectionType Type => RejectionType.Permanent;
public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria)

View File

@ -26,6 +26,7 @@ public DelaySpecification(IPendingReleaseService pendingReleaseService,
_logger = logger;
}
public SpecificationPriority Priority => SpecificationPriority.Database;
public RejectionType Type => RejectionType.Temporary;
public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria)

View File

@ -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);
}
}
}

View File

@ -26,6 +26,7 @@ public HistorySpecification(IHistoryService historyService,
_logger = logger;
}
public SpecificationPriority Priority => SpecificationPriority.Database;
public RejectionType Type => RejectionType.Permanent;
public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria)
@ -59,7 +60,7 @@ public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase
{
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);

View File

@ -14,6 +14,7 @@ public MonitoredEpisodeSpecification(Logger logger)
_logger = logger;
}
public SpecificationPriority Priority => SpecificationPriority.Default;
public RejectionType Type => RejectionType.Permanent;
public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria)

View File

@ -20,6 +20,7 @@ public ProperSpecification(QualityUpgradableSpecification qualityUpgradableSpeci
_logger = logger;
}
public SpecificationPriority Priority => SpecificationPriority.Default;
public RejectionType Type => RejectionType.Permanent;
public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria)

View File

@ -15,6 +15,7 @@ public SameEpisodesGrabSpecification(SameEpisodesSpecification sameEpisodesSpeci
_logger = logger;
}
public SpecificationPriority Priority => SpecificationPriority.Default;
public RejectionType Type => RejectionType.Permanent;
public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria)

View File

@ -16,6 +16,7 @@ public DailyEpisodeMatchSpecification(Logger logger, IEpisodeService episodeServ
_episodeService = episodeService;
}
public SpecificationPriority Priority => SpecificationPriority.Database;
public RejectionType Type => RejectionType.Permanent;
public Decision IsSatisfiedBy(RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteria)
@ -40,4 +41,4 @@ public Decision IsSatisfiedBy(RemoteEpisode remoteEpisode, SearchCriteriaBase se
return Decision.Accept();
}
}
}
}

View File

@ -15,6 +15,7 @@ public EpisodeRequestedSpecification(Logger logger)
_logger = logger;
}
public SpecificationPriority Priority => SpecificationPriority.Default;
public RejectionType Type => RejectionType.Permanent;
public Decision IsSatisfiedBy(RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteria)

View File

@ -13,6 +13,7 @@ public SeasonMatchSpecification(Logger logger)
_logger = logger;
}
public SpecificationPriority Priority => SpecificationPriority.Default;
public RejectionType Type => RejectionType.Permanent;
public Decision IsSatisfiedBy(RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteria)
@ -34,4 +35,4 @@ public Decision IsSatisfiedBy(RemoteEpisode remoteEpisode, SearchCriteriaBase se
return Decision.Accept();
}
}
}
}

View File

@ -13,6 +13,7 @@ public SeriesSpecification(Logger logger)
_logger = logger;
}
public SpecificationPriority Priority => SpecificationPriority.Default;
public RejectionType Type => RejectionType.Permanent;
public Decision IsSatisfiedBy(RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteria)
@ -33,4 +34,4 @@ public Decision IsSatisfiedBy(RemoteEpisode remoteEpisode, SearchCriteriaBase se
return Decision.Accept();
}
}
}
}

View File

@ -14,6 +14,7 @@ public SingleEpisodeSearchMatchSpecification(Logger logger)
_logger = logger;
}
public SpecificationPriority Priority => SpecificationPriority.Default;
public RejectionType Type => RejectionType.Permanent;
public Decision IsSatisfiedBy(RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteria)
@ -47,4 +48,4 @@ public Decision IsSatisfiedBy(RemoteEpisode remoteEpisode, SearchCriteriaBase se
return Decision.Accept();
}
}
}
}

View File

@ -13,6 +13,7 @@ public TorrentSeedingSpecification(Logger logger)
_logger = logger;
}
public SpecificationPriority Priority => SpecificationPriority.Default;
public RejectionType Type => RejectionType.Permanent;
@ -34,4 +35,4 @@ public Decision IsSatisfiedBy(RemoteEpisode remoteEpisode, SearchCriteriaBase se
return Decision.Accept();
}
}
}
}

View File

@ -16,6 +16,7 @@ public UpgradeDiskSpecification(QualityUpgradableSpecification qualityUpgradable
_logger = logger;
}
public SpecificationPriority Priority => SpecificationPriority.Default;
public RejectionType Type => RejectionType.Permanent;
public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria)

View File

@ -315,6 +315,7 @@
<Compile Include="DecisionEngine\Rejection.cs" />
<Compile Include="DecisionEngine\RejectionType.cs" />
<Compile Include="DecisionEngine\SameEpisodesSpecification.cs" />
<Compile Include="DecisionEngine\SpecificationPriority.cs" />
<Compile Include="DecisionEngine\Specifications\AcceptableSizeSpecification.cs" />
<Compile Include="DecisionEngine\Specifications\BlacklistSpecification.cs" />
<Compile Include="DecisionEngine\Specifications\AnimeVersionUpgradeSpecification.cs" />
@ -330,6 +331,7 @@
<Compile Include="DecisionEngine\Specifications\RetentionSpecification.cs" />
<Compile Include="DecisionEngine\Specifications\RssSync\DelaySpecification.cs" />
<Compile Include="DecisionEngine\Specifications\RssSync\HistorySpecification.cs" />
<Compile Include="DecisionEngine\Specifications\RssSync\DeletedEpisodeFileSpecification.cs" />
<Compile Include="DecisionEngine\Specifications\RssSync\MonitoredEpisodeSpecification.cs" />
<Compile Include="DecisionEngine\Specifications\RssSync\ProperSpecification.cs" />
<Compile Include="DecisionEngine\Specifications\Search\DailyEpisodeMatchSpecification.cs" />