diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/DownloadDecisionMakerFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/DownloadDecisionMakerFixture.cs index 23b662982..90b251ace 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/DownloadDecisionMakerFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/DownloadDecisionMakerFixture.cs @@ -10,6 +10,7 @@ using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; using NzbDrone.Test.Common; +using FizzWare.NBuilder; namespace NzbDrone.Core.Test.DecisionEngineTests { @@ -141,7 +142,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests results.Should().BeEmpty(); } - [Test] public void should_not_attempt_to_map_episode_series_title_is_blank() + [Test] + public void should_not_attempt_to_map_episode_series_title_is_blank() { GivenSpecifications(_pass1, _pass2, _pass3); _reports[0].Title = "1937 - Snow White and the Seven Dwarves"; @@ -204,5 +206,49 @@ namespace NzbDrone.Core.Test.DecisionEngineTests result.Should().HaveCount(1); } + + [Test] + public void should_only_include_reports_for_requested_episodes() + { + var series = Builder.CreateNew().Build(); + + var episodes = Builder.CreateListOfSize(2) + .All() + .With(v => v.SeriesId, series.Id) + .With(v => v.Series, series) + .With(v => v.SeasonNumber, 1) + .With(v => v.SceneSeasonNumber, 2) + .BuildList(); + + 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) + }).ToList(); + + Mocker.GetMock() + .Setup(v => v.Map(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((p,id,c) => + new RemoteEpisode + { + DownloadAllowed = true, + ParsedEpisodeInfo = p, + Series = series, + Episodes = episodes.Where(v => v.SceneEpisodeNumber == p.EpisodeNumbers.First()).ToList() + }); + + Mocker.SetConstant>(new List + { + Mocker.Resolve() + }); + + var decisions = Subject.GetSearchDecision(reports, criteria); + + var approvedDecisions = decisions.Where(v => v.Approved).ToList(); + + approvedDecisions.Count.Should().Be(1); + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/IndexerSearchTests/NzbSearchServiceFixture.cs b/src/NzbDrone.Core.Test/IndexerSearchTests/NzbSearchServiceFixture.cs new file mode 100644 index 000000000..d6780b56f --- /dev/null +++ b/src/NzbDrone.Core.Test/IndexerSearchTests/NzbSearchServiceFixture.cs @@ -0,0 +1,190 @@ +using NzbDrone.Core.IndexerSearch; +using NzbDrone.Core.Test.Framework; +using FizzWare.NBuilder; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using NzbDrone.Core.Tv; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.IndexerSearch.Definitions; + +namespace NzbDrone.Core.Test.IndexerSearchTests +{ + public class NzbSearchServiceFixture : CoreTest + { + private Series _xemSeries; + private List _xemEpisodes; + + [SetUp] + public void SetUp() + { + var indexer = Mocker.GetMock(); + indexer.SetupGet(s => s.SupportsSearching).Returns(true); + + Mocker.GetMock() + .Setup(s => s.GetAvailableProviders()) + .Returns(new List { indexer.Object }); + + Mocker.GetMock() + .Setup(s => s.GetSearchDecision(It.IsAny>(), It.IsAny())) + .Returns(new List()); + + _xemSeries = Builder.CreateNew() + .With(v => v.UseSceneNumbering = true) + .Build(); + + _xemEpisodes = new List(); + + Mocker.GetMock() + .Setup(v => v.GetSeries(_xemSeries.Id)) + .Returns(_xemSeries); + + Mocker.GetMock() + .Setup(v => v.GetEpisodesBySeason(_xemSeries.Id, It.IsAny())) + .Returns((i, j) => _xemEpisodes.Where(d => d.SeasonNumber == j).ToList()); + } + + private void WithEpisode(int seasonNumber, int episodeNumber, int sceneSeasonNumber, int sceneEpisodeNumber) + { + var episode = Builder.CreateNew() + .With(v => v.SeriesId == _xemSeries.Id) + .With(v => v.Series == _xemSeries) + .With(v => v.SeasonNumber, seasonNumber) + .With(v => v.EpisodeNumber, episodeNumber) + .With(v => v.SceneSeasonNumber, sceneSeasonNumber) + .With(v => v.SceneEpisodeNumber, sceneEpisodeNumber) + .Build(); + + _xemEpisodes.Add(episode); + } + + private void WithEpisodes() + { + // Season 1 maps to Scene Season 2 (one-to-one) + WithEpisode(1, 12, 2, 3); + WithEpisode(1, 13, 2, 4); + + // Season 2 maps to Scene Season 3 & 4 (one-to-one) + WithEpisode(2, 1, 3, 11); + WithEpisode(2, 2, 3, 12); + WithEpisode(2, 3, 4, 11); + WithEpisode(2, 4, 4, 12); + + // Season 3 maps to Scene Season 5 (partial) + // Season 4 maps to Scene Season 5 & 6 (partial) + WithEpisode(3, 1, 5, 11); + WithEpisode(3, 2, 5, 12); + WithEpisode(4, 1, 5, 13); + WithEpisode(4, 2, 5, 14); + WithEpisode(4, 3, 6, 11); + WithEpisode(5, 1, 6, 12); + + // Season 7+ maps normally, so no mapping specified. + WithEpisode(7, 1, 0, 0); + WithEpisode(7, 2, 0, 0); + } + + private List WatchForSearchCriteria() + { + List result = new List(); + + Mocker.GetMock() + .Setup(v => v.Fetch(It.IsAny(), It.IsAny())) + .Callback((i, s) => result.Add(s)) + .Returns(new List()); + + Mocker.GetMock() + .Setup(v => v.Fetch(It.IsAny(), It.IsAny())) + .Callback((i, s) => result.Add(s)) + .Returns(new List()); + + return result; + } + + [Test] + public void scene_episodesearch() + { + WithEpisodes(); + + var allCriteria = WatchForSearchCriteria(); + + Subject.EpisodeSearch(_xemEpisodes.First()); + + var criteria = allCriteria.OfType().ToList(); + + criteria.Count.Should().Be(1); + criteria[0].SeasonNumber.Should().Be(2); + criteria[0].EpisodeNumber.Should().Be(3); + } + + [Test] + public void scene_seasonsearch() + { + WithEpisodes(); + + var allCriteria = WatchForSearchCriteria(); + + Subject.SeasonSearch(_xemSeries.Id, 1); + + var criteria = allCriteria.OfType().ToList(); + + criteria.Count.Should().Be(1); + criteria[0].SeasonNumber.Should().Be(2); + } + + [Test] + public void scene_seasonsearch_should_search_multiple_seasons() + { + WithEpisodes(); + + var allCriteria = WatchForSearchCriteria(); + + Subject.SeasonSearch(_xemSeries.Id, 2); + + var criteria = allCriteria.OfType().ToList(); + + criteria.Count.Should().Be(2); + criteria[0].SeasonNumber.Should().Be(3); + criteria[1].SeasonNumber.Should().Be(4); + } + + [Test] + public void scene_seasonsearch_should_search_single_episode_if_possible() + { + WithEpisodes(); + + var allCriteria = WatchForSearchCriteria(); + + Subject.SeasonSearch(_xemSeries.Id, 4); + + var criteria1 = allCriteria.OfType().ToList(); + var criteria2 = allCriteria.OfType().ToList(); + + criteria1.Count.Should().Be(1); + criteria1[0].SeasonNumber.Should().Be(5); + + criteria2.Count.Should().Be(1); + criteria2[0].SeasonNumber.Should().Be(6); + criteria2[0].EpisodeNumber.Should().Be(11); + } + + [Test] + public void scene_seasonsearch_should_use_seasonnumber_if_no_scene_number_is_available() + { + WithEpisodes(); + + var allCriteria = WatchForSearchCriteria(); + + Subject.SeasonSearch(_xemSeries.Id, 7); + + var criteria = allCriteria.OfType().ToList(); + + criteria.Count.Should().Be(1); + criteria[0].SeasonNumber.Should().Be(7); + } + } +} diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index f140c68b8..70086788a 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -138,6 +138,7 @@ + diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/EpisodeRequestedSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/EpisodeRequestedSpecification.cs new file mode 100644 index 000000000..6df94a6fc --- /dev/null +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/EpisodeRequestedSpecification.cs @@ -0,0 +1,43 @@ +using System.Linq; +using NLog; +using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.Parser.Model; + + +namespace NzbDrone.Core.DecisionEngine.Specifications.Search +{ + public class EpisodeRequestedSpecification : IDecisionEngineSpecification + { + private readonly Logger _logger; + + public EpisodeRequestedSpecification(Logger logger) + { + _logger = logger; + } + + public string RejectionReason + { + get + { + return "Episode wasn't requested"; + } + } + + public bool IsSatisfiedBy(RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteria) + { + if (searchCriteria == null) + { + return true; + } + var criteriaEpisodes = searchCriteria.Episodes.Select(v => v.Id).ToList(); + var remoteEpisodes = remoteEpisode.Episodes.Select(v => v.Id).ToList(); + if (!criteriaEpisodes.Intersect(remoteEpisodes).Any()) + { + _logger.Debug("Release rejected since the episode wasn't requested: {0}", remoteEpisode.ParsedEpisodeInfo); + return false; + } + + return true; + } + } +} diff --git a/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs b/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs index b2b3ddb1d..1b71323e6 100644 --- a/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs +++ b/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs @@ -138,10 +138,53 @@ namespace NzbDrone.Core.IndexerSearch return SearchSpecial(series, episodes); } - var searchSpec = Get(series, episodes); - searchSpec.SeasonNumber = seasonNumber; + List downloadDecisions = new List(); - return Dispatch(indexer => _feedFetcher.Fetch(indexer, searchSpec), searchSpec); + if (series.UseSceneNumbering) + { + var sceneSeasonGroups = episodes.GroupBy(v => + { + if (v.SceneSeasonNumber == 0 && v.SceneEpisodeNumber == 0) + return v.SeasonNumber; + else + return v.SceneSeasonNumber; + }).Distinct(); + + foreach (var sceneSeasonEpisodes in sceneSeasonGroups) + { + if (sceneSeasonEpisodes.Count() == 1) + { + var episode = sceneSeasonEpisodes.First(); + var searchSpec = Get(series, sceneSeasonEpisodes.ToList()); + searchSpec.SeasonNumber = sceneSeasonEpisodes.Key; + if (episode.SceneSeasonNumber == 0 && episode.SceneEpisodeNumber == 0) + searchSpec.EpisodeNumber = episode.EpisodeNumber; + else + searchSpec.EpisodeNumber = episode.SceneEpisodeNumber; + + var decisions = Dispatch(indexer => _feedFetcher.Fetch(indexer, searchSpec), searchSpec); + downloadDecisions.AddRange(decisions); + } + else + { + var searchSpec = Get(series, sceneSeasonEpisodes.ToList()); + searchSpec.SeasonNumber = sceneSeasonEpisodes.Key; + + var decisions = Dispatch(indexer => _feedFetcher.Fetch(indexer, searchSpec), searchSpec); + downloadDecisions.AddRange(decisions); + } + } + } + else + { + var searchSpec = Get(series, episodes); + searchSpec.SeasonNumber = seasonNumber; + + var decisions = Dispatch(indexer => _feedFetcher.Fetch(indexer, searchSpec), searchSpec); + downloadDecisions.AddRange(decisions); + } + + return downloadDecisions; } private TSpec Get(Series series, List episodes) where TSpec : SearchCriteriaBase, new() diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 0e3b6e8b7..c331efd94 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -217,6 +217,7 @@ +