Searching refactored

#ND-135 fixed
pull/4/head
Mark McDowall 12 years ago
parent 829ba1cf92
commit 5e7c0951b7

@ -142,7 +142,16 @@
<Compile Include="IndexerTests\NzbxFixture.cs" /> <Compile Include="IndexerTests\NzbxFixture.cs" />
<Compile Include="JobTests\RenameSeasonJobFixture.cs" /> <Compile Include="JobTests\RenameSeasonJobFixture.cs" />
<Compile Include="ProviderTests\RootDirProviderTests\FreeSpaceOnDrivesFixture.cs" /> <Compile Include="ProviderTests\RootDirProviderTests\FreeSpaceOnDrivesFixture.cs" />
<Compile Include="ProviderTests\SearchProviderTests\GetSeriesTitleFixture.cs" /> <Compile Include="ProviderTests\SearchTests\ProcessResultsFixture.cs" />
<Compile Include="ProviderTests\SearchTests\DailyEpisodeSearchTests\CheckReportFixture.cs" />
<Compile Include="ProviderTests\SearchTests\EpisodeSearchTests\CheckReportFixture.cs" />
<Compile Include="ProviderTests\SearchTests\EpisodeSearchTests\PerformSearchFixture.cs" />
<Compile Include="ProviderTests\SearchTests\GetSearchTitleFixture.cs" />
<Compile Include="ProviderTests\SearchTests\DailyEpisodeSearchTests\PerformSearchFixture.cs" />
<Compile Include="ProviderTests\SearchTests\PartialSeasonSearchTests\CheckReportFixture.cs" />
<Compile Include="ProviderTests\SearchTests\PartialSeasonSearchTests\PerformSearchFixture.cs" />
<Compile Include="ProviderTests\SearchTests\PerformSearchTestBase.cs" />
<Compile Include="ProviderTests\SearchTests\TestSearch.cs" />
<Compile Include="ProviderTests\TvRageMappingProviderTests\FindMatchingTvRageSeriesFixture.cs" /> <Compile Include="ProviderTests\TvRageMappingProviderTests\FindMatchingTvRageSeriesFixture.cs" />
<Compile Include="ProviderTests\TvRageMappingProviderTests\ProcessResultsFixture.cs" /> <Compile Include="ProviderTests\TvRageMappingProviderTests\ProcessResultsFixture.cs" />
<Compile Include="ProviderTests\TvRageProviderTests\GetSeriesFixture.cs" /> <Compile Include="ProviderTests\TvRageProviderTests\GetSeriesFixture.cs" />
@ -190,10 +199,6 @@
<Compile Include="ProviderTests\ReferenceDataProviderTest.cs" /> <Compile Include="ProviderTests\ReferenceDataProviderTest.cs" />
<Compile Include="ProviderTests\NotificationProviderTests\NotificationProviderFixture.cs" /> <Compile Include="ProviderTests\NotificationProviderTests\NotificationProviderFixture.cs" />
<Compile Include="ProviderTests\DownloadClientTests\SabProviderTests\QueueFixture.cs" /> <Compile Include="ProviderTests\DownloadClientTests\SabProviderTests\QueueFixture.cs" />
<Compile Include="ProviderTests\SearchProviderTests\ProcessDailySearchResultsFixture.cs" />
<Compile Include="ProviderTests\SearchProviderTests\SearchFixture.cs" />
<Compile Include="ProviderTests\SearchProviderTests\PerformSearchFixture.cs" />
<Compile Include="ProviderTests\SearchProviderTests\ProcessSearchResultsFixture.cs" />
<Compile Include="ProviderTests\NewznabProviderTest.cs" /> <Compile Include="ProviderTests\NewznabProviderTest.cs" />
<Compile Include="ProviderTests\DiskProviderTests\FreeDiskSpaceTest.cs" /> <Compile Include="ProviderTests\DiskProviderTests\FreeDiskSpaceTest.cs" />
<Compile Include="ProviderTests\ProwlProviderTest.cs" /> <Compile Include="ProviderTests\ProwlProviderTest.cs" />

@ -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<Series>
.CreateNew()
.With(s => s.Title = "Scandal (2012)")
.Build();
}
private void WithSceneName()
{
Mocker.GetMock<SceneMappingProvider>()
.Setup(s => s.GetSceneName(_series.SeriesId))
.Returns("Scandal");
}
[Test]
public void should_return_scene_name_when_sceneName_is_available()
{
WithSceneName();
Mocker.Resolve<SearchProvider>().GetSeriesTitle(_series).Should().Be(SCENE_NAME);
}
[Test]
public void should_return_seriesTitle_when_sceneName_is_not_available()
{
Mocker.Resolve<SearchProvider>().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<SearchProvider>().GetSeriesTitle(_series).Should().Be(expectedTitle);
}
}
}

