diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/MatchesGrabSpecificationFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/MatchesGrabSpecificationFixture.cs new file mode 100644 index 000000000..6dae08e61 --- /dev/null +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/MatchesGrabSpecificationFixture.cs @@ -0,0 +1,119 @@ +using System.Collections.Generic; +using FizzWare.NBuilder; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using NzbDrone.Core.Download; +using NzbDrone.Core.History; +using NzbDrone.Core.MediaFiles.EpisodeImport.Specifications; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications +{ + [TestFixture] + public class MatchesGrabSpecificationFixture : CoreTest + { + private Episode _episode1; + private Episode _episode2; + private Episode _episode3; + private LocalEpisode _localEpisode; + private DownloadClientItem _downloadClientItem; + + [SetUp] + public void Setup() + { + _episode1 = Builder.CreateNew() + .With(e => e.Id = 1) + .Build(); + + _episode2 = Builder.CreateNew() + .With(e => e.Id = 2) + .Build(); + + _episode3 = Builder.CreateNew() + .With(e => e.Id = 3) + .Build(); + + _localEpisode = Builder.CreateNew() + .With(l => l.Path = @"C:\Test\Unsorted\Series.Title.S01E01.720p.HDTV-Sonarr\S01E05.mkv".AsOsAgnostic()) + .With(l => l.Episodes = new List { _episode1 }) + .Build(); + + _downloadClientItem = Builder.CreateNew().Build(); + } + + private void GivenHistoryForEpisodes(params Episode[] episodes) + { + var history = new List(); + + foreach (var episode in episodes) + { + history.Add(Builder.CreateNew() + .With(h => h.EventType = EpisodeHistoryEventType.Grabbed) + .With(h => h.EpisodeId = episode.Id) + .Build()); + } + + Mocker.GetMock() + .Setup(s => s.FindByDownloadId(It.IsAny())) + .Returns(history); + } + + [Test] + public void should_be_accepted_for_existing_file() + { + _localEpisode.ExistingFile = true; + + Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeTrue(); + } + + [Test] + public void should_be_accepted_if_no_download_client_item() + { + Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeTrue(); + } + + [Test] + public void should_be_accepted_if_no_grab_history() + { + GivenHistoryForEpisodes(); + + Subject.IsSatisfiedBy(_localEpisode, _downloadClientItem).Accepted.Should().BeTrue(); + } + + [Test] + public void should_be_accepted_if_file_episode_matches_single_grab_history() + { + GivenHistoryForEpisodes(_episode1); + + Subject.IsSatisfiedBy(_localEpisode, _downloadClientItem).Accepted.Should().BeTrue(); + } + + [Test] + public void should_be_accepted_if_file_episode_is_in_multi_episode_grab_history() + { + GivenHistoryForEpisodes(_episode1, _episode2); + + Subject.IsSatisfiedBy(_localEpisode, _downloadClientItem).Accepted.Should().BeTrue(); + } + + [Test] + public void should_be_rejected_if_file_episode_does_not_match_single_grab_history() + { + GivenHistoryForEpisodes(_episode2); + + Subject.IsSatisfiedBy(_localEpisode, _downloadClientItem).Accepted.Should().BeFalse(); + } + + [Test] + public void should_be_rejected_if_file_episode_is_not_in_multi_episode_grab_history() + { + GivenHistoryForEpisodes(_episode2, _episode3); + + Subject.IsSatisfiedBy(_localEpisode, _downloadClientItem).Accepted.Should().BeFalse(); + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/MatchesGrabSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/MatchesGrabSpecification.cs new file mode 100644 index 000000000..532103f17 --- /dev/null +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/MatchesGrabSpecification.cs @@ -0,0 +1,70 @@ +using System.Collections.Generic; +using System.Linq; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Download; +using NzbDrone.Core.History; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications +{ + public class MatchesGrabSpecification : IImportDecisionEngineSpecification + { + private readonly Logger _logger; + private readonly IParsingService _parsingService; + private readonly IHistoryService _historyService; + + public MatchesGrabSpecification(IParsingService parsingService, IHistoryService historyService, Logger logger) + { + _logger = logger; + _parsingService = parsingService; + _historyService = historyService; + } + + public Decision IsSatisfiedBy(LocalEpisode localEpisode, DownloadClientItem downloadClientItem) + { + if (localEpisode.ExistingFile) + { + return Decision.Accept(); + } + + if (downloadClientItem == null) + { + return Decision.Accept(); + } + + var grabbedHistory = _historyService.FindByDownloadId(downloadClientItem.DownloadId) + .Where(h => h.EventType == EpisodeHistoryEventType.Grabbed) + .ToList(); + + if (grabbedHistory.Empty()) + { + return Decision.Accept(); + } + + var unexpected = localEpisode.Episodes.Where(e => grabbedHistory.All(o => o.EpisodeId != e.Id)).ToList(); + + if (unexpected.Any()) + { + _logger.Debug("Unexpected episode(s) in file: {0}", FormatEpisode(unexpected)); + + if (unexpected.Count == 1) + { + return Decision.Reject("Episode {0} was not found in the grabbed release: {1}", FormatEpisode(unexpected), grabbedHistory.First().SourceTitle); + } + + return Decision.Reject("Episodes {0} were not found in the grabbed release: {1}", FormatEpisode(unexpected), grabbedHistory.First().SourceTitle); + } + + return Decision.Accept(); + } + + private string FormatEpisode(List episodes) + { + return string.Join(", ", episodes.Select(e => $"{e.SeasonNumber}x{e.EpisodeNumber:00}")); + } + } +}