diff --git a/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 7c45300f8..9d18b6fda 100644 --- a/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -99,6 +99,7 @@ + diff --git a/NzbDrone.Core.Test/ProviderTests/SearchProviderTests/PerformSearchFixture.cs b/NzbDrone.Core.Test/ProviderTests/SearchProviderTests/PerformSearchFixture.cs index 0a132b11e..3d87877f4 100644 --- a/NzbDrone.Core.Test/ProviderTests/SearchProviderTests/PerformSearchFixture.cs +++ b/NzbDrone.Core.Test/ProviderTests/SearchProviderTests/PerformSearchFixture.cs @@ -56,6 +56,8 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests _episodeIndexer1 = new Mock(); _episodeIndexer1.Setup(c => c.FetchEpisode(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(parseResults); + _episodeIndexer1.Setup(c => c.FetchDailyEpisode(It.IsAny(), It.IsAny())) + .Returns(parseResults); _episodeIndexer1.Setup(c => c.FetchSeason(It.IsAny(), It.IsAny())) .Returns(parseResults); _episodeIndexer1.Setup(c => c.FetchPartialSeason(It.IsAny(), It.IsAny(), It.IsAny())) @@ -65,6 +67,8 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests _episodeIndexer2 = new Mock(); _episodeIndexer2.Setup(c => c.FetchEpisode(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(parseResults); + _episodeIndexer2.Setup(c => c.FetchDailyEpisode(It.IsAny(), It.IsAny())) + .Returns(parseResults); _episodeIndexer2.Setup(c => c.FetchSeason(It.IsAny(), It.IsAny())) .Returns(parseResults); _episodeIndexer2.Setup(c => c.FetchPartialSeason(It.IsAny(), It.IsAny(), It.IsAny())) @@ -123,6 +127,15 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests , times); } + private void VerifyFetchDailyEpisode(Times times) + { + _episodeIndexer1.Verify(v => v.FetchDailyEpisode(_series.Title, It.IsAny()) + , times); + + _episodeIndexer2.Verify(v => v.FetchDailyEpisode(_series.Title, It.IsAny()) + , times); + } + private void VerifyFetchEpisodeWithSceneName(Times times) { _episodeIndexer1.Verify(v => v.FetchEpisode(SCENE_NAME, SEASON_NUMBER, It.IsAny()) @@ -210,6 +223,21 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests VerifyFetchEpisode(Times.Once()); } + [Test] + public void PerformSearch_for_daily_episode_should_call_FetchEpisode() + { + //Setup + _series.IsDaily = true; + + //Act + var result = Mocker.Resolve().PerformSearch(MockNotification, _series, SEASON_NUMBER, _episodes); + + //Assert + result.Should().HaveCount(PARSE_RESULT_COUNT * 2); + + VerifyFetchDailyEpisode(Times.Once()); + } + [Test] public void PerformSearch_for_partial_season_should_call_FetchPartialSeason() { diff --git a/NzbDrone.Core.Test/ProviderTests/SearchProviderTests/SearchFixture.cs b/NzbDrone.Core.Test/ProviderTests/SearchProviderTests/SearchFixture.cs new file mode 100644 index 000000000..13670e5a5 --- /dev/null +++ b/NzbDrone.Core.Test/ProviderTests/SearchProviderTests/SearchFixture.cs @@ -0,0 +1,206 @@ +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 SearchFixture : CoreTest + { + private const string SCENE_NAME = "Scene Name"; + private const int SEASON_NUMBER = 5; + private const int PARSE_RESULT_COUNT = 10; + + private Mock _episodeIndexer1; + private Mock _episodeIndexer2; + private Mock _brokenIndexer; + private Mock _nullIndexer; + + private List _indexers; + + private Series _series; + private IList _episodes; + + [SetUp] + public void Setup() + { + var parseResults = Builder.CreateListOfSize(PARSE_RESULT_COUNT) + .Build(); + + _series = Builder.CreateNew() + .Build(); + + _episodes = Builder.CreateListOfSize(1) + .Build(); + + BuildIndexers(parseResults); + + _indexers = new List { _episodeIndexer1.Object, _episodeIndexer2.Object }; + + Mocker.GetMock() + .Setup(c => c.GetEnabledIndexers()) + .Returns(_indexers); + } + + private void BuildIndexers(IList parseResults) + { + _episodeIndexer1 = new Mock(); + _episodeIndexer1.Setup(c => c.FetchEpisode(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(parseResults); + _episodeIndexer1.Setup(c => c.FetchSeason(It.IsAny(), It.IsAny())) + .Returns(parseResults); + _episodeIndexer1.Setup(c => c.FetchPartialSeason(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(parseResults); + + + _episodeIndexer2 = new Mock(); + _episodeIndexer2.Setup(c => c.FetchEpisode(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(parseResults); + _episodeIndexer2.Setup(c => c.FetchSeason(It.IsAny(), It.IsAny())) + .Returns(parseResults); + _episodeIndexer2.Setup(c => c.FetchPartialSeason(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(parseResults); + + _brokenIndexer = new Mock(); + _brokenIndexer.Setup(c => c.FetchEpisode(It.IsAny(), It.IsAny(), It.IsAny())) + .Throws(new Exception()); + + _nullIndexer = new Mock(); + _nullIndexer.Setup(c => c.FetchEpisode(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns>(null); + } + + private void WithTwoGoodOneBrokenIndexer() + { + _indexers = new List { _episodeIndexer1.Object, _brokenIndexer.Object, _episodeIndexer2.Object }; + + Mocker.GetMock() + .Setup(c => c.GetEnabledIndexers()) + .Returns(_indexers); + } + + private void WithNullIndexers() + { + _indexers = new List { _nullIndexer.Object, _nullIndexer.Object }; + + Mocker.GetMock() + .Setup(c => c.GetEnabledIndexers()) + .Returns(_indexers); + } + + private void WithSceneName() + { + Mocker.GetMock() + .Setup(s => s.GetSceneName(_series.SeriesId)).Returns(SCENE_NAME); + } + + private void With30Episodes() + { + _episodes = Builder.CreateListOfSize(30) + .Build(); + } + + private void WithNullEpisodes() + { + _episodes = null; + } + + private void VerifyFetchEpisode(Times times) + { + _episodeIndexer1.Verify(v => v.FetchEpisode(_series.Title, SEASON_NUMBER, It.IsAny()) + , times); + + _episodeIndexer2.Verify(v => v.FetchEpisode(_series.Title, SEASON_NUMBER, It.IsAny()) + , times); + } + + private void VerifyFetchEpisodeWithSceneName(Times times) + { + _episodeIndexer1.Verify(v => v.FetchEpisode(SCENE_NAME, SEASON_NUMBER, It.IsAny()) + , times); + + _episodeIndexer2.Verify(v => v.FetchEpisode(SCENE_NAME, SEASON_NUMBER, It.IsAny()) + , times); + } + + private void VerifyFetchEpisodeBrokenIndexer(Times times) + { + _brokenIndexer.Verify(v => v.FetchEpisode(It.IsAny(), SEASON_NUMBER, It.IsAny()) + , times); + } + + private void VerifyFetchPartialSeason(Times times) + { + _episodeIndexer1.Verify(v => v.FetchPartialSeason(_series.Title, SEASON_NUMBER, It.IsAny()) + , times); + + _episodeIndexer2.Verify(v => v.FetchPartialSeason(_series.Title, SEASON_NUMBER, It.IsAny()) + , times); + } + + private void VerifyFetchSeason(Times times) + { + _episodeIndexer1.Verify(v => v.FetchSeason(_series.Title, SEASON_NUMBER), times); + _episodeIndexer1.Verify(v => v.FetchSeason(_series.Title, SEASON_NUMBER), times); + } + + [Test] + public void SeasonSearch_should_skip_daily_series() + { + //Setup + _series.IsDaily = true; + + Mocker.GetMock().Setup(s => s.GetSeries(1)).Returns(_series); + + //Act + var result = Mocker.Resolve().SeasonSearch(MockNotification, _series.SeriesId, 1); + + //Assert + result.Should().BeFalse(); + } + + [Test] + public void PartialSeasonSearch_should_skip_daily_series() + { + //Setup + _series.IsDaily = true; + + Mocker.GetMock().Setup(s => s.GetSeries(1)).Returns(_series); + + //Act + var result = Mocker.Resolve().PartialSeasonSearch(MockNotification, _series.SeriesId, 1); + + //Assert + result.Should().BeEmpty(); + } + + [Test] + public void EpisodeSearch_should_skip_if_air_date_is_null() + { + //Setup + _series.IsDaily = true; + var episode = _episodes.First(); + episode.AirDate = null; + episode.Series = _series; + + Mocker.GetMock().Setup(s => s.GetEpisode(episode.EpisodeId)) + .Returns(episode); + + //Act + var result = Mocker.Resolve().EpisodeSearch(MockNotification, episode.EpisodeId); + + //Assert + result.Should().BeFalse(); + ExceptionVerification.ExcpectedWarns(1); + } + } +} diff --git a/NzbDrone.Core/Providers/Indexer/IndexerBase.cs b/NzbDrone.Core/Providers/Indexer/IndexerBase.cs index cdedde253..90d656bd5 100644 --- a/NzbDrone.Core/Providers/Indexer/IndexerBase.cs +++ b/NzbDrone.Core/Providers/Indexer/IndexerBase.cs @@ -179,6 +179,33 @@ namespace NzbDrone.Core.Providers.Indexer } + public virtual IList FetchDailyEpisode(string seriesTitle, DateTime airDate) + { + _logger.Debug("Searching {0} for {1}-{2}", Name, seriesTitle, airDate.ToShortDateString()); + + var result = new List(); + + var searchModel = new SearchModel + { + SeriesTitle = GetQueryTitle(seriesTitle), + AirDate = airDate, + SearchType = SearchType.DailySearch + }; + + 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}-{2}, Found {3}", Name, seriesTitle, airDate.ToShortDateString(), result.Count); + return result; + + } + private IEnumerable Fetch(string url) { var result = new List(); diff --git a/NzbDrone.Core/Providers/Jobs/UpdateInfoJob.cs b/NzbDrone.Core/Providers/Jobs/UpdateInfoJob.cs index e0307692a..a15761e89 100644 --- a/NzbDrone.Core/Providers/Jobs/UpdateInfoJob.cs +++ b/NzbDrone.Core/Providers/Jobs/UpdateInfoJob.cs @@ -11,12 +11,15 @@ namespace NzbDrone.Core.Providers.Jobs { private readonly SeriesProvider _seriesProvider; private readonly EpisodeProvider _episodeProvider; + private readonly ReferenceDataProvider _referenceDataProvider; [Inject] - public UpdateInfoJob(SeriesProvider seriesProvider, EpisodeProvider episodeProvider) + public UpdateInfoJob(SeriesProvider seriesProvider, EpisodeProvider episodeProvider, + ReferenceDataProvider referenceDataProvider) { _seriesProvider = seriesProvider; _episodeProvider = episodeProvider; + _referenceDataProvider = referenceDataProvider; } public UpdateInfoJob() @@ -46,6 +49,9 @@ namespace NzbDrone.Core.Providers.Jobs seriesToUpdate = new List() { _seriesProvider.GetSeries(targetId) }; } + //Update any Daily Series in the DB with the IsDaily flag + _referenceDataProvider.UpdateDailySeries(); + foreach (var series in seriesToUpdate) { notification.CurrentMessage = "Updating " + series.Title; diff --git a/NzbDrone.Core/Providers/SearchProvider.cs b/NzbDrone.Core/Providers/SearchProvider.cs index 3e07d6d1e..5d8105968 100644 --- a/NzbDrone.Core/Providers/SearchProvider.cs +++ b/NzbDrone.Core/Providers/SearchProvider.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text; @@ -50,6 +51,10 @@ namespace NzbDrone.Core.Providers return false; } + //Return false if the series is a daily series (we only support individual episode searching + if (series.IsDaily) + return false; + notification.CurrentMessage = String.Format("Searching for {0} Season {1}", series.Title, seasonNumber); var reports = PerformSearch(notification, series, seasonNumber); @@ -96,6 +101,10 @@ namespace NzbDrone.Core.Providers return new List(); } + //Return empty list if the series is a daily series (we only support individual episode searching + if (series.IsDaily) + return new List(); + notification.CurrentMessage = String.Format("Searching for {0} Season {1}", series.Title, seasonNumber); var episodes = _episodeProvider.GetEpisodesBySeason(seriesId, seasonNumber); @@ -121,36 +130,18 @@ namespace NzbDrone.Core.Providers Logger.Error("Unable to find an episode {0} in database", episodeId); return false; } + notification.CurrentMessage = "Searching for " + episode; var series = _seriesProvider.GetSeries(episode.SeriesId); - var indexers = _indexerProvider.GetEnabledIndexers(); - var reports = new List(); - - var title = _sceneMappingProvider.GetSceneName(series.SeriesId); - - if (string.IsNullOrWhiteSpace(title)) + if (episode.Series.IsDaily && !episode.AirDate.HasValue) { - title = series.Title; + Logger.Warn("AirDate is not Valid for: {0}", episode); + return false; } - 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); - } - } + var reports = PerformSearch(notification, series, episode.SeasonNumber, new List { episode }); Logger.Debug("Finished searching all indexers. Total {0}", reports.Count); notification.CurrentMessage = "Processing search results"; @@ -184,15 +175,23 @@ namespace NzbDrone.Core.Providers if (episodes == null) reports.AddRange(indexer.FetchSeason(title, seasonNumber)); - else if(episodes.Count == 1) - reports.AddRange(indexer.FetchEpisode(title, seasonNumber, episodes.First().EpisodeNumber)); + //Treat as single episode + else if (episodes.Count == 1) + { + if (!series.IsDaily) + reports.AddRange(indexer.FetchEpisode(title, seasonNumber, episodes.First().EpisodeNumber)); + + //Daily Episode + else + reports.AddRange(indexer.FetchDailyEpisode(title, episodes.First().AirDate.Value)); + } //Treat as Partial Season else { var prefixes = GetEpisodeNumberPrefixes(episodes.Select(s => s.EpisodeNumber)); - foreach(var episodePrefix in prefixes) + foreach (var episodePrefix in prefixes) { reports.AddRange(indexer.FetchPartialSeason(title, seasonNumber, episodePrefix)); }