@ -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<IndexerBase> _episodeIndexer1;
private Mock<IndexerBase> _episodeIndexer2;
private Mock<IndexerBase> _brokenIndexer;
private Mock<IndexerBase> _nullIndexer;
private List<IndexerBase> _indexers;
private Series _series;
private IList<Episode> _episodes;
[SetUp]
public void Setup()
{
var parseResults = Builder<EpisodeParseResult>.CreateListOfSize(PARSE_RESULT_COUNT)
.Build();
_series = Builder<Series>.CreateNew()
.Build();
_episodes = Builder<Episode>.CreateListOfSize(1)
.Build();
BuildIndexers(parseResults);
_indexers = new List<IndexerBase> { _episodeIndexer1.Object, _episodeIndexer2.Object };
Mocker.GetMock<IndexerProvider>()
.Setup(c => c.GetEnabledIndexers())
.Returns(_indexers);
}
private void BuildIndexers(IList<EpisodeParseResult> parseResults)
{
_episodeIndexer1 = new Mock<IndexerBase>();
_episodeIndexer1.Setup(c => c.FetchEpisode(It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>()))
.Returns(parseResults);
_episodeIndexer1.Setup(c => c.FetchDailyEpisode(It.IsAny<string>(), It.IsAny<DateTime>()))
.Returns(parseResults);
_episodeIndexer1.Setup(c => c.FetchSeason(It.IsAny<string>(), It.IsAny<int>()))
.Returns(parseResults);
_episodeIndexer1.Setup(c => c.FetchPartialSeason(It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>()))
.Returns(parseResults);
_episodeIndexer1.Setup(s => s.Name).Returns("Episode Indexer 1");
_episodeIndexer2 = new Mock<IndexerBase>();
_episodeIndexer2.Setup(c => c.FetchEpisode(It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>()))
.Returns(parseResults);
_episodeIndexer2.Setup(c => c.FetchDailyEpisode(It.IsAny<string>(), It.IsAny<DateTime>()))
.Returns(parseResults);
_episodeIndexer2.Setup(c => c.FetchSeason(It.IsAny<string>(), It.IsAny<int>()))
.Returns(parseResults);
_episodeIndexer2.Setup(c => c.FetchPartialSeason(It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>()))
.Returns(parseResults);
_episodeIndexer2.Setup(s => s.Name).Returns("Episode Indexer 2");
_brokenIndexer = new Mock<IndexerBase>();
_brokenIndexer.Setup(c => c.FetchEpisode(It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>()))
.Throws(new Exception());
_brokenIndexer.Setup(s => s.Name).Returns("Broken Indexer");
_nullIndexer = new Mock<IndexerBase>();
_nullIndexer.Setup(c => c.FetchEpisode(It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>()))
.Returns<List<EpisodeParseResult>>(null);
_nullIndexer.Setup(s => s.Name).Returns("Null Indexer");
}
private void WithTwoGoodOneBrokenIndexer()
{
_indexers = new List<IndexerBase> { _episodeIndexer1.Object, _brokenIndexer.Object, _episodeIndexer2.Object };
Mocker.GetMock<IndexerProvider>()
.Setup(c => c.GetEnabledIndexers())
.Returns(_indexers);
}
private void WithNullIndexers()
{
_indexers = new List<IndexerBase> { _nullIndexer.Object, _nullIndexer.Object };
Mocker.GetMock<IndexerProvider>()
.Setup(c => c.GetEnabledIndexers())
.Returns(_indexers);
}
private void WithSceneName()
{
Mocker.GetMock<SceneMappingProvider>()
.Setup(s => s.GetSceneName(_series.SeriesId)).Returns(SCENE_NAME);
}
private void With30Episodes()
{
_episodes = Builder<Episode>.CreateListOfSize(30)
.Build();
}
private void WithNullEpisodes()
{
_episodes = null;
}
private void VerifyFetchEpisode(Times times)
{
_episodeIndexer1.Verify(v => v.FetchEpisode(_series.Title, SEASON_NUMBER, It.IsAny<int>())
, times);
_episodeIndexer2.Verify(v => v.FetchEpisode(_series.Title, SEASON_NUMBER, It.IsAny<int>())
, times);
}
private void VerifyFetchDailyEpisode(Times times)
{
_episodeIndexer1.Verify(v => v.FetchDailyEpisode(_series.Title, It.IsAny<DateTime>())
, times);
_episodeIndexer2.Verify(v => v.FetchDailyEpisode(_series.Title, It.IsAny<DateTime>())
, times);
}
private void VerifyFetchEpisodeWithSceneName(Times times)
{
_episodeIndexer1.Verify(v => v.FetchEpisode(SCENE_NAME, SEASON_NUMBER, It.IsAny<int>())
, times);
_episodeIndexer2.Verify(v => v.FetchEpisode(SCENE_NAME, SEASON_NUMBER, It.IsAny<int>())
, times);
}
private void VerifyFetchEpisodeBrokenIndexer(Times times)
{
_brokenIndexer.Verify(v => v.FetchEpisode(It.IsAny<string>(), SEASON_NUMBER, It.IsAny<int>())
, times);
}
private void VerifyFetchPartialSeason(Times times)
{
_episodeIndexer1.Verify(v => v.FetchPartialSeason(_series.Title, SEASON_NUMBER, It.IsAny<int>())
, times);
_episodeIndexer2.Verify(v => v.FetchPartialSeason(_series.Title, SEASON_NUMBER, It.IsAny<int>())
, 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<SearchProvider>().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<SearchProvider>().PerformEpisodeSearch(_series, SEASON_NUMBER, EPISODE_NUMBER);
//Assert
result.Should().HaveCount(PARSE_RESULT_COUNT * 2);
VerifyFetchEpisode(Times.Once());
VerifyFetchEpisodeBrokenIndexer(Times.Once());
Mocker.GetMock<SceneMappingProvider>().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<SearchProvider>().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<SearchProvider>().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<SearchProvider>().PerformPartialSeasonSearch(_series, SEASON_NUMBER, new List<int>{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<SearchProvider>().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<SearchProvider>().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<SearchProvider>().PerformEpisodeSearch(_series, SEASON_NUMBER, EPISODE_NUMBER);
VerifyFetchEpisodeWithSceneName(Times.Once());
}
}
}

@ -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<Series>.CreateNew()
.With(s => s.SeriesId = 79488)
.With(s => s.Title = "30 Rock")
.Build();
_mismatchedSeries = Builder<Series>.CreateNew()
.With(s => s.SeriesId = 12345)
.With(s => s.Title = "Not 30 Rock")
.Build();
}
private void WithMatchingSeries()
{
Mocker.GetMock<SeriesProvider>()
.Setup(s => s.FindSeries(It.IsAny<string>())).Returns(_matchingSeries);
}
private void WithMisMatchedSeries()
{
Mocker.GetMock<SeriesProvider>()
.Setup(s => s.FindSeries(It.IsAny<string>())).Returns(_mismatchedSeries);
}
private void WithNullSeries()
{
Mocker.GetMock<SeriesProvider>()
.Setup(s => s.FindSeries(It.IsAny<string>())).Returns(_nullSeries);
}
private void WithSuccessfulDownload()
{
Mocker.GetMock<DownloadProvider>()
.Setup(s => s.DownloadReport(It.IsAny<EpisodeParseResult>()))
.Returns(true);
}
private void WithFailingDownload()
{
Mocker.GetMock<DownloadProvider>()
.Setup(s => s.DownloadReport(It.IsAny<EpisodeParseResult>()))
.Returns(false);
}
private void WithQualityNeeded()
{
Mocker.GetMock<AllowedDownloadSpecification>()
.Setup(s => s.IsSatisfiedBy(It.IsAny<EpisodeParseResult>()))
.Returns(ReportRejectionType.None);
}
private void WithQualityNotNeeded()
{
Mocker.GetMock<AllowedDownloadSpecification>()
.Setup(s => s.IsSatisfiedBy(It.IsAny<EpisodeParseResult>()))
.Returns(ReportRejectionType.ExistingQualityIsEqualOrBetter);
}
[Test]
public void processSearchResults_higher_quality_should_be_called_first()
{
var parseResults = Builder<EpisodeParseResult>.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<AllowedDownloadSpecification>()
.Setup(s => s.IsSatisfiedBy(It.Is<EpisodeParseResult>(d => d.Quality.Quality == QualityTypes.Bluray1080p)))
.Returns(ReportRejectionType.None);
//Act
var result = Mocker.Resolve<SearchProvider>().ProcessSearchResults(MockNotification, parseResults, _matchingSeries, DateTime.Today);
//Assert
result.Should().Contain(n => n.Success);
Mocker.GetMock<AllowedDownloadSpecification>().Verify(c => c.IsSatisfiedBy(It.IsAny<EpisodeParseResult>()),
Times.Once());
Mocker.GetMock<DownloadProvider>().Verify(c => c.DownloadReport(It.IsAny<EpisodeParseResult>()),
Times.Once());
}
[Test]
public void processSearchResults_when_quality_is_not_needed_should_check_the_rest()
{
var parseResults = Builder<EpisodeParseResult>.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<SearchProvider>().ProcessSearchResults(MockNotification, parseResults, _matchingSeries, DateTime.Today);
//Assert
result.Should().NotContain(n => n.Success);
Mocker.GetMock<AllowedDownloadSpecification>().Verify(c => c.IsSatisfiedBy(It.IsAny<EpisodeParseResult>()),
Times.Exactly(5));
Mocker.GetMock<DownloadProvider>().Verify(c => c.DownloadReport(It.IsAny<EpisodeParseResult>()),
Times.Never());
}
[Test]
public void processSearchResults_should_skip_if_series_is_null()
{
var parseResults = Builder<EpisodeParseResult>.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<SearchProvider>().ProcessSearchResults(MockNotification, parseResults, _matchingSeries, DateTime.Today);
//Assert
result.Should().NotContain(n => n.Success);
Mocker.GetMock<DownloadProvider>().Verify(c => c.DownloadReport(It.IsAny<EpisodeParseResult>()),
Times.Never());
}
[Test]
public void processSearchResults_should_skip_if_series_is_mismatched()
{
var parseResults = Builder<EpisodeParseResult>.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<SearchProvider>().ProcessSearchResults(MockNotification, parseResults, _matchingSeries, DateTime.Today);
//Assert
result.Should().NotContain(n => n.Success);
Mocker.GetMock<DownloadProvider>().Verify(c => c.DownloadReport(It.IsAny<EpisodeParseResult>()),
Times.Never());
}
[Test]
public void processSearchResults_should_return_after_successful_download()
{
var parseResults = Builder<EpisodeParseResult>.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<SearchProvider>().ProcessSearchResults(MockNotification, parseResults, _matchingSeries, DateTime.Today);
//Assert
result.Should().Contain(n => n.Success);
Mocker.GetMock<DownloadProvider>().Verify(c => c.DownloadReport(It.IsAny<EpisodeParseResult>()),
Times.Once());
}
[Test]
public void processSearchResults_should_try_next_if_download_fails()
{
var parseResults = Builder<EpisodeParseResult>.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<DownloadProvider>()
.Setup(s => s.DownloadReport(It.Is<EpisodeParseResult>(d => d.Quality.Quality == QualityTypes.DVD)))
.Returns(false);
Mocker.GetMock<DownloadProvider>()
.Setup(s => s.DownloadReport(It.Is<EpisodeParseResult>(d => d.Quality.Quality == QualityTypes.SDTV)))
.Returns(true);
//Act
var result = Mocker.Resolve<SearchProvider>().ProcessSearchResults(MockNotification, parseResults, _matchingSeries, DateTime.Today);
//Assert
result.Should().Contain(n => n.Success);
Mocker.GetMock<DownloadProvider>().Verify(c => c.DownloadReport(It.IsAny<EpisodeParseResult>()),
Times.Exactly(2));
}
[Test]
public void processSearchResults_should_skip_if_parseResult_does_not_have_airdate()
{
var parseResults = Builder<EpisodeParseResult>.CreateListOfSize(5)
.All()
.With(e => e.AirDate = null)
.With(e => e.Quality = new QualityModel(QualityTypes.HDTV720p, false))
.Build();
WithMatchingSeries();
//Act
var result = Mocker.Resolve<SearchProvider>().ProcessSearchResults(MockNotification, parseResults, _matchingSeries, DateTime.Today);
//Assert
result.Should().NotContain(n => n.Success);
Mocker.GetMock<DownloadProvider>().Verify(c => c.DownloadReport(It.IsAny<EpisodeParseResult>()),
Times.Never());
}
[Test]
public void processSearchResults_should_skip_if_parseResult_airdate_does_not_match()
{
var parseResults = Builder<EpisodeParseResult>.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<SearchProvider>().ProcessSearchResults(MockNotification, parseResults, _matchingSeries, DateTime.Today);
//Assert
result.Should().NotContain(n => n.Success);
Mocker.GetMock<DownloadProvider>().Verify(c => c.DownloadReport(It.IsAny<EpisodeParseResult>()),
Times.Never());
}
}
}

