From 198ff059c4287f24af01860e573691b73b690dd1 Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Sun, 30 Mar 2014 13:50:17 +0200 Subject: [PATCH 1/2] Fixed: Season Search now correctly uses scene numbering. --- .../DownloadDecisionMakerFixture.cs | 43 ++++- .../NzbSearchServiceFixture.cs | 170 ++++++++++++++++++ .../NzbDrone.Core.Test.csproj | 1 + .../DecisionEngine/DownloadDecisionMaker.cs | 11 ++ .../IndexerSearch/NzbSearchService.cs | 39 +++- 5 files changed, 260 insertions(+), 4 deletions(-) create mode 100644 src/NzbDrone.Core.Test/IndexerSearchTests/NzbSearchServiceFixture.cs diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/DownloadDecisionMakerFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/DownloadDecisionMakerFixture.cs index 23b662982..2d501ca2a 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,44 @@ 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()); + + var decisions = Subject.GetSearchDecision(reports, criteria); + + Assert.AreEqual(1, decisions.Count); + } } } \ 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..2ccfa51ea --- /dev/null +++ b/src/NzbDrone.Core.Test/IndexerSearchTests/NzbSearchServiceFixture.cs @@ -0,0 +1,170 @@ +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 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); + } + + 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(); + + Assert.AreEqual(1, criteria.Count); + Assert.AreEqual(2, criteria[0].SeasonNumber); + Assert.AreEqual(3, criteria[0].EpisodeNumber); + } + + [Test] + public void scene_seasonsearch() + { + WithEpisodes(); + + var allCriteria = WatchForSearchCriteria(); + + Subject.SeasonSearch(_xemSeries.Id, 1); + + var criteria = allCriteria.OfType().ToList(); + + Assert.AreEqual(1, criteria.Count); + Assert.AreEqual(2, criteria[0].SeasonNumber); + } + + [Test] + public void scene_seasonsearch_should_search_multiple_seasons() + { + WithEpisodes(); + + var allCriteria = WatchForSearchCriteria(); + + Subject.SeasonSearch(_xemSeries.Id, 2); + + var criteria = allCriteria.OfType().ToList(); + + Assert.AreEqual(2, criteria.Count); + Assert.AreEqual(3, criteria[0].SeasonNumber); + Assert.AreEqual(4, criteria[1].SeasonNumber); + } + + [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(); + + Assert.AreEqual(1, criteria1.Count); + Assert.AreEqual(5, criteria1[0].SeasonNumber); + + Assert.AreEqual(1, criteria2.Count); + Assert.AreEqual(6, criteria2[0].SeasonNumber); + Assert.AreEqual(11, criteria2[0].EpisodeNumber); + } + } +} 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/DownloadDecisionMaker.cs b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs index cfed22a5c..b5b13d9a7 100644 --- a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs +++ b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs @@ -97,6 +97,17 @@ namespace NzbDrone.Core.DecisionEngine if (decision != null) { + if (searchCriteria != null) + { + var criteriaEpisodes = searchCriteria.Episodes.Select(v => v.Id).ToList(); + var remoteEpisodes = decision.RemoteEpisode.Episodes.Select(v => v.Id).ToList(); + if (!criteriaEpisodes.Intersect(remoteEpisodes).Any()) + { + _logger.Debug("Release rejected since the episode wasn't requested: {0}", decision.RemoteEpisode.ParsedEpisodeInfo); + continue; + } + } + if (decision.Rejections.Any()) { _logger.Debug("Release rejected for the following reasons: {0}", String.Join(", ", decision.Rejections)); diff --git a/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs b/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs index b2b3ddb1d..a5b140beb 100644 --- a/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs +++ b/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs @@ -138,10 +138,43 @@ 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 => v.SceneSeasonNumber).Distinct(); + + foreach (var sceneSeasonEpisodes in sceneSeasonGroups) + { + if (sceneSeasonEpisodes.Count() == 1) + { + var searchSpec = Get(series, sceneSeasonEpisodes.ToList()); + searchSpec.SeasonNumber = sceneSeasonEpisodes.Key; + searchSpec.EpisodeNumber = sceneSeasonEpisodes.First().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() From 38b0fae29a676084bad1ff59cd9ec440dadddd9b Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Tue, 1 Apr 2014 21:39:17 +0200 Subject: [PATCH 2/2] Moved Episode Not Requested check to new Specification. Updated tests. --- .../DownloadDecisionMakerFixture.cs | 9 +++- .../NzbSearchServiceFixture.cs | 54 +++++++++++++------ .../DecisionEngine/DownloadDecisionMaker.cs | 11 ---- .../Search/EpisodeRequestedSpecification.cs | 43 +++++++++++++++ .../IndexerSearch/NzbSearchService.cs | 14 ++++- src/NzbDrone.Core/NzbDrone.Core.csproj | 1 + 6 files changed, 100 insertions(+), 32 deletions(-) create mode 100644 src/NzbDrone.Core/DecisionEngine/Specifications/Search/EpisodeRequestedSpecification.cs diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/DownloadDecisionMakerFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/DownloadDecisionMakerFixture.cs index 2d501ca2a..90b251ace 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/DownloadDecisionMakerFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/DownloadDecisionMakerFixture.cs @@ -239,11 +239,16 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Episodes = episodes.Where(v => v.SceneEpisodeNumber == p.EpisodeNumbers.First()).ToList() }); - Mocker.SetConstant>(new List()); + Mocker.SetConstant>(new List + { + Mocker.Resolve() + }); var decisions = Subject.GetSearchDecision(reports, criteria); - Assert.AreEqual(1, decisions.Count); + 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 index 2ccfa51ea..d6780b56f 100644 --- a/src/NzbDrone.Core.Test/IndexerSearchTests/NzbSearchServiceFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerSearchTests/NzbSearchServiceFixture.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Text; +using FluentAssertions; using Moq; using NUnit.Framework; using NzbDrone.Core.Tv; @@ -26,12 +27,12 @@ namespace NzbDrone.Core.Test.IndexerSearchTests Mocker.GetMock() .Setup(s => s.GetAvailableProviders()) - .Returns(new List{indexer.Object}); + .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(); @@ -81,12 +82,16 @@ namespace NzbDrone.Core.Test.IndexerSearchTests 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)) @@ -94,7 +99,7 @@ namespace NzbDrone.Core.Test.IndexerSearchTests Mocker.GetMock() .Setup(v => v.Fetch(It.IsAny(), It.IsAny())) - .Callback((i,s) => result.Add(s)) + .Callback((i, s) => result.Add(s)) .Returns(new List()); return result; @@ -111,9 +116,9 @@ namespace NzbDrone.Core.Test.IndexerSearchTests var criteria = allCriteria.OfType().ToList(); - Assert.AreEqual(1, criteria.Count); - Assert.AreEqual(2, criteria[0].SeasonNumber); - Assert.AreEqual(3, criteria[0].EpisodeNumber); + criteria.Count.Should().Be(1); + criteria[0].SeasonNumber.Should().Be(2); + criteria[0].EpisodeNumber.Should().Be(3); } [Test] @@ -127,8 +132,8 @@ namespace NzbDrone.Core.Test.IndexerSearchTests var criteria = allCriteria.OfType().ToList(); - Assert.AreEqual(1, criteria.Count); - Assert.AreEqual(2, criteria[0].SeasonNumber); + criteria.Count.Should().Be(1); + criteria[0].SeasonNumber.Should().Be(2); } [Test] @@ -142,9 +147,9 @@ namespace NzbDrone.Core.Test.IndexerSearchTests var criteria = allCriteria.OfType().ToList(); - Assert.AreEqual(2, criteria.Count); - Assert.AreEqual(3, criteria[0].SeasonNumber); - Assert.AreEqual(4, criteria[1].SeasonNumber); + criteria.Count.Should().Be(2); + criteria[0].SeasonNumber.Should().Be(3); + criteria[1].SeasonNumber.Should().Be(4); } [Test] @@ -159,12 +164,27 @@ namespace NzbDrone.Core.Test.IndexerSearchTests var criteria1 = allCriteria.OfType().ToList(); var criteria2 = allCriteria.OfType().ToList(); - Assert.AreEqual(1, criteria1.Count); - Assert.AreEqual(5, criteria1[0].SeasonNumber); + 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(); - Assert.AreEqual(1, criteria2.Count); - Assert.AreEqual(6, criteria2[0].SeasonNumber); - Assert.AreEqual(11, criteria2[0].EpisodeNumber); + criteria.Count.Should().Be(1); + criteria[0].SeasonNumber.Should().Be(7); } } } diff --git a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs index b5b13d9a7..cfed22a5c 100644 --- a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs +++ b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs @@ -97,17 +97,6 @@ namespace NzbDrone.Core.DecisionEngine if (decision != null) { - if (searchCriteria != null) - { - var criteriaEpisodes = searchCriteria.Episodes.Select(v => v.Id).ToList(); - var remoteEpisodes = decision.RemoteEpisode.Episodes.Select(v => v.Id).ToList(); - if (!criteriaEpisodes.Intersect(remoteEpisodes).Any()) - { - _logger.Debug("Release rejected since the episode wasn't requested: {0}", decision.RemoteEpisode.ParsedEpisodeInfo); - continue; - } - } - if (decision.Rejections.Any()) { _logger.Debug("Release rejected for the following reasons: {0}", String.Join(", ", decision.Rejections)); 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 a5b140beb..1b71323e6 100644 --- a/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs +++ b/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs @@ -142,15 +142,25 @@ namespace NzbDrone.Core.IndexerSearch if (series.UseSceneNumbering) { - var sceneSeasonGroups = episodes.GroupBy(v => v.SceneSeasonNumber).Distinct(); + 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; - searchSpec.EpisodeNumber = sceneSeasonEpisodes.First().SceneEpisodeNumber; + 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); 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 @@ +