diff --git a/NzbDrone.Core.Test/FluentTest.cs b/NzbDrone.Core.Test/FluentTest.cs index 8a703d3fa..797bc5a4a 100644 --- a/NzbDrone.Core.Test/FluentTest.cs +++ b/NzbDrone.Core.Test/FluentTest.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using FluentAssertions; using NUnit.Framework; @@ -164,5 +165,31 @@ namespace NzbDrone.Core.Test //Resolve result.Should().Be("http://www.nzbdrone.com"); } + + [Test] + public void MaxOrDefault_should_return_zero_when_collection_is_empty() + { + //Setup + + + //Act + var result = (new List()).MaxOrDefault(); + + //Resolve + result.Should().Be(0); + } + + [Test] + public void MaxOrDefault_should_return_max_when_collection_is_not_empty() + { + //Setup + var list = new List {6, 4, 5, 3, 8, 10}; + + //Act + var result = list.MaxOrDefault(); + + //Resolve + result.Should().Be(10); + } } } diff --git a/NzbDrone.Core.Test/JobTests/RecentBacklogSearchJobTest.cs b/NzbDrone.Core.Test/JobTests/RecentBacklogSearchJobTest.cs new file mode 100644 index 000000000..3a21b6ff4 --- /dev/null +++ b/NzbDrone.Core.Test/JobTests/RecentBacklogSearchJobTest.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +using FizzWare.NBuilder; +using Moq; +using NUnit.Framework; +using NzbDrone.Core.Model.Notification; +using NzbDrone.Core.Providers; +using NzbDrone.Core.Providers.Jobs; +using NzbDrone.Core.Repository; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Test.Common.AutoMoq; + +namespace NzbDrone.Core.Test.JobTests +{ + [TestFixture] + public class RecentBacklogSearchJobTest : CoreTest + { + [SetUp] + public void Setup() + { + + } + + [Test] + public void no_missing_epsiodes_should_not_trigger_any_search() + { + //Setup + var episodes = new List(); + + Mocker.GetMock() + .Setup(s => s.EpisodesWithoutFiles(true)).Returns(episodes); + + //Act + Mocker.Resolve().Start(MockNotification, 0, 0); + + //Assert + Mocker.GetMock().Verify(c => c.Start(MockNotification, It.IsAny(), 0), + Times.Never()); + } + + [Test] + public void should_only_process_missing_episodes_from_the_last_30_days() + { + //Setup + var episodes = Builder.CreateListOfSize(50) + .TheFirst(5) + .With(e => e.AirDate = DateTime.Today) + .TheNext(5) + .With(e => e.AirDate = DateTime.Today.AddDays(-1)) //Today + .TheNext(5) + .With(e => e.AirDate = DateTime.Today.AddDays(-5)) //Yeserday + .TheNext(5) + .With(e => e.AirDate = DateTime.Today.AddDays(-10)) + .TheNext(5) + .With(e => e.AirDate = DateTime.Today.AddDays(-15)) + .TheNext(5) + .With(e => e.AirDate = DateTime.Today.AddDays(-20)) + .TheNext(5) + .With(e => e.AirDate = DateTime.Today.AddDays(-25)) + .TheNext(5) + .With(e => e.AirDate = DateTime.Today.AddDays(-30)) + .TheNext(5) + .With(e => e.AirDate = DateTime.Today.AddDays(-31)) //31 Days + .TheNext(5) + .With(e => e.AirDate = DateTime.Today.AddDays(-35)) + .Build(); + + Mocker.GetMock() + .Setup(s => s.EpisodesWithoutFiles(true)).Returns(episodes); + + Mocker.GetMock().Setup(c => c.Start(It.IsAny(), It.IsAny(), 0)); + + //Act + Mocker.Resolve().Start(MockNotification, 0, 0); + + //Assert + Mocker.GetMock().Verify(c => c.Start(It.IsAny(), It.IsAny(), 0), + Times.Exactly(40)); + } + } +} diff --git a/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 1645b0f7d..ad6b33b72 100644 --- a/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -96,6 +96,7 @@ + diff --git a/NzbDrone.Core.Test/ProviderTests/EpisodeProviderTest.cs b/NzbDrone.Core.Test/ProviderTests/EpisodeProviderTest.cs index 036fb8a52..0b75cb2a0 100644 --- a/NzbDrone.Core.Test/ProviderTests/EpisodeProviderTest.cs +++ b/NzbDrone.Core.Test/ProviderTests/EpisodeProviderTest.cs @@ -326,6 +326,51 @@ namespace NzbDrone.Core.Test.ProviderTests mocker.VerifyAllMocks(); } + [Test] + public void RefreshEpisodeInfo_should_set_older_than_1900_to_null_for_existing_episodes() + { + //Arrange + const int seriesId = 71663; + + var fakeEpisode = Builder.CreateNew() + .With(e => e.TvDbEpisodeId = 12345) + .With(e => e.AirDate = DateTime.Today) + .Build(); + + var fakeTvDbEpisodes = Builder.CreateNew().With( + c => c.Episodes = + new List(Builder.CreateListOfSize(1) + .All() + .With(l => l.Language = new TvdbLanguage(0, "eng", "a")).And(e => e.FirstAired = DateTime.Now) + .TheFirst(1).With(e => e.FirstAired = new DateTime(1800, 1, 1)) + .Build()) + ).With(c => c.Id = seriesId).Build(); + + var fakeSeries = Builder.CreateNew().With(c => c.SeriesId = seriesId).Build(); + + var mocker = new AutoMoqer(); + + var db = TestDbHelper.GetEmptyDatabase(); + mocker.SetConstant(db); + + db.Insert(fakeSeries); + db.Insert(fakeEpisode); + + mocker.GetMock() + .Setup(c => c.GetSeries(seriesId, true)) + .Returns(fakeTvDbEpisodes); + + //Act + mocker.Resolve().RefreshEpisodeInfo(fakeSeries); + + //Assert + var storedEpisodes = mocker.Resolve().GetEpisodeBySeries(seriesId).ToList(); + storedEpisodes.Should().HaveCount(1); + storedEpisodes.Where(e => e.AirDate == null).Should().HaveCount(1); + + mocker.VerifyAllMocks(); + } + [Test] public void RefreshEpisodeInfo_ignore_episode_zero() { diff --git a/NzbDrone.Core.Test/ProviderTests/EpisodeProviderTest_GetEpisodesByParseResult.cs b/NzbDrone.Core.Test/ProviderTests/EpisodeProviderTest_GetEpisodesByParseResult.cs index c0e57037e..0db4ffd3b 100644 --- a/NzbDrone.Core.Test/ProviderTests/EpisodeProviderTest_GetEpisodesByParseResult.cs +++ b/NzbDrone.Core.Test/ProviderTests/EpisodeProviderTest_GetEpisodesByParseResult.cs @@ -1,16 +1,19 @@ // ReSharper disable RedundantUsingDirective +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.Repository; using NzbDrone.Core.Test.Framework; using NzbDrone.Test.Common.AutoMoq; +using PetaPoco; namespace NzbDrone.Core.Test.ProviderTests { @@ -255,6 +258,57 @@ namespace NzbDrone.Core.Test.ProviderTests episodes.Should().BeEmpty(); } + [Test] + public void GetEpisodeParseResult_should_return_single_episode_when_air_date_is_provided() + { + //Setup + var fakeEpisode = Builder.CreateListOfSize(1) + .All() + .With(e => e.AirDate = DateTime.Today) + .Build() + .ToList(); + + var fakeSeries = Builder.CreateNew() + .With(s => s.SeriesId = 1) + .Build(); + + Mocker.GetMock().Setup(s => s.Fetch(It.IsAny(), It.IsAny())) + .Returns(fakeEpisode); + + //Act + var episodes = Mocker.Resolve() + .GetEpisodesByParseResult(new EpisodeParseResult { AirDate = DateTime.Today, Series = fakeSeries }, true); + + //Assert + episodes.Should().HaveCount(1); + episodes.First().AirDate.Should().Be(DateTime.Today); + + Mocker.GetMock().Verify(v=> v.Insert(It.IsAny()), Times.Never()); + } + + [Test] + public void GetEpisodeParseResult_get_daily_should_add_new_episode() + { + //Setup + var fakeSeries = Builder.CreateNew() + .With(s => s.SeriesId = 1) + .Build(); + + Mocker.GetMock().Setup(s => s.Fetch(It.IsAny(), It.IsAny())) + .Returns(new List()); + + Mocker.GetMock().Setup(s => s.Insert(It.IsAny())) + .Returns(1); + + //Act + var episodes = Mocker.Resolve() + .GetEpisodesByParseResult(new EpisodeParseResult { AirDate = DateTime.Today, Series = fakeSeries }, true); + + //Assert + episodes.Should().HaveCount(1); + episodes.First().AirDate.Should().Be(DateTime.Today); + Mocker.GetMock().Verify(v => v.Insert(It.IsAny()), Times.Once()); + } } } \ No newline at end of file diff --git a/NzbDrone.Core.Test/ProviderTests/InventoryProvider_IsMonitoredTest.cs b/NzbDrone.Core.Test/ProviderTests/InventoryProvider_IsMonitoredTest.cs index 2911231c9..125718c5c 100644 --- a/NzbDrone.Core.Test/ProviderTests/InventoryProvider_IsMonitoredTest.cs +++ b/NzbDrone.Core.Test/ProviderTests/InventoryProvider_IsMonitoredTest.cs @@ -25,6 +25,7 @@ namespace NzbDrone.Core.Test.ProviderTests private Episode episode; private Episode episode2; private EpisodeParseResult parseResultSingle; + private EpisodeParseResult parseResultDaily; [SetUp] public void Setup() @@ -49,6 +50,14 @@ namespace NzbDrone.Core.Test.ProviderTests AirDate = DateTime.Now.AddDays(-12).Date, }; + parseResultDaily = new EpisodeParseResult() + { + CleanTitle = "Title", + Language = LanguageType.English, + Quality = new Quality(QualityTypes.Bluray720p, true), + AirDate = DateTime.Now.AddDays(-12).Date, + }; + episode = Builder.CreateNew() .With(c => c.EpisodeNumber = parseResultMulti.EpisodeNumbers[0]) @@ -239,6 +248,25 @@ namespace NzbDrone.Core.Test.ProviderTests mocker.VerifyAllMocks(); } + [Test] + public void IsMonitored_daily_not_ignored_should_return_true() + { + var mocker = new AutoMoqer(MockBehavior.Strict); + + mocker.GetMock() + .Setup(p => p.FindSeries(It.IsAny())) + .Returns(series); + + mocker.GetMock() + .Setup(p => p.GetEpisodesByParseResult(It.IsAny(), true)) + .Returns(new List { episode }); + + episode.Ignored = false; + + var result = mocker.Resolve().IsMonitored(parseResultDaily); + //Assert + result.Should().BeTrue(); + } } } \ No newline at end of file diff --git a/NzbDrone.Core/CentralDispatch.cs b/NzbDrone.Core/CentralDispatch.cs index 30e4686cc..8830485e0 100644 --- a/NzbDrone.Core/CentralDispatch.cs +++ b/NzbDrone.Core/CentralDispatch.cs @@ -95,6 +95,7 @@ namespace NzbDrone.Core Kernel.Bind().To().InSingletonScope(); Kernel.Bind().To().InSingletonScope(); Kernel.Bind().To().InSingletonScope(); + Kernel.Bind().To().InSingletonScope(); Kernel.Get().Initialize(); Kernel.Get().StartTimer(30); diff --git a/NzbDrone.Core/Fluent.cs b/NzbDrone.Core/Fluent.cs index fe611d249..f99c0d038 100644 --- a/NzbDrone.Core/Fluent.cs +++ b/NzbDrone.Core/Fluent.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Runtime.InteropServices; using System.Text; @@ -51,5 +52,15 @@ namespace NzbDrone.Core { return uri.AbsoluteUri.Remove(uri.AbsoluteUri.Length - String.Join("", uri.Segments).Length - uri.Query.Length); } + + public static int MaxOrDefault(this IEnumerable ints) + { + var intList = ints.ToList(); + + if (intList.Count() == 0) + return 0; + + return intList.Max(); + } } } diff --git a/NzbDrone.Core/NzbDrone.Core.csproj b/NzbDrone.Core/NzbDrone.Core.csproj index 794da4ac6..f4f155d70 100644 --- a/NzbDrone.Core/NzbDrone.Core.csproj +++ b/NzbDrone.Core/NzbDrone.Core.csproj @@ -235,6 +235,7 @@ + diff --git a/NzbDrone.Core/Providers/EpisodeProvider.cs b/NzbDrone.Core/Providers/EpisodeProvider.cs index 508abcf1c..597ed7746 100644 --- a/NzbDrone.Core/Providers/EpisodeProvider.cs +++ b/NzbDrone.Core/Providers/EpisodeProvider.cs @@ -125,6 +125,46 @@ namespace NzbDrone.Core.Providers { var result = new List(); + if (parseResult.AirDate.HasValue) + { + var episodeInfo = GetEpisode(parseResult.Series.SeriesId, parseResult.AirDate.Value); + + //if still null we should add the temp episode + if (episodeInfo == null && autoAddNew) + { + Logger.Debug("Episode {0} doesn't exist in db. adding it now.", parseResult); + episodeInfo = new Episode + { + SeriesId = parseResult.Series.SeriesId, + AirDate = parseResult.AirDate.Value, + Title = "TBD", + Overview = String.Empty + }; + + var episodesInSeries = GetEpisodeBySeries(parseResult.Series.SeriesId); + + //Find the current season number + var maxSeasonNumber = episodesInSeries.Select(s => s.SeasonNumber).MaxOrDefault(); + + //Set the season number + episodeInfo.SeasonNumber = (maxSeasonNumber == 0) ? 1 : maxSeasonNumber; + + //Find the latest episode number + var maxEpisodeNumber = episodesInSeries + .Where(w => w.SeasonNumber == episodeInfo.SeasonNumber) + .Select(s => s.EpisodeNumber).MaxOrDefault(); + + //Set the episode number to max + 1 + episodeInfo.EpisodeNumber = maxEpisodeNumber + 1; + + AddEpisode(episodeInfo); + } + + //Add to Result and Return (There will only be one episode to return) + result.Add(episodeInfo); + return result; + } + if (parseResult.EpisodeNumbers == null) return result; @@ -258,9 +298,10 @@ namespace NzbDrone.Core.Providers episodeToUpdate.Overview = episode.Overview; if (episode.FirstAired.Year > 1900) - { episodeToUpdate.AirDate = episode.FirstAired.Date; - } + + else + episodeToUpdate.AirDate = null; successCount++; } diff --git a/NzbDrone.Core/Providers/Jobs/BacklogSearchJob.cs b/NzbDrone.Core/Providers/Jobs/BacklogSearchJob.cs index aa53b9b5d..a0c1d71c6 100644 --- a/NzbDrone.Core/Providers/Jobs/BacklogSearchJob.cs +++ b/NzbDrone.Core/Providers/Jobs/BacklogSearchJob.cs @@ -59,6 +59,8 @@ namespace NzbDrone.Core.Providers.Jobs var countInDb = _episodeProvider.GetEpisodeNumbersBySeason(seriesId, seasonNumber).Count; + //Todo: Download a full season if more than n% is missing? + if (count != countInDb) { //Add the episodes to be processed manually diff --git a/NzbDrone.Core/Providers/Jobs/RecentBacklogSearchJob.cs b/NzbDrone.Core/Providers/Jobs/RecentBacklogSearchJob.cs new file mode 100644 index 000000000..801e138a9 --- /dev/null +++ b/NzbDrone.Core/Providers/Jobs/RecentBacklogSearchJob.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NLog; +using NzbDrone.Core.Model; +using NzbDrone.Core.Model.Notification; +using NzbDrone.Core.Model.Search; +using NzbDrone.Core.Repository; + +namespace NzbDrone.Core.Providers.Jobs +{ + public class RecentBacklogSearchJob : IJob + { + private readonly EpisodeProvider _episodeProvider; + private readonly EpisodeSearchJob _episodeSearchJob; + + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + + public RecentBacklogSearchJob(EpisodeProvider episodeProvider, EpisodeSearchJob episodeSearchJob) + { + _episodeProvider = episodeProvider; + _episodeSearchJob = episodeSearchJob; + } + + public string Name + { + get { return "Recent Backlog Search"; } + } + + public int DefaultInterval + { + get { return 1440; } + } + + public void Start(ProgressNotification notification, int targetId, int secondaryTargetId) + { + //Get episodes that are considered missing and aired in the last 30 days + var missingEpisodes = _episodeProvider.EpisodesWithoutFiles(true).Where(e => e.AirDate >= DateTime.Today.AddDays(-30)); + + Logger.Debug("Processing missing episodes from the last 30 days"); + //Process the list of remaining episodes, 1 by 1 + foreach (var episode in missingEpisodes) + { + _episodeSearchJob.Start(notification, episode.EpisodeId, 0); + } + } + } +} \ No newline at end of file diff --git a/NzbDrone.Core/Providers/SearchProvider.cs b/NzbDrone.Core/Providers/SearchProvider.cs index 2c92bfed6..3e07d6d1e 100644 --- a/NzbDrone.Core/Providers/SearchProvider.cs +++ b/NzbDrone.Core/Providers/SearchProvider.cs @@ -155,15 +155,12 @@ namespace NzbDrone.Core.Providers 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) + return true; - //TODO:fix this so when search returns more than one episode - //its populated with more than the original episode. - reports.ForEach(c => - { - c.Series = series; - }); - - return (ProcessSearchResults(notification, reports, series, episode.SeasonNumber, episode.EpisodeNumber).Count == 1); + 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; } public List PerformSearch(ProgressNotification notification, Series series, int seasonNumber, IList episodes = null) diff --git a/NzbDrone.Web/Scripts/NzbDrone/AutoComplete.js b/NzbDrone.Web/Scripts/NzbDrone/AutoComplete.js index d6ade9fef..9b520fe73 100644 --- a/NzbDrone.Web/Scripts/NzbDrone/AutoComplete.js +++ b/NzbDrone.Web/Scripts/NzbDrone/AutoComplete.js @@ -3,10 +3,19 @@ cache: false }); + bindAutoCompletes(); +}); + +// +$('.folderLookup:not(.ui-autocomplete-input), .seriesLookup:not(.ui-autocomplete-input), .localSeriesLookup:not(.ui-autocomplete-input)').live('focus', function (event) { + bindAutoCompletes(); +}); + +function bindAutoCompletes() { bindFolderAutoComplete(".folderLookup"); bindSeriesAutoComplete(".seriesLookup"); bindLocalSeriesAutoComplete(".localSeriesLookup"); -}); +} function bindFolderAutoComplete(selector) { diff --git a/NzbDrone.Web/Scripts/NzbDrone/addSeries.js b/NzbDrone.Web/Scripts/NzbDrone/addSeries.js index a00814c0d..5a93ca03b 100644 --- a/NzbDrone.Web/Scripts/NzbDrone/addSeries.js +++ b/NzbDrone.Web/Scripts/NzbDrone/addSeries.js @@ -74,7 +74,7 @@ function refreshRoot() { $('#rootDirs').html(data); }); reloadAddNew(); - reloadExistingSeries(); + reloadExistingSeries(); } diff --git a/NzbDrone.Web/Views/Series/Index.cshtml b/NzbDrone.Web/Views/Series/Index.cshtml index 03208a7e4..20f17f398 100644 --- a/NzbDrone.Web/Views/Series/Index.cshtml +++ b/NzbDrone.Web/Views/Series/Index.cshtml @@ -85,7 +85,7 @@ NzbDrone { commands.Edit().ButtonType(GridButtonType.Image); commands.Delete().ButtonType(GridButtonType.Image); - }).Title("Actions").Width(80); + }).Title("Actions").Width(90); }) .Editable(editor => editor.Mode(GridEditMode.PopUp))