@ -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<IndexerBase> _episodeIndexer1;
private Mock<IndexerBase> _episodeIndexer2;
private Mock<IndexerBase> _brokenIndexer;
private Mock<IndexerBase> _nullIndexer;
private List<IndexerBase> _indexers;
private Series _series;
private IList<Episode> _episodes;
[SetUp]
public void Setup()
{
var parseResults = Builder<EpisodeParseResult>.CreateListOfSize(PARSE_RESULT_COUNT)
.Build();
_series = Builder<Series>.CreateNew()
.Build();
_episodes = Builder<Episode>.CreateListOfSize(1)
.Build();
BuildIndexers(parseResults);
_indexers = new List<IndexerBase> { _episodeIndexer1.Object, _episodeIndexer2.Object };
Mocker.GetMock<IndexerProvider>()
.Setup(c => c.GetEnabledIndexers())
.Returns(_indexers);
}
private void BuildIndexers(IList<EpisodeParseResult> parseResults)
{
_episodeIndexer1 = new Mock<IndexerBase>();
_episodeIndexer1.Setup(c => c.FetchEpisode(It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>()))
.Returns(parseResults);
_episodeIndexer1.Setup(c => c.FetchSeason(It.IsAny<string>(), It.IsAny<int>()))
.Returns(parseResults);
_episodeIndexer1.Setup(c => c.FetchPartialSeason(It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>()))
.Returns(parseResults);
_episodeIndexer2 = new Mock<IndexerBase>();
_episodeIndexer2.Setup(c => c.FetchEpisode(It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>()))
.Returns(parseResults);
_episodeIndexer2.Setup(c => c.FetchSeason(It.IsAny<string>(), It.IsAny<int>()))
.Returns(parseResults);
_episodeIndexer2.Setup(c => c.FetchPartialSeason(It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>()))
.Returns(parseResults);
_brokenIndexer = new Mock<IndexerBase>();
_brokenIndexer.Setup(c => c.FetchEpisode(It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>()))
.Throws(new Exception());
_nullIndexer = new Mock<IndexerBase>();
_nullIndexer.Setup(c => c.FetchEpisode(It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>()))
.Returns<List<EpisodeParseResult>>(null);
}
private void WithTwoGoodOneBrokenIndexer()
{
_indexers = new List<IndexerBase> { _episodeIndexer1.Object, _brokenIndexer.Object, _episodeIndexer2.Object };
Mocker.GetMock<IndexerProvider>()
.Setup(c => c.GetEnabledIndexers())
.Returns(_indexers);
}
private void WithNullIndexers()
{
_indexers = new List<IndexerBase> { _nullIndexer.Object, _nullIndexer.Object };
Mocker.GetMock<IndexerProvider>()
.Setup(c => c.GetEnabledIndexers())
.Returns(_indexers);
}
private void WithSceneName()
{
Mocker.GetMock<SceneMappingProvider>()
.Setup(s => s.GetSceneName(_series.SeriesId)).Returns(SCENE_NAME);
}
private void With30Episodes()
{
_episodes = Builder<Episode>.CreateListOfSize(30)
.Build();
}
private void WithNullEpisodes()
{
_episodes = null;
}
private void VerifyFetchEpisode(Times times)
{
_episodeIndexer1.Verify(v => v.FetchEpisode(_series.Title, SEASON_NUMBER, It.IsAny<int>())
, times);
_episodeIndexer2.Verify(v => v.FetchEpisode(_series.Title, SEASON_NUMBER, It.IsAny<int>())
, times);
}
private void VerifyFetchEpisodeWithSceneName(Times times)
{
_episodeIndexer1.Verify(v => v.FetchEpisode(SCENE_NAME, SEASON_NUMBER, It.IsAny<int>())
, times);
_episodeIndexer2.Verify(v => v.FetchEpisode(SCENE_NAME, SEASON_NUMBER, It.IsAny<int>())
, times);
}
private void VerifyFetchEpisodeBrokenIndexer(Times times)
{
_brokenIndexer.Verify(v => v.FetchEpisode(It.IsAny<string>(), SEASON_NUMBER, It.IsAny<int>())
, times);
}
private void VerifyFetchPartialSeason(Times times)
{
_episodeIndexer1.Verify(v => v.FetchPartialSeason(_series.Title, SEASON_NUMBER, It.IsAny<int>())
, times);
_episodeIndexer2.Verify(v => v.FetchPartialSeason(_series.Title, SEASON_NUMBER, It.IsAny<int>())
, 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<SeriesProvider>().Setup(s => s.GetSeries(1)).Returns(_series);
//Act
var result = Mocker.Resolve<SearchProvider>().SeasonSearch(MockNotification, _series.SeriesId, 1);
//Assert
result.Should().BeEmpty();
}
[Test]
public void PartialSeasonSearch_should_skip_daily_series()
{
//Setup
_series.IsDaily = true;
Mocker.GetMock<SeriesProvider>().Setup(s => s.GetSeries(1)).Returns(_series);
//Act
var result = Mocker.Resolve<SearchProvider>().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<UpgradePossibleSpecification>().Setup(s => s.IsSatisfiedBy(It.IsAny<Episode>()))
.Returns(true);
Mocker.GetMock<EpisodeProvider>().Setup(s => s.GetEpisode(episode.EpisodeId))
.Returns(episode);
//Act
var result = Mocker.Resolve<SearchProvider>().EpisodeSearch(MockNotification, episode.EpisodeId);
//Assert
result.Should().BeFalse();
ExceptionVerification.ExpectedWarns(1);
}
}
}

@ -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<Series>
.CreateNew()
.Build();
_episode = Builder<Episode>
.CreateNew()
.With(e => e.SeriesId = _series.SeriesId)
.With(e => e.Series = _series)
.Build();
_episodeParseResult = Builder<EpisodeParseResult>
.CreateNew()
.With(p => p.AirDate = _episode.AirDate)
.With(p => p.Episodes = new List<Episode> { _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<DailyEpisodeSearch>()
.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<DailyEpisodeSearch>()
.CheckReport(_series, new { Episode = _episode }, _episodeParseResult, _searchHistoryItem)
.SearchError
.Should()
.Be(ReportRejectionType.WrongEpisode);
}
[Test]
public void should_not_return_error_when_airDates_match()
{
Mocker.Resolve<DailyEpisodeSearch>()
.CheckReport(_series, new { Episode = _episode }, _episodeParseResult, _searchHistoryItem)
.SearchError
.Should()
.Be(ReportRejectionType.None);
}
}
}

@ -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<ArgumentException>(() =>
Mocker.Resolve<DailyEpisodeSearch>()
.PerformSearch(_series, new { Episode = nullEpisode }, notification));
}
[Test]
public void should_fetch_results_from_indexers()
{
WithValidIndexers();
Mocker.Resolve<DailyEpisodeSearch>()
.PerformSearch(_series, new {Episode = _episode}, notification)
.Should()
.HaveCount(20);
}
[Test]
public void should_log_error_when_fetching_from_indexer_fails()
{
WithInvalidIndexers();
Mocker.Resolve<DailyEpisodeSearch>()
.PerformSearch(_series, new { Episode = _episode }, notification)
.Should()
.HaveCount(0);
ExceptionVerification.ExpectedErrors(2);
}
}
}

@ -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<Series>
.CreateNew()
.Build();
_episode = Builder<Episode>
.CreateNew()
.With(e => e.SeriesId = _series.SeriesId)
.With(e => e.Series = _series)
.Build();
_episodeParseResult = Builder<EpisodeParseResult>
.CreateNew()
.With(p => p.SeasonNumber = 1)
.With(p => p.EpisodeNumbers = new List<int>{ _episode.EpisodeNumber })
.With(p => p.Episodes = new List<Episode> { _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<EpisodeSearch>()
.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<EpisodeSearch>()
.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<EpisodeSearch>()
.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<EpisodeSearch>()
.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<EpisodeSearch>()
.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<EpisodeSearch>()
.CheckReport(_series, new { Episode = _episode }, _episodeParseResult, _searchHistoryItem)
.SearchError
.Should()
.Be(ReportRejectionType.None);
}
}
}

@ -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<ArgumentException>(() =>
Mocker.Resolve<EpisodeSearch>()
.PerformSearch(_series, new { Episode = nullEpisode }, notification));
}
[Test]
public void should_fetch_results_from_indexers()
{
WithValidIndexers();
Mocker.Resolve<EpisodeSearch>()
.PerformSearch(_series, new {Episode = _episode}, notification)
.Should()
.HaveCount(20);
}
[Test]
public void should_log_error_when_fetching_from_indexer_fails()
{
WithInvalidIndexers();
Mocker.Resolve<EpisodeSearch>()
.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<EpisodeSearch>()
.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<EpisodeSearch>()
.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<EpisodeSearch>()
.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());
}
}
}

@ -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<Series>
.CreateNew()
.With(s => s.Title = "Hawaii Five-0")
.Build();
}
private void WithSceneMapping()
{
Mocker.GetMock<SceneMappingProvider>()
.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<TestSearch>().GetSearchTitle(_series, 5)
.Should().Be(_series.Title);
}
[Test]
public void should_return_scene_mapping_when_one_exists()
{
WithSceneMapping();
Mocker.Resolve<TestSearch>().GetSearchTitle(_series, 5)
.Should().Be("Hawaii Five 0 2010");
}
[Test]
public void should_replace_ampersand_with_and()
{
_series.Title = "Franklin & Bash";
Mocker.Resolve<TestSearch>().GetSearchTitle(_series, 5)
.Should().Be("Franklin and Bash");
}
}
}

