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 @@ using NzbDrone.Core.Test.Framework; 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 @@ namespace NzbDrone.Core.Test.DecisionEngineTests public void Setup() { Mocker.Resolve(); - _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 @@ namespace NzbDrone.Core.Test.DecisionEngineTests 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 @@ namespace NzbDrone.Core.Test.DecisionEngineTests } [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 @@ namespace NzbDrone.Core.Blacklisting 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 @@ namespace NzbDrone.Core.Blacklisting { 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 @@ namespace NzbDrone.Core.Datastore.Migration { 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; 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 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync 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 @@ namespace NzbDrone.Core.Download 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 @@ namespace NzbDrone.Core.Download 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 @@ namespace NzbDrone.Core.History 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 @@ namespace NzbDrone.Core.History 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 @@ namespace NzbDrone.Core.History 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 @@ namespace NzbDrone.Core.History 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 @@ namespace NzbDrone.Core.History 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, - }; - - history.Data.Add("DownloadClient", message.DownloadClient); - history.Data.Add("DownloadClientId", message.DownloadClientId); - - _historyRepository.Insert(history); + 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); + + _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 @@ -