From 485f618e0261a34750a6443fd688313aefd1db6d Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 27 Aug 2011 22:45:36 -0700 Subject: [PATCH] Full Season searching added (Single NZB). SearchProvider added to perform both Season and Episode searching, triggered via jobs. Tests added for season searching. --- NzbDrone.Core.Test/IndexerProviderTest.cs | 9 +- NzbDrone.Core.Test/NzbDrone.Core.Test.csproj | 5 +- NzbDrone.Core.Test/SearchJobTest.cs | 37 +++ ...bTest.cs => SearchProviderTest_Episode.cs} | 44 +--- .../SearchProviderTest_Season.cs | 223 +++++++++++++++++ NzbDrone.Core.Test/SeasonSearchJobTest.cs | 105 -------- NzbDrone.Core/Model/Search/SearchModel.cs | 16 ++ NzbDrone.Core/Model/Search/SearchType.cs | 14 ++ NzbDrone.Core/NzbDrone.Core.csproj | 3 + .../Providers/Indexer/IndexerBase.cs | 42 +++- NzbDrone.Core/Providers/Indexer/Newzbin.cs | 20 +- NzbDrone.Core/Providers/Indexer/NzbMatrix.cs | 14 +- NzbDrone.Core/Providers/Indexer/NzbsOrg.cs | 15 +- NzbDrone.Core/Providers/Indexer/NzbsRUs.cs | 3 +- .../Providers/Jobs/EpisodeSearchJob.cs | 104 +------- .../Providers/Jobs/SeasonSearchJob.cs | 14 +- NzbDrone.Core/Providers/SearchProvider.cs | 236 ++++++++++++++++++ NzbDrone.Web/Views/Series/Details.cshtml | 2 +- 18 files changed, 644 insertions(+), 262 deletions(-) create mode 100644 NzbDrone.Core.Test/SearchJobTest.cs rename NzbDrone.Core.Test/{EpisodeSearchJobTest.cs => SearchProviderTest_Episode.cs} (87%) create mode 100644 NzbDrone.Core.Test/SearchProviderTest_Season.cs delete mode 100644 NzbDrone.Core.Test/SeasonSearchJobTest.cs create mode 100644 NzbDrone.Core/Model/Search/SearchModel.cs create mode 100644 NzbDrone.Core/Model/Search/SearchType.cs create mode 100644 NzbDrone.Core/Providers/SearchProvider.cs diff --git a/NzbDrone.Core.Test/IndexerProviderTest.cs b/NzbDrone.Core.Test/IndexerProviderTest.cs index a586ab5f7..b18d92a9c 100644 --- a/NzbDrone.Core.Test/IndexerProviderTest.cs +++ b/NzbDrone.Core.Test/IndexerProviderTest.cs @@ -7,6 +7,7 @@ using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.Model; +using NzbDrone.Core.Model.Search; using NzbDrone.Core.Providers; using NzbDrone.Core.Providers.Core; using NzbDrone.Core.Providers.Indexer; @@ -73,7 +74,7 @@ protected override string[] Urls get { return new[] { "www.google.com" }; } } - protected override IList GetSearchUrls(string seriesTitle, int seasonNumber, int episodeNumber) + protected override IList GetSearchUrls(SearchModel searchModel) { throw new NotImplementedException(); } @@ -112,7 +113,7 @@ protected override string[] Urls get { return new[] { "http://rss.nzbmatrix.com/rss.php?cat=TV" }; } } - protected override IList GetSearchUrls(string seriesTitle, int seasonNumber, int episodeNumber) + protected override IList GetSearchUrls(SearchModel searchModel) { throw new NotImplementedException(); } @@ -135,14 +136,12 @@ public override string Name get { return "Custom parser"; } } - - protected override string[] Urls { get { return new[] { "http://www.google.com" }; } } - protected override IList GetSearchUrls(string seriesTitle, int seasonNumber, int episodeNumber) + protected override IList GetSearchUrls(SearchModel searchModel) { throw new NotImplementedException(); } diff --git a/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index b5c5c8787..b537afc25 100644 --- a/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -88,8 +88,9 @@ + - + @@ -104,7 +105,7 @@ - + diff --git a/NzbDrone.Core.Test/SearchJobTest.cs b/NzbDrone.Core.Test/SearchJobTest.cs new file mode 100644 index 000000000..215dcd399 --- /dev/null +++ b/NzbDrone.Core.Test/SearchJobTest.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using AutoMoq; +using Moq; +using NUnit.Framework; +using NzbDrone.Core.Model.Notification; +using NzbDrone.Core.Providers; +using NzbDrone.Core.Providers.Jobs; + +namespace NzbDrone.Core.Test +{ + [TestFixture] + public class SearchJobTest + { + [TestCase(0)] + [TestCase(-1)] + [TestCase(-100)] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void start_target_id_less_than_0_throws_exception(int target) + { + var mocker = new AutoMoqer(MockBehavior.Strict); + mocker.Resolve().Start(new ProgressNotification("Test"), target, 0); + } + + [TestCase(0)] + [TestCase(-1)] + [TestCase(-100)] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void start_secondary_target_id_less_than_0_throws_exception(int target) + { + var mocker = new AutoMoqer(MockBehavior.Strict); + mocker.Resolve().Start(new ProgressNotification("Test"), 0, target); + } + } +} diff --git a/NzbDrone.Core.Test/EpisodeSearchJobTest.cs b/NzbDrone.Core.Test/SearchProviderTest_Episode.cs similarity index 87% rename from NzbDrone.Core.Test/EpisodeSearchJobTest.cs rename to NzbDrone.Core.Test/SearchProviderTest_Episode.cs index e69784e1a..2ec34e751 100644 --- a/NzbDrone.Core.Test/EpisodeSearchJobTest.cs +++ b/NzbDrone.Core.Test/SearchProviderTest_Episode.cs @@ -19,7 +19,7 @@ namespace NzbDrone.Core.Test { [TestFixture] // ReSharper disable InconsistentNaming - public class EpisodeSearchJobTest : TestBase + public class SearchProviderTest_Episode : TestBase { [Test] public void processResults_ParseResult_should_return_after_match() @@ -40,7 +40,7 @@ public void processResults_ParseResult_should_return_after_match() //Act - mocker.Resolve().ProcessResults(new ProgressNotification("Test"), episode, parseResults); + mocker.Resolve().ProcessEpisodeSearchResults(new ProgressNotification("Test"), episode, parseResults); //Assert mocker.VerifyAllMocks(); @@ -76,7 +76,7 @@ public void processResults_higher_quality_should_be_called_first() .Returns(true); //Act - mocker.Resolve().ProcessResults(new ProgressNotification("Test"), episode, parseResults); + mocker.Resolve().ProcessEpisodeSearchResults(new ProgressNotification("Test"), episode, parseResults); //Assert mocker.VerifyAllMocks(); @@ -109,7 +109,7 @@ public void processResults_when_same_quality_proper_should_be_called_first() //Act - mocker.Resolve().ProcessResults(new ProgressNotification("Test"), episode, parseResults); + mocker.Resolve().ProcessEpisodeSearchResults(new ProgressNotification("Test"), episode, parseResults); //Assert mocker.VerifyAllMocks(); @@ -134,7 +134,7 @@ public void processResults_when_not_needed_should_check_the_rest() .Setup(c => c.IsQualityNeeded(It.IsAny())).Returns(false); //Act - mocker.Resolve().ProcessResults(new ProgressNotification("Test"), episode, parseResults); + mocker.Resolve().ProcessEpisodeSearchResults(new ProgressNotification("Test"), episode, parseResults); //Assert mocker.VerifyAllMocks(); @@ -158,7 +158,7 @@ public void processResults_failed_IsNeeded_should_check_the_rest() .Setup(c => c.IsQualityNeeded(It.IsAny())).Throws(new Exception()); //Act - mocker.Resolve().ProcessResults(new ProgressNotification("Test"), episode, parseResults); + mocker.Resolve().ProcessEpisodeSearchResults(new ProgressNotification("Test"), episode, parseResults); //Assert mocker.VerifyAllMocks(); @@ -185,7 +185,7 @@ public void processResults_failed_download_should_not_check_the_rest() .Setup(c => c.DownloadReport(It.IsAny())).Throws(new Exception()); //Act - mocker.Resolve().ProcessResults(new ProgressNotification("Test"), episode, parseResults); + mocker.Resolve().ProcessEpisodeSearchResults(new ProgressNotification("Test"), episode, parseResults); //Assert mocker.VerifyAllMocks(); @@ -200,25 +200,7 @@ public void processResults_failed_download_should_not_check_the_rest() - [TestCase(0)] - [TestCase(-1)] - [TestCase(-100)] - [ExpectedException(typeof(ArgumentOutOfRangeException))] - public void start_target_id_less_than_0_throws_exception(int target) - { - var mocker = new AutoMoqer(MockBehavior.Strict); - mocker.Resolve().Start(new ProgressNotification("Test"), target, 0); - } - - [TestCase(0)] - [TestCase(-1)] - [TestCase(-100)] - [ExpectedException(typeof(ArgumentOutOfRangeException))] - public void start_secondary_target_id_less_than_0_throws_exception(int target) - { - var mocker = new AutoMoqer(MockBehavior.Strict); - mocker.Resolve().Start(new ProgressNotification("Test"), 0, target); - } + [Test] public void start_should_search_all_providers() @@ -237,12 +219,10 @@ public void start_should_search_all_providers() .Setup(c => c.GetEpisode(episode.EpisodeId)) .Returns(episode); - var indexer1 = new Mock(); indexer1.Setup(c => c.FetchEpisode(episode.Series.Title, episode.SeasonNumber, episode.EpisodeNumber)) .Returns(parseResults).Verifiable(); - var indexer2 = new Mock(); indexer2.Setup(c => c.FetchEpisode(episode.Series.Title, episode.SeasonNumber, episode.EpisodeNumber)) .Returns(parseResults).Verifiable(); @@ -260,7 +240,7 @@ public void start_should_search_all_providers() .Setup(s => s.GetSceneName(It.IsAny())).Returns(""); //Act - mocker.Resolve().Start(new ProgressNotification("Test"), episode.EpisodeId, 0); + mocker.Resolve().EpisodeSearch(new ProgressNotification("Test"), episode.EpisodeId); //Assert @@ -311,7 +291,7 @@ public void start_should_use_scene_name_to_search() .Setup(s => s.GetSceneName(71256)).Returns("The Daily Show"); //Act - mocker.Resolve().Start(new ProgressNotification("Test"), episode.EpisodeId, 0); + mocker.Resolve().EpisodeSearch(new ProgressNotification("Test"), episode.EpisodeId); //Assert @@ -368,7 +348,7 @@ public void start_failed_indexer_should_not_break_job() .Setup(s => s.GetSceneName(It.IsAny())).Returns(""); //Act - mocker.Resolve().Start(new ProgressNotification("Test"), episode.EpisodeId, 0); + mocker.Resolve().EpisodeSearch(new ProgressNotification("Test"), episode.EpisodeId); //Assert @@ -394,7 +374,7 @@ public void start_no_episode_found_should_return_with_error_logged() .Returns(null); //Act - mocker.Resolve().Start(new ProgressNotification("Test"), 12, 0); + mocker.Resolve().EpisodeSearch(new ProgressNotification("Test"), 12); //Assert diff --git a/NzbDrone.Core.Test/SearchProviderTest_Season.cs b/NzbDrone.Core.Test/SearchProviderTest_Season.cs new file mode 100644 index 000000000..34789433e --- /dev/null +++ b/NzbDrone.Core.Test/SearchProviderTest_Season.cs @@ -0,0 +1,223 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using AutoMoq; +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.Jobs; +using NzbDrone.Core.Repository; +using NzbDrone.Core.Repository.Quality; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test +{ + [TestFixture] + // ReSharper disable InconsistentNaming + public class SearchProviderTest_Season : TestBase + { + [Test] + public void SeasonSearch_season_success() + { + var series = Builder.CreateNew() + .With(s => s.SeriesId = 1) + .With(s => s.Title = "Title1") + .Build(); + + var episodes = Builder.CreateListOfSize(5) + .WhereAll() + .Have(e => e.Series = series) + .Have(e => e.SeriesId = 1) + .Have(e => e.SeasonNumber = 1) + .Have(e => e.Ignored = false) + .Build(); + + var parseResults = Builder.CreateListOfSize(4) + .WhereTheFirst(1) + .Has(p => p.CleanTitle = "title") + .Has(p => p.SeasonNumber = 1) + .Has(p => p.FullSeason = true) + .Has(p => p.EpisodeNumbers = null) + .Build(); + + var mocker = new AutoMoqer(MockBehavior.Strict); + + var notification = new ProgressNotification("Season Search"); + + var indexer1 = new Mock(); + indexer1.Setup(c => c.FetchSeason(episodes[0].Series.Title, episodes[0].SeasonNumber)) + .Returns(parseResults).Verifiable(); + + var indexer2 = new Mock(); + indexer2.Setup(c => c.FetchSeason(episodes[0].Series.Title, episodes[0].SeasonNumber)) + .Returns(parseResults).Verifiable(); + + var indexers = new List { indexer1.Object, indexer2.Object }; + + mocker.GetMock() + .Setup(c => c.GetEnabledIndexers()) + .Returns(indexers); + + mocker.GetMock() + .Setup(c => c.GetSeries(1)).Returns(series); + + mocker.GetMock() + .Setup(c => c.GetEpisodesBySeason(1, 1)).Returns(episodes); + + mocker.GetMock() + .Setup(s => s.GetSceneName(1)).Returns(String.Empty); + + mocker.GetMock() + .Setup(s => s.IsQualityNeeded(It.IsAny())).Returns(true); + + mocker.GetMock() + .Setup(s => s.DownloadReport(It.IsAny())).Returns(true); + + //Act + mocker.Resolve().SeasonSearch(notification, 1, 1); + + //Assert + mocker.VerifyAllMocks(); + mocker.GetMock().Verify(c => c.Start(notification, It.IsAny(), 0), + Times.Never()); + } + + [Test] + public void SeasonSearch_season_failure() + { + var series = Builder.CreateNew() + .With(s => s.SeriesId = 1) + .With(s => s.Title = "Title1") + .Build(); + + var episodes = Builder.CreateListOfSize(5) + .WhereAll() + .Have(e => e.Series = series) + .Have(e => e.SeriesId = 1) + .Have(e => e.SeasonNumber = 1) + .Have(e => e.Ignored = false) + .Build(); + + var parseResults = Builder.CreateListOfSize(4) + .Build(); + + var mocker = new AutoMoqer(MockBehavior.Strict); + + var notification = new ProgressNotification("Season Search"); + + var indexer1 = new Mock(); + indexer1.Setup(c => c.FetchSeason(episodes[0].Series.Title, episodes[0].SeasonNumber)) + .Returns(parseResults).Verifiable(); + + var indexer2 = new Mock(); + indexer2.Setup(c => c.FetchSeason(episodes[0].Series.Title, episodes[0].SeasonNumber)) + .Returns(parseResults).Verifiable(); + + var indexers = new List { indexer1.Object, indexer2.Object }; + + mocker.GetMock() + .Setup(c => c.GetEnabledIndexers()) + .Returns(indexers); + + mocker.GetMock() + .Setup(c => c.GetSeries(1)).Returns(series); + + mocker.GetMock() + .Setup(c => c.GetEpisodesBySeason(1, 1)).Returns(episodes); + + mocker.GetMock() + .Setup(s => s.GetSceneName(1)).Returns(String.Empty); + + //mocker.GetMock() + // .Setup(s => s.IsQualityNeeded(It.IsAny())).Returns(true); + + //mocker.GetMock() + // .Setup(s => s.DownloadReport(It.IsAny())).Returns(true); + + //Act + mocker.Resolve().SeasonSearch(notification, 1, 1); + + //Assert + ExceptionVerification.ExcpectedWarns(1); + mocker.VerifyAllMocks(); + mocker.GetMock().Verify(c => c.Start(notification, It.IsAny(), 0), + Times.Never()); + + } + + [Test] + public void ProcessSeasonSearchResults_success() + { + var series = Builder.CreateNew() + .With(s => s.SeriesId = 1) + .With(s => s.Title = "Title1") + .Build(); + + var parseResults = Builder.CreateListOfSize(4) + .WhereTheFirst(1) + .Has(p => p.CleanTitle = "title") + .Has(p => p.SeasonNumber = 1) + .Has(p => p.FullSeason = true) + .Has(p => p.EpisodeNumbers = null) + .Build(); + + var mocker = new AutoMoqer(MockBehavior.Strict); + + var notification = new ProgressNotification("Season Search"); + + mocker.GetMock() + .Setup(s => s.IsQualityNeeded(It.IsAny())).Returns(true); + + mocker.GetMock() + .Setup(s => s.DownloadReport(It.IsAny())).Returns(true); + + //Act + mocker.Resolve().ProcessSeasonSearchResults(notification, series, 1, parseResults); + + //Assert + mocker.VerifyAllMocks(); + mocker.GetMock().Verify(c => c.Start(notification, It.IsAny(), 0), + Times.Never()); + + } + + [Test] + public void ProcessSeasonSearchResults_failure() + { + var series = Builder.CreateNew() + .With(s => s.SeriesId = 1) + .With(s => s.Title = "Title1") + .Build(); + + var parseResults = Builder.CreateListOfSize(4) + .WhereTheFirst(1) + .Has(p => p.CleanTitle = "title") + .Has(p => p.SeasonNumber = 1) + .Has(p => p.FullSeason = true) + .Has(p => p.EpisodeNumbers = null) + .Build(); + + var mocker = new AutoMoqer(MockBehavior.Strict); + + var notification = new ProgressNotification("Season Search"); + + mocker.GetMock() + .Setup(s => s.IsQualityNeeded(It.IsAny())).Returns(false); + + //Act + mocker.Resolve().ProcessSeasonSearchResults(notification, series, 1, parseResults); + + //Assert + ExceptionVerification.ExcpectedWarns(1); + mocker.VerifyAllMocks(); + mocker.GetMock().Verify(c => c.Start(notification, It.IsAny(), 0), + Times.Never()); + + } + } +} \ No newline at end of file diff --git a/NzbDrone.Core.Test/SeasonSearchJobTest.cs b/NzbDrone.Core.Test/SeasonSearchJobTest.cs deleted file mode 100644 index 76e51255b..000000000 --- a/NzbDrone.Core.Test/SeasonSearchJobTest.cs +++ /dev/null @@ -1,105 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using AutoMoq; -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.Jobs; -using NzbDrone.Core.Repository; -using NzbDrone.Core.Repository.Quality; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test -{ - [TestFixture] - // ReSharper disable InconsistentNaming - public class SeasonSearchJobTest : TestBase - { - [Test] - public void SeasonSearch_success() - { - var episodes = Builder.CreateListOfSize(5) - .WhereAll() - .Have(e => e.SeriesId = 1) - .Have(e => e.SeasonNumber = 1) - .Have(e => e.Ignored = false) - .Build(); - - var mocker = new AutoMoqer(MockBehavior.Strict); - - var notification = new ProgressNotification("Season Search"); - - mocker.GetMock() - .Setup(c => c.GetEpisodesBySeason(1, 1)).Returns(episodes); - - mocker.GetMock() - .Setup(c => c.Start(notification, It.IsAny(), 0)).Verifiable(); - - //Act - mocker.Resolve().Start(notification, 1, 1); - - //Assert - mocker.VerifyAllMocks(); - mocker.GetMock().Verify(c => c.Start(notification, It.IsAny(), 0), - Times.Exactly(episodes.Count)); - } - - [Test] - public void SeasonSearch_no_episodes() - { - var mocker = new AutoMoqer(MockBehavior.Strict); - var notification = new ProgressNotification("Season Search"); - List nullList = null; - - mocker.GetMock() - .Setup(c => c.GetEpisodesBySeason(1, 1)).Returns(nullList); - - //Act - mocker.Resolve().Start(notification, 1, 1); - - //Assert - mocker.VerifyAllMocks(); - mocker.GetMock().Verify(c => c.Start(notification, It.IsAny(), 0), - Times.Never()); - ExceptionVerification.ExcpectedWarns(1); - } - - [Test] - public void SeasonSearch_skip_ignored() - { - var episodes = Builder.CreateListOfSize(10) - .WhereAll() - .Have(e => e.SeriesId = 1) - .Have(e => e.SeasonNumber = 1) - .WhereTheFirst(5) - .Have(e => e.Ignored = false) - .AndTheRemaining() - .Have(e => e.Ignored = true) - .Build(); - - var mocker = new AutoMoqer(MockBehavior.Strict); - - var notification = new ProgressNotification("Season Search"); - - mocker.GetMock() - .Setup(c => c.GetEpisodesBySeason(1, 1)).Returns(episodes); - - mocker.GetMock() - .Setup(c => c.Start(notification, It.IsAny(), 0)).Verifiable(); - - //Act - mocker.Resolve().Start(notification, 1, 1); - - //Assert - mocker.VerifyAllMocks(); - mocker.GetMock().Verify(c => c.Start(notification, It.IsAny(), 0), - Times.Exactly(5)); - } - } -} \ No newline at end of file diff --git a/NzbDrone.Core/Model/Search/SearchModel.cs b/NzbDrone.Core/Model/Search/SearchModel.cs new file mode 100644 index 000000000..70ef93960 --- /dev/null +++ b/NzbDrone.Core/Model/Search/SearchModel.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Model.Search +{ + public class SearchModel + { + public string SeriesTitle { get; set; } + public int EpisodeNumber { get; set; } + public int SeasonNumber { get; set; } + public DateTime AirDate { get; set; } + public SearchType SearchType { get; set; } + } +} diff --git a/NzbDrone.Core/Model/Search/SearchType.cs b/NzbDrone.Core/Model/Search/SearchType.cs new file mode 100644 index 000000000..deac8a229 --- /dev/null +++ b/NzbDrone.Core/Model/Search/SearchType.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Model.Search +{ + public enum SearchType + { + EpisodeSearch = 0, + DailySearch = 1, + SeasonSearch = 2 + } +} diff --git a/NzbDrone.Core/NzbDrone.Core.csproj b/NzbDrone.Core/NzbDrone.Core.csproj index b08a5880e..5935d1f1c 100644 --- a/NzbDrone.Core/NzbDrone.Core.csproj +++ b/NzbDrone.Core/NzbDrone.Core.csproj @@ -193,6 +193,8 @@ + + @@ -202,6 +204,7 @@ + diff --git a/NzbDrone.Core/Providers/Indexer/IndexerBase.cs b/NzbDrone.Core/Providers/Indexer/IndexerBase.cs index a0379f1b6..333ba0ac3 100644 --- a/NzbDrone.Core/Providers/Indexer/IndexerBase.cs +++ b/NzbDrone.Core/Providers/Indexer/IndexerBase.cs @@ -8,6 +8,7 @@ using Ninject; using NLog; using NzbDrone.Core.Model; +using NzbDrone.Core.Model.Search; using NzbDrone.Core.Providers.Core; namespace NzbDrone.Core.Providers.Indexer @@ -57,11 +58,9 @@ protected virtual NetworkCredential Credentials /// /// Gets the rss url for specific episode search /// - /// The series title. - /// The season number. - /// The episode number. + /// SearchModel containing episode information /// - protected abstract IList GetSearchUrls(string seriesTitle, int seasonNumber, int episodeNumber); + protected abstract IList GetSearchUrls(SearchModel searchModel); /// /// This method can be overwritten to provide indexer specific info parsing @@ -99,6 +98,31 @@ public virtual IList FetchRss() return result; } + public virtual IList FetchSeason(string seriesTitle, int seasonNumber) + { + _logger.Debug("Searching {0} for {1}-Season {2}", Name, seriesTitle, seasonNumber); + + var result = new List(); + + var searchModel = new SearchModel + { + SeriesTitle = GetQueryTitle(seriesTitle), + SeasonNumber = seasonNumber, + SearchType = SearchType.SeasonSearch + }; + + var searchUrls = GetSearchUrls(searchModel); + + foreach (var url in searchUrls) + { + result.AddRange(Fetch(url)); + } + + result = result.Where(e => e.CleanTitle == Parser.NormalizeTitle(seriesTitle)).ToList(); + + _logger.Info("Finished searching {0} for {1}-S{2}, Found {3}", Name, seriesTitle, seasonNumber, result.Count); + return result; + } public virtual IList FetchEpisode(string seriesTitle, int seasonNumber, int episodeNumber) { @@ -106,7 +130,15 @@ public virtual IList FetchEpisode(string seriesTitle, int se var result = new List(); - var searchUrls = GetSearchUrls(GetQueryTitle(seriesTitle), seasonNumber, episodeNumber); + var searchModel = new SearchModel + { + SeriesTitle = GetQueryTitle(seriesTitle), + SeasonNumber = seasonNumber, + EpisodeNumber = episodeNumber, + SearchType = SearchType.EpisodeSearch + }; + + var searchUrls = GetSearchUrls(searchModel); foreach (var url in searchUrls) { diff --git a/NzbDrone.Core/Providers/Indexer/Newzbin.cs b/NzbDrone.Core/Providers/Indexer/Newzbin.cs index f0f8b76de..3edb4e6b6 100644 --- a/NzbDrone.Core/Providers/Indexer/Newzbin.cs +++ b/NzbDrone.Core/Providers/Indexer/Newzbin.cs @@ -5,6 +5,7 @@ using System.Text.RegularExpressions; using Ninject; using NzbDrone.Core.Model; +using NzbDrone.Core.Model.Search; using NzbDrone.Core.Providers.Core; namespace NzbDrone.Core.Providers.Indexer @@ -38,10 +39,25 @@ protected override NetworkCredential Credentials get { return new NetworkCredential(_configProvider.NewzbinUsername, _configProvider.NewzbinPassword); } } - protected override IList GetSearchUrls(string seriesTitle, int seasonNumber, int episodeNumber) + protected override IList GetSearchUrls(SearchModel searchModel) { + if (searchModel.SearchType == SearchType.EpisodeSearch) + { + return new List + { + String.Format( + @"http://www.newzbin.com/search/query/?q={0}+{1}x{2:00}&fpn=p&searchaction=Go&category=8&{3}", + searchModel.SeriesTitle, searchModel.SeasonNumber, + searchModel.EpisodeNumber, UrlParams) + }; + } - return new List { String.Format(@"http://www.newzbin.com/search/query/?q={0}+{1}x{2:00}&fpn=p&searchaction=Go&category=8&{3}", GetQueryTitle(seriesTitle), seasonNumber, episodeNumber, UrlParams) }; + return new List + { + String.Format( + @"http://www.newzbin.com/search/query/?q={0}+Season+{1}&fpn=p&searchaction=Go&category=8&{2}", + searchModel.SeriesTitle, searchModel.SeasonNumber, UrlParams) + }; } public override string Name diff --git a/NzbDrone.Core/Providers/Indexer/NzbMatrix.cs b/NzbDrone.Core/Providers/Indexer/NzbMatrix.cs index e7d7b6671..24b0cc467 100644 --- a/NzbDrone.Core/Providers/Indexer/NzbMatrix.cs +++ b/NzbDrone.Core/Providers/Indexer/NzbMatrix.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.ServiceModel.Syndication; using Ninject; +using NzbDrone.Core.Model.Search; using NzbDrone.Core.Providers.Core; namespace NzbDrone.Core.Providers.Indexer @@ -38,13 +39,22 @@ protected override string NzbDownloadUrl(SyndicationItem item) return item.Links[0].Uri.ToString(); } - protected override IList GetSearchUrls(string seriesTitle, int seasonNumber, int episodeNumber) + protected override IList GetSearchUrls(SearchModel searchModel) { var searchUrls = new List(); foreach (var url in Urls) { - searchUrls.Add(String.Format("{0}&term={1}+s{2:00}e{3:00}", url, GetQueryTitle(seriesTitle), seasonNumber, episodeNumber)); + if (searchModel.SearchType == SearchType.EpisodeSearch) + { + searchUrls.Add(String.Format("{0}&term={1}+s{2:00}e{3:00}", url, searchModel.SeriesTitle, + searchModel.SeasonNumber, searchModel.EpisodeNumber)); + } + + else + { + searchUrls.Add(String.Format("{0}&term={1}+Season", url, searchModel.SeriesTitle)); + } } return searchUrls; diff --git a/NzbDrone.Core/Providers/Indexer/NzbsOrg.cs b/NzbDrone.Core/Providers/Indexer/NzbsOrg.cs index dc0d5fac9..206b82b73 100644 --- a/NzbDrone.Core/Providers/Indexer/NzbsOrg.cs +++ b/NzbDrone.Core/Providers/Indexer/NzbsOrg.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.ServiceModel.Syndication; using Ninject; +using NzbDrone.Core.Model.Search; using NzbDrone.Core.Providers.Core; namespace NzbDrone.Core.Providers.Indexer @@ -36,13 +37,23 @@ protected override string NzbDownloadUrl(SyndicationItem item) return item.Id; } - protected override IList GetSearchUrls(string seriesTitle, int seasonNumber, int episodeNumber) + protected override IList GetSearchUrls(SearchModel searchModel) { var searchUrls = new List(); foreach (var url in Urls) { - searchUrls.Add(String.Format("{0}&action=search&q={1}+s{2:00}e{3:00}", url, GetQueryTitle(seriesTitle), seasonNumber, episodeNumber)); + if (searchModel.SearchType == SearchType.EpisodeSearch) + { + searchUrls.Add(String.Format("{0}&action=search&q={1}+s{2:00}e{3:00}", url, + searchModel.SeriesTitle, searchModel.SeasonNumber, searchModel.EpisodeNumber)); + } + + else + { + searchUrls.Add(String.Format("{0}&action=search&q={1}+Season", url, + searchModel.SeriesTitle)); + } } return searchUrls; diff --git a/NzbDrone.Core/Providers/Indexer/NzbsRUs.cs b/NzbDrone.Core/Providers/Indexer/NzbsRUs.cs index 8b3ae356a..d09d2b627 100644 --- a/NzbDrone.Core/Providers/Indexer/NzbsRUs.cs +++ b/NzbDrone.Core/Providers/Indexer/NzbsRUs.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.ServiceModel.Syndication; using Ninject; +using NzbDrone.Core.Model.Search; using NzbDrone.Core.Providers.Core; namespace NzbDrone.Core.Providers.Indexer @@ -36,7 +37,7 @@ protected override string NzbDownloadUrl(SyndicationItem item) return item.Links[0].Uri.ToString(); } - protected override IList GetSearchUrls(string seriesTitle, int seasonNumber, int episodeNumber) + protected override IList GetSearchUrls(SearchModel searchModel) { return new List(); } diff --git a/NzbDrone.Core/Providers/Jobs/EpisodeSearchJob.cs b/NzbDrone.Core/Providers/Jobs/EpisodeSearchJob.cs index dac5a6d9a..ea02abcf4 100644 --- a/NzbDrone.Core/Providers/Jobs/EpisodeSearchJob.cs +++ b/NzbDrone.Core/Providers/Jobs/EpisodeSearchJob.cs @@ -11,24 +11,12 @@ namespace NzbDrone.Core.Providers.Jobs { public class EpisodeSearchJob : IJob { - private readonly InventoryProvider _inventoryProvider; - private readonly DownloadProvider _downloadProvider; - private readonly IndexerProvider _indexerProvider; - private readonly EpisodeProvider _episodeProvider; - private readonly SceneMappingProvider _sceneNameMappingProvider; - - private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + private readonly SearchProvider _searchProvider; [Inject] - public EpisodeSearchJob(InventoryProvider inventoryProvider, DownloadProvider downloadProvider, - IndexerProvider indexerProvider, EpisodeProvider episodeProvider, - SceneMappingProvider sceneNameMappingProvider) + public EpisodeSearchJob(SearchProvider searchProvider) { - _inventoryProvider = inventoryProvider; - _downloadProvider = downloadProvider; - _indexerProvider = indexerProvider; - _episodeProvider = episodeProvider; - _sceneNameMappingProvider = sceneNameMappingProvider; + _searchProvider = searchProvider; } public EpisodeSearchJob() @@ -51,91 +39,7 @@ public virtual void Start(ProgressNotification notification, int targetId, int s if (targetId <= 0) throw new ArgumentOutOfRangeException("targetId"); - var episode = _episodeProvider.GetEpisode(targetId); - - if (episode == null) - { - Logger.Error("Unable to find an episode {0} in database", targetId); - return; - } - notification.CurrentMessage = "Searching for " + episode; - - - var series = episode.Series; - - var indexers = _indexerProvider.GetEnabledIndexers(); - var reports = new List(); - - var title = _sceneNameMappingProvider.GetSceneName(series.SeriesId); - - if (string.IsNullOrWhiteSpace(title)) - { - title = series.Title; - } - - foreach (var indexer in indexers) - { - try - { - //notification.CurrentMessage = String.Format("Searching for {0} in {1}", episode, indexer.Name); - - //TODO:Add support for daily episodes, maybe search using both date and season/episode? - var indexerResults = indexer.FetchEpisode(title, episode.SeasonNumber, episode.EpisodeNumber); - - reports.AddRange(indexerResults); - } - catch (Exception e) - { - Logger.ErrorException("An error has occurred while fetching items from " + indexer.Name, e); - } - } - - Logger.Debug("Finished searching all indexers. Total {0}", reports.Count); - notification.CurrentMessage = "Processing search results"; - - - //TODO:fix this so when search returns more than one episode - //TODO:-its populated with more than the original episode. - reports.ForEach(c => - { - c.Series = series; - }); - - ProcessResults(notification, episode, reports); - } - - public void ProcessResults(ProgressNotification notification, Episode episode, IEnumerable reports) - { - foreach (var episodeParseResult in reports.OrderByDescending(c => c.Quality)) - { - try - { - Logger.Trace("Analysing report " + episodeParseResult); - if (_inventoryProvider.IsQualityNeeded(episodeParseResult)) - { - Logger.Debug("Found '{0}'. Adding to download queue.", episodeParseResult); - try - { - _downloadProvider.DownloadReport(episodeParseResult); - notification.CurrentMessage = String.Format("{0} {1} Added to download queue", episode, episodeParseResult.Quality); - } - 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); - } - - return; - } - } - catch (Exception e) - { - Logger.ErrorException("An error has occurred while processing parse result items from " + episodeParseResult, e); - } - } - - Logger.Warn("Unable to find {0} in any of indexers.", episode); - notification.CurrentMessage = String.Format("Unable to find {0} in any of indexers.", episode); + _searchProvider.EpisodeSearch(notification, targetId); } } } \ No newline at end of file diff --git a/NzbDrone.Core/Providers/Jobs/SeasonSearchJob.cs b/NzbDrone.Core/Providers/Jobs/SeasonSearchJob.cs index 21456377c..698d62961 100644 --- a/NzbDrone.Core/Providers/Jobs/SeasonSearchJob.cs +++ b/NzbDrone.Core/Providers/Jobs/SeasonSearchJob.cs @@ -10,15 +10,18 @@ namespace NzbDrone.Core.Providers.Jobs { public class SeasonSearchJob : IJob { - private readonly EpisodeProvider _episodeProvider; + private readonly SearchProvider _searchProvider; private readonly EpisodeSearchJob _episodeSearchJob; + private readonly EpisodeProvider _episodeProvider; private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); - public SeasonSearchJob(EpisodeProvider episodeProvider, EpisodeSearchJob episodeSearchJob) + public SeasonSearchJob(SearchProvider searchProvider, EpisodeSearchJob episodeSearchJob, + EpisodeProvider episodeProvider) { - _episodeProvider = episodeProvider; + _searchProvider = searchProvider; _episodeSearchJob = episodeSearchJob; + _episodeProvider = episodeProvider; } public string Name @@ -39,6 +42,9 @@ public void Start(ProgressNotification notification, int targetId, int secondary if (secondaryTargetId <= 0) throw new ArgumentOutOfRangeException("secondaryTargetId"); + if (_searchProvider.SeasonSearch(notification, targetId, secondaryTargetId)) + return; + Logger.Debug("Getting episodes from database for series: {0} and season: {1}", targetId, secondaryTargetId); var episodes = _episodeProvider.GetEpisodesBySeason(targetId, secondaryTargetId); @@ -48,8 +54,6 @@ public void Start(ProgressNotification notification, int targetId, int secondary return; } - //Todo: Search for a full season NZB before individual episodes - foreach (var episode in episodes.Where(e => !e.Ignored)) { _episodeSearchJob.Start(notification, episode.EpisodeId, 0); diff --git a/NzbDrone.Core/Providers/SearchProvider.cs b/NzbDrone.Core/Providers/SearchProvider.cs new file mode 100644 index 000000000..4605e32bc --- /dev/null +++ b/NzbDrone.Core/Providers/SearchProvider.cs @@ -0,0 +1,236 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NLog; +using Ninject; +using NzbDrone.Core.Model; +using NzbDrone.Core.Model.Notification; +using NzbDrone.Core.Providers.Jobs; +using NzbDrone.Core.Repository; + +namespace NzbDrone.Core.Providers +{ + public class SearchProvider + { + //Season and Episode Searching + private readonly EpisodeProvider _episodeProvider; + private readonly InventoryProvider _inventoryProvider; + private readonly DownloadProvider _downloadProvider; + private readonly SeriesProvider _seriesProvider; + private readonly IndexerProvider _indexerProvider; + private readonly SceneMappingProvider _sceneMappingProvider; + + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + + [Inject] + public SearchProvider(EpisodeProvider episodeProvider, InventoryProvider inventoryProvider, + DownloadProvider downloadProvider, SeriesProvider seriesProvider, + IndexerProvider indexerProvider, SceneMappingProvider sceneMappingProvider) + { + _episodeProvider = episodeProvider; + _inventoryProvider = inventoryProvider; + _downloadProvider = downloadProvider; + _seriesProvider = seriesProvider; + _indexerProvider = indexerProvider; + _sceneMappingProvider = sceneMappingProvider; + } + + public SearchProvider() + { + } + + public virtual bool SeasonSearch(ProgressNotification notification, int seriesId, int seasonNumber) + { + var series = _seriesProvider.GetSeries(seriesId); + + if (series == null) + { + Logger.Error("Unable to find an series {0} in database", seriesId); + return false; + } + + notification.CurrentMessage = String.Format("Searching for {0} Season {1}", series, seasonNumber); + + var indexers = _indexerProvider.GetEnabledIndexers(); + var reports = new List(); + + var title = _sceneMappingProvider.GetSceneName(series.SeriesId); + + if (string.IsNullOrWhiteSpace(title)) + { + title = series.Title; + } + + foreach (var indexer in indexers) + { + try + { + var indexerResults = indexer.FetchSeason(title, seasonNumber); + + reports.AddRange(indexerResults); + } + catch (Exception e) + { + Logger.ErrorException("An error has occurred while fetching items from " + indexer.Name, e); + } + } + + Logger.Debug("Finished searching all indexers. Total {0}", reports.Count); + + if (reports.Count == 0) + return false; + + Logger.Debug("Getting episodes from database for series: {0} and season: {1}", seriesId, seasonNumber); + var episodes = _episodeProvider.GetEpisodesBySeason(seriesId, seasonNumber); + + if (episodes == null) + { + Logger.Warn("No episodes in database found for series: {0} and season: {1}.", seriesId, seasonNumber); + return false; + } + + var episodeNumbers = new List(); + episodeNumbers.AddRange(episodes.Select(e => e.EpisodeNumber)); + + notification.CurrentMessage = "Processing search results"; + + var reportsToProcess = reports.Where(p => p.FullSeason && p.SeasonNumber == seasonNumber).ToList(); + + reportsToProcess.ForEach(c => + { + c.Series = series; + c.EpisodeNumbers = episodeNumbers; + }); + + return ProcessSeasonSearchResults(notification, series, seasonNumber, reportsToProcess); + } + + public bool ProcessSeasonSearchResults(ProgressNotification notification, Series series, int seasonNumber, IEnumerable reports) + { + foreach (var episodeParseResult in reports.OrderByDescending(c => c.Quality)) + { + try + { + Logger.Trace("Analysing report " + episodeParseResult); + if (_inventoryProvider.IsQualityNeeded(episodeParseResult)) + { + Logger.Debug("Found '{0}'. Adding to download queue.", episodeParseResult); + try + { + _downloadProvider.DownloadReport(episodeParseResult); + notification.CurrentMessage = String.Format("{0} Season {1} {2} Added to download queue", series.Title, seasonNumber, episodeParseResult.Quality); + } + 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); + } + + return true; + } + } + catch (Exception e) + { + Logger.ErrorException("An error has occurred while processing parse result items from " + episodeParseResult, e); + } + } + + Logger.Warn("Unable to find {0} Season {1} in any of indexers.", series.Title, seasonNumber); + notification.CurrentMessage = String.Format("Unable to find {0} Season {1} in any of indexers.", series.Title, seasonNumber); + + return false; + } + + 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; + } + notification.CurrentMessage = "Searching for " + episode; + + + var series = episode.Series; + + var indexers = _indexerProvider.GetEnabledIndexers(); + var reports = new List(); + + var title = _sceneMappingProvider.GetSceneName(series.SeriesId); + + if (string.IsNullOrWhiteSpace(title)) + { + title = series.Title; + } + + foreach (var indexer in indexers) + { + try + { + //notification.CurrentMessage = String.Format("Searching for {0} in {1}", episode, indexer.Name); + + //TODO:Add support for daily episodes, maybe search using both date and season/episode? + var indexerResults = indexer.FetchEpisode(title, episode.SeasonNumber, episode.EpisodeNumber); + + reports.AddRange(indexerResults); + } + catch (Exception e) + { + Logger.ErrorException("An error has occurred while fetching items from " + indexer.Name, e); + } + } + + Logger.Debug("Finished searching all indexers. Total {0}", reports.Count); + notification.CurrentMessage = "Processing search results"; + + + //TODO:fix this so when search returns more than one episode + //TODO:-its populated with more than the original episode. + reports.ForEach(c => + { + c.Series = series; + }); + + return ProcessEpisodeSearchResults(notification, episode, reports); + } + + public bool ProcessEpisodeSearchResults(ProgressNotification notification, Episode episode, IEnumerable reports) + { + foreach (var episodeParseResult in reports.OrderByDescending(c => c.Quality)) + { + try + { + Logger.Trace("Analysing report " + episodeParseResult); + if (_inventoryProvider.IsQualityNeeded(episodeParseResult)) + { + Logger.Debug("Found '{0}'. Adding to download queue.", episodeParseResult); + try + { + _downloadProvider.DownloadReport(episodeParseResult); + notification.CurrentMessage = String.Format("{0} {1} Added to download queue", episode, episodeParseResult.Quality); + } + 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); + } + + return true; + } + } + catch (Exception e) + { + Logger.ErrorException("An error has occurred while processing parse result items from " + episodeParseResult, e); + } + } + + Logger.Warn("Unable to find {0} in any of indexers.", episode); + notification.CurrentMessage = String.Format("Unable to find {0} in any of indexers.", episode); + + return false; + } + } +} diff --git a/NzbDrone.Web/Views/Series/Details.cshtml b/NzbDrone.Web/Views/Series/Details.cshtml index 5acb77878..86ea63e9b 100644 --- a/NzbDrone.Web/Views/Series/Details.cshtml +++ b/NzbDrone.Web/Views/Series/Details.cshtml @@ -23,7 +23,7 @@ font-size: 15px; margin: 2px; padding: 2px 5px; - width: 95px; + width: 100px; } .seasonToggleLabel