diff --git a/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index b38055dee..96228512e 100644 --- a/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -102,7 +102,10 @@ + + + diff --git a/NzbDrone.Core.Test/ProviderTests/ReferenceDataProviderTest.cs b/NzbDrone.Core.Test/ProviderTests/ReferenceDataProviderTest.cs new file mode 100644 index 000000000..d1a50f068 --- /dev/null +++ b/NzbDrone.Core.Test/ProviderTests/ReferenceDataProviderTest.cs @@ -0,0 +1,187 @@ +using System; +using System.Linq; + +using FizzWare.NBuilder; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using NzbDrone.Core.Providers; +using NzbDrone.Core.Providers.Core; +using NzbDrone.Core.Repository; +using NzbDrone.Core.Repository.Quality; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Test.Common; +using NzbDrone.Test.Common.AutoMoq; +using PetaPoco; + +namespace NzbDrone.Core.Test.ProviderTests +{ + [TestFixture] + // ReSharper disable InconsistentNaming + public class ReferenceDataProviderTest : CoreTest + { + private string validSeriesIds = String.Format("1{0}2{0}3{0}4{0}5", Environment.NewLine); + private string invalidSeriesIds = String.Format("1{0}2{0}NaN{0}4{0}5", Environment.NewLine); + + [Test] + public void GetDailySeriesIds_should_return_list_of_int_when_all_are_valid() + { + //Setup + Mocker.GetMock().Setup(s => s.DownloadString("http://www.nzbdrone.com/DailySeries.csv")) + .Returns(validSeriesIds); + + //Act + var result = Mocker.Resolve().GetDailySeriesIds(); + + //Assert + result.Should().HaveCount(5); + } + + [Test] + public void GetDailySeriesIds_should_return_list_of_int_when_any_are_valid() + { + //Setup + Mocker.GetMock().Setup(s => s.DownloadString("http://www.nzbdrone.com/DailySeries.csv")) + .Returns(invalidSeriesIds); + + //Act + var result = Mocker.Resolve().GetDailySeriesIds(); + + //Assert + result.Should().HaveCount(4); + } + + [Test] + public void GetDailySeriesIds_should_return_empty_list_of_int_when_server_is_unavailable() + { + //Setup + Mocker.GetMock().Setup(s => s.DownloadString("http://www.nzbdrone.com/DailySeries.csv")) + .Throws(new Exception()); + + //Act + var result = Mocker.Resolve().GetDailySeriesIds(); + + //Assert + result.Should().HaveCount(0); + ExceptionVerification.ExcpectedWarns(1); + } + + [Test] + public void IsDailySeries_should_return_true() + { + //Setup + Mocker.GetMock().Setup(s => s.DownloadString("http://www.nzbdrone.com/DailySeries.csv")) + .Returns(validSeriesIds); + + //Act + var result = Mocker.Resolve().IsSeriesDaily(1); + + //Assert + result.Should().BeTrue(); + } + + [Test] + public void IsDailySeries_should_return_false() + { + //Setup + Mocker.GetMock().Setup(s => s.DownloadString("http://www.nzbdrone.com/DailySeries.csv")) + .Returns(validSeriesIds); + + //Act + var result = Mocker.Resolve().IsSeriesDaily(10); + + //Assert + result.Should().BeFalse(); + } + + [Test] + public void UpdateDailySeries_should_update_series_that_match_daily_series_list() + { + WithRealDb(); + + var fakeSeries = Builder.CreateListOfSize(5) + .All() + .With(s => s.IsDaily = false) + .Build(); + + Db.InsertMany(fakeSeries); + + //Setup + Mocker.GetMock().Setup(s => s.DownloadString("http://www.nzbdrone.com/DailySeries.csv")) + .Returns(validSeriesIds); + + //Act + Mocker.Resolve().UpdateDailySeries(); + + //Assert + var result = Db.Fetch(); + + result.Where(s => s.IsDaily).Should().HaveCount(5); + } + + [Test] + public void UpdateDailySeries_should_update_series_should_skip_series_that_dont_match() + { + WithRealDb(); + + var fakeSeries = Builder.CreateListOfSize(5) + .All() + .With(s => s.IsDaily = false) + .TheFirst(1) + .With(s => s.SeriesId = 10) + .TheNext(1) + .With(s => s.SeriesId = 11) + .TheNext(1) + .With(s => s.SeriesId = 12) + .Build(); + + Db.InsertMany(fakeSeries); + + //Setup + Mocker.GetMock().Setup(s => s.DownloadString("http://www.nzbdrone.com/DailySeries.csv")) + .Returns(validSeriesIds); + + //Act + Mocker.Resolve().UpdateDailySeries(); + + //Assert + var result = Db.Fetch(); + + result.Where(s => !s.IsDaily).Should().HaveCount(3); + result.Where(s => s.IsDaily).Should().HaveCount(2); + } + + [Test] + public void UpdateDailySeries_should_update_series_should_not_overwrite_existing_isDaily() + { + WithRealDb(); + + var fakeSeries = Builder.CreateListOfSize(5) + .All() + .With(s => s.IsDaily = false) + .TheFirst(1) + .With(s => s.SeriesId = 10) + .With(s => s.IsDaily = true) + .TheNext(1) + .With(s => s.SeriesId = 11) + .TheNext(1) + .With(s => s.SeriesId = 12) + .Build(); + + Db.InsertMany(fakeSeries); + + //Setup + Mocker.GetMock().Setup(s => s.DownloadString("http://www.nzbdrone.com/DailySeries.csv")) + .Returns(validSeriesIds); + + //Act + Mocker.Resolve().UpdateDailySeries(); + + //Assert + var result = Db.Fetch(); + + result.Where(s => s.IsDaily).Should().HaveCount(3); + result.Where(s => !s.IsDaily).Should().HaveCount(2); + } + } +} \ No newline at end of file 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/ProcessDailySearchResultsFixture.cs b/NzbDrone.Core.Test/ProviderTests/SearchProviderTests/ProcessDailySearchResultsFixture.cs new file mode 100644 index 000000000..cca5a0841 --- /dev/null +++ b/NzbDrone.Core.Test/ProviderTests/SearchProviderTests/ProcessDailySearchResultsFixture.cs @@ -0,0 +1,278 @@ +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.Repository; +using NzbDrone.Core.Repository.Quality; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Test.Common; +using NzbDrone.Test.Common.AutoMoq; + +namespace NzbDrone.Core.Test.ProviderTests.SearchProviderTests +{ + [TestFixture] + // ReSharper disable InconsistentNaming + public class ProcessDailySearchResultsFixture : CoreTest + { + private Series _matchingSeries = null; + private Series _mismatchedSeries = null; + private Series _nullSeries = null; + + [SetUp] + public void setup() + { + _matchingSeries = Builder.CreateNew() + .With(s => s.SeriesId = 79488) + .With(s => s.Title = "30 Rock") + .Build(); + + _mismatchedSeries = Builder.CreateNew() + .With(s => s.SeriesId = 12345) + .With(s => s.Title = "Not 30 Rock") + .Build(); + } + + private void WithMatchingSeries() + { + Mocker.GetMock() + .Setup(s => s.FindSeries(It.IsAny())).Returns(_matchingSeries); + } + + private void WithMisMatchedSeries() + { + Mocker.GetMock() + .Setup(s => s.FindSeries(It.IsAny())).Returns(_mismatchedSeries); + } + + private void WithNullSeries() + { + Mocker.GetMock() + .Setup(s => s.FindSeries(It.IsAny())).Returns(_nullSeries); + } + + private void WithSuccessfulDownload() + { + Mocker.GetMock() + .Setup(s => s.DownloadReport(It.IsAny())) + .Returns(true); + } + + private void WithFailingDownload() + { + Mocker.GetMock() + .Setup(s => s.DownloadReport(It.IsAny())) + .Returns(false); + } + + private void WithQualityNeeded() + { + Mocker.GetMock() + .Setup(s => s.IsQualityNeeded(It.IsAny())) + .Returns(true); + } + + private void WithQualityNotNeeded() + { + Mocker.GetMock() + .Setup(s => s.IsQualityNeeded(It.IsAny())) + .Returns(false); + } + + [Test] + public void processSearchResults_higher_quality_should_be_called_first() + { + var parseResults = Builder.CreateListOfSize(5) + .All() + .With(c => c.AirDate = DateTime.Today) + .With(c => c.Quality = new Quality(QualityTypes.DVD, true)) + .Random(1) + .With(c => c.Quality = new Quality(QualityTypes.Bluray1080p, true)) + .Build(); + + WithMatchingSeries(); + WithSuccessfulDownload(); + + Mocker.GetMock() + .Setup(s => s.IsQualityNeeded(It.Is(d => d.Quality.QualityType == QualityTypes.Bluray1080p))) + .Returns(true); + + //Act + var result = Mocker.Resolve().ProcessSearchResults(MockNotification, parseResults, _matchingSeries, DateTime.Today); + + //Assert + result.Should().BeTrue(); + + Mocker.GetMock().Verify(c => c.IsQualityNeeded(It.IsAny()), + Times.Once()); + Mocker.GetMock().Verify(c => c.DownloadReport(It.IsAny()), + Times.Once()); + } + + [Test] + public void processSearchResults_when_quality_is_not_needed_should_check_the_rest() + { + var parseResults = Builder.CreateListOfSize(5) + .All() + .With(c => c.AirDate = DateTime.Today) + .With(c => c.Quality = new Quality(QualityTypes.DVD, true)) + .Build(); + + WithMatchingSeries(); + WithQualityNotNeeded(); + + //Act + var result = Mocker.Resolve().ProcessSearchResults(MockNotification, parseResults, _matchingSeries, DateTime.Today); + + //Assert + result.Should().BeFalse(); + + Mocker.GetMock().Verify(c => c.IsQualityNeeded(It.IsAny()), + Times.Exactly(5)); + Mocker.GetMock().Verify(c => c.DownloadReport(It.IsAny()), + Times.Never()); + } + + [Test] + public void processSearchResults_should_skip_if_series_is_null() + { + var parseResults = Builder.CreateListOfSize(5) + .All() + .With(e => e.AirDate = DateTime.Today) + .Build(); + + WithNullSeries(); + + //Act + var result = Mocker.Resolve().ProcessSearchResults(MockNotification, parseResults, _matchingSeries, DateTime.Today); + + //Assert + result.Should().BeFalse(); + + Mocker.GetMock().Verify(c => c.DownloadReport(It.IsAny()), + Times.Never()); + } + + [Test] + public void processSearchResults_should_skip_if_series_is_mismatched() + { + var parseResults = Builder.CreateListOfSize(5) + .All() + .With(e => e.AirDate = DateTime.Today) + .Build(); + + WithMisMatchedSeries(); + + //Act + var result = Mocker.Resolve().ProcessSearchResults(MockNotification, parseResults, _matchingSeries, DateTime.Today); + + //Assert + result.Should().BeFalse(); + + Mocker.GetMock().Verify(c => c.DownloadReport(It.IsAny()), + Times.Never()); + } + + [Test] + public void processSearchResults_should_return_after_successful_download() + { + var parseResults = Builder.CreateListOfSize(2) + .All() + .With(e => e.AirDate = DateTime.Today) + .With(c => c.Quality = new Quality(QualityTypes.DVD, true)) + .Build(); + + WithMatchingSeries(); + WithQualityNeeded(); + WithSuccessfulDownload(); + + //Act + var result = Mocker.Resolve().ProcessSearchResults(MockNotification, parseResults, _matchingSeries, DateTime.Today); + + //Assert + result.Should().BeTrue(); + + Mocker.GetMock().Verify(c => c.DownloadReport(It.IsAny()), + Times.Once()); + } + + [Test] + public void processSearchResults_should_try_next_if_download_fails() + { + var parseResults = Builder.CreateListOfSize(2) + .All() + .With(e => e.AirDate = DateTime.Today) + .With(c => c.Quality = new Quality(QualityTypes.DVD, true)) + .TheLast(1) + .With(c => c.Quality = new Quality(QualityTypes.SDTV, true)) + .Build(); + + WithMatchingSeries(); + WithQualityNeeded(); + + Mocker.GetMock() + .Setup(s => s.DownloadReport(It.Is(d => d.Quality.QualityType == QualityTypes.DVD))) + .Returns(false); + + Mocker.GetMock() + .Setup(s => s.DownloadReport(It.Is(d => d.Quality.QualityType == QualityTypes.SDTV))) + .Returns(true); + + //Act + var result = Mocker.Resolve().ProcessSearchResults(MockNotification, parseResults, _matchingSeries, DateTime.Today); + + //Assert + result.Should().BeTrue(); + + Mocker.GetMock().Verify(c => c.DownloadReport(It.IsAny()), + Times.Exactly(2)); + } + + [Test] + public void processSearchResults_should_skip_if_parseResult_does_not_have_airdate() + { + var parseResults = Builder.CreateListOfSize(5) + .All() + .With(e => e.AirDate = null) + .Build(); + + WithMatchingSeries(); + + //Act + var result = Mocker.Resolve().ProcessSearchResults(MockNotification, parseResults, _matchingSeries, DateTime.Today); + + //Assert + result.Should().BeFalse(); + + Mocker.GetMock().Verify(c => c.DownloadReport(It.IsAny()), + Times.Never()); + } + + [Test] + public void processSearchResults_should_skip_if_parseResult_airdate_does_not_match() + { + var parseResults = Builder.CreateListOfSize(5) + .All() + .With(e => e.AirDate = DateTime.Today.AddDays(10)) + .Build(); + + WithMatchingSeries(); + + //Act + var result = Mocker.Resolve().ProcessSearchResults(MockNotification, parseResults, _matchingSeries, DateTime.Today); + + //Assert + result.Should().BeFalse(); + + Mocker.GetMock().Verify(c => c.DownloadReport(It.IsAny()), + Times.Never()); + } + } +} \ No newline at end of file diff --git a/NzbDrone.Core.Test/ProviderTests/SearchProviderTests/SearchFixture.cs b/NzbDrone.Core.Test/ProviderTests/SearchProviderTests/SearchFixture.cs 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/Datastore/Migrations/Migration20111125.cs b/NzbDrone.Core/Datastore/Migrations/Migration20111125.cs new file mode 100644 index 000000000..4a46828bf --- /dev/null +++ b/NzbDrone.Core/Datastore/Migrations/Migration20111125.cs @@ -0,0 +1,21 @@ +using System; +using System.Data; +using Migrator.Framework; + +namespace NzbDrone.Core.Datastore.Migrations +{ + + [Migration(20111125)] + public class Migration2011125 : Migration + { + public override void Up() + { + Database.AddColumn("Series", "IsDaily", DbType.Boolean, ColumnProperty.Null); + } + + public override void Down() + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/NzbDrone.Core/NzbDrone.Core.csproj b/NzbDrone.Core/NzbDrone.Core.csproj index 51c897420..b702b6240 100644 --- a/NzbDrone.Core/NzbDrone.Core.csproj +++ b/NzbDrone.Core/NzbDrone.Core.csproj @@ -204,6 +204,7 @@ + @@ -261,6 +262,7 @@ + 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/AppUpdateJob.cs b/NzbDrone.Core/Providers/Jobs/AppUpdateJob.cs index 34375b659..d7113969c 100644 --- a/NzbDrone.Core/Providers/Jobs/AppUpdateJob.cs +++ b/NzbDrone.Core/Providers/Jobs/AppUpdateJob.cs @@ -39,7 +39,7 @@ namespace NzbDrone.Core.Providers.Jobs public int DefaultInterval { - get { return 0; } + get { return 10080; } } public virtual void Start(ProgressNotification notification, int targetId, int secondaryTargetId) diff --git a/NzbDrone.Core/Providers/Jobs/BacklogSearchJob.cs b/NzbDrone.Core/Providers/Jobs/BacklogSearchJob.cs index a0c1d71c6..219f8964d 100644 --- a/NzbDrone.Core/Providers/Jobs/BacklogSearchJob.cs +++ b/NzbDrone.Core/Providers/Jobs/BacklogSearchJob.cs @@ -32,7 +32,7 @@ namespace NzbDrone.Core.Providers.Jobs public int DefaultInterval { - get { return 0; } + get { return 43200; } } public void Start(ProgressNotification notification, int targetId, int secondaryTargetId) diff --git a/NzbDrone.Core/Providers/Jobs/UpdateInfoJob.cs b/NzbDrone.Core/Providers/Jobs/UpdateInfoJob.cs index a0161b950..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() @@ -31,7 +34,7 @@ namespace NzbDrone.Core.Providers.Jobs public int DefaultInterval { - get { return 1440; } //Daily + get { return 720; } //Daily } public virtual void Start(ProgressNotification notification, int targetId, int secondaryTargetId) @@ -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/ReferenceDataProvider.cs b/NzbDrone.Core/Providers/ReferenceDataProvider.cs new file mode 100644 index 000000000..f80d4dacb --- /dev/null +++ b/NzbDrone.Core/Providers/ReferenceDataProvider.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using NLog; +using NzbDrone.Core.Providers.Core; +using PetaPoco; + +namespace NzbDrone.Core.Providers +{ + public class ReferenceDataProvider + { + private readonly IDatabase _database; + private readonly HttpProvider _httpProvider; + + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + + public ReferenceDataProvider(IDatabase database, HttpProvider httpProvider) + { + _database = database; + _httpProvider = httpProvider; + } + + public virtual void UpdateDailySeries() + { + //Update all series in DB + //DailySeries.csv + + var seriesIds = GetDailySeriesIds(); + + var dailySeriesString = String.Join(", ", seriesIds); + var sql = String.Format("UPDATE Series SET IsDaily = 1 WHERE SeriesId in ({0})", dailySeriesString); + + _database.Execute(sql); + } + + public virtual bool IsSeriesDaily(int seriesId) + { + return GetDailySeriesIds().Contains(seriesId); + } + + public List GetDailySeriesIds() + { + try + { + var dailySeries = _httpProvider.DownloadString("http://www.nzbdrone.com/DailySeries.csv"); + + var seriesIds = new List(); + + using (var reader = new StringReader(dailySeries)) + { + string line; + while ((line = reader.ReadLine()) != null) + { + int seriesId; + + if (Int32.TryParse(line, out seriesId)) + seriesIds.Add(seriesId); + } + } + + return seriesIds; + } + catch(Exception ex) + { + Logger.WarnException("Failed to get Daily Series", ex); + return new List(); + } + + } + } +} diff --git a/NzbDrone.Core/Providers/SearchProvider.cs b/NzbDrone.Core/Providers/SearchProvider.cs index 35eabbbf6..f0346bd9c 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 NLog; @@ -48,6 +49,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); @@ -94,6 +99,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); @@ -119,41 +128,26 @@ 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"; - if (ProcessSearchResults(notification, reports, series, episode.SeasonNumber, episode.EpisodeNumber).Count == 1) + if (!series.IsDaily && ProcessSearchResults(notification, reports, series, episode.SeasonNumber, episode.EpisodeNumber).Count == 1) + return true; + + if (series.IsDaily && ProcessSearchResults(notification, reports, series, episode.AirDate.Value)) return true; Logger.Warn("Unable to find {0} in any of indexers.", episode); @@ -182,15 +176,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)); } @@ -267,6 +269,54 @@ namespace NzbDrone.Core.Providers return successes; } + public bool ProcessSearchResults(ProgressNotification notification, IEnumerable reports, Series series, DateTime airDate) + { + foreach (var episodeParseResult in reports.OrderByDescending(c => c.Quality)) + { + try + { + 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) + 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) + continue; + + if (_inventoryProvider.IsQualityNeeded(episodeParseResult)) + { + 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); + + return true; + } + } + 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); + } + } + } + catch (Exception e) + { + Logger.ErrorException("An error has occurred while processing parse result items from " + episodeParseResult, e); + } + } + return false; + } + private List GetEpisodeNumberPrefixes(IEnumerable episodeNumbers) { var results = new List(); diff --git a/NzbDrone.Core/Repository/Series.cs b/NzbDrone.Core/Repository/Series.cs index 1d6a702c9..454179487 100644 --- a/NzbDrone.Core/Repository/Series.cs +++ b/NzbDrone.Core/Repository/Series.cs @@ -41,6 +41,8 @@ namespace NzbDrone.Core.Repository public string BannerUrl { get; set; } + public bool IsDaily { get; set; } + /// /// Gets or sets a value indicating whether this is hidden. /// diff --git a/NzbDrone.Web/Content/IndexerSettings.css b/NzbDrone.Web/Content/IndexerSettings.css index 617199c66..393b26243 100644 --- a/NzbDrone.Web/Content/IndexerSettings.css +++ b/NzbDrone.Web/Content/IndexerSettings.css @@ -34,18 +34,18 @@ .providerSection { float: left; - width: 270px; + width: 255px; margin: 2px; border:solid 1px #CCCCCD; display: inline-block; - overflow: hidden; - padding: 5px 5px 5px 5px; + overflow: auto; + padding: 3px; } .providerOptions label { margin-top: 10px; - margin-left: 7px; + margin-left: 3px; margin-right: 25px; float: left; font-weight: bold; @@ -58,7 +58,7 @@ padding:4px 2px; border:solid 1px #aacfe4; width:170px; - margin-right: 2px; + margin-right: 0px; } .providerOptions select @@ -80,7 +80,7 @@ input[type="checkbox"] #newznabProviders { - overflow: hidden; + overflow: auto; margin-top: 5px; margin-bottom: 10px; } \ No newline at end of file diff --git a/NzbDrone.Web/Content/QualitySettings.css b/NzbDrone.Web/Content/QualitySettings.css index 86ea2506e..c4483fa51 100644 --- a/NzbDrone.Web/Content/QualitySettings.css +++ b/NzbDrone.Web/Content/QualitySettings.css @@ -1,7 +1,13 @@ -#bottom +#top +{ + overflow: auto; +} + +#bottom { margin-top: -10px; margin-left: 15px; + overflow: auto; } #addItem @@ -17,6 +23,11 @@ margin-bottom: 5px; } +#profiles +{ + overflow: auto; +} + /* QualityProfileItem */ .quality-selectee { diff --git a/NzbDrone.Web/Views/Settings/EpisodeSorting.cshtml b/NzbDrone.Web/Views/Settings/EpisodeSorting.cshtml index cbeead9f4..26bf5e8f2 100644 --- a/NzbDrone.Web/Views/Settings/EpisodeSorting.cshtml +++ b/NzbDrone.Web/Views/Settings/EpisodeSorting.cshtml @@ -7,14 +7,9 @@ } @@ -81,12 +76,9 @@ @Html.DropDownListFor(m => m.MultiEpisodeStyle, Model.MultiEpisodeStyles, new { @class = "inputClass selectClass" }) -
-
-
-
-
-
+
+
+
diff --git a/NzbDrone.Web/Views/Settings/Growl.cshtml b/NzbDrone.Web/Views/Settings/Growl.cshtml index e48426bae..c000651e0 100644 --- a/NzbDrone.Web/Views/Settings/Growl.cshtml +++ b/NzbDrone.Web/Views/Settings/Growl.cshtml @@ -5,7 +5,7 @@ Layout = null; } -
+
diff --git a/NzbDrone.Web/Views/Settings/Indexers.cshtml b/NzbDrone.Web/Views/Settings/Indexers.cshtml index e9d39a0f8..3fabdd09b 100644 --- a/NzbDrone.Web/Views/Settings/Indexers.cshtml +++ b/NzbDrone.Web/Views/Settings/Indexers.cshtml @@ -2,9 +2,11 @@ @model NzbDrone.Web.Models.IndexerSettingsModel @section HeaderContent{ + } @section TitleContent{ @@ -39,7 +51,7 @@ Settings

NZBs.org

-
+
@@ -55,7 +67,7 @@ Settings

NZBMatrix

-
+
@@ -71,7 +83,7 @@ Settings

NZBsRus

-
+
@@ -87,7 +99,7 @@ Settings

Newsbin

-
+
@@ -103,13 +115,14 @@ Settings

Newznab

-
+
@Html.CheckBoxFor(m => m.NewznabEnabled, new { @class = "inputClass checkClass" }) -

-

+
+
+

Add Newznab Provider diff --git a/NzbDrone.Web/Views/Settings/Notifications.cshtml b/NzbDrone.Web/Views/Settings/Notifications.cshtml index f779f0869..2f559d1a8 100644 --- a/NzbDrone.Web/Views/Settings/Notifications.cshtml +++ b/NzbDrone.Web/Views/Settings/Notifications.cshtml @@ -8,9 +8,9 @@ .notifier { width: 560px; - border:solid 2px #CCCCCD; padding: 5px; margin-left: -8px; + overflow: auto; } .notifier h4 diff --git a/NzbDrone.Web/Views/Settings/Prowl.cshtml b/NzbDrone.Web/Views/Settings/Prowl.cshtml index 9677134ee..ebefe6801 100644 --- a/NzbDrone.Web/Views/Settings/Prowl.cshtml +++ b/NzbDrone.Web/Views/Settings/Prowl.cshtml @@ -5,7 +5,7 @@ Layout = null; } -
+
diff --git a/NzbDrone.Web/Views/Settings/Quality.cshtml b/NzbDrone.Web/Views/Settings/Quality.cshtml index 0133cd2ae..c586e1f14 100644 --- a/NzbDrone.Web/Views/Settings/Quality.cshtml +++ b/NzbDrone.Web/Views/Settings/Quality.cshtml @@ -14,7 +14,7 @@ Settings
@using (Html.BeginForm("SaveQuality", "Settings", FormMethod.Post, new { id = "form", name = "form" })) { -
+

Quality

@@ -69,14 +69,14 @@ Settings

-
+
-
+
@foreach (var item in Model.Profiles) { Html.RenderAction("GetQualityProfileView", item); diff --git a/NzbDrone.Web/Views/Settings/Smtp.cshtml b/NzbDrone.Web/Views/Settings/Smtp.cshtml index 1da419135..72bbda310 100644 --- a/NzbDrone.Web/Views/Settings/Smtp.cshtml +++ b/NzbDrone.Web/Views/Settings/Smtp.cshtml @@ -5,7 +5,7 @@ Layout = null; } -
+
diff --git a/NzbDrone.Web/Views/Settings/Twitter.cshtml b/NzbDrone.Web/Views/Settings/Twitter.cshtml index 9da92ad58..51079af87 100644 --- a/NzbDrone.Web/Views/Settings/Twitter.cshtml +++ b/NzbDrone.Web/Views/Settings/Twitter.cshtml @@ -5,7 +5,7 @@ Layout = null; } -
+
diff --git a/NzbDrone.Web/Views/Settings/Xbmc.cshtml b/NzbDrone.Web/Views/Settings/Xbmc.cshtml index f18eeb54d..57d556193 100644 --- a/NzbDrone.Web/Views/Settings/Xbmc.cshtml +++ b/NzbDrone.Web/Views/Settings/Xbmc.cshtml @@ -5,7 +5,7 @@ Layout = null; } -
+