@ -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<Episode> _episodes;
private EpisodeParseResult _episodeParseResult;
private SearchHistoryItem _searchHistoryItem;
[SetUp]
public void Setup()
{
_series = Builder<Series>
.CreateNew()
.Build();
_episodes = Builder<Episode>
.CreateListOfSize(10)
.All()
.With(e => e.SeriesId = _series.SeriesId)
.With(e => e.Series = _series)
.Build()
.ToList();
_episodeParseResult = Builder<EpisodeParseResult>
.CreateNew()
.With(p => p.SeasonNumber = 1)
.Build();
_searchHistoryItem = new SearchHistoryItem();
}
[Test]
public void should_return_wrongSeason_when_season_does_not_match()
{
Mocker.Resolve<PartialSeasonSearch>()
.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<PartialSeasonSearch>()
.CheckReport(_series, new { SeasonNumber = 1, Episodes = _episodes }, _episodeParseResult, _searchHistoryItem)
.SearchError.Should().Be(ReportRejectionType.None);
}
}
}

@ -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<ArgumentException>(() =>
Mocker.Resolve<PartialSeasonSearch>()
.PerformSearch(_series, new
{
SeasonNumber = -1,
Episodes = new List<Episode>{ new Episode() }
}, notification));
}
[Test]
public void should_throw_if_episodes_is_empty()
{
Assert.Throws<ArgumentException>(() =>
Mocker.Resolve<PartialSeasonSearch>()
.PerformSearch(_series, new { SeasonNumber = 10, Episodes = new List<Episode>() }, notification));
}
[Test]
public void should_fetch_results_from_indexers()
{
WithValidIndexers();
Mocker.Resolve<PartialSeasonSearch>()
.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<PartialSeasonSearch>()
.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<PartialSeasonSearch>()
.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());
}
}
}

@ -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<Episode> _episodes;
protected ProgressNotification notification = new ProgressNotification("Testing");
protected Mock<IndexerBase> _indexer1;
protected Mock<IndexerBase> _indexer2;
protected List<IndexerBase> _indexers;
protected IList<EpisodeParseResult> _parseResults;
[SetUp]
public void Setup()
{
_series = Builder<Series>
.CreateNew()
.Build();
_episode = Builder<Episode>
.CreateNew()
.With(e => e.SeriesId = _series.SeriesId)
.With(e => e.Series = _series)
.Build();
_episodes = Builder<Episode>
.CreateListOfSize(10)
.All()
.With(e => e.SeriesId = _series.SeriesId)
.With(e => e.Series = _series)
.Build()
.ToList();
_parseResults = Builder<EpisodeParseResult>
.CreateListOfSize(10)
.Build();
_indexer1 = new Mock<IndexerBase>();
_indexer2 = new Mock<IndexerBase>();
_indexers = new List<IndexerBase> { _indexer1.Object, _indexer2.Object };
Mocker.GetMock<IndexerProvider>()
.Setup(c => c.GetEnabledIndexers())
.Returns(_indexers);
}
protected void WithValidIndexers()
{
_indexer1.Setup(c => c.FetchEpisode(It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>()))
.Returns(_parseResults);
_indexer1.Setup(c => c.FetchDailyEpisode(It.IsAny<string>(), It.IsAny<DateTime>()))
.Returns(_parseResults);
_indexer1.Setup(c => c.FetchPartialSeason(It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>()))
.Returns(_parseResults);
_indexer2.Setup(c => c.FetchEpisode(It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>()))
.Returns(_parseResults);
_indexer2.Setup(c => c.FetchDailyEpisode(It.IsAny<string>(), It.IsAny<DateTime>()))
.Returns(_parseResults);
_indexer2.Setup(c => c.FetchPartialSeason(It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>()))
.Returns(_parseResults);
}
protected void WithInvalidIndexers()
{
_indexer1.Setup(c => c.FetchEpisode(It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>()))
.Throws(new Exception());
_indexer1.Setup(c => c.FetchDailyEpisode(It.IsAny<string>(), It.IsAny<DateTime>()))
.Throws(new Exception());
_indexer1.Setup(c => c.FetchPartialSeason(It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>()))
.Throws(new Exception());
_indexer2.Setup(c => c.FetchEpisode(It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>()))
.Throws(new Exception());
_indexer2.Setup(c => c.FetchDailyEpisode(It.IsAny<string>(), It.IsAny<DateTime>()))
.Throws(new Exception());
_indexer2.Setup(c => c.FetchPartialSeason(It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>()))
.Throws(new Exception());
_indexer1.SetupGet(c => c.Name).Returns("Indexer1");
_indexer1.SetupGet(c => c.Name).Returns("Indexer2");
}
}
}

