diff --git a/src/NzbDrone.Api/Config/DownloadClientConfigModule.cs b/src/NzbDrone.Api/Config/DownloadClientConfigModule.cs index 31e56685e..9517acda2 100644 --- a/src/NzbDrone.Api/Config/DownloadClientConfigModule.cs +++ b/src/NzbDrone.Api/Config/DownloadClientConfigModule.cs @@ -16,6 +16,15 @@ namespace NzbDrone.Api.Config .SetValidator(rootFolderValidator) .SetValidator(pathExistsValidator) .When(c => !String.IsNullOrWhiteSpace(c.DownloadedEpisodesFolder)); + + SharedValidator.RuleFor(c => c.BlacklistGracePeriod) + .InclusiveBetween(1, 24); + + SharedValidator.RuleFor(c => c.BlacklistRetryInterval) + .InclusiveBetween(5, 120); + + SharedValidator.RuleFor(c => c.BlacklistRetryLimit) + .InclusiveBetween(0, 10); } } } \ No newline at end of file diff --git a/src/NzbDrone.Api/Config/DownloadClientConfigResource.cs b/src/NzbDrone.Api/Config/DownloadClientConfigResource.cs index c6525577c..14a9eff74 100644 --- a/src/NzbDrone.Api/Config/DownloadClientConfigResource.cs +++ b/src/NzbDrone.Api/Config/DownloadClientConfigResource.cs @@ -12,5 +12,8 @@ namespace NzbDrone.Api.Config public Boolean AutoRedownloadFailed { get; set; } public Boolean RemoveFailedDownloads { get; set; } public Boolean EnableFailedDownloadHandling { get; set; } + public Int32 BlacklistGracePeriod { get; set; } + public Int32 BlacklistRetryInterval { get; set; } + public Int32 BlacklistRetryLimit { get; set; } } } diff --git a/src/NzbDrone.Api/Indexers/ReleaseResource.cs b/src/NzbDrone.Api/Indexers/ReleaseResource.cs index 03955b5f8..c99982d69 100644 --- a/src/NzbDrone.Api/Indexers/ReleaseResource.cs +++ b/src/NzbDrone.Api/Indexers/ReleaseResource.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using NzbDrone.Api.REST; using NzbDrone.Core.Parser; using NzbDrone.Core.Qualities; -using NzbDrone.Core.Tv; namespace NzbDrone.Api.Indexers { @@ -11,6 +10,7 @@ namespace NzbDrone.Api.Indexers { public QualityModel Quality { get; set; } public Int32 Age { get; set; } + public Double AgeHours { get; set; } public Int64 Size { get; set; } public String Indexer { get; set; } public String ReleaseGroup { get; set; } diff --git a/src/NzbDrone.Common.Test/CacheTests/CachedManagerFixture.cs b/src/NzbDrone.Common.Test/CacheTests/CachedManagerFixture.cs index 1f424d4a1..ec3f9de35 100644 --- a/src/NzbDrone.Common.Test/CacheTests/CachedManagerFixture.cs +++ b/src/NzbDrone.Common.Test/CacheTests/CachedManagerFixture.cs @@ -7,7 +7,7 @@ using NzbDrone.Test.Common; namespace NzbDrone.Common.Test.CacheTests { [TestFixture] - public class CachedManagerFixture : TestBase + public class CachedManagerFixture : TestBase { [Test] public void should_return_proper_type_of_cache() diff --git a/src/NzbDrone.Common/Cache/CacheManger.cs b/src/NzbDrone.Common/Cache/CacheManger.cs index 08062d177..e702b6b45 100644 --- a/src/NzbDrone.Common/Cache/CacheManger.cs +++ b/src/NzbDrone.Common/Cache/CacheManger.cs @@ -4,7 +4,7 @@ using NzbDrone.Common.EnsureThat; namespace NzbDrone.Common.Cache { - public interface ICacheManger + public interface ICacheManager { ICached GetCache(Type host, string name); ICached GetCache(Type host); @@ -12,11 +12,11 @@ namespace NzbDrone.Common.Cache ICollection Caches { get; } } - public class CacheManger : ICacheManger + public class CacheManager : ICacheManager { private readonly ICached _cache; - public CacheManger() + public CacheManager() { _cache = new Cached(); diff --git a/src/NzbDrone.Common/DictionaryExtensions.cs b/src/NzbDrone.Common/DictionaryExtensions.cs index da0f4786f..22c6184ea 100644 --- a/src/NzbDrone.Common/DictionaryExtensions.cs +++ b/src/NzbDrone.Common/DictionaryExtensions.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; namespace NzbDrone.Common { @@ -12,5 +11,17 @@ namespace NzbDrone.Common TValue value; return dictionary.TryGetValue(key, out value) ? value : defaultValue; } + + public static Dictionary Merge(this Dictionary first, Dictionary second) + { + if (first == null) throw new ArgumentNullException("first"); + if (second == null) throw new ArgumentNullException("second"); + + var merged = new Dictionary(); + first.ToList().ForEach(kv => merged[kv.Key] = kv.Value); + second.ToList().ForEach(kv => merged[kv.Key] = kv.Value); + + return merged; + } } } diff --git a/src/NzbDrone.Core.Test/Blacklisting/BlacklistRepositoryFixture.cs b/src/NzbDrone.Core.Test/Blacklisting/BlacklistRepositoryFixture.cs index eb7b38c57..0edaa308c 100644 --- a/src/NzbDrone.Core.Test/Blacklisting/BlacklistRepositoryFixture.cs +++ b/src/NzbDrone.Core.Test/Blacklisting/BlacklistRepositoryFixture.cs @@ -48,7 +48,7 @@ namespace NzbDrone.Core.Test.Blacklisting { Subject.Insert(_blacklist); - Subject.Blacklisted(_blacklist.SeriesId, _blacklist.SourceTitle.ToUpperInvariant()).Should().BeTrue(); + Subject.Blacklisted(_blacklist.SeriesId, _blacklist.SourceTitle.ToUpperInvariant()).Should().HaveCount(1); } } } diff --git a/src/NzbDrone.Core.Test/Blacklisting/BlacklistServiceFixture.cs b/src/NzbDrone.Core.Test/Blacklisting/BlacklistServiceFixture.cs index 8e3bec1b9..e3cfd0654 100644 --- a/src/NzbDrone.Core.Test/Blacklisting/BlacklistServiceFixture.cs +++ b/src/NzbDrone.Core.Test/Blacklisting/BlacklistServiceFixture.cs @@ -1,11 +1,11 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; 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 { @@ -26,6 +26,8 @@ namespace NzbDrone.Core.Test.Blacklisting DownloadClient = "SabnzbdClient", DownloadClientId = "Sabnzbd_nzo_2dfh73k" }; + + _event.Data.Add("publishedDate", DateTime.UtcNow.ToString("s") + "Z"); } [Test] diff --git a/src/NzbDrone.Core.Test/Download/FailedDownloadServiceFixture.cs b/src/NzbDrone.Core.Test/Download/FailedDownloadServiceFixture.cs index fc8bf395f..dec70e91f 100644 --- a/src/NzbDrone.Core.Test/Download/FailedDownloadServiceFixture.cs +++ b/src/NzbDrone.Core.Test/Download/FailedDownloadServiceFixture.cs @@ -76,6 +76,16 @@ namespace NzbDrone.Core.Test.Download .Returns(_failed); } + private void GivenGracePeriod(int hours) + { + Mocker.GetMock().SetupGet(s => s.BlacklistGracePeriod).Returns(hours); + } + + private void GivenRetryLimit(int count) + { + Mocker.GetMock().SetupGet(s => s.BlacklistRetryLimit).Returns(count); + } + private void VerifyNoFailedDownloads() { Mocker.GetMock() @@ -270,5 +280,91 @@ namespace NzbDrone.Core.Test.Download VerifyNoFailedDownloads(); } + + [Test] + public void should_process_if_ageHours_is_not_set() + { + GivenFailedDownloadClientHistory(); + + var historyGrabbed = Builder.CreateListOfSize(1) + .Build() + .ToList(); + + historyGrabbed.First().Data.Add("downloadClient", "SabnzbdClient"); + historyGrabbed.First().Data.Add("downloadClientId", _failed.First().Id); + + GivenGrabbedHistory(historyGrabbed); + GivenNoFailedHistory(); + + Subject.Execute(new CheckForFailedDownloadCommand()); + + VerifyFailedDownloads(); + } + + [Test] + public void should_process_if_age_is_greater_than_grace_period() + { + GivenFailedDownloadClientHistory(); + + var historyGrabbed = Builder.CreateListOfSize(1) + .Build() + .ToList(); + + historyGrabbed.First().Data.Add("downloadClient", "SabnzbdClient"); + historyGrabbed.First().Data.Add("downloadClientId", _failed.First().Id); + historyGrabbed.First().Data.Add("ageHours", "48"); + + GivenGrabbedHistory(historyGrabbed); + GivenNoFailedHistory(); + + Subject.Execute(new CheckForFailedDownloadCommand()); + + VerifyFailedDownloads(); + } + + [Test] + public void should_process_if_retry_count_is_greater_than_grace_period() + { + GivenFailedDownloadClientHistory(); + + var historyGrabbed = Builder.CreateListOfSize(1) + .Build() + .ToList(); + + historyGrabbed.First().Data.Add("downloadClient", "SabnzbdClient"); + historyGrabbed.First().Data.Add("downloadClientId", _failed.First().Id); + historyGrabbed.First().Data.Add("ageHours", "48"); + + GivenGrabbedHistory(historyGrabbed); + GivenNoFailedHistory(); + GivenGracePeriod(6); + + Subject.Execute(new CheckForFailedDownloadCommand()); + + VerifyFailedDownloads(); + } + + [Test] + public void should_not_process_if_age_is_less_than_grace_period() + { + GivenFailedDownloadClientHistory(); + + var historyGrabbed = Builder.CreateListOfSize(1) + .Build() + .ToList(); + + historyGrabbed.First().Data.Add("downloadClient", "SabnzbdClient"); + historyGrabbed.First().Data.Add("downloadClientId", _failed.First().Id); + historyGrabbed.First().Data.Add("ageHours", "1"); + + GivenGrabbedHistory(historyGrabbed); + GivenNoFailedHistory(); + GivenGracePeriod(6); + GivenRetryLimit(1); + + Subject.Execute(new CheckForFailedDownloadCommand()); + + VerifyNoFailedDownloads(); + } } } diff --git a/src/NzbDrone.Core/Blacklisting/Blacklist.cs b/src/NzbDrone.Core/Blacklisting/Blacklist.cs index c68f55ff3..19dd800a6 100644 --- a/src/NzbDrone.Core/Blacklisting/Blacklist.cs +++ b/src/NzbDrone.Core/Blacklisting/Blacklist.cs @@ -2,16 +2,16 @@ using System.Collections.Generic; using NzbDrone.Core.Datastore; using NzbDrone.Core.Qualities; -using NzbDrone.Core.Tv; namespace NzbDrone.Core.Blacklisting { public class Blacklist : ModelBase { - public int SeriesId { get; set; } - public List EpisodeIds { get; set; } - public string SourceTitle { get; set; } + public Int32 SeriesId { get; set; } + public List EpisodeIds { get; set; } + public String SourceTitle { get; set; } public QualityModel Quality { get; set; } public DateTime Date { get; set; } + public DateTime? PublishedDate { get; set; } } } diff --git a/src/NzbDrone.Core/Blacklisting/BlacklistRepository.cs b/src/NzbDrone.Core/Blacklisting/BlacklistRepository.cs index 3fd39d627..4e105865f 100644 --- a/src/NzbDrone.Core/Blacklisting/BlacklistRepository.cs +++ b/src/NzbDrone.Core/Blacklisting/BlacklistRepository.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using System.Collections.Generic; using NzbDrone.Core.Datastore; using NzbDrone.Core.Messaging.Events; @@ -8,7 +6,7 @@ namespace NzbDrone.Core.Blacklisting { public interface IBlacklistRepository : IBasicRepository { - bool Blacklisted(int seriesId, string sourceTitle); + List Blacklisted(int seriesId, string sourceTitle); List BlacklistedBySeries(int seriesId); } @@ -19,11 +17,10 @@ namespace NzbDrone.Core.Blacklisting { } - public bool Blacklisted(int seriesId, string sourceTitle) + public List Blacklisted(int seriesId, string sourceTitle) { return Query.Where(e => e.SeriesId == seriesId) - .AndWhere(e => e.SourceTitle.Contains(sourceTitle)) - .Any(); + .AndWhere(e => e.SourceTitle.Contains(sourceTitle)); } public List BlacklistedBySeries(int seriesId) diff --git a/src/NzbDrone.Core/Blacklisting/BlacklistService.cs b/src/NzbDrone.Core/Blacklisting/BlacklistService.cs index 3c4d416e2..d51ad2d72 100644 --- a/src/NzbDrone.Core/Blacklisting/BlacklistService.cs +++ b/src/NzbDrone.Core/Blacklisting/BlacklistService.cs @@ -1,4 +1,8 @@ using System; +using System.Linq; +using NLog; +using NzbDrone.Common; +using NzbDrone.Core.Configuration; using NzbDrone.Core.Datastore; using NzbDrone.Core.Download; using NzbDrone.Core.Messaging.Commands; @@ -9,25 +13,31 @@ namespace NzbDrone.Core.Blacklisting { public interface IBlacklistService { - bool Blacklisted(int seriesId,string sourceTitle); + bool Blacklisted(int seriesId,string sourceTitle, DateTime publishedDate); PagingSpec Paged(PagingSpec pagingSpec); void Delete(int id); } - public class BlacklistService : IBlacklistService, IExecute, IHandle, IHandle + public class BlacklistService : IBlacklistService, + IExecute, + IHandle, + IHandle { private readonly IBlacklistRepository _blacklistRepository; private readonly IRedownloadFailedDownloads _redownloadFailedDownloadService; - public BlacklistService(IBlacklistRepository blacklistRepository, IRedownloadFailedDownloads redownloadFailedDownloadService) + public BlacklistService(IBlacklistRepository blacklistRepository, + IRedownloadFailedDownloads redownloadFailedDownloadService) { _blacklistRepository = blacklistRepository; _redownloadFailedDownloadService = redownloadFailedDownloadService; } - public bool Blacklisted(int seriesId, string sourceTitle) + public bool Blacklisted(int seriesId, string sourceTitle, DateTime publishedDate) { - return _blacklistRepository.Blacklisted(seriesId,sourceTitle); + var blacklisted = _blacklistRepository.Blacklisted(seriesId, sourceTitle); + + return blacklisted.Any(item => HasSamePublishedDate(item, publishedDate)); } public PagingSpec Paged(PagingSpec pagingSpec) @@ -40,6 +50,14 @@ namespace NzbDrone.Core.Blacklisting _blacklistRepository.Delete(id); } + private bool HasSamePublishedDate(Blacklist item, DateTime publishedDate) + { + if (!item.PublishedDate.HasValue) return true; + + return item.PublishedDate.Value.AddDays(-2) <= publishedDate && + item.PublishedDate.Value.AddDays(2) >= publishedDate; + } + public void Execute(ClearBlacklistCommand message) { _blacklistRepository.Purge(); @@ -53,7 +71,8 @@ namespace NzbDrone.Core.Blacklisting EpisodeIds = message.EpisodeIds, SourceTitle = message.SourceTitle, Quality = message.Quality, - Date = DateTime.UtcNow + Date = DateTime.UtcNow, + PublishedDate = DateTime.Parse(message.Data.GetValueOrDefault("publishedDate", null)) }; _blacklistRepository.Insert(blacklist); diff --git a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs index ed6d9fc36..ef9c5e4dd 100644 --- a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs +++ b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs @@ -46,9 +46,9 @@ namespace NzbDrone.Core.Configuration private readonly string _configFile; - public ConfigFileProvider(IAppFolderInfo appFolderInfo, ICacheManger cacheManger, IEventAggregator eventAggregator) + public ConfigFileProvider(IAppFolderInfo appFolderInfo, ICacheManager cacheManager, IEventAggregator eventAggregator) { - _cache = cacheManger.GetCache(GetType()); + _cache = cacheManager.GetCache(GetType()); _eventAggregator = eventAggregator; _configFile = appFolderInfo.GetConfigPath(); } diff --git a/src/NzbDrone.Core/Configuration/ConfigService.cs b/src/NzbDrone.Core/Configuration/ConfigService.cs index 883d463ad..3fc54f350 100644 --- a/src/NzbDrone.Core/Configuration/ConfigService.cs +++ b/src/NzbDrone.Core/Configuration/ConfigService.cs @@ -130,6 +130,27 @@ namespace NzbDrone.Core.Configuration set { SetValue("RemoveFailedDownloads", value); } } + public Int32 BlacklistGracePeriod + { + get { return GetValueInt("BlacklistGracePeriod", 2); } + + set { SetValue("BlacklistGracePeriod", value); } + } + + public Int32 BlacklistRetryInterval + { + get { return GetValueInt("BlacklistRetryInterval", 60); } + + set { SetValue("BlacklistRetryInterval", value); } + } + + public Int32 BlacklistRetryLimit + { + get { return GetValueInt("BlacklistRetryLimit", 1); } + + set { SetValue("BlacklistRetryLimit", value); } + } + public Boolean EnableFailedDownloadHandling { get { return GetValueBoolean("EnableFailedDownloadHandling", true); } diff --git a/src/NzbDrone.Core/Configuration/IConfigService.cs b/src/NzbDrone.Core/Configuration/IConfigService.cs index caaaea82a..a3cbef057 100644 --- a/src/NzbDrone.Core/Configuration/IConfigService.cs +++ b/src/NzbDrone.Core/Configuration/IConfigService.cs @@ -19,6 +19,10 @@ namespace NzbDrone.Core.Configuration Boolean AutoRedownloadFailed { get; set; } Boolean RemoveFailedDownloads { get; set; } Boolean EnableFailedDownloadHandling { get; set; } + Int32 BlacklistGracePeriod { get; set; } + Int32 BlacklistRetryInterval { get; set; } + Int32 BlacklistRetryLimit { get; set; } + //Media Management Boolean AutoUnmonitorPreviouslyDownloadedEpisodes { get; set; } diff --git a/src/NzbDrone.Core/DataAugmentation/Scene/SceneMappingService.cs b/src/NzbDrone.Core/DataAugmentation/Scene/SceneMappingService.cs index 91d18268b..bbe1e08a2 100644 --- a/src/NzbDrone.Core/DataAugmentation/Scene/SceneMappingService.cs +++ b/src/NzbDrone.Core/DataAugmentation/Scene/SceneMappingService.cs @@ -25,13 +25,13 @@ namespace NzbDrone.Core.DataAugmentation.Scene private readonly ICached _getSceneNameCache; private readonly ICached _gettvdbIdCache; - public SceneMappingService(ISceneMappingRepository repository, ISceneMappingProxy sceneMappingProxy, ICacheManger cacheManger, Logger logger) + public SceneMappingService(ISceneMappingRepository repository, ISceneMappingProxy sceneMappingProxy, ICacheManager cacheManager, Logger logger) { _repository = repository; _sceneMappingProxy = sceneMappingProxy; - _getSceneNameCache = cacheManger.GetCache(GetType(), "scene_name"); - _gettvdbIdCache = cacheManger.GetCache(GetType(), "tvdb_id"); + _getSceneNameCache = cacheManager.GetCache(GetType(), "scene_name"); + _gettvdbIdCache = cacheManager.GetCache(GetType(), "tvdb_id"); _logger = logger; } diff --git a/src/NzbDrone.Core/DataAugmentation/Xem/XemService.cs b/src/NzbDrone.Core/DataAugmentation/Xem/XemService.cs index 682129d77..57fa653af 100644 --- a/src/NzbDrone.Core/DataAugmentation/Xem/XemService.cs +++ b/src/NzbDrone.Core/DataAugmentation/Xem/XemService.cs @@ -18,14 +18,14 @@ namespace NzbDrone.Core.DataAugmentation.Xem public XemService(IEpisodeService episodeService, IXemProxy xemProxy, - ISeriesService seriesService, ICacheManger cacheManger, Logger logger) + ISeriesService seriesService, ICacheManager cacheManager, Logger logger) { _episodeService = episodeService; _xemProxy = xemProxy; _seriesService = seriesService; _logger = logger; _logger = logger; - _cache = cacheManger.GetCache(GetType()); + _cache = cacheManager.GetCache(GetType()); } private void PerformUpdate(Series series) diff --git a/src/NzbDrone.Core/Datastore/Migration/047_add_published_date_blacklist_column.cs b/src/NzbDrone.Core/Datastore/Migration/047_add_published_date_blacklist_column.cs new file mode 100644 index 000000000..a7bbc9b9b --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/047_add_published_date_blacklist_column.cs @@ -0,0 +1,14 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(47)] + public class add_temporary_blacklist_columns : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Alter.Table("Blacklist").AddColumn("PublishedDate").AsDateTime().Nullable(); + } + } +} diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/BlacklistSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/BlacklistSpecification.cs index 8d00b0d3b..6ca2b588a 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/BlacklistSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/BlacklistSpecification.cs @@ -27,7 +27,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications } } - public virtual bool IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) + public bool IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { if (!_configService.EnableFailedDownloadHandling) { @@ -35,7 +35,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications return true; } - if (_blacklistService.Blacklisted(subject.Series.Id, subject.Release.Title)) + if (_blacklistService.Blacklisted(subject.Series.Id, subject.Release.Title, subject.Release.PublishDate)) { _logger.Debug("{0} is blacklisted, rejecting.", subject.Release.Title); return false; diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RetrySpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RetrySpecification.cs new file mode 100644 index 000000000..c99d24c59 --- /dev/null +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RetrySpecification.cs @@ -0,0 +1,63 @@ +using System; +using System.Linq; +using NLog; +using NzbDrone.Common; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.History; +using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.DecisionEngine.Specifications +{ + public class RetrySpecification : IDecisionEngineSpecification + { + private readonly IHistoryService _historyService; + private readonly IConfigService _configService; + private readonly Logger _logger; + + public RetrySpecification(IHistoryService historyService, IConfigService configService, Logger logger) + { + _historyService = historyService; + _configService = configService; + _logger = logger; + } + + public string RejectionReason + { + get + { + return "Release has been retried too many times"; + } + } + + public bool IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) + { + if (!_configService.EnableFailedDownloadHandling) + { + _logger.Debug("Failed Download Handling is not enabled"); + return true; + } + + var history = _historyService.FindBySourceTitle(subject.Release.Title); + + if (history.Count(h => h.EventType == HistoryEventType.Grabbed && + HasSamePublishedDate(h, subject.Release.PublishDate)) > + _configService.BlacklistRetryLimit) + { + _logger.Debug("Release has been attempted more times than allowed, rejecting"); + return false; + } + + return true; + } + + private bool HasSamePublishedDate(History.History item, DateTime publishedDate) + { + DateTime itemsPublishedDate; + + if (!DateTime.TryParse(item.Data.GetValueOrDefault("PublishedDate", null), out itemsPublishedDate)) return true; + + return itemsPublishedDate.AddDays(-2) <= publishedDate && itemsPublishedDate.AddDays(2) >= publishedDate; + } + } +} diff --git a/src/NzbDrone.Core/Download/Clients/Blackhole/Blackhole.cs b/src/NzbDrone.Core/Download/Clients/Blackhole/Blackhole.cs index d04007166..949845b8f 100644 --- a/src/NzbDrone.Core/Download/Clients/Blackhole/Blackhole.cs +++ b/src/NzbDrone.Core/Download/Clients/Blackhole/Blackhole.cs @@ -58,6 +58,11 @@ namespace NzbDrone.Core.Download.Clients.Blackhole { } + public override void RetryDownload(string id) + { + throw new NotImplementedException(); + } + public override void Test() { PerformTest(Settings.Folder); diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs index 46d4cd715..d0cd625f1 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs @@ -138,6 +138,11 @@ namespace NzbDrone.Core.Download.Clients.Nzbget _proxy.RemoveFromHistory(id, Settings); } + public override void RetryDownload(string id) + { + _proxy.RetryDownload(id, Settings); + } + public override void Test() { _proxy.GetVersion(Settings); diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetProxy.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetProxy.cs index 91d4de26d..a4467f38f 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/NzbgetProxy.cs @@ -17,6 +17,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget List GetHistory(NzbgetSettings settings); VersionResponse GetVersion(NzbgetSettings settings); void RemoveFromHistory(string id, NzbgetSettings settings); + void RetryDownload(string id, NzbgetSettings settings); } public class NzbgetProxy : INzbgetProxy @@ -98,6 +99,23 @@ namespace NzbDrone.Core.Download.Clients.Nzbget } } + public void RetryDownload(string id, NzbgetSettings settings) + { + var history = GetHistory(settings); + var item = history.SingleOrDefault(h => h.Parameters.SingleOrDefault(p => p.Name == "drone") != null); + + if (item == null) + { + _logger.Warn("Unable to return item to queue, Unknown ID: {0}", id); + return; + } + + if (!EditQueue("HistoryReturn", 0, "", item.Id, settings)) + { + _logger.Warn("Failed to return item to queue from history, {0} [{1}]", item.Name, item.Id); + } + } + private bool EditQueue(string command, int offset, string editText, int id, NzbgetSettings settings) { var parameters = new object[] { command, offset, editText, id }; diff --git a/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs b/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs index 7b521d748..930a75ec0 100644 --- a/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs +++ b/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs @@ -80,6 +80,11 @@ namespace NzbDrone.Core.Download.Clients.Pneumatic { } + public override void RetryDownload(string id) + { + throw new NotImplementedException(); + } + public override void Test() { PerformTest(Settings.Folder); diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs index d157820f0..0242a9c5d 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs @@ -1,12 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; -using Newtonsoft.Json.Linq; using NLog; using NzbDrone.Common; using NzbDrone.Common.Cache; -using NzbDrone.Common.Serializer; -using NzbDrone.Core.Download.Clients.Sabnzbd.Responses; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; @@ -18,20 +15,20 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd { private readonly IHttpProvider _httpProvider; private readonly IParsingService _parsingService; - private readonly ISabnzbdProxy _sabnzbdProxy; + private readonly ISabnzbdProxy _proxy; private readonly ICached> _queueCache; private readonly Logger _logger; public Sabnzbd(IHttpProvider httpProvider, - ICacheManger cacheManger, + ICacheManager cacheManager, IParsingService parsingService, - ISabnzbdProxy sabnzbdProxy, + ISabnzbdProxy proxy, Logger logger) { _httpProvider = httpProvider; _parsingService = parsingService; - _sabnzbdProxy = sabnzbdProxy; - _queueCache = cacheManger.GetCache>(GetType(), "queue"); + _proxy = proxy; + _queueCache = cacheManager.GetCache>(GetType(), "queue"); _logger = logger; } @@ -45,7 +42,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd using (var nzb = _httpProvider.DownloadStream(url)) { _logger.Info("Adding report [{0}] to the queue.", title); - var response = _sabnzbdProxy.DownloadNzb(nzb, title, category, priority, Settings); + var response = _proxy.DownloadNzb(nzb, title, category, priority, Settings); if (response != null && response.Ids.Any()) { @@ -64,7 +61,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd try { - sabQueue = _sabnzbdProxy.GetQueue(0, 0, Settings); + sabQueue = _proxy.GetQueue(0, 0, Settings); } catch (DownloadClientException ex) { @@ -105,7 +102,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd try { - sabHistory = _sabnzbdProxy.GetHistory(start, limit, Settings); + sabHistory = _proxy.GetHistory(start, limit, Settings); } catch (DownloadClientException ex) { @@ -135,17 +132,22 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd public override void RemoveFromQueue(string id) { - _sabnzbdProxy.RemoveFrom("queue", id, Settings); + _proxy.RemoveFrom("queue", id, Settings); } public override void RemoveFromHistory(string id) { - _sabnzbdProxy.RemoveFrom("history", id, Settings); + _proxy.RemoveFrom("history", id, Settings); + } + + public override void RetryDownload(string id) + { + _proxy.RetryDownload(id, Settings); } public override void Test() { - _sabnzbdProxy.GetCategories(Settings); + _proxy.GetCategories(Settings); } public void Execute(TestSabnzbdCommand message) @@ -153,7 +155,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd var settings = new SabnzbdSettings(); settings.InjectFrom(message); - _sabnzbdProxy.GetCategories(settings); + _proxy.GetCategories(settings); } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdProxy.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdProxy.cs index 51f18cac5..1623b6eab 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdProxy.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdProxy.cs @@ -20,6 +20,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd SabnzbdCategoryResponse GetCategories(SabnzbdSettings settings); SabnzbdQueue GetQueue(int start, int limit, SabnzbdSettings settings); SabnzbdHistory GetHistory(int start, int limit, SabnzbdSettings settings); + void RetryDownload(string id, SabnzbdSettings settings); } public class SabnzbdProxy : ISabnzbdProxy @@ -111,6 +112,14 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd return Json.Deserialize(JObject.Parse(response).SelectToken("history").ToString()); } + public void RetryDownload(string id, SabnzbdSettings settings) + { + var request = new RestRequest(); + var action = String.Format("mode=retry&value={0}", id); + + ProcessRequest(request, action, settings); + } + private IRestClient BuildClient(string action, SabnzbdSettings settings) { var protocol = settings.UseSsl ? "https" : "http"; diff --git a/src/NzbDrone.Core/Download/DownloadClientBase.cs b/src/NzbDrone.Core/Download/DownloadClientBase.cs index b38131161..8cf5a0717 100644 --- a/src/NzbDrone.Core/Download/DownloadClientBase.cs +++ b/src/NzbDrone.Core/Download/DownloadClientBase.cs @@ -43,6 +43,7 @@ namespace NzbDrone.Core.Download public abstract IEnumerable GetHistory(int start = 0, int limit = 10); public abstract void RemoveFromQueue(string id); public abstract void RemoveFromHistory(string id); + public abstract void RetryDownload(string id); public abstract void Test(); } } diff --git a/src/NzbDrone.Core/Download/DownloadFailedEvent.cs b/src/NzbDrone.Core/Download/DownloadFailedEvent.cs index 6950081c2..527b77e41 100644 --- a/src/NzbDrone.Core/Download/DownloadFailedEvent.cs +++ b/src/NzbDrone.Core/Download/DownloadFailedEvent.cs @@ -2,12 +2,16 @@ using System.Collections.Generic; using NzbDrone.Common.Messaging; using NzbDrone.Core.Qualities; -using NzbDrone.Core.Tv; namespace NzbDrone.Core.Download { public class DownloadFailedEvent : IEvent { + public DownloadFailedEvent() + { + Data = new Dictionary(); + } + public Int32 SeriesId { get; set; } public List EpisodeIds { get; set; } public QualityModel Quality { get; set; } @@ -15,5 +19,6 @@ namespace NzbDrone.Core.Download public String DownloadClient { get; set; } public String DownloadClientId { get; set; } public String Message { get; set; } + public Dictionary Data { get; set; } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Download/FailedDownload.cs b/src/NzbDrone.Core/Download/FailedDownload.cs new file mode 100644 index 000000000..eead58f05 --- /dev/null +++ b/src/NzbDrone.Core/Download/FailedDownload.cs @@ -0,0 +1,11 @@ +using System; + +namespace NzbDrone.Core.Download +{ + public class FailedDownload + { + public HistoryItem DownloadClientHistoryItem { get; set; } + public DateTime LastRetry { get; set; } + public Int32 RetryCount { get; set; } + } +} diff --git a/src/NzbDrone.Core/Download/FailedDownloadService.cs b/src/NzbDrone.Core/Download/FailedDownloadService.cs index 2805cc1c6..468e144c0 100644 --- a/src/NzbDrone.Core/Download/FailedDownloadService.cs +++ b/src/NzbDrone.Core/Download/FailedDownloadService.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using NLog; using NzbDrone.Common; +using NzbDrone.Common.Cache; using NzbDrone.Core.Configuration; using NzbDrone.Core.History; using NzbDrone.Core.Messaging.Commands; @@ -23,6 +24,8 @@ namespace NzbDrone.Core.Download private readonly IConfigService _configService; private readonly Logger _logger; + private readonly ICached _failedDownloads; + private static string DOWNLOAD_CLIENT = "downloadClient"; private static string DOWNLOAD_CLIENT_ID = "downloadClientId"; @@ -30,6 +33,7 @@ namespace NzbDrone.Core.Download IHistoryService historyService, IEventAggregator eventAggregator, IConfigService configService, + ICacheManager cacheManager, Logger logger) { _downloadClientProvider = downloadClientProvider; @@ -37,6 +41,8 @@ namespace NzbDrone.Core.Download _eventAggregator = eventAggregator; _configService = configService; _logger = logger; + + _failedDownloads = cacheManager.GetCache(GetType(), "queue"); } public void MarkAsFailed(int historyId) @@ -127,6 +133,12 @@ namespace NzbDrone.Core.Download continue; } + if (FailedDownloadForRecentRelease(failedItem, historyItems)) + { + _logger.Debug("Recent release Failed, do not blacklist"); + continue; + } + if (failedHistory.Any(h => failedLocal.Id.Equals(h.Data.GetValueOrDefault(DOWNLOAD_CLIENT_ID)))) { _logger.Debug("Already added to history as failed"); @@ -152,19 +164,21 @@ namespace NzbDrone.Core.Download private void PublishDownloadFailedEvent(List historyItems, string message) { var historyItem = historyItems.First(); - string downloadClient; - string downloadClientId; - _eventAggregator.PublishEvent(new DownloadFailedEvent - { - SeriesId = historyItem.SeriesId, - EpisodeIds = historyItems.Select(h => h.EpisodeId).ToList(), - Quality = historyItem.Quality, - SourceTitle = historyItem.SourceTitle, - DownloadClient = historyItem.Data.GetValueOrDefault(DOWNLOAD_CLIENT), - DownloadClientId = historyItem.Data.GetValueOrDefault(DOWNLOAD_CLIENT_ID), - Message = message - }); + var downloadFailedEvent = new DownloadFailedEvent + { + SeriesId = historyItem.SeriesId, + EpisodeIds = historyItems.Select(h => h.EpisodeId).ToList(), + Quality = historyItem.Quality, + SourceTitle = historyItem.SourceTitle, + DownloadClient = historyItem.Data.GetValueOrDefault(DOWNLOAD_CLIENT), + DownloadClientId = historyItem.Data.GetValueOrDefault(DOWNLOAD_CLIENT_ID), + Message = message + }; + + downloadFailedEvent.Data = downloadFailedEvent.Data.Merge(historyItem.Data); + + _eventAggregator.PublishEvent(downloadFailedEvent); } private IDownloadClient GetDownloadClient() @@ -179,6 +193,56 @@ namespace NzbDrone.Core.Download return downloadClient; } + private bool FailedDownloadForRecentRelease(HistoryItem failedDownloadHistoryItem, List matchingHistoryItems) + { + double ageHours; + + if (!Double.TryParse(matchingHistoryItems.First().Data.GetValueOrDefault("ageHours"), out ageHours)) + { + _logger.Debug("Unable to determine age of failed download"); + return false; + } + + if (ageHours > _configService.BlacklistGracePeriod) + { + _logger.Debug("Failed download is older than the grace period"); + return false; + } + + var tracked = _failedDownloads.Get(failedDownloadHistoryItem.Id, () => new FailedDownload + { + DownloadClientHistoryItem = failedDownloadHistoryItem, + LastRetry = DateTime.UtcNow + } + ); + + if (tracked.RetryCount >= _configService.BlacklistRetryLimit) + { + _logger.Debug("Retry limit reached"); + return false; + } + + if (tracked.LastRetry.AddMinutes(_configService.BlacklistRetryInterval) < DateTime.UtcNow) + { + _logger.Debug("Retrying failed release"); + tracked.LastRetry = DateTime.UtcNow; + tracked.RetryCount++; + + try + { + GetDownloadClient().RetryDownload(failedDownloadHistoryItem.Id); + } + + catch (NotImplementedException ex) + { + _logger.Debug("Retrying failed downloads is not supported by your download client"); + return false; + } + } + + return true; + } + public void Execute(CheckForFailedDownloadCommand message) { if (!_configService.EnableFailedDownloadHandling) diff --git a/src/NzbDrone.Core/Download/IDownloadClient.cs b/src/NzbDrone.Core/Download/IDownloadClient.cs index b0e3e4734..d246e9645 100644 --- a/src/NzbDrone.Core/Download/IDownloadClient.cs +++ b/src/NzbDrone.Core/Download/IDownloadClient.cs @@ -11,6 +11,7 @@ namespace NzbDrone.Core.Download IEnumerable GetHistory(int start = 0, int limit = 0); void RemoveFromQueue(string id); void RemoveFromHistory(string id); + void RetryDownload(string id); void Test(); } } diff --git a/src/NzbDrone.Core/Download/RedownloadFailedDownloadService.cs b/src/NzbDrone.Core/Download/RedownloadFailedDownloadService.cs index b6e0c0de9..95914a5a7 100644 --- a/src/NzbDrone.Core/Download/RedownloadFailedDownloadService.cs +++ b/src/NzbDrone.Core/Download/RedownloadFailedDownloadService.cs @@ -40,10 +40,7 @@ namespace NzbDrone.Core.Download { _logger.Debug("Failed download only contains one episode, searching again"); - _commandExecutor.PublishCommandAsync(new EpisodeSearchCommand - { - EpisodeIds = episodeIds.ToList() - }); + _commandExecutor.PublishCommandAsync(new EpisodeSearchCommand(episodeIds)); return; } @@ -66,10 +63,7 @@ namespace NzbDrone.Core.Download _logger.Debug("Failed download contains multiple episodes, probably a double episode, searching again"); - _commandExecutor.PublishCommandAsync(new EpisodeSearchCommand - { - EpisodeIds = episodeIds.ToList() - }); + _commandExecutor.PublishCommandAsync(new EpisodeSearchCommand(episodeIds)); } } } diff --git a/src/NzbDrone.Core/History/HistoryRepository.cs b/src/NzbDrone.Core/History/HistoryRepository.cs index 3d0070cac..dead6df7c 100644 --- a/src/NzbDrone.Core/History/HistoryRepository.cs +++ b/src/NzbDrone.Core/History/HistoryRepository.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using Marr.Data.QGen; using NzbDrone.Core.Datastore; -using NzbDrone.Core.Datastore.Extentions; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; @@ -18,6 +17,7 @@ namespace NzbDrone.Core.History List Failed(); List Grabbed(); History MostRecentForEpisode(int episodeId); + List FindBySourceTitle(string sourceTitle); } public class HistoryRepository : BasicRepository, IHistoryRepository @@ -69,6 +69,16 @@ namespace NzbDrone.Core.History .FirstOrDefault(); } + public List FindBySourceTitle(string sourceTitle) + { + return Query.Where(h => h.SourceTitle.Contains(sourceTitle)); + } + + public List AllForEpisode(int episodeId) + { + return Query.Where(h => h.EpisodeId == episodeId); + } + protected override SortBuilder GetPagedQuery(QueryBuilder query, PagingSpec pagingSpec) { var baseQuery = query.Join(JoinType.Inner, h => h.Series, (h, s) => h.SeriesId == s.Id) diff --git a/src/NzbDrone.Core/History/HistoryService.cs b/src/NzbDrone.Core/History/HistoryService.cs index 41868977c..c95bc233a 100644 --- a/src/NzbDrone.Core/History/HistoryService.cs +++ b/src/NzbDrone.Core/History/HistoryService.cs @@ -23,6 +23,7 @@ namespace NzbDrone.Core.History List Grabbed(); History MostRecentForEpisode(int episodeId); History Get(int id); + List FindBySourceTitle(string sourceTitle); } public class HistoryService : IHistoryService, IHandle, IHandle, IHandle @@ -71,6 +72,11 @@ namespace NzbDrone.Core.History return _historyRepository.Get(id); } + public List FindBySourceTitle(string sourceTitle) + { + return _historyRepository.FindBySourceTitle(sourceTitle); + } + public void Purge() { _historyRepository.Purge(); @@ -107,6 +113,8 @@ namespace NzbDrone.Core.History history.Data.Add("NzbInfoUrl", message.Episode.Release.InfoUrl); history.Data.Add("ReleaseGroup", message.Episode.ParsedEpisodeInfo.ReleaseGroup); history.Data.Add("Age", message.Episode.Release.Age.ToString()); + history.Data.Add("AgeHours", message.Episode.Release.AgeHours.ToString()); + history.Data.Add("PublishedDate", message.Episode.Release.PublishDate.ToString("s") + "Z"); history.Data.Add("DownloadClient", message.DownloadClient); if (!String.IsNullOrWhiteSpace(message.DownloadClientId)) diff --git a/src/NzbDrone.Core/IndexerSearch/EpisodeSearchCommand.cs b/src/NzbDrone.Core/IndexerSearch/EpisodeSearchCommand.cs index 27861c869..67e275118 100644 --- a/src/NzbDrone.Core/IndexerSearch/EpisodeSearchCommand.cs +++ b/src/NzbDrone.Core/IndexerSearch/EpisodeSearchCommand.cs @@ -14,5 +14,14 @@ namespace NzbDrone.Core.IndexerSearch return true; } } + + public EpisodeSearchCommand() + { + } + + public EpisodeSearchCommand(List episodeIds) + { + EpisodeIds = episodeIds; + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/IndexerSearch/MissingEpisodeSearchCommand.cs b/src/NzbDrone.Core/IndexerSearch/MissingEpisodeSearchCommand.cs index f595f2a52..fffd02661 100644 --- a/src/NzbDrone.Core/IndexerSearch/MissingEpisodeSearchCommand.cs +++ b/src/NzbDrone.Core/IndexerSearch/MissingEpisodeSearchCommand.cs @@ -14,5 +14,14 @@ namespace NzbDrone.Core.IndexerSearch return true; } } + + public MissingEpisodeSearchCommand() + { + } + + public MissingEpisodeSearchCommand(List episodeIds) + { + EpisodeIds = episodeIds; + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Messaging/Commands/Tracking/CommandTrackingService.cs b/src/NzbDrone.Core/Messaging/Commands/Tracking/CommandTrackingService.cs index f4ea7ddd0..d7e649b14 100644 --- a/src/NzbDrone.Core/Messaging/Commands/Tracking/CommandTrackingService.cs +++ b/src/NzbDrone.Core/Messaging/Commands/Tracking/CommandTrackingService.cs @@ -21,9 +21,9 @@ namespace NzbDrone.Core.Messaging.Commands.Tracking { private readonly ICached _cache; - public CommandTrackingService(ICacheManger cacheManger) + public CommandTrackingService(ICacheManager cacheManager) { - _cache = cacheManger.GetCache(GetType()); + _cache = cacheManager.GetCache(GetType()); } public Command GetById(int id) diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index f42a89f96..5578d03f1 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -192,6 +192,7 @@ Code + @@ -209,6 +210,7 @@ + @@ -258,6 +260,7 @@ + diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index e2ebcdc31..d3caa5729 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -48,12 +48,12 @@ namespace NzbDrone.Core.Organizer public FileNameBuilder(INamingConfigService namingConfigService, IQualityDefinitionService qualityDefinitionService, - ICacheManger cacheManger, + ICacheManager cacheManager, Logger logger) { _namingConfigService = namingConfigService; _qualityDefinitionService = qualityDefinitionService; - _patternCache = cacheManger.GetCache(GetType()); + _patternCache = cacheManager.GetCache(GetType()); _logger = logger; } diff --git a/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs b/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs index 332c851e2..d2b6201c8 100644 --- a/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs +++ b/src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs @@ -13,7 +13,7 @@ namespace NzbDrone.Core.Parser.Model public DateTime PublishDate { get; set; } - public int Age + public Int32 Age { get { @@ -28,6 +28,21 @@ namespace NzbDrone.Core.Parser.Model } } + public Double AgeHours + { + get + { + return DateTime.UtcNow.Subtract(PublishDate).TotalHours; + } + + //This prevents manually downloading a release from blowing up in mono + //TODO: Is there a better way? + private set + { + + } + } + public int TvRageId { get; set; } public override string ToString() diff --git a/src/NzbDrone.Test.Common/TestBase.cs b/src/NzbDrone.Test.Common/TestBase.cs index 5809a8816..403b52fec 100644 --- a/src/NzbDrone.Test.Common/TestBase.cs +++ b/src/NzbDrone.Test.Common/TestBase.cs @@ -89,7 +89,7 @@ namespace NzbDrone.Test.Common GetType().IsPublic.Should().BeTrue("All Test fixtures should be public to work in mono."); - Mocker.SetConstant(new CacheManger()); + Mocker.SetConstant(new CacheManager()); Mocker.SetConstant(LogManager.GetLogger("TestLogger")); diff --git a/src/UI/Episode/Search/ManualLayout.js b/src/UI/Episode/Search/ManualLayout.js index 46dcff938..5e03caf88 100644 --- a/src/UI/Episode/Search/ManualLayout.js +++ b/src/UI/Episode/Search/ManualLayout.js @@ -6,8 +6,9 @@ define( 'Cells/FileSizeCell', 'Cells/QualityCell', 'Cells/ApprovalStatusCell', - 'Release/DownloadReportCell' - ], function (Marionette, Backgrid, FileSizeCell, QualityCell, ApprovalStatusCell, DownloadReportCell) { + 'Release/DownloadReportCell', + 'Release/AgeCell' + ], function (Marionette, Backgrid, FileSizeCell, QualityCell, ApprovalStatusCell, DownloadReportCell, AgeCell) { return Marionette.Layout.extend({ template: 'Episode/Search/ManualLayoutTemplate', @@ -22,7 +23,7 @@ define( name : 'age', label : 'Age', sortable: true, - cell : Backgrid.IntegerCell + cell : AgeCell }, { name : 'title', diff --git a/src/UI/Release/AgeCell.js b/src/UI/Release/AgeCell.js new file mode 100644 index 000000000..c879b6886 --- /dev/null +++ b/src/UI/Release/AgeCell.js @@ -0,0 +1,36 @@ +'use strict'; + +define( + [ + 'backgrid', + 'Shared/FormatHelpers' + ], function (Backgrid, FormatHelpers) { + return Backgrid.Cell.extend({ + + className: 'age-cell', + + render: function () { + var age = this.model.get('age'); + var ageHours = this.model.get('ageHours'); + + if (age === 0) { + this.$el.html('{0} {1}'.format(ageHours.toFixed(1), this.plural(Math.round(ageHours), 'hour'))); + } + + else { + this.$el.html('{0} {1}'.format(age, this.plural(age, 'day'))); + } + + this.delegateEvents(); + return this; + }, + + plural: function (input, unit) { + if (input === 1) { + return unit; + } + + return unit + 's'; + } + }); + }); diff --git a/src/UI/Settings/DownloadClient/FailedDownloadHandling/FailedDownloadHandlingViewTemplate.html b/src/UI/Settings/DownloadClient/FailedDownloadHandling/FailedDownloadHandlingViewTemplate.html index 90c7764e0..70f0a0dba 100644 --- a/src/UI/Settings/DownloadClient/FailedDownloadHandling/FailedDownloadHandlingViewTemplate.html +++ b/src/UI/Settings/DownloadClient/FailedDownloadHandling/FailedDownloadHandlingViewTemplate.html @@ -61,5 +61,41 @@ + +
+ + +
+ + + + + +
+
+ +
+ + +
+ + + + + +
+
+ +
+ + +
+ + + + + +
+
\ No newline at end of file