From c708b5ce1a8e67a721d1cf8389270afcba13794b Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Wed, 16 Oct 2013 17:20:28 -0700 Subject: [PATCH 01/31] Only run InheritFolderPermissions on Windows --- .../MediaFiles/EpisodeFileMovingService.cs | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs b/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs index e4e58c6ed..5981475a0 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs @@ -3,6 +3,7 @@ using System.Linq; using NLog; using NzbDrone.Common; +using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Organizer; using NzbDrone.Core.Parser.Model; @@ -84,15 +85,19 @@ private void MoveFile(EpisodeFile episodeFile, Series series, string destination _diskProvider.SetFolderWriteTime(seasonFolder, episodeFile.DateAdded); } - //Wrapped in Try/Catch to prevent this from causing issues with remote NAS boxes, the move worked, which is more important. - try + //We should only run this on Windows + if (OsInfo.IsWindows) { - _diskProvider.InheritFolderPermissions(destinationFilename); - } - catch (UnauthorizedAccessException ex) - { - _logger.Debug("Unable to apply folder permissions to: ", destinationFilename); - _logger.TraceException(ex.Message, ex); + //Wrapped in Try/Catch to prevent this from causing issues with remote NAS boxes, the move worked, which is more important. + try + { + _diskProvider.InheritFolderPermissions(destinationFilename); + } + catch (UnauthorizedAccessException ex) + { + _logger.Debug("Unable to apply folder permissions to: ", destinationFilename); + _logger.TraceException(ex.Message, ex); + } } } } From 3586d59d6cf8a2124e9acf13eb7f17b2b08b7383 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 17 Oct 2013 11:25:08 -0700 Subject: [PATCH 02/31] Fixed: Now able to parse series names that use underscores instead of spaces --- src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs | 2 ++ src/NzbDrone.Core/Parser/Parser.cs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs index d74c5aedf..da0be0eb0 100644 --- a/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs @@ -80,6 +80,7 @@ public class ParserFixture : CoreTest [TestCase("(Game of Thrones s03 e - \"Game of Thrones Season 3 Episode 10\"", "Game of Thrones", 3, 10)] [TestCase("House.Hunters.International.S05E607.720p.hdtv.x264", "House.Hunters.International", 5, 607)] [TestCase("Adventure.Time.With.Finn.And.Jake.S01E20.720p.BluRay.x264-DEiMOS", "Adventure.Time.With.Finn.And.Jake", 1, 20)] + [TestCase("Hostages.S01E04.2-45.PM.[HDTV-720p].mkv", "Hostages", 1, 4)] public void ParseTitle_single(string postTitle, string title, int seasonNumber, int episodeNumber) { var result = Parser.Parser.ParseTitle(postTitle); @@ -230,6 +231,7 @@ public void full_season_release_parse(string postTitle, string title, int season [TestCase("The.Daily.Show", "dailyshow")] [TestCase("Castle (2009)", "castle2009")] [TestCase("Parenthood.2010", "parenthood2010")] + [TestCase("Law_and_Order_SVU", "lawordersvu")] public void series_name_normalize(string parsedSeriesName, string seriesName) { var result = parsedSeriesName.CleanSeriesTitle(); diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 85ef1be7c..d98429894 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -64,7 +64,7 @@ public static class Parser RegexOptions.IgnoreCase | RegexOptions.Compiled) }; - private static readonly Regex NormalizeRegex = new Regex(@"((^|\W)(a|an|the|and|or|of)($|\W|_))|\W|_|(?:(?<=[^0-9]+)|\b)(?!(?:19\d{2}|20\d{2}))\d+(?=[^0-9ip]+|\b)", + private static readonly Regex NormalizeRegex = new Regex(@"((^|\W|_)(a|an|the|and|or|of)($|\W|_))|\W|_|(?:(?<=[^0-9]+)|\b)(?!(?:19\d{2}|20\d{2}))\d+(?=[^0-9ip]+|\b)", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex SimpleTitleRegex = new Regex(@"480[i|p]|720[i|p]|1080[i|p]|[x|h|x\s|h\s]264|DD\W?5\W1|\<|\>|\?|\*|\:|\|", From e7780af212b812eec8d762ec904414c5a1e59944 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 17 Oct 2013 18:28:30 -0700 Subject: [PATCH 03/31] Better name from LocalEpisode in EpisodeImportedEvent --- src/NzbDrone.Core/History/HistoryService.cs | 6 +++--- src/NzbDrone.Core/MediaFiles/Events/EpisodeImportedEvent.cs | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/NzbDrone.Core/History/HistoryService.cs b/src/NzbDrone.Core/History/HistoryService.cs index ed09ae94e..09b372b2c 100644 --- a/src/NzbDrone.Core/History/HistoryService.cs +++ b/src/NzbDrone.Core/History/HistoryService.cs @@ -81,13 +81,13 @@ public void Handle(EpisodeGrabbedEvent message) public void Handle(EpisodeImportedEvent message) { - foreach (var episode in message.DroppedEpisode.Episodes) + foreach (var episode in message.EpisodeInfo.Episodes) { var history = new History { EventType = HistoryEventType.DownloadFolderImported, Date = DateTime.UtcNow, - Quality = message.DroppedEpisode.Quality, + Quality = message.EpisodeInfo.Quality, SourceTitle = message.ImportedEpisode.SceneName, SeriesId = message.ImportedEpisode.SeriesId, EpisodeId = episode.Id @@ -95,7 +95,7 @@ public void Handle(EpisodeImportedEvent message) //Won't have a value since we publish this event before saving to DB. //history.Data.Add("FileId", message.ImportedEpisode.Id.ToString()); - history.Data.Add("DroppedPath", message.DroppedEpisode.Path); + history.Data.Add("DroppedPath", message.EpisodeInfo.Path); history.Data.Add("ImportedPath", message.ImportedEpisode.Path); _historyRepository.Insert(history); diff --git a/src/NzbDrone.Core/MediaFiles/Events/EpisodeImportedEvent.cs b/src/NzbDrone.Core/MediaFiles/Events/EpisodeImportedEvent.cs index 2f166b069..38db811a6 100644 --- a/src/NzbDrone.Core/MediaFiles/Events/EpisodeImportedEvent.cs +++ b/src/NzbDrone.Core/MediaFiles/Events/EpisodeImportedEvent.cs @@ -5,12 +5,12 @@ namespace NzbDrone.Core.MediaFiles.Events { public class EpisodeImportedEvent : IEvent { - public LocalEpisode DroppedEpisode { get; private set; } + public LocalEpisode EpisodeInfo { get; private set; } public EpisodeFile ImportedEpisode { get; private set; } - public EpisodeImportedEvent(LocalEpisode droppedEpisode, EpisodeFile importedEpisode) + public EpisodeImportedEvent(LocalEpisode episodeInfo, EpisodeFile importedEpisode) { - DroppedEpisode = droppedEpisode; + EpisodeInfo = episodeInfo; ImportedEpisode = importedEpisode; } } From aa26d68f183b32658463821892d35601a5e085ee Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 18 Oct 2013 17:48:14 -0700 Subject: [PATCH 04/31] Updating manually now uses a command so it shows progress --- src/NzbDrone.Api/Update/UpdateModule.cs | 11 ----------- src/NzbDrone.Core/NzbDrone.Core.csproj | 1 + .../Update/Commands/InstallUpdateCommand.cs | 13 +++++++++++++ src/NzbDrone.Core/Update/InstallUpdateService.cs | 7 ++++++- src/UI/System/Update/UpdateItemView.js | 7 ++++--- 5 files changed, 24 insertions(+), 15 deletions(-) create mode 100644 src/NzbDrone.Core/Update/Commands/InstallUpdateCommand.cs diff --git a/src/NzbDrone.Api/Update/UpdateModule.cs b/src/NzbDrone.Api/Update/UpdateModule.cs index 2ade0b76d..c35f3f50f 100644 --- a/src/NzbDrone.Api/Update/UpdateModule.cs +++ b/src/NzbDrone.Api/Update/UpdateModule.cs @@ -22,7 +22,6 @@ public UpdateModule(IRecentUpdateProvider recentUpdateProvider, _recentUpdateProvider = recentUpdateProvider; _installUpdateService = installUpdateService; GetResourceAll = GetRecentUpdates; - Post["/"] = x=> InstallUpdate(); } private List GetRecentUpdates() @@ -46,16 +45,6 @@ private List GetRecentUpdates() return resources; } - - private Response InstallUpdate() - { - var updateResource = Request.Body.FromJson(); - - var updatePackage = updateResource.InjectTo(); - _installUpdateService.InstallUpdate(updatePackage); - - return updateResource.AsResponse(); - } } public class UpdateResource : RestResource diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 5c25f088e..3906a937b 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -570,6 +570,7 @@ + diff --git a/src/NzbDrone.Core/Update/Commands/InstallUpdateCommand.cs b/src/NzbDrone.Core/Update/Commands/InstallUpdateCommand.cs new file mode 100644 index 000000000..8dfe88f1c --- /dev/null +++ b/src/NzbDrone.Core/Update/Commands/InstallUpdateCommand.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NzbDrone.Core.Messaging.Commands; + +namespace NzbDrone.Core.Update.Commands +{ + public class InstallUpdateCommand : Command + { + public UpdatePackage UpdatePackage { get; set; } + } +} diff --git a/src/NzbDrone.Core/Update/InstallUpdateService.cs b/src/NzbDrone.Core/Update/InstallUpdateService.cs index 6b4b10ab3..15b1e8bad 100644 --- a/src/NzbDrone.Core/Update/InstallUpdateService.cs +++ b/src/NzbDrone.Core/Update/InstallUpdateService.cs @@ -16,7 +16,7 @@ public interface IInstallUpdates void InstallUpdate(UpdatePackage updatePackage); } - public class InstallUpdateService : IInstallUpdates, IExecute + public class InstallUpdateService : IInstallUpdates, IExecute, IExecute { private readonly ICheckUpdateService _checkUpdateService; private readonly Logger _logger; @@ -89,5 +89,10 @@ public void Execute(ApplicationUpdateCommand message) InstallUpdate(latestAvailable); } } + + public void Execute(InstallUpdateCommand message) + { + InstallUpdate(message.UpdatePackage); + } } } diff --git a/src/UI/System/Update/UpdateItemView.js b/src/UI/System/Update/UpdateItemView.js index 79186cd65..362b76690 100644 --- a/src/UI/System/Update/UpdateItemView.js +++ b/src/UI/System/Update/UpdateItemView.js @@ -2,8 +2,9 @@ define( [ - 'marionette' - ], function (Marionette) { + 'marionette', + 'Commands/CommandController' + ], function (Marionette, CommandController) { return Marionette.ItemView.extend({ template: 'System/Update/UpdateItemViewTemplate', @@ -12,7 +13,7 @@ define( }, _installUpdate: function () { - this.model.save(); + CommandController.Execute('installUpdate', { updatePackage: this.model.toJSON() }); } }); }); From f14fff676e53b024e674b6705b41c17e31f88c91 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 20 Oct 2013 11:01:30 -0700 Subject: [PATCH 05/31] Fixed some UI issues Fixed: Add folder won't show loading when there aren't any folders --- src/UI/AddSeries/AddSeriesLayout.js | 5 ++++- src/UI/AddSeries/RootFolders/Layout.js | 2 +- src/UI/Commands/CommandController.js | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/UI/AddSeries/AddSeriesLayout.js b/src/UI/AddSeries/AddSeriesLayout.js index 23c21d2ed..1b778546d 100644 --- a/src/UI/AddSeries/AddSeriesLayout.js +++ b/src/UI/AddSeries/AddSeriesLayout.js @@ -37,7 +37,10 @@ define( initialize: function () { QualityProfileCollection.fetch(); - RootFolderCollection.fetch(); + RootFolderCollection.fetch() + .done(function () { + RootFolderCollection.synced = true; + }); }, onShow: function () { diff --git a/src/UI/AddSeries/RootFolders/Layout.js b/src/UI/AddSeries/RootFolders/Layout.js index 795f62edc..3b54c90a1 100644 --- a/src/UI/AddSeries/RootFolders/Layout.js +++ b/src/UI/AddSeries/RootFolders/Layout.js @@ -37,7 +37,7 @@ define( onRender: function () { this.currentDirs.show(new LoadingView()); - if (RootFolderCollection.any()) { + if (RootFolderCollection.synced) { this._showCurrentDirs(); } diff --git a/src/UI/Commands/CommandController.js b/src/UI/Commands/CommandController.js index 4bb78f011..60684bbf5 100644 --- a/src/UI/Commands/CommandController.js +++ b/src/UI/Commands/CommandController.js @@ -43,7 +43,7 @@ define( } }); - CommandCollection.bind('add sync', function () { + CommandCollection.bind('sync', function () { var command = CommandCollection.findCommand(options.command); if (command) { self._bindToCommandModel.call(self, command, options); From 743754a041d93fe0c9074701df0bd9a3995abdb1 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 20 Oct 2013 11:17:56 -0700 Subject: [PATCH 06/31] Catch any errors setting last write time so we don't blow up the whole process Fixed: Prevent error when importing files that causes the process to fail --- .../MediaFiles/EpisodeFileMovingService.cs | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs b/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs index 5981475a0..43c018656 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeFileMovingService.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Linq; +using Growl.Connector; using NLog; using NzbDrone.Common; using NzbDrone.Common.EnvironmentInfo; @@ -74,15 +75,23 @@ private void MoveFile(EpisodeFile episodeFile, Series series, string destination _logger.Debug("Moving [{0}] > [{1}]", episodeFile.Path, destinationFilename); _diskProvider.MoveFile(episodeFile.Path, destinationFilename); - _logger.Trace("Setting last write time on series folder: {0}", series.Path); - _diskProvider.SetFolderWriteTime(series.Path, episodeFile.DateAdded); - - if (series.SeasonFolder) + try { - var seasonFolder = Path.GetDirectoryName(destinationFilename); + _logger.Trace("Setting last write time on series folder: {0}", series.Path); + _diskProvider.SetFolderWriteTime(series.Path, episodeFile.DateAdded); - _logger.Trace("Setting last write time on season folder: {0}", seasonFolder); - _diskProvider.SetFolderWriteTime(seasonFolder, episodeFile.DateAdded); + if (series.SeasonFolder) + { + var seasonFolder = Path.GetDirectoryName(destinationFilename); + + _logger.Trace("Setting last write time on season folder: {0}", seasonFolder); + _diskProvider.SetFolderWriteTime(seasonFolder, episodeFile.DateAdded); + } + } + + catch (Exception ex) + { + _logger.WarnException("Unable to set last write time", ex); } //We should only run this on Windows From 46bd5d1767a262085ce076cf3b0ea2a94c837974 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Tue, 22 Oct 2013 17:42:43 -0700 Subject: [PATCH 07/31] Fixed: Skip last write time check on linux for _UNPACK_ folders --- .../NotUnpackingSpecificationFixture.cs | 11 +++++++++++ .../Specifications/NotUnpackingSpecification.cs | 7 +++++++ 2 files changed, 18 insertions(+) diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecificationFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecificationFixture.cs index 05a45a1f6..d05a0931d 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecificationFixture.cs @@ -68,5 +68,16 @@ public void should_return_false_if_in_working_folder_and_last_write_time_was_rec Subject.IsSatisfiedBy(_localEpisode).Should().BeFalse(); } + + [Test] + public void should_return_false_if_unopacking_on_linux() + { + LinuxOnly(); + + GivenInWorkingFolder(); + GivenLastWriteTimeUtc(DateTime.UtcNow.AddDays(-5)); + + Subject.IsSatisfiedBy(_localEpisode).Should().BeFalse(); + } } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecification.cs index b8212e12c..2445e0701 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecification.cs @@ -2,6 +2,7 @@ using System.IO; using NLog; using NzbDrone.Common; +using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.Configuration; using NzbDrone.Core.Parser.Model; @@ -34,6 +35,12 @@ public bool IsSatisfiedBy(LocalEpisode localEpisode) { if (Directory.GetParent(localEpisode.Path).Name.StartsWith(workingFolder)) { + if (OsInfo.IsLinux) + { + _logger.Trace("{0} is still being unpacked", localEpisode.Path); + return false; + } + if (_diskProvider.GetLastFileWrite(localEpisode.Path) > DateTime.UtcNow.AddMinutes(-5)) { _logger.Trace("{0} appears to be unpacking still", localEpisode.Path); From 52da5b643d459f6dc1f34d96af88dbed015658a9 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Tue, 22 Oct 2013 22:17:02 -0700 Subject: [PATCH 08/31] Using string for airdate instead of DateTime in models to prevent timezone issues Fixed: Manual search air by date shows can now be sent to download client --- src/NzbDrone.Api/Indexers/ReleaseResource.cs | 2 +- .../ParserTests/ParserFixture.cs | 3 +- .../ParsingServiceTests/GetEpisodesFixture.cs | 10 +++--- .../Search/DailyEpisodeMatchSpecification.cs | 4 +-- .../Definitions/DailyEpisodeSearchCriteria.cs | 4 +-- .../IndexerSearch/NzbSearchService.cs | 2 +- .../Indexers/IndexerFetchService.cs | 2 +- src/NzbDrone.Core/NzbDrone.Core.csproj | 1 + .../Parser/InvalidDateException.cs | 19 +++++++++++ .../Parser/Model/ParsedEpisodeInfo.cs | 11 +++++-- src/NzbDrone.Core/Parser/Parser.cs | 33 +++++++++++++------ src/NzbDrone.Core/Parser/ParsingService.cs | 8 ++--- src/NzbDrone.Core/Tv/EpisodeRepository.cs | 12 +++---- src/NzbDrone.Core/Tv/EpisodeService.cs | 8 ++--- 14 files changed, 79 insertions(+), 40 deletions(-) create mode 100644 src/NzbDrone.Core/Parser/InvalidDateException.cs diff --git a/src/NzbDrone.Api/Indexers/ReleaseResource.cs b/src/NzbDrone.Api/Indexers/ReleaseResource.cs index a745f3869..fda9ef67a 100644 --- a/src/NzbDrone.Api/Indexers/ReleaseResource.cs +++ b/src/NzbDrone.Api/Indexers/ReleaseResource.cs @@ -18,7 +18,7 @@ public class ReleaseResource : RestResource public Boolean SceneSource { get; set; } public Int32 SeasonNumber { get; set; } public Language Language { get; set; } - public DateTime? AirDate { get; set; } + public String AirDate { get; set; } public String SeriesTitle { get; set; } public int[] EpisodeNumbers { get; set; } public Boolean Approved { get; set; } diff --git a/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs index da0be0eb0..987917d9c 100644 --- a/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs @@ -6,6 +6,7 @@ using NzbDrone.Common.Expansive; using NzbDrone.Core.Parser; using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; using NzbDrone.Test.Common; namespace NzbDrone.Core.Test.ParserTests @@ -169,7 +170,7 @@ public void parse_daily_episodes(string postTitle, string title, int year, int m var airDate = new DateTime(year, month, day); result.Should().NotBeNull(); result.SeriesTitle.Should().Be(title.CleanSeriesTitle()); - result.AirDate.Should().Be(airDate); + result.AirDate.Should().Be(airDate.ToString(Episode.AIR_DATE_FORMAT)); result.EpisodeNumbers.Should().BeNull(); } diff --git a/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/GetEpisodesFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/GetEpisodesFixture.cs index 35dc82373..b1d235dee 100644 --- a/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/GetEpisodesFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/GetEpisodesFixture.cs @@ -61,7 +61,7 @@ private void GivenDailySeries() private void GivenDailyParseResult() { - _parsedEpisodeInfo.AirDate = DateTime.Today; + _parsedEpisodeInfo.AirDate = DateTime.Today.ToString(Episode.AIR_DATE_FORMAT); } private void GivenSceneNumberingSeries() @@ -78,7 +78,7 @@ public void should_get_daily_episode_episode_when_search_criteria_is_null() Subject.Map(_parsedEpisodeInfo, _series.TvRageId); Mocker.GetMock() - .Verify(v => v.FindEpisode(It.IsAny(), It.IsAny()), Times.Once()); + .Verify(v => v.FindEpisode(It.IsAny(), It.IsAny()), Times.Once()); } [Test] @@ -90,19 +90,19 @@ public void should_use_search_criteria_episode_when_it_matches_daily() Subject.Map(_parsedEpisodeInfo, _series.TvRageId, _singleEpisodeSearchCriteria); Mocker.GetMock() - .Verify(v => v.FindEpisode(It.IsAny(), It.IsAny()), Times.Never()); + .Verify(v => v.FindEpisode(It.IsAny(), It.IsAny()), Times.Never()); } [Test] public void should_fallback_to_daily_episode_lookup_when_search_criteria_episode_doesnt_match() { GivenDailySeries(); - _parsedEpisodeInfo.AirDate = DateTime.Today.AddDays(-5); + _parsedEpisodeInfo.AirDate = DateTime.Today.AddDays(-5).ToString(Episode.AIR_DATE_FORMAT); ; Subject.Map(_parsedEpisodeInfo, _series.TvRageId, _singleEpisodeSearchCriteria); Mocker.GetMock() - .Verify(v => v.FindEpisode(It.IsAny(), It.IsAny()), Times.Once()); + .Verify(v => v.FindEpisode(It.IsAny(), It.IsAny()), Times.Once()); } [Test] diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/DailyEpisodeMatchSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/DailyEpisodeMatchSpecification.cs index daaf3146c..cee1ae288 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/DailyEpisodeMatchSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/DailyEpisodeMatchSpecification.cs @@ -34,9 +34,9 @@ public bool IsSatisfiedBy(RemoteEpisode remoteEpisode, SearchCriteriaBase search if (dailySearchSpec == null) return true; - var episode = _episodeService.GetEpisode(dailySearchSpec.Series.Id, dailySearchSpec.Airtime); + var episode = _episodeService.GetEpisode(dailySearchSpec.Series.Id, dailySearchSpec.AirDate.ToString(Episode.AIR_DATE_FORMAT)); - if (!remoteEpisode.ParsedEpisodeInfo.AirDate.HasValue || remoteEpisode.ParsedEpisodeInfo.AirDate.Value.ToString(Episode.AIR_DATE_FORMAT) != episode.AirDate) + if (!remoteEpisode.ParsedEpisodeInfo.IsDaily() || remoteEpisode.ParsedEpisodeInfo.AirDate != episode.AirDate) { _logger.Trace("Episode AirDate does not match searched episode number, skipping."); return false; diff --git a/src/NzbDrone.Core/IndexerSearch/Definitions/DailyEpisodeSearchCriteria.cs b/src/NzbDrone.Core/IndexerSearch/Definitions/DailyEpisodeSearchCriteria.cs index 72d7fb195..3ffba7578 100644 --- a/src/NzbDrone.Core/IndexerSearch/Definitions/DailyEpisodeSearchCriteria.cs +++ b/src/NzbDrone.Core/IndexerSearch/Definitions/DailyEpisodeSearchCriteria.cs @@ -4,11 +4,11 @@ namespace NzbDrone.Core.IndexerSearch.Definitions { public class DailyEpisodeSearchCriteria : SearchCriteriaBase { - public DateTime Airtime { get; set; } + public DateTime AirDate { get; set; } public override string ToString() { - return string.Format("[{0} : {1}", SceneTitle, Airtime); + return string.Format("[{0} : {1}", SceneTitle, AirDate); } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs b/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs index 96b52e8cf..0981c5eb9 100644 --- a/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs +++ b/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs @@ -98,7 +98,7 @@ private List SearchDaily(Series series, Episode episode) { var airDate = DateTime.ParseExact(episode.AirDate, Episode.AIR_DATE_FORMAT, CultureInfo.InvariantCulture); var searchSpec = Get(series, new List{ episode }); - searchSpec.Airtime = airDate; + searchSpec.AirDate = airDate; return Dispatch(indexer => _feedFetcher.Fetch(indexer, searchSpec), searchSpec); } diff --git a/src/NzbDrone.Core/Indexers/IndexerFetchService.cs b/src/NzbDrone.Core/Indexers/IndexerFetchService.cs index b571f6466..2de0c51b0 100644 --- a/src/NzbDrone.Core/Indexers/IndexerFetchService.cs +++ b/src/NzbDrone.Core/Indexers/IndexerFetchService.cs @@ -87,7 +87,7 @@ public IList Fetch(IIndexer indexer, DailyEpisodeSearchCriteria sea { _logger.Debug("Searching for {0}", searchCriteria); - var searchUrls = indexer.GetDailyEpisodeSearchUrls(searchCriteria.QueryTitle, searchCriteria.Series.TvRageId, searchCriteria.Airtime); + var searchUrls = indexer.GetDailyEpisodeSearchUrls(searchCriteria.QueryTitle, searchCriteria.Series.TvRageId, searchCriteria.AirDate); var result = Fetch(indexer, searchUrls); _logger.Info("Finished searching {0} for {1}. Found {2}", indexer, searchCriteria, result.Count); diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 3906a937b..e3527efb7 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -307,6 +307,7 @@ + diff --git a/src/NzbDrone.Core/Parser/InvalidDateException.cs b/src/NzbDrone.Core/Parser/InvalidDateException.cs new file mode 100644 index 000000000..dfc509295 --- /dev/null +++ b/src/NzbDrone.Core/Parser/InvalidDateException.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NzbDrone.Common.Exceptions; + +namespace NzbDrone.Core.Parser +{ + public class InvalidDateException : NzbDroneException + { + public InvalidDateException(string message, params object[] args) : base(message, args) + { + } + + public InvalidDateException(string message) : base(message) + { + } + } +} diff --git a/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs b/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs index 492248882..fe89f6dee 100644 --- a/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs +++ b/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs @@ -10,7 +10,7 @@ public class ParsedEpisodeInfo public QualityModel Quality { get; set; } public int SeasonNumber { get; set; } public int[] EpisodeNumbers { get; set; } - public DateTime? AirDate { get; set; } + public String AirDate { get; set; } public Language Language { get; set; } public bool FullSeason { get; set; } @@ -19,9 +19,9 @@ public override string ToString() { string episodeString = "[Unknown Episode]"; - if (AirDate != null && EpisodeNumbers == null) + if (IsDaily() && EpisodeNumbers == null) { - episodeString = string.Format("{0}", AirDate.Value.ToString("yyyy-MM-dd")); + episodeString = string.Format("{0}", AirDate); } else if (FullSeason) { @@ -34,5 +34,10 @@ public override string ToString() return string.Format("{0} - {1} {2}", SeriesTitle, episodeString, Quality); } + + public bool IsDaily() + { + return !String.IsNullOrWhiteSpace(AirDate); + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index d98429894..a58464c96 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -6,6 +6,7 @@ using NLog; using NzbDrone.Common.Instrumentation; using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Tv; namespace NzbDrone.Core.Parser { @@ -110,16 +111,20 @@ public static ParsedEpisodeInfo ParseTitle(string title) if (match.Count != 0) { - var result = ParseMatchCollection(match); - if (result != null) + try { - //Check if episode is in the future (most likely a parse error) - if (result.AirDate > DateTime.Now.AddDays(1).Date || result.AirDate < new DateTime(1970, 1, 1)) - break; - - result.Language = ParseLanguage(title); - result.Quality = QualityParser.ParseQuality(title); - return result; + var result = ParseMatchCollection(match); + if (result != null) + { + result.Language = ParseLanguage(title); + result.Quality = QualityParser.ParseQuality(title); + return result; + } + } + catch (InvalidDateException ex) + { + Logger.TraceException(ex.Message, ex); + break; } } } @@ -212,9 +217,17 @@ private static ParsedEpisodeInfo ParseMatchCollection(MatchCollection matchColle airmonth = tempDay; } + var airDate = new DateTime(airYear, airmonth, airday); + + //Check if episode is in the future (most likely a parse error) + if (airDate > DateTime.Now.AddDays(1).Date || airDate < new DateTime(1970, 1, 1)) + { + throw new InvalidDateException("Invalid date found: {0}", airDate); + } + result = new ParsedEpisodeInfo { - AirDate = new DateTime(airYear, airmonth, airday).Date, + AirDate = airDate.ToString(Episode.AIR_DATE_FORMAT), }; } diff --git a/src/NzbDrone.Core/Parser/ParsingService.cs b/src/NzbDrone.Core/Parser/ParsingService.cs index b678c5eeb..134e507e3 100644 --- a/src/NzbDrone.Core/Parser/ParsingService.cs +++ b/src/NzbDrone.Core/Parser/ParsingService.cs @@ -104,7 +104,7 @@ public List GetEpisodes(ParsedEpisodeInfo parsedEpisodeInfo, Series ser { var result = new List(); - if (parsedEpisodeInfo.AirDate.HasValue) + if (parsedEpisodeInfo.IsDaily()) { if (series.SeriesType == SeriesTypes.Standard) { @@ -112,7 +112,7 @@ public List GetEpisodes(ParsedEpisodeInfo parsedEpisodeInfo, Series ser return null; } - var episodeInfo = GetDailyEpisode(series, parsedEpisodeInfo.AirDate.Value, searchCriteria); + var episodeInfo = GetDailyEpisode(series, parsedEpisodeInfo.AirDate, searchCriteria); if (episodeInfo != null) { @@ -223,14 +223,14 @@ private Series GetSeries(ParsedEpisodeInfo parsedEpisodeInfo, int tvRageId) return series; } - private Episode GetDailyEpisode(Series series, DateTime airDate, SearchCriteriaBase searchCriteria) + private Episode GetDailyEpisode(Series series, String airDate, SearchCriteriaBase searchCriteria) { Episode episodeInfo = null; if (searchCriteria != null) { episodeInfo = searchCriteria.Episodes.SingleOrDefault( - e => e.AirDate == airDate.ToString(Episode.AIR_DATE_FORMAT)); + e => e.AirDate == airDate); } if (episodeInfo == null) diff --git a/src/NzbDrone.Core/Tv/EpisodeRepository.cs b/src/NzbDrone.Core/Tv/EpisodeRepository.cs index 57d8acbc1..daba4d5e0 100644 --- a/src/NzbDrone.Core/Tv/EpisodeRepository.cs +++ b/src/NzbDrone.Core/Tv/EpisodeRepository.cs @@ -11,8 +11,8 @@ namespace NzbDrone.Core.Tv public interface IEpisodeRepository : IBasicRepository { Episode Find(int seriesId, int season, int episodeNumber); - Episode Get(int seriesId, DateTime date); - Episode Find(int seriesId, DateTime date); + Episode Get(int seriesId, String date); + Episode Find(int seriesId, String date); List GetEpisodes(int seriesId); List GetEpisodes(int seriesId, int seasonNumber); List GetEpisodeByFileId(int fileId); @@ -39,14 +39,14 @@ public Episode Find(int seriesId, int season, int episodeNumber) return Query.SingleOrDefault(s => s.SeriesId == seriesId && s.SeasonNumber == season && s.EpisodeNumber == episodeNumber); } - public Episode Get(int seriesId, DateTime date) + public Episode Get(int seriesId, String date) { - return Query.Single(s => s.SeriesId == seriesId && s.AirDate == date.ToString(Episode.AIR_DATE_FORMAT)); + return Query.Single(s => s.SeriesId == seriesId && s.AirDate == date); } - public Episode Find(int seriesId, DateTime date) + public Episode Find(int seriesId, String date) { - return Query.SingleOrDefault(s => s.SeriesId == seriesId && s.AirDate == date.ToString(Episode.AIR_DATE_FORMAT)); + return Query.SingleOrDefault(s => s.SeriesId == seriesId && s.AirDate == date); } public List GetEpisodes(int seriesId) diff --git a/src/NzbDrone.Core/Tv/EpisodeService.cs b/src/NzbDrone.Core/Tv/EpisodeService.cs index 8bd88187d..9ef46761f 100644 --- a/src/NzbDrone.Core/Tv/EpisodeService.cs +++ b/src/NzbDrone.Core/Tv/EpisodeService.cs @@ -14,8 +14,8 @@ public interface IEpisodeService { Episode GetEpisode(int id); Episode FindEpisode(int seriesId, int seasonNumber, int episodeNumber, bool useScene = false); - Episode GetEpisode(int seriesId, DateTime date); - Episode FindEpisode(int seriesId, DateTime date); + Episode GetEpisode(int seriesId, String date); + Episode FindEpisode(int seriesId, String date); List GetEpisodeBySeries(int seriesId); List GetEpisodesBySeason(int seriesId, int seasonNumber); PagingSpec EpisodesWithoutFiles(PagingSpec pagingSpec); @@ -62,12 +62,12 @@ public Episode FindEpisode(int seriesId, int seasonNumber, int episodeNumber, bo return _episodeRepository.Find(seriesId, seasonNumber, episodeNumber); } - public Episode GetEpisode(int seriesId, DateTime date) + public Episode GetEpisode(int seriesId, String date) { return _episodeRepository.Get(seriesId, date); } - public Episode FindEpisode(int seriesId, DateTime date) + public Episode FindEpisode(int seriesId, String date) { return _episodeRepository.Find(seriesId, date); } From a5e08eefae28c17960f5193188e2339ab5711567 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Tue, 22 Oct 2013 22:25:17 -0700 Subject: [PATCH 09/31] Made NotUnpackingSpec test WindowsOnly --- .../Specifications/NotUnpackingSpecificationFixture.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecificationFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecificationFixture.cs index d05a0931d..db8afed53 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotUnpackingSpecificationFixture.cs @@ -54,6 +54,8 @@ public void should_return_true_if_not_in_working_folder() [Test] public void should_return_true_when_in_old_working_folder() { + WindowsOnly(); + GivenInWorkingFolder(); GivenLastWriteTimeUtc(DateTime.UtcNow.AddHours(-1)); From fa2bc7610207f49c651b7e485c58c4b33536b23f Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 18 Oct 2013 22:35:34 -0700 Subject: [PATCH 10/31] Posting nzbs to SAB instead of sending an URL to download --- .../SabProviderTests/SabProviderFixture.cs | 44 +--------- .../Clients/Sabnzbd/SabCommunicationProxy.cs | 88 +++++++++++++++++++ .../Download/Clients/Sabnzbd/SabnzbdClient.cs | 27 +++--- src/NzbDrone.Core/NzbDrone.Core.csproj | 2 +- 4 files changed, 103 insertions(+), 58 deletions(-) create mode 100644 src/NzbDrone.Core/Download/Clients/Sabnzbd/SabCommunicationProxy.cs diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabProviderTests/SabProviderFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabProviderTests/SabProviderFixture.cs index f23a2b762..c72c165e5 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabProviderTests/SabProviderFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabProviderTests/SabProviderFixture.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.Linq; using System.Net; using FizzWare.NBuilder; @@ -46,30 +47,6 @@ public void Setup() .ToList(); } - private void WithFailResponse() - { - Mocker.GetMock() - .Setup(s => s.DownloadString(It.IsAny())).Returns("{ \"status\": false, \"error\": \"API Key Required\" }"); - } - - [Test] - public void add_url_should_format_request_properly() - { - Mocker.GetMock(MockBehavior.Strict) - .Setup(s => s.DownloadString("http://192.168.5.55:2222/api?mode=addurl&name=http://www.nzbclub.com/nzb_download.aspx?mid=1950232&priority=0&pp=3&cat=tv&nzbname=My+Series+Name+-+5x2-5x3+-+My+title+%5bBluray720p%5d+%5bProper%5d&output=json&apikey=5c770e3197e4fe763423ee7c392c25d1&ma_username=admin&ma_password=pass")) - .Returns("{ \"status\": true }"); - - - Subject.DownloadNzb(_remoteEpisode); - } - - [Test] - public void add_by_url_should_detect_and_handle_sab_errors() - { - WithFailResponse(); - Assert.Throws(() => Subject.DownloadNzb(_remoteEpisode)); - } - [Test] public void should_be_able_to_get_categories_when_config_is_passed_in() { @@ -195,15 +172,6 @@ public void Test_should_return_version_as_a_string() result.Should().Be("0.6.9"); } - [Test] - public void should_throw_when_WebException_is_thrown() - { - Mocker.GetMock() - .Setup(s => s.DownloadString(It.IsAny())).Throws(new WebException()); - - Assert.Throws(() => Subject.DownloadNzb(_remoteEpisode)); - } - [Test] public void downloadNzb_should_use_sabRecentTvPriority_when_recentEpisode_is_true() { @@ -211,16 +179,10 @@ public void downloadNzb_should_use_sabRecentTvPriority_when_recentEpisode_is_tru .SetupGet(s => s.SabRecentTvPriority) .Returns(SabPriorityType.High); - - Mocker.GetMock() - .Setup(s => s.DownloadString("http://192.168.5.55:2222/api?mode=addurl&name=http://www.nzbclub.com/nzb_download.aspx?mid=1950232&priority=1&pp=3&cat=tv&nzbname=My+Series+Name+-+5x2-5x3+-+My+title+%5bBluray720p%5d+%5bProper%5d&output=json&apikey=5c770e3197e4fe763423ee7c392c25d1&ma_username=admin&ma_password=pass")) - .Returns("{ \"status\": true }"); - - Subject.DownloadNzb(_remoteEpisode); - Mocker.GetMock() - .Verify(v => v.DownloadString("http://192.168.5.55:2222/api?mode=addurl&name=http://www.nzbclub.com/nzb_download.aspx?mid=1950232&priority=1&pp=3&cat=tv&nzbname=My+Series+Name+-+5x2-5x3+-+My+title+%5bBluray720p%5d+%5bProper%5d&output=json&apikey=5c770e3197e4fe763423ee7c392c25d1&ma_username=admin&ma_password=pass"), Times.Once()); + Mocker.GetMock() + .Verify(v => v.DownloadNzb(It.IsAny(), It.IsAny(), It.IsAny(), (int)SabPriorityType.High), Times.Once()); } } } diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabCommunicationProxy.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabCommunicationProxy.cs new file mode 100644 index 000000000..ed26110c6 --- /dev/null +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabCommunicationProxy.cs @@ -0,0 +1,88 @@ +using System; +using System.IO; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Configuration; +using RestSharp; + +namespace NzbDrone.Core.Download.Clients.Sabnzbd +{ + public interface ISabCommunicationProxy + { + string DownloadNzb(Stream nzb, string name, string category, int priority); + string ProcessRequest(IRestRequest restRequest, string action); + } + + public class SabCommunicationProxy : ISabCommunicationProxy + { + private readonly IConfigService _configService; + + public SabCommunicationProxy(IConfigService configService) + { + _configService = configService; + } + + public string DownloadNzb(Stream nzb, string title, string category, int priority) + { + var request = new RestRequest(Method.POST); + var action = String.Format("mode=addfile&cat={0}&priority={1}", category, priority); + + request.AddFile("name", ReadFully(nzb), title, "application/x-nzb"); + + return ProcessRequest(request, action); + } + + public string ProcessRequest(IRestRequest restRequest, string action) + { + var client = BuildClient(action); + var response = client.Execute(restRequest); + + CheckForError(response); + + return response.Content; + } + + private IRestClient BuildClient(string action) + { + var protocol = _configService.SabUseSsl ? "https" : "http"; + + var url = string.Format(@"{0}://{1}:{2}/api?{3}&apikey={4}&ma_username={5}&ma_password={6}&output=json", + protocol, + _configService.SabHost, + _configService.SabPort, + action, + _configService.SabApiKey, + _configService.SabUsername, + _configService.SabPassword); + + return new RestClient(url); + } + + private void CheckForError(IRestResponse response) + { + if (response.ResponseStatus != ResponseStatus.Completed) + { + throw new ApplicationException("Unable to connect to SABnzbd, please check your settings"); + } + + var result = Json.Deserialize(response.Content); + + if (result.Status != null && result.Status.Equals("false", StringComparison.InvariantCultureIgnoreCase)) + throw new ApplicationException(result.Error); + } + + //TODO: Find a better home for this + private byte[] ReadFully(Stream input) + { + byte[] buffer = new byte[16 * 1024]; + using (MemoryStream ms = new MemoryStream()) + { + int read; + while ((read = input.Read(buffer, 0, buffer.Length)) > 0) + { + ms.Write(buffer, 0, read); + } + return ms.ToArray(); + } + } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdClient.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdClient.cs index 4e1fabcfc..c7ae37e34 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdClient.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdClient.cs @@ -55,6 +55,7 @@ public class SabnzbdClient : IDownloadClient private readonly IConfigService _configService; private readonly IHttpProvider _httpProvider; private readonly IParsingService _parsingService; + private readonly ISabCommunicationProxy _sabCommunicationProxy; private readonly ICached> _queueCache; private readonly Logger _logger; @@ -62,11 +63,13 @@ public SabnzbdClient(IConfigService configService, IHttpProvider httpProvider, ICacheManger cacheManger, IParsingService parsingService, + ISabCommunicationProxy sabCommunicationProxy, Logger logger) { _configService = configService; _httpProvider = httpProvider; _parsingService = parsingService; + _sabCommunicationProxy = sabCommunicationProxy; _queueCache = cacheManger.GetCache>(GetType(), "queue"); _logger = logger; } @@ -75,24 +78,16 @@ public void DownloadNzb(RemoteEpisode remoteEpisode) { var url = remoteEpisode.Release.DownloadUrl; var title = remoteEpisode.Release.Title; + var category = _configService.SabTvCategory; + var priority = remoteEpisode.IsRecentEpisode() ? (int)_configService.SabRecentTvPriority : (int)_configService.SabOlderTvPriority; - string cat = _configService.SabTvCategory; - int priority = remoteEpisode.IsRecentEpisode() ? (int)_configService.SabRecentTvPriority : (int)_configService.SabOlderTvPriority; + using (var nzb = _httpProvider.DownloadStream(url)) + { + _logger.Info("Adding report [{0}] to the queue.", title); + var response = _sabCommunicationProxy.DownloadNzb(nzb, title, category, priority); - string name = url.Replace("&", "%26"); - string nzbName = HttpUtility.UrlEncode(title); - - string action = string.Format("mode=addurl&name={0}&priority={1}&pp=3&cat={2}&nzbname={3}&output=json", - name, priority, cat, nzbName); - - string request = GetSabRequest(action); - _logger.Info("Adding report [{0}] to the queue.", title); - - var response = _httpProvider.DownloadString(request); - - _logger.Debug("Queue Response: [{0}]", response); - - CheckForError(response); + _logger.Debug("Queue Response: [{0}]", response); + } } public bool IsConfigured diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index e3527efb7..b7f1607c4 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -225,6 +225,7 @@ + @@ -627,7 +628,6 @@ - From 2e1b92154331121318d4d17ad68c1770abdbafab Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 20 Oct 2013 18:30:46 -0700 Subject: [PATCH 11/31] Storing nzo_id from SAB in history (data) --- .../Download/Clients/BlackholeProvider.cs | 5 +- .../Download/Clients/Nzbget/NzbgetClient.cs | 3 +- .../Download/Clients/PneumaticClient.cs | 6 +-- .../Download/Clients/Sabnzbd/SabnzbdClient.cs | 46 +++---------------- src/NzbDrone.Core/Download/DownloadService.cs | 14 ++++-- .../Download/EpisodeGrabbedEvent.cs | 5 +- src/NzbDrone.Core/Download/IDownloadClient.cs | 2 +- src/NzbDrone.Core/History/HistoryService.cs | 6 +++ 8 files changed, 36 insertions(+), 51 deletions(-) diff --git a/src/NzbDrone.Core/Download/Clients/BlackholeProvider.cs b/src/NzbDrone.Core/Download/Clients/BlackholeProvider.cs index 1598ee647..2fe37fe78 100644 --- a/src/NzbDrone.Core/Download/Clients/BlackholeProvider.cs +++ b/src/NzbDrone.Core/Download/Clients/BlackholeProvider.cs @@ -22,7 +22,7 @@ public BlackholeProvider(IConfigService configService, IHttpProvider httpProvide _logger = logger; } - public void DownloadNzb(RemoteEpisode remoteEpisode) + public string DownloadNzb(RemoteEpisode remoteEpisode) { var url = remoteEpisode.Release.DownloadUrl; var title = remoteEpisode.Release.Title; @@ -34,8 +34,9 @@ public void DownloadNzb(RemoteEpisode remoteEpisode) _logger.Trace("Downloading NZB from: {0} to: {1}", url, filename); _httpProvider.DownloadFile(url, filename); - _logger.Trace("NZB Download succeeded, saved to: {0}", filename); + + return null; } public bool IsConfigured diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetClient.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetClient.cs index f3d692d32..9e1d7d7b4 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetClient.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetClient.cs @@ -24,7 +24,7 @@ public NzbgetClient(IConfigService configService, IHttpProvider httpProvider, IP _logger = logger; } - public void DownloadNzb(RemoteEpisode remoteEpisode) + public string DownloadNzb(RemoteEpisode remoteEpisode) { var url = remoteEpisode.Release.DownloadUrl; var title = remoteEpisode.Release.Title + ".nzb"; @@ -46,6 +46,7 @@ public void DownloadNzb(RemoteEpisode remoteEpisode) var success = Json.Deserialize(response).Result; _logger.Debug("Queue Response: [{0}]", success); + return null; } public bool IsConfigured diff --git a/src/NzbDrone.Core/Download/Clients/PneumaticClient.cs b/src/NzbDrone.Core/Download/Clients/PneumaticClient.cs index 537683243..3cef9b226 100644 --- a/src/NzbDrone.Core/Download/Clients/PneumaticClient.cs +++ b/src/NzbDrone.Core/Download/Clients/PneumaticClient.cs @@ -26,7 +26,7 @@ public PneumaticClient(IConfigService configService, IHttpProvider httpProvider, _diskProvider = diskProvider; } - public void DownloadNzb(RemoteEpisode remoteEpisode) + public string DownloadNzb(RemoteEpisode remoteEpisode) { var url = remoteEpisode.Release.DownloadUrl; var title = remoteEpisode.Release.Title; @@ -41,8 +41,6 @@ public void DownloadNzb(RemoteEpisode remoteEpisode) //Save to the Pneumatic directory (The user will need to ensure its accessible by XBMC) var filename = Path.Combine(_configService.PneumaticFolder, title + ".nzb"); - - logger.Trace("Downloading NZB from: {0} to: {1}", url, filename); _httpProvider.DownloadFile(url, filename); @@ -50,6 +48,8 @@ public void DownloadNzb(RemoteEpisode remoteEpisode) var contents = String.Format("plugin://plugin.program.pneumatic/?mode=strm&type=add_file&nzb={0}&nzbname={1}", filename, title); _diskProvider.WriteAllText(Path.Combine(_configService.DownloadedEpisodesFolder, title + ".strm"), contents); + + return null; } public bool IsConfigured diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdClient.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdClient.cs index c7ae37e34..0eddaea99 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdClient.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdClient.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Web; using Newtonsoft.Json.Linq; using NLog; @@ -13,43 +14,6 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd { - public class SabRequestBuilder - { - private readonly IConfigService _configService; - - public SabRequestBuilder(IConfigService configService) - { - _configService = configService; - } - - public IRestRequest AddToQueueRequest(RemoteEpisode remoteEpisode) - { - string cat = _configService.SabTvCategory; - int priority = (int)_configService.SabRecentTvPriority; - - string name = remoteEpisode.Release.DownloadUrl.Replace("&", "%26"); - string nzbName = HttpUtility.UrlEncode(remoteEpisode.Release.Title); - - string action = string.Format("mode=addurl&name={0}&priority={1}&pp=3&cat={2}&nzbname={3}&output=json", - name, priority, cat, nzbName); - - string request = GetSabRequest(action); - - return new RestRequest(request); - } - - private string GetSabRequest(string action) - { - return string.Format(@"http://{0}:{1}/api?{2}&apikey={3}&ma_username={4}&ma_password={5}", - _configService.SabHost, - _configService.SabPort, - action, - _configService.SabApiKey, - _configService.SabUsername, - _configService.SabPassword); - } - } - public class SabnzbdClient : IDownloadClient { private readonly IConfigService _configService; @@ -74,7 +38,7 @@ public SabnzbdClient(IConfigService configService, _logger = logger; } - public void DownloadNzb(RemoteEpisode remoteEpisode) + public string DownloadNzb(RemoteEpisode remoteEpisode) { var url = remoteEpisode.Release.DownloadUrl; var title = remoteEpisode.Release.Title; @@ -84,9 +48,11 @@ public void DownloadNzb(RemoteEpisode remoteEpisode) using (var nzb = _httpProvider.DownloadStream(url)) { _logger.Info("Adding report [{0}] to the queue.", title); - var response = _sabCommunicationProxy.DownloadNzb(nzb, title, category, priority); + var response = Json.Deserialize(_sabCommunicationProxy.DownloadNzb(nzb, title, category, priority)); - _logger.Debug("Queue Response: [{0}]", response); + _logger.Debug("Queue Response: [{0}]", response.Status); + + return response.Ids.First(); } } diff --git a/src/NzbDrone.Core/Download/DownloadService.cs b/src/NzbDrone.Core/Download/DownloadService.cs index f41acdafa..05dfd4737 100644 --- a/src/NzbDrone.Core/Download/DownloadService.cs +++ b/src/NzbDrone.Core/Download/DownloadService.cs @@ -1,4 +1,5 @@ -using NLog; +using System; +using NLog; using NzbDrone.Common.EnsureThat; using NzbDrone.Core.Instrumentation; using NzbDrone.Core.Messaging.Events; @@ -40,10 +41,17 @@ public void DownloadReport(RemoteEpisode remoteEpisode) return; } - downloadClient.DownloadNzb(remoteEpisode); + var downloadClientId = downloadClient.DownloadNzb(remoteEpisode); + var episodeGrabbedEvent = new EpisodeGrabbedEvent(remoteEpisode); + + if (!String.IsNullOrWhiteSpace(downloadClientId)) + { + episodeGrabbedEvent.DownloadClient = downloadClient.GetType().Name; + episodeGrabbedEvent.DownloadClientId = downloadClientId; + } _logger.ProgressInfo("Report sent to download client. {0}", downloadTitle); - _eventAggregator.PublishEvent(new EpisodeGrabbedEvent(remoteEpisode)); + _eventAggregator.PublishEvent(episodeGrabbedEvent); } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Download/EpisodeGrabbedEvent.cs b/src/NzbDrone.Core/Download/EpisodeGrabbedEvent.cs index cd6fc46cc..a6a9f0b52 100644 --- a/src/NzbDrone.Core/Download/EpisodeGrabbedEvent.cs +++ b/src/NzbDrone.Core/Download/EpisodeGrabbedEvent.cs @@ -1,4 +1,5 @@ -using NzbDrone.Common.Messaging; +using System; +using NzbDrone.Common.Messaging; using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.Download @@ -6,6 +7,8 @@ namespace NzbDrone.Core.Download public class EpisodeGrabbedEvent : IEvent { public RemoteEpisode Episode { get; private set; } + public String DownloadClient { get; set; } + public String DownloadClientId { get; set; } public EpisodeGrabbedEvent(RemoteEpisode episode) { diff --git a/src/NzbDrone.Core/Download/IDownloadClient.cs b/src/NzbDrone.Core/Download/IDownloadClient.cs index ce32b62b2..5270b62b4 100644 --- a/src/NzbDrone.Core/Download/IDownloadClient.cs +++ b/src/NzbDrone.Core/Download/IDownloadClient.cs @@ -5,7 +5,7 @@ namespace NzbDrone.Core.Download { public interface IDownloadClient { - void DownloadNzb(RemoteEpisode remoteEpisode); + string DownloadNzb(RemoteEpisode remoteEpisode); bool IsConfigured { get; } IEnumerable GetQueue(); } diff --git a/src/NzbDrone.Core/History/HistoryService.cs b/src/NzbDrone.Core/History/HistoryService.cs index 09b372b2c..568cc3058 100644 --- a/src/NzbDrone.Core/History/HistoryService.cs +++ b/src/NzbDrone.Core/History/HistoryService.cs @@ -75,6 +75,12 @@ public void Handle(EpisodeGrabbedEvent message) history.Data.Add("ReleaseGroup", message.Episode.Release.ReleaseGroup); history.Data.Add("Age", message.Episode.Release.Age.ToString()); + if (!String.IsNullOrWhiteSpace(message.DownloadClientId)) + { + history.Data.Add("DownloadClient", message.DownloadClient); + history.Data.Add("DownloadClientId", message.DownloadClientId); + } + _historyRepository.Insert(history); } } From e64d2f33d65cbcd06b3db30b60919a4ae8ba31c5 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Tue, 22 Oct 2013 00:31:36 -0700 Subject: [PATCH 12/31] Failed downloads are added to history --- .../Download/FailedDownloadServiceFixture.cs | 198 ++++++++++++++++++ .../NzbDrone.Core.Test.csproj | 1 + .../Download/Clients/BlackholeProvider.cs | 8 +- .../Download/Clients/Nzbget/NzbgetClient.cs | 5 + .../Download/Clients/PneumaticClient.cs | 5 + .../Download/Clients/Sabnzbd/SabnzbdClient.cs | 38 +++- .../Download/DownloadFailedEvent.cs | 16 ++ .../Download/FailedDownloadCommand.cs | 9 + .../Download/FailedDownloadService.cs | 87 ++++++++ src/NzbDrone.Core/Download/HistoryItem.cs | 22 ++ src/NzbDrone.Core/Download/IDownloadClient.cs | 1 + src/NzbDrone.Core/History/History.cs | 6 +- .../History/HistoryRepository.cs | 16 ++ src/NzbDrone.Core/History/HistoryService.cs | 34 ++- src/NzbDrone.Core/Jobs/TaskManager.cs | 4 +- src/NzbDrone.Core/NzbDrone.Core.csproj | 4 + src/UI/Cells/EventTypeCell.js | 4 + src/UI/Content/icons.less | 5 + 18 files changed, 444 insertions(+), 19 deletions(-) create mode 100644 src/NzbDrone.Core.Test/Download/FailedDownloadServiceFixture.cs create mode 100644 src/NzbDrone.Core/Download/DownloadFailedEvent.cs create mode 100644 src/NzbDrone.Core/Download/FailedDownloadCommand.cs create mode 100644 src/NzbDrone.Core/Download/FailedDownloadService.cs create mode 100644 src/NzbDrone.Core/Download/HistoryItem.cs diff --git a/src/NzbDrone.Core.Test/Download/FailedDownloadServiceFixture.cs b/src/NzbDrone.Core.Test/Download/FailedDownloadServiceFixture.cs new file mode 100644 index 000000000..478515d6a --- /dev/null +++ b/src/NzbDrone.Core.Test/Download/FailedDownloadServiceFixture.cs @@ -0,0 +1,198 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FizzWare.NBuilder; +using Moq; +using NUnit.Framework; +using NzbDrone.Core.Download; +using NzbDrone.Core.History; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Test.Download +{ + [TestFixture] + public class FailedDownloadServiceFixture : CoreTest + { + private Series _series; + private Episode _episode; + private List _completed; + private List _failed; + + [SetUp] + public void Setup() + { + _series = Builder.CreateNew().Build(); + _episode = Builder.CreateNew().Build(); + + _completed = Builder.CreateListOfSize(5) + .All() + .With(h => h.Status = HistoryStatus.Completed) + .Build() + .ToList(); + + _failed = Builder.CreateListOfSize(1) + .All() + .With(h => h.Status = HistoryStatus.Failed) + .Build() + .ToList(); + + Mocker.GetMock() + .Setup(c => c.GetDownloadClient()).Returns(Mocker.GetMock().Object); + } + + private void GivenNoRecentHistory() + { + Mocker.GetMock() + .Setup(s => s.BetweenDates(It.IsAny(), It.IsAny(), HistoryEventType.Grabbed)) + .Returns(new List()); + } + + private void GivenRecentHistory(List history) + { + Mocker.GetMock() + .Setup(s => s.BetweenDates(It.IsAny(), It.IsAny(), HistoryEventType.Grabbed)) + .Returns(history); + } + + private void GivenNoFailedHistory() + { + Mocker.GetMock() + .Setup(s => s.Failed()) + .Returns(new List()); + } + + private void GivenFailedHistory(List failedHistory) + { + Mocker.GetMock() + .Setup(s => s.Failed()) + .Returns(failedHistory); + } + + private void GivenFailedDownloadClientHistory() + { + Mocker.GetMock() + .Setup(s => s.GetHistory(0, 20)) + .Returns(_failed); + } + + private void VerifyNoFailedDownloads() + { + Mocker.GetMock() + .Verify(v => v.PublishEvent(It.IsAny()), Times.Never()); + } + + private void VerifyFailedDownloads(int count = 1) + { + Mocker.GetMock() + .Verify(v => v.PublishEvent(It.IsAny()), Times.Exactly(count)); + } + + [Test] + public void should_not_process_if_no_download_client_history() + { + Mocker.GetMock() + .Setup(s => s.GetHistory(0, 20)) + .Returns(new List()); + + Subject.Execute(new FailedDownloadCommand()); + + Mocker.GetMock() + .Verify(s => s.BetweenDates(It.IsAny(), It.IsAny(), HistoryEventType.Grabbed), + Times.Never()); + + VerifyNoFailedDownloads(); + } + + [Test] + public void should_not_process_if_no_failed_items_in_download_client_history() + { + Mocker.GetMock() + .Setup(s => s.GetHistory(0, 20)) + .Returns(_completed); + + Subject.Execute(new FailedDownloadCommand()); + + Mocker.GetMock() + .Verify(s => s.BetweenDates(It.IsAny(), It.IsAny(), HistoryEventType.Grabbed), + Times.Never()); + + VerifyNoFailedDownloads(); + } + + [Test] + public void should_not_process_if_matching_history_is_not_found() + { + GivenNoRecentHistory(); + GivenFailedDownloadClientHistory(); + + Subject.Execute(new FailedDownloadCommand()); + + VerifyNoFailedDownloads(); + } + + [Test] + public void should_not_process_if_already_added_to_history_as_failed() + { + GivenFailedDownloadClientHistory(); + + var history = Builder.CreateListOfSize(1) + .Build() + .ToList(); + + GivenRecentHistory(history); + GivenFailedHistory(history); + + history.First().Data.Add("downloadClient", "SabnzbdClient"); + history.First().Data.Add("downloadClientId", _failed.First().Id); + + Subject.Execute(new FailedDownloadCommand()); + + VerifyNoFailedDownloads(); + } + + [Test] + public void should_process_if_not_already_in_failed_history() + { + GivenFailedDownloadClientHistory(); + + var history = Builder.CreateListOfSize(1) + .Build() + .ToList(); + + GivenRecentHistory(history); + GivenNoFailedHistory(); + + history.First().Data.Add("downloadClient", "SabnzbdClient"); + history.First().Data.Add("downloadClientId", _failed.First().Id); + + Subject.Execute(new FailedDownloadCommand()); + + VerifyFailedDownloads(); + } + + [Test] + public void should_process_for_each_failed_episode() + { + GivenFailedDownloadClientHistory(); + + var history = Builder.CreateListOfSize(2) + .Build() + .ToList(); + + GivenRecentHistory(history); + GivenNoFailedHistory(); + + history.ForEach(h => + { + h.Data.Add("downloadClient", "SabnzbdClient"); + h.Data.Add("downloadClientId", _failed.First().Id); + }); + + Subject.Execute(new FailedDownloadCommand()); + + VerifyFailedDownloads(2); + } + } +} diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index e75b4d0ac..f0a409e97 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -124,6 +124,7 @@ + diff --git a/src/NzbDrone.Core/Download/Clients/BlackholeProvider.cs b/src/NzbDrone.Core/Download/Clients/BlackholeProvider.cs index 2fe37fe78..a5d8e5fe2 100644 --- a/src/NzbDrone.Core/Download/Clients/BlackholeProvider.cs +++ b/src/NzbDrone.Core/Download/Clients/BlackholeProvider.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; using NLog; using NzbDrone.Common; @@ -51,5 +52,10 @@ public IEnumerable GetQueue() { return new QueueItem[0]; } + + public IEnumerable GetHistory(int start = 0, int limit = 0) + { + return new HistoryItem[0]; + } } } diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetClient.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetClient.cs index 9e1d7d7b4..94f83ed9d 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetClient.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetClient.cs @@ -91,6 +91,11 @@ public virtual IEnumerable GetQueue() } } + public IEnumerable GetHistory(int start = 0, int limit = 0) + { + return new HistoryItem[0]; + } + public virtual VersionModel GetVersion(string host = null, int port = 0, string username = null, string password = null) { //Get saved values if any of these are defaults diff --git a/src/NzbDrone.Core/Download/Clients/PneumaticClient.cs b/src/NzbDrone.Core/Download/Clients/PneumaticClient.cs index 3cef9b226..1036f7d9b 100644 --- a/src/NzbDrone.Core/Download/Clients/PneumaticClient.cs +++ b/src/NzbDrone.Core/Download/Clients/PneumaticClient.cs @@ -65,6 +65,11 @@ public IEnumerable GetQueue() return new QueueItem[0]; } + public IEnumerable GetHistory(int start = 0, int limit = 0) + { + return new HistoryItem[0]; + } + public virtual bool IsInQueue(RemoteEpisode newEpisode) { return false; diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdClient.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdClient.cs index 0eddaea99..106e5fc71 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdClient.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdClient.cs @@ -38,6 +38,15 @@ public SabnzbdClient(IConfigService configService, _logger = logger; } + public bool IsConfigured + { + get + { + return !string.IsNullOrWhiteSpace(_configService.SabHost) + && _configService.SabPort != 0; + } + } + public string DownloadNzb(RemoteEpisode remoteEpisode) { var url = remoteEpisode.Release.DownloadUrl; @@ -56,15 +65,6 @@ public string DownloadNzb(RemoteEpisode remoteEpisode) } } - public bool IsConfigured - { - get - { - return !string.IsNullOrWhiteSpace(_configService.SabHost) - && _configService.SabPort != 0; - } - } - public IEnumerable GetQueue() { return _queueCache.Get("queue", () => @@ -104,7 +104,7 @@ public IEnumerable GetQueue() }, TimeSpan.FromSeconds(10)); } - public virtual List GetHistory(int start = 0, int limit = 0) + public IEnumerable GetHistory(int start = 0, int limit = 0) { string action = String.Format("mode=history&output=json&start={0}&limit={1}", start, limit); string request = GetSabRequest(action); @@ -113,7 +113,23 @@ public virtual List GetHistory(int start = 0, int limit = 0) CheckForError(response); var items = Json.Deserialize(JObject.Parse(response).SelectToken("history").ToString()).Items; - return items ?? new List(); + var historyItems = new List(); + + foreach (var sabHistoryItem in items) + { + var historyItem = new HistoryItem(); + historyItem.Id = sabHistoryItem.Id; + historyItem.Title = sabHistoryItem.Title; + historyItem.Size = sabHistoryItem.Size; + historyItem.DownloadTime = sabHistoryItem.DownloadTime; + historyItem.Storage = sabHistoryItem.Storage; + historyItem.Category = sabHistoryItem.Category; + historyItem.Status = sabHistoryItem.Status == "Failed" ? HistoryStatus.Failed : HistoryStatus.Completed; + + historyItems.Add(historyItem); + } + + return historyItems; } public virtual SabCategoryModel GetCategories(string host = null, int port = 0, string apiKey = null, string username = null, string password = null) diff --git a/src/NzbDrone.Core/Download/DownloadFailedEvent.cs b/src/NzbDrone.Core/Download/DownloadFailedEvent.cs new file mode 100644 index 000000000..f83444bb2 --- /dev/null +++ b/src/NzbDrone.Core/Download/DownloadFailedEvent.cs @@ -0,0 +1,16 @@ +using System; +using NzbDrone.Common.Messaging; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Download +{ + public class DownloadFailedEvent : IEvent + { + public Series Series { get; set; } + public Episode Episode { get; set; } + public QualityModel Quality { get; set; } + public String SourceTitle { get; set; } + public String DownloadClient { get; set; } + public String DownloadClientId { get; set; } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Download/FailedDownloadCommand.cs b/src/NzbDrone.Core/Download/FailedDownloadCommand.cs new file mode 100644 index 000000000..864921ba1 --- /dev/null +++ b/src/NzbDrone.Core/Download/FailedDownloadCommand.cs @@ -0,0 +1,9 @@ +using NzbDrone.Core.Messaging.Commands; + +namespace NzbDrone.Core.Download +{ + public class FailedDownloadCommand : Command + { + + } +} diff --git a/src/NzbDrone.Core/Download/FailedDownloadService.cs b/src/NzbDrone.Core/Download/FailedDownloadService.cs new file mode 100644 index 000000000..79431acf1 --- /dev/null +++ b/src/NzbDrone.Core/Download/FailedDownloadService.cs @@ -0,0 +1,87 @@ +using System; +using System.Linq; +using NLog; +using NzbDrone.Core.History; +using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Messaging.Events; + +namespace NzbDrone.Core.Download +{ + public class FailedDownloadService : IExecute + { + private readonly IProvideDownloadClient _downloadClientProvider; + private readonly IHistoryService _historyService; + private readonly IEventAggregator _eventAggregator; + private readonly Logger _logger; + + private static string DOWNLOAD_CLIENT = "downloadClient"; + private static string DOWNLOAD_CLIENT_ID = "downloadClientId"; + + public FailedDownloadService(IProvideDownloadClient downloadClientProvider, + IHistoryService historyService, + IEventAggregator eventAggregator, + Logger logger) + { + _downloadClientProvider = downloadClientProvider; + _historyService = historyService; + _eventAggregator = eventAggregator; + _logger = logger; + } + + private void CheckForFailedDownloads() + { + var downloadClient = _downloadClientProvider.GetDownloadClient(); + var downloadClientHistory = downloadClient.GetHistory(0, 20).ToList(); + + var failedItems = downloadClientHistory.Where(h => h.Status == HistoryStatus.Failed).ToList(); + + if (!failedItems.Any()) + { + _logger.Trace("Yay! No failed downloads"); + return; + } + + var recentHistory = _historyService.BetweenDates(DateTime.UtcNow.AddDays(-1), DateTime.UtcNow, HistoryEventType.Grabbed); + var failedHistory = _historyService.Failed(); + + foreach (var failedItem in failedItems) + { + var failedLocal = failedItem; + var historyItems = recentHistory.Where(h => h.Data.ContainsKey(DOWNLOAD_CLIENT) && + h.Data[DOWNLOAD_CLIENT_ID].Equals(failedLocal.Id)) + .ToList(); + + if (!historyItems.Any()) + { + _logger.Trace("Unable to find matching history item"); + continue; + } + + if (failedHistory.Any(h => h.Data.ContainsKey(DOWNLOAD_CLIENT_ID) && + h.Data[DOWNLOAD_CLIENT_ID].Equals(failedLocal.Id))) + { + _logger.Trace("Already added to history as failed"); + continue; + } + + foreach (var historyItem in historyItems) + { + _eventAggregator.PublishEvent(new DownloadFailedEvent + { + Series = historyItem.Series, + Episode = historyItem.Episode, + Quality = historyItem.Quality, + SourceTitle = historyItem.SourceTitle, + DownloadClient = historyItem.Data[DOWNLOAD_CLIENT], + DownloadClientId = historyItem.Data[DOWNLOAD_CLIENT_ID] + }); + } + } + } + + public void Execute(FailedDownloadCommand message) + { + CheckForFailedDownloads(); + } + } +} diff --git a/src/NzbDrone.Core/Download/HistoryItem.cs b/src/NzbDrone.Core/Download/HistoryItem.cs new file mode 100644 index 000000000..094270107 --- /dev/null +++ b/src/NzbDrone.Core/Download/HistoryItem.cs @@ -0,0 +1,22 @@ +using System; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.Download +{ + public class HistoryItem + { + public String Id { get; set; } + public String Title { get; set; } + public String Size { get; set; } + public String Category { get; set; } + public Int32 DownloadTime { get; set; } + public String Storage { get; set; } + public HistoryStatus Status { get; set; } + } + + public enum HistoryStatus + { + Completed = 0, + Failed = 1 + } +} diff --git a/src/NzbDrone.Core/Download/IDownloadClient.cs b/src/NzbDrone.Core/Download/IDownloadClient.cs index 5270b62b4..f330dbc86 100644 --- a/src/NzbDrone.Core/Download/IDownloadClient.cs +++ b/src/NzbDrone.Core/Download/IDownloadClient.cs @@ -8,5 +8,6 @@ public interface IDownloadClient string DownloadNzb(RemoteEpisode remoteEpisode); bool IsConfigured { get; } IEnumerable GetQueue(); + IEnumerable GetHistory(int start = 0, int limit = 0); } } diff --git a/src/NzbDrone.Core/History/History.cs b/src/NzbDrone.Core/History/History.cs index 886ae9c4e..6be5f97a9 100644 --- a/src/NzbDrone.Core/History/History.cs +++ b/src/NzbDrone.Core/History/History.cs @@ -17,12 +17,9 @@ public History() public string SourceTitle { get; set; } public QualityModel Quality { get; set; } public DateTime Date { get; set; } - public Episode Episode { get; set; } public Series Series { get; set; } - public HistoryEventType EventType { get; set; } - public Dictionary Data { get; set; } } @@ -32,7 +29,8 @@ public enum HistoryEventType Unknown = 0, Grabbed = 1, SeriesFolderImported = 2, - DownloadFolderImported = 3 + DownloadFolderImported = 3, + DownloadFailed = 4 } } \ No newline at end of file diff --git a/src/NzbDrone.Core/History/HistoryRepository.cs b/src/NzbDrone.Core/History/HistoryRepository.cs index 5a6cf5244..1c3c7807f 100644 --- a/src/NzbDrone.Core/History/HistoryRepository.cs +++ b/src/NzbDrone.Core/History/HistoryRepository.cs @@ -13,6 +13,8 @@ public interface IHistoryRepository : IBasicRepository { void Trim(); List GetBestQualityInHistory(int episodeId); + List BetweenDates(DateTime startDate, DateTime endDate, HistoryEventType eventType); + List Failed(); } public class HistoryRepository : BasicRepository, IHistoryRepository @@ -38,6 +40,20 @@ public List GetBestQualityInHistory(int episodeId) return history.Select(h => h.Quality).ToList(); } + public List BetweenDates(DateTime startDate, DateTime endDate, HistoryEventType eventType) + { + return Query.Join(JoinType.Inner, h => h.Series, (h, s) => h.SeriesId == s.Id) + .Join(JoinType.Inner, h => h.Episode, (h, e) => h.EpisodeId == e.Id) + .Where(h => h.Date >= startDate) + .AndWhere(h => h.Date <= endDate) + .AndWhere(h => h.EventType == eventType); + } + + public List Failed() + { + return Query.Where(h => h.EventType == HistoryEventType.DownloadFailed); + } + public override PagingSpec GetPaged(PagingSpec pagingSpec) { pagingSpec.Records = GetPagedQuery(pagingSpec).ToList(); diff --git a/src/NzbDrone.Core/History/HistoryService.cs b/src/NzbDrone.Core/History/HistoryService.cs index 568cc3058..0244d1180 100644 --- a/src/NzbDrone.Core/History/HistoryService.cs +++ b/src/NzbDrone.Core/History/HistoryService.cs @@ -18,9 +18,11 @@ public interface IHistoryService void Trim(); QualityModel GetBestQualityInHistory(int episodeId); PagingSpec Paged(PagingSpec pagingSpec); + List BetweenDates(DateTime startDate, DateTime endDate, HistoryEventType eventType); + List Failed(); } - public class HistoryService : IHistoryService, IHandle, IHandle + public class HistoryService : IHistoryService, IHandle, IHandle, IHandle { private readonly IHistoryRepository _historyRepository; private readonly Logger _logger; @@ -41,6 +43,16 @@ public PagingSpec Paged(PagingSpec pagingSpec) return _historyRepository.GetPaged(pagingSpec); } + public List BetweenDates(DateTime startDate, DateTime endDate, HistoryEventType eventType) + { + return _historyRepository.BetweenDates(startDate, endDate, eventType); + } + + public List Failed() + { + return _historyRepository.Failed(); + } + public void Purge() { _historyRepository.Purge(); @@ -51,7 +63,7 @@ public virtual void Trim() _historyRepository.Trim(); } - public virtual QualityModel GetBestQualityInHistory(int episodeId) + public QualityModel GetBestQualityInHistory(int episodeId) { return _historyRepository.GetBestQualityInHistory(episodeId).OrderByDescending(q => q).FirstOrDefault(); } @@ -107,5 +119,23 @@ public void Handle(EpisodeImportedEvent message) _historyRepository.Insert(history); } } + + public void Handle(DownloadFailedEvent message) + { + var history = new History + { + EventType = HistoryEventType.DownloadFailed, + Date = DateTime.UtcNow, + Quality = message.Quality, + SourceTitle = message.SourceTitle, + SeriesId = message.Series.Id, + EpisodeId = message.Episode.Id, + }; + + history.Data.Add("DownloadClient", message.DownloadClient); + history.Data.Add("DownloadClientId", message.DownloadClientId); + + _historyRepository.Insert(history); + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Jobs/TaskManager.cs b/src/NzbDrone.Core/Jobs/TaskManager.cs index e2bc22da2..c7d28b727 100644 --- a/src/NzbDrone.Core/Jobs/TaskManager.cs +++ b/src/NzbDrone.Core/Jobs/TaskManager.cs @@ -7,6 +7,7 @@ using NzbDrone.Core.DataAugmentation; using NzbDrone.Core.DataAugmentation.Scene; using NzbDrone.Core.DataAugmentation.Xem; +using NzbDrone.Core.Download; using NzbDrone.Core.Housekeeping; using NzbDrone.Core.Indexers; using NzbDrone.Core.Instrumentation.Commands; @@ -54,7 +55,8 @@ public void Handle(ApplicationStartedEvent message) new ScheduledTask{ Interval = 1*60, TypeName = typeof(TrimLogCommand).FullName}, new ScheduledTask{ Interval = 3*60, TypeName = typeof(UpdateSceneMappingCommand).FullName}, new ScheduledTask{ Interval = 1, TypeName = typeof(TrackedCommandCleanupCommand).FullName}, - new ScheduledTask{ Interval = 24*60, TypeName = typeof(HousekeepingCommand).FullName} + new ScheduledTask{ Interval = 24*60, TypeName = typeof(HousekeepingCommand).FullName}, + new ScheduledTask{ Interval = 1, TypeName = typeof(FailedDownloadCommand).FullName} }; var currentTasks = _scheduledTaskRepository.All(); diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index b7f1607c4..c73a36f09 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -226,9 +226,13 @@ + + + + diff --git a/src/UI/Cells/EventTypeCell.js b/src/UI/Cells/EventTypeCell.js index 3b9bb8b0a..47c179061 100644 --- a/src/UI/Cells/EventTypeCell.js +++ b/src/UI/Cells/EventTypeCell.js @@ -29,6 +29,10 @@ define( icon = 'icon-nd-imported'; toolTip = 'Episode downloaded successfully and picked up from download client'; break; + case 'downloadFailed': + icon = 'icon-nd-download-failed'; + toolTip = 'Episode download failed'; + break; default : icon = 'icon-question'; toolTip = 'unknown event'; diff --git a/src/UI/Content/icons.less b/src/UI/Content/icons.less index 131e82b07..14f35d6dd 100644 --- a/src/UI/Content/icons.less +++ b/src/UI/Content/icons.less @@ -156,4 +156,9 @@ .icon-fatal:before { .icon(@remove-sign); color : purple; +} + +.icon-nd-download-failed:before { + .icon(@cloud-download); + color: @errorText; } \ No newline at end of file From 71c0a340e789968f19346d1b4f9547c4a7200c28 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Tue, 22 Oct 2013 00:39:28 -0700 Subject: [PATCH 13/31] Fixed sab test --- .../SabProviderTests/SabProviderFixture.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabProviderTests/SabProviderFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabProviderTests/SabProviderFixture.cs index c72c165e5..615c7a7e4 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabProviderTests/SabProviderFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabProviderTests/SabProviderFixture.cs @@ -179,10 +179,14 @@ public void downloadNzb_should_use_sabRecentTvPriority_when_recentEpisode_is_tru .SetupGet(s => s.SabRecentTvPriority) .Returns(SabPriorityType.High); + Mocker.GetMock() + .Setup(s => s.DownloadNzb(It.IsAny(), It.IsAny(), It.IsAny(), (int)SabPriorityType.High)) + .Returns("{ \"status\": \"true\", \"nzo_ids\": [ \"sab_id_goes_here\" ] }"); + Subject.DownloadNzb(_remoteEpisode); Mocker.GetMock() - .Verify(v => v.DownloadNzb(It.IsAny(), It.IsAny(), It.IsAny(), (int)SabPriorityType.High), Times.Once()); + .Verify(v => v.DownloadNzb(It.IsAny(), It.IsAny(), It.IsAny(), (int)SabPriorityType.High), Times.Once()); } } } From 1f5bcfeb75b65897991eac1c9b6e0e8d0306f431 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Tue, 22 Oct 2013 23:36:31 -0700 Subject: [PATCH 14/31] Blacklist is now used when processing results --- .../UpgradeHistorySpecificationFixture.cs | 15 ++++++- src/NzbDrone.Core/Blacklisting/Blacklist.cs | 15 +++++++ .../Blacklisting/BlacklistRepository.cs | 23 ++++++++++ .../Blacklisting/BlacklistService.cs | 43 +++++++++++++++++++ .../Migration/028_add_blacklist_table.cs | 19 ++++++++ src/NzbDrone.Core/Datastore/TableMapping.cs | 3 ++ .../Specifications/BlacklistSpecification.cs | 39 +++++++++++++++++ .../RssSync/UpgradeHistorySpecification.cs | 15 ++++++- src/NzbDrone.Core/History/History.cs | 2 - src/NzbDrone.Core/NzbDrone.Core.csproj | 5 +++ 10 files changed, 175 insertions(+), 4 deletions(-) create mode 100644 src/NzbDrone.Core/Blacklisting/Blacklist.cs create mode 100644 src/NzbDrone.Core/Blacklisting/BlacklistRepository.cs create mode 100644 src/NzbDrone.Core/Blacklisting/BlacklistService.cs create mode 100644 src/NzbDrone.Core/Datastore/Migration/028_add_blacklist_table.cs create mode 100644 src/NzbDrone.Core/DecisionEngine/Specifications/BlacklistSpecification.cs diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeHistorySpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeHistorySpecificationFixture.cs index 70e81dc8a..fcb932d00 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeHistorySpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeHistorySpecificationFixture.cs @@ -3,6 +3,8 @@ using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.DecisionEngine.Specifications.RssSync; +using NzbDrone.Core.Download; +using NzbDrone.Core.Download.Clients.Sabnzbd; using NzbDrone.Core.History; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; @@ -64,6 +66,9 @@ public void Setup() Mocker.GetMock().Setup(c => c.GetBestQualityInHistory(1)).Returns(_notupgradableQuality); Mocker.GetMock().Setup(c => c.GetBestQualityInHistory(2)).Returns(_notupgradableQuality); Mocker.GetMock().Setup(c => c.GetBestQualityInHistory(3)).Returns(null); + + Mocker.GetMock() + .Setup(c => c.GetDownloadClient()).Returns(Mocker.GetMock().Object); } private void WithFirstReportUpgradable() @@ -76,7 +81,6 @@ private void WithSecondReportUpgradable() Mocker.GetMock().Setup(c => c.GetBestQualityInHistory(2)).Returns(_upgradableQuality); } - [Test] public void should_be_upgradable_if_only_episode_is_upgradable() { @@ -129,5 +133,14 @@ public void should_return_true_if_it_is_a_search() { _upgradeHistory.IsSatisfiedBy(_parseResultMulti, new SeasonSearchCriteria()).Should().BeTrue(); } + + [Test] + public void should_return_true_if_using_sabnzbd() + { + Mocker.GetMock() + .Setup(c => c.GetDownloadClient()).Returns(Mocker.Resolve()); + + _upgradeHistory.IsSatisfiedBy(_parseResultMulti, new SeasonSearchCriteria()).Should().BeTrue(); + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Blacklisting/Blacklist.cs b/src/NzbDrone.Core/Blacklisting/Blacklist.cs new file mode 100644 index 000000000..444818d8e --- /dev/null +++ b/src/NzbDrone.Core/Blacklisting/Blacklist.cs @@ -0,0 +1,15 @@ +using System; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Blacklisting +{ + public class Blacklist : ModelBase + { + public int EpisodeId { get; set; } + public int SeriesId { get; set; } + public string SourceTitle { get; set; } + public QualityModel Quality { get; set; } + public DateTime Date { get; set; } + } +} diff --git a/src/NzbDrone.Core/Blacklisting/BlacklistRepository.cs b/src/NzbDrone.Core/Blacklisting/BlacklistRepository.cs new file mode 100644 index 000000000..90d026325 --- /dev/null +++ b/src/NzbDrone.Core/Blacklisting/BlacklistRepository.cs @@ -0,0 +1,23 @@ +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Messaging.Events; + +namespace NzbDrone.Core.Blacklisting +{ + public interface IBlacklistRepository : IBasicRepository + { + bool Blacklisted(string sourceTitle); + } + + public class BlacklistRepository : BasicRepository, IBlacklistRepository + { + public BlacklistRepository(IDatabase database, IEventAggregator eventAggregator) : + base(database, eventAggregator) + { + } + + public bool Blacklisted(string sourceTitle) + { + return Query.Any(e => e.SourceTitle == sourceTitle); + } + } +} diff --git a/src/NzbDrone.Core/Blacklisting/BlacklistService.cs b/src/NzbDrone.Core/Blacklisting/BlacklistService.cs new file mode 100644 index 000000000..756c6673c --- /dev/null +++ b/src/NzbDrone.Core/Blacklisting/BlacklistService.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NzbDrone.Core.Download; +using NzbDrone.Core.Messaging.Events; + +namespace NzbDrone.Core.Blacklisting +{ + public interface IBlacklistService + { + bool Blacklisted(string sourceTitle); + } + + public class BlacklistService : IBlacklistService, IHandle + { + private readonly IBlacklistRepository _blacklistRepository; + + public BlacklistService(IBlacklistRepository blacklistRepository) + { + _blacklistRepository = blacklistRepository; + } + + public bool Blacklisted(string sourceTitle) + { + return _blacklistRepository.Blacklisted(sourceTitle); + } + + public void Handle(DownloadFailedEvent message) + { + var blacklist = new Blacklist + { + SeriesId = message.Series.Id, + EpisodeId = message.Episode.Id, + SourceTitle = message.SourceTitle, + Quality = message.Quality, + Date = DateTime.UtcNow + }; + + _blacklistRepository.Insert(blacklist); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/028_add_blacklist_table.cs b/src/NzbDrone.Core/Datastore/Migration/028_add_blacklist_table.cs new file mode 100644 index 000000000..94a3dda93 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/028_add_blacklist_table.cs @@ -0,0 +1,19 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(28)] + public class add_blacklist_table : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Create.TableForModel("Blacklist") + .WithColumn("SeriesId").AsInt32() + .WithColumn("EpisodeId").AsInt32() + .WithColumn("SourceTitle").AsString() + .WithColumn("Quality").AsString() + .WithColumn("Date").AsDateTime(); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index ec341d6dc..4d4c9bc38 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -4,6 +4,7 @@ using Marr.Data; using Marr.Data.Mapping; using NzbDrone.Common.Reflection; +using NzbDrone.Core.Blacklisting; using NzbDrone.Core.Configuration; using NzbDrone.Core.DataAugmentation.Scene; using NzbDrone.Core.Datastore.Converters; @@ -67,6 +68,8 @@ public static void Map() Mapper.Entity().RegisterModel("NamingConfig"); Mapper.Entity().MapResultSet(); + + Mapper.Entity().RegisterModel("Blacklist"); } private static void RegisterMappers() diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/BlacklistSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/BlacklistSpecification.cs new file mode 100644 index 000000000..cea341a2e --- /dev/null +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/BlacklistSpecification.cs @@ -0,0 +1,39 @@ +using System.Linq; +using NLog; +using NzbDrone.Core.Blacklisting; +using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.DecisionEngine.Specifications +{ + public class BlacklistSpecification : IDecisionEngineSpecification + { + private readonly IBlacklistService _blacklistService; + private readonly Logger _logger; + + public BlacklistSpecification(IBlacklistService blacklistService, Logger logger) + { + _blacklistService = blacklistService; + _logger = logger; + } + + public string RejectionReason + { + get + { + return "Release is blacklisted"; + } + } + + public virtual bool IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) + { + if (_blacklistService.Blacklisted(subject.Release.Title)) + { + _logger.Trace("Release is blacklisted"); + return false; + } + + return true; + } + } +} diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/UpgradeHistorySpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/UpgradeHistorySpecification.cs index e844cb91d..28cd5bbb2 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/UpgradeHistorySpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/UpgradeHistorySpecification.cs @@ -1,4 +1,6 @@ using NLog; +using NzbDrone.Core.Download; +using NzbDrone.Core.Download.Clients.Sabnzbd; using NzbDrone.Core.History; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; @@ -9,12 +11,17 @@ public class UpgradeHistorySpecification : IDecisionEngineSpecification { private readonly IHistoryService _historyService; private readonly QualityUpgradableSpecification _qualityUpgradableSpecification; + private readonly IProvideDownloadClient _downloadClientProvider; private readonly Logger _logger; - public UpgradeHistorySpecification(IHistoryService historyService, QualityUpgradableSpecification qualityUpgradableSpecification, Logger logger) + public UpgradeHistorySpecification(IHistoryService historyService, + QualityUpgradableSpecification qualityUpgradableSpecification, + IProvideDownloadClient downloadClientProvider, + Logger logger) { _historyService = historyService; _qualityUpgradableSpecification = qualityUpgradableSpecification; + _downloadClientProvider = downloadClientProvider; _logger = logger; } @@ -34,6 +41,12 @@ public virtual bool IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase sear return true; } + if (_downloadClientProvider.GetDownloadClient().GetType() == typeof (SabnzbdClient)) + { + _logger.Trace("Skipping history check in favour of blacklist"); + return true; + } + foreach (var episode in subject.Episodes) { var bestQualityInHistory = _historyService.GetBestQualityInHistory(episode.Id); diff --git a/src/NzbDrone.Core/History/History.cs b/src/NzbDrone.Core/History/History.cs index 6be5f97a9..94b345e59 100644 --- a/src/NzbDrone.Core/History/History.cs +++ b/src/NzbDrone.Core/History/History.cs @@ -23,7 +23,6 @@ public History() public Dictionary Data { get; set; } } - public enum HistoryEventType { Unknown = 0, @@ -32,5 +31,4 @@ public enum HistoryEventType DownloadFolderImported = 3, DownloadFailed = 4 } - } \ No newline at end of file diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index c73a36f09..1c60b1d90 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -119,6 +119,9 @@ Properties\SharedAssemblyInfo.cs + + + @@ -181,6 +184,7 @@ Code + @@ -200,6 +204,7 @@ + From 68e40bca29bf47b912d71fc31097a5d5218cec67 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Wed, 23 Oct 2013 08:19:27 -0700 Subject: [PATCH 15/31] Blacklist check is case insensitive now --- .../SabProviderTests/QueueFixture.cs | 324 ------------------ .../Blacklisting/BlacklistRepository.cs | 5 +- .../IndexerSearch/SearchAndDownloadService.cs | 39 --- 3 files changed, 3 insertions(+), 365 deletions(-) delete mode 100644 src/NzbDrone.Core.Test/Download/DownloadClientTests/SabProviderTests/QueueFixture.cs delete mode 100644 src/NzbDrone.Core/IndexerSearch/SearchAndDownloadService.cs diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabProviderTests/QueueFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabProviderTests/QueueFixture.cs deleted file mode 100644 index 1b5c75c22..000000000 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabProviderTests/QueueFixture.cs +++ /dev/null @@ -1,324 +0,0 @@ -/*using System; -using System.Collections.Generic; -using System.Linq; -using FluentAssertions; -using Moq; -using NUnit.Framework; -using NzbDrone.Common; -using NzbDrone.Core.Configuration; -using NzbDrone.Core.Download.Clients.Sabnzbd; -using NzbDrone.Core.Model; -using NzbDrone.Core.Qualities; -using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; -using NzbDrone.Test.Common; - -namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabProviderTests -{ - [TestFixture] - - public class QueueFixture : CoreTest - { - [SetUp] - public void Setup() - { - string sabHost = "192.168.5.55"; - int sabPort = 2222; - string apikey = "5c770e3197e4fe763423ee7c392c25d1"; - string username = "admin"; - string password = "pass"; - string cat = "tv"; - - var fakeConfig = Mocker.GetMock(); - fakeConfig.SetupGet(c => c.SabHost).Returns(sabHost); - fakeConfig.SetupGet(c => c.SabPort).Returns(sabPort); - fakeConfig.SetupGet(c => c.SabApiKey).Returns(apikey); - fakeConfig.SetupGet(c => c.SabUsername).Returns(username); - fakeConfig.SetupGet(c => c.SabPassword).Returns(password); - fakeConfig.SetupGet(c => c.SabTvCategory).Returns(cat); - } - - private void WithFullQueue() - { - Mocker.GetMock() - .Setup( - s => - s.DownloadString( - "http://192.168.5.55:2222/api?mode=queue&output=json&start=0&limit=0&apikey=5c770e3197e4fe763423ee7c392c25d1&ma_username=admin&ma_password=pass")) - .Returns(ReadAllText("Files","Queue.txt")); - } - - private void WithEmptyQueue() - { - Mocker.GetMock() - .Setup(s => s.DownloadString("http://192.168.5.55:2222/api?mode=queue&output=json&start=0&limit=0&apikey=5c770e3197e4fe763423ee7c392c25d1&ma_username=admin&ma_password=pass")) - .Returns(ReadAllText("Files","QueueEmpty.txt")); - } - - private void WithFailResponse() - { - Mocker.GetMock() - .Setup(s => s.DownloadString(It.IsAny())).Returns(ReadAllText("Files","JsonError.txt")); - } - - private void WithUnknownPriorityQueue() - { - Mocker.GetMock() - .Setup( - s => - s.DownloadString( - "http://192.168.5.55:2222/api?mode=queue&output=json&start=0&limit=0&apikey=5c770e3197e4fe763423ee7c392c25d1&ma_username=admin&ma_password=pass")) - .Returns(ReadAllText("Files", "QueueUnknownPriority.txt")); - } - - [Test] - public void GetQueue_should_return_an_empty_list_when_the_queue_is_empty() - { - WithEmptyQueue(); - - var result = Mocker.Resolve().GetQueue(); - - result.Should().BeEmpty(); - } - - [Test] - public void GetQueue_should_throw_when_there_is_an_error_getting_the_queue() - { - WithFailResponse(); - - Assert.Throws(() => Mocker.Resolve().GetQueue(), "API Key Incorrect"); - } - - [Test] - public void GetQueue_should_return_a_list_with_items_when_the_queue_has_items() - { - WithFullQueue(); - - var result = Mocker.Resolve().GetQueue(); - - result.Should().HaveCount(7); - } - - [Test] - public void GetQueue_should_return_a_list_with_items_even_when_priority_is_non_standard() - { - WithUnknownPriorityQueue(); - - var result = Mocker.Resolve().GetQueue(); - - result.Should().HaveCount(7); - } - - [Test] - public void is_in_queue_should_find_if_exact_episode_is_in_queue() - { - WithFullQueue(); - - var parseResult = new RemoteEpisode - { - EpisodeTitle = "Title", - EpisodeNumbers = new List { 5 }, - SeasonNumber = 1, - Quality = new QualityModel { Quality = Quality.SDTV, Proper = false }, - Series = new Series { Title = "30 Rock", CleanTitle = Parser.NormalizeTitle("30 Rock") }, - }; - - - var result = Mocker.Resolve().IsInQueue(parseResult); - - result.Should().BeTrue(); - } - - [Test] - public void is_in_queue_should_find_if_exact_daily_episode_is_in_queue() - { - WithFullQueue(); - - var parseResult = new RemoteEpisode - { - Quality = new QualityModel { Quality = Quality.Bluray720p, Proper = false }, - AirDate = new DateTime(2011, 12, 01), - Series = new Series { Title = "The Dailyshow", CleanTitle = Parser.NormalizeTitle("The Dailyshow"), SeriesType = SeriesTypes.Daily }, - }; - - - var result = Mocker.Resolve().IsInQueue(parseResult); - - result.Should().BeTrue(); - } - - [Test] - public void is_in_queue_should_find_if_exact_full_season_release_is_in_queue() - { - WithFullQueue(); - - - var parseResult = new RemoteEpisode - { - Quality = new QualityModel { Quality = Quality.Bluray720p, Proper = false }, - FullSeason = true, - SeasonNumber = 5, - Series = new Series { Title = "My Name is earl", CleanTitle = Parser.NormalizeTitle("My Name is earl") }, - }; - - var result = Mocker.Resolve().IsInQueue(parseResult); - - result.Should().BeTrue(); - } - - public static object[] DifferentEpisodeCases = - { - new object[] { 2, new[] { 5 }, "30 Rock", Quality.Bluray1080p, true }, //Same Series, Different Season, Episode - new object[] { 1, new[] { 6 }, "30 Rock", Quality.Bluray1080p, true }, //Same series, different episodes - new object[] { 1, new[] { 6, 7, 8 }, "30 Rock", Quality.Bluray1080p, true }, //Same series, different episodes - new object[] { 1, new[] { 6 }, "Some other show", Quality.Bluray1080p, true }, //Different series, same season, episode - new object[] { 1, new[] { 5 }, "Rock", Quality.Bluray1080p, true }, //Similar series, same season, episodes - new object[] { 1, new[] { 5 }, "30 Rock", Quality.Bluray720p, false }, //Same series, higher quality - new object[] { 1, new[] { 5 }, "30 Rock", Quality.HDTV720p, true } //Same series, higher quality - }; - - [Test, TestCaseSource("DifferentEpisodeCases")] - public void IsInQueue_should_not_find_diffrent_episode_queue(int season, int[] episodes, string title, Quality qualityType, bool proper) - { - WithFullQueue(); - - var parseResult = new RemoteEpisode - { - EpisodeTitle = "Title", - EpisodeNumbers = new List(episodes), - SeasonNumber = season, - Quality = new QualityModel { Quality = qualityType, Proper = proper }, - Series = new Series { Title = title, CleanTitle = Parser.NormalizeTitle(title) }, - }; - - var result = Mocker.Resolve().IsInQueue(parseResult); - - result.Should().BeFalse(); - } - - public static object[] LowerQualityCases = - { - new object[] { 1, new[] { 5 }, "30 Rock", Quality.SDTV, false }, //Same Series, lower quality - new object[] { 1, new[] { 5 }, "30 rocK", Quality.SDTV, false }, //Same Series, different casing - new object[] { 1, new[] { 5 }, "30 RocK", Quality.HDTV720p, false }, //Same Series, same quality - new object[] { 1, new[] { 5, 6 }, "30 RocK", Quality.HDTV720p, false }, //Same Series, same quality, one different episode - new object[] { 1, new[] { 5, 6 }, "30 RocK", Quality.HDTV720p, false }, //Same Series, same quality, one different episode - new object[] { 4, new[] { 8 }, "Parks and Recreation", Quality.WEBDL720p, false }, //Same Series, same quality - }; - - [Test, TestCaseSource("LowerQualityCases")] - public void IsInQueue_should_find_same_or_lower_quality_episode_queue(int season, int[] episodes, string title, Quality qualityType, bool proper) - { - WithFullQueue(); - - var parseResult = new RemoteEpisode - { - EpisodeTitle = "Title", - EpisodeNumbers = new List(episodes), - SeasonNumber = season, - Quality = new QualityModel { Quality = qualityType, Proper = proper }, - Series = new Series { Title = title, CleanTitle = Parser.NormalizeTitle(title) }, - }; - - var result = Mocker.Resolve().IsInQueue(parseResult); - - result.Should().BeTrue(); - } - - public static object[] DuplicateItemsCases = - { - new object[] { 5, new[] { 13 }, "The Big Bang Theory", Quality.SDTV, false }, //Same Series, lower quality - new object[] { 5, new[] { 13 }, "The Big Bang Theory", Quality.HDTV720p, false }, //Same Series, same quality - new object[] { 5, new[] { 13 }, "The Big Bang Theory", Quality.HDTV720p, true }, //Same Series, same quality - new object[] { 5, new[] { 13, 14 }, "The Big Bang Theory", Quality.HDTV720p, false } //Same Series, same quality, one diffrent episode - }; - - [Test, TestCaseSource("DuplicateItemsCases")] - public void IsInQueue_should_find_items_marked_as_duplicate(int season, int[] episodes, string title, Quality qualityType, bool proper) - { - WithFullQueue(); - - var parseResult = new RemoteEpisode - { - EpisodeTitle = "Title", - EpisodeNumbers = new List(episodes), - SeasonNumber = season, - Quality = new QualityModel { Quality = qualityType, Proper = proper }, - Series = new Series { Title = title, CleanTitle = Parser.NormalizeTitle(title) }, - }; - - var result = Mocker.Resolve().IsInQueue(parseResult); - - result.Should().BeTrue(); - } - - public static object[] DoubleEpisodeCases = - { - new object[] { 3, new[] { 14, 15 }, "My Name Is Earl", Quality.Bluray720p, false }, - new object[] { 3, new[] { 15 }, "My Name Is Earl", Quality.DVD, false }, - new object[] { 3, new[] { 14 }, "My Name Is Earl", Quality.HDTV720p, false }, - new object[] { 3, new[] { 15, 16 }, "My Name Is Earl", Quality.SDTV, false } - }; - - [Test, TestCaseSource("DoubleEpisodeCases")] - public void IsInQueue_should_find_double_episodes_(int season, int[] episodes, string title, Quality qualityType, bool proper) - { - WithFullQueue(); - - var parseResult = new RemoteEpisode - { - EpisodeTitle = "Title", - EpisodeNumbers = new List(episodes), - SeasonNumber = season, - Quality = new QualityModel { Quality = qualityType, Proper = proper }, - Series = new Series { Title = title, CleanTitle = Parser.NormalizeTitle(title) }, - }; - - var result = Mocker.Resolve().IsInQueue(parseResult); - - result.Should().BeTrue(); - } - - [Test] - public void IsInQueue_should_return_false_if_queue_is_empty() - { - WithEmptyQueue(); - - var parseResult = new RemoteEpisode - { - EpisodeTitle = "Title", - EpisodeNumbers = new List { 1 }, - SeasonNumber = 2, - Quality = new QualityModel { Quality = Quality.Bluray1080p, Proper = true }, - Series = new Series { Title = "Test", CleanTitle = Parser.NormalizeTitle("Test") }, - }; - - var result = Mocker.Resolve().IsInQueue(parseResult); - - result.Should().BeFalse(); - } - - [Test] - public void GetQueue_should_parse_timeleft_with_hours_greater_than_24_hours() - { - WithFullQueue(); - - var result = Mocker.Resolve().GetQueue(); - - result.Should().NotBeEmpty(); - var timeleft = result.First(q => q.Id == "SABnzbd_nzo_qv6ilb").Timeleft; - timeleft.Days.Should().Be(2); - timeleft.Hours.Should().Be(9); - timeleft.Minutes.Should().Be(27); - timeleft.Seconds.Should().Be(45); - } - - [TearDown] - public void TearDown() - { - ExceptionVerification.IgnoreWarns(); - } - - - } -}*/ \ No newline at end of file diff --git a/src/NzbDrone.Core/Blacklisting/BlacklistRepository.cs b/src/NzbDrone.Core/Blacklisting/BlacklistRepository.cs index 90d026325..c0b7e5fbf 100644 --- a/src/NzbDrone.Core/Blacklisting/BlacklistRepository.cs +++ b/src/NzbDrone.Core/Blacklisting/BlacklistRepository.cs @@ -1,4 +1,5 @@ -using NzbDrone.Core.Datastore; +using System; +using NzbDrone.Core.Datastore; using NzbDrone.Core.Messaging.Events; namespace NzbDrone.Core.Blacklisting @@ -17,7 +18,7 @@ public BlacklistRepository(IDatabase database, IEventAggregator eventAggregator) public bool Blacklisted(string sourceTitle) { - return Query.Any(e => e.SourceTitle == sourceTitle); + return Query.Any(e => e.SourceTitle.Contains(sourceTitle)); } } } diff --git a/src/NzbDrone.Core/IndexerSearch/SearchAndDownloadService.cs b/src/NzbDrone.Core/IndexerSearch/SearchAndDownloadService.cs deleted file mode 100644 index d15c9c63c..000000000 --- a/src/NzbDrone.Core/IndexerSearch/SearchAndDownloadService.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; - -namespace NzbDrone.Core.IndexerSearch -{ - - interface ISearchAndDownload - { - void SearchSingle(int seriesId, int seasonNumber, int episodeNumber); - void SearchDaily(int seriesId, DateTime airDate); - void SearchSeason(int seriesId, int seasonNumber); - } - - /* public class SearchAndDownloadService : ISearchAndDownload - { - private readonly ISearchForNzb _searchService; - private readonly IMakeDownloadDecision _downloadDecisionMaker; - - public SearchAndDownloadService(ISearchForNzb searchService, IMakeDownloadDecision downloadDecisionMaker) - { - _searchService = searchService; - _downloadDecisionMaker = downloadDecisionMaker; - } - - public void FetchSearchSingle(int seriesId, int seasonNumber, int episodeNumber) - { - var result = _searchService.SearchSingle(seriesId, seasonNumber, episodeNumber); - } - - public void SearchDaily(int seriesId, DateTime airDate) - { - throw new NotImplementedException(); - } - - public void SearchSeason(int seriesId, int seasonNumber) - { - throw new NotImplementedException(); - } - }*/ -} \ No newline at end of file From 8520fe3e0c10785525db18f2a5e371dfefc7ac64 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Wed, 23 Oct 2013 22:13:04 -0700 Subject: [PATCH 16/31] Blacklisting will trigger episode search --- ...ture.cs => HistorySpecificationFixture.cs} | 53 +++++++++++++-- .../NzbDrone.Core.Test.csproj | 3 +- src/NzbDrone.Core/Blacklisting/Blacklist.cs | 3 +- .../Blacklisting/BlacklistService.cs | 10 ++- .../Migration/028_add_blacklist_table.cs | 2 +- ...ecification.cs => HistorySpecification.cs} | 17 ++++- .../Download/DownloadFailedEvent.cs | 5 +- .../Download/FailedDownloadService.cs | 29 ++++---- .../RedownloadFailedDownloadService.cs | 67 +++++++++++++++++++ .../History/HistoryRepository.cs | 15 ++++- src/NzbDrone.Core/History/HistoryService.cs | 37 +++++++--- src/NzbDrone.Core/NzbDrone.Core.csproj | 4 +- 12 files changed, 197 insertions(+), 48 deletions(-) rename src/NzbDrone.Core.Test/DecisionEngineTests/{UpgradeHistorySpecificationFixture.cs => HistorySpecificationFixture.cs} (76%) rename src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/{UpgradeHistorySpecification.cs => HistorySpecification.cs} (75%) create mode 100644 src/NzbDrone.Core/Download/RedownloadFailedDownloadService.cs diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeHistorySpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/HistorySpecificationFixture.cs similarity index 76% rename from src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeHistorySpecificationFixture.cs rename to src/NzbDrone.Core.Test/DecisionEngineTests/HistorySpecificationFixture.cs index fcb932d00..0fe7b7ba0 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeHistorySpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/HistorySpecificationFixture.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using FizzWare.NBuilder; using FluentAssertions; +using Moq; using NUnit.Framework; using NzbDrone.Core.DecisionEngine.Specifications.RssSync; using NzbDrone.Core.Download; @@ -17,9 +18,9 @@ namespace NzbDrone.Core.Test.DecisionEngineTests { [TestFixture] - public class UpgradeHistorySpecificationFixture : CoreTest + public class HistorySpecificationFixture : CoreTest { - private UpgradeHistorySpecification _upgradeHistory; + private HistorySpecification _upgradeHistory; private RemoteEpisode _parseResultMulti; private RemoteEpisode _parseResultSingle; @@ -31,7 +32,7 @@ public class UpgradeHistorySpecificationFixture : CoreTest(); - _upgradeHistory = Mocker.Resolve(); + _upgradeHistory = Mocker.Resolve(); var singleEpisodeList = new List { new Episode { Id = 1, SeasonNumber = 12, EpisodeNumber = 3 } }; var doubleEpisodeList = new List { @@ -81,6 +82,18 @@ private void WithSecondReportUpgradable() Mocker.GetMock().Setup(c => c.GetBestQualityInHistory(2)).Returns(_upgradableQuality); } + private void GivenSabnzbdDownloadClient() + { + Mocker.GetMock() + .Setup(c => c.GetDownloadClient()).Returns(Mocker.Resolve()); + } + + private void GivenMostRecentForEpisode(HistoryEventType eventType) + { + Mocker.GetMock().Setup(s => s.MostRecentForEpisode(It.IsAny())) + .Returns(new History.History { EventType = eventType }); + } + [Test] public void should_be_upgradable_if_only_episode_is_upgradable() { @@ -135,12 +148,38 @@ public void should_return_true_if_it_is_a_search() } [Test] - public void should_return_true_if_using_sabnzbd() + public void should_return_true_if_using_sabnzbd_and_nothing_in_history() { - Mocker.GetMock() - .Setup(c => c.GetDownloadClient()).Returns(Mocker.Resolve()); + GivenSabnzbdDownloadClient(); - _upgradeHistory.IsSatisfiedBy(_parseResultMulti, new SeasonSearchCriteria()).Should().BeTrue(); + _upgradeHistory.IsSatisfiedBy(_parseResultMulti, null).Should().BeTrue(); + } + + [Test] + public void should_return_false_if_most_recent_in_history_is_grabbed() + { + GivenSabnzbdDownloadClient(); + GivenMostRecentForEpisode(HistoryEventType.Grabbed); + + _upgradeHistory.IsSatisfiedBy(_parseResultMulti, null).Should().BeFalse(); + } + + [Test] + public void should_return_true_if_most_recent_in_history_is_failed() + { + GivenSabnzbdDownloadClient(); + GivenMostRecentForEpisode(HistoryEventType.DownloadFailed); + + _upgradeHistory.IsSatisfiedBy(_parseResultMulti, null).Should().BeTrue(); + } + + [Test] + public void should_return_true_if_most_recent_in_history_is_imported() + { + GivenSabnzbdDownloadClient(); + GivenMostRecentForEpisode(HistoryEventType.DownloadFolderImported); + + _upgradeHistory.IsSatisfiedBy(_parseResultMulti, null).Should().BeTrue(); } } } \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index f0a409e97..08aff02d2 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -121,7 +121,6 @@ - @@ -194,7 +193,7 @@ - + diff --git a/src/NzbDrone.Core/Blacklisting/Blacklist.cs b/src/NzbDrone.Core/Blacklisting/Blacklist.cs index 444818d8e..94cc5ffed 100644 --- a/src/NzbDrone.Core/Blacklisting/Blacklist.cs +++ b/src/NzbDrone.Core/Blacklisting/Blacklist.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using NzbDrone.Core.Datastore; using NzbDrone.Core.Tv; @@ -6,8 +7,8 @@ namespace NzbDrone.Core.Blacklisting { public class Blacklist : ModelBase { - public int EpisodeId { get; set; } public int SeriesId { get; set; } + public List EpisodeIds { get; set; } public string SourceTitle { get; set; } public QualityModel Quality { get; set; } public DateTime Date { get; set; } diff --git a/src/NzbDrone.Core/Blacklisting/BlacklistService.cs b/src/NzbDrone.Core/Blacklisting/BlacklistService.cs index 756c6673c..4f15c59f9 100644 --- a/src/NzbDrone.Core/Blacklisting/BlacklistService.cs +++ b/src/NzbDrone.Core/Blacklisting/BlacklistService.cs @@ -15,10 +15,12 @@ public interface IBlacklistService public class BlacklistService : IBlacklistService, IHandle { private readonly IBlacklistRepository _blacklistRepository; + private readonly IRedownloadFailedDownloads _redownloadFailedDownloadService; - public BlacklistService(IBlacklistRepository blacklistRepository) + public BlacklistService(IBlacklistRepository blacklistRepository, IRedownloadFailedDownloads redownloadFailedDownloadService) { _blacklistRepository = blacklistRepository; + _redownloadFailedDownloadService = redownloadFailedDownloadService; } public bool Blacklisted(string sourceTitle) @@ -30,14 +32,16 @@ public void Handle(DownloadFailedEvent message) { var blacklist = new Blacklist { - SeriesId = message.Series.Id, - EpisodeId = message.Episode.Id, + SeriesId = message.SeriesId, + EpisodeIds = message.EpisodeIds, SourceTitle = message.SourceTitle, Quality = message.Quality, Date = DateTime.UtcNow }; _blacklistRepository.Insert(blacklist); + + _redownloadFailedDownloadService.Redownload(message.SeriesId, message.EpisodeIds); } } } diff --git a/src/NzbDrone.Core/Datastore/Migration/028_add_blacklist_table.cs b/src/NzbDrone.Core/Datastore/Migration/028_add_blacklist_table.cs index 94a3dda93..0514c9689 100644 --- a/src/NzbDrone.Core/Datastore/Migration/028_add_blacklist_table.cs +++ b/src/NzbDrone.Core/Datastore/Migration/028_add_blacklist_table.cs @@ -10,7 +10,7 @@ protected override void MainDbUpgrade() { Create.TableForModel("Blacklist") .WithColumn("SeriesId").AsInt32() - .WithColumn("EpisodeId").AsInt32() + .WithColumn("EpisodeIds").AsString() .WithColumn("SourceTitle").AsString() .WithColumn("Quality").AsString() .WithColumn("Date").AsDateTime(); diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/UpgradeHistorySpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs similarity index 75% rename from src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/UpgradeHistorySpecification.cs rename to src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs index 28cd5bbb2..319b76ce5 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/UpgradeHistorySpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs @@ -3,18 +3,19 @@ using NzbDrone.Core.Download.Clients.Sabnzbd; using NzbDrone.Core.History; using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.MetadataSource.Trakt; using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync { - public class UpgradeHistorySpecification : IDecisionEngineSpecification + public class HistorySpecification : IDecisionEngineSpecification { private readonly IHistoryService _historyService; private readonly QualityUpgradableSpecification _qualityUpgradableSpecification; private readonly IProvideDownloadClient _downloadClientProvider; private readonly Logger _logger; - public UpgradeHistorySpecification(IHistoryService historyService, + public HistorySpecification(IHistoryService historyService, QualityUpgradableSpecification qualityUpgradableSpecification, IProvideDownloadClient downloadClientProvider, Logger logger) @@ -43,7 +44,17 @@ public virtual bool IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase sear if (_downloadClientProvider.GetDownloadClient().GetType() == typeof (SabnzbdClient)) { - _logger.Trace("Skipping history check in favour of blacklist"); + _logger.Trace("Performing history status check on report"); + foreach (var episode in subject.Episodes) + { + _logger.Trace("Checking current status of episode [{0}] in history", episode.Id); + var mostRecent = _historyService.MostRecentForEpisode(episode.Id); + + if (mostRecent != null && mostRecent.EventType == HistoryEventType.Grabbed) + { + return false; + } + } return true; } diff --git a/src/NzbDrone.Core/Download/DownloadFailedEvent.cs b/src/NzbDrone.Core/Download/DownloadFailedEvent.cs index f83444bb2..e2ebde2bc 100644 --- a/src/NzbDrone.Core/Download/DownloadFailedEvent.cs +++ b/src/NzbDrone.Core/Download/DownloadFailedEvent.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using NzbDrone.Common.Messaging; using NzbDrone.Core.Tv; @@ -6,8 +7,8 @@ namespace NzbDrone.Core.Download { public class DownloadFailedEvent : IEvent { - public Series Series { get; set; } - public Episode Episode { get; set; } + public Int32 SeriesId { get; set; } + public List EpisodeIds { get; set; } public QualityModel Quality { get; set; } public String SourceTitle { get; set; } public String DownloadClient { get; set; } diff --git a/src/NzbDrone.Core/Download/FailedDownloadService.cs b/src/NzbDrone.Core/Download/FailedDownloadService.cs index 79431acf1..edcd10da1 100644 --- a/src/NzbDrone.Core/Download/FailedDownloadService.cs +++ b/src/NzbDrone.Core/Download/FailedDownloadService.cs @@ -41,15 +41,15 @@ private void CheckForFailedDownloads() return; } - var recentHistory = _historyService.BetweenDates(DateTime.UtcNow.AddDays(-1), DateTime.UtcNow, HistoryEventType.Grabbed); + var grabbedHistory = _historyService.Grabbed(); var failedHistory = _historyService.Failed(); foreach (var failedItem in failedItems) { var failedLocal = failedItem; - var historyItems = recentHistory.Where(h => h.Data.ContainsKey(DOWNLOAD_CLIENT) && - h.Data[DOWNLOAD_CLIENT_ID].Equals(failedLocal.Id)) - .ToList(); + var historyItems = grabbedHistory.Where(h => h.Data.ContainsKey(DOWNLOAD_CLIENT) && + h.Data[DOWNLOAD_CLIENT_ID].Equals(failedLocal.Id)) + .ToList(); if (!historyItems.Any()) { @@ -64,18 +64,17 @@ private void CheckForFailedDownloads() continue; } - foreach (var historyItem in historyItems) + var historyItem = historyItems.First(); + + _eventAggregator.PublishEvent(new DownloadFailedEvent { - _eventAggregator.PublishEvent(new DownloadFailedEvent - { - Series = historyItem.Series, - Episode = historyItem.Episode, - Quality = historyItem.Quality, - SourceTitle = historyItem.SourceTitle, - DownloadClient = historyItem.Data[DOWNLOAD_CLIENT], - DownloadClientId = historyItem.Data[DOWNLOAD_CLIENT_ID] - }); - } + SeriesId = historyItem.SeriesId, + EpisodeIds = historyItems.Select(h => h.EpisodeId).ToList(), + Quality = historyItem.Quality, + SourceTitle = historyItem.SourceTitle, + DownloadClient = historyItem.Data[DOWNLOAD_CLIENT], + DownloadClientId = historyItem.Data[DOWNLOAD_CLIENT_ID] + }); } } diff --git a/src/NzbDrone.Core/Download/RedownloadFailedDownloadService.cs b/src/NzbDrone.Core/Download/RedownloadFailedDownloadService.cs new file mode 100644 index 000000000..dd78b61fa --- /dev/null +++ b/src/NzbDrone.Core/Download/RedownloadFailedDownloadService.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NLog; +using NzbDrone.Core.IndexerSearch; +using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Download +{ + public interface IRedownloadFailedDownloads + { + void Redownload(int seriesId, List episodeIds); + } + + public class RedownloadFailedDownloadService : IRedownloadFailedDownloads + { + private readonly IEpisodeService _episodeService; + private readonly ICommandExecutor _commandExecutor; + private readonly Logger _logger; + + public RedownloadFailedDownloadService(IEpisodeService episodeService, ICommandExecutor commandExecutor, Logger logger) + { + _episodeService = episodeService; + _commandExecutor = commandExecutor; + _logger = logger; + } + + public void Redownload(int seriesId, List episodeIds) + { + if (episodeIds.Count == 1) + { + _logger.Trace("Failed download only contains one episode, searching again"); + + _commandExecutor.PublishCommandAsync(new EpisodeSearchCommand + { + EpisodeIds = episodeIds.ToList() + }); + + return; + } + + var seasonNumber = _episodeService.GetEpisode(episodeIds.First()).SeasonNumber; + var episodesInSeason = _episodeService.GetEpisodesBySeason(seriesId, seasonNumber); + + if (episodeIds.Count == episodesInSeason.Count) + { + _logger.Trace("Failed download was entire season, searching again"); + + _commandExecutor.PublishCommandAsync(new SeasonSearchCommand + { + SeriesId = seriesId, + SeasonNumber = seasonNumber + }); + + return; + } + + _logger.Trace("Failed download contains multiple episodes, probably a double episode, searching again"); + + _commandExecutor.PublishCommandAsync(new EpisodeSearchCommand + { + EpisodeIds = episodeIds.ToList() + }); + } + } +} diff --git a/src/NzbDrone.Core/History/HistoryRepository.cs b/src/NzbDrone.Core/History/HistoryRepository.cs index 1c3c7807f..59d54c745 100644 --- a/src/NzbDrone.Core/History/HistoryRepository.cs +++ b/src/NzbDrone.Core/History/HistoryRepository.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Linq.Expressions; using Marr.Data.QGen; using NzbDrone.Core.Datastore; using NzbDrone.Core.Messaging.Events; @@ -15,6 +14,8 @@ public interface IHistoryRepository : IBasicRepository List GetBestQualityInHistory(int episodeId); List BetweenDates(DateTime startDate, DateTime endDate, HistoryEventType eventType); List Failed(); + List Grabbed(); + History MostRecentForEpisode(int episodeId); } public class HistoryRepository : BasicRepository, IHistoryRepository @@ -54,6 +55,18 @@ public List Failed() return Query.Where(h => h.EventType == HistoryEventType.DownloadFailed); } + public List Grabbed() + { + return Query.Where(h => h.EventType == HistoryEventType.Grabbed); + } + + public History MostRecentForEpisode(int episodeId) + { + return Query.Where(h => h.EpisodeId == episodeId) + .OrderByDescending(h => h.Date) + .FirstOrDefault(); + } + public override PagingSpec GetPaged(PagingSpec pagingSpec) { pagingSpec.Records = GetPagedQuery(pagingSpec).ToList(); diff --git a/src/NzbDrone.Core/History/HistoryService.cs b/src/NzbDrone.Core/History/HistoryService.cs index 0244d1180..881900b26 100644 --- a/src/NzbDrone.Core/History/HistoryService.cs +++ b/src/NzbDrone.Core/History/HistoryService.cs @@ -20,6 +20,8 @@ public interface IHistoryService PagingSpec Paged(PagingSpec pagingSpec); List BetweenDates(DateTime startDate, DateTime endDate, HistoryEventType eventType); List Failed(); + List Grabbed(); + History MostRecentForEpisode(int episodeId); } public class HistoryService : IHistoryService, IHandle, IHandle, IHandle @@ -53,6 +55,16 @@ public List Failed() return _historyRepository.Failed(); } + public List Grabbed() + { + return _historyRepository.Grabbed(); + } + + public History MostRecentForEpisode(int episodeId) + { + return _historyRepository.MostRecentForEpisode(episodeId); + } + public void Purge() { _historyRepository.Purge(); @@ -122,20 +134,23 @@ public void Handle(EpisodeImportedEvent message) public void Handle(DownloadFailedEvent message) { - var history = new History + foreach (var episodeId in message.EpisodeIds) { - EventType = HistoryEventType.DownloadFailed, - Date = DateTime.UtcNow, - Quality = message.Quality, - SourceTitle = message.SourceTitle, - SeriesId = message.Series.Id, - EpisodeId = message.Episode.Id, - }; + var history = new History + { + EventType = HistoryEventType.DownloadFailed, + Date = DateTime.UtcNow, + Quality = message.Quality, + SourceTitle = message.SourceTitle, + SeriesId = message.SeriesId, + EpisodeId = episodeId, + }; - history.Data.Add("DownloadClient", message.DownloadClient); - history.Data.Add("DownloadClientId", message.DownloadClientId); + history.Data.Add("DownloadClient", message.DownloadClient); + history.Data.Add("DownloadClientId", message.DownloadClientId); - _historyRepository.Insert(history); + _historyRepository.Insert(history); + } } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 1c60b1d90..7a7d5b516 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -223,7 +223,7 @@ - + @@ -239,6 +239,7 @@ + @@ -358,7 +359,6 @@ - From 5150f9bd919d6c7b9b133444c7712c9cabeec5e8 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Wed, 23 Oct 2013 22:24:26 -0700 Subject: [PATCH 17/31] Fixed broken tests --- .../Download/FailedDownloadServiceFixture.cs | 26 +++++++------------ 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/src/NzbDrone.Core.Test/Download/FailedDownloadServiceFixture.cs b/src/NzbDrone.Core.Test/Download/FailedDownloadServiceFixture.cs index 478515d6a..969b0443f 100644 --- a/src/NzbDrone.Core.Test/Download/FailedDownloadServiceFixture.cs +++ b/src/NzbDrone.Core.Test/Download/FailedDownloadServiceFixture.cs @@ -8,24 +8,18 @@ using NzbDrone.Core.History; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Test.Framework; -using NzbDrone.Core.Tv; namespace NzbDrone.Core.Test.Download { [TestFixture] public class FailedDownloadServiceFixture : CoreTest { - private Series _series; - private Episode _episode; private List _completed; private List _failed; [SetUp] public void Setup() { - _series = Builder.CreateNew().Build(); - _episode = Builder.CreateNew().Build(); - _completed = Builder.CreateListOfSize(5) .All() .With(h => h.Status = HistoryStatus.Completed) @@ -42,17 +36,17 @@ public void Setup() .Setup(c => c.GetDownloadClient()).Returns(Mocker.GetMock().Object); } - private void GivenNoRecentHistory() + private void GivenNoGrabbedHistory() { Mocker.GetMock() - .Setup(s => s.BetweenDates(It.IsAny(), It.IsAny(), HistoryEventType.Grabbed)) + .Setup(s => s.Grabbed()) .Returns(new List()); } - private void GivenRecentHistory(List history) + private void GivenGrabbedHistory(List history) { Mocker.GetMock() - .Setup(s => s.BetweenDates(It.IsAny(), It.IsAny(), HistoryEventType.Grabbed)) + .Setup(s => s.Grabbed()) .Returns(history); } @@ -86,7 +80,7 @@ private void VerifyNoFailedDownloads() private void VerifyFailedDownloads(int count = 1) { Mocker.GetMock() - .Verify(v => v.PublishEvent(It.IsAny()), Times.Exactly(count)); + .Verify(v => v.PublishEvent(It.Is(d => d.EpisodeIds.Count == count)), Times.Once()); } [Test] @@ -124,7 +118,7 @@ public void should_not_process_if_no_failed_items_in_download_client_history() [Test] public void should_not_process_if_matching_history_is_not_found() { - GivenNoRecentHistory(); + GivenNoGrabbedHistory(); GivenFailedDownloadClientHistory(); Subject.Execute(new FailedDownloadCommand()); @@ -141,7 +135,7 @@ public void should_not_process_if_already_added_to_history_as_failed() .Build() .ToList(); - GivenRecentHistory(history); + GivenGrabbedHistory(history); GivenFailedHistory(history); history.First().Data.Add("downloadClient", "SabnzbdClient"); @@ -161,7 +155,7 @@ public void should_process_if_not_already_in_failed_history() .Build() .ToList(); - GivenRecentHistory(history); + GivenGrabbedHistory(history); GivenNoFailedHistory(); history.First().Data.Add("downloadClient", "SabnzbdClient"); @@ -173,7 +167,7 @@ public void should_process_if_not_already_in_failed_history() } [Test] - public void should_process_for_each_failed_episode() + public void should_have_multiple_episode_ids_when_multi_episode_release_fails() { GivenFailedDownloadClientHistory(); @@ -181,7 +175,7 @@ public void should_process_for_each_failed_episode() .Build() .ToList(); - GivenRecentHistory(history); + GivenGrabbedHistory(history); GivenNoFailedHistory(); history.ForEach(h => From 1684ad6e167ffa65902e054dab05cd386c1e6d04 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 24 Oct 2013 08:17:39 -0700 Subject: [PATCH 18/31] List will be converted to json and stored in the DB --- src/NzbDrone.Core/Datastore/TableMapping.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index 4d4c9bc38..5b5fe331f 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -83,6 +83,7 @@ private static void RegisterMappers() MapRepository.Instance.RegisterTypeConverter(typeof(Enum), new EnumIntConverter()); MapRepository.Instance.RegisterTypeConverter(typeof(Quality), new QualityIntConverter()); MapRepository.Instance.RegisterTypeConverter(typeof(Dictionary), new EmbeddedDocumentConverter()); + MapRepository.Instance.RegisterTypeConverter(typeof(List), new EmbeddedDocumentConverter()); } private static void RegisterProviderSettingConverter() From f99573e3342e713ad24afd4c997efc014592eee7 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 24 Oct 2013 08:34:39 -0700 Subject: [PATCH 19/31] Added some blacklist tests --- .../BlacklistRepositoryFixture.cs | 57 +++++++++++++++++++ .../Blacklisting/BlacklistServiceFixture.cs | 52 +++++++++++++++++ .../NzbDrone.Core.Test.csproj | 2 + 3 files changed, 111 insertions(+) create mode 100644 src/NzbDrone.Core.Test/Blacklisting/BlacklistRepositoryFixture.cs create mode 100644 src/NzbDrone.Core.Test/Blacklisting/BlacklistServiceFixture.cs diff --git a/src/NzbDrone.Core.Test/Blacklisting/BlacklistRepositoryFixture.cs b/src/NzbDrone.Core.Test/Blacklisting/BlacklistRepositoryFixture.cs new file mode 100644 index 000000000..39ff23c96 --- /dev/null +++ b/src/NzbDrone.Core.Test/Blacklisting/BlacklistRepositoryFixture.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using NzbDrone.Core.Blacklisting; +using NzbDrone.Core.Download; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Test.Blacklisting +{ + [TestFixture] + public class BlacklistRepositoryFixture : DbTest + { + private Blacklist _blacklist; + + [SetUp] + public void Setup() + { + _blacklist = new Blacklist + { + SeriesId = 12345, + EpisodeIds = new List {1}, + Quality = new QualityModel(Quality.Bluray720p), + SourceTitle = "series.title.s01e01", + Date = DateTime.UtcNow + }; + } + + [Test] + public void should_be_able_to_write_to_database() + { + Subject.Insert(_blacklist); + Subject.All().Should().HaveCount(1); + } + + [Test] + public void should_should_have_episode_ids() + { + Subject.Insert(_blacklist); + + Subject.All().First().EpisodeIds.Should().Contain(_blacklist.EpisodeIds); + } + + [Test] + public void should_check_for_blacklisted_title_case_insensative() + { + Subject.Insert(_blacklist); + + Subject.Blacklisted(_blacklist.SourceTitle.ToUpperInvariant()).Should().BeTrue(); + } + } +} diff --git a/src/NzbDrone.Core.Test/Blacklisting/BlacklistServiceFixture.cs b/src/NzbDrone.Core.Test/Blacklisting/BlacklistServiceFixture.cs new file mode 100644 index 000000000..85d19db97 --- /dev/null +++ b/src/NzbDrone.Core.Test/Blacklisting/BlacklistServiceFixture.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Moq; +using NUnit.Framework; +using NzbDrone.Core.Blacklisting; +using NzbDrone.Core.Download; +using NzbDrone.Core.Qualities; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Test.Blacklisting +{ + [TestFixture] + public class BlacklistServiceFixture : CoreTest + { + private DownloadFailedEvent _event; + + [SetUp] + public void Setup() + { + _event = new DownloadFailedEvent + { + SeriesId = 12345, + EpisodeIds = new List {1}, + Quality = new QualityModel(Quality.Bluray720p), + SourceTitle = "series.title.s01e01", + DownloadClient = "SabnzbdClient", + DownloadClientId = "Sabnzbd_nzo_2dfh73k" + }; + } + + [Test] + public void should_trigger_redownload() + { + Subject.Handle(_event); + + Mocker.GetMock() + .Verify(v => v.Redownload(_event.SeriesId, _event.EpisodeIds), Times.Once()); + } + + [Test] + public void should_add_to_repository() + { + Subject.Handle(_event); + + Mocker.GetMock() + .Verify(v => v.Insert(It.Is(b => b.EpisodeIds == _event.EpisodeIds)), Times.Once()); + } + } +} diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 08aff02d2..2cee27fdc 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -99,6 +99,8 @@ + + From 6dd2951f801dbbbdc0a13f49a44bd3371cd13c70 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 24 Oct 2013 15:12:39 -0700 Subject: [PATCH 20/31] Redownload after failure is an advanced option. New: Handle failed downloads and attempt to find another release (SABnzbd only) --- .../Configuration/ConfigService.cs | 7 +++++++ .../Configuration/IConfigService.cs | 1 + .../RedownloadFailedDownloadService.cs | 11 +++++++++- .../FileManagementViewTemplate.html | 20 +++++++++++++++++++ 4 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Configuration/ConfigService.cs b/src/NzbDrone.Core/Configuration/ConfigService.cs index a7bebdb6d..f8f3908b7 100644 --- a/src/NzbDrone.Core/Configuration/ConfigService.cs +++ b/src/NzbDrone.Core/Configuration/ConfigService.cs @@ -261,6 +261,13 @@ public Boolean AutoDownloadPropers set { SetValue("AutoDownloadPropers", value); } } + public Boolean AutoRedownloadFailed + { + get { return GetValueBoolean("AutoRedownloadFailed", true); } + + set { SetValue("AutoRedownloadFailed", value); } + } + public string DownloadClientWorkingFolders { get { return GetValue("DownloadClientWorkingFolders", "_UNPACK_|_FAILED_"); } diff --git a/src/NzbDrone.Core/Configuration/IConfigService.cs b/src/NzbDrone.Core/Configuration/IConfigService.cs index e226740b0..59a24c7fe 100644 --- a/src/NzbDrone.Core/Configuration/IConfigService.cs +++ b/src/NzbDrone.Core/Configuration/IConfigService.cs @@ -39,6 +39,7 @@ public interface IConfigService Int32 RssSyncInterval { get; set; } Boolean AutoDownloadPropers { get; set; } String DownloadClientWorkingFolders { get; set; } + Boolean AutoRedownloadFailed { get; set; } void SaveValues(Dictionary configValues); } } diff --git a/src/NzbDrone.Core/Download/RedownloadFailedDownloadService.cs b/src/NzbDrone.Core/Download/RedownloadFailedDownloadService.cs index dd78b61fa..dd40220c9 100644 --- a/src/NzbDrone.Core/Download/RedownloadFailedDownloadService.cs +++ b/src/NzbDrone.Core/Download/RedownloadFailedDownloadService.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using NLog; +using NzbDrone.Core.Configuration; using NzbDrone.Core.IndexerSearch; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Tv; @@ -15,12 +16,14 @@ public interface IRedownloadFailedDownloads public class RedownloadFailedDownloadService : IRedownloadFailedDownloads { + private readonly IConfigService _configService; private readonly IEpisodeService _episodeService; private readonly ICommandExecutor _commandExecutor; private readonly Logger _logger; - public RedownloadFailedDownloadService(IEpisodeService episodeService, ICommandExecutor commandExecutor, Logger logger) + public RedownloadFailedDownloadService(IConfigService configService, IEpisodeService episodeService, ICommandExecutor commandExecutor, Logger logger) { + _configService = configService; _episodeService = episodeService; _commandExecutor = commandExecutor; _logger = logger; @@ -28,6 +31,12 @@ public RedownloadFailedDownloadService(IEpisodeService episodeService, ICommandE public void Redownload(int seriesId, List episodeIds) { + if (!_configService.AutoRedownloadFailed) + { + _logger.Trace("Auto redownloading failed episodes is disabled"); + return; + } + if (episodeIds.Count == 1) { _logger.Trace("Failed download only contains one episode, searching again"); diff --git a/src/UI/Settings/MediaManagement/FileManagement/FileManagementViewTemplate.html b/src/UI/Settings/MediaManagement/FileManagement/FileManagementViewTemplate.html index 02938c4e0..e66c50607 100644 --- a/src/UI/Settings/MediaManagement/FileManagement/FileManagementViewTemplate.html +++ b/src/UI/Settings/MediaManagement/FileManagement/FileManagementViewTemplate.html @@ -41,6 +41,26 @@ +
+ + +
+
+
From 769fcdfc80ff9f9a0722e580c503eed7cc854684 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 24 Oct 2013 18:25:04 -0700 Subject: [PATCH 21/31] Added message to failed history events --- src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdClient.cs | 1 + src/NzbDrone.Core/Download/DownloadFailedEvent.cs | 1 + src/NzbDrone.Core/Download/FailedDownloadService.cs | 3 ++- src/NzbDrone.Core/Download/HistoryItem.cs | 1 + src/NzbDrone.Core/History/HistoryService.cs | 1 + 5 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdClient.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdClient.cs index 106e5fc71..cba13d63f 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdClient.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdClient.cs @@ -124,6 +124,7 @@ public IEnumerable GetHistory(int start = 0, int limit = 0) historyItem.DownloadTime = sabHistoryItem.DownloadTime; historyItem.Storage = sabHistoryItem.Storage; historyItem.Category = sabHistoryItem.Category; + historyItem.Message = sabHistoryItem.FailMessage; historyItem.Status = sabHistoryItem.Status == "Failed" ? HistoryStatus.Failed : HistoryStatus.Completed; historyItems.Add(historyItem); diff --git a/src/NzbDrone.Core/Download/DownloadFailedEvent.cs b/src/NzbDrone.Core/Download/DownloadFailedEvent.cs index e2ebde2bc..188ab24f9 100644 --- a/src/NzbDrone.Core/Download/DownloadFailedEvent.cs +++ b/src/NzbDrone.Core/Download/DownloadFailedEvent.cs @@ -13,5 +13,6 @@ public class DownloadFailedEvent : IEvent public String SourceTitle { get; set; } public String DownloadClient { get; set; } public String DownloadClientId { get; set; } + public String Message { get; set; } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Download/FailedDownloadService.cs b/src/NzbDrone.Core/Download/FailedDownloadService.cs index edcd10da1..60accd3bc 100644 --- a/src/NzbDrone.Core/Download/FailedDownloadService.cs +++ b/src/NzbDrone.Core/Download/FailedDownloadService.cs @@ -73,7 +73,8 @@ private void CheckForFailedDownloads() Quality = historyItem.Quality, SourceTitle = historyItem.SourceTitle, DownloadClient = historyItem.Data[DOWNLOAD_CLIENT], - DownloadClientId = historyItem.Data[DOWNLOAD_CLIENT_ID] + DownloadClientId = historyItem.Data[DOWNLOAD_CLIENT_ID], + Message = failedItem.Message }); } } diff --git a/src/NzbDrone.Core/Download/HistoryItem.cs b/src/NzbDrone.Core/Download/HistoryItem.cs index 094270107..1dd969f29 100644 --- a/src/NzbDrone.Core/Download/HistoryItem.cs +++ b/src/NzbDrone.Core/Download/HistoryItem.cs @@ -11,6 +11,7 @@ public class HistoryItem public String Category { get; set; } public Int32 DownloadTime { get; set; } public String Storage { get; set; } + public String Message { get; set; } public HistoryStatus Status { get; set; } } diff --git a/src/NzbDrone.Core/History/HistoryService.cs b/src/NzbDrone.Core/History/HistoryService.cs index 881900b26..e738393db 100644 --- a/src/NzbDrone.Core/History/HistoryService.cs +++ b/src/NzbDrone.Core/History/HistoryService.cs @@ -148,6 +148,7 @@ public void Handle(DownloadFailedEvent message) history.Data.Add("DownloadClient", message.DownloadClient); history.Data.Add("DownloadClientId", message.DownloadClientId); + history.Data.Add("Message", message.Message); _historyRepository.Insert(history); } From d634dd1e5cbaabc7bb269d20a19fae3472061812 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 24 Oct 2013 22:55:32 -0700 Subject: [PATCH 22/31] Failed downloads are removed from queue/history (opt out) --- .../Configuration/ConfigService.cs | 7 ++ .../Configuration/IConfigService.cs | 1 + .../Download/Clients/BlackholeProvider.cs | 8 ++ .../Download/Clients/Nzbget/NzbgetClient.cs | 10 ++ .../Download/Clients/PneumaticClient.cs | 8 ++ .../Clients/Sabnzbd/SabCommunicationProxy.cs | 9 ++ .../Download/Clients/Sabnzbd/SabnzbdClient.cs | 12 +- .../Download/FailedDownloadService.cs | 107 ++++++++++++++---- src/NzbDrone.Core/Download/IDownloadClient.cs | 2 + .../FileManagementViewTemplate.html | 38 +++++-- 10 files changed, 174 insertions(+), 28 deletions(-) diff --git a/src/NzbDrone.Core/Configuration/ConfigService.cs b/src/NzbDrone.Core/Configuration/ConfigService.cs index f8f3908b7..b51d92611 100644 --- a/src/NzbDrone.Core/Configuration/ConfigService.cs +++ b/src/NzbDrone.Core/Configuration/ConfigService.cs @@ -268,6 +268,13 @@ public Boolean AutoRedownloadFailed set { SetValue("AutoRedownloadFailed", value); } } + public Boolean RemoveFailedDownloads + { + get { return GetValueBoolean("RemoveFailedDownloads", true); } + + set { SetValue("RemoveFailedDownloads", value); } + } + public string DownloadClientWorkingFolders { get { return GetValue("DownloadClientWorkingFolders", "_UNPACK_|_FAILED_"); } diff --git a/src/NzbDrone.Core/Configuration/IConfigService.cs b/src/NzbDrone.Core/Configuration/IConfigService.cs index 59a24c7fe..f4578d4c1 100644 --- a/src/NzbDrone.Core/Configuration/IConfigService.cs +++ b/src/NzbDrone.Core/Configuration/IConfigService.cs @@ -40,6 +40,7 @@ public interface IConfigService Boolean AutoDownloadPropers { get; set; } String DownloadClientWorkingFolders { get; set; } Boolean AutoRedownloadFailed { get; set; } + Boolean RemoveFailedDownloads { get; set; } void SaveValues(Dictionary configValues); } } diff --git a/src/NzbDrone.Core/Download/Clients/BlackholeProvider.cs b/src/NzbDrone.Core/Download/Clients/BlackholeProvider.cs index a5d8e5fe2..5f43ca351 100644 --- a/src/NzbDrone.Core/Download/Clients/BlackholeProvider.cs +++ b/src/NzbDrone.Core/Download/Clients/BlackholeProvider.cs @@ -57,5 +57,13 @@ public IEnumerable GetHistory(int start = 0, int limit = 0) { return new HistoryItem[0]; } + + public void RemoveFromQueue(string id) + { + } + + public void RemoveFromHistory(string id) + { + } } } diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetClient.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetClient.cs index 94f83ed9d..66acf0f92 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetClient.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetClient.cs @@ -96,6 +96,16 @@ public IEnumerable GetHistory(int start = 0, int limit = 0) return new HistoryItem[0]; } + public void RemoveFromQueue(string id) + { + throw new NotImplementedException(); + } + + public void RemoveFromHistory(string id) + { + throw new NotImplementedException(); + } + public virtual VersionModel GetVersion(string host = null, int port = 0, string username = null, string password = null) { //Get saved values if any of these are defaults diff --git a/src/NzbDrone.Core/Download/Clients/PneumaticClient.cs b/src/NzbDrone.Core/Download/Clients/PneumaticClient.cs index 1036f7d9b..f536965b2 100644 --- a/src/NzbDrone.Core/Download/Clients/PneumaticClient.cs +++ b/src/NzbDrone.Core/Download/Clients/PneumaticClient.cs @@ -70,6 +70,14 @@ public IEnumerable GetHistory(int start = 0, int limit = 0) return new HistoryItem[0]; } + public void RemoveFromQueue(string id) + { + } + + public void RemoveFromHistory(string id) + { + } + public virtual bool IsInQueue(RemoteEpisode newEpisode) { return false; diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabCommunicationProxy.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabCommunicationProxy.cs index ed26110c6..f1d348b17 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabCommunicationProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabCommunicationProxy.cs @@ -9,6 +9,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd public interface ISabCommunicationProxy { string DownloadNzb(Stream nzb, string name, string category, int priority); + void RemoveFrom(string source, string id); string ProcessRequest(IRestRequest restRequest, string action); } @@ -31,6 +32,14 @@ public string DownloadNzb(Stream nzb, string title, string category, int priorit return ProcessRequest(request, action); } + public void RemoveFrom(string source, string id) + { + var request = new RestRequest(); + var action = String.Format("mode={0}&name=delete&del_files=1&value={1}", source, id); + + ProcessRequest(request, action); + } + public string ProcessRequest(IRestRequest restRequest, string action) { var client = BuildClient(action); diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdClient.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdClient.cs index cba13d63f..c5799f5ce 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdClient.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdClient.cs @@ -89,7 +89,7 @@ public IEnumerable GetQueue() queueItem.Timeleft = sabQueueItem.Timeleft; queueItem.Status = sabQueueItem.Status; - var parsedEpisodeInfo = Parser.Parser.ParseTitle(queueItem.Title); + var parsedEpisodeInfo = Parser.Parser.ParseTitle(queueItem.Title.Replace("ENCRYPTED / ", "")); if (parsedEpisodeInfo == null) continue; var remoteEpisode = _parsingService.Map(parsedEpisodeInfo, 0); @@ -133,6 +133,16 @@ public IEnumerable GetHistory(int start = 0, int limit = 0) return historyItems; } + public void RemoveFromQueue(string id) + { + _sabCommunicationProxy.RemoveFrom("queue", id); + } + + public void RemoveFromHistory(string id) + { + _sabCommunicationProxy.RemoveFrom("history", id); + } + public virtual SabCategoryModel GetCategories(string host = null, int port = 0, string apiKey = null, string username = null, string password = null) { //Get saved values if any of these are defaults diff --git a/src/NzbDrone.Core/Download/FailedDownloadService.cs b/src/NzbDrone.Core/Download/FailedDownloadService.cs index 60accd3bc..f83eb9ccc 100644 --- a/src/NzbDrone.Core/Download/FailedDownloadService.cs +++ b/src/NzbDrone.Core/Download/FailedDownloadService.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using System.Linq; using NLog; +using NzbDrone.Core.Configuration; using NzbDrone.Core.History; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; @@ -12,44 +14,53 @@ public class FailedDownloadService : IExecute private readonly IProvideDownloadClient _downloadClientProvider; private readonly IHistoryService _historyService; private readonly IEventAggregator _eventAggregator; + private readonly IConfigService _configService; private readonly Logger _logger; + private readonly IDownloadClient _downloadClient; + private static string DOWNLOAD_CLIENT = "downloadClient"; private static string DOWNLOAD_CLIENT_ID = "downloadClientId"; public FailedDownloadService(IProvideDownloadClient downloadClientProvider, IHistoryService historyService, IEventAggregator eventAggregator, + IConfigService configService, Logger logger) { _downloadClientProvider = downloadClientProvider; _historyService = historyService; _eventAggregator = eventAggregator; + _configService = configService; _logger = logger; + + _downloadClient = _downloadClientProvider.GetDownloadClient(); } private void CheckForFailedDownloads() { - var downloadClient = _downloadClientProvider.GetDownloadClient(); - var downloadClientHistory = downloadClient.GetHistory(0, 20).ToList(); + var grabbedHistory = _historyService.Grabbed(); + var failedHistory = _historyService.Failed(); - var failedItems = downloadClientHistory.Where(h => h.Status == HistoryStatus.Failed).ToList(); + CheckQueue(grabbedHistory, failedHistory); + CheckHistory(grabbedHistory, failedHistory); + } + + private void CheckQueue(List grabbedHistory, List failedHistory) + { + var downloadClientQueue = _downloadClient.GetQueue().ToList(); + var failedItems = downloadClientQueue.Where(q => q.Title.StartsWith("ENCRYPTED / ")).ToList(); if (!failedItems.Any()) { - _logger.Trace("Yay! No failed downloads"); + _logger.Trace("Yay! No encrypted downloads"); return; } - var grabbedHistory = _historyService.Grabbed(); - var failedHistory = _historyService.Failed(); - foreach (var failedItem in failedItems) { var failedLocal = failedItem; - var historyItems = grabbedHistory.Where(h => h.Data.ContainsKey(DOWNLOAD_CLIENT) && - h.Data[DOWNLOAD_CLIENT_ID].Equals(failedLocal.Id)) - .ToList(); + var historyItems = GetHistoryItems(grabbedHistory, failedLocal.Id); if (!historyItems.Any()) { @@ -64,21 +75,77 @@ private void CheckForFailedDownloads() continue; } - var historyItem = historyItems.First(); + PublishDownloadFailedEvent(historyItems, "Encypted download detected"); - _eventAggregator.PublishEvent(new DownloadFailedEvent + if (_configService.RemoveFailedDownloads) { - SeriesId = historyItem.SeriesId, - EpisodeIds = historyItems.Select(h => h.EpisodeId).ToList(), - Quality = historyItem.Quality, - SourceTitle = historyItem.SourceTitle, - DownloadClient = historyItem.Data[DOWNLOAD_CLIENT], - DownloadClientId = historyItem.Data[DOWNLOAD_CLIENT_ID], - Message = failedItem.Message - }); + _logger.Info("Removing encrypted download from queue: {0}", failedItem.Title.Replace("ENCRYPTED / ", "")); + _downloadClient.RemoveFromQueue(failedItem.Id); + } } } + private void CheckHistory(List grabbedHistory, List failedHistory) + { + var downloadClientHistory = _downloadClient.GetHistory(0, 20).ToList(); + var failedItems = downloadClientHistory.Where(h => h.Status == HistoryStatus.Failed).ToList(); + + if (!failedItems.Any()) + { + _logger.Trace("Yay! No failed downloads"); + return; + } + + foreach (var failedItem in failedItems) + { + var failedLocal = failedItem; + var historyItems = GetHistoryItems(grabbedHistory, failedLocal.Id); + + if (!historyItems.Any()) + { + _logger.Trace("Unable to find matching history item"); + continue; + } + + if (failedHistory.Any(h => h.Data.ContainsKey(DOWNLOAD_CLIENT_ID) && + h.Data[DOWNLOAD_CLIENT_ID].Equals(failedLocal.Id))) + { + _logger.Trace("Already added to history as failed"); + continue; + } + + PublishDownloadFailedEvent(historyItems, failedItem.Message); + + if (_configService.RemoveFailedDownloads) + { + _logger.Info("Removing failed download from history: {0}", failedItem.Title); + _downloadClient.RemoveFromHistory(failedItem.Id); + } + } + } + + private List GetHistoryItems(List grabbedHistory, string downloadClientId) + { + return grabbedHistory.Where(h => h.Data.ContainsKey(DOWNLOAD_CLIENT) && + h.Data[DOWNLOAD_CLIENT_ID].Equals(downloadClientId)) + .ToList(); + } + + private void PublishDownloadFailedEvent(List historyItems, string message) + { + var historyItem = historyItems.First(); + _eventAggregator.PublishEvent(new DownloadFailedEvent + { + SeriesId = historyItem.SeriesId, + EpisodeIds = historyItems.Select(h => h.EpisodeId).ToList(), + Quality = historyItem.Quality, + SourceTitle = historyItem.SourceTitle, + DownloadClient = historyItem.Data[DOWNLOAD_CLIENT], + DownloadClientId = historyItem.Data[DOWNLOAD_CLIENT_ID], + Message = message + }); + } + public void Execute(FailedDownloadCommand message) { CheckForFailedDownloads(); diff --git a/src/NzbDrone.Core/Download/IDownloadClient.cs b/src/NzbDrone.Core/Download/IDownloadClient.cs index f330dbc86..42107372b 100644 --- a/src/NzbDrone.Core/Download/IDownloadClient.cs +++ b/src/NzbDrone.Core/Download/IDownloadClient.cs @@ -9,5 +9,7 @@ public interface IDownloadClient bool IsConfigured { get; } IEnumerable GetQueue(); IEnumerable GetHistory(int start = 0, int limit = 0); + void RemoveFromQueue(string id); + void RemoveFromHistory(string id); } } diff --git a/src/UI/Settings/MediaManagement/FileManagement/FileManagementViewTemplate.html b/src/UI/Settings/MediaManagement/FileManagement/FileManagementViewTemplate.html index e66c50607..2313306db 100644 --- a/src/UI/Settings/MediaManagement/FileManagement/FileManagementViewTemplate.html +++ b/src/UI/Settings/MediaManagement/FileManagement/FileManagementViewTemplate.html @@ -42,7 +42,22 @@
- + + +
+ + + + +
+
+ + +
+ Failed Download Handling + +
+
- +
- +
- - - +
-
+ \ No newline at end of file From 4578adf1c034d6522d71932391484e67b1cc93a9 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 28 Oct 2013 17:27:28 -0700 Subject: [PATCH 23/31] Added message to download failed history events Fixed: NZB info url on history details --- .../Details/HistoryDetailsViewTemplate.html | 37 ++++++++++++------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/src/UI/History/Details/HistoryDetailsViewTemplate.html b/src/UI/History/Details/HistoryDetailsViewTemplate.html index 2dd83ed5e..0dc280370 100644 --- a/src/UI/History/Details/HistoryDetailsViewTemplate.html +++ b/src/UI/History/Details/HistoryDetailsViewTemplate.html @@ -3,7 +3,9 @@

- Details + {{#if_eq eventType compare="grabbed"}}Grabbed{{/if_eq}} + {{#if_eq eventType compare="downloadFailed"}}Download Failed{{/if_eq}} + {{#if_eq eventType compare="downloadFolderImported"}}Episode Imported{{/if_eq}}

@@ -27,24 +29,33 @@ {{#if nzbInfoUrl}}
Info
-
{{infoUrl}}o
+
{{nzbInfoUrl}}
{{/if}} {{/with}} - {{else}} + {{/if_eq}} + {{#if_eq eventType compare="downloadFailed"}} +
+ {{#with data}} +
Message
+
{{message}}
+ {{/with}} +
+ {{/if_eq}} + {{#if_eq eventType compare="downloadFolderImported"}} {{#if data}} {{#with data}} -
- {{#if droppedPath}} -
Source:
-
{{droppedPath}}
- {{/if}} +
+ {{#if droppedPath}} +
Source:
+
{{droppedPath}}
+ {{/if}} - {{#if importedPath}} -
Imported To:
-
{{importedPath}}
- {{/if}} -
+ {{#if importedPath}} +
Imported To:
+
{{importedPath}}
+ {{/if}} +
{{/with}} {{else}} No details available From 7c6fad155ac013d76175b5daabde011ff9394b82 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 28 Oct 2013 17:41:35 -0700 Subject: [PATCH 24/31] Added option to disable blacklisting, both the queue check and the spec --- .../Configuration/ConfigService.cs | 7 +++ .../Configuration/IConfigService.cs | 1 + .../Specifications/BlacklistSpecification.cs | 13 ++++- .../Download/FailedDownloadService.cs | 6 ++ .../FileManagement/FileManagementView.js | 19 ++++++- .../FileManagementViewTemplate.html | 56 +++++++++++++------ .../MediaManagement/Naming/NamingView.js | 4 +- 7 files changed, 84 insertions(+), 22 deletions(-) diff --git a/src/NzbDrone.Core/Configuration/ConfigService.cs b/src/NzbDrone.Core/Configuration/ConfigService.cs index b51d92611..c480dd7cf 100644 --- a/src/NzbDrone.Core/Configuration/ConfigService.cs +++ b/src/NzbDrone.Core/Configuration/ConfigService.cs @@ -275,6 +275,13 @@ public Boolean RemoveFailedDownloads set { SetValue("RemoveFailedDownloads", value); } } + public Boolean EnableFailedDownloadHandling + { + get { return GetValueBoolean("EnableFailedDownloadHandling", true); } + + set { SetValue("EnableFailedDownloadHandling", value); } + } + public string DownloadClientWorkingFolders { get { return GetValue("DownloadClientWorkingFolders", "_UNPACK_|_FAILED_"); } diff --git a/src/NzbDrone.Core/Configuration/IConfigService.cs b/src/NzbDrone.Core/Configuration/IConfigService.cs index f4578d4c1..8df4fbaa2 100644 --- a/src/NzbDrone.Core/Configuration/IConfigService.cs +++ b/src/NzbDrone.Core/Configuration/IConfigService.cs @@ -41,6 +41,7 @@ public interface IConfigService String DownloadClientWorkingFolders { get; set; } Boolean AutoRedownloadFailed { get; set; } Boolean RemoveFailedDownloads { get; set; } + Boolean EnableFailedDownloadHandling { get; set; } void SaveValues(Dictionary configValues); } } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/BlacklistSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/BlacklistSpecification.cs index cea341a2e..7aa5c6b32 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/BlacklistSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/BlacklistSpecification.cs @@ -1,6 +1,7 @@ using System.Linq; using NLog; using NzbDrone.Core.Blacklisting; +using NzbDrone.Core.Configuration; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; @@ -9,11 +10,13 @@ namespace NzbDrone.Core.DecisionEngine.Specifications public class BlacklistSpecification : IDecisionEngineSpecification { private readonly IBlacklistService _blacklistService; + private readonly IConfigService _configService; private readonly Logger _logger; - public BlacklistSpecification(IBlacklistService blacklistService, Logger logger) + public BlacklistSpecification(IBlacklistService blacklistService, IConfigService configService, Logger logger) { _blacklistService = blacklistService; + _configService = configService; _logger = logger; } @@ -27,9 +30,15 @@ public string RejectionReason public virtual bool IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { + if (!_configService.EnableFailedDownloadHandling) + { + _logger.Trace("Failed Download Handling is not enabled"); + return true; + } + if (_blacklistService.Blacklisted(subject.Release.Title)) { - _logger.Trace("Release is blacklisted"); + _logger.Trace("{0} is blacklisted", subject.Release.Title); return false; } diff --git a/src/NzbDrone.Core/Download/FailedDownloadService.cs b/src/NzbDrone.Core/Download/FailedDownloadService.cs index f83eb9ccc..59cbb5bd2 100644 --- a/src/NzbDrone.Core/Download/FailedDownloadService.cs +++ b/src/NzbDrone.Core/Download/FailedDownloadService.cs @@ -39,6 +39,12 @@ public FailedDownloadService(IProvideDownloadClient downloadClientProvider, private void CheckForFailedDownloads() { + if (!_configService.EnableFailedDownloadHandling) + { + _logger.Trace("Failed Download Handling is not enabled"); + return; + } + var grabbedHistory = _historyService.Grabbed(); var failedHistory = _historyService.Failed(); diff --git a/src/UI/Settings/MediaManagement/FileManagement/FileManagementView.js b/src/UI/Settings/MediaManagement/FileManagement/FileManagementView.js index ff6a247b9..8cd2c120e 100644 --- a/src/UI/Settings/MediaManagement/FileManagement/FileManagementView.js +++ b/src/UI/Settings/MediaManagement/FileManagement/FileManagementView.js @@ -10,11 +10,28 @@ define( template: 'Settings/MediaManagement/FileManagement/FileManagementViewTemplate', ui: { - recyclingBin: '.x-path' + recyclingBin : '.x-path', + failedDownloadHandlingCheckbox: '.x-failed-download-handling', + failedDownloadOptions : '.x-failed-download-options' + }, + + events: { + 'change .x-failed-download-handling': '_setFailedDownloadOptionsVisibility' }, onShow: function () { this.ui.recyclingBin.autoComplete('/directories'); + }, + + _setFailedDownloadOptionsVisibility: function () { + var checked = this.ui.failedDownloadHandlingCheckbox.prop('checked'); + if (checked) { + this.ui.failedDownloadOptions.slideDown(); + } + + else { + this.ui.failedDownloadOptions.slideUp(); + } } }); diff --git a/src/UI/Settings/MediaManagement/FileManagement/FileManagementViewTemplate.html b/src/UI/Settings/MediaManagement/FileManagement/FileManagementViewTemplate.html index 2313306db..ae02dc6a5 100644 --- a/src/UI/Settings/MediaManagement/FileManagement/FileManagementViewTemplate.html +++ b/src/UI/Settings/MediaManagement/FileManagement/FileManagementViewTemplate.html @@ -57,11 +57,11 @@ Failed Download Handling
- +
-
- +
+
+ -
-
From 436644318b92e964008dc5144c7f2c4eb09eddce Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 31 Oct 2013 16:50:39 -0700 Subject: [PATCH 28/31] Added name + year lookups New: Support series lookup when year has been appended to the release name --- .../NzbDrone.Core.Test.csproj | 2 + .../ParsingServiceTests/GetSeriesFixture.cs | 51 +++++++++ .../ParserTests/SeriesTitleInfoFixture.cs | 64 +++++++++++ src/NzbDrone.Core/NzbDrone.Core.csproj | 1 + .../Parser/Model/ParsedEpisodeInfo.cs | 1 + .../Parser/Model/SeriesTitleInfo.cs | 14 +++ src/NzbDrone.Core/Parser/Parser.cs | 101 +++++++++++------- src/NzbDrone.Core/Parser/ParsingService.cs | 15 ++- src/NzbDrone.Core/Tv/SeriesRepository.cs | 7 ++ src/NzbDrone.Core/Tv/SeriesService.cs | 6 ++ 10 files changed, 220 insertions(+), 42 deletions(-) create mode 100644 src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/GetSeriesFixture.cs create mode 100644 src/NzbDrone.Core.Test/ParserTests/SeriesTitleInfoFixture.cs create mode 100644 src/NzbDrone.Core/Parser/Model/SeriesTitleInfo.cs diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 2cee27fdc..66bd95291 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -169,7 +169,9 @@ + + diff --git a/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/GetSeriesFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/GetSeriesFixture.cs new file mode 100644 index 000000000..bad109bf9 --- /dev/null +++ b/src/NzbDrone.Core.Test/ParserTests/ParsingServiceTests/GetSeriesFixture.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Moq; +using NUnit.Framework; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests +{ + [TestFixture] + public class GetSeriesFixture : CoreTest + { + [Test] + public void should_use_passed_in_title_when_it_cannot_be_parsed() + { + const string title = "30 Rock"; + + Subject.GetSeries(title); + + Mocker.GetMock() + .Verify(s => s.FindByTitle(title), Times.Once()); + } + + [Test] + public void should_use_parsed_series_title() + { + const string title = "30.Rock.S01E01.720p.hdtv"; + + Subject.GetSeries(title); + + Mocker.GetMock() + .Verify(s => s.FindByTitle(Parser.Parser.ParseTitle(title).SeriesTitle), Times.Once()); + } + + [Test] + public void should_fallback_to_title_without_year_and_year_when_title_lookup_fails() + { + const string title = "House.2004.S01E01.720p.hdtv"; + var parsedEpisodeInfo = Parser.Parser.ParseTitle(title); + + Subject.GetSeries(title); + + Mocker.GetMock() + .Verify(s => s.FindByTitle(parsedEpisodeInfo.SeriesTitleInfo.TitleWithoutYear, + parsedEpisodeInfo.SeriesTitleInfo.Year), Times.Once()); + } + } +} diff --git a/src/NzbDrone.Core.Test/ParserTests/SeriesTitleInfoFixture.cs b/src/NzbDrone.Core.Test/ParserTests/SeriesTitleInfoFixture.cs new file mode 100644 index 000000000..5f2e00b9c --- /dev/null +++ b/src/NzbDrone.Core.Test/ParserTests/SeriesTitleInfoFixture.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.ParserTests +{ + [TestFixture] + public class SeriesTitleInfoFixture : CoreTest + { + [Test] + public void should_have_year_zero_when_title_doesnt_have_a_year() + { + const string title = "House.S01E01.pilot.720p.hdtv"; + + var result = Parser.Parser.ParseTitle(title).SeriesTitleInfo; + + result.Year.Should().Be(0); + } + + [Test] + public void should_have_same_title_for_title_and_title_without_year_when_title_doesnt_have_a_year() + { + const string title = "House.S01E01.pilot.720p.hdtv"; + + var result = Parser.Parser.ParseTitle(title).SeriesTitleInfo; + + result.Title.Should().Be(result.TitleWithoutYear); + } + + [Test] + public void should_have_year_when_title_has_a_year() + { + const string title = "House.2004.S01E01.pilot.720p.hdtv"; + + var result = Parser.Parser.ParseTitle(title).SeriesTitleInfo; + + result.Year.Should().Be(2004); + } + + [Test] + public void should_have_year_in_title_when_title_has_a_year() + { + const string title = "House.2004.S01E01.pilot.720p.hdtv"; + + var result = Parser.Parser.ParseTitle(title).SeriesTitleInfo; + + result.Title.Should().Be("house2004"); + } + + [Test] + public void should_title_without_year_should_not_contain_year() + { + const string title = "House.2004.S01E01.pilot.720p.hdtv"; + + var result = Parser.Parser.ParseTitle(title).SeriesTitleInfo; + + result.TitleWithoutYear.Should().Be("house"); + } + } +} diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 7a7d5b516..9007ad74e 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -319,6 +319,7 @@ + diff --git a/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs b/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs index fe89f6dee..14fd53a80 100644 --- a/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs +++ b/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs @@ -7,6 +7,7 @@ namespace NzbDrone.Core.Parser.Model public class ParsedEpisodeInfo { public string SeriesTitle { get; set; } + public SeriesTitleInfo SeriesTitleInfo { get; set; } public QualityModel Quality { get; set; } public int SeasonNumber { get; set; } public int[] EpisodeNumbers { get; set; } diff --git a/src/NzbDrone.Core/Parser/Model/SeriesTitleInfo.cs b/src/NzbDrone.Core/Parser/Model/SeriesTitleInfo.cs new file mode 100644 index 000000000..5ced83c40 --- /dev/null +++ b/src/NzbDrone.Core/Parser/Model/SeriesTitleInfo.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Parser.Model +{ + public class SeriesTitleInfo + { + public string Title { get; set; } + public string TitleWithoutYear { get; set; } + public int Year { get; set; } + } +} diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index a58464c96..9122245ab 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -76,6 +76,9 @@ public static class Parser private static readonly Regex LanguageRegex = new Regex(@"(?:\W|_)(?ita|italian)|(?german\b)|(?flemish)|(?greek)|(?(?:\W|_)FR)(?:\W|_)", RegexOptions.IgnoreCase | RegexOptions.Compiled); + private static readonly Regex YearInTitleRegex = new Regex(@"^(?.+?)(?:\W|_)?(?<year>\d{4})", + RegexOptions.IgnoreCase | RegexOptions.Compiled); + public static ParsedEpisodeInfo ParsePath(string path) { var fileInfo = new FileInfo(path); @@ -139,6 +142,58 @@ public static ParsedEpisodeInfo ParseTitle(string title) return null; } + public static string ParseSeriesName(string title) + { + Logger.Trace("Parsing string '{0}'", title); + + var parseResult = ParseTitle(title); + + if (parseResult == null) + { + return CleanSeriesTitle(title); + } + + return parseResult.SeriesTitle; + } + + public static string CleanSeriesTitle(this string title) + { + long number = 0; + + //If Title only contains numbers return it as is. + if (Int64.TryParse(title, out number)) + return title; + + return NormalizeRegex.Replace(title, String.Empty).ToLower(); + } + + public static string CleanupEpisodeTitle(string title) + { + //this will remove (1),(2) from the end of multi part episodes. + return MultiPartCleanupRegex.Replace(title, string.Empty).Trim(); + } + + private static SeriesTitleInfo GetSeriesTitleInfo(string title) + { + var seriesTitleInfo = new SeriesTitleInfo(); + seriesTitleInfo.Title = title; + + var match = YearInTitleRegex.Match(title); + + if (!match.Success) + { + seriesTitleInfo.TitleWithoutYear = title; + } + + else + { + seriesTitleInfo.TitleWithoutYear = match.Groups["title"].Value; + seriesTitleInfo.Year = Convert.ToInt32(match.Groups["year"].Value); + } + + return seriesTitleInfo; + } + private static ParsedEpisodeInfo ParseMatchCollection(MatchCollection matchCollection) { var seriesName = matchCollection[0].Groups["title"].Value.Replace('.', ' '); @@ -168,10 +223,10 @@ private static ParsedEpisodeInfo ParseMatchCollection(MatchCollection matchColle return null; result = new ParsedEpisodeInfo - { - SeasonNumber = seasons.First(), - EpisodeNumbers = new int[0], - }; + { + SeasonNumber = seasons.First(), + EpisodeNumbers = new int[0], + }; foreach (Match matchGroup in matchCollection) { @@ -226,32 +281,19 @@ private static ParsedEpisodeInfo ParseMatchCollection(MatchCollection matchColle } result = new ParsedEpisodeInfo - { - AirDate = airDate.ToString(Episode.AIR_DATE_FORMAT), - }; + { + AirDate = airDate.ToString(Episode.AIR_DATE_FORMAT), + }; } result.SeriesTitle = CleanSeriesTitle(seriesName); + result.SeriesTitleInfo = GetSeriesTitleInfo(result.SeriesTitle); Logger.Trace("Episode Parsed. {0}", result); return result; } - public static string ParseSeriesName(string title) - { - Logger.Trace("Parsing string '{0}'", title); - - var parseResult = ParseTitle(title); - - if (parseResult == null) - { - return CleanSeriesTitle(title); - } - - return parseResult.SeriesTitle; - } - private static Language ParseLanguage(string title) { var lowerTitle = title.ToLower(); @@ -345,22 +387,5 @@ private static bool ValidateBeforeParsing(string title) return true; } - - public static string CleanSeriesTitle(this string title) - { - long number = 0; - - //If Title only contains numbers return it as is. - if (Int64.TryParse(title, out number)) - return title; - - return NormalizeRegex.Replace(title, String.Empty).ToLower(); - } - - public static string CleanupEpisodeTitle(string title) - { - //this will remove (1),(2) from the end of multi part episodes. - return MultiPartCleanupRegex.Replace(title, string.Empty).Trim(); - } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Parser/ParsingService.cs b/src/NzbDrone.Core/Parser/ParsingService.cs index 134e507e3..3cd3aa407 100644 --- a/src/NzbDrone.Core/Parser/ParsingService.cs +++ b/src/NzbDrone.Core/Parser/ParsingService.cs @@ -68,15 +68,22 @@ public LocalEpisode GetEpisodes(string filename, Series series, bool sceneSource public Series GetSeries(string title) { - var searchTitle = title; var parsedEpisodeInfo = Parser.ParseTitle(title); - if (parsedEpisodeInfo != null) + if (parsedEpisodeInfo == null) { - searchTitle = parsedEpisodeInfo.SeriesTitle; + return _seriesService.FindByTitle(title); } - return _seriesService.FindByTitle(searchTitle); + var series = _seriesService.FindByTitle(parsedEpisodeInfo.SeriesTitle); + + if (series == null) + { + series = _seriesService.FindByTitle(parsedEpisodeInfo.SeriesTitleInfo.TitleWithoutYear, + parsedEpisodeInfo.SeriesTitleInfo.Year); + } + + return series; } public RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int tvRageId, SearchCriteriaBase searchCriteria = null) diff --git a/src/NzbDrone.Core/Tv/SeriesRepository.cs b/src/NzbDrone.Core/Tv/SeriesRepository.cs index 0c7d0288e..dbdc1c191 100644 --- a/src/NzbDrone.Core/Tv/SeriesRepository.cs +++ b/src/NzbDrone.Core/Tv/SeriesRepository.cs @@ -10,6 +10,7 @@ public interface ISeriesRepository : IBasicRepository<Series> { bool SeriesPathExists(string path); Series FindByTitle(string cleanTitle); + Series FindByTitle(string cleanTitle, int year); Series FindByTvdbId(int tvdbId); Series FindByTvRageId(int tvRageId); void SetSeriesType(int seriesId, SeriesTypes seriesTypes); @@ -32,6 +33,12 @@ public Series FindByTitle(string cleanTitle) return Query.SingleOrDefault(s => s.CleanTitle.Equals(cleanTitle, StringComparison.InvariantCultureIgnoreCase)); } + public Series FindByTitle(string cleanTitle, int year) + { + return Query.SingleOrDefault(s => s.CleanTitle.Equals(cleanTitle, StringComparison.InvariantCultureIgnoreCase) && + s.Year == year); + } + public Series FindByTvdbId(int tvdbId) { return Query.SingleOrDefault(s => s.TvdbId.Equals(tvdbId)); diff --git a/src/NzbDrone.Core/Tv/SeriesService.cs b/src/NzbDrone.Core/Tv/SeriesService.cs index 7496a15e7..6f94faa84 100644 --- a/src/NzbDrone.Core/Tv/SeriesService.cs +++ b/src/NzbDrone.Core/Tv/SeriesService.cs @@ -19,6 +19,7 @@ public interface ISeriesService Series FindByTvdbId(int tvdbId); Series FindByTvRageId(int tvRageId); Series FindByTitle(string title); + Series FindByTitle(string title, int year); void SetSeriesType(int seriesId, SeriesTypes seriesTypes); void DeleteSeries(int seriesId, bool deleteFiles); List<Series> GetAllSeries(); @@ -100,6 +101,11 @@ public Series FindByTitle(string title) return _seriesRepository.FindByTitle(Parser.Parser.CleanSeriesTitle(title)); } + public Series FindByTitle(string title, int year) + { + return _seriesRepository.FindByTitle(title, year); + } + public void SetSeriesType(int seriesId, SeriesTypes seriesTypes) { _seriesRepository.SetSeriesType(seriesId, seriesTypes); From e7ac2247abd30403780bff573fc661d6aede9053 Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Thu, 31 Oct 2013 23:15:15 -0700 Subject: [PATCH 29/31] Log details New: Logs now have details available --- .../ParserTests/ParserFixture.cs | 3 +++ .../History/Table/ControlsColumnTemplate.html | 2 -- src/UI/History/Table/HistoryDetailsCell.js | 2 +- src/UI/Shared/Modal/Controller.js | 13 ++++++++--- src/UI/System/Logs/Files/LogFileLayout.js | 4 ++-- src/UI/System/Logs/Files/Row.js | 4 ++-- src/UI/System/Logs/Logs.less | 4 ++++ .../Logs/Table/Details/LogDetailsView.js | 11 ++++++++++ .../Table/Details/LogDetailsViewTemplate.html | 22 +++++++++++++++++++ src/UI/System/Logs/Table/LogRow.js | 19 ++++++++++++++++ src/UI/System/Logs/Table/LogsTableLayout.js | 5 +++-- src/UI/vent.js | 1 + 12 files changed, 78 insertions(+), 12 deletions(-) delete mode 100644 src/UI/History/Table/ControlsColumnTemplate.html create mode 100644 src/UI/System/Logs/Table/Details/LogDetailsView.js create mode 100644 src/UI/System/Logs/Table/Details/LogDetailsViewTemplate.html create mode 100644 src/UI/System/Logs/Table/LogRow.js diff --git a/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs index 987917d9c..5652f3b6c 100644 --- a/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/ParserFixture.cs @@ -22,6 +22,9 @@ public class ParserFixture : CoreTest * [TestCase("Desparate Housewives - S07E22 - 7x23 - And Lots of Security.. [HDTV-720p].mkv", "Desparate Housewives", 7, new[] { 22, 23 }, 2)] * [TestCase("S07E22 - 7x23 - And Lots of Security.. [HDTV-720p].mkv", "", 7, new[] { 22, 23 }, 2)] * (Game of Thrones s03 e - "Game of Thrones Season 3 Episode 10" + * The.Man.of.Steel.1994-05.33.hybrid.DreamGirl-Novus-HD + * Superman.-.The.Man.of.Steel.1994-06.34.hybrid.DreamGirl-Novus-HD + * Superman.-.The.Man.of.Steel.1994-05.33.hybrid.DreamGirl-Novus-HD */ [TestCase("Sonny.With.a.Chance.S02E15", "Sonny.With.a.Chance", 2, 15)] diff --git a/src/UI/History/Table/ControlsColumnTemplate.html b/src/UI/History/Table/ControlsColumnTemplate.html deleted file mode 100644 index 869b7b965..000000000 --- a/src/UI/History/Table/ControlsColumnTemplate.html +++ /dev/null @@ -1,2 +0,0 @@ -<i class="icon-remove x-remove" title="Remove"/> -<i class="icon-repeat x-redownload" title="Re-Download"/> diff --git a/src/UI/History/Table/HistoryDetailsCell.js b/src/UI/History/Table/HistoryDetailsCell.js index 5f29097c3..64c268508 100644 --- a/src/UI/History/Table/HistoryDetailsCell.js +++ b/src/UI/History/Table/HistoryDetailsCell.js @@ -21,7 +21,7 @@ define( }, _showDetails: function () { - vent.trigger(vent.Commands.ShowHistoryDetails, { history: this.model }); + vent.trigger(vent.Commands.ShowHistoryDetails, { model: this.model }); } }); }); diff --git a/src/UI/Shared/Modal/Controller.js b/src/UI/Shared/Modal/Controller.js index 465119a0f..349db691d 100644 --- a/src/UI/Shared/Modal/Controller.js +++ b/src/UI/Shared/Modal/Controller.js @@ -7,8 +7,9 @@ define( 'Series/Edit/EditSeriesView', 'Series/Delete/DeleteSeriesView', 'Episode/EpisodeDetailsLayout', - 'History/Details/HistoryDetailsView' - ], function (vent, AppLayout, Marionette, EditSeriesView, DeleteSeriesView, EpisodeDetailsLayout, HistoryDetailsView) { + 'History/Details/HistoryDetailsView', + 'System/Logs/Table/Details/LogDetailsView' + ], function (vent, AppLayout, Marionette, EditSeriesView, DeleteSeriesView, EpisodeDetailsLayout, HistoryDetailsView, LogDetailsView) { return Marionette.AppRouter.extend({ @@ -18,6 +19,7 @@ define( vent.on(vent.Commands.DeleteSeriesCommand, this._deleteSeries, this); vent.on(vent.Commands.ShowEpisodeDetails, this._showEpisode, this); vent.on(vent.Commands.ShowHistoryDetails, this._showHistory, this); + vent.on(vent.Commands.ShowLogDetails, this._showLogDetails, this); }, _closeModal: function () { @@ -40,7 +42,12 @@ define( }, _showHistory: function (options) { - var view = new HistoryDetailsView({ model: options.history }); + var view = new HistoryDetailsView({ model: options.model }); + AppLayout.modalRegion.show(view); + }, + + _showLogDetails: function (options) { + var view = new LogDetailsView({ model: options.model }); AppLayout.modalRegion.show(view); } }); diff --git a/src/UI/System/Logs/Files/LogFileLayout.js b/src/UI/System/Logs/Files/LogFileLayout.js index e4c272dfe..f8de17828 100644 --- a/src/UI/System/Logs/Files/LogFileLayout.js +++ b/src/UI/System/Logs/Files/LogFileLayout.js @@ -130,12 +130,12 @@ define( filename: filename }); - this.listenToOnce(contentsModel, 'sync', this._showContents); + this.listenToOnce(contentsModel, 'sync', this._showDetails); contentsModel.fetch({ dataType: 'text' }); }, - _showContents: function (model) { + _showDetails: function (model) { this.contents.show(new ContentsView({ model: model })); }, diff --git a/src/UI/System/Logs/Files/Row.js b/src/UI/System/Logs/Files/Row.js index 543893b00..926869008 100644 --- a/src/UI/System/Logs/Files/Row.js +++ b/src/UI/System/Logs/Files/Row.js @@ -9,10 +9,10 @@ define( className: 'log-file-row', events: { - 'click': '_showContents' + 'click': '_showDetails' }, - _showContents: function () { + _showDetails: function () { vent.trigger(vent.Commands.ShowLogFile, { model: this.model }); } }); diff --git a/src/UI/System/Logs/Logs.less b/src/UI/System/Logs/Logs.less index c61c3a135..f5deaf3c5 100644 --- a/src/UI/System/Logs/Logs.less +++ b/src/UI/System/Logs/Logs.less @@ -18,4 +18,8 @@ .log-file-row { .clickable; +} + +.log-row { + .clickable; } \ No newline at end of file diff --git a/src/UI/System/Logs/Table/Details/LogDetailsView.js b/src/UI/System/Logs/Table/Details/LogDetailsView.js new file mode 100644 index 000000000..eba74360e --- /dev/null +++ b/src/UI/System/Logs/Table/Details/LogDetailsView.js @@ -0,0 +1,11 @@ +'use strict'; +define( + [ + 'vent', + 'marionette' + ], function (vent, Marionette) { + + return Marionette.ItemView.extend({ + template: 'System/Logs/Table/Details/LogDetailsViewTemplate' + }); + }); diff --git a/src/UI/System/Logs/Table/Details/LogDetailsViewTemplate.html b/src/UI/System/Logs/Table/Details/LogDetailsViewTemplate.html new file mode 100644 index 000000000..5ad8f7594 --- /dev/null +++ b/src/UI/System/Logs/Table/Details/LogDetailsViewTemplate.html @@ -0,0 +1,22 @@ +<div class="log-details-modal"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + + <h3>Details</h3> + + </div> + <div class="modal-body"> + Message + <pre>{{message}}</pre> + + {{#if exception}} + <br/> + Exception + <pre>{{exception}}</pre> + {{/if}} + </div> + <div class="modal-footer"> + <button class="btn" data-dismiss="modal">close</button> + </div> +</div> + diff --git a/src/UI/System/Logs/Table/LogRow.js b/src/UI/System/Logs/Table/LogRow.js new file mode 100644 index 000000000..1507f6ff3 --- /dev/null +++ b/src/UI/System/Logs/Table/LogRow.js @@ -0,0 +1,19 @@ +'use strict'; +define( + [ + 'vent', + 'backgrid' + ], function (vent, Backgrid) { + + return Backgrid.Row.extend({ + className: 'log-row', + + events: { + 'click': '_showDetails' + }, + + _showDetails: function () { + vent.trigger(vent.Commands.ShowLogDetails, { model: this.model }); + } + }); + }); diff --git a/src/UI/System/Logs/Table/LogsTableLayout.js b/src/UI/System/Logs/Table/LogsTableLayout.js index 9288474c3..95a59851a 100644 --- a/src/UI/System/Logs/Table/LogsTableLayout.js +++ b/src/UI/System/Logs/Table/LogsTableLayout.js @@ -6,11 +6,12 @@ define( 'backgrid', 'System/Logs/Table/LogTimeCell', 'System/Logs/Table/LogLevelCell', + 'System/Logs/Table/LogRow', 'Shared/Grid/Pager', 'System/Logs/LogsCollection', 'Shared/Toolbar/ToolbarLayout', 'Shared/LoadingView' - ], function (vent, Marionette, Backgrid, LogTimeCell, LogLevelCell, GridPager, LogCollection, ToolbarLayout, LoadingView) { + ], function (vent, Marionette, Backgrid, LogTimeCell, LogLevelCell, LogRow, GridPager, LogCollection, ToolbarLayout, LoadingView) { return Marionette.Layout.extend({ template: 'System/Logs/Table/LogsTableLayoutTemplate', @@ -77,7 +78,7 @@ define( _showTable: function () { this.grid.show(new Backgrid.Grid({ - row : Backgrid.Row, + row : LogRow, columns : this.columns, collection: this.collection, className : 'table table-hover' diff --git a/src/UI/vent.js b/src/UI/vent.js index b8b7eff51..4170ac72b 100644 --- a/src/UI/vent.js +++ b/src/UI/vent.js @@ -20,6 +20,7 @@ define( CloseModalCommand : 'CloseModalCommand', ShowEpisodeDetails : 'ShowEpisodeDetails', ShowHistoryDetails : 'ShowHistoryDetails', + ShowLogDetails : 'ShowLogDetails', SaveSettings : 'saveSettings', ShowLogFile : 'showLogFile' }; From 77a5fd62d27f7334c7b672e1c81286d0939a12fa Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Sun, 3 Nov 2013 22:39:37 -0800 Subject: [PATCH 30/31] Better sample checks Fixed: Sample checking relies on runtime instead of file size (Windows) Fixed: Minimum file size for 1080p releases is now 140MB (lower will be considered samples) --- .../NotSampleSpecificationFixture.cs | 142 +++++++++--------- .../DownloadedEpisodesImportService.cs | 6 +- .../EpisodeImport/ImportApprovedEpisodes.cs | 2 +- .../Specifications/NotSampleSpecification.cs | 49 ++++-- 4 files changed, 114 insertions(+), 85 deletions(-) diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotSampleSpecificationFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotSampleSpecificationFixture.cs index 3699ffeb6..e6c6d0ad1 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotSampleSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotSampleSpecificationFixture.cs @@ -7,6 +7,7 @@ using NzbDrone.Core.MediaFiles.EpisodeImport.Specifications; using NzbDrone.Core.MediaFiles.MediaInfo; using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; using NzbDrone.Test.Common; @@ -36,106 +37,46 @@ public void Setup() { Path = @"C:\Test\30 Rock\30.rock.s01e01.avi", Episodes = episodes, - Series = _series + Series = _series, + Quality = new QualityModel(Quality.HDTV720p) }; } - private void WithDailySeries() - { - _series.SeriesType = SeriesTypes.Daily; - } - - private void WithSeasonZero() - { - _localEpisode.Episodes[0].SeasonNumber = 0; - } - - private void WithFileSize(long size) + private void GivenFileSize(long size) { _localEpisode.Size = size; } - private void WithLength(int minutes) + private void GivenRuntime(int seconds) { Mocker.GetMock<IVideoFileInfoReader>() .Setup(s => s.GetRunTime(It.IsAny<String>())) - .Returns(new TimeSpan(0, 0, minutes, 0)); + .Returns(new TimeSpan(0, 0, seconds)); } [Test] public void should_return_true_if_series_is_daily() { - WithDailySeries(); - + _series.SeriesType = SeriesTypes.Daily; Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue(); } [Test] public void should_return_true_if_season_zero() { - WithSeasonZero(); - + _localEpisode.Episodes[0].SeasonNumber = 0; Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue(); } [Test] - public void should_return_false_if_undersize_and_under_length() + public void should_return_true_for_existing_file() { - WithFileSize(10.Megabytes()); - WithLength(1); - - Subject.IsSatisfiedBy(_localEpisode).Should().BeFalse(); - } - - [Test] - public void should_return_true_if_undersize() - { - WithFileSize(10.Megabytes()); - WithLength(10); - + _localEpisode.ExistingFile = true; Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue(); } [Test] - public void should_return_true_if_under_length() - { - WithFileSize(100.Megabytes()); - WithLength(1); - - Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue(); - } - - [Test] - public void should_return_true_if_over_size_and_length() - { - WithFileSize(100.Megabytes()); - WithLength(10); - - Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue(); - } - - [Test] - public void should_not_check_lenght_if_file_is_large_enough() - { - WithFileSize(100.Megabytes()); - - Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue(); - - Mocker.GetMock<IVideoFileInfoReader>().Verify(c => c.GetRunTime(It.IsAny<string>()), Times.Never()); - } - - [Test] - public void should_log_error_if_run_time_is_0_and_under_sample_size() - { - WithFileSize(40.Megabytes()); - WithLength(0); - - Subject.IsSatisfiedBy(_localEpisode).Should().BeFalse(); - ExceptionVerification.ExpectedErrors(1); - } - - [Test] - public void should_skip_check_for_flv_file() + public void should_return_true_for_flv() { _localEpisode.Path = @"C:\Test\some.show.s01e01.flv"; @@ -143,5 +84,66 @@ public void should_skip_check_for_flv_file() Mocker.GetMock<IVideoFileInfoReader>().Verify(c => c.GetRunTime(It.IsAny<string>()), Times.Never()); } + + [Test] + public void should_not_run_runtime_check_on_linux() + { + LinuxOnly(); + GivenFileSize(1000.Megabytes()); + + Subject.IsSatisfiedBy(_localEpisode); + + Mocker.GetMock<IVideoFileInfoReader>().Verify(v => v.GetRunTime(It.IsAny<String>()), Times.Never()); + } + + [Test] + public void should_run_runtime_check_on_windows() + { + GivenRuntime(120); + GivenFileSize(1000.Megabytes()); + + Subject.IsSatisfiedBy(_localEpisode); + + Mocker.GetMock<IVideoFileInfoReader>().Verify(v => v.GetRunTime(It.IsAny<String>()), Times.Once()); + } + + [Test] + public void should_return_false_if_runtime_is_less_than_minimum() + { + GivenRuntime(60); + + Subject.IsSatisfiedBy(_localEpisode).Should().BeFalse(); + } + + [Test] + public void should_return_true_if_runtime_greater_than_than_minimum() + { + GivenRuntime(120); + + Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue(); + } + + [Test] + public void should_return_false_if_file_size_is_under_minimum() + { + LinuxOnly(); + + GivenRuntime(120); + GivenFileSize(20.Megabytes()); + + Subject.IsSatisfiedBy(_localEpisode).Should().BeFalse(); + } + + [Test] + public void should_return_false_if_file_size_is_under_minimum_for_larger_limits() + { + LinuxOnly(); + + GivenRuntime(120); + GivenFileSize(120.Megabytes()); + _localEpisode.Quality = new QualityModel(Quality.Bluray1080p); + + Subject.IsSatisfiedBy(_localEpisode).Should().BeFalse(); + } } } diff --git a/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs b/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs index 5f36b5910..4cb226005 100644 --- a/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs +++ b/src/NzbDrone.Core/MediaFiles/DownloadedEpisodesImportService.cs @@ -7,7 +7,6 @@ using NzbDrone.Core.Configuration; using NzbDrone.Core.MediaFiles.Commands; using NzbDrone.Core.MediaFiles.EpisodeImport; -using NzbDrone.Core.MediaFiles.EpisodeImport.Specifications; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Parser; using NzbDrone.Core.Tv; @@ -74,10 +73,7 @@ private void ProcessDownloadedEpisodesFolder() if (importedFiles.Any()) { - if (_diskProvider.GetFolderSize(subFolder) < NotSampleSpecification.SampleSizeLimit) - { - _diskProvider.DeleteFolder(subFolder, true); - } + _diskProvider.DeleteFolder(subFolder, true); } } catch (Exception e) diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs index 86d10675e..ce14d7054 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/ImportApprovedEpisodes.cs @@ -41,7 +41,7 @@ public List<ImportDecision> Import(List<ImportDecision> decisions, bool newDownl var qualifiedImports = GetQualifiedImports(decisions); var imported = new List<ImportDecision>(); - foreach (var importDecision in qualifiedImports) + foreach (var importDecision in qualifiedImports.OrderByDescending(e => e.LocalEpisode.Size)) { var localEpisode = importDecision.LocalEpisode; diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotSampleSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotSampleSpecification.cs index dba6cf400..ccf833ce7 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotSampleSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/NotSampleSpecification.cs @@ -1,8 +1,11 @@ using System; +using System.Collections.Generic; using System.IO; using NLog; +using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.MediaFiles.MediaInfo; using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications @@ -11,6 +14,7 @@ public class NotSampleSpecification : IImportDecisionEngineSpecification { private readonly IVideoFileInfoReader _videoFileInfoReader; private readonly Logger _logger; + private static List<Quality> _largeSampleSizeQualities = new List<Quality> { Quality.HDTV1080p, Quality.WEBDL1080p, Quality.Bluray1080p }; public NotSampleSpecification(IVideoFileInfoReader videoFileInfoReader, Logger logger) @@ -31,6 +35,12 @@ public static long SampleSizeLimit public bool IsSatisfiedBy(LocalEpisode localEpisode) { + if (localEpisode.ExistingFile) + { + _logger.Trace("Existing file, skipping sample check"); + return true; + } + if (localEpisode.Series.SeriesType == SeriesTypes.Daily) { _logger.Trace("Daily Series, skipping sample check"); @@ -43,28 +53,49 @@ public bool IsSatisfiedBy(LocalEpisode localEpisode) return true; } - if (Path.GetExtension(localEpisode.Path).Equals(".flv", StringComparison.InvariantCultureIgnoreCase)) + var extension = Path.GetExtension(localEpisode.Path); + + if (extension != null && extension.Equals(".flv", StringComparison.InvariantCultureIgnoreCase)) { - _logger.Trace("Skipping smaple check for .flv file"); + _logger.Trace("Skipping sample check for .flv file"); return true; } - if (localEpisode.Size > SampleSizeLimit) + if (OsInfo.IsWindows) { + var runTime = _videoFileInfoReader.GetRunTime(localEpisode.Path); + + if (runTime.TotalMinutes.Equals(0)) + { + _logger.Error("[{0}] has a runtime of 0, is it a valid video file?", localEpisode); + return false; + } + + if (runTime.TotalSeconds < 90) + { + _logger.Trace("[{0}] appears to be a sample. Size: {1} Runtime: {2}", localEpisode.Path, localEpisode.Size, runTime); + return false; + } + + _logger.Trace("Runtime is over 2 minutes, skipping file size check"); return true; } - var runTime = _videoFileInfoReader.GetRunTime(localEpisode.Path); + return CheckSize(localEpisode); + } - if (runTime.TotalMinutes.Equals(0)) + private bool CheckSize(LocalEpisode localEpisode) + { + if (_largeSampleSizeQualities.Contains(localEpisode.Quality.Quality)) { - _logger.Error("[{0}] has a runtime of 0, is it a valid video file?", localEpisode); - return false; + if (localEpisode.Size < SampleSizeLimit * 2) + { + return false; + } } - if (runTime.TotalMinutes < 3) + if (localEpisode.Size < SampleSizeLimit) { - _logger.Trace("[{0}] appears to be a sample. Size: {1} Runtime: {2}", localEpisode.Path, localEpisode.Size, runTime); return false; } From d111be17ad44799b6cb989b3a782c9f2291c89cc Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Sun, 3 Nov 2013 22:44:35 -0800 Subject: [PATCH 31/31] Fixed broken sample tests --- .../Specifications/NotSampleSpecificationFixture.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotSampleSpecificationFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotSampleSpecificationFixture.cs index e6c6d0ad1..62c610bbf 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotSampleSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/NotSampleSpecificationFixture.cs @@ -99,6 +99,8 @@ public void should_not_run_runtime_check_on_linux() [Test] public void should_run_runtime_check_on_windows() { + WindowsOnly(); + GivenRuntime(120); GivenFileSize(1000.Megabytes()); @@ -110,6 +112,7 @@ public void should_run_runtime_check_on_windows() [Test] public void should_return_false_if_runtime_is_less_than_minimum() { + WindowsOnly(); GivenRuntime(60); Subject.IsSatisfiedBy(_localEpisode).Should().BeFalse(); @@ -118,6 +121,7 @@ public void should_return_false_if_runtime_is_less_than_minimum() [Test] public void should_return_true_if_runtime_greater_than_than_minimum() { + WindowsOnly(); GivenRuntime(120); Subject.IsSatisfiedBy(_localEpisode).Should().BeTrue();