@ -1,6 +1,7 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text;
using FizzWare.NBuilder; using FizzWare.NBuilder;
using FluentAssertions; using FluentAssertions;
using Moq; using Moq;
@ -9,23 +10,28 @@ using NzbDrone.Core.Model;
using NzbDrone.Core.Model.Notification; using NzbDrone.Core.Model.Notification;
using NzbDrone.Core.Providers; using NzbDrone.Core.Providers;
using NzbDrone.Core.Providers.DecisionEngine; using NzbDrone.Core.Providers.DecisionEngine;
using NzbDrone.Core.Providers.Search;
using NzbDrone.Core.Repository; using NzbDrone.Core.Repository;
using NzbDrone.Core.Repository.Quality; using NzbDrone.Core.Repository.Quality;
using NzbDrone.Core.Repository.Search; 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] [TestFixture]
// ReSharper disable InconsistentNaming public class ProcessResultsFixture : TestBase
public class ProcessSearchResultsFixture : CoreTest
{ {
private Series _matchingSeries = null; private Series _matchingSeries;
private Series _mismatchedSeries = null; private Series _mismatchedSeries;
private Series _nullSeries = null; private Series _nullSeries = null;
private SearchHistory _searchHistory;
private ProgressNotification _notification;
private IList<Episode> _episodes;
[SetUp] [SetUp]
public void setup() public void Setup()
{ {
_matchingSeries = Builder<Series>.CreateNew() _matchingSeries = Builder<Series>.CreateNew()
.With(s => s.SeriesId = 79488) .With(s => s.SeriesId = 79488)
@ -36,6 +42,17 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests
.With(s => s.SeriesId = 12345) .With(s => s.SeriesId = 12345)
.With(s => s.Title = "Not 30 Rock") .With(s => s.Title = "Not 30 Rock")
.Build(); .Build();
_searchHistory = new SearchHistory();
_notification = new ProgressNotification("Test");
_episodes = Builder<Episode>
.CreateListOfSize(1)
.Build();
Mocker.GetMock<EpisodeProvider>()
.Setup(s => s.GetEpisodesByParseResult(It.IsAny<EpisodeParseResult>()))
.Returns(_episodes);
} }
private void WithMatchingSeries() private void WithMatchingSeries()
@ -85,8 +102,11 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests
} }
[Test] [Test]
public void processSearchResults_higher_quality_should_be_called_first() public void should_process_higher_quality_results_first()
{ {
WithMatchingSeries();
WithSuccessfulDownload();
var parseResults = Builder<EpisodeParseResult>.CreateListOfSize(5) var parseResults = Builder<EpisodeParseResult>.CreateListOfSize(5)
.All() .All()
.With(e => e.SeasonNumber = 1) .With(e => e.SeasonNumber = 1)
@ -96,21 +116,18 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests
.Random(1) .Random(1)
.With(c => c.Quality = new QualityModel(QualityTypes.Bluray1080p, true)) .With(c => c.Quality = new QualityModel(QualityTypes.Bluray1080p, true))
.With(c => c.Age = 100) .With(c => c.Age = 100)
.Build(); .Build()
.ToList();
WithMatchingSeries();
WithSuccessfulDownload();
Mocker.GetMock<AllowedDownloadSpecification>() Mocker.GetMock<AllowedDownloadSpecification>()
.Setup(s => s.IsSatisfiedBy(It.Is<EpisodeParseResult>(d => d.Quality.Quality == QualityTypes.Bluray1080p))) .Setup(s => s.IsSatisfiedBy(It.Is<EpisodeParseResult>(d => d.Quality.Quality == QualityTypes.Bluray1080p)))
.Returns(ReportRejectionType.None); .Returns(ReportRejectionType.None);
//Act var result = Mocker.Resolve<TestSearch>().ProcessReports(_matchingSeries, new { }, parseResults, _searchHistory, _notification);
var result = Mocker.Resolve<SearchProvider>().ProcessSearchResults(new ProgressNotification("Test"), parseResults, new SearchHistory(), _matchingSeries, 1, 1);
//Assert result.SearchHistoryItems.Should().HaveCount(parseResults.Count);
result.Should().HaveCount(parseResults.Count); result.SearchHistoryItems.Should().Contain(s => s.Success);
result.Should().Contain(s => s.Success);
Mocker.GetMock<AllowedDownloadSpecification>().Verify(c => c.IsSatisfiedBy(It.IsAny<EpisodeParseResult>()), Mocker.GetMock<AllowedDownloadSpecification>().Verify(c => c.IsSatisfiedBy(It.IsAny<EpisodeParseResult>()),
Times.Once()); Times.Once());
@ -119,31 +136,29 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests
} }
[Test] [Test]
public void processSearchResults_newer_report_should_be_called_first() public void should_process_newer_reports_first()
{ {
WithMatchingSeries();
WithSuccessfulDownload();
var parseResults = Builder<EpisodeParseResult>.CreateListOfSize(5) var parseResults = Builder<EpisodeParseResult>.CreateListOfSize(5)
.All() .All()
.With(e => e.SeasonNumber = 1) .With(e => e.SeasonNumber = 1)
.With(e => e.EpisodeNumbers = new List<int> { 1 }) .With(e => e.EpisodeNumbers = new List<int> { 1 })
.With(c => c.Quality = new QualityModel(QualityTypes.Bluray1080p, true)) .With(c => c.Quality = new QualityModel(QualityTypes.Bluray1080p, true))
.With(c => c.Age = 300) .With(c => c.Age = 300)
.Build(); .Build()
.ToList();
parseResults[2].Age = 100; parseResults[2].Age = 100;
WithMatchingSeries();
WithSuccessfulDownload();
Mocker.GetMock<AllowedDownloadSpecification>() Mocker.GetMock<AllowedDownloadSpecification>()
.Setup(s => s.IsSatisfiedBy(It.IsAny<EpisodeParseResult>())).Returns(ReportRejectionType.None); .Setup(s => s.IsSatisfiedBy(It.IsAny<EpisodeParseResult>())).Returns(ReportRejectionType.None);
//Act var result = Mocker.Resolve<TestSearch>().ProcessReports(_matchingSeries, new { }, parseResults, _searchHistory, _notification);
var result = Mocker.Resolve<SearchProvider>().ProcessSearchResults(MockNotification, parseResults, new SearchHistory(), _matchingSeries, 1, 1);
//Assert result.SearchHistoryItems.Should().HaveCount(parseResults.Count);
result.Should().HaveCount(parseResults.Count); result.SearchHistoryItems.Should().Contain(s => s.Success);
result.Should().Contain(s => s.Success);
Mocker.GetMock<DownloadProvider>().Verify(c => c.DownloadReport(It.Is<EpisodeParseResult>(d => d.Age != 100)), Times.Never()); Mocker.GetMock<DownloadProvider>().Verify(c => c.DownloadReport(It.Is<EpisodeParseResult>(d => d.Age != 100)), Times.Never());
@ -151,24 +166,23 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests
} }
[Test] [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<EpisodeParseResult>.CreateListOfSize(5) var parseResults = Builder<EpisodeParseResult>.CreateListOfSize(5)
.All() .All()
.With(e => e.SeasonNumber = 1) .With(e => e.SeasonNumber = 1)
.With(e => e.EpisodeNumbers = new List<int> { 1 }) .With(e => e.EpisodeNumbers = new List<int> { 1 })
.With(c => c.Quality = new QualityModel(QualityTypes.DVD, true)) .With(c => c.Quality = new QualityModel(QualityTypes.DVD, true))
.Build(); .Build()
.ToList();
WithMatchingSeries();
WithQualityNotNeeded();
//Act var result = Mocker.Resolve<TestSearch>().ProcessReports(_matchingSeries, new { }, parseResults, _searchHistory, _notification);
var result = Mocker.Resolve<SearchProvider>().ProcessSearchResults(new ProgressNotification("Test"), parseResults, new SearchHistory(), _matchingSeries, 1, 1);
//Assert result.SearchHistoryItems.Should().HaveCount(parseResults.Count);
result.Should().HaveCount(parseResults.Count); result.SearchHistoryItems.Should().NotContain(s => s.Success);
result.Should().NotContain(s => s.Success);
Mocker.GetMock<AllowedDownloadSpecification>().Verify(c => c.IsSatisfiedBy(It.IsAny<EpisodeParseResult>()), Mocker.GetMock<AllowedDownloadSpecification>().Verify(c => c.IsSatisfiedBy(It.IsAny<EpisodeParseResult>()),
Times.Exactly(5)); Times.Exactly(5));
@ -177,99 +191,55 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests
} }
[Test] [Test]
public void processSearchResults_should_skip_if_series_is_null() public void should_should_skip_if_series_is_not_watched()
{ {
var parseResults = Builder<EpisodeParseResult>.CreateListOfSize(5) var parseResults = Builder<EpisodeParseResult>.CreateListOfSize(5)
.All() .All()
.With(e => e.SeasonNumber = 1) .With(e => e.SeasonNumber = 1)
.With(e => e.EpisodeNumbers = new List<int> { 1 }) .With(e => e.EpisodeNumbers = new List<int> { 1 })
.With(e => e.Quality = new QualityModel(QualityTypes.HDTV720p, false)) .With(e => e.Quality = new QualityModel(QualityTypes.HDTV720p, false))
.Build(); .Build()
.ToList();
WithNullSeries(); WithNullSeries();
//Act //Act
var result = Mocker.Resolve<SearchProvider>().ProcessSearchResults(new ProgressNotification("Test"), parseResults, new SearchHistory(), _matchingSeries, 1, 1); var result = Mocker.Resolve<TestSearch>().ProcessReports(_matchingSeries, new { }, parseResults, _searchHistory, _notification);
//Assert //Assert
result.Should().HaveCount(parseResults.Count); result.SearchHistoryItems.Should().HaveCount(parseResults.Count);
result.Should().NotContain(s => s.Success); result.SearchHistoryItems.Should().NotContain(s => s.Success);
Mocker.GetMock<DownloadProvider>().Verify(c => c.DownloadReport(It.IsAny<EpisodeParseResult>()), Mocker.GetMock<DownloadProvider>().Verify(c => c.DownloadReport(It.IsAny<EpisodeParseResult>()),
Times.Never()); Times.Never());
} }
[Test] [Test]
public void processSearchResults_should_skip_if_series_is_mismatched() public void should_skip_if_series_does_not_match_searched_series()
{ {
var parseResults = Builder<EpisodeParseResult>.CreateListOfSize(5) var parseResults = Builder<EpisodeParseResult>.CreateListOfSize(5)
.All() .All()
.With(e => e.SeasonNumber = 1) .With(e => e.SeasonNumber = 1)
.With(e => e.EpisodeNumbers = new List<int> { 1 }) .With(e => e.EpisodeNumbers = new List<int> { 1 })
.With(e => e.Quality = new QualityModel(QualityTypes.HDTV720p, false)) .With(e => e.Quality = new QualityModel(QualityTypes.HDTV720p, false))
.Build(); .Build()
.ToList();
WithMisMatchedSeries(); WithMisMatchedSeries();
//Act //Act
var result = Mocker.Resolve<SearchProvider>().ProcessSearchResults(new ProgressNotification("Test"), parseResults, new SearchHistory(), _matchingSeries, 1, 1); var result = Mocker.Resolve<TestSearch>().ProcessReports(_matchingSeries, new { }, parseResults, _searchHistory, _notification);
//Assert
result.Should().HaveCount(parseResults.Count);
result.Should().NotContain(s => s.Success);
Mocker.GetMock<DownloadProvider>().Verify(c => c.DownloadReport(It.IsAny<EpisodeParseResult>()),
Times.Never());
}
[Test]
public void processSearchResults_should_skip_if_season_doesnt_match()
{
var parseResults = Builder<EpisodeParseResult>.CreateListOfSize(5)
.All()
.With(e => e.SeasonNumber = 2)
.With(e => e.EpisodeNumbers = new List<int> { 1 })
.With(e => e.Quality = new QualityModel(QualityTypes.HDTV720p, false))
.Build();
WithMatchingSeries();
//Act
var result = Mocker.Resolve<SearchProvider>().ProcessSearchResults(new ProgressNotification("Test"), parseResults, new SearchHistory(), _matchingSeries, 1, 1);
//Assert
result.Should().HaveCount(parseResults.Count);
result.Should().NotContain(s => s.Success);
Mocker.GetMock<DownloadProvider>().Verify(c => c.DownloadReport(It.IsAny<EpisodeParseResult>()),
Times.Never());
}
[Test]
public void processSearchResults_should_skip_if_episodeNumber_doesnt_match()
{
var parseResults = Builder<EpisodeParseResult>.CreateListOfSize(5)
.All()
.With(e => e.SeasonNumber = 1)
.With(e => e.EpisodeNumbers = new List<int> { 2 })
.With(e => e.Quality = new QualityModel(QualityTypes.HDTV720p, false))
.Build();
WithMatchingSeries();
//Act
var result = Mocker.Resolve<SearchProvider>().ProcessSearchResults(new ProgressNotification("Test"), parseResults, new SearchHistory(), _matchingSeries, 1, 1);
//Assert //Assert
result.Should().HaveCount(parseResults.Count); result.SearchHistoryItems.Should().HaveCount(parseResults.Count);
result.Should().NotContain(s => s.Success); result.SearchHistoryItems.Should().NotContain(s => s.Success);
Mocker.GetMock<DownloadProvider>().Verify(c => c.DownloadReport(It.IsAny<EpisodeParseResult>()), Mocker.GetMock<DownloadProvider>().Verify(c => c.DownloadReport(It.IsAny<EpisodeParseResult>()),
Times.Never()); Times.Never());
} }
[Test] [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<EpisodeParseResult>.CreateListOfSize(2) var parseResults = Builder<EpisodeParseResult>.CreateListOfSize(2)
.All() .All()
@ -278,25 +248,26 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests
.With(c => c.Quality = new QualityModel(QualityTypes.DVD, true)) .With(c => c.Quality = new QualityModel(QualityTypes.DVD, true))
.TheLast(1) .TheLast(1)
.With(e => e.EpisodeNumbers = new List<int> { 1, 2, 3, 4, 5 }) .With(e => e.EpisodeNumbers = new List<int> { 1, 2, 3, 4, 5 })
.Build(); .Build()
.ToList();
WithMatchingSeries(); WithMatchingSeries();
WithQualityNeeded(); WithQualityNeeded();
WithSuccessfulDownload(); WithSuccessfulDownload();
//Act //Act
var result = Mocker.Resolve<SearchProvider>().ProcessSearchResults(new ProgressNotification("Test"), parseResults, new SearchHistory(), _matchingSeries, 1); var result = Mocker.Resolve<TestSearch>().ProcessReports(_matchingSeries, new { }, parseResults, _searchHistory, _notification);
//Assert //Assert
result.Should().HaveCount(parseResults.Count); result.SearchHistoryItems.Should().HaveCount(parseResults.Count);
result.Should().Contain(s => s.Success); result.SearchHistoryItems.Should().Contain(s => s.Success);
Mocker.GetMock<DownloadProvider>().Verify(c => c.DownloadReport(It.IsAny<EpisodeParseResult>()), Mocker.GetMock<DownloadProvider>().Verify(c => c.DownloadReport(It.IsAny<EpisodeParseResult>()),
Times.Once()); Times.Once());
} }
[Test] [Test]
public void processSearchResults_should_try_next_if_download_fails() public void should_try_next_report_if_download_fails()
{ {
var parseResults = Builder<EpisodeParseResult>.CreateListOfSize(2) var parseResults = Builder<EpisodeParseResult>.CreateListOfSize(2)
.All() .All()
@ -305,7 +276,8 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests
.With(c => c.Quality = new QualityModel(QualityTypes.DVD, true)) .With(c => c.Quality = new QualityModel(QualityTypes.DVD, true))
.TheLast(1) .TheLast(1)
.With(c => c.Quality = new QualityModel(QualityTypes.SDTV, true)) .With(c => c.Quality = new QualityModel(QualityTypes.SDTV, true))
.Build(); .Build()
.ToList();
WithMatchingSeries(); WithMatchingSeries();
WithQualityNeeded(); WithQualityNeeded();
@ -319,18 +291,18 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests
.Returns(true); .Returns(true);
//Act //Act
var result = Mocker.Resolve<SearchProvider>().ProcessSearchResults(new ProgressNotification("Test"), parseResults, new SearchHistory(), _matchingSeries, 1); var result = Mocker.Resolve<TestSearch>().ProcessReports(_matchingSeries, new { }, parseResults, _searchHistory, _notification);
//Assert //Assert
result.Should().HaveCount(parseResults.Count); result.SearchHistoryItems.Should().HaveCount(parseResults.Count);
result.Should().Contain(s => s.Success); result.SearchHistoryItems.Should().Contain(s => s.Success);
Mocker.GetMock<DownloadProvider>().Verify(c => c.DownloadReport(It.IsAny<EpisodeParseResult>()), Mocker.GetMock<DownloadProvider>().Verify(c => c.DownloadReport(It.IsAny<EpisodeParseResult>()),
Times.Exactly(2)); Times.Exactly(2));
} }
[Test] [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<EpisodeParseResult>.CreateListOfSize(5) var parseResults = Builder<EpisodeParseResult>.CreateListOfSize(5)
.All() .All()
@ -341,9 +313,8 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests
.Random(1) .Random(1)
.With(c => c.Quality = new QualityModel(QualityTypes.Bluray1080p, true)) .With(c => c.Quality = new QualityModel(QualityTypes.Bluray1080p, true))
.With(c => c.Age = 100) .With(c => c.Age = 100)
.Build(); .Build()
.ToList();
var searchHistory = new SearchHistory();
WithMatchingSeries(); WithMatchingSeries();
WithSuccessfulDownload(); WithSuccessfulDownload();
@ -353,11 +324,11 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests
.Returns(ReportRejectionType.None); .Returns(ReportRejectionType.None);
//Act //Act
var result = Mocker.Resolve<SearchProvider>().ProcessSearchResults(new ProgressNotification("Test"), parseResults, searchHistory, _matchingSeries, 1, 1); var result = Mocker.Resolve<TestSearch>().ProcessReports(_matchingSeries, new { }, parseResults, _searchHistory, _notification);
//Assert //Assert
searchHistory.Successes.Should().NotBeNull(); result.Successes.Should().NotBeNull();
searchHistory.Successes.Should().NotBeEmpty(); result.Successes.Should().NotBeEmpty();
Mocker.GetMock<AllowedDownloadSpecification>().Verify(c => c.IsSatisfiedBy(It.IsAny<EpisodeParseResult>()), Mocker.GetMock<AllowedDownloadSpecification>().Verify(c => c.IsSatisfiedBy(It.IsAny<EpisodeParseResult>()),
Times.Once()); Times.Once());
@ -365,4 +336,4 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests
Times.Once()); Times.Once());
} }
} }
} }

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

