diff --git a/NzbDrone.Core.Test/Files/SceneMappings.csv b/NzbDrone.Core.Test/Files/SceneMappings.csv new file mode 100644 index 000000000..4238b2ebb --- /dev/null +++ b/NzbDrone.Core.Test/Files/SceneMappings.csv @@ -0,0 +1,5 @@ +csinewyork,73696,CSI +csiny,73696,CSI +csi,72546,CSI +csilasvegas,72546,CSI +archer,110381,Archer \ No newline at end of file diff --git a/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 96228512e..5d3ccc7e0 100644 --- a/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -159,7 +159,7 @@ - + @@ -235,6 +235,9 @@ Designer Always + + Always + Always diff --git a/NzbDrone.Core.Test/ProviderTests/EpisodeProviderTest.cs b/NzbDrone.Core.Test/ProviderTests/EpisodeProviderTest.cs index 6d1f46385..86e2a637b 100644 --- a/NzbDrone.Core.Test/ProviderTests/EpisodeProviderTest.cs +++ b/NzbDrone.Core.Test/ProviderTests/EpisodeProviderTest.cs @@ -582,6 +582,57 @@ public void existing_episodes_keep_their_episodeId_file_id() updatedEpisodes.Should().OnlyContain(c => c.Ignored == true); } + [Test] + public void RefreshEpisodeInfo_should_ignore_new_episode_for_ignored_season() + { + //Arrange + const int seriesId = 71663; + const int episodeCount = 2; + + var fakeEpisode = Builder.CreateNew() + .With(e => e.SeasonNumber = 5) + .With(e => e.EpisodeNumber = 1) + .With(e => e.TvDbEpisodeId = 11) + .With(e => e.SeriesId = seriesId) + .With(e => e.Ignored = true) + .Build(); + + var tvdbSeries = Builder.CreateNew().With( + c => c.Episodes = + new List(Builder.CreateListOfSize(episodeCount). + All() + .With(l => l.Language = new TvdbLanguage(0, "eng", "a")) + .With(e => e.SeasonNumber = 5) + .TheFirst(1) + .With(e => e.EpisodeNumber = 1) + .With(e => e.Id = 11) + .TheNext(1) + .With(e => e.EpisodeNumber = 2) + .With(e => e.Id = 22) + .Build()) + ).With(c => c.Id = seriesId).Build(); + + var fakeSeries = Builder.CreateNew().With(c => c.SeriesId = seriesId).Build(); + + WithRealDb(); + + Db.Insert(fakeSeries); + Db.Insert(fakeEpisode); + + Mocker.GetMock() + .Setup(c => c.GetSeries(seriesId, true)) + .Returns(tvdbSeries); + + //Act + Mocker.Resolve().RefreshEpisodeInfo(fakeSeries); + + //Assert + var result = Mocker.Resolve().GetEpisodeBySeries(seriesId).ToList(); + Mocker.GetMock().VerifyAll(); + result.Should().HaveCount(episodeCount); + result.Where(e => e.Ignored).Should().HaveCount(episodeCount); + } + [Test] public void IsSeasonIgnored_should_return_true_if_all_episodes_ignored() { diff --git a/NzbDrone.Core.Test/ProviderTests/EpisodeProviderTest_GetEpisodesByParseResult.cs b/NzbDrone.Core.Test/ProviderTests/EpisodeProviderTest_GetEpisodesByParseResult.cs index 52183b8ac..eb95b1696 100644 --- a/NzbDrone.Core.Test/ProviderTests/EpisodeProviderTest_GetEpisodesByParseResult.cs +++ b/NzbDrone.Core.Test/ProviderTests/EpisodeProviderTest_GetEpisodesByParseResult.cs @@ -127,7 +127,6 @@ public void Multi_GetSeason_Episode_Exists() ep.Should().HaveCount(2); Db.Fetch().Should().HaveCount(2); ep.First().ShouldHave().AllPropertiesBut(e => e.Series); - parseResult.EpisodeTitle.Should().Be(fakeEpisode.Title); } [Test] @@ -313,5 +312,77 @@ public void GetEpisodeParseResult_get_daily_should_not_add_new_episode_when_auto episodes.Should().BeEmpty(); Db.Fetch().Should().BeEmpty(); } + + [Test] + public void GetEpisodeParseResult_should_return_multiple_titles_for_multiple_episodes() + { + WithRealDb(); + + var fakeEpisode = Builder.CreateNew() + .With(e => e.SeriesId = 1) + .With(e => e.SeasonNumber = 2) + .With(e => e.EpisodeNumber = 10) + .With(e => e.Title = "Title1") + .Build(); + + var fakeEpisode2 = Builder.CreateNew() + .With(e => e.SeriesId = 1) + .With(e => e.SeasonNumber = 2) + .With(e => e.EpisodeNumber = 11) + .With(e => e.Title = "Title2") + .Build(); + + var fakeSeries = Builder.CreateNew().Build(); + + Db.Insert(fakeEpisode); + Db.Insert(fakeEpisode2); + Db.Insert(fakeSeries); + + var parseResult = new EpisodeParseResult + { + Series = fakeSeries, + SeasonNumber = 2, + EpisodeNumbers = new List { fakeEpisode.EpisodeNumber, fakeEpisode2.EpisodeNumber } + }; + + var ep = Mocker.Resolve().GetEpisodesByParseResult(parseResult); + + ep.Should().HaveCount(2); + Db.Fetch().Should().HaveCount(2); + ep.First().ShouldHave().AllPropertiesBut(e => e.Series); + parseResult.EpisodeTitle.Should().Be(fakeEpisode.Title + " + " + fakeEpisode2.Title); + } + + [Test] + public void GetEpisodeParseResult_should_return_single_title_for_single_episode() + { + WithRealDb(); + + var fakeEpisode = Builder.CreateNew() + .With(e => e.SeriesId = 1) + .With(e => e.SeasonNumber = 2) + .With(e => e.EpisodeNumber = 10) + .With(e => e.Title = "Title1") + .Build(); + + var fakeSeries = Builder.CreateNew().Build(); + + Db.Insert(fakeEpisode); + Db.Insert(fakeSeries); + + var parseResult = new EpisodeParseResult + { + Series = fakeSeries, + SeasonNumber = 2, + EpisodeNumbers = new List { fakeEpisode.EpisodeNumber } + }; + + var ep = Mocker.Resolve().GetEpisodesByParseResult(parseResult); + + ep.Should().HaveCount(1); + Db.Fetch().Should().HaveCount(1); + ep.First().ShouldHave().AllPropertiesBut(e => e.Series); + parseResult.EpisodeTitle.Should().Be(fakeEpisode.Title); + } } } diff --git a/NzbDrone.Core.Test/ProviderTests/InventoryProvider_IsAcceptableSizeTest.cs b/NzbDrone.Core.Test/ProviderTests/InventoryProvider_IsAcceptableSizeTest.cs index ab6048a0a..bf218c914 100644 --- a/NzbDrone.Core.Test/ProviderTests/InventoryProvider_IsAcceptableSizeTest.cs +++ b/NzbDrone.Core.Test/ProviderTests/InventoryProvider_IsAcceptableSizeTest.cs @@ -364,5 +364,28 @@ public void IsAcceptableSize_true_unlimited_60_minute() //Assert result.Should().BeTrue(); } + + [Test] + public void IsAcceptableSize_should_treat_daily_series_as_single_episode() + { + parseResultSingle.Series = series60minutes; + parseResultSingle.Size = 300.Megabytes(); + parseResultSingle.AirDate = DateTime.Today; + parseResultSingle.EpisodeNumbers = null; + + qualityType.MaxSize = (int)600.Megabytes(); + + Mocker.GetMock().Setup(s => s.Get(1)).Returns(qualityType); + + Mocker.GetMock().Setup( + s => s.IsFirstOrLastEpisodeOfSeason(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(true); + + //Act + bool result = Mocker.Resolve().IsAcceptableSize(parseResultSingle); + + //Assert + result.Should().BeTrue(); + } } } \ No newline at end of file diff --git a/NzbDrone.Core.Test/ProviderTests/JobProviderTests/JobProviderFixture.cs b/NzbDrone.Core.Test/ProviderTests/JobProviderTests/JobProviderFixture.cs index 55fe4b26c..82b2fe868 100644 --- a/NzbDrone.Core.Test/ProviderTests/JobProviderTests/JobProviderFixture.cs +++ b/NzbDrone.Core.Test/ProviderTests/JobProviderTests/JobProviderFixture.cs @@ -458,4 +458,4 @@ public void trygin_to_queue_unregistered_job_should_fail() } -} \ No newline at end of file +} diff --git a/NzbDrone.Core.Test/ProviderTests/SabProviderTest.cs b/NzbDrone.Core.Test/ProviderTests/SabProviderTest.cs index 4297c63d3..29dbb0bea 100644 --- a/NzbDrone.Core.Test/ProviderTests/SabProviderTest.cs +++ b/NzbDrone.Core.Test/ProviderTests/SabProviderTest.cs @@ -321,6 +321,32 @@ public void sab_season_title(bool proper, string expected) Assert.AreEqual(expected, actual); } + [TestCase(true, "My Series Name - 2011-12-01 - My Episode Title [Bluray720p] [Proper]")] + [TestCase(false, "My Series Name - 2011-12-01 - My Episode Title [Bluray720p]")] + public void sab_daily_series_title(bool proper, string expected) + { + var mocker = new AutoMoqer(); + + var series = Builder.CreateNew() + .With(c => c.Path = @"d:\tv shows\My Series Name") + .With(c => c.IsDaily = true) + .Build(); + + var parsResult = new EpisodeParseResult + { + AirDate = new DateTime(2011, 12,1), + Quality = new Quality(QualityTypes.Bluray720p, proper), + Series = series, + EpisodeTitle = "My Episode Title", + }; + + //Act + var actual = mocker.Resolve().GetSabTitle(parsResult); + + //Assert + Assert.AreEqual(expected, actual); + } + [Test] [Explicit] public void AddNewzbingByUrlSuccess() diff --git a/NzbDrone.Core.Test/SceneMappingTest.cs b/NzbDrone.Core.Test/ProviderTests/SceneMappingProviderTest.cs similarity index 52% rename from NzbDrone.Core.Test/SceneMappingTest.cs rename to NzbDrone.Core.Test/ProviderTests/SceneMappingProviderTest.cs index 5c801f892..bf520f615 100644 --- a/NzbDrone.Core.Test/SceneMappingTest.cs +++ b/NzbDrone.Core.Test/ProviderTests/SceneMappingProviderTest.cs @@ -1,7 +1,13 @@  +using System; +using System.IO; +using System.Net; 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.Test.Framework; using NzbDrone.Test.Common.AutoMoq; @@ -10,8 +16,24 @@ namespace NzbDrone.Core.Test { [TestFixture] // ReSharper disable InconsistentNaming - public class SceneMappingTest : CoreTest + public class SceneMappingProviderTest : CoreTest { + private const string SceneMappingUrl = "http://www.nzbdrone.com/SceneMappings.csv"; + + private void WithValidCsv() + { + Mocker.GetMock() + .Setup(s => s.DownloadString(SceneMappingUrl)) + .Returns(File.ReadAllText(@".\Files\SceneMappings.csv")); + } + + private void WithErrorDownloadingCsv() + { + Mocker.GetMock() + .Setup(s => s.DownloadString(SceneMappingUrl)) + .Throws(new WebException()); + } + [Test] public void GetSceneName_exists() { @@ -136,5 +158,103 @@ public void GetSceneName_multiple_clean_names() //Assert Assert.AreEqual(fakeMap.SceneName, sceneName); } + + [Test] + public void UpdateMappings_should_add_all_mappings_to_database() + { + //Setup + WithRealDb(); + WithValidCsv(); + + //Act + Mocker.Resolve().UpdateMappings(); + + //Assert + Mocker.Verify(v => v.DownloadString(It.IsAny()), Times.Once()); + var result = Db.Fetch(); + result.Should().HaveCount(5); + } + + [Test] + public void UpdateMappings_should_overwrite_existing_mappings() + { + //Setup + var fakeMap = Builder.CreateNew() + .With(f => f.SeriesId = 12345) + .With(f => f.SceneName = "Law and Order") + .With(f => f.SceneName = "laworder") + .Build(); + + WithRealDb(); + WithValidCsv(); + Db.Insert(fakeMap); + + //Act + Mocker.Resolve().UpdateMappings(); + + //Assert + Mocker.Verify(v => v.DownloadString(It.IsAny()), Times.Once()); + var result = Db.Fetch(); + result.Should().HaveCount(5); + } + + [Test] + public void UpdateMappings_should_not_delete_if_csv_download_fails() + { + //Setup + var fakeMap = Builder.CreateNew() + .With(f => f.SeriesId = 12345) + .With(f => f.SceneName = "Law and Order") + .With(f => f.SceneName = "laworder") + .Build(); + + WithRealDb(); + WithErrorDownloadingCsv(); + Db.Insert(fakeMap); + + //Act + Mocker.Resolve().UpdateMappings(); + + //Assert + Mocker.Verify(v => v.DownloadString(It.IsAny()), Times.Once()); + var result = Db.Fetch(); + result.Should().HaveCount(1); + } + + [Test] + public void UpdateIfEmpty_should_not_update_if_count_is_not_zero() + { + //Setup + var fakeMap = Builder.CreateNew() + .With(f => f.SeriesId = 12345) + .With(f => f.SceneName = "Law and Order") + .With(f => f.SceneName = "laworder") + .Build(); + + WithRealDb(); + Db.Insert(fakeMap); + + //Act + Mocker.Resolve().UpdateIfEmpty(); + + //Assert + Mocker.Verify(v => v.DownloadString(It.IsAny()), Times.Never()); + } + + [Test] + public void UpdateIfEmpty_should_update_if_count_is_zero() + { + //Setup + WithRealDb(); + WithValidCsv(); + + //Act + Mocker.Resolve().UpdateIfEmpty(); + + //Assert + Mocker.Verify(v => v.DownloadString(SceneMappingUrl), Times.Once()); + var result = Db.Fetch(); + result.Should().HaveCount(5); + } } } diff --git a/NzbDrone.Core/Providers/EpisodeProvider.cs b/NzbDrone.Core/Providers/EpisodeProvider.cs index ab9747f82..e99437667 100644 --- a/NzbDrone.Core/Providers/EpisodeProvider.cs +++ b/NzbDrone.Core/Providers/EpisodeProvider.cs @@ -161,6 +161,7 @@ public virtual IList GetEpisodesByParseResult(EpisodeParseResult parseR if (episodeInfo != null) { result.Add(episodeInfo); + parseResult.EpisodeTitle = episodeInfo.Title; } return result; @@ -199,7 +200,8 @@ public virtual IList GetEpisodesByParseResult(EpisodeParseResult parseR if (episodeInfo != null) { result.Add(episodeInfo); - parseResult.EpisodeTitle = episodeInfo.Title; + parseResult.EpisodeTitle += String.Format(" + {0}", episodeInfo.Title); + parseResult.EpisodeTitle = parseResult.EpisodeTitle.Trim('+', ' '); } else { diff --git a/NzbDrone.Core/Providers/InventoryProvider.cs b/NzbDrone.Core/Providers/InventoryProvider.cs index fd9a99b73..2ebd78d1f 100644 --- a/NzbDrone.Core/Providers/InventoryProvider.cs +++ b/NzbDrone.Core/Providers/InventoryProvider.cs @@ -156,12 +156,13 @@ public virtual bool IsAcceptableSize(EpisodeParseResult parseResult) //Multiply maxSize by Series.Runtime maxSize = maxSize * series.Runtime; - //Multiply maxSize by the number of episodes parsed - maxSize = maxSize * parseResult.EpisodeNumbers.Count; + //Multiply maxSize by the number of episodes parsed (if EpisodeNumbers is null it will be treated as a single episode) + if (parseResult.EpisodeNumbers != null) + maxSize = maxSize * parseResult.EpisodeNumbers.Count; //Check if there was only one episode parsed //and it is the first or last episode of the season - if (parseResult.EpisodeNumbers.Count == 1 && + if (parseResult.EpisodeNumbers != null && parseResult.EpisodeNumbers.Count == 1 && _episodeProvider.IsFirstOrLastEpisodeOfSeason(series.SeriesId, parseResult.SeasonNumber, parseResult.EpisodeNumbers[0])) { diff --git a/NzbDrone.Core/Providers/SabProvider.cs b/NzbDrone.Core/Providers/SabProvider.cs index 8d9d92126..2562926ca 100644 --- a/NzbDrone.Core/Providers/SabProvider.cs +++ b/NzbDrone.Core/Providers/SabProvider.cs @@ -117,6 +117,17 @@ public virtual String GetSabTitle(EpisodeParseResult parseResult) return seasonResult; } + if (parseResult.Series.IsDaily) + { + var dailyResult = String.Format("{0} - {1:yyyy-MM-dd} - {2} [{3}]", new DirectoryInfo(parseResult.Series.Path).Name, + parseResult.AirDate, parseResult.EpisodeTitle, parseResult.Quality.QualityType); + + if (parseResult.Quality.Proper) + dailyResult += " [Proper]"; + + return dailyResult; + } + //Show Name - 1x01-1x02 - Episode Name //Show Name - 1x01 - Episode Name var episodeString = new List(); diff --git a/NzbDrone.Core/Providers/SceneMappingProvider.cs b/NzbDrone.Core/Providers/SceneMappingProvider.cs index 9ef2b0f53..313d0c6b5 100644 --- a/NzbDrone.Core/Providers/SceneMappingProvider.cs +++ b/NzbDrone.Core/Providers/SceneMappingProvider.cs @@ -29,7 +29,7 @@ public virtual bool UpdateMappings() { try { - var mapping = _httpProvider.DownloadString("http://vps.nzbdrone.com/SceneMappings.csv"); + var mapping = _httpProvider.DownloadString("http://www.nzbdrone.com/SceneMappings.csv"); var newMaps = new List(); using (var reader = new StringReader(mapping)) @@ -59,7 +59,7 @@ public virtual bool UpdateMappings() catch (Exception ex) { - Logger.InfoException("Failed to Update Scene Mappings", ex); + Logger.InfoException("Failed to Update Scene Mappings:", ex); return false; } return true; @@ -67,6 +67,8 @@ public virtual bool UpdateMappings() public virtual string GetSceneName(int seriesId) { + UpdateIfEmpty(); + var item = _database.FirstOrDefault("WHERE SeriesId = @0", seriesId); if (item == null) @@ -77,6 +79,8 @@ public virtual string GetSceneName(int seriesId) public virtual Nullable GetSeriesId(string cleanName) { + UpdateIfEmpty(); + var item = _database.SingleOrDefault("WHERE CleanTitle = @0", cleanName); if (item == null) @@ -84,5 +88,13 @@ public virtual Nullable GetSeriesId(string cleanName) return item.SeriesId; } + + public void UpdateIfEmpty() + { + var count = _database.ExecuteScalar("SELECT COUNT(*) FROM SceneMappings"); + + if (count == 0) + UpdateMappings(); + } } } diff --git a/NzbDrone.Core/Providers/SearchProvider.cs b/NzbDrone.Core/Providers/SearchProvider.cs index f0346bd9c..6de2b72e4 100644 --- a/NzbDrone.Core/Providers/SearchProvider.cs +++ b/NzbDrone.Core/Providers/SearchProvider.cs @@ -131,23 +131,21 @@ public virtual bool EpisodeSearch(ProgressNotification notification, int episode notification.CurrentMessage = "Searching for " + episode; - var series = _seriesProvider.GetSeries(episode.SeriesId); - if (episode.Series.IsDaily && !episode.AirDate.HasValue) { Logger.Warn("AirDate is not Valid for: {0}", episode); return false; } - var reports = PerformSearch(notification, series, episode.SeasonNumber, new List { episode }); + 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 (!series.IsDaily && ProcessSearchResults(notification, reports, series, episode.SeasonNumber, episode.EpisodeNumber).Count == 1) + if (!episode.Series.IsDaily && ProcessSearchResults(notification, reports, episode.Series, episode.SeasonNumber, episode.EpisodeNumber).Count == 1) return true; - if (series.IsDaily && ProcessSearchResults(notification, reports, series, episode.AirDate.Value)) + if (episode.Series.IsDaily && ProcessSearchResults(notification, reports, episode.Series, episode.AirDate.Value)) return true; Logger.Warn("Unable to find {0} in any of indexers.", episode); diff --git a/NzbDrone.Web/Controllers/SeriesController.cs b/NzbDrone.Web/Controllers/SeriesController.cs index 4a461565a..f891b22d2 100644 --- a/NzbDrone.Web/Controllers/SeriesController.cs +++ b/NzbDrone.Web/Controllers/SeriesController.cs @@ -93,7 +93,7 @@ public ActionResult _AjaxSeasonGrid(int seriesId, int seasonNumber) { using (MiniProfiler.StepStatic("Controller")) { - var episodes = GetEpisodeModels(_episodeProvider.GetEpisodesBySeason(seriesId, seasonNumber)); + var episodes = GetEpisodeModels(_episodeProvider.GetEpisodesBySeason(seriesId, seasonNumber)).OrderByDescending(e => e.EpisodeNumber); return View(new GridModel(episodes)); } } diff --git a/NzbDrone.Web/Views/Series/Details.cshtml b/NzbDrone.Web/Views/Series/Details.cshtml index 22a57044e..fe08cc4f1 100644 --- a/NzbDrone.Web/Views/Series/Details.cshtml +++ b/NzbDrone.Web/Views/Series/Details.cshtml @@ -126,18 +126,12 @@ .Width(80); }) .DetailView(detailView => detailView.ClientTemplate("
<#= Overview #>
<#= Path #>
")) - .Sortable(rows => rows.OrderBy(epSort => epSort.Add(c => c.EpisodeNumber).Descending()).Enabled(false)) + //.Sortable(rows => rows.OrderBy(epSort => epSort.Add(c => c.EpisodeNumber).Descending()).Enabled(false)) .Footer(true) .DataBinding( d => d.Ajax().Select("_AjaxSeasonGrid", "Series", new RouteValueDictionary { { "seriesId", Model.SeriesId }, { "seasonNumber", season } })) - @*.ToolBar(toolbar => toolbar.Template(@ - - ))*@ .ClientEvents(clientEvents => { clientEvents.OnRowDataBound("grid_rowBound");