From 5e7c0951b7d3cc9a383e9d1dd9108f60a8d4e061 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 13 Jan 2013 00:24:48 -0800 Subject: [PATCH] Searching refactored #ND-135 fixed --- NzbDrone.Core.Test/NzbDrone.Core.Test.csproj | 15 +- .../GetSeriesTitleFixture.cs | 59 -- .../PerformSearchFixture.cs | 282 --------- .../ProcessDailySearchResultsFixture.cs | 283 --------- .../SearchProviderTests/SearchFixture.cs | 210 ------- .../CheckReportFixture.cs | 84 +++ .../PerformSearchFixture.cs | 54 ++ .../EpisodeSearchTests/CheckReportFixture.cs | 131 ++++ .../PerformSearchFixture.cs | 106 ++++ .../SearchTests/GetSearchTitleFixture.cs | 60 ++ .../CheckReportFixture.cs | 66 ++ .../PerformSearchFixture.cs | 81 +++ .../SearchTests/PerformSearchTestBase.cs | 100 +++ .../ProcessResultsFixture.cs} | 197 +++--- .../ProviderTests/SearchTests/TestSearch.cs | 67 ++ NzbDrone.Core/Jobs/EpisodeSearchJob.cs | 51 +- NzbDrone.Core/Jobs/SeasonSearchJob.cs | 4 +- NzbDrone.Core/NzbDrone.Core.csproj | 4 + .../Providers/Search/DailyEpisodeSearch.cs | 78 +++ .../Providers/Search/EpisodeSearch.cs | 95 ++- .../Providers/Search/PartialSeasonSearch.cs | 110 ++++ NzbDrone.Core/Providers/Search/SearchBase.cs | 161 +++-- NzbDrone.Core/Providers/SearchProvider.cs | 512 +--------------- NzbDrone.Core/Providers/SearchProvider2.cs | 580 ++++++++++++++++++ 24 files changed, 1851 insertions(+), 1539 deletions(-) delete mode 100644 NzbDrone.Core.Test/ProviderTests/SearchProviderTests/GetSeriesTitleFixture.cs delete mode 100644 NzbDrone.Core.Test/ProviderTests/SearchProviderTests/PerformSearchFixture.cs delete mode 100644 NzbDrone.Core.Test/ProviderTests/SearchProviderTests/ProcessDailySearchResultsFixture.cs delete mode 100644 NzbDrone.Core.Test/ProviderTests/SearchProviderTests/SearchFixture.cs create mode 100644 NzbDrone.Core.Test/ProviderTests/SearchTests/DailyEpisodeSearchTests/CheckReportFixture.cs create mode 100644 NzbDrone.Core.Test/ProviderTests/SearchTests/DailyEpisodeSearchTests/PerformSearchFixture.cs create mode 100644 NzbDrone.Core.Test/ProviderTests/SearchTests/EpisodeSearchTests/CheckReportFixture.cs create mode 100644 NzbDrone.Core.Test/ProviderTests/SearchTests/EpisodeSearchTests/PerformSearchFixture.cs create mode 100644 NzbDrone.Core.Test/ProviderTests/SearchTests/GetSearchTitleFixture.cs create mode 100644 NzbDrone.Core.Test/ProviderTests/SearchTests/PartialSeasonSearchTests/CheckReportFixture.cs create mode 100644 NzbDrone.Core.Test/ProviderTests/SearchTests/PartialSeasonSearchTests/PerformSearchFixture.cs create mode 100644 NzbDrone.Core.Test/ProviderTests/SearchTests/PerformSearchTestBase.cs rename NzbDrone.Core.Test/ProviderTests/{SearchProviderTests/ProcessSearchResultsFixture.cs => SearchTests/ProcessResultsFixture.cs} (63%) create mode 100644 NzbDrone.Core.Test/ProviderTests/SearchTests/TestSearch.cs create mode 100644 NzbDrone.Core/Providers/Search/DailyEpisodeSearch.cs create mode 100644 NzbDrone.Core/Providers/Search/PartialSeasonSearch.cs create mode 100644 NzbDrone.Core/Providers/SearchProvider2.cs diff --git a/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 46aad0b85..1018e14e7 100644 --- a/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -142,7 +142,16 @@ - + + + + + + + + + + @@ -190,10 +199,6 @@ - - - - diff --git a/NzbDrone.Core.Test/ProviderTests/SearchProviderTests/GetSeriesTitleFixture.cs b/NzbDrone.Core.Test/ProviderTests/SearchProviderTests/GetSeriesTitleFixture.cs deleted file mode 100644 index 2fad2dfa1..000000000 --- a/NzbDrone.Core.Test/ProviderTests/SearchProviderTests/GetSeriesTitleFixture.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using FizzWare.NBuilder; -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Core.Providers; -using NzbDrone.Core.Repository; -using NzbDrone.Test.Common; - -namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests -{ - public class GetSeriesTitleFixture : TestBase - { - private Series _series; - private const string SCENE_NAME = "Scandal"; - - [SetUp] - public void Setup() - { - _series = Builder - .CreateNew() - .With(s => s.Title = "Scandal (2012)") - .Build(); - } - - private void WithSceneName() - { - Mocker.GetMock() - .Setup(s => s.GetSceneName(_series.SeriesId)) - .Returns("Scandal"); - } - - [Test] - public void should_return_scene_name_when_sceneName_is_available() - { - WithSceneName(); - - Mocker.Resolve().GetSeriesTitle(_series).Should().Be(SCENE_NAME); - } - - [Test] - public void should_return_seriesTitle_when_sceneName_is_not_available() - { - Mocker.Resolve().GetSeriesTitle(_series).Should().Be(_series.Title); - } - - [TestCase("Mike & Molly", "Mike and Molly")] - [TestCase("Franklin & Bash", "Franklin and Bash")] - [TestCase("Law & Order", "Law and Order")] - public void should_replace_ampersand_with_and_when_returning_title(string seriesTitle, string expectedTitle) - { - _series.Title = seriesTitle; - - Mocker.Resolve().GetSeriesTitle(_series).Should().Be(expectedTitle); - } - } -} diff --git a/NzbDrone.Core.Test/ProviderTests/SearchProviderTests/PerformSearchFixture.cs b/NzbDrone.Core.Test/ProviderTests/SearchProviderTests/PerformSearchFixture.cs deleted file mode 100644 index f3de2b4c2..000000000 --- a/NzbDrone.Core.Test/ProviderTests/SearchProviderTests/PerformSearchFixture.cs +++ /dev/null @@ -1,282 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using FizzWare.NBuilder; -using FluentAssertions; -using Moq; -using NUnit.Framework; -using NzbDrone.Core.Model; -using NzbDrone.Core.Providers; -using NzbDrone.Core.Providers.Indexer; -using NzbDrone.Core.Repository; -using NzbDrone.Core.Test.Framework; -using NzbDrone.Test.Common; - -namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests -{ - public class PerformSearchFixture : CoreTest - { - private const string SCENE_NAME = "Scene Name"; - private const int SEASON_NUMBER = 5; - private const int EPISODE_NUMBER = 1; - private const int PARSE_RESULT_COUNT = 10; - - private Mock _episodeIndexer1; - private Mock _episodeIndexer2; - private Mock _brokenIndexer; - private Mock _nullIndexer; - - private List _indexers; - - private Series _series; - private IList _episodes; - - [SetUp] - public void Setup() - { - var parseResults = Builder.CreateListOfSize(PARSE_RESULT_COUNT) - .Build(); - - _series = Builder.CreateNew() - .Build(); - - _episodes = Builder.CreateListOfSize(1) - .Build(); - - BuildIndexers(parseResults); - - _indexers = new List { _episodeIndexer1.Object, _episodeIndexer2.Object }; - - Mocker.GetMock() - .Setup(c => c.GetEnabledIndexers()) - .Returns(_indexers); - } - - private void BuildIndexers(IList parseResults) - { - _episodeIndexer1 = new Mock(); - _episodeIndexer1.Setup(c => c.FetchEpisode(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(parseResults); - _episodeIndexer1.Setup(c => c.FetchDailyEpisode(It.IsAny(), It.IsAny())) - .Returns(parseResults); - _episodeIndexer1.Setup(c => c.FetchSeason(It.IsAny(), It.IsAny())) - .Returns(parseResults); - _episodeIndexer1.Setup(c => c.FetchPartialSeason(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(parseResults); - _episodeIndexer1.Setup(s => s.Name).Returns("Episode Indexer 1"); - - _episodeIndexer2 = new Mock(); - _episodeIndexer2.Setup(c => c.FetchEpisode(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(parseResults); - _episodeIndexer2.Setup(c => c.FetchDailyEpisode(It.IsAny(), It.IsAny())) - .Returns(parseResults); - _episodeIndexer2.Setup(c => c.FetchSeason(It.IsAny(), It.IsAny())) - .Returns(parseResults); - _episodeIndexer2.Setup(c => c.FetchPartialSeason(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(parseResults); - _episodeIndexer2.Setup(s => s.Name).Returns("Episode Indexer 2"); - - _brokenIndexer = new Mock(); - _brokenIndexer.Setup(c => c.FetchEpisode(It.IsAny(), It.IsAny(), It.IsAny())) - .Throws(new Exception()); - _brokenIndexer.Setup(s => s.Name).Returns("Broken Indexer"); - - _nullIndexer = new Mock(); - _nullIndexer.Setup(c => c.FetchEpisode(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns>(null); - _nullIndexer.Setup(s => s.Name).Returns("Null Indexer"); - } - - private void WithTwoGoodOneBrokenIndexer() - { - _indexers = new List { _episodeIndexer1.Object, _brokenIndexer.Object, _episodeIndexer2.Object }; - - Mocker.GetMock() - .Setup(c => c.GetEnabledIndexers()) - .Returns(_indexers); - } - - private void WithNullIndexers() - { - _indexers = new List { _nullIndexer.Object, _nullIndexer.Object }; - - Mocker.GetMock() - .Setup(c => c.GetEnabledIndexers()) - .Returns(_indexers); - } - - private void WithSceneName() - { - Mocker.GetMock() - .Setup(s => s.GetSceneName(_series.SeriesId)).Returns(SCENE_NAME); - } - - private void With30Episodes() - { - _episodes = Builder.CreateListOfSize(30) - .Build(); - } - - private void WithNullEpisodes() - { - _episodes = null; - } - - private void VerifyFetchEpisode(Times times) - { - _episodeIndexer1.Verify(v => v.FetchEpisode(_series.Title, SEASON_NUMBER, It.IsAny()) - , times); - - _episodeIndexer2.Verify(v => v.FetchEpisode(_series.Title, SEASON_NUMBER, It.IsAny()) - , times); - } - - private void VerifyFetchDailyEpisode(Times times) - { - _episodeIndexer1.Verify(v => v.FetchDailyEpisode(_series.Title, It.IsAny()) - , times); - - _episodeIndexer2.Verify(v => v.FetchDailyEpisode(_series.Title, It.IsAny()) - , times); - } - - private void VerifyFetchEpisodeWithSceneName(Times times) - { - _episodeIndexer1.Verify(v => v.FetchEpisode(SCENE_NAME, SEASON_NUMBER, It.IsAny()) - , times); - - _episodeIndexer2.Verify(v => v.FetchEpisode(SCENE_NAME, SEASON_NUMBER, It.IsAny()) - , times); - } - - private void VerifyFetchEpisodeBrokenIndexer(Times times) - { - _brokenIndexer.Verify(v => v.FetchEpisode(It.IsAny(), SEASON_NUMBER, It.IsAny()) - , times); - } - - private void VerifyFetchPartialSeason(Times times) - { - _episodeIndexer1.Verify(v => v.FetchPartialSeason(_series.Title, SEASON_NUMBER, It.IsAny()) - , times); - - _episodeIndexer2.Verify(v => v.FetchPartialSeason(_series.Title, SEASON_NUMBER, It.IsAny()) - , times); - } - - private void VerifyFetchSeason(Times times) - { - _episodeIndexer1.Verify(v => v.FetchSeason(_series.Title, SEASON_NUMBER), times); - _episodeIndexer1.Verify(v => v.FetchSeason(_series.Title, SEASON_NUMBER), times); - } - - [Test] - public void PerformSearch_should_search_all_enabled_providers() - { - //Act - var result = Mocker.Resolve().PerformEpisodeSearch(_series, SEASON_NUMBER, _episodes.First().EpisodeNumber); - - //Assert - VerifyFetchEpisode(Times.Once()); - result.Should().HaveCount(PARSE_RESULT_COUNT * 2); - } - - [Test] - public void broken_indexer_should_not_block_other_indexers() - { - //Setup - WithTwoGoodOneBrokenIndexer(); - - //Act - var result = Mocker.Resolve().PerformEpisodeSearch(_series, SEASON_NUMBER, EPISODE_NUMBER); - - //Assert - result.Should().HaveCount(PARSE_RESULT_COUNT * 2); - - VerifyFetchEpisode(Times.Once()); - VerifyFetchEpisodeBrokenIndexer(Times.Once()); - - Mocker.GetMock().Verify(c => c.GetSceneName(_series.SeriesId), - Times.Once()); - - ExceptionVerification.ExpectedErrors(1); - } - - [Test] - public void PerformSearch_for_episode_should_call_FetchEpisode() - { - //Act - var result = Mocker.Resolve().PerformEpisodeSearch(_series, SEASON_NUMBER, EPISODE_NUMBER); - - //Assert - result.Should().HaveCount(PARSE_RESULT_COUNT * 2); - - VerifyFetchEpisode(Times.Once()); - } - - [Test] - public void PerformSearch_for_daily_episode_should_call_FetchEpisode() - { - //Setup - _series.IsDaily = true; - - //Act - var result = Mocker.Resolve().PerformDailyEpisodeSearch(_series, _episodes.First()); - - //Assert - result.Should().HaveCount(PARSE_RESULT_COUNT * 2); - - VerifyFetchDailyEpisode(Times.Once()); - } - - [Test] - public void PerformSearch_for_partial_season_should_call_FetchPartialSeason() - { - With30Episodes(); - - //Act - var result = Mocker.Resolve().PerformPartialSeasonSearch(_series, SEASON_NUMBER, new List{0, 1, 2, 3}); - - //Assert - result.Should().HaveCount(80); - VerifyFetchPartialSeason(Times.Exactly(4)); - } - - [Test] - public void PerformSearch_for_season_should_call_FetchSeason() - { - WithNullEpisodes(); - - //Act - var result = Mocker.Resolve().PerformSeasonSearch(_series, SEASON_NUMBER); - - //Assert - result.Should().HaveCount(20); - VerifyFetchSeason(Times.Once()); - } - - [Test] - public void PerformSearch_should_return_empty_list_when_results_from_indexers_are_null() - { - //Setup - WithNullIndexers(); - - //Act - var result = Mocker.Resolve().PerformEpisodeSearch(_series, SEASON_NUMBER, EPISODE_NUMBER); - - //Assert - result.Should().HaveCount(0); - ExceptionVerification.ExpectedErrors(2); - } - - [Test] - public void should_use_scene_name_to_search_for_episode_when_avilable() - { - WithSceneName(); - - Mocker.Resolve().PerformEpisodeSearch(_series, SEASON_NUMBER, EPISODE_NUMBER); - - VerifyFetchEpisodeWithSceneName(Times.Once()); - } - } -} diff --git a/NzbDrone.Core.Test/ProviderTests/SearchProviderTests/ProcessDailySearchResultsFixture.cs b/NzbDrone.Core.Test/ProviderTests/SearchProviderTests/ProcessDailySearchResultsFixture.cs deleted file mode 100644 index 72783d57c..000000000 --- a/NzbDrone.Core.Test/ProviderTests/SearchProviderTests/ProcessDailySearchResultsFixture.cs +++ /dev/null @@ -1,283 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -using FizzWare.NBuilder; -using FluentAssertions; -using Moq; -using NUnit.Framework; -using NzbDrone.Core.Model; -using NzbDrone.Core.Model.Notification; -using NzbDrone.Core.Providers; -using NzbDrone.Core.Providers.DecisionEngine; -using NzbDrone.Core.Providers.Indexer; -using NzbDrone.Core.Repository; -using NzbDrone.Core.Repository.Quality; -using NzbDrone.Core.Test.Framework; -using NzbDrone.Test.Common; -using NzbDrone.Test.Common.AutoMoq; - -namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests -{ - [TestFixture] - // ReSharper disable InconsistentNaming - public class ProcessDailySearchResultsFixture : CoreTest - { - private Series _matchingSeries = null; - private Series _mismatchedSeries = null; - private Series _nullSeries = null; - - [SetUp] - public void setup() - { - _matchingSeries = Builder.CreateNew() - .With(s => s.SeriesId = 79488) - .With(s => s.Title = "30 Rock") - .Build(); - - _mismatchedSeries = Builder.CreateNew() - .With(s => s.SeriesId = 12345) - .With(s => s.Title = "Not 30 Rock") - .Build(); - } - - private void WithMatchingSeries() - { - Mocker.GetMock() - .Setup(s => s.FindSeries(It.IsAny())).Returns(_matchingSeries); - } - - private void WithMisMatchedSeries() - { - Mocker.GetMock() - .Setup(s => s.FindSeries(It.IsAny())).Returns(_mismatchedSeries); - } - - private void WithNullSeries() - { - Mocker.GetMock() - .Setup(s => s.FindSeries(It.IsAny())).Returns(_nullSeries); - } - - private void WithSuccessfulDownload() - { - Mocker.GetMock() - .Setup(s => s.DownloadReport(It.IsAny())) - .Returns(true); - } - - private void WithFailingDownload() - { - Mocker.GetMock() - .Setup(s => s.DownloadReport(It.IsAny())) - .Returns(false); - } - - private void WithQualityNeeded() - { - Mocker.GetMock() - .Setup(s => s.IsSatisfiedBy(It.IsAny())) - .Returns(ReportRejectionType.None); - } - - private void WithQualityNotNeeded() - { - Mocker.GetMock() - .Setup(s => s.IsSatisfiedBy(It.IsAny())) - .Returns(ReportRejectionType.ExistingQualityIsEqualOrBetter); - } - - [Test] - public void processSearchResults_higher_quality_should_be_called_first() - { - var parseResults = Builder.CreateListOfSize(5) - .All() - .With(c => c.AirDate = DateTime.Today) - .With(c => c.Quality = new QualityModel(QualityTypes.DVD, true)) - .Random(1) - .With(c => c.Quality = new QualityModel(QualityTypes.Bluray1080p, true)) - .Build(); - - WithMatchingSeries(); - WithSuccessfulDownload(); - - Mocker.GetMock() - .Setup(s => s.IsSatisfiedBy(It.Is(d => d.Quality.Quality == QualityTypes.Bluray1080p))) - .Returns(ReportRejectionType.None); - - //Act - var result = Mocker.Resolve().ProcessSearchResults(MockNotification, parseResults, _matchingSeries, DateTime.Today); - - //Assert - result.Should().Contain(n => n.Success); - - Mocker.GetMock().Verify(c => c.IsSatisfiedBy(It.IsAny()), - Times.Once()); - Mocker.GetMock().Verify(c => c.DownloadReport(It.IsAny()), - Times.Once()); - } - - [Test] - public void processSearchResults_when_quality_is_not_needed_should_check_the_rest() - { - var parseResults = Builder.CreateListOfSize(5) - .All() - .With(c => c.AirDate = DateTime.Today) - .With(c => c.Quality = new QualityModel(QualityTypes.DVD, true)) - .Build(); - - WithMatchingSeries(); - WithQualityNotNeeded(); - - //Act - var result = Mocker.Resolve().ProcessSearchResults(MockNotification, parseResults, _matchingSeries, DateTime.Today); - - //Assert - result.Should().NotContain(n => n.Success); - - Mocker.GetMock().Verify(c => c.IsSatisfiedBy(It.IsAny()), - Times.Exactly(5)); - Mocker.GetMock().Verify(c => c.DownloadReport(It.IsAny()), - Times.Never()); - } - - [Test] - public void processSearchResults_should_skip_if_series_is_null() - { - var parseResults = Builder.CreateListOfSize(5) - .All() - .With(e => e.AirDate = DateTime.Today) - .With(e => e.Quality = new QualityModel(QualityTypes.HDTV720p, false)) - .Build(); - - WithNullSeries(); - - //Act - var result = Mocker.Resolve().ProcessSearchResults(MockNotification, parseResults, _matchingSeries, DateTime.Today); - - //Assert - result.Should().NotContain(n => n.Success); - - Mocker.GetMock().Verify(c => c.DownloadReport(It.IsAny()), - Times.Never()); - } - - [Test] - public void processSearchResults_should_skip_if_series_is_mismatched() - { - var parseResults = Builder.CreateListOfSize(5) - .All() - .With(e => e.AirDate = DateTime.Today) - .With(e => e.Quality = new QualityModel(QualityTypes.HDTV720p, false)) - .Build(); - - WithMisMatchedSeries(); - - //Act - var result = Mocker.Resolve().ProcessSearchResults(MockNotification, parseResults, _matchingSeries, DateTime.Today); - - //Assert - result.Should().NotContain(n => n.Success); - - Mocker.GetMock().Verify(c => c.DownloadReport(It.IsAny()), - Times.Never()); - } - - [Test] - public void processSearchResults_should_return_after_successful_download() - { - var parseResults = Builder.CreateListOfSize(2) - .All() - .With(e => e.AirDate = DateTime.Today) - .With(c => c.Quality = new QualityModel(QualityTypes.DVD, true)) - .Build(); - - WithMatchingSeries(); - WithQualityNeeded(); - WithSuccessfulDownload(); - - //Act - var result = Mocker.Resolve().ProcessSearchResults(MockNotification, parseResults, _matchingSeries, DateTime.Today); - - //Assert - result.Should().Contain(n => n.Success); - - Mocker.GetMock().Verify(c => c.DownloadReport(It.IsAny()), - Times.Once()); - } - - [Test] - public void processSearchResults_should_try_next_if_download_fails() - { - var parseResults = Builder.CreateListOfSize(2) - .All() - .With(e => e.AirDate = DateTime.Today) - .With(c => c.Quality = new QualityModel(QualityTypes.DVD, true)) - .TheLast(1) - .With(c => c.Quality = new QualityModel(QualityTypes.SDTV, true)) - .Build(); - - WithMatchingSeries(); - WithQualityNeeded(); - - Mocker.GetMock() - .Setup(s => s.DownloadReport(It.Is(d => d.Quality.Quality == QualityTypes.DVD))) - .Returns(false); - - Mocker.GetMock() - .Setup(s => s.DownloadReport(It.Is(d => d.Quality.Quality == QualityTypes.SDTV))) - .Returns(true); - - //Act - var result = Mocker.Resolve().ProcessSearchResults(MockNotification, parseResults, _matchingSeries, DateTime.Today); - - //Assert - result.Should().Contain(n => n.Success); - - Mocker.GetMock().Verify(c => c.DownloadReport(It.IsAny()), - Times.Exactly(2)); - } - - [Test] - public void processSearchResults_should_skip_if_parseResult_does_not_have_airdate() - { - var parseResults = Builder.CreateListOfSize(5) - .All() - .With(e => e.AirDate = null) - .With(e => e.Quality = new QualityModel(QualityTypes.HDTV720p, false)) - .Build(); - - WithMatchingSeries(); - - //Act - var result = Mocker.Resolve().ProcessSearchResults(MockNotification, parseResults, _matchingSeries, DateTime.Today); - - //Assert - result.Should().NotContain(n => n.Success); - - Mocker.GetMock().Verify(c => c.DownloadReport(It.IsAny()), - Times.Never()); - } - - [Test] - public void processSearchResults_should_skip_if_parseResult_airdate_does_not_match() - { - var parseResults = Builder.CreateListOfSize(5) - .All() - .With(e => e.AirDate = DateTime.Today.AddDays(10)) - .With(e => e.Quality = new QualityModel(QualityTypes.HDTV720p, false)) - .Build(); - - WithMatchingSeries(); - - //Act - var result = Mocker.Resolve().ProcessSearchResults(MockNotification, parseResults, _matchingSeries, DateTime.Today); - - //Assert - result.Should().NotContain(n => n.Success); - - Mocker.GetMock().Verify(c => c.DownloadReport(It.IsAny()), - Times.Never()); - } - } -} \ No newline at end of file diff --git a/NzbDrone.Core.Test/ProviderTests/SearchProviderTests/SearchFixture.cs b/NzbDrone.Core.Test/ProviderTests/SearchProviderTests/SearchFixture.cs deleted file mode 100644 index 2f23e3ed8..000000000 --- a/NzbDrone.Core.Test/ProviderTests/SearchProviderTests/SearchFixture.cs +++ /dev/null @@ -1,210 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using FizzWare.NBuilder; -using FluentAssertions; -using Moq; -using NUnit.Framework; -using NzbDrone.Core.Model; -using NzbDrone.Core.Providers; -using NzbDrone.Core.Providers.DecisionEngine; -using NzbDrone.Core.Providers.Indexer; -using NzbDrone.Core.Repository; -using NzbDrone.Core.Test.Framework; -using NzbDrone.Test.Common; - -namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests -{ - public class SearchFixture : CoreTest - { - private const string SCENE_NAME = "Scene Name"; - private const int SEASON_NUMBER = 5; - private const int PARSE_RESULT_COUNT = 10; - - private Mock _episodeIndexer1; - private Mock _episodeIndexer2; - private Mock _brokenIndexer; - private Mock _nullIndexer; - - private List _indexers; - - private Series _series; - private IList _episodes; - - [SetUp] - public void Setup() - { - var parseResults = Builder.CreateListOfSize(PARSE_RESULT_COUNT) - .Build(); - - _series = Builder.CreateNew() - .Build(); - - _episodes = Builder.CreateListOfSize(1) - .Build(); - - BuildIndexers(parseResults); - - _indexers = new List { _episodeIndexer1.Object, _episodeIndexer2.Object }; - - Mocker.GetMock() - .Setup(c => c.GetEnabledIndexers()) - .Returns(_indexers); - } - - private void BuildIndexers(IList parseResults) - { - _episodeIndexer1 = new Mock(); - _episodeIndexer1.Setup(c => c.FetchEpisode(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(parseResults); - _episodeIndexer1.Setup(c => c.FetchSeason(It.IsAny(), It.IsAny())) - .Returns(parseResults); - _episodeIndexer1.Setup(c => c.FetchPartialSeason(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(parseResults); - - - _episodeIndexer2 = new Mock(); - _episodeIndexer2.Setup(c => c.FetchEpisode(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(parseResults); - _episodeIndexer2.Setup(c => c.FetchSeason(It.IsAny(), It.IsAny())) - .Returns(parseResults); - _episodeIndexer2.Setup(c => c.FetchPartialSeason(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(parseResults); - - _brokenIndexer = new Mock(); - _brokenIndexer.Setup(c => c.FetchEpisode(It.IsAny(), It.IsAny(), It.IsAny())) - .Throws(new Exception()); - - _nullIndexer = new Mock(); - _nullIndexer.Setup(c => c.FetchEpisode(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns>(null); - } - - private void WithTwoGoodOneBrokenIndexer() - { - _indexers = new List { _episodeIndexer1.Object, _brokenIndexer.Object, _episodeIndexer2.Object }; - - Mocker.GetMock() - .Setup(c => c.GetEnabledIndexers()) - .Returns(_indexers); - } - - private void WithNullIndexers() - { - _indexers = new List { _nullIndexer.Object, _nullIndexer.Object }; - - Mocker.GetMock() - .Setup(c => c.GetEnabledIndexers()) - .Returns(_indexers); - } - - private void WithSceneName() - { - Mocker.GetMock() - .Setup(s => s.GetSceneName(_series.SeriesId)).Returns(SCENE_NAME); - } - - private void With30Episodes() - { - _episodes = Builder.CreateListOfSize(30) - .Build(); - } - - private void WithNullEpisodes() - { - _episodes = null; - } - - private void VerifyFetchEpisode(Times times) - { - _episodeIndexer1.Verify(v => v.FetchEpisode(_series.Title, SEASON_NUMBER, It.IsAny()) - , times); - - _episodeIndexer2.Verify(v => v.FetchEpisode(_series.Title, SEASON_NUMBER, It.IsAny()) - , times); - } - - private void VerifyFetchEpisodeWithSceneName(Times times) - { - _episodeIndexer1.Verify(v => v.FetchEpisode(SCENE_NAME, SEASON_NUMBER, It.IsAny()) - , times); - - _episodeIndexer2.Verify(v => v.FetchEpisode(SCENE_NAME, SEASON_NUMBER, It.IsAny()) - , times); - } - - private void VerifyFetchEpisodeBrokenIndexer(Times times) - { - _brokenIndexer.Verify(v => v.FetchEpisode(It.IsAny(), SEASON_NUMBER, It.IsAny()) - , times); - } - - private void VerifyFetchPartialSeason(Times times) - { - _episodeIndexer1.Verify(v => v.FetchPartialSeason(_series.Title, SEASON_NUMBER, It.IsAny()) - , times); - - _episodeIndexer2.Verify(v => v.FetchPartialSeason(_series.Title, SEASON_NUMBER, It.IsAny()) - , times); - } - - private void VerifyFetchSeason(Times times) - { - _episodeIndexer1.Verify(v => v.FetchSeason(_series.Title, SEASON_NUMBER), times); - _episodeIndexer1.Verify(v => v.FetchSeason(_series.Title, SEASON_NUMBER), times); - } - - [Test] - public void SeasonSearch_should_skip_daily_series() - { - //Setup - _series.IsDaily = true; - - Mocker.GetMock().Setup(s => s.GetSeries(1)).Returns(_series); - - //Act - var result = Mocker.Resolve().SeasonSearch(MockNotification, _series.SeriesId, 1); - - //Assert - result.Should().BeEmpty(); - } - - [Test] - public void PartialSeasonSearch_should_skip_daily_series() - { - //Setup - _series.IsDaily = true; - - Mocker.GetMock().Setup(s => s.GetSeries(1)).Returns(_series); - - //Act - var result = Mocker.Resolve().PartialSeasonSearch(MockNotification, _series.SeriesId, 1); - - //Assert - result.Should().BeEmpty(); - } - - [Test] - public void EpisodeSearch_should_skip_if_air_date_is_null_and_is_a_daily_series() - { - //Setup - _series.IsDaily = true; - var episode = _episodes.First(); - episode.AirDate = null; - episode.Series = _series; - - Mocker.GetMock().Setup(s => s.IsSatisfiedBy(It.IsAny())) - .Returns(true); - - Mocker.GetMock().Setup(s => s.GetEpisode(episode.EpisodeId)) - .Returns(episode); - - //Act - var result = Mocker.Resolve().EpisodeSearch(MockNotification, episode.EpisodeId); - - //Assert - result.Should().BeFalse(); - ExceptionVerification.ExpectedWarns(1); - } - } -} diff --git a/NzbDrone.Core.Test/ProviderTests/SearchTests/DailyEpisodeSearchTests/CheckReportFixture.cs b/NzbDrone.Core.Test/ProviderTests/SearchTests/DailyEpisodeSearchTests/CheckReportFixture.cs new file mode 100644 index 000000000..b94ab791b --- /dev/null +++ b/NzbDrone.Core.Test/ProviderTests/SearchTests/DailyEpisodeSearchTests/CheckReportFixture.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FizzWare.NBuilder; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using NzbDrone.Core.Model; +using NzbDrone.Core.Model.Notification; +using NzbDrone.Core.Providers; +using NzbDrone.Core.Providers.Indexer; +using NzbDrone.Core.Providers.Search; +using NzbDrone.Core.Repository; +using NzbDrone.Core.Repository.Search; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.ProviderTests.SearchTests.DailyEpisodeSearchTests +{ + [TestFixture] + public class CheckReportFixture : TestBase + { + private Series _series; + private Episode _episode; + private EpisodeParseResult _episodeParseResult; + private SearchHistoryItem _searchHistoryItem; + + [SetUp] + public void Setup() + { + _series = Builder + .CreateNew() + .Build(); + + _episode = Builder + .CreateNew() + .With(e => e.SeriesId = _series.SeriesId) + .With(e => e.Series = _series) + .Build(); + + _episodeParseResult = Builder + .CreateNew() + .With(p => p.AirDate = _episode.AirDate) + .With(p => p.Episodes = new List { _episode }) + .With(p => p.Series = _series) + .Build(); + + _searchHistoryItem = new SearchHistoryItem(); + } + + [Test] + public void should_return_WrongEpisode_is_parseResult_doesnt_have_airdate() + { + _episodeParseResult.AirDate = null; + + Mocker.Resolve() + .CheckReport(_series, new {Episode = _episode}, _episodeParseResult, _searchHistoryItem) + .SearchError + .Should() + .Be(ReportRejectionType.WrongEpisode); + } + + [Test] + public void should_return_WrongEpisode_is_parseResult_airdate_doesnt_match_episode() + { + _episodeParseResult.AirDate = _episode.AirDate.Value.AddDays(-10); + + Mocker.Resolve() + .CheckReport(_series, new { Episode = _episode }, _episodeParseResult, _searchHistoryItem) + .SearchError + .Should() + .Be(ReportRejectionType.WrongEpisode); + } + + [Test] + public void should_not_return_error_when_airDates_match() + { + Mocker.Resolve() + .CheckReport(_series, new { Episode = _episode }, _episodeParseResult, _searchHistoryItem) + .SearchError + .Should() + .Be(ReportRejectionType.None); + } + } +} diff --git a/NzbDrone.Core.Test/ProviderTests/SearchTests/DailyEpisodeSearchTests/PerformSearchFixture.cs b/NzbDrone.Core.Test/ProviderTests/SearchTests/DailyEpisodeSearchTests/PerformSearchFixture.cs new file mode 100644 index 000000000..35b51b5ba --- /dev/null +++ b/NzbDrone.Core.Test/ProviderTests/SearchTests/DailyEpisodeSearchTests/PerformSearchFixture.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FizzWare.NBuilder; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using NzbDrone.Core.Model; +using NzbDrone.Core.Model.Notification; +using NzbDrone.Core.Providers; +using NzbDrone.Core.Providers.Indexer; +using NzbDrone.Core.Providers.Search; +using NzbDrone.Core.Repository; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.ProviderTests.SearchTests.DailyEpisodeSearchTests +{ + [TestFixture] + public class PerformSearchFixture : PerformSearchTestBase + { + [Test] + public void should_throw_if_episode_is_null() + { + Episode nullEpisode = null; + Assert.Throws(() => + Mocker.Resolve() + .PerformSearch(_series, new { Episode = nullEpisode }, notification)); + } + + [Test] + public void should_fetch_results_from_indexers() + { + WithValidIndexers(); + + Mocker.Resolve() + .PerformSearch(_series, new {Episode = _episode}, notification) + .Should() + .HaveCount(20); + } + + [Test] + public void should_log_error_when_fetching_from_indexer_fails() + { + WithInvalidIndexers(); + + Mocker.Resolve() + .PerformSearch(_series, new { Episode = _episode }, notification) + .Should() + .HaveCount(0); + + ExceptionVerification.ExpectedErrors(2); + } + } +} diff --git a/NzbDrone.Core.Test/ProviderTests/SearchTests/EpisodeSearchTests/CheckReportFixture.cs b/NzbDrone.Core.Test/ProviderTests/SearchTests/EpisodeSearchTests/CheckReportFixture.cs new file mode 100644 index 000000000..c0d4f2f38 --- /dev/null +++ b/NzbDrone.Core.Test/ProviderTests/SearchTests/EpisodeSearchTests/CheckReportFixture.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FizzWare.NBuilder; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using NzbDrone.Core.Model; +using NzbDrone.Core.Model.Notification; +using NzbDrone.Core.Providers; +using NzbDrone.Core.Providers.Indexer; +using NzbDrone.Core.Providers.Search; +using NzbDrone.Core.Repository; +using NzbDrone.Core.Repository.Search; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.ProviderTests.SearchTests.EpisodeSearchTests +{ + [TestFixture] + public class CheckReportFixture : TestBase + { + private Series _series; + private Episode _episode; + private EpisodeParseResult _episodeParseResult; + private SearchHistoryItem _searchHistoryItem; + + [SetUp] + public void Setup() + { + _series = Builder + .CreateNew() + .Build(); + + _episode = Builder + .CreateNew() + .With(e => e.SeriesId = _series.SeriesId) + .With(e => e.Series = _series) + .Build(); + + _episodeParseResult = Builder + .CreateNew() + .With(p => p.SeasonNumber = 1) + .With(p => p.EpisodeNumbers = new List{ _episode.EpisodeNumber }) + .With(p => p.Episodes = new List { _episode }) + .With(p => p.Series = _series) + .Build(); + + _searchHistoryItem = new SearchHistoryItem(); + } + + [Test] + public void should_return_WrongSeason_when_season_doesnt_match() + { + _episode.SeasonNumber = 10; + + Mocker.Resolve() + .CheckReport(_series, new {Episode = _episode}, _episodeParseResult, _searchHistoryItem) + .SearchError + .Should() + .Be(ReportRejectionType.WrongSeason); + } + + [Test] + public void should_return_WrongEpisode_when_episode_doesnt_match() + { + _episode.EpisodeNumber = 10; + + Mocker.Resolve() + .CheckReport(_series, new { Episode = _episode }, _episodeParseResult, _searchHistoryItem) + .SearchError + .Should() + .Be(ReportRejectionType.WrongEpisode); + } + + [Test] + public void should_not_return_error_when_season_and_episode_match() + { + Mocker.Resolve() + .CheckReport(_series, new { Episode = _episode }, _episodeParseResult, _searchHistoryItem) + .SearchError + .Should() + .Be(ReportRejectionType.None); + } + + [Test] + public void should_return_WrongSeason_when_season_doesnt_match_for_scene_series() + { + _series.UseSceneNumbering = true; + _episode.SceneSeasonNumber = 10; + _episode.SeasonNumber = 10; + _episode.EpisodeNumber = 10; + + Mocker.Resolve() + .CheckReport(_series, new { Episode = _episode }, _episodeParseResult, _searchHistoryItem) + .SearchError + .Should() + .Be(ReportRejectionType.WrongSeason); + } + + [Test] + public void should_return_WrongEpisode_when_episode_doesnt_match_for_scene_series() + { + _series.UseSceneNumbering = true; + _episode.SceneEpisodeNumber = 10; + _episode.SeasonNumber = 10; + _episode.EpisodeNumber = 10; + + Mocker.Resolve() + .CheckReport(_series, new { Episode = _episode }, _episodeParseResult, _searchHistoryItem) + .SearchError + .Should() + .Be(ReportRejectionType.WrongEpisode); + } + + [Test] + public void should_not_return_error_when_season_and_episode_match_for_scene_series() + { + _series.UseSceneNumbering = true; + _episode.SceneSeasonNumber = _episode.SeasonNumber; + _episode.SceneEpisodeNumber = _episode.EpisodeNumber; + _episode.SeasonNumber = 10; + _episode.EpisodeNumber = 10; + + Mocker.Resolve() + .CheckReport(_series, new { Episode = _episode }, _episodeParseResult, _searchHistoryItem) + .SearchError + .Should() + .Be(ReportRejectionType.None); + } + } +} diff --git a/NzbDrone.Core.Test/ProviderTests/SearchTests/EpisodeSearchTests/PerformSearchFixture.cs b/NzbDrone.Core.Test/ProviderTests/SearchTests/EpisodeSearchTests/PerformSearchFixture.cs new file mode 100644 index 000000000..5fa687fab --- /dev/null +++ b/NzbDrone.Core.Test/ProviderTests/SearchTests/EpisodeSearchTests/PerformSearchFixture.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FizzWare.NBuilder; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using NzbDrone.Core.Model; +using NzbDrone.Core.Model.Notification; +using NzbDrone.Core.Providers; +using NzbDrone.Core.Providers.Indexer; +using NzbDrone.Core.Providers.Search; +using NzbDrone.Core.Repository; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.ProviderTests.SearchTests.EpisodeSearchTests +{ + [TestFixture] + public class PerformSearchFixture : PerformSearchTestBase + { + [Test] + public void should_throw_if_episode_is_null() + { + Episode nullEpisode = null; + Assert.Throws(() => + Mocker.Resolve() + .PerformSearch(_series, new { Episode = nullEpisode }, notification)); + } + + [Test] + public void should_fetch_results_from_indexers() + { + WithValidIndexers(); + + Mocker.Resolve() + .PerformSearch(_series, new {Episode = _episode}, notification) + .Should() + .HaveCount(20); + } + + [Test] + public void should_log_error_when_fetching_from_indexer_fails() + { + WithInvalidIndexers(); + + Mocker.Resolve() + .PerformSearch(_series, new { Episode = _episode }, notification) + .Should() + .HaveCount(0); + + ExceptionVerification.ExpectedErrors(2); + } + + [Test] + public void should_use_scene_numbering_when_available() + { + _series.UseSceneNumbering = true; + _episode.SceneEpisodeNumber = 5; + _episode.SceneSeasonNumber = 10; + + WithValidIndexers(); + + Mocker.Resolve() + .PerformSearch(_series, new { Episode = _episode }, notification) + .Should() + .HaveCount(20); + + _indexer1.Verify(v => v.FetchEpisode(_series.Title, 10, 5), Times.Once()); + _indexer2.Verify(v => v.FetchEpisode(_series.Title, 10, 5), Times.Once()); + } + + [Test] + public void should_use_standard_numbering_when_scene_series_set_but_info_is_not_available() + { + _series.UseSceneNumbering = true; + _episode.SceneEpisodeNumber = 0; + _episode.SceneSeasonNumber = 0; + + WithValidIndexers(); + + Mocker.Resolve() + .PerformSearch(_series, new { Episode = _episode }, notification) + .Should() + .HaveCount(20); + + _indexer1.Verify(v => v.FetchEpisode(_series.Title, _episode.SeasonNumber, _episode.EpisodeNumber), Times.Once()); + _indexer2.Verify(v => v.FetchEpisode(_series.Title, _episode.SeasonNumber, _episode.EpisodeNumber), Times.Once()); + } + + [Test] + public void should_use_standard_numbering_when_not_scene_series() + { + _series.UseSceneNumbering = false; + + WithValidIndexers(); + + Mocker.Resolve() + .PerformSearch(_series, new { Episode = _episode }, notification) + .Should() + .HaveCount(20); + + _indexer1.Verify(v => v.FetchEpisode(_series.Title, _episode.SeasonNumber, _episode.EpisodeNumber), Times.Once()); + _indexer2.Verify(v => v.FetchEpisode(_series.Title, _episode.SeasonNumber, _episode.EpisodeNumber), Times.Once()); + } + } +} diff --git a/NzbDrone.Core.Test/ProviderTests/SearchTests/GetSearchTitleFixture.cs b/NzbDrone.Core.Test/ProviderTests/SearchTests/GetSearchTitleFixture.cs new file mode 100644 index 000000000..245d8357d --- /dev/null +++ b/NzbDrone.Core.Test/ProviderTests/SearchTests/GetSearchTitleFixture.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Providers; +using NzbDrone.Core.Providers.Search; +using NzbDrone.Core.Repository; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.ProviderTests.SearchTests +{ + public class GetSearchTitleFixture : TestBase + { + private Series _series; + + [SetUp] + public void Setup() + { + _series = Builder + .CreateNew() + .With(s => s.Title = "Hawaii Five-0") + .Build(); + } + + private void WithSceneMapping() + { + Mocker.GetMock() + .Setup(s => s.GetSceneName(_series.SeriesId)) + .Returns("Hawaii Five 0 2010"); + } + + [Test] + public void should_return_series_title_when_there_is_no_scene_mapping() + { + Mocker.Resolve().GetSearchTitle(_series, 5) + .Should().Be(_series.Title); + } + + [Test] + public void should_return_scene_mapping_when_one_exists() + { + WithSceneMapping(); + + Mocker.Resolve().GetSearchTitle(_series, 5) + .Should().Be("Hawaii Five 0 2010"); + } + + [Test] + public void should_replace_ampersand_with_and() + { + _series.Title = "Franklin & Bash"; + + Mocker.Resolve().GetSearchTitle(_series, 5) + .Should().Be("Franklin and Bash"); + } + } +} diff --git a/NzbDrone.Core.Test/ProviderTests/SearchTests/PartialSeasonSearchTests/CheckReportFixture.cs b/NzbDrone.Core.Test/ProviderTests/SearchTests/PartialSeasonSearchTests/CheckReportFixture.cs new file mode 100644 index 000000000..fd2f3062e --- /dev/null +++ b/NzbDrone.Core.Test/ProviderTests/SearchTests/PartialSeasonSearchTests/CheckReportFixture.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FizzWare.NBuilder; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using NzbDrone.Core.Model; +using NzbDrone.Core.Model.Notification; +using NzbDrone.Core.Providers; +using NzbDrone.Core.Providers.Indexer; +using NzbDrone.Core.Providers.Search; +using NzbDrone.Core.Repository; +using NzbDrone.Core.Repository.Search; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.ProviderTests.SearchTests.PartialSeasonSearchTests +{ + [TestFixture] + public class CheckReportFixture : TestBase + { + private Series _series; + private List _episodes; + private EpisodeParseResult _episodeParseResult; + private SearchHistoryItem _searchHistoryItem; + + [SetUp] + public void Setup() + { + _series = Builder + .CreateNew() + .Build(); + + _episodes = Builder + .CreateListOfSize(10) + .All() + .With(e => e.SeriesId = _series.SeriesId) + .With(e => e.Series = _series) + .Build() + .ToList(); + + _episodeParseResult = Builder + .CreateNew() + .With(p => p.SeasonNumber = 1) + .Build(); + + _searchHistoryItem = new SearchHistoryItem(); + } + + [Test] + public void should_return_wrongSeason_when_season_does_not_match() + { + Mocker.Resolve() + .CheckReport(_series, new { SeasonNumber = 2, Episodes = _episodes }, _episodeParseResult, _searchHistoryItem) + .SearchError.Should().Be(ReportRejectionType.WrongSeason); + } + + [Test] + public void should_not_return_error_when_season_matches() + { + Mocker.Resolve() + .CheckReport(_series, new { SeasonNumber = 1, Episodes = _episodes }, _episodeParseResult, _searchHistoryItem) + .SearchError.Should().Be(ReportRejectionType.None); + } + } +} diff --git a/NzbDrone.Core.Test/ProviderTests/SearchTests/PartialSeasonSearchTests/PerformSearchFixture.cs b/NzbDrone.Core.Test/ProviderTests/SearchTests/PartialSeasonSearchTests/PerformSearchFixture.cs new file mode 100644 index 000000000..6855113d5 --- /dev/null +++ b/NzbDrone.Core.Test/ProviderTests/SearchTests/PartialSeasonSearchTests/PerformSearchFixture.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FizzWare.NBuilder; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using NzbDrone.Core.Model; +using NzbDrone.Core.Model.Notification; +using NzbDrone.Core.Providers; +using NzbDrone.Core.Providers.Indexer; +using NzbDrone.Core.Providers.Search; +using NzbDrone.Core.Repository; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.ProviderTests.SearchTests.PartialSeasonSearchTests +{ + [TestFixture] + public class PerformSearchFixture : PerformSearchTestBase + { + [Test] + public void should_throw_if_season_number_is_less_than_zero() + { + Assert.Throws(() => + Mocker.Resolve() + .PerformSearch(_series, new + { + SeasonNumber = -1, + Episodes = new List{ new Episode() } + }, notification)); + } + + [Test] + public void should_throw_if_episodes_is_empty() + { + Assert.Throws(() => + Mocker.Resolve() + .PerformSearch(_series, new { SeasonNumber = 10, Episodes = new List() }, notification)); + } + + [Test] + public void should_fetch_results_from_indexers() + { + WithValidIndexers(); + + Mocker.Resolve() + .PerformSearch(_series, new { SeasonNumber = 1, Episodes = _episodes }, notification) + .Should() + .HaveCount(40); + } + + [Test] + public void should_log_error_when_fetching_from_indexer_fails() + { + WithInvalidIndexers(); + + Mocker.Resolve() + .PerformSearch(_series, new { SeasonNumber = 1, Episodes = _episodes }, notification) + .Should() + .HaveCount(0); + + ExceptionVerification.ExpectedErrors(4); + } + + [Test] + public void should_hit_each_indexer_once_for_each_prefix() + { + WithValidIndexers(); + + Mocker.Resolve() + .PerformSearch(_series, new { SeasonNumber = 1, Episodes = _episodes }, notification) + .Should() + .HaveCount(40); + + _indexer1.Verify(v => v.FetchPartialSeason(_series.Title, 1, 0), Times.Once()); + _indexer1.Verify(v => v.FetchPartialSeason(_series.Title, 1, 1), Times.Once()); + _indexer2.Verify(v => v.FetchPartialSeason(_series.Title, 1, 0), Times.Once()); + _indexer2.Verify(v => v.FetchPartialSeason(_series.Title, 1, 1), Times.Once()); + } + } +} diff --git a/NzbDrone.Core.Test/ProviderTests/SearchTests/PerformSearchTestBase.cs b/NzbDrone.Core.Test/ProviderTests/SearchTests/PerformSearchTestBase.cs new file mode 100644 index 000000000..b06a65090 --- /dev/null +++ b/NzbDrone.Core.Test/ProviderTests/SearchTests/PerformSearchTestBase.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using FizzWare.NBuilder; +using Moq; +using NUnit.Framework; +using NzbDrone.Core.Model; +using NzbDrone.Core.Model.Notification; +using NzbDrone.Core.Providers; +using NzbDrone.Core.Providers.Indexer; +using NzbDrone.Core.Repository; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.ProviderTests.SearchTests +{ + public class PerformSearchTestBase : TestBase + { + protected Series _series; + protected Episode _episode; + protected List _episodes; + protected ProgressNotification notification = new ProgressNotification("Testing"); + + protected Mock _indexer1; + protected Mock _indexer2; + protected List _indexers; + protected IList _parseResults; + + [SetUp] + public void Setup() + { + _series = Builder + .CreateNew() + .Build(); + + _episode = Builder + .CreateNew() + .With(e => e.SeriesId = _series.SeriesId) + .With(e => e.Series = _series) + .Build(); + + _episodes = Builder + .CreateListOfSize(10) + .All() + .With(e => e.SeriesId = _series.SeriesId) + .With(e => e.Series = _series) + .Build() + .ToList(); + + _parseResults = Builder + .CreateListOfSize(10) + .Build(); + + _indexer1 = new Mock(); + _indexer2 = new Mock(); + _indexers = new List { _indexer1.Object, _indexer2.Object }; + + Mocker.GetMock() + .Setup(c => c.GetEnabledIndexers()) + .Returns(_indexers); + } + + protected void WithValidIndexers() + { + _indexer1.Setup(c => c.FetchEpisode(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_parseResults); + _indexer1.Setup(c => c.FetchDailyEpisode(It.IsAny(), It.IsAny())) + .Returns(_parseResults); + _indexer1.Setup(c => c.FetchPartialSeason(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_parseResults); + + _indexer2.Setup(c => c.FetchEpisode(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_parseResults); + _indexer2.Setup(c => c.FetchDailyEpisode(It.IsAny(), It.IsAny())) + .Returns(_parseResults); + _indexer2.Setup(c => c.FetchPartialSeason(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_parseResults); + } + + protected void WithInvalidIndexers() + { + _indexer1.Setup(c => c.FetchEpisode(It.IsAny(), It.IsAny(), It.IsAny())) + .Throws(new Exception()); + _indexer1.Setup(c => c.FetchDailyEpisode(It.IsAny(), It.IsAny())) + .Throws(new Exception()); + _indexer1.Setup(c => c.FetchPartialSeason(It.IsAny(), It.IsAny(), It.IsAny())) + .Throws(new Exception()); + + _indexer2.Setup(c => c.FetchEpisode(It.IsAny(), It.IsAny(), It.IsAny())) + .Throws(new Exception()); + _indexer2.Setup(c => c.FetchDailyEpisode(It.IsAny(), It.IsAny())) + .Throws(new Exception()); + _indexer2.Setup(c => c.FetchPartialSeason(It.IsAny(), It.IsAny(), It.IsAny())) + .Throws(new Exception()); + + _indexer1.SetupGet(c => c.Name).Returns("Indexer1"); + _indexer1.SetupGet(c => c.Name).Returns("Indexer2"); + } + } +} diff --git a/NzbDrone.Core.Test/ProviderTests/SearchProviderTests/ProcessSearchResultsFixture.cs b/NzbDrone.Core.Test/ProviderTests/SearchTests/ProcessResultsFixture.cs similarity index 63% rename from NzbDrone.Core.Test/ProviderTests/SearchProviderTests/ProcessSearchResultsFixture.cs rename to NzbDrone.Core.Test/ProviderTests/SearchTests/ProcessResultsFixture.cs index 52c74cb77..246a78c8e 100644 --- a/NzbDrone.Core.Test/ProviderTests/SearchProviderTests/ProcessSearchResultsFixture.cs +++ b/NzbDrone.Core.Test/ProviderTests/SearchTests/ProcessResultsFixture.cs @@ -1,6 +1,7 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; - +using System.Text; using FizzWare.NBuilder; using FluentAssertions; using Moq; @@ -9,23 +10,28 @@ using NzbDrone.Core.Model; using NzbDrone.Core.Model.Notification; using NzbDrone.Core.Providers; using NzbDrone.Core.Providers.DecisionEngine; +using NzbDrone.Core.Providers.Search; using NzbDrone.Core.Repository; using NzbDrone.Core.Repository.Quality; using NzbDrone.Core.Repository.Search; -using NzbDrone.Core.Test.Framework; +using NzbDrone.Test.Common; -namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests +namespace NzbDrone.Core.Test.ProviderTests.SearchTests { [TestFixture] - // ReSharper disable InconsistentNaming - public class ProcessSearchResultsFixture : CoreTest + public class ProcessResultsFixture : TestBase { - private Series _matchingSeries = null; - private Series _mismatchedSeries = null; + private Series _matchingSeries; + private Series _mismatchedSeries; private Series _nullSeries = null; + private SearchHistory _searchHistory; + private ProgressNotification _notification; + + private IList _episodes; + [SetUp] - public void setup() + public void Setup() { _matchingSeries = Builder.CreateNew() .With(s => s.SeriesId = 79488) @@ -36,6 +42,17 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests .With(s => s.SeriesId = 12345) .With(s => s.Title = "Not 30 Rock") .Build(); + + _searchHistory = new SearchHistory(); + _notification = new ProgressNotification("Test"); + + _episodes = Builder + .CreateListOfSize(1) + .Build(); + + Mocker.GetMock() + .Setup(s => s.GetEpisodesByParseResult(It.IsAny())) + .Returns(_episodes); } private void WithMatchingSeries() @@ -85,8 +102,11 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests } [Test] - public void processSearchResults_higher_quality_should_be_called_first() + public void should_process_higher_quality_results_first() { + WithMatchingSeries(); + WithSuccessfulDownload(); + var parseResults = Builder.CreateListOfSize(5) .All() .With(e => e.SeasonNumber = 1) @@ -96,21 +116,18 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests .Random(1) .With(c => c.Quality = new QualityModel(QualityTypes.Bluray1080p, true)) .With(c => c.Age = 100) - .Build(); + .Build() + .ToList(); - WithMatchingSeries(); - WithSuccessfulDownload(); Mocker.GetMock() .Setup(s => s.IsSatisfiedBy(It.Is(d => d.Quality.Quality == QualityTypes.Bluray1080p))) .Returns(ReportRejectionType.None); - //Act - var result = Mocker.Resolve().ProcessSearchResults(new ProgressNotification("Test"), parseResults, new SearchHistory(), _matchingSeries, 1, 1); + var result = Mocker.Resolve().ProcessReports(_matchingSeries, new { }, parseResults, _searchHistory, _notification); - //Assert - result.Should().HaveCount(parseResults.Count); - result.Should().Contain(s => s.Success); + result.SearchHistoryItems.Should().HaveCount(parseResults.Count); + result.SearchHistoryItems.Should().Contain(s => s.Success); Mocker.GetMock().Verify(c => c.IsSatisfiedBy(It.IsAny()), Times.Once()); @@ -119,31 +136,29 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests } [Test] - public void processSearchResults_newer_report_should_be_called_first() + public void should_process_newer_reports_first() { + WithMatchingSeries(); + WithSuccessfulDownload(); + var parseResults = Builder.CreateListOfSize(5) .All() .With(e => e.SeasonNumber = 1) .With(e => e.EpisodeNumbers = new List { 1 }) .With(c => c.Quality = new QualityModel(QualityTypes.Bluray1080p, true)) .With(c => c.Age = 300) - .Build(); + .Build() + .ToList(); parseResults[2].Age = 100; - - - WithMatchingSeries(); - WithSuccessfulDownload(); Mocker.GetMock() .Setup(s => s.IsSatisfiedBy(It.IsAny())).Returns(ReportRejectionType.None); - //Act - var result = Mocker.Resolve().ProcessSearchResults(MockNotification, parseResults, new SearchHistory(), _matchingSeries, 1, 1); + var result = Mocker.Resolve().ProcessReports(_matchingSeries, new { }, parseResults, _searchHistory, _notification); - //Assert - result.Should().HaveCount(parseResults.Count); - result.Should().Contain(s => s.Success); + result.SearchHistoryItems.Should().HaveCount(parseResults.Count); + result.SearchHistoryItems.Should().Contain(s => s.Success); Mocker.GetMock().Verify(c => c.DownloadReport(It.Is(d => d.Age != 100)), Times.Never()); @@ -151,24 +166,23 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests } [Test] - public void processSearchResults_when_quality_is_not_needed_should_check_the_rest() + public void should_check_other_reports_when_quality_is_not_wanted() { + WithMatchingSeries(); + WithQualityNotNeeded(); + var parseResults = Builder.CreateListOfSize(5) .All() .With(e => e.SeasonNumber = 1) .With(e => e.EpisodeNumbers = new List { 1 }) .With(c => c.Quality = new QualityModel(QualityTypes.DVD, true)) - .Build(); - - WithMatchingSeries(); - WithQualityNotNeeded(); + .Build() + .ToList(); - //Act - var result = Mocker.Resolve().ProcessSearchResults(new ProgressNotification("Test"), parseResults, new SearchHistory(), _matchingSeries, 1, 1); + var result = Mocker.Resolve().ProcessReports(_matchingSeries, new { }, parseResults, _searchHistory, _notification); - //Assert - result.Should().HaveCount(parseResults.Count); - result.Should().NotContain(s => s.Success); + result.SearchHistoryItems.Should().HaveCount(parseResults.Count); + result.SearchHistoryItems.Should().NotContain(s => s.Success); Mocker.GetMock().Verify(c => c.IsSatisfiedBy(It.IsAny()), Times.Exactly(5)); @@ -177,99 +191,55 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests } [Test] - public void processSearchResults_should_skip_if_series_is_null() + public void should_should_skip_if_series_is_not_watched() { var parseResults = Builder.CreateListOfSize(5) .All() .With(e => e.SeasonNumber = 1) .With(e => e.EpisodeNumbers = new List { 1 }) .With(e => e.Quality = new QualityModel(QualityTypes.HDTV720p, false)) - .Build(); + .Build() + .ToList(); WithNullSeries(); //Act - var result = Mocker.Resolve().ProcessSearchResults(new ProgressNotification("Test"), parseResults, new SearchHistory(), _matchingSeries, 1, 1); + var result = Mocker.Resolve().ProcessReports(_matchingSeries, new { }, parseResults, _searchHistory, _notification); //Assert - result.Should().HaveCount(parseResults.Count); - result.Should().NotContain(s => s.Success); + result.SearchHistoryItems.Should().HaveCount(parseResults.Count); + result.SearchHistoryItems.Should().NotContain(s => s.Success); Mocker.GetMock().Verify(c => c.DownloadReport(It.IsAny()), Times.Never()); } [Test] - public void processSearchResults_should_skip_if_series_is_mismatched() + public void should_skip_if_series_does_not_match_searched_series() { var parseResults = Builder.CreateListOfSize(5) .All() .With(e => e.SeasonNumber = 1) .With(e => e.EpisodeNumbers = new List { 1 }) .With(e => e.Quality = new QualityModel(QualityTypes.HDTV720p, false)) - .Build(); + .Build() + .ToList(); WithMisMatchedSeries(); //Act - var result = Mocker.Resolve().ProcessSearchResults(new ProgressNotification("Test"), parseResults, new SearchHistory(), _matchingSeries, 1, 1); - - //Assert - result.Should().HaveCount(parseResults.Count); - result.Should().NotContain(s => s.Success); - - Mocker.GetMock().Verify(c => c.DownloadReport(It.IsAny()), - Times.Never()); - } - - [Test] - public void processSearchResults_should_skip_if_season_doesnt_match() - { - var parseResults = Builder.CreateListOfSize(5) - .All() - .With(e => e.SeasonNumber = 2) - .With(e => e.EpisodeNumbers = new List { 1 }) - .With(e => e.Quality = new QualityModel(QualityTypes.HDTV720p, false)) - .Build(); - - WithMatchingSeries(); - - //Act - var result = Mocker.Resolve().ProcessSearchResults(new ProgressNotification("Test"), parseResults, new SearchHistory(), _matchingSeries, 1, 1); - - //Assert - result.Should().HaveCount(parseResults.Count); - result.Should().NotContain(s => s.Success); - - Mocker.GetMock().Verify(c => c.DownloadReport(It.IsAny()), - Times.Never()); - } - - [Test] - public void processSearchResults_should_skip_if_episodeNumber_doesnt_match() - { - var parseResults = Builder.CreateListOfSize(5) - .All() - .With(e => e.SeasonNumber = 1) - .With(e => e.EpisodeNumbers = new List { 2 }) - .With(e => e.Quality = new QualityModel(QualityTypes.HDTV720p, false)) - .Build(); - - WithMatchingSeries(); - - //Act - var result = Mocker.Resolve().ProcessSearchResults(new ProgressNotification("Test"), parseResults, new SearchHistory(), _matchingSeries, 1, 1); + var result = Mocker.Resolve().ProcessReports(_matchingSeries, new { }, parseResults, _searchHistory, _notification); //Assert - result.Should().HaveCount(parseResults.Count); - result.Should().NotContain(s => s.Success); + result.SearchHistoryItems.Should().HaveCount(parseResults.Count); + result.SearchHistoryItems.Should().NotContain(s => s.Success); Mocker.GetMock().Verify(c => c.DownloadReport(It.IsAny()), Times.Never()); } [Test] - public void processSearchResults_should_skip_if_any_episodeNumber_was_already_added_to_download_queue() + public void should_skip_if_episode_was_already_downloaded() { var parseResults = Builder.CreateListOfSize(2) .All() @@ -278,25 +248,26 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests .With(c => c.Quality = new QualityModel(QualityTypes.DVD, true)) .TheLast(1) .With(e => e.EpisodeNumbers = new List { 1, 2, 3, 4, 5 }) - .Build(); + .Build() + .ToList(); WithMatchingSeries(); WithQualityNeeded(); WithSuccessfulDownload(); //Act - var result = Mocker.Resolve().ProcessSearchResults(new ProgressNotification("Test"), parseResults, new SearchHistory(), _matchingSeries, 1); + var result = Mocker.Resolve().ProcessReports(_matchingSeries, new { }, parseResults, _searchHistory, _notification); //Assert - result.Should().HaveCount(parseResults.Count); - result.Should().Contain(s => s.Success); + result.SearchHistoryItems.Should().HaveCount(parseResults.Count); + result.SearchHistoryItems.Should().Contain(s => s.Success); Mocker.GetMock().Verify(c => c.DownloadReport(It.IsAny()), Times.Once()); } [Test] - public void processSearchResults_should_try_next_if_download_fails() + public void should_try_next_report_if_download_fails() { var parseResults = Builder.CreateListOfSize(2) .All() @@ -305,7 +276,8 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests .With(c => c.Quality = new QualityModel(QualityTypes.DVD, true)) .TheLast(1) .With(c => c.Quality = new QualityModel(QualityTypes.SDTV, true)) - .Build(); + .Build() + .ToList(); WithMatchingSeries(); WithQualityNeeded(); @@ -319,18 +291,18 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests .Returns(true); //Act - var result = Mocker.Resolve().ProcessSearchResults(new ProgressNotification("Test"), parseResults, new SearchHistory(), _matchingSeries, 1); + var result = Mocker.Resolve().ProcessReports(_matchingSeries, new { }, parseResults, _searchHistory, _notification); //Assert - result.Should().HaveCount(parseResults.Count); - result.Should().Contain(s => s.Success); + result.SearchHistoryItems.Should().HaveCount(parseResults.Count); + result.SearchHistoryItems.Should().Contain(s => s.Success); Mocker.GetMock().Verify(c => c.DownloadReport(It.IsAny()), Times.Exactly(2)); } [Test] - public void processSearchResults_Successes_should_not_be_null_or_empty() + public void should_return_valid_successes_when_one_or_more_downloaded() { var parseResults = Builder.CreateListOfSize(5) .All() @@ -341,9 +313,8 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests .Random(1) .With(c => c.Quality = new QualityModel(QualityTypes.Bluray1080p, true)) .With(c => c.Age = 100) - .Build(); - - var searchHistory = new SearchHistory(); + .Build() + .ToList(); WithMatchingSeries(); WithSuccessfulDownload(); @@ -353,11 +324,11 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests .Returns(ReportRejectionType.None); //Act - var result = Mocker.Resolve().ProcessSearchResults(new ProgressNotification("Test"), parseResults, searchHistory, _matchingSeries, 1, 1); + var result = Mocker.Resolve().ProcessReports(_matchingSeries, new { }, parseResults, _searchHistory, _notification); //Assert - searchHistory.Successes.Should().NotBeNull(); - searchHistory.Successes.Should().NotBeEmpty(); + result.Successes.Should().NotBeNull(); + result.Successes.Should().NotBeEmpty(); Mocker.GetMock().Verify(c => c.IsSatisfiedBy(It.IsAny()), Times.Once()); @@ -365,4 +336,4 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests Times.Once()); } } -} \ No newline at end of file +} diff --git a/NzbDrone.Core.Test/ProviderTests/SearchTests/TestSearch.cs b/NzbDrone.Core.Test/ProviderTests/SearchTests/TestSearch.cs new file mode 100644 index 000000000..e7aa7b19f --- /dev/null +++ b/NzbDrone.Core.Test/ProviderTests/SearchTests/TestSearch.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using NLog; +using NzbDrone.Core.Model; +using NzbDrone.Core.Providers; +using NzbDrone.Core.Providers.DecisionEngine; +using NzbDrone.Core.Providers.Search; +using NzbDrone.Core.Repository.Search; + +namespace NzbDrone.Core.Test.ProviderTests.SearchTests +{ + public class TestSearch : SearchBase + { + private static readonly Logger logger = LogManager.GetCurrentClassLogger(); + + public TestSearch(SeriesProvider seriesProvider, EpisodeProvider episodeProvider, DownloadProvider downloadProvider, + IndexerProvider indexerProvider, SceneMappingProvider sceneMappingProvider, + AllowedDownloadSpecification allowedDownloadSpecification, SearchHistoryProvider searchHistoryProvider) + : base(seriesProvider, episodeProvider, downloadProvider, indexerProvider, sceneMappingProvider, + allowedDownloadSpecification, searchHistoryProvider) + { + } + + public override List PerformSearch(Repository.Series series, dynamic options, Model.Notification.ProgressNotification notification) + { + if (options.Episode == null) + throw new ArgumentException("Episode is invalid"); + + notification.CurrentMessage = "Looking for " + options.Episode; + + var reports = new List(); + var title = GetSearchTitle(series); + + var seasonNumber = options.Episode.SeasonNumber; + var episodeNumber = options.Episode.EpisodeNumber; + + Parallel.ForEach(_indexerProvider.GetEnabledIndexers(), indexer => + { + try + { + reports.AddRange(indexer.FetchEpisode(title, seasonNumber, episodeNumber)); + } + + catch (Exception e) + { + logger.ErrorException(String.Format("An error has occurred while searching for {0}-S{1:00}E{2:00} from: {3}", + series.Title, options.SeasonNumber, options.EpisodeNumber, indexer.Name), e); + } + }); + + return reports; + } + + public override SearchHistoryItem CheckReport(Repository.Series series, dynamic options, EpisodeParseResult episodeParseResult, Repository.Search.SearchHistoryItem item) + { + return item; + } + + protected override void FinalizeSearch(Repository.Series series, dynamic options, bool reportsFound, Model.Notification.ProgressNotification notification) + { + logger.Warn("Unable to find {0} in any of indexers.", series.Title); + } + } +} diff --git a/NzbDrone.Core/Jobs/EpisodeSearchJob.cs b/NzbDrone.Core/Jobs/EpisodeSearchJob.cs index 8a28b2edf..5bb25d927 100644 --- a/NzbDrone.Core/Jobs/EpisodeSearchJob.cs +++ b/NzbDrone.Core/Jobs/EpisodeSearchJob.cs @@ -1,17 +1,30 @@ using System.Linq; using System; +using NLog; using NzbDrone.Core.Model.Notification; using NzbDrone.Core.Providers; +using NzbDrone.Core.Providers.DecisionEngine; +using NzbDrone.Core.Providers.Search; namespace NzbDrone.Core.Jobs { public class EpisodeSearchJob : IJob { - private readonly SearchProvider _searchProvider; + private readonly EpisodeProvider _episodeProvider; + private readonly UpgradePossibleSpecification _upgradePossibleSpecification; + private readonly EpisodeSearch _episodeSearch; + private readonly DailyEpisodeSearch _dailyEpisodeSearch; - public EpisodeSearchJob(SearchProvider searchProvider) + private static readonly Logger logger = LogManager.GetCurrentClassLogger(); + + public EpisodeSearchJob(EpisodeProvider episodeProvider, UpgradePossibleSpecification upgradePossibleSpecification, + EpisodeSearch episodeSearch, DailyEpisodeSearch dailyEpisodeSearch) { - _searchProvider = searchProvider; + if(dailyEpisodeSearch == null) throw new ArgumentNullException("dailyEpisodeSearch"); + _episodeProvider = episodeProvider; + _upgradePossibleSpecification = upgradePossibleSpecification; + _episodeSearch = episodeSearch; + _dailyEpisodeSearch = dailyEpisodeSearch; } public EpisodeSearchJob() @@ -34,7 +47,37 @@ namespace NzbDrone.Core.Jobs if (options == null || options.EpisodeId <= 0) throw new ArgumentException("options"); - _searchProvider.EpisodeSearch(notification, options.EpisodeId); + var episode = _episodeProvider.GetEpisode(options.EpisodeId); + + if (episode == null) + { + logger.Error("Unable to find an episode {0} in database", options.EpisodeId); + return; + } + + if (!_upgradePossibleSpecification.IsSatisfiedBy(episode)) + { + logger.Info("Search for {0} was aborted, file in disk meets or exceeds Profile's Cutoff", episode); + notification.CurrentMessage = String.Format("Skipping search for {0}, the file you have is already at cutoff", episode); + return; + } + + if (episode.Series.IsDaily) + { + if (!episode.AirDate.HasValue) + { + logger.Warn("AirDate is not Valid for: {0}", episode); + notification.CurrentMessage = String.Format("Search for {0} Failed, AirDate is invalid", episode); + return; + } + + _dailyEpisodeSearch.Search(episode.Series, new { Episode = episode }, notification); + } + + else + { + _episodeSearch.Search(episode.Series, new { Episode = episode }, notification); + } } } } \ No newline at end of file diff --git a/NzbDrone.Core/Jobs/SeasonSearchJob.cs b/NzbDrone.Core/Jobs/SeasonSearchJob.cs index 42e7391c1..e93c40b03 100644 --- a/NzbDrone.Core/Jobs/SeasonSearchJob.cs +++ b/NzbDrone.Core/Jobs/SeasonSearchJob.cs @@ -67,9 +67,9 @@ namespace NzbDrone.Core.Jobs if (episodes.Count == successes.Count) return; - var missingEpisodes = episodes.Select(e => e.EpisodeNumber).Except(successes).ToList(); + var missingEpisodes = episodes.Select(e => e.EpisodeId).Except(successes).ToList(); - foreach (var episode in episodes.Where(e => !e.Ignored && missingEpisodes.Contains(e.EpisodeNumber)).OrderBy(o => o.EpisodeNumber)) + foreach (var episode in episodes.Where(e => !e.Ignored && missingEpisodes.Contains(e.EpisodeId)).OrderBy(o => o.EpisodeNumber)) { _episodeSearchJob.Start(notification, new { EpisodeId = episode.EpisodeId }); } diff --git a/NzbDrone.Core/NzbDrone.Core.csproj b/NzbDrone.Core/NzbDrone.Core.csproj index 42139a6f8..5776cedf0 100644 --- a/NzbDrone.Core/NzbDrone.Core.csproj +++ b/NzbDrone.Core/NzbDrone.Core.csproj @@ -313,6 +313,7 @@ + @@ -328,6 +329,9 @@ + + + diff --git a/NzbDrone.Core/Providers/Search/DailyEpisodeSearch.cs b/NzbDrone.Core/Providers/Search/DailyEpisodeSearch.cs new file mode 100644 index 000000000..a82f3f252 --- /dev/null +++ b/NzbDrone.Core/Providers/Search/DailyEpisodeSearch.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using NLog; +using NzbDrone.Core.Model; +using NzbDrone.Core.Model.Notification; +using NzbDrone.Core.Providers.DecisionEngine; +using NzbDrone.Core.Repository; +using NzbDrone.Core.Repository.Search; + +namespace NzbDrone.Core.Providers.Search +{ + public class DailyEpisodeSearch : SearchBase + { + private static readonly Logger logger = LogManager.GetCurrentClassLogger(); + + public DailyEpisodeSearch(SeriesProvider seriesProvider, EpisodeProvider episodeProvider, DownloadProvider downloadProvider, IndexerProvider indexerProvider, + SceneMappingProvider sceneMappingProvider, AllowedDownloadSpecification allowedDownloadSpecification, + SearchHistoryProvider searchHistoryProvider) + : base(seriesProvider, episodeProvider, downloadProvider, indexerProvider, sceneMappingProvider, + allowedDownloadSpecification, searchHistoryProvider) + { + } + + public override List PerformSearch(Series series, dynamic options, ProgressNotification notification) + { + if (options.Episode == null) + throw new ArgumentException("Episode is invalid"); + + notification.CurrentMessage = "Looking for " + options.Episode; + + var reports = new List(); + var title = GetSearchTitle(series); + + Parallel.ForEach(_indexerProvider.GetEnabledIndexers(), indexer => + { + try + { + reports.AddRange(indexer.FetchDailyEpisode(title, options.Episode.AirDate)); + } + + catch (Exception e) + { + logger.ErrorException(String.Format("An error has occurred while searching for {0} - {1:yyyy-MM-dd} from: {2}", + series.Title, options.Episode.AirDate, indexer.Name), e); + } + }); + + return reports; + } + + public override SearchHistoryItem CheckReport(Series series, dynamic options, EpisodeParseResult episodeParseResult, + SearchHistoryItem item) + { + Episode episode = options.Episode; + + if (!episodeParseResult.AirDate.HasValue || episodeParseResult.AirDate.Value != episode.AirDate.Value) + { + logger.Trace("Episode AirDate does not match searched episode number, skipping."); + item.SearchError = ReportRejectionType.WrongEpisode; + + return item; + } + + return item; + } + + protected override void FinalizeSearch(Series series, dynamic options, Boolean reportsFound, ProgressNotification notification) + { + logger.Warn("Unable to find {0} in any of indexers.", options.Episode); + + notification.CurrentMessage = reportsFound ? String.Format("Sorry, couldn't find {0}, that matches your preferences.", options.Episode) + : String.Format("Sorry, couldn't find {0} in any of indexers.", options.Episode); + } + } +} \ No newline at end of file diff --git a/NzbDrone.Core/Providers/Search/EpisodeSearch.cs b/NzbDrone.Core/Providers/Search/EpisodeSearch.cs index e1e856b84..5d2988841 100644 --- a/NzbDrone.Core/Providers/Search/EpisodeSearch.cs +++ b/NzbDrone.Core/Providers/Search/EpisodeSearch.cs @@ -5,6 +5,7 @@ using System.Text; using System.Threading.Tasks; using NLog; using NzbDrone.Core.Model; +using NzbDrone.Core.Model.Notification; using NzbDrone.Core.Providers.DecisionEngine; using NzbDrone.Core.Repository; using NzbDrone.Core.Repository.Search; @@ -15,50 +16,106 @@ namespace NzbDrone.Core.Providers.Search { private static readonly Logger logger = LogManager.GetCurrentClassLogger(); - public EpisodeSearch(EpisodeProvider episodeProvider, DownloadProvider downloadProvider, - SeriesProvider seriesProvider, IndexerProvider indexerProvider, - SceneMappingProvider sceneMappingProvider, UpgradePossibleSpecification upgradePossibleSpecification, - AllowedDownloadSpecification allowedDownloadSpecification, SearchHistoryProvider searchHistoryProvider) - : base(episodeProvider, downloadProvider, seriesProvider, indexerProvider, sceneMappingProvider, - upgradePossibleSpecification, allowedDownloadSpecification, searchHistoryProvider) + public EpisodeSearch(SeriesProvider seriesProvider, EpisodeProvider episodeProvider, DownloadProvider downloadProvider, IndexerProvider indexerProvider, + SceneMappingProvider sceneMappingProvider, AllowedDownloadSpecification allowedDownloadSpecification, + SearchHistoryProvider searchHistoryProvider) + : base(seriesProvider, episodeProvider, downloadProvider, indexerProvider, sceneMappingProvider, + allowedDownloadSpecification, searchHistoryProvider) { } - protected override List Search(Series series, dynamic options) + public override List PerformSearch(Series series, dynamic options, ProgressNotification notification) { - if (options == null) - throw new ArgumentNullException(options); + //Todo: Daily and Anime or separate them out? + //Todo: Epsiodes that use scene numbering - if (options.SeasonNumber < 0) - throw new ArgumentException("SeasonNumber is invalid"); + if (options.Episode == null) + throw new ArgumentException("Episode is invalid"); - if (options.EpisodeNumber < 0) - throw new ArgumentException("EpisodeNumber is invalid"); + notification.CurrentMessage = "Looking for " + options.Episode; var reports = new List(); - var title = GetSeriesTitle(series); + var title = GetSearchTitle(series); + + var seasonNumber = options.Episode.SeasonNumber; + var episodeNumber = options.Episode.EpisodeNumber; + + if (series.UseSceneNumbering) + { + if(options.Episode.SceneSeasonNumber > 0 && options.Episode.SceneEpisodeNumber > 0) + { + logger.Trace("Using Scene Numbering for: {0}", options.Episode); + seasonNumber = options.Episode.SceneSeasonNumber; + episodeNumber = options.Episode.SceneEpisodeNumber; + } + } Parallel.ForEach(_indexerProvider.GetEnabledIndexers(), indexer => { try { - reports.AddRange(indexer.FetchEpisode(title, options.SeasonNumber, options.EpisodeNumber)); + reports.AddRange(indexer.FetchEpisode(title, seasonNumber, episodeNumber)); } catch (Exception e) { logger.ErrorException(String.Format("An error has occurred while searching for {0}-S{1:00}E{2:00} from: {3}", - series.Title, options.SeasonNumber, options.EpisodeNumber, indexer.Name), e); + series.Title, options.Episode.SeasonNumber, options.Episode.EpisodeNumber, indexer.Name), e); } }); return reports; } - protected override SearchHistoryItem CheckEpisode(Series series, List episodes, EpisodeParseResult episodeParseResult, - SearchHistoryItem item) + public override SearchHistoryItem CheckReport(Series series, dynamic options, EpisodeParseResult episodeParseResult, + SearchHistoryItem item) { - throw new NotImplementedException(); + if(series.UseSceneNumbering && options.Episode.SeasonNumber > 0 && options.Episode.EpisodeNumber > 0) + { + if (options.Episode.SceneSeasonNumber != episodeParseResult.SeasonNumber) + { + logger.Trace("Season number does not match searched season number, skipping."); + item.SearchError = ReportRejectionType.WrongSeason; + + return item; + } + + if (!episodeParseResult.EpisodeNumbers.Contains(options.Episode.SceneEpisodeNumber)) + { + logger.Trace("Episode number does not match searched episode number, skipping."); + item.SearchError = ReportRejectionType.WrongEpisode; + + return item; + } + + return item; + } + + if(options.Episode.SeasonNumber != episodeParseResult.SeasonNumber) + { + logger.Trace("Season number does not match searched season number, skipping."); + item.SearchError = ReportRejectionType.WrongSeason; + + return item; + } + + if (!episodeParseResult.EpisodeNumbers.Contains(options.Episode.EpisodeNumber)) + { + logger.Trace("Episode number does not match searched episode number, skipping."); + item.SearchError = ReportRejectionType.WrongEpisode; + + return item; + } + + return item; + } + + protected override void FinalizeSearch(Series series, dynamic options, Boolean reportsFound, ProgressNotification notification) + { + logger.Warn("Unable to find {0} in any of indexers.", options.Episode); + + notification.CurrentMessage = reportsFound ? String.Format("Sorry, couldn't find {0}, that matches your preferences.", options.Episode) + : String.Format("Sorry, couldn't find {0} in any of indexers.", options.Episode); } } } diff --git a/NzbDrone.Core/Providers/Search/PartialSeasonSearch.cs b/NzbDrone.Core/Providers/Search/PartialSeasonSearch.cs new file mode 100644 index 000000000..3672fb44e --- /dev/null +++ b/NzbDrone.Core/Providers/Search/PartialSeasonSearch.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using NLog; +using NzbDrone.Core.Model; +using NzbDrone.Core.Model.Notification; +using NzbDrone.Core.Providers.DecisionEngine; +using NzbDrone.Core.Repository; +using NzbDrone.Core.Repository.Search; + +namespace NzbDrone.Core.Providers.Search +{ + public class PartialSeasonSearch : SearchBase + { + private static readonly Logger logger = LogManager.GetCurrentClassLogger(); + + public PartialSeasonSearch(SeriesProvider seriesProvider, EpisodeProvider episodeProvider, DownloadProvider downloadProvider, IndexerProvider indexerProvider, + SceneMappingProvider sceneMappingProvider, AllowedDownloadSpecification allowedDownloadSpecification, + SearchHistoryProvider searchHistoryProvider) + : base(seriesProvider, episodeProvider, downloadProvider, indexerProvider, sceneMappingProvider, + allowedDownloadSpecification, searchHistoryProvider) + { + } + + public override List PerformSearch(Series series, dynamic options, ProgressNotification notification) + { + if (options.SeasonNumber == null || options.SeasonNumber < 0) + throw new ArgumentException("SeasonNumber is invalid"); + + if (options.Episodes == null) + throw new ArgumentException("Episodes were not provided"); + + List episodes = options.Episodes; + + if (!episodes.Any()) + throw new ArgumentException("Episodes were not provided"); + + notification.CurrentMessage = String.Format("Looking for {0} - Season {1}", series.Title, options.SeasonNumber); + + var reports = new List(); + object reportsLock = new object(); + + var title = GetSearchTitle(series); + var prefixes = GetEpisodeNumberPrefixes(episodes.Select(e => e.EpisodeNumber)); + + foreach(var p in prefixes) + { + var prefix = p; + + Parallel.ForEach(_indexerProvider.GetEnabledIndexers(), indexer => + { + try + { + lock(reportsLock) + { + reports.AddRange(indexer.FetchPartialSeason(title, options.SeasonNumber, prefix)); + } + } + + catch(Exception e) + { + logger.ErrorException( + String.Format( + "An error has occurred while searching for {0} Season {1:00} Prefix: {2} from: {3}", + series.Title, options.SeasonNumber, prefix, indexer.Name), + e); + } + }); + } + + return reports; + } + + public override SearchHistoryItem CheckReport(Series series, dynamic options, EpisodeParseResult episodeParseResult, + SearchHistoryItem item) + { + if(options.SeasonNumber != episodeParseResult.SeasonNumber) + { + logger.Trace("Season number does not match searched season number, skipping."); + item.SearchError = ReportRejectionType.WrongSeason; + + return item; + } + + return item; + } + + protected override void FinalizeSearch(Series series, dynamic options, Boolean reportsFound, ProgressNotification notification) + { + logger.Warn("Unable to find {0} - Season {1} in any of indexers.", series.Title, options.SeasonNumber); + + notification.CurrentMessage = reportsFound ? String.Format("Sorry, couldn't find {0} Season {1:00}, that matches your preferences.", series.Title, options.SeasonNumber) + : String.Format("Sorry, couldn't find {0} Season {1:00} in any of indexers.", series.Title, options.SeasonNumber); + } + + private List GetEpisodeNumberPrefixes(IEnumerable episodeNumbers) + { + var results = new List(); + + foreach (var i in episodeNumbers) + { + results.Add(i / 10); + } + + return results.Distinct().ToList(); + } + } +} diff --git a/NzbDrone.Core/Providers/Search/SearchBase.cs b/NzbDrone.Core/Providers/Search/SearchBase.cs index 243f7fc36..a4c390f80 100644 --- a/NzbDrone.Core/Providers/Search/SearchBase.cs +++ b/NzbDrone.Core/Providers/Search/SearchBase.cs @@ -13,106 +13,142 @@ namespace NzbDrone.Core.Providers.Search { public abstract class SearchBase { + protected readonly SeriesProvider _seriesProvider; protected readonly EpisodeProvider _episodeProvider; protected readonly DownloadProvider _downloadProvider; - protected readonly SeriesProvider _seriesProvider; protected readonly IndexerProvider _indexerProvider; protected readonly SceneMappingProvider _sceneMappingProvider; - protected readonly UpgradePossibleSpecification _upgradePossibleSpecification; protected readonly AllowedDownloadSpecification _allowedDownloadSpecification; protected readonly SearchHistoryProvider _searchHistoryProvider; private static readonly Logger logger = LogManager.GetCurrentClassLogger(); - protected SearchBase(EpisodeProvider episodeProvider, DownloadProvider downloadProvider,SeriesProvider seriesProvider, + protected SearchBase(SeriesProvider seriesProvider, EpisodeProvider episodeProvider, DownloadProvider downloadProvider, IndexerProvider indexerProvider, SceneMappingProvider sceneMappingProvider, - UpgradePossibleSpecification upgradePossibleSpecification, AllowedDownloadSpecification allowedDownloadSpecification, + AllowedDownloadSpecification allowedDownloadSpecification, SearchHistoryProvider searchHistoryProvider) { + _seriesProvider = seriesProvider; _episodeProvider = episodeProvider; _downloadProvider = downloadProvider; - _seriesProvider = seriesProvider; _indexerProvider = indexerProvider; _sceneMappingProvider = sceneMappingProvider; - _upgradePossibleSpecification = upgradePossibleSpecification; _allowedDownloadSpecification = allowedDownloadSpecification; _searchHistoryProvider = searchHistoryProvider; } - protected SearchBase() + public abstract List PerformSearch(Series series, dynamic options, ProgressNotification notification); + public abstract SearchHistoryItem CheckReport(Series series, dynamic options, EpisodeParseResult episodeParseResult, + SearchHistoryItem item); + + protected abstract void FinalizeSearch(Series series, dynamic options, Boolean reportsFound, ProgressNotification notification); + + public virtual List Search(Series series, dynamic options, ProgressNotification notification) { - } + if (options == null) + throw new ArgumentNullException(options); - protected abstract List Search(Series series, dynamic options); - protected abstract SearchHistoryItem CheckEpisode(Series series, List episodes, EpisodeParseResult episodeParseResult, - SearchHistoryItem item); + var searchResult = new SearchHistory + { + SearchTime = DateTime.Now, + SeriesId = series.SeriesId, + EpisodeId = options.GetType().GetProperty("Episode") != null ? options.Episode.EpisodeId : null + }; + + List reports = PerformSearch(series, options, notification); + + logger.Debug("Finished searching all indexers. Total {0}", reports.Count); + notification.CurrentMessage = "Processing search results"; + + ProcessReports(series, options, reports, searchResult, notification); + _searchHistoryProvider.Add(searchResult); + + if(searchResult.Successes.Any()) + return searchResult.Successes; + + FinalizeSearch(series, options, reports.Any(), notification); + return new List(); + } - protected virtual SearchHistoryItem ProcessReport(EpisodeParseResult episodeParseResult, Series series, List episodes) + public virtual SearchHistory ProcessReports(Series series, dynamic options, List episodeParseResults, + SearchHistory searchResult, ProgressNotification notification) { - try + var items = new List(); + searchResult.Successes = new List(); + + foreach(var episodeParseResult in episodeParseResults + .OrderByDescending(c => c.Quality) + .ThenBy(c => c.EpisodeNumbers.MinOrDefault()) + .ThenBy(c => c.Age)) { - var item = new SearchHistoryItem - { - ReportTitle = episodeParseResult.OriginalString, - NzbUrl = episodeParseResult.NzbUrl, - Indexer = episodeParseResult.Indexer, - Quality = episodeParseResult.Quality.Quality, - Proper = episodeParseResult.Quality.Proper, - Size = episodeParseResult.Size, - Age = episodeParseResult.Age, - Language = episodeParseResult.Language - }; - - logger.Trace("Analysing report " + episodeParseResult); - - //Get the matching series - episodeParseResult.Series = _seriesProvider.FindSeries(episodeParseResult.CleanTitle); - - //If series is null or doesn't match the series we're looking for return - if (episodeParseResult.Series == null || episodeParseResult.Series.SeriesId != series.SeriesId) + try { - item.SearchError = ReportRejectionType.WrongSeries; - return item; + var item = new SearchHistoryItem + { + ReportTitle = episodeParseResult.OriginalString, + NzbUrl = episodeParseResult.NzbUrl, + Indexer = episodeParseResult.Indexer, + Quality = episodeParseResult.Quality.Quality, + Proper = episodeParseResult.Quality.Proper, + Size = episodeParseResult.Size, + Age = episodeParseResult.Age, + Language = episodeParseResult.Language + }; + + items.Add(item); + + logger.Trace("Analysing report " + episodeParseResult); + episodeParseResult.Series = _seriesProvider.FindSeries(episodeParseResult.CleanTitle); + + if(episodeParseResult.Series == null || episodeParseResult.Series.SeriesId != series.SeriesId) + { + item.SearchError = ReportRejectionType.WrongSeries; + continue; + } + + episodeParseResult.Episodes = _episodeProvider.GetEpisodesByParseResult(episodeParseResult); + + if (searchResult.Successes.Intersect(episodeParseResult.Episodes.Select(e => e.EpisodeId)).Any()) + { + item.SearchError = ReportRejectionType.Skipped; + continue; + } + + CheckReport(series, options, episodeParseResult, item); + if (item.SearchError != ReportRejectionType.None) + continue; + + item.SearchError = _allowedDownloadSpecification.IsSatisfiedBy(episodeParseResult); + + if(item.SearchError == ReportRejectionType.None) + { + if(DownloadReport(notification, episodeParseResult, item)) + searchResult.Successes.AddRange(episodeParseResult.Episodes.Select(e => e.EpisodeId)); + } } - - //If parse result doesn't have an air date or it doesn't match passed in airdate, skip the report. - if (CheckEpisode(series, episodes, item).SearchError != ReportRejectionType.None) + catch(Exception e) { - return item; + logger.ErrorException("An error has occurred while processing parse result items from " + episodeParseResult, e); } - - episodeParseResult.Episodes = _episodeProvider.GetEpisodesByParseResult(episodeParseResult); - - item.SearchError = _allowedDownloadSpecification.IsSatisfiedBy(episodeParseResult); - return item; - } - catch (Exception e) - { - logger.ErrorException("An error has occurred while processing parse result items from " + episodeParseResult, e); } - return null; + searchResult.SearchHistoryItems = items; + return searchResult; } - protected virtual SearchHistoryItem DownloadReport(ProgressNotification notification, EpisodeParseResult episodeParseResult, SearchHistoryItem item) + public virtual Boolean DownloadReport(ProgressNotification notification, EpisodeParseResult episodeParseResult, SearchHistoryItem item) { - //Todo: Customize download message per search type? (override) - logger.Debug("Found '{0}'. Adding to download queue.", episodeParseResult); try { if (_downloadProvider.DownloadReport(episodeParseResult)) { - notification.CurrentMessage = - String.Format("{0} - {1} {2} Added to download queue", - episodeParseResult.Series.Title, episodeParseResult.AirDate.Value.ToShortDateString(), episodeParseResult.Quality); - + notification.CurrentMessage = String.Format("{0} Added to download queue", episodeParseResult); item.Success = true; + return true; } - else - { - item.SearchError = ReportRejectionType.DownloadClientFailure; - } + + item.SearchError = ReportRejectionType.DownloadClientFailure; } catch (Exception e) { @@ -121,12 +157,13 @@ namespace NzbDrone.Core.Providers.Search item.SearchError = ReportRejectionType.DownloadClientFailure; } - return item; + return false; } - protected virtual string GetSeriesTitle(Series series, int seasonNumber = -1) - { + public virtual string GetSearchTitle(Series series, int seasonNumber = -1) + { //Todo: Add support for per season lookup (used for anime) + //Todo: Add support for multiple names var title = _sceneMappingProvider.GetSceneName(series.SeriesId); if (String.IsNullOrWhiteSpace(title)) diff --git a/NzbDrone.Core/Providers/SearchProvider.cs b/NzbDrone.Core/Providers/SearchProvider.cs index b6a84888e..ba3026cfa 100644 --- a/NzbDrone.Core/Providers/SearchProvider.cs +++ b/NzbDrone.Core/Providers/SearchProvider.cs @@ -7,6 +7,7 @@ using NLog; using NzbDrone.Core.Model; using NzbDrone.Core.Model.Notification; using NzbDrone.Core.Providers.DecisionEngine; +using NzbDrone.Core.Providers.Search; using NzbDrone.Core.Repository; using NzbDrone.Core.Repository.Search; @@ -14,30 +15,18 @@ namespace NzbDrone.Core.Providers { public class SearchProvider { - private readonly EpisodeProvider _episodeProvider; - private readonly DownloadProvider _downloadProvider; private readonly SeriesProvider _seriesProvider; - private readonly IndexerProvider _indexerProvider; - private readonly SceneMappingProvider _sceneMappingProvider; - private readonly UpgradePossibleSpecification _upgradePossibleSpecification; - private readonly AllowedDownloadSpecification _allowedDownloadSpecification; - private readonly SearchHistoryProvider _searchHistoryProvider; + private readonly EpisodeProvider _episodeProvider; + private readonly PartialSeasonSearch _partialSeasonSearch; private static readonly Logger logger = LogManager.GetCurrentClassLogger(); - public SearchProvider(EpisodeProvider episodeProvider, DownloadProvider downloadProvider, SeriesProvider seriesProvider, - IndexerProvider indexerProvider, SceneMappingProvider sceneMappingProvider, - UpgradePossibleSpecification upgradePossibleSpecification, AllowedDownloadSpecification allowedDownloadSpecification, - SearchHistoryProvider searchHistoryProvider) + public SearchProvider(SeriesProvider seriesProvider, EpisodeProvider episodeProvider, + PartialSeasonSearch partialSeasonSearch) { - _episodeProvider = episodeProvider; - _downloadProvider = downloadProvider; _seriesProvider = seriesProvider; - _indexerProvider = indexerProvider; - _sceneMappingProvider = sceneMappingProvider; - _upgradePossibleSpecification = upgradePossibleSpecification; - _allowedDownloadSpecification = allowedDownloadSpecification; - _searchHistoryProvider = searchHistoryProvider; + _episodeProvider = episodeProvider; + _partialSeasonSearch = partialSeasonSearch; } public SearchProvider() @@ -46,13 +35,6 @@ namespace NzbDrone.Core.Providers public virtual List SeasonSearch(ProgressNotification notification, int seriesId, int seasonNumber) { - var searchResult = new SearchHistory - { - SearchTime = DateTime.Now, - SeriesId = seriesId, - SeasonNumber = seasonNumber - }; - var series = _seriesProvider.GetSeries(seriesId); if (series == null) @@ -76,59 +58,12 @@ namespace NzbDrone.Core.Providers return new List(); } - notification.CurrentMessage = String.Format("Searching for {0} Season {1}", series.Title, seasonNumber); - - List reports; - - if (series.UseSceneNumbering) - { - var sceneSeasonNumbers = episodes.Select(e => e.SceneSeasonNumber).ToList(); - var sceneEpisodeNumbers = episodes.Select(e => e.SceneEpisodeNumber).ToList(); - - if (sceneSeasonNumbers.Distinct().Count() > 1) - { - logger.Trace("Uses scene numbering, but multiple seasons found, skipping."); - return new List(); - } - - reports = PerformSeasonSearch(series, sceneSeasonNumbers.First()); - - reports.Where(p => p.FullSeason && p.SeasonNumber == sceneSeasonNumbers.First()).ToList().ForEach( - e => e.EpisodeNumbers = sceneEpisodeNumbers.ToList() - ); - } - - else - { - reports = PerformSeasonSearch(series, seasonNumber); - - reports.Where(p => p.FullSeason && p.SeasonNumber == seasonNumber).ToList().ForEach( - e => e.EpisodeNumbers = episodes.Select(ep => ep.EpisodeNumber).ToList() - ); - } - - logger.Debug("Finished searching all indexers. Total {0}", reports.Count); - - if (reports.Count == 0) - return new List(); - - notification.CurrentMessage = "Processing search results"; - - searchResult.SearchHistoryItems = ProcessSearchResults(notification, reports, searchResult, series, seasonNumber); - _searchHistoryProvider.Add(searchResult); - - return searchResult.Successes; + //Todo: Support full season searching + return new List(); } public virtual List PartialSeasonSearch(ProgressNotification notification, int seriesId, int seasonNumber) { - var searchResult = new SearchHistory - { - SearchTime = DateTime.Now, - SeriesId = seriesId, - SeasonNumber = seasonNumber - }; - var series = _seriesProvider.GetSeries(seriesId); if (series == null) @@ -143,438 +78,15 @@ namespace NzbDrone.Core.Providers return new List(); } - notification.CurrentMessage = String.Format("Searching for {0} Season {1}", series.Title, seasonNumber); var episodes = _episodeProvider.GetEpisodesBySeason(seriesId, seasonNumber); - List reports; - - if (series.UseSceneNumbering) - { - var sceneSeasonNumbers = episodes.Select(e => e.SceneSeasonNumber).ToList(); - var sceneEpisodeNumbers = episodes.Select(e => e.SceneEpisodeNumber).ToList(); - - if (sceneSeasonNumbers.Distinct().Count() > 1) - { - logger.Trace("Uses scene numbering, but multiple seasons found, skipping."); - return new List(); - } - - reports = PerformPartialSeasonSearch(series, sceneSeasonNumbers.First(), GetEpisodeNumberPrefixes(sceneEpisodeNumbers)); - } - - else + if (episodes == null || episodes.Count == 0) { - reports = PerformPartialSeasonSearch(series, seasonNumber, GetEpisodeNumberPrefixes(episodes.Select(e => e.EpisodeNumber))); - } - - logger.Debug("Finished searching all indexers. Total {0}", reports.Count); - - if (reports.Count == 0) + logger.Warn("No episodes in database found for series: {0} Season: {1}.", seriesId, seasonNumber); return new List(); - - notification.CurrentMessage = "Processing search results"; - searchResult.SearchHistoryItems = ProcessSearchResults(notification, reports, searchResult, series, seasonNumber); - - _searchHistoryProvider.Add(searchResult); - return searchResult.Successes; - } - - public virtual bool EpisodeSearch(ProgressNotification notification, int episodeId) - { - var episode = _episodeProvider.GetEpisode(episodeId); - - if (episode == null) - { - logger.Error("Unable to find an episode {0} in database", episodeId); - return false; - } - - if (!_upgradePossibleSpecification.IsSatisfiedBy(episode)) - { - logger.Info("Search for {0} was aborted, file in disk meets or exceeds Profile's Cutoff", episode); - notification.CurrentMessage = String.Format("Skipping search for {0}, the file you have is already at cutoff", episode); - return false; - } - - notification.CurrentMessage = "Looking for " + episode; - List reports; - - var searchResult = new SearchHistory - { - SearchTime = DateTime.Now, - SeriesId = episode.Series.SeriesId, - EpisodeId = episodeId - }; - - if (episode.Series.IsDaily) - { - if (!episode.AirDate.HasValue) - { - logger.Warn("AirDate is not Valid for: {0}", episode); - notification.CurrentMessage = String.Format("Search for {0} Failed, AirDate is invalid", episode); - return false; - } - - reports = PerformDailyEpisodeSearch(episode.Series, episode); - - logger.Debug("Finished searching all indexers. Total {0}", reports.Count); - notification.CurrentMessage = "Processing search results"; - - searchResult.SearchHistoryItems = ProcessSearchResults(notification, reports, episode.Series, episode.AirDate.Value); - _searchHistoryProvider.Add(searchResult); - - if (searchResult.SearchHistoryItems.Any(r => r.Success)) - return true; - } - - else if (episode.Series.UseSceneNumbering) - { - var seasonNumber = episode.SceneSeasonNumber; - var episodeNumber = episode.SceneEpisodeNumber; - - if (seasonNumber == 0 && episodeNumber == 0) - { - seasonNumber = episode.SeasonNumber; - episodeNumber = episode.EpisodeNumber; - } - - reports = PerformEpisodeSearch(episode.Series, seasonNumber, episodeNumber); - - searchResult.SearchHistoryItems = ProcessSearchResults( - notification, - reports, - searchResult, - episode.Series, - seasonNumber, - episodeNumber - ); - - _searchHistoryProvider.Add(searchResult); - - if (searchResult.SearchHistoryItems.Any(r => r.Success)) - return true; - } - - else - { - reports = PerformEpisodeSearch(episode.Series, episode.SeasonNumber, episode.EpisodeNumber); - - searchResult.SearchHistoryItems = ProcessSearchResults(notification, reports, searchResult, episode.Series, episode.SeasonNumber, episode.EpisodeNumber); - _searchHistoryProvider.Add(searchResult); - - if (searchResult.SearchHistoryItems.Any(r => r.Success)) - return true; - } - - logger.Warn("Unable to find {0} in any of indexers.", episode); - - notification.CurrentMessage = reports.Any() ? String.Format("Sorry, couldn't find {0}, that matches your preferences.", episode) - : String.Format("Sorry, couldn't find {0} in any of indexers.", episode); - - return false; - } - - public List ProcessSearchResults(ProgressNotification notification, IEnumerable reports, SearchHistory searchResult, Series series, int seasonNumber, int? episodeNumber = null) - { - var items = new List(); - searchResult.Successes = new List(); - - foreach (var episodeParseResult in reports.OrderByDescending(c => c.Quality) - .ThenBy(c => c.EpisodeNumbers.MinOrDefault()) - .ThenBy(c => c.Age)) - { - try - { - logger.Trace("Analysing report " + episodeParseResult); - - var item = new SearchHistoryItem - { - ReportTitle = episodeParseResult.OriginalString, - NzbUrl = episodeParseResult.NzbUrl, - Indexer = episodeParseResult.Indexer, - Quality = episodeParseResult.Quality.Quality, - Proper = episodeParseResult.Quality.Proper, - Size = episodeParseResult.Size, - Age = episodeParseResult.Age, - Language = episodeParseResult.Language - }; - - items.Add(item); - - //Get the matching series - episodeParseResult.Series = _seriesProvider.FindSeries(episodeParseResult.CleanTitle); - - //If series is null or doesn't match the series we're looking for return - if (episodeParseResult.Series == null || episodeParseResult.Series.SeriesId != series.SeriesId) - { - logger.Trace("Unexpected series for search: {0}. Skipping.", episodeParseResult.CleanTitle); - item.SearchError = ReportRejectionType.WrongSeries; - continue; - } - - //If SeasonNumber doesn't match or episode is not in the in the list in the parse result, skip the report. - if (episodeParseResult.SeasonNumber != seasonNumber) - { - logger.Trace("Season number does not match searched season number, skipping."); - item.SearchError = ReportRejectionType.WrongSeason; - continue; - } - - //If the EpisodeNumber was passed in and it is not contained in the parseResult, skip the report. - if (episodeNumber.HasValue && !episodeParseResult.EpisodeNumbers.Contains(episodeNumber.Value)) - { - logger.Trace("Searched episode number is not contained in post, skipping."); - item.SearchError = ReportRejectionType.WrongEpisode; - continue; - } - - //Make sure we haven't already downloaded a report with this episodenumber, if we have, skip the report. - if (searchResult.Successes.Intersect(episodeParseResult.EpisodeNumbers).Any()) - { - logger.Trace("Episode has already been downloaded in this search, skipping."); - item.SearchError = ReportRejectionType.Skipped; - continue; - } - - episodeParseResult.Episodes = _episodeProvider.GetEpisodesByParseResult(episodeParseResult); - - item.SearchError = _allowedDownloadSpecification.IsSatisfiedBy(episodeParseResult); - if (item.SearchError == ReportRejectionType.None) - { - logger.Debug("Found '{0}'. Adding to download queue.", episodeParseResult); - try - { - if (_downloadProvider.DownloadReport(episodeParseResult)) - { - notification.CurrentMessage = String.Format("{0} Added to download queue", episodeParseResult); - - //Add the list of episode numbers from this release - searchResult.Successes.AddRange(episodeParseResult.EpisodeNumbers); - item.Success = true; - } - else - { - item.SearchError = ReportRejectionType.DownloadClientFailure; - } - } - catch (Exception e) - { - logger.ErrorException("Unable to add report to download queue." + episodeParseResult, e); - notification.CurrentMessage = String.Format("Unable to add report to download queue. {0}", episodeParseResult); - item.SearchError = ReportRejectionType.DownloadClientFailure; - } - } - } - catch (Exception e) - { - logger.ErrorException("An error has occurred while processing parse result items from " + episodeParseResult, e); - } - } - - return items; - } - - public List ProcessSearchResults(ProgressNotification notification, IEnumerable reports, Series series, DateTime airDate) - { - var items = new List(); - var skip = false; - - foreach (var episodeParseResult in reports.OrderByDescending(c => c.Quality)) - { - try - { - var item = new SearchHistoryItem - { - ReportTitle = episodeParseResult.OriginalString, - NzbUrl = episodeParseResult.NzbUrl, - Indexer = episodeParseResult.Indexer, - Quality = episodeParseResult.Quality.Quality, - Proper = episodeParseResult.Quality.Proper, - Size = episodeParseResult.Size, - Age = episodeParseResult.Age, - Language = episodeParseResult.Language - }; - - items.Add(item); - - if (skip) - { - item.SearchError = ReportRejectionType.Skipped; - continue; - } - - logger.Trace("Analysing report " + episodeParseResult); - - //Get the matching series - episodeParseResult.Series = _seriesProvider.FindSeries(episodeParseResult.CleanTitle); - - //If series is null or doesn't match the series we're looking for return - if (episodeParseResult.Series == null || episodeParseResult.Series.SeriesId != series.SeriesId) - { - item.SearchError = ReportRejectionType.WrongSeries; - continue; - } - - //If parse result doesn't have an air date or it doesn't match passed in airdate, skip the report. - if (!episodeParseResult.AirDate.HasValue || episodeParseResult.AirDate.Value.Date != airDate.Date) - { - item.SearchError = ReportRejectionType.WrongEpisode; - continue; - } - - episodeParseResult.Episodes = _episodeProvider.GetEpisodesByParseResult(episodeParseResult); - - item.SearchError = _allowedDownloadSpecification.IsSatisfiedBy(episodeParseResult); - if (item.SearchError == ReportRejectionType.None) - { - logger.Debug("Found '{0}'. Adding to download queue.", episodeParseResult); - try - { - if (_downloadProvider.DownloadReport(episodeParseResult)) - { - notification.CurrentMessage = - String.Format("{0} - {1} {2} Added to download queue", - episodeParseResult.Series.Title, episodeParseResult.AirDate.Value.ToShortDateString(), episodeParseResult.Quality); - - item.Success = true; - skip = true; - } - else - { - item.SearchError = ReportRejectionType.DownloadClientFailure; - } - } - catch (Exception e) - { - logger.ErrorException("Unable to add report to download queue." + episodeParseResult, e); - notification.CurrentMessage = String.Format("Unable to add report to download queue. {0}", episodeParseResult); - item.SearchError = ReportRejectionType.DownloadClientFailure; - } - } - } - catch (Exception e) - { - logger.ErrorException("An error has occurred while processing parse result items from " + episodeParseResult, e); - } - } - - return items; - } - - private List GetEpisodeNumberPrefixes(IEnumerable episodeNumbers) - { - var results = new List(); - - foreach (var i in episodeNumbers) - { - results.Add(i / 10); - } - - return results.Distinct().ToList(); - } - - public List PerformEpisodeSearch(Series series, int seasonNumber, int episodeNumber) - { - var reports = new List(); - var title = GetSeriesTitle(series); - - Parallel.ForEach(_indexerProvider.GetEnabledIndexers(), indexer => - { - try - { - reports.AddRange(indexer.FetchEpisode(title, seasonNumber, episodeNumber)); - } - - catch (Exception e) - { - logger.ErrorException(String.Format("An error has occurred while searching for {0}-S{1:00}E{2:00} from: {3}", - series.Title, seasonNumber, episodeNumber, indexer.Name), e); - } - }); - - return reports; - } - - public List PerformDailyEpisodeSearch(Series series, Episode episode) - { - var reports = new List(); - var title = GetSeriesTitle(series); - - Parallel.ForEach(_indexerProvider.GetEnabledIndexers(), indexer => - { - try - { - logger.Trace("Episode {0} is a daily episode, searching as daily", episode); - reports.AddRange(indexer.FetchDailyEpisode(title, episode.AirDate.Value)); - } - - catch (Exception e) - { - logger.ErrorException(String.Format("An error has occurred while searching for {0}-{1} from: {2}", - series.Title, episode.AirDate, indexer.Name), e); - } - }); - - return reports; - } - - public List PerformPartialSeasonSearch(Series series, int seasonNumber, List prefixes) - { - var reports = new List(); - var title = GetSeriesTitle(series); - - Parallel.ForEach(_indexerProvider.GetEnabledIndexers(), indexer => - { - try - { - foreach (var episodePrefix in prefixes) - { - reports.AddRange(indexer.FetchPartialSeason(title, seasonNumber, episodePrefix)); - } - } - - catch (Exception e) - { - logger.ErrorException(String.Format("An error has occurred while searching for {0}-S{1:00} from: {2}", - series.Title, seasonNumber, indexer.Name), e); - } - }); - - return reports; - } - - public List PerformSeasonSearch(Series series, int seasonNumber) - { - var reports = new List(); - var title = GetSeriesTitle(series); - - Parallel.ForEach(_indexerProvider.GetEnabledIndexers(), indexer => - { - try - { - reports.AddRange(indexer.FetchSeason(title, seasonNumber)); - } - - catch (Exception e) - { - logger.ErrorException("An error has occurred while searching for items from: " + indexer.Name, e); - } - }); - - return reports; - } - - public string GetSeriesTitle(Series series) - { - var title = _sceneMappingProvider.GetSceneName(series.SeriesId); - - if(String.IsNullOrWhiteSpace(title)) - { - title = series.Title; - title = title.Replace("&", "and"); } - return title; + return _partialSeasonSearch.Search(series, new {SeasonNumber = seasonNumber, Episodes = episodes}, notification); } } } diff --git a/NzbDrone.Core/Providers/SearchProvider2.cs b/NzbDrone.Core/Providers/SearchProvider2.cs new file mode 100644 index 000000000..c3562f713 --- /dev/null +++ b/NzbDrone.Core/Providers/SearchProvider2.cs @@ -0,0 +1,580 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using NLog; +using NzbDrone.Core.Model; +using NzbDrone.Core.Model.Notification; +using NzbDrone.Core.Providers.DecisionEngine; +using NzbDrone.Core.Repository; +using NzbDrone.Core.Repository.Search; + +namespace NzbDrone.Core.Providers +{ + public class SearchProvider2 + { + private readonly EpisodeProvider _episodeProvider; + private readonly DownloadProvider _downloadProvider; + private readonly SeriesProvider _seriesProvider; + private readonly IndexerProvider _indexerProvider; + private readonly SceneMappingProvider _sceneMappingProvider; + private readonly UpgradePossibleSpecification _upgradePossibleSpecification; + private readonly AllowedDownloadSpecification _allowedDownloadSpecification; + private readonly SearchHistoryProvider _searchHistoryProvider; + + private static readonly Logger logger = LogManager.GetCurrentClassLogger(); + + public SearchProvider2(EpisodeProvider episodeProvider, DownloadProvider downloadProvider, SeriesProvider seriesProvider, + IndexerProvider indexerProvider, SceneMappingProvider sceneMappingProvider, + UpgradePossibleSpecification upgradePossibleSpecification, AllowedDownloadSpecification allowedDownloadSpecification, + SearchHistoryProvider searchHistoryProvider) + { + _episodeProvider = episodeProvider; + _downloadProvider = downloadProvider; + _seriesProvider = seriesProvider; + _indexerProvider = indexerProvider; + _sceneMappingProvider = sceneMappingProvider; + _upgradePossibleSpecification = upgradePossibleSpecification; + _allowedDownloadSpecification = allowedDownloadSpecification; + _searchHistoryProvider = searchHistoryProvider; + } + + public SearchProvider2() + { + } + + public virtual List SeasonSearch(ProgressNotification notification, int seriesId, int seasonNumber) + { + var searchResult = new SearchHistory + { + SearchTime = DateTime.Now, + SeriesId = seriesId, + SeasonNumber = seasonNumber + }; + + var series = _seriesProvider.GetSeries(seriesId); + + if (series == null) + { + logger.Error("Unable to find an series {0} in database", seriesId); + return new List(); + } + + if (series.IsDaily) + { + logger.Trace("Daily series detected, skipping season search: {0}", series.Title); + return new List(); + } + + logger.Debug("Getting episodes from database for series: {0} and season: {1}", seriesId, seasonNumber); + var episodes = _episodeProvider.GetEpisodesBySeason(seriesId, seasonNumber); + + if (episodes == null || episodes.Count == 0) + { + logger.Warn("No episodes in database found for series: {0} and season: {1}.", seriesId, seasonNumber); + return new List(); + } + + notification.CurrentMessage = String.Format("Searching for {0} Season {1}", series.Title, seasonNumber); + + List reports; + + if (series.UseSceneNumbering) + { + var sceneSeasonNumbers = episodes.Select(e => e.SceneSeasonNumber).ToList(); + var sceneEpisodeNumbers = episodes.Select(e => e.SceneEpisodeNumber).ToList(); + + if (sceneSeasonNumbers.Distinct().Count() > 1) + { + logger.Trace("Uses scene numbering, but multiple seasons found, skipping."); + return new List(); + } + + reports = PerformSeasonSearch(series, sceneSeasonNumbers.First()); + + reports.Where(p => p.FullSeason && p.SeasonNumber == sceneSeasonNumbers.First()).ToList().ForEach( + e => e.EpisodeNumbers = sceneEpisodeNumbers.ToList() + ); + } + + else + { + reports = PerformSeasonSearch(series, seasonNumber); + + reports.Where(p => p.FullSeason && p.SeasonNumber == seasonNumber).ToList().ForEach( + e => e.EpisodeNumbers = episodes.Select(ep => ep.EpisodeNumber).ToList() + ); + } + + logger.Debug("Finished searching all indexers. Total {0}", reports.Count); + + if (reports.Count == 0) + return new List(); + + notification.CurrentMessage = "Processing search results"; + + searchResult.SearchHistoryItems = ProcessSearchResults(notification, reports, searchResult, series, seasonNumber); + _searchHistoryProvider.Add(searchResult); + + return searchResult.Successes; + } + + public virtual List PartialSeasonSearch(ProgressNotification notification, int seriesId, int seasonNumber) + { + var searchResult = new SearchHistory + { + SearchTime = DateTime.Now, + SeriesId = seriesId, + SeasonNumber = seasonNumber + }; + + var series = _seriesProvider.GetSeries(seriesId); + + if (series == null) + { + logger.Error("Unable to find an series {0} in database", seriesId); + return new List(); + } + + if (series.IsDaily) + { + logger.Trace("Daily series detected, skipping season search: {0}", series.Title); + return new List(); + } + + notification.CurrentMessage = String.Format("Searching for {0} Season {1}", series.Title, seasonNumber); + var episodes = _episodeProvider.GetEpisodesBySeason(seriesId, seasonNumber); + + List reports; + + if (series.UseSceneNumbering) + { + var sceneSeasonNumbers = episodes.Select(e => e.SceneSeasonNumber).ToList(); + var sceneEpisodeNumbers = episodes.Select(e => e.SceneEpisodeNumber).ToList(); + + if (sceneSeasonNumbers.Distinct().Count() > 1) + { + logger.Trace("Uses scene numbering, but multiple seasons found, skipping."); + return new List(); + } + + reports = PerformPartialSeasonSearch(series, sceneSeasonNumbers.First(), GetEpisodeNumberPrefixes(sceneEpisodeNumbers)); + } + + else + { + reports = PerformPartialSeasonSearch(series, seasonNumber, GetEpisodeNumberPrefixes(episodes.Select(e => e.EpisodeNumber))); + } + + logger.Debug("Finished searching all indexers. Total {0}", reports.Count); + + if (reports.Count == 0) + return new List(); + + notification.CurrentMessage = "Processing search results"; + searchResult.SearchHistoryItems = ProcessSearchResults(notification, reports, searchResult, series, seasonNumber); + + _searchHistoryProvider.Add(searchResult); + return searchResult.Successes; + } + + public virtual bool EpisodeSearch(ProgressNotification notification, int episodeId) + { + var episode = _episodeProvider.GetEpisode(episodeId); + + if (episode == null) + { + logger.Error("Unable to find an episode {0} in database", episodeId); + return false; + } + + if (!_upgradePossibleSpecification.IsSatisfiedBy(episode)) + { + logger.Info("Search for {0} was aborted, file in disk meets or exceeds Profile's Cutoff", episode); + notification.CurrentMessage = String.Format("Skipping search for {0}, the file you have is already at cutoff", episode); + return false; + } + + notification.CurrentMessage = "Looking for " + episode; + List reports; + + var searchResult = new SearchHistory + { + SearchTime = DateTime.Now, + SeriesId = episode.Series.SeriesId, + EpisodeId = episodeId + }; + + if (episode.Series.IsDaily) + { + if (!episode.AirDate.HasValue) + { + logger.Warn("AirDate is not Valid for: {0}", episode); + notification.CurrentMessage = String.Format("Search for {0} Failed, AirDate is invalid", episode); + return false; + } + + reports = PerformDailyEpisodeSearch(episode.Series, episode); + + logger.Debug("Finished searching all indexers. Total {0}", reports.Count); + notification.CurrentMessage = "Processing search results"; + + searchResult.SearchHistoryItems = ProcessSearchResults(notification, reports, episode.Series, episode.AirDate.Value); + _searchHistoryProvider.Add(searchResult); + + if (searchResult.SearchHistoryItems.Any(r => r.Success)) + return true; + } + + else if (episode.Series.UseSceneNumbering) + { + var seasonNumber = episode.SceneSeasonNumber; + var episodeNumber = episode.SceneEpisodeNumber; + + if (seasonNumber == 0 && episodeNumber == 0) + { + seasonNumber = episode.SeasonNumber; + episodeNumber = episode.EpisodeNumber; + } + + reports = PerformEpisodeSearch(episode.Series, seasonNumber, episodeNumber); + + searchResult.SearchHistoryItems = ProcessSearchResults( + notification, + reports, + searchResult, + episode.Series, + seasonNumber, + episodeNumber + ); + + _searchHistoryProvider.Add(searchResult); + + if (searchResult.SearchHistoryItems.Any(r => r.Success)) + return true; + } + + else + { + reports = PerformEpisodeSearch(episode.Series, episode.SeasonNumber, episode.EpisodeNumber); + + searchResult.SearchHistoryItems = ProcessSearchResults(notification, reports, searchResult, episode.Series, episode.SeasonNumber, episode.EpisodeNumber); + _searchHistoryProvider.Add(searchResult); + + if (searchResult.SearchHistoryItems.Any(r => r.Success)) + return true; + } + + logger.Warn("Unable to find {0} in any of indexers.", episode); + + notification.CurrentMessage = reports.Any() ? String.Format("Sorry, couldn't find {0}, that matches your preferences.", episode) + : String.Format("Sorry, couldn't find {0} in any of indexers.", episode); + + return false; + } + + public List ProcessSearchResults(ProgressNotification notification, IEnumerable reports, SearchHistory searchResult, Series series, int seasonNumber, int? episodeNumber = null) + { + var items = new List(); + searchResult.Successes = new List(); + + foreach (var episodeParseResult in reports.OrderByDescending(c => c.Quality) + .ThenBy(c => c.EpisodeNumbers.MinOrDefault()) + .ThenBy(c => c.Age)) + { + try + { + logger.Trace("Analysing report " + episodeParseResult); + + var item = new SearchHistoryItem + { + ReportTitle = episodeParseResult.OriginalString, + NzbUrl = episodeParseResult.NzbUrl, + Indexer = episodeParseResult.Indexer, + Quality = episodeParseResult.Quality.Quality, + Proper = episodeParseResult.Quality.Proper, + Size = episodeParseResult.Size, + Age = episodeParseResult.Age, + Language = episodeParseResult.Language + }; + + items.Add(item); + + //Get the matching series + episodeParseResult.Series = _seriesProvider.FindSeries(episodeParseResult.CleanTitle); + + //If series is null or doesn't match the series we're looking for return + if (episodeParseResult.Series == null || episodeParseResult.Series.SeriesId != series.SeriesId) + { + logger.Trace("Unexpected series for search: {0}. Skipping.", episodeParseResult.CleanTitle); + item.SearchError = ReportRejectionType.WrongSeries; + continue; + } + + //If SeasonNumber doesn't match or episode is not in the in the list in the parse result, skip the report. + if (episodeParseResult.SeasonNumber != seasonNumber) + { + logger.Trace("Season number does not match searched season number, skipping."); + item.SearchError = ReportRejectionType.WrongSeason; + continue; + } + + //If the EpisodeNumber was passed in and it is not contained in the parseResult, skip the report. + if (episodeNumber.HasValue && !episodeParseResult.EpisodeNumbers.Contains(episodeNumber.Value)) + { + logger.Trace("Searched episode number is not contained in post, skipping."); + item.SearchError = ReportRejectionType.WrongEpisode; + continue; + } + + //Make sure we haven't already downloaded a report with this episodenumber, if we have, skip the report. + if (searchResult.Successes.Intersect(episodeParseResult.EpisodeNumbers).Any()) + { + logger.Trace("Episode has already been downloaded in this search, skipping."); + item.SearchError = ReportRejectionType.Skipped; + continue; + } + + episodeParseResult.Episodes = _episodeProvider.GetEpisodesByParseResult(episodeParseResult); + + item.SearchError = _allowedDownloadSpecification.IsSatisfiedBy(episodeParseResult); + if (item.SearchError == ReportRejectionType.None) + { + logger.Debug("Found '{0}'. Adding to download queue.", episodeParseResult); + try + { + if (_downloadProvider.DownloadReport(episodeParseResult)) + { + notification.CurrentMessage = String.Format("{0} Added to download queue", episodeParseResult); + + //Add the list of episode numbers from this release + searchResult.Successes.AddRange(episodeParseResult.EpisodeNumbers); + item.Success = true; + } + else + { + item.SearchError = ReportRejectionType.DownloadClientFailure; + } + } + catch (Exception e) + { + logger.ErrorException("Unable to add report to download queue." + episodeParseResult, e); + notification.CurrentMessage = String.Format("Unable to add report to download queue. {0}", episodeParseResult); + item.SearchError = ReportRejectionType.DownloadClientFailure; + } + } + } + catch (Exception e) + { + logger.ErrorException("An error has occurred while processing parse result items from " + episodeParseResult, e); + } + } + + return items; + } + + public List ProcessSearchResults(ProgressNotification notification, IEnumerable reports, Series series, DateTime airDate) + { + var items = new List(); + var skip = false; + + foreach (var episodeParseResult in reports.OrderByDescending(c => c.Quality)) + { + try + { + var item = new SearchHistoryItem + { + ReportTitle = episodeParseResult.OriginalString, + NzbUrl = episodeParseResult.NzbUrl, + Indexer = episodeParseResult.Indexer, + Quality = episodeParseResult.Quality.Quality, + Proper = episodeParseResult.Quality.Proper, + Size = episodeParseResult.Size, + Age = episodeParseResult.Age, + Language = episodeParseResult.Language + }; + + items.Add(item); + + if (skip) + { + item.SearchError = ReportRejectionType.Skipped; + continue; + } + + logger.Trace("Analysing report " + episodeParseResult); + + //Get the matching series + episodeParseResult.Series = _seriesProvider.FindSeries(episodeParseResult.CleanTitle); + + //If series is null or doesn't match the series we're looking for return + if (episodeParseResult.Series == null || episodeParseResult.Series.SeriesId != series.SeriesId) + { + item.SearchError = ReportRejectionType.WrongSeries; + continue; + } + + //If parse result doesn't have an air date or it doesn't match passed in airdate, skip the report. + if (!episodeParseResult.AirDate.HasValue || episodeParseResult.AirDate.Value.Date != airDate.Date) + { + item.SearchError = ReportRejectionType.WrongEpisode; + continue; + } + + episodeParseResult.Episodes = _episodeProvider.GetEpisodesByParseResult(episodeParseResult); + + item.SearchError = _allowedDownloadSpecification.IsSatisfiedBy(episodeParseResult); + if (item.SearchError == ReportRejectionType.None) + { + logger.Debug("Found '{0}'. Adding to download queue.", episodeParseResult); + try + { + if (_downloadProvider.DownloadReport(episodeParseResult)) + { + notification.CurrentMessage = + String.Format("{0} - {1} {2} Added to download queue", + episodeParseResult.Series.Title, episodeParseResult.AirDate.Value.ToShortDateString(), episodeParseResult.Quality); + + item.Success = true; + skip = true; + } + else + { + item.SearchError = ReportRejectionType.DownloadClientFailure; + } + } + catch (Exception e) + { + logger.ErrorException("Unable to add report to download queue." + episodeParseResult, e); + notification.CurrentMessage = String.Format("Unable to add report to download queue. {0}", episodeParseResult); + item.SearchError = ReportRejectionType.DownloadClientFailure; + } + } + } + catch (Exception e) + { + logger.ErrorException("An error has occurred while processing parse result items from " + episodeParseResult, e); + } + } + + return items; + } + + private List GetEpisodeNumberPrefixes(IEnumerable episodeNumbers) + { + var results = new List(); + + foreach (var i in episodeNumbers) + { + results.Add(i / 10); + } + + return results.Distinct().ToList(); + } + + public List PerformEpisodeSearch(Series series, int seasonNumber, int episodeNumber) + { + var reports = new List(); + var title = GetSeriesTitle(series); + + Parallel.ForEach(_indexerProvider.GetEnabledIndexers(), indexer => + { + try + { + reports.AddRange(indexer.FetchEpisode(title, seasonNumber, episodeNumber)); + } + + catch (Exception e) + { + logger.ErrorException(String.Format("An error has occurred while searching for {0}-S{1:00}E{2:00} from: {3}", + series.Title, seasonNumber, episodeNumber, indexer.Name), e); + } + }); + + return reports; + } + + public List PerformDailyEpisodeSearch(Series series, Episode episode) + { + var reports = new List(); + var title = GetSeriesTitle(series); + + Parallel.ForEach(_indexerProvider.GetEnabledIndexers(), indexer => + { + try + { + logger.Trace("Episode {0} is a daily episode, searching as daily", episode); + reports.AddRange(indexer.FetchDailyEpisode(title, episode.AirDate.Value)); + } + + catch (Exception e) + { + logger.ErrorException(String.Format("An error has occurred while searching for {0}-{1} from: {2}", + series.Title, episode.AirDate, indexer.Name), e); + } + }); + + return reports; + } + + public List PerformPartialSeasonSearch(Series series, int seasonNumber, List prefixes) + { + var reports = new List(); + var title = GetSeriesTitle(series); + + Parallel.ForEach(_indexerProvider.GetEnabledIndexers(), indexer => + { + try + { + foreach (var episodePrefix in prefixes) + { + reports.AddRange(indexer.FetchPartialSeason(title, seasonNumber, episodePrefix)); + } + } + + catch (Exception e) + { + logger.ErrorException(String.Format("An error has occurred while searching for {0}-S{1:00} from: {2}", + series.Title, seasonNumber, indexer.Name), e); + } + }); + + return reports; + } + + public List PerformSeasonSearch(Series series, int seasonNumber) + { + var reports = new List(); + var title = GetSeriesTitle(series); + + Parallel.ForEach(_indexerProvider.GetEnabledIndexers(), indexer => + { + try + { + reports.AddRange(indexer.FetchSeason(title, seasonNumber)); + } + + catch (Exception e) + { + logger.ErrorException("An error has occurred while searching for items from: " + indexer.Name, e); + } + }); + + return reports; + } + + public string GetSeriesTitle(Series series) + { + var title = _sceneMappingProvider.GetSceneName(series.SeriesId); + + if(String.IsNullOrWhiteSpace(title)) + { + title = series.Title; + title = title.Replace("&", "and"); + } + + return title; + } + } +}