@ -1,17 +1,30 @@
using System.Linq; using System.Linq;
using System; using System;
using NLog;
using NzbDrone.Core.Model.Notification; using NzbDrone.Core.Model.Notification;
using NzbDrone.Core.Providers; using NzbDrone.Core.Providers;
using NzbDrone.Core.Providers.DecisionEngine;
using NzbDrone.Core.Providers.Search;
namespace NzbDrone.Core.Jobs namespace NzbDrone.Core.Jobs
{ {
public class EpisodeSearchJob : IJob 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() public EpisodeSearchJob()
@ -34,7 +47,37 @@ namespace NzbDrone.Core.Jobs
if (options == null || options.EpisodeId <= 0) if (options == null || options.EpisodeId <= 0)
throw new ArgumentException("options"); 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);
}
} }
} }
} }

@ -67,9 +67,9 @@ namespace NzbDrone.Core.Jobs
if (episodes.Count == successes.Count) if (episodes.Count == successes.Count)
return; 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 }); _episodeSearchJob.Start(notification, new { EpisodeId = episode.EpisodeId });
} }

@ -313,6 +313,7 @@
<Compile Include="Model\Xem\XemValues.cs" /> <Compile Include="Model\Xem\XemValues.cs" />
<Compile Include="AutofacSignalrDependencyResolver.cs" /> <Compile Include="AutofacSignalrDependencyResolver.cs" />
<Compile Include="Providers\BannerProvider.cs" /> <Compile Include="Providers\BannerProvider.cs" />
<Compile Include="Providers\SearchProvider2.cs" />
<Compile Include="Providers\DecisionEngine\AllowedReleaseGroupSpecification.cs" /> <Compile Include="Providers\DecisionEngine\AllowedReleaseGroupSpecification.cs" />
<Compile Include="Providers\DecisionEngine\CustomStartDateSpecification.cs" /> <Compile Include="Providers\DecisionEngine\CustomStartDateSpecification.cs" />
<Compile Include="Providers\DownloadClients\PneumaticProvider.cs" /> <Compile Include="Providers\DownloadClients\PneumaticProvider.cs" />
@ -328,6 +329,9 @@
<Compile Include="Providers\Metadata\Xbmc.cs" /> <Compile Include="Providers\Metadata\Xbmc.cs" />
<Compile Include="Providers\RecycleBinProvider.cs" /> <Compile Include="Providers\RecycleBinProvider.cs" />
<Compile Include="Providers\SearchHistoryProvider.cs" /> <Compile Include="Providers\SearchHistoryProvider.cs" />
<Compile Include="Providers\Search\DailyEpisodeSearch.cs" />
<Compile Include="Providers\Search\PartialSeasonSearch.cs" />
<Compile Include="Providers\Search\EpisodeSearch.cs" />
<Compile Include="Providers\Search\SearchBase.cs" /> <Compile Include="Providers\Search\SearchBase.cs" />
<Compile Include="Providers\SeasonProvider.cs" /> <Compile Include="Providers\SeasonProvider.cs" />
<Compile Include="Jobs\RecentBacklogSearchJob.cs" /> <Compile Include="Jobs\RecentBacklogSearchJob.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<EpisodeParseResult> 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<EpisodeParseResult>();
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);
}
}
}

