diff --git a/NzbDrone.Core.Test/JobTests/SeasonSearchJobTest.cs b/NzbDrone.Core.Test/JobTests/SeasonSearchJobTest.cs index d30e91f8d..ff8775278 100644 --- a/NzbDrone.Core.Test/JobTests/SeasonSearchJobTest.cs +++ b/NzbDrone.Core.Test/JobTests/SeasonSearchJobTest.cs @@ -6,9 +6,11 @@ using FizzWare.NBuilder; using Moq; using NUnit.Framework; using NzbDrone.Core.Jobs; +using NzbDrone.Core.Model; using NzbDrone.Core.Model.Notification; using NzbDrone.Core.Providers; using NzbDrone.Core.Repository; +using NzbDrone.Core.Repository.Search; using NzbDrone.Core.Test.Framework; using NzbDrone.Test.Common.AutoMoq; @@ -39,10 +41,16 @@ namespace NzbDrone.Core.Test.JobTests [Test] public void SeasonSearch_partial_season_success() { + var resultItems = Builder.CreateListOfSize(5) + .All() + .With(e => e.SearchError = ReportRejectionType.None) + .With(e => e.Success = true) + .Build(); + var episodes = Builder.CreateListOfSize(5) .All() - .With(e => e.SeriesId = 1) .With(e => e.SeasonNumber = 1) + .With(e => e.SeriesId = 5) .Build(); var notification = new ProgressNotification("Season Search"); @@ -55,7 +63,7 @@ namespace NzbDrone.Core.Test.JobTests Mocker.GetMock() .Setup(c => c.PartialSeasonSearch(notification, 1, 1)) - .Returns(episodes.Select(e => e.EpisodeNumber).ToList()); + .Returns(resultItems.ToList()); //Act Mocker.Resolve().Start(notification, 1, 1); @@ -88,7 +96,7 @@ namespace NzbDrone.Core.Test.JobTests Mocker.GetMock() .Setup(c => c.PartialSeasonSearch(notification, 1, 1)) - .Returns(new List{1}); + .Returns(new List{ new SearchResultItem{ Success = true }}); //Act Mocker.Resolve().Start(notification, 1, 1); @@ -122,7 +130,7 @@ namespace NzbDrone.Core.Test.JobTests Mocker.GetMock() .Setup(c => c.PartialSeasonSearch(notification, 1, 1)) - .Returns(new List()); + .Returns(new List { new SearchResultItem { Success = false, SearchError = ReportRejectionType.Size} }); //Act diff --git a/NzbDrone.Core.Test/ProviderTests/DecisionEngineTests/AllowedDownloadSpecificationFixture.cs b/NzbDrone.Core.Test/ProviderTests/DecisionEngineTests/AllowedDownloadSpecificationFixture.cs index c33f6c89c..17ff465c9 100644 --- a/NzbDrone.Core.Test/ProviderTests/DecisionEngineTests/AllowedDownloadSpecificationFixture.cs +++ b/NzbDrone.Core.Test/ProviderTests/DecisionEngineTests/AllowedDownloadSpecificationFixture.cs @@ -88,42 +88,42 @@ namespace NzbDrone.Core.Test.ProviderTests.DecisionEngineTests [Test] public void should_be_allowed_if_all_conditions_are_met() { - spec.IsSatisfiedBy(parseResult).Should().BeTrue(); + spec.IsSatisfiedBy(parseResult).Should().Be(ReportRejectionType.None); } [Test] public void should_not_be_allowed_if_profile_is_not_allowed() { WithProfileNotAllowed(); - spec.IsSatisfiedBy(parseResult).Should().BeFalse(); + spec.IsSatisfiedBy(parseResult).Should().Be(ReportRejectionType.QualityNotWanted); } [Test] public void should_not_be_allowed_if_size_is_not_allowed() { WithNotAcceptableSize(); - spec.IsSatisfiedBy(parseResult).Should().BeFalse(); + spec.IsSatisfiedBy(parseResult).Should().Be(ReportRejectionType.Size); } [Test] public void should_not_be_allowed_if_disk_is_not_upgrade() { WithNoDiskUpgrade(); - spec.IsSatisfiedBy(parseResult).Should().BeFalse(); + spec.IsSatisfiedBy(parseResult).Should().Be(ReportRejectionType.ExistingQualityIsEqualOrBetter); } [Test] public void should_not_be_allowed_if_episode_is_already_in_queue() { WithEpisodeAlreadyInQueue(); - spec.IsSatisfiedBy(parseResult).Should().BeFalse(); + spec.IsSatisfiedBy(parseResult).Should().Be(ReportRejectionType.AlreadyInQueue); } [Test] public void should_not_be_allowed_if_report_is_over_retention() { WithOverRetention(); - spec.IsSatisfiedBy(parseResult).Should().BeFalse(); + spec.IsSatisfiedBy(parseResult).Should().Be(ReportRejectionType.Retention); } [Test] @@ -134,7 +134,7 @@ namespace NzbDrone.Core.Test.ProviderTests.DecisionEngineTests WithProfileNotAllowed(); WithOverRetention(); - spec.IsSatisfiedBy(parseResult).Should().BeFalse(); + spec.IsSatisfiedBy(parseResult).Should().Be(ReportRejectionType.QualityNotWanted); } } } \ No newline at end of file diff --git a/NzbDrone.Core.Test/ProviderTests/SearchProviderTests/ProcessDailySearchResultsFixture.cs b/NzbDrone.Core.Test/ProviderTests/SearchProviderTests/ProcessDailySearchResultsFixture.cs index 404f8acd0..5c1f6dfb3 100644 --- a/NzbDrone.Core.Test/ProviderTests/SearchProviderTests/ProcessDailySearchResultsFixture.cs +++ b/NzbDrone.Core.Test/ProviderTests/SearchProviderTests/ProcessDailySearchResultsFixture.cs @@ -77,14 +77,14 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests { Mocker.GetMock() .Setup(s => s.IsSatisfiedBy(It.IsAny())) - .Returns(true); + .Returns(ReportRejectionType.None); } private void WithQualityNotNeeded() { Mocker.GetMock() .Setup(s => s.IsSatisfiedBy(It.IsAny())) - .Returns(false); + .Returns(ReportRejectionType.ExistingQualityIsEqualOrBetter); } [Test] @@ -103,13 +103,13 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests Mocker.GetMock() .Setup(s => s.IsSatisfiedBy(It.Is(d => d.Quality.QualityType == QualityTypes.Bluray1080p))) - .Returns(true); + .Returns(ReportRejectionType.None); //Act var result = Mocker.Resolve().ProcessSearchResults(MockNotification, parseResults, _matchingSeries, DateTime.Today); //Assert - result.Should().BeTrue(); + result.Should().Contain(n => n.Success); Mocker.GetMock().Verify(c => c.IsSatisfiedBy(It.IsAny()), Times.Once()); @@ -133,7 +133,7 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests var result = Mocker.Resolve().ProcessSearchResults(MockNotification, parseResults, _matchingSeries, DateTime.Today); //Assert - result.Should().BeFalse(); + result.Should().NotContain(n => n.Success); Mocker.GetMock().Verify(c => c.IsSatisfiedBy(It.IsAny()), Times.Exactly(5)); @@ -155,7 +155,7 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests var result = Mocker.Resolve().ProcessSearchResults(MockNotification, parseResults, _matchingSeries, DateTime.Today); //Assert - result.Should().BeFalse(); + result.Should().NotContain(n => n.Success); Mocker.GetMock().Verify(c => c.DownloadReport(It.IsAny()), Times.Never()); @@ -175,7 +175,7 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests var result = Mocker.Resolve().ProcessSearchResults(MockNotification, parseResults, _matchingSeries, DateTime.Today); //Assert - result.Should().BeFalse(); + result.Should().NotContain(n => n.Success); Mocker.GetMock().Verify(c => c.DownloadReport(It.IsAny()), Times.Never()); @@ -198,7 +198,7 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests var result = Mocker.Resolve().ProcessSearchResults(MockNotification, parseResults, _matchingSeries, DateTime.Today); //Assert - result.Should().BeTrue(); + result.Should().Contain(n => n.Success); Mocker.GetMock().Verify(c => c.DownloadReport(It.IsAny()), Times.Once()); @@ -230,7 +230,7 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests var result = Mocker.Resolve().ProcessSearchResults(MockNotification, parseResults, _matchingSeries, DateTime.Today); //Assert - result.Should().BeTrue(); + result.Should().Contain(n => n.Success); Mocker.GetMock().Verify(c => c.DownloadReport(It.IsAny()), Times.Exactly(2)); @@ -250,7 +250,7 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests var result = Mocker.Resolve().ProcessSearchResults(MockNotification, parseResults, _matchingSeries, DateTime.Today); //Assert - result.Should().BeFalse(); + result.Should().NotContain(n => n.Success); Mocker.GetMock().Verify(c => c.DownloadReport(It.IsAny()), Times.Never()); @@ -270,7 +270,7 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests var result = Mocker.Resolve().ProcessSearchResults(MockNotification, parseResults, _matchingSeries, DateTime.Today); //Assert - result.Should().BeFalse(); + result.Should().NotContain(n => n.Success); Mocker.GetMock().Verify(c => c.DownloadReport(It.IsAny()), Times.Never()); diff --git a/NzbDrone.Core.Test/ProviderTests/SearchProviderTests/ProcessSearchResultsFixture.cs b/NzbDrone.Core.Test/ProviderTests/SearchProviderTests/ProcessSearchResultsFixture.cs index 40d79bce6..72a6544ca 100644 --- a/NzbDrone.Core.Test/ProviderTests/SearchProviderTests/ProcessSearchResultsFixture.cs +++ b/NzbDrone.Core.Test/ProviderTests/SearchProviderTests/ProcessSearchResultsFixture.cs @@ -73,14 +73,14 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests { Mocker.GetMock() .Setup(s => s.IsSatisfiedBy(It.IsAny())) - .Returns(true); + .Returns(ReportRejectionType.None); } private void WithQualityNotNeeded() { Mocker.GetMock() .Setup(s => s.IsSatisfiedBy(It.IsAny())) - .Returns(false); + .Returns(ReportRejectionType.ExistingQualityIsEqualOrBetter); } [Test] @@ -102,14 +102,14 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests Mocker.GetMock() .Setup(s => s.IsSatisfiedBy(It.Is(d => d.Quality.QualityType == QualityTypes.Bluray1080p))) - .Returns(true); + .Returns(ReportRejectionType.None); //Act var result = Mocker.Resolve().ProcessSearchResults(new ProgressNotification("Test"), parseResults, _matchingSeries, 1, 1); //Assert - result.Should().HaveCount(1); - result.First().Should().Be(1); + result.Should().HaveCount(parseResults.Count); + result.Should().Contain(s => s.Success); Mocker.GetMock().Verify(c => c.IsSatisfiedBy(It.IsAny()), Times.Once()); @@ -135,13 +135,14 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests WithSuccessfulDownload(); Mocker.GetMock() - .Setup(s => s.IsSatisfiedBy(It.IsAny())).Returns(true); + .Setup(s => s.IsSatisfiedBy(It.IsAny())).Returns(ReportRejectionType.None); //Act var result = Mocker.Resolve().ProcessSearchResults(MockNotification, parseResults, _matchingSeries, 1, 1); //Assert - result.Should().HaveCount(1); + result.Should().HaveCount(parseResults.Count); + result.Should().Contain(s => s.Success); Mocker.GetMock().Verify(c => c.DownloadReport(It.Is(d => d.Age != 100)), Times.Never()); @@ -165,7 +166,8 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests var result = Mocker.Resolve().ProcessSearchResults(new ProgressNotification("Test"), parseResults, _matchingSeries, 1, 1); //Assert - result.Should().HaveCount(0); + result.Should().HaveCount(parseResults.Count); + result.Should().NotContain(s => s.Success); Mocker.GetMock().Verify(c => c.IsSatisfiedBy(It.IsAny()), Times.Exactly(5)); @@ -188,7 +190,8 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests var result = Mocker.Resolve().ProcessSearchResults(new ProgressNotification("Test"), parseResults, _matchingSeries, 1, 1); //Assert - result.Should().HaveCount(0); + result.Should().HaveCount(parseResults.Count); + result.Should().NotContain(s => s.Success); Mocker.GetMock().Verify(c => c.DownloadReport(It.IsAny()), Times.Never()); @@ -209,7 +212,8 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests var result = Mocker.Resolve().ProcessSearchResults(new ProgressNotification("Test"), parseResults, _matchingSeries, 1, 1); //Assert - result.Should().HaveCount(0); + result.Should().HaveCount(parseResults.Count); + result.Should().NotContain(s => s.Success); Mocker.GetMock().Verify(c => c.DownloadReport(It.IsAny()), Times.Never()); @@ -230,7 +234,8 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests var result = Mocker.Resolve().ProcessSearchResults(new ProgressNotification("Test"), parseResults, _matchingSeries, 1, 1); //Assert - result.Should().HaveCount(0); + result.Should().HaveCount(parseResults.Count); + result.Should().NotContain(s => s.Success); Mocker.GetMock().Verify(c => c.DownloadReport(It.IsAny()), Times.Never()); @@ -251,7 +256,8 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests var result = Mocker.Resolve().ProcessSearchResults(new ProgressNotification("Test"), parseResults, _matchingSeries, 1, 1); //Assert - result.Should().HaveCount(0); + result.Should().HaveCount(parseResults.Count); + result.Should().NotContain(s => s.Success); Mocker.GetMock().Verify(c => c.DownloadReport(It.IsAny()), Times.Never()); @@ -277,7 +283,8 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests var result = Mocker.Resolve().ProcessSearchResults(new ProgressNotification("Test"), parseResults, _matchingSeries, 1); //Assert - result.Should().HaveCount(1); + result.Should().HaveCount(parseResults.Count); + result.Should().Contain(s => s.Success); Mocker.GetMock().Verify(c => c.DownloadReport(It.IsAny()), Times.Once()); @@ -310,7 +317,8 @@ namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests var result = Mocker.Resolve().ProcessSearchResults(new ProgressNotification("Test"), parseResults, _matchingSeries, 1); //Assert - result.Should().HaveCount(1); + result.Should().HaveCount(parseResults.Count); + result.Should().Contain(s => s.Success); Mocker.GetMock().Verify(c => c.DownloadReport(It.IsAny()), Times.Exactly(2)); diff --git a/NzbDrone.Core/Jobs/RssSyncJob.cs b/NzbDrone.Core/Jobs/RssSyncJob.cs index fc10aafd5..e663dffee 100644 --- a/NzbDrone.Core/Jobs/RssSyncJob.cs +++ b/NzbDrone.Core/Jobs/RssSyncJob.cs @@ -70,7 +70,7 @@ namespace NzbDrone.Core.Jobs try { if (_isMonitoredEpisodeSpecification.IsSatisfiedBy(episodeParseResult) && - _allowedDownloadSpecification.IsSatisfiedBy(episodeParseResult) && + _allowedDownloadSpecification.IsSatisfiedBy(episodeParseResult) == ReportRejectionType.None && _upgradeHistorySpecification.IsSatisfiedBy(episodeParseResult)) { _downloadProvider.DownloadReport(episodeParseResult); diff --git a/NzbDrone.Core/Jobs/SeasonSearchJob.cs b/NzbDrone.Core/Jobs/SeasonSearchJob.cs index d30a9396e..1ac305bd7 100644 --- a/NzbDrone.Core/Jobs/SeasonSearchJob.cs +++ b/NzbDrone.Core/Jobs/SeasonSearchJob.cs @@ -60,15 +60,15 @@ namespace NzbDrone.Core.Jobs //Perform a Partial Season Search var addedSeries = _searchProvider.PartialSeasonSearch(notification, targetId, secondaryTargetId); - addedSeries.Distinct().ToList().Sort(); - var episodeNumbers = episodes.Where(w => w.AirDate <= DateTime.Today.AddDays(1)).Select(s => s.EpisodeNumber).ToList(); - episodeNumbers.Sort(); + //addedSeries.Distinct().ToList().Sort(); + //var episodeNumbers = episodes.Where(w => w.AirDate <= DateTime.Today.AddDays(1)).Select(s => s.EpisodeNumber).ToList(); + //episodeNumbers.Sort(); - if (addedSeries.SequenceEqual(episodeNumbers)) - return; + //if (addedSeries.SequenceEqual(episodeNumbers)) + // return; - //Get the list of episodes that weren't downloaded - var missingEpisodes = episodeNumbers.Except(addedSeries).ToList(); + ////Get the list of episodes that weren't downloaded + //var missingEpisodes = episodeNumbers.Except(addedSeries).ToList(); //TODO: do one by one check only when max number of feeds have been returned by the indexer //Only process episodes that is in missing episodes (To ensure we double check if the episode is available) diff --git a/NzbDrone.Core/Model/ReportRejectionType.cs b/NzbDrone.Core/Model/ReportRejectionType.cs new file mode 100644 index 000000000..0d2ea1b46 --- /dev/null +++ b/NzbDrone.Core/Model/ReportRejectionType.cs @@ -0,0 +1,21 @@ +using System.Linq; + +namespace NzbDrone.Core.Model +{ + public enum ReportRejectionType + { + None = 0, + WrongSeries = 1, + QualityNotWanted = 2, + WrongSeason = 3, + WrongEpisode = 3, + Size = 3, + Retention = 3, + ExistingQualityIsEqualOrBetter = 4, + Cutoff = 5, + AlreadyInQueue = 6, + DownloadClientFailure = 7, + Skipped = 8, + Failure, + } +} diff --git a/NzbDrone.Core/NzbDrone.Core.csproj b/NzbDrone.Core/NzbDrone.Core.csproj index 3932bdaf1..a26ab3fca 100644 --- a/NzbDrone.Core/NzbDrone.Core.csproj +++ b/NzbDrone.Core/NzbDrone.Core.csproj @@ -277,6 +277,7 @@ + @@ -304,6 +305,9 @@ Code + + + Code diff --git a/NzbDrone.Core/Providers/DecisionEngine/AllowedDownloadSpecification.cs b/NzbDrone.Core/Providers/DecisionEngine/AllowedDownloadSpecification.cs index 7321c6a4c..c6e437c5f 100644 --- a/NzbDrone.Core/Providers/DecisionEngine/AllowedDownloadSpecification.cs +++ b/NzbDrone.Core/Providers/DecisionEngine/AllowedDownloadSpecification.cs @@ -2,6 +2,7 @@ using NLog; using Ninject; using NzbDrone.Core.Model; +using NzbDrone.Core.Repository.Search; namespace NzbDrone.Core.Providers.DecisionEngine { @@ -30,16 +31,16 @@ namespace NzbDrone.Core.Providers.DecisionEngine { } - public virtual bool IsSatisfiedBy(EpisodeParseResult subject) + public virtual ReportRejectionType IsSatisfiedBy(EpisodeParseResult subject) { - if (!_qualityAllowedByProfileSpecification.IsSatisfiedBy(subject)) return false; - if (!_upgradeDiskSpecification.IsSatisfiedBy(subject)) return false; - if (!_retentionSpecification.IsSatisfiedBy(subject)) return false; - if (!_acceptableSizeSpecification.IsSatisfiedBy(subject)) return false; - if (_alreadyInQueueSpecification.IsSatisfiedBy(subject)) return false; + if (!_qualityAllowedByProfileSpecification.IsSatisfiedBy(subject)) return ReportRejectionType.QualityNotWanted; + if (!_upgradeDiskSpecification.IsSatisfiedBy(subject)) return ReportRejectionType.ExistingQualityIsEqualOrBetter; + if (!_retentionSpecification.IsSatisfiedBy(subject)) return ReportRejectionType.Retention; + if (!_acceptableSizeSpecification.IsSatisfiedBy(subject)) return ReportRejectionType.Size; + if (_alreadyInQueueSpecification.IsSatisfiedBy(subject)) return ReportRejectionType.AlreadyInQueue; logger.Debug("Episode {0} is needed", subject); - return true; + return ReportRejectionType.None; } } } \ No newline at end of file diff --git a/NzbDrone.Core/Providers/SearchProvider.cs b/NzbDrone.Core/Providers/SearchProvider.cs index 1e4ea8a5f..324b3a2fe 100644 --- a/NzbDrone.Core/Providers/SearchProvider.cs +++ b/NzbDrone.Core/Providers/SearchProvider.cs @@ -8,6 +8,7 @@ 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 { @@ -21,13 +22,15 @@ namespace NzbDrone.Core.Providers private readonly SceneMappingProvider _sceneMappingProvider; private readonly UpgradePossibleSpecification _upgradePossibleSpecification; private readonly AllowedDownloadSpecification _allowedDownloadSpecification; + private readonly SearchResultProvider _searchResultProvider; private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); [Inject] public SearchProvider(EpisodeProvider episodeProvider, DownloadProvider downloadProvider, SeriesProvider seriesProvider, IndexerProvider indexerProvider, SceneMappingProvider sceneMappingProvider, - UpgradePossibleSpecification upgradePossibleSpecification, AllowedDownloadSpecification allowedDownloadSpecification) + UpgradePossibleSpecification upgradePossibleSpecification, AllowedDownloadSpecification allowedDownloadSpecification, + SearchResultProvider searchResultProvider) { _episodeProvider = episodeProvider; _downloadProvider = downloadProvider; @@ -36,6 +39,7 @@ namespace NzbDrone.Core.Providers _sceneMappingProvider = sceneMappingProvider; _upgradePossibleSpecification = upgradePossibleSpecification; _allowedDownloadSpecification = allowedDownloadSpecification; + _searchResultProvider = searchResultProvider; } public SearchProvider() @@ -44,13 +48,20 @@ namespace NzbDrone.Core.Providers public virtual bool SeasonSearch(ProgressNotification notification, int seriesId, int seasonNumber) { + var searchResult = new SearchResult + { + 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 false; - } + } //Return false if the series is a daily series (we only support individual episode searching if (series.IsDaily) @@ -80,46 +91,45 @@ namespace NzbDrone.Core.Providers e => e.EpisodeNumbers = episodeNumbers.ToList() ); - var downloadedEpisodes = ProcessSearchResults(notification, reports, series, seasonNumber); + searchResult.SearchResultItems = ProcessSearchResults(notification, reports, series, seasonNumber); - downloadedEpisodes.Sort(); - episodeNumbers.ToList().Sort(); - - //Returns true if the list of downloaded episodes matches the list of episode numbers - //(either a full season release was grabbed or all individual episodes) - return (downloadedEpisodes.SequenceEqual(episodeNumbers)); + return (searchResult.SearchResultItems.Select(s => s.Success).Count() == episodeNumbers.Count); } - public virtual List PartialSeasonSearch(ProgressNotification notification, int seriesId, int seasonNumber) + public virtual List PartialSeasonSearch(ProgressNotification notification, int seriesId, int seasonNumber) { - //This method will search for episodes in a season in groups of 10 episodes S01E0, S01E1, S01E2, etc + var searchResult = new SearchResult + { + 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(); + 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(); + return new List(); notification.CurrentMessage = String.Format("Searching for {0} Season {1}", series.Title, seasonNumber); - var episodes = _episodeProvider.GetEpisodesBySeason(seriesId, seasonNumber); - var reports = PerformSearch(notification, series, seasonNumber, episodes); - Logger.Debug("Finished searching all indexers. Total {0}", reports.Count); if (reports.Count == 0) - return new List(); + return new List(); notification.CurrentMessage = "Processing search results"; + searchResult.SearchResultItems = ProcessSearchResults(notification, reports, series, seasonNumber); - return ProcessSearchResults(notification, reports, series, seasonNumber); + _searchResultProvider.Add(searchResult); + return searchResult.SearchResultItems; } public virtual bool EpisodeSearch(ProgressNotification notification, int episodeId) @@ -136,7 +146,7 @@ namespace NzbDrone.Core.Providers 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}, file you have is already at cutoff", episode); + notification.CurrentMessage = String.Format("Skipping search for {0}, the file you have is already at cutoff", episode); return false; } @@ -145,19 +155,41 @@ namespace NzbDrone.Core.Providers if (episode.Series.IsDaily && !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; } + var searchResult = new SearchResult + { + SearchTime = DateTime.Now, + SeriesId = episode.Series.SeriesId + }; + var reports = PerformSearch(notification, episode.Series, episode.SeasonNumber, new List { episode }); Logger.Debug("Finished searching all indexers. Total {0}", reports.Count); notification.CurrentMessage = "Processing search results"; - if (!episode.Series.IsDaily && ProcessSearchResults(notification, reports, episode.Series, episode.SeasonNumber, episode.EpisodeNumber).Count == 1) - return true; + if (episode.Series.IsDaily) + { + searchResult.AirDate = episode.AirDate.Value; + searchResult.SearchResultItems = ProcessSearchResults(notification, reports, episode.Series, episode.AirDate.Value); - if (episode.Series.IsDaily && ProcessSearchResults(notification, reports, episode.Series, episode.AirDate.Value)) + _searchResultProvider.Add(searchResult); + + if (searchResult.SearchResultItems.Any(r => r.Success)) + return true; + + return false; + } + + if (!episode.Series.IsDaily) + { + searchResult.SeasonNumber = episode.SeasonNumber; + searchResult.EpisodeId = episodeId; + ProcessSearchResults(notification, reports, episode.Series, episode.SeasonNumber, episode.EpisodeNumber); return true; + } Logger.Warn("Unable to find {0} in any of indexers.", episode); @@ -170,7 +202,6 @@ namespace NzbDrone.Core.Providers notification.CurrentMessage = String.Format("Sorry, couldn't find you {0} in any of indexers.", episode); } - return false; } @@ -227,9 +258,10 @@ namespace NzbDrone.Core.Providers return reports; } - public List ProcessSearchResults(ProgressNotification notification, IEnumerable reports, Series series, int seasonNumber, int? episodeNumber = null) + public List ProcessSearchResults(ProgressNotification notification, IEnumerable reports, Series series, int seasonNumber, int? episodeNumber = null) { var successes = new List(); + var items = new List(); foreach (var episodeParseResult in reports.OrderByDescending(c => c.Quality).ThenBy(c => c.Age)) { @@ -237,6 +269,14 @@ namespace NzbDrone.Core.Providers { Logger.Trace("Analysing report " + episodeParseResult); + var item = new SearchResultItem + { + ReportTitle = episodeParseResult.OriginalString, + NzbUrl = episodeParseResult.NzbUrl + }; + + items.Add(item); + //Get the matching series episodeParseResult.Series = _seriesProvider.FindSeries(episodeParseResult.CleanTitle); @@ -244,6 +284,7 @@ namespace NzbDrone.Core.Providers if (episodeParseResult.Series == null || episodeParseResult.Series.SeriesId != series.SeriesId) { Logger.Trace("Unexpected series for search: {0}. Skipping.", episodeParseResult.CleanTitle); + item.SearchError = ReportRejectionType.WrongSeries; continue; } @@ -251,6 +292,7 @@ namespace NzbDrone.Core.Providers if (episodeParseResult.SeasonNumber != seasonNumber) { Logger.Trace("Season number does not match searched season number, skipping."); + item.SearchError = ReportRejectionType.WrongSeason; continue; } @@ -258,6 +300,7 @@ namespace NzbDrone.Core.Providers if (episodeNumber.HasValue && !episodeParseResult.EpisodeNumbers.Contains(episodeNumber.Value)) { Logger.Trace("Searched episode number is not contained in post, skipping."); + item.SearchError = ReportRejectionType.WrongEpisode; continue; } @@ -265,10 +308,12 @@ namespace NzbDrone.Core.Providers if (successes.Intersect(episodeParseResult.EpisodeNumbers).Any()) { Logger.Trace("Episode has already been downloaded in this search, skipping."); + item.SearchError = ReportRejectionType.Skipped; continue; } - if (_allowedDownloadSpecification.IsSatisfiedBy(episodeParseResult)) + var rejectionType = _allowedDownloadSpecification.IsSatisfiedBy(episodeParseResult); + if (rejectionType == ReportRejectionType.None) { Logger.Debug("Found '{0}'. Adding to download queue.", episodeParseResult); try @@ -279,12 +324,18 @@ namespace NzbDrone.Core.Providers //Add the list of episode numbers from this release 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; } } } @@ -294,15 +345,32 @@ namespace NzbDrone.Core.Providers } } - return successes; + return items; } - public bool ProcessSearchResults(ProgressNotification notification, IEnumerable reports, Series series, DateTime airDate) + public List ProcessSearchResults(ProgressNotification notification, IEnumerable reports, Series series, DateTime airDate) { + var items = new List(); + var skip = false; + foreach (var episodeParseResult in reports.OrderByDescending(c => c.Quality)) { try { + var item = new SearchResultItem + { + ReportTitle = episodeParseResult.OriginalString, + NzbUrl = episodeParseResult.NzbUrl + }; + + items.Add(item); + + if (skip) + { + item.SearchError = ReportRejectionType.Skipped; + continue; + } + Logger.Trace("Analysing report " + episodeParseResult); //Get the matching series @@ -310,13 +378,20 @@ namespace NzbDrone.Core.Providers //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; + } - if (_allowedDownloadSpecification.IsSatisfiedBy(episodeParseResult)) + var allowedDownload = _allowedDownloadSpecification.IsSatisfiedBy(episodeParseResult); + if (allowedDownload == ReportRejectionType.None) { Logger.Debug("Found '{0}'. Adding to download queue.", episodeParseResult); try @@ -327,7 +402,12 @@ namespace NzbDrone.Core.Providers String.Format("{0} - {1} {2} Added to download queue", episodeParseResult.Series.Title, episodeParseResult.AirDate.Value.ToShortDateString(), episodeParseResult.Quality); - return true; + item.Success = true; + skip = true; + } + else + { + item.SearchError = ReportRejectionType.DownloadClientFailure; } } catch (Exception e) @@ -336,13 +416,18 @@ namespace NzbDrone.Core.Providers notification.CurrentMessage = String.Format("Unable to add report to download queue. {0}", episodeParseResult); } } + else + { + item.SearchError = allowedDownload; + } } catch (Exception e) { Logger.ErrorException("An error has occurred while processing parse result items from " + episodeParseResult, e); } } - return false; + + return items; } private List GetEpisodeNumberPrefixes(IEnumerable episodeNumbers) diff --git a/NzbDrone.Core/Providers/SearchResultProvider.cs b/NzbDrone.Core/Providers/SearchResultProvider.cs new file mode 100644 index 000000000..a4b8472cb --- /dev/null +++ b/NzbDrone.Core/Providers/SearchResultProvider.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NLog; +using Ninject; +using NzbDrone.Core.Repository.Search; +using PetaPoco; + +namespace NzbDrone.Core.Providers +{ + public class SearchResultProvider + { + private readonly IDatabase _database; + private static readonly Logger logger = LogManager.GetCurrentClassLogger(); + + [Inject] + public SearchResultProvider(IDatabase database) + { + _database = database; + } + + public SearchResultProvider() + { + + } + + public virtual void Add(SearchResult searchResult) + { + logger.Trace("Adding new search result"); + var id = Convert.ToInt32(_database.Insert(searchResult)); + + searchResult.SearchResultItems.ForEach(s => s.Id = id); + logger.Trace("Adding search result items"); + _database.InsertMany(searchResult.SearchResultItems); + } + + public virtual void Delete(int id) + { + logger.Trace("Deleting search result items attached to: {0}", id); + _database.Execute("DELETE FROM SearchResultItems WHERE SearchResultId = @0", id); + + logger.Trace("Deleting search result: {0}", id); + _database.Delete(id); + } + + public virtual List AllSearchResults() + { + return _database.Fetch(); + } + + public virtual SearchResult GetSearchResult(int id) + { + var result = _database.Single(id); + result.SearchResultItems = _database.Fetch("WHERE SearchResultId = @0", id); + + return result; + } + } +} diff --git a/NzbDrone.Core/Repository/Search/SearchResult.cs b/NzbDrone.Core/Repository/Search/SearchResult.cs new file mode 100644 index 000000000..3350895a1 --- /dev/null +++ b/NzbDrone.Core/Repository/Search/SearchResult.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using NzbDrone.Core.Model; +using NzbDrone.Core.Repository.Quality; +using PetaPoco; + +namespace NzbDrone.Core.Repository.Search +{ + [PrimaryKey("Id", autoIncrement = true)] + [TableName("SearchResults")] + public class SearchResult + { + public int Id { get; set; } + public int SeriesId { get; set; } + public int? SeasonNumber { get; set; } + public int? EpisodeId { get; set; } + public DateTime? AirDate { get; set; } + public DateTime SearchTime { get; set; } + + [ResultColumn] + public List SearchResultItems { get; set; } + } +} \ No newline at end of file diff --git a/NzbDrone.Core/Repository/Search/SearchResultItem.cs b/NzbDrone.Core/Repository/Search/SearchResultItem.cs new file mode 100644 index 000000000..94160a6b1 --- /dev/null +++ b/NzbDrone.Core/Repository/Search/SearchResultItem.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using NzbDrone.Core.Model; +using NzbDrone.Core.Repository.Quality; +using PetaPoco; + +namespace NzbDrone.Core.Repository.Search +{ + [PrimaryKey("Id", autoIncrement = true)] + [TableName("SearchResultItems")] + public class SearchResultItem + { + public int Id { get; set; } + public int SearchResultId { get; set; } + public string ReportTitle { get; set; } + public string NzbUrl { get; set; } + public string NzbInfoUrl { get; set; } + public bool Success { get; set; } + public ReportRejectionType SearchError { get; set; } + } +} \ No newline at end of file