@ -5,6 +5,7 @@ using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using NLog; using NLog;
using NzbDrone.Core.Model; using NzbDrone.Core.Model;
using NzbDrone.Core.Model.Notification;
using NzbDrone.Core.Providers.DecisionEngine; using NzbDrone.Core.Providers.DecisionEngine;
using NzbDrone.Core.Repository; using NzbDrone.Core.Repository;
using NzbDrone.Core.Repository.Search; using NzbDrone.Core.Repository.Search;
@ -15,50 +16,106 @@ namespace NzbDrone.Core.Providers.Search
{ {
private static readonly Logger logger = LogManager.GetCurrentClassLogger(); private static readonly Logger logger = LogManager.GetCurrentClassLogger();
public EpisodeSearch(EpisodeProvider episodeProvider, DownloadProvider downloadProvider, public EpisodeSearch(SeriesProvider seriesProvider, EpisodeProvider episodeProvider, DownloadProvider downloadProvider, IndexerProvider indexerProvider,
SeriesProvider seriesProvider, IndexerProvider indexerProvider, SceneMappingProvider sceneMappingProvider, AllowedDownloadSpecification allowedDownloadSpecification,
SceneMappingProvider sceneMappingProvider, UpgradePossibleSpecification upgradePossibleSpecification, SearchHistoryProvider searchHistoryProvider)
AllowedDownloadSpecification allowedDownloadSpecification, SearchHistoryProvider searchHistoryProvider) : base(seriesProvider, episodeProvider, downloadProvider, indexerProvider, sceneMappingProvider,
: base(episodeProvider, downloadProvider, seriesProvider, indexerProvider, sceneMappingProvider, allowedDownloadSpecification, searchHistoryProvider)
upgradePossibleSpecification, allowedDownloadSpecification, searchHistoryProvider)
{ {
} }
protected override List<EpisodeParseResult> Search(Series series, dynamic options) public override List<EpisodeParseResult> PerformSearch(Series series, dynamic options, ProgressNotification notification)
{ {
if (options == null) //Todo: Daily and Anime or separate them out?
throw new ArgumentNullException(options); //Todo: Epsiodes that use scene numbering
if (options.SeasonNumber < 0) if (options.Episode == null)
throw new ArgumentException("SeasonNumber is invalid"); throw new ArgumentException("Episode is invalid");
if (options.EpisodeNumber < 0) notification.CurrentMessage = "Looking for " + options.Episode;
throw new ArgumentException("EpisodeNumber is invalid");
var reports = new List<EpisodeParseResult>(); var reports = new List<EpisodeParseResult>();
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 => Parallel.ForEach(_indexerProvider.GetEnabledIndexers(), indexer =>
{ {
try try
{ {
reports.AddRange(indexer.FetchEpisode(title, options.SeasonNumber, options.EpisodeNumber)); reports.AddRange(indexer.FetchEpisode(title, seasonNumber, episodeNumber));
} }
catch (Exception e) catch (Exception e)
{ {
logger.ErrorException(String.Format("An error has occurred while searching for {0}-S{1:00}E{2:00} from: {3}", 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; return reports;
} }
protected override SearchHistoryItem CheckEpisode(Series series, List<Episode> episodes, EpisodeParseResult episodeParseResult, public override SearchHistoryItem CheckReport(Series series, dynamic options, EpisodeParseResult episodeParseResult,
SearchHistoryItem item) 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);
} }
} }
} }

@ -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<EpisodeParseResult> 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<Episode> 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<EpisodeParseResult>();
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<int> GetEpisodeNumberPrefixes(IEnumerable<int> episodeNumbers)
{
var results = new List<int>();
foreach (var i in episodeNumbers)
{
results.Add(i / 10);
}
return results.Distinct().ToList();
}
}
}

@ -13,106 +13,142 @@ namespace NzbDrone.Core.Providers.Search
{ {
public abstract class SearchBase public abstract class SearchBase
{ {
protected readonly SeriesProvider _seriesProvider;
protected readonly EpisodeProvider _episodeProvider; protected readonly EpisodeProvider _episodeProvider;
protected readonly DownloadProvider _downloadProvider; protected readonly DownloadProvider _downloadProvider;
protected readonly SeriesProvider _seriesProvider;
protected readonly IndexerProvider _indexerProvider; protected readonly IndexerProvider _indexerProvider;
protected readonly SceneMappingProvider _sceneMappingProvider; protected readonly SceneMappingProvider _sceneMappingProvider;
protected readonly UpgradePossibleSpecification _upgradePossibleSpecification;
protected readonly AllowedDownloadSpecification _allowedDownloadSpecification; protected readonly AllowedDownloadSpecification _allowedDownloadSpecification;
protected readonly SearchHistoryProvider _searchHistoryProvider; protected readonly SearchHistoryProvider _searchHistoryProvider;
private static readonly Logger logger = LogManager.GetCurrentClassLogger(); 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, IndexerProvider indexerProvider, SceneMappingProvider sceneMappingProvider,
UpgradePossibleSpecification upgradePossibleSpecification, AllowedDownloadSpecification allowedDownloadSpecification, AllowedDownloadSpecification allowedDownloadSpecification,
SearchHistoryProvider searchHistoryProvider) SearchHistoryProvider searchHistoryProvider)
{ {
_seriesProvider = seriesProvider;
_episodeProvider = episodeProvider; _episodeProvider = episodeProvider;
_downloadProvider = downloadProvider; _downloadProvider = downloadProvider;
_seriesProvider = seriesProvider;
_indexerProvider = indexerProvider; _indexerProvider = indexerProvider;
_sceneMappingProvider = sceneMappingProvider; _sceneMappingProvider = sceneMappingProvider;
_upgradePossibleSpecification = upgradePossibleSpecification;
_allowedDownloadSpecification = allowedDownloadSpecification; _allowedDownloadSpecification = allowedDownloadSpecification;
_searchHistoryProvider = searchHistoryProvider; _searchHistoryProvider = searchHistoryProvider;
} }
protected SearchBase() public abstract List<EpisodeParseResult> 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<Int32> Search(Series series, dynamic options, ProgressNotification notification)
{ {
} if (options == null)
throw new ArgumentNullException(options);
protected abstract List<EpisodeParseResult> Search(Series series, dynamic options); var searchResult = new SearchHistory
protected abstract SearchHistoryItem CheckEpisode(Series series, List<Episode> episodes, EpisodeParseResult episodeParseResult, {
SearchHistoryItem item); SearchTime = DateTime.Now,
SeriesId = series.SeriesId,
EpisodeId = options.GetType().GetProperty("Episode") != null ? options.Episode.EpisodeId : null
};
List<EpisodeParseResult> 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<Int32>();
}
protected virtual SearchHistoryItem ProcessReport(EpisodeParseResult episodeParseResult, Series series, List<Episode> episodes) public virtual SearchHistory ProcessReports(Series series, dynamic options, List<EpisodeParseResult> episodeParseResults,
SearchHistory searchResult, ProgressNotification notification)
{ {
try var items = new List<SearchHistoryItem>();
searchResult.Successes = new List<Int32>();
foreach(var episodeParseResult in episodeParseResults
.OrderByDescending(c => c.Quality)
.ThenBy(c => c.EpisodeNumbers.MinOrDefault())
.ThenBy(c => c.Age))
{ {
var item = new SearchHistoryItem try
{
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)
{ {
item.SearchError = ReportRejectionType.WrongSeries; var item = new SearchHistoryItem
return item; {
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));
}
} }
catch(Exception e)
//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)
{ {
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); logger.Debug("Found '{0}'. Adding to download queue.", episodeParseResult);
try try
{ {
if (_downloadProvider.DownloadReport(episodeParseResult)) if (_downloadProvider.DownloadReport(episodeParseResult))
{ {
notification.CurrentMessage = notification.CurrentMessage = String.Format("{0} Added to download queue", episodeParseResult);
String.Format("{0} - {1} {2} Added to download queue",
episodeParseResult.Series.Title, episodeParseResult.AirDate.Value.ToShortDateString(), episodeParseResult.Quality);
item.Success = true; item.Success = true;
return true;
} }
else
{ item.SearchError = ReportRejectionType.DownloadClientFailure;
item.SearchError = ReportRejectionType.DownloadClientFailure;
}
} }
catch (Exception e) catch (Exception e)
{ {
@ -121,12 +157,13 @@ namespace NzbDrone.Core.Providers.Search
item.SearchError = ReportRejectionType.DownloadClientFailure; 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 per season lookup (used for anime)
//Todo: Add support for multiple names
var title = _sceneMappingProvider.GetSceneName(series.SeriesId); var title = _sceneMappingProvider.GetSceneName(series.SeriesId);
if (String.IsNullOrWhiteSpace(title)) if (String.IsNullOrWhiteSpace(title))

@ -7,6 +7,7 @@ using NLog;
using NzbDrone.Core.Model; using NzbDrone.Core.Model;
using NzbDrone.Core.Model.Notification; using NzbDrone.Core.Model.Notification;
using NzbDrone.Core.Providers.DecisionEngine; using NzbDrone.Core.Providers.DecisionEngine;
using NzbDrone.Core.Providers.Search;
using NzbDrone.Core.Repository; using NzbDrone.Core.Repository;
using NzbDrone.Core.Repository.Search; using NzbDrone.Core.Repository.Search;
@ -14,30 +15,18 @@ namespace NzbDrone.Core.Providers
{ {
public class SearchProvider public class SearchProvider
{ {
private readonly EpisodeProvider _episodeProvider;
private readonly DownloadProvider _downloadProvider;
private readonly SeriesProvider _seriesProvider; private readonly SeriesProvider _seriesProvider;
private readonly IndexerProvider _indexerProvider; private readonly EpisodeProvider _episodeProvider;
private readonly SceneMappingProvider _sceneMappingProvider; private readonly PartialSeasonSearch _partialSeasonSearch;
private readonly UpgradePossibleSpecification _upgradePossibleSpecification;
private readonly AllowedDownloadSpecification _allowedDownloadSpecification;
private readonly SearchHistoryProvider _searchHistoryProvider;
private static readonly Logger logger = LogManager.GetCurrentClassLogger(); private static readonly Logger logger = LogManager.GetCurrentClassLogger();
public SearchProvider(EpisodeProvider episodeProvider, DownloadProvider downloadProvider, SeriesProvider seriesProvider, public SearchProvider(SeriesProvider seriesProvider, EpisodeProvider episodeProvider,
IndexerProvider indexerProvider, SceneMappingProvider sceneMappingProvider, PartialSeasonSearch partialSeasonSearch)
UpgradePossibleSpecification upgradePossibleSpecification, AllowedDownloadSpecification allowedDownloadSpecification,
SearchHistoryProvider searchHistoryProvider)
{ {
_episodeProvider = episodeProvider;
_downloadProvider = downloadProvider;
_seriesProvider = seriesProvider; _seriesProvider = seriesProvider;
_indexerProvider = indexerProvider; _episodeProvider = episodeProvider;
_sceneMappingProvider = sceneMappingProvider; _partialSeasonSearch = partialSeasonSearch;
_upgradePossibleSpecification = upgradePossibleSpecification;
_allowedDownloadSpecification = allowedDownloadSpecification;
_searchHistoryProvider = searchHistoryProvider;
} }
public SearchProvider() public SearchProvider()
@ -46,13 +35,6 @@ namespace NzbDrone.Core.Providers
public virtual List<int> SeasonSearch(ProgressNotification notification, int seriesId, int seasonNumber) public virtual List<int> SeasonSearch(ProgressNotification notification, int seriesId, int seasonNumber)
{ {
var searchResult = new SearchHistory
{
SearchTime = DateTime.Now,
SeriesId = seriesId,
SeasonNumber = seasonNumber
};
var series = _seriesProvider.GetSeries(seriesId); var series = _seriesProvider.GetSeries(seriesId);
if (series == null) if (series == null)
@ -76,59 +58,12 @@ namespace NzbDrone.Core.Providers
return new List<int>(); return new List<int>();
} }
notification.CurrentMessage = String.Format("Searching for {0} Season {1}", series.Title, seasonNumber); //Todo: Support full season searching
return new List<int>();
List<EpisodeParseResult> 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<int>();
}
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<int>();
notification.CurrentMessage = "Processing search results";
searchResult.SearchHistoryItems = ProcessSearchResults(notification, reports, searchResult, series, seasonNumber);
_searchHistoryProvider.Add(searchResult);
return searchResult.Successes;
} }
public virtual List<int> PartialSeasonSearch(ProgressNotification notification, int seriesId, int seasonNumber) public virtual List<int> PartialSeasonSearch(ProgressNotification notification, int seriesId, int seasonNumber)
{ {
var searchResult = new SearchHistory
{
SearchTime = DateTime.Now,
SeriesId = seriesId,
SeasonNumber = seasonNumber
};
var series = _seriesProvider.GetSeries(seriesId); var series = _seriesProvider.GetSeries(seriesId);
if (series == null) if (series == null)
@ -143,438 +78,15 @@ namespace NzbDrone.Core.Providers
return new List<int>(); return new List<int>();
} }
notification.CurrentMessage = String.Format("Searching for {0} Season {1}", series.Title, seasonNumber);
var episodes = _episodeProvider.GetEpisodesBySeason(seriesId, seasonNumber); var episodes = _episodeProvider.GetEpisodesBySeason(seriesId, seasonNumber);
List<EpisodeParseResult> reports; if (episodes == null || episodes.Count == 0)
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<int>();
}
reports = PerformPartialSeasonSearch(series, sceneSeasonNumbers.First(), GetEpisodeNumberPrefixes(sceneEpisodeNumbers));
}
else
{ {
reports = PerformPartialSeasonSearch(series, seasonNumber, GetEpisodeNumberPrefixes(episodes.Select(e => e.EpisodeNumber))); logger.Warn("No episodes in database found for series: {0} Season: {1}.", seriesId, seasonNumber);
}
logger.Debug("Finished searching all indexers. Total {0}", reports.Count);
if (reports.Count == 0)
return new List<int>(); return new List<int>();
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<EpisodeParseResult> 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<SearchHistoryItem> ProcessSearchResults(ProgressNotification notification, IEnumerable<EpisodeParseResult> reports, SearchHistory searchResult, Series series, int seasonNumber, int? episodeNumber = null)
{
var items = new List<SearchHistoryItem>();
searchResult.Successes = new List<int>();
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<SearchHistoryItem> ProcessSearchResults(ProgressNotification notification, IEnumerable<EpisodeParseResult> reports, Series series, DateTime airDate)
{
var items = new List<SearchHistoryItem>();
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<int> GetEpisodeNumberPrefixes(IEnumerable<int> episodeNumbers)
{
var results = new List<int>();
foreach (var i in episodeNumbers)
{
results.Add(i / 10);
}
return results.Distinct().ToList();
}
public List<EpisodeParseResult> PerformEpisodeSearch(Series series, int seasonNumber, int episodeNumber)
{
var reports = new List<EpisodeParseResult>();
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<EpisodeParseResult> PerformDailyEpisodeSearch(Series series, Episode episode)
{
var reports = new List<EpisodeParseResult>();
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<EpisodeParseResult> PerformPartialSeasonSearch(Series series, int seasonNumber, List<int> prefixes)
{
var reports = new List<EpisodeParseResult>();
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<EpisodeParseResult> PerformSeasonSearch(Series series, int seasonNumber)
{
var reports = new List<EpisodeParseResult>();
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);
} }
} }
} }

@ -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<int> 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<int>();
}
if (series.IsDaily)
{
logger.Trace("Daily series detected, skipping season search: {0}", series.Title);
return new List<int>();
}
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<int>();
}
notification.CurrentMessage = String.Format("Searching for {0} Season {1}", series.Title, seasonNumber);
List<EpisodeParseResult> 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<int>();
}
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<int>();
notification.CurrentMessage = "Processing search results";
searchResult.SearchHistoryItems = ProcessSearchResults(notification, reports, searchResult, series, seasonNumber);
_searchHistoryProvider.Add(searchResult);
return searchResult.Successes;
}
public virtual List<int> 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<int>();
}
if (series.IsDaily)
{
logger.Trace("Daily series detected, skipping season search: {0}", series.Title);
return new List<int>();
}
notification.CurrentMessage = String.Format("Searching for {0} Season {1}", series.Title, seasonNumber);
var episodes = _episodeProvider.GetEpisodesBySeason(seriesId, seasonNumber);
List<EpisodeParseResult> 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<int>();
}
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<int>();
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<EpisodeParseResult> 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<SearchHistoryItem> ProcessSearchResults(ProgressNotification notification, IEnumerable<EpisodeParseResult> reports, SearchHistory searchResult, Series series, int seasonNumber, int? episodeNumber = null)
{
var items = new List<SearchHistoryItem>();
searchResult.Successes = new List<int>();
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<SearchHistoryItem> ProcessSearchResults(ProgressNotification notification, IEnumerable<EpisodeParseResult> reports, Series series, DateTime airDate)
{
var items = new List<SearchHistoryItem>();
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<int> GetEpisodeNumberPrefixes(IEnumerable<int> episodeNumbers)
{
var results = new List<int>();
foreach (var i in episodeNumbers)
{
results.Add(i / 10);
}
return results.Distinct().ToList();
}
public List<EpisodeParseResult> PerformEpisodeSearch(Series series, int seasonNumber, int episodeNumber)
{
var reports = new List<EpisodeParseResult>();
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<EpisodeParseResult> PerformDailyEpisodeSearch(Series series, Episode episode)
{
var reports = new List<EpisodeParseResult>();
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<EpisodeParseResult> PerformPartialSeasonSearch(Series series, int seasonNumber, List<int> prefixes)
{
var reports = new List<EpisodeParseResult>();
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<EpisodeParseResult> PerformSeasonSearch(Series series, int seasonNumber)
{
var reports = new List<EpisodeParseResult>();
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;
}
}
}
Loading…
Cancel
Save