From e21574a203a89809c0aeca2f2c15d75d91e63917 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Tue, 1 Apr 2014 13:07:41 -0700 Subject: [PATCH 01/25] Blacklisting improvements New: New releases that fail will be retried a second time after waiting 1hr (configurable) Fixed: Blacklisting releases with the same date and vastly different ages --- .../Config/DownloadClientConfigModule.cs | 9 ++ .../Config/DownloadClientConfigResource.cs | 3 + src/NzbDrone.Api/Indexers/ReleaseResource.cs | 2 +- .../CacheTests/CachedManagerFixture.cs | 2 +- src/NzbDrone.Common/Cache/CacheManger.cs | 6 +- src/NzbDrone.Common/DictionaryExtensions.cs | 13 ++- .../BlacklistRepositoryFixture.cs | 2 +- .../Blacklisting/BlacklistServiceFixture.cs | 6 +- .../Download/FailedDownloadServiceFixture.cs | 96 +++++++++++++++++++ src/NzbDrone.Core/Blacklisting/Blacklist.cs | 8 +- .../Blacklisting/BlacklistRepository.cs | 11 +-- .../Blacklisting/BlacklistService.cs | 31 ++++-- .../Configuration/ConfigFileProvider.cs | 4 +- .../Configuration/ConfigService.cs | 21 ++++ .../Configuration/IConfigService.cs | 4 + .../Scene/SceneMappingService.cs | 6 +- .../DataAugmentation/Xem/XemService.cs | 4 +- ...047_add_published_date_blacklist_column.cs | 14 +++ .../Specifications/BlacklistSpecification.cs | 4 +- .../Specifications/RetrySpecification.cs | 63 ++++++++++++ .../Download/Clients/Blackhole/Blackhole.cs | 5 + .../Download/Clients/Nzbget/Nzbget.cs | 5 + .../Download/Clients/Nzbget/NzbgetProxy.cs | 18 ++++ .../Download/Clients/Pneumatic/Pneumatic.cs | 5 + .../Download/Clients/Sabnzbd/Sabnzbd.cs | 32 ++++--- .../Download/Clients/Sabnzbd/SabnzbdProxy.cs | 9 ++ .../Download/DownloadClientBase.cs | 1 + .../Download/DownloadFailedEvent.cs | 7 +- src/NzbDrone.Core/Download/FailedDownload.cs | 11 +++ .../Download/FailedDownloadService.cs | 88 ++++++++++++++--- src/NzbDrone.Core/Download/IDownloadClient.cs | 1 + .../RedownloadFailedDownloadService.cs | 10 +- .../History/HistoryRepository.cs | 12 ++- src/NzbDrone.Core/History/HistoryService.cs | 8 ++ .../IndexerSearch/EpisodeSearchCommand.cs | 9 ++ .../MissingEpisodeSearchCommand.cs | 9 ++ .../Tracking/CommandTrackingService.cs | 4 +- src/NzbDrone.Core/NzbDrone.Core.csproj | 3 + .../Organizer/FileNameBuilder.cs | 4 +- src/NzbDrone.Core/Parser/Model/ReleaseInfo.cs | 17 +++- src/NzbDrone.Test.Common/TestBase.cs | 2 +- src/UI/Episode/Search/ManualLayout.js | 7 +- src/UI/Release/AgeCell.js | 36 +++++++ .../FailedDownloadHandlingViewTemplate.html | 36 +++++++ 44 files changed, 567 insertions(+), 81 deletions(-) create mode 100644 src/NzbDrone.Core/Datastore/Migration/047_add_published_date_blacklist_column.cs create mode 100644 src/NzbDrone.Core/DecisionEngine/Specifications/RetrySpecification.cs create mode 100644 src/NzbDrone.Core/Download/FailedDownload.cs create mode 100644 src/UI/Release/AgeCell.js 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 From 6133bc6db740e34c47234637f094bb5953918aec Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Tue, 8 Apr 2014 17:25:56 -0700 Subject: [PATCH 02/25] Missing episode search will skip already queued releases --- src/NzbDrone.Core/IndexerSearch/EpisodeSearchService.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/IndexerSearch/EpisodeSearchService.cs b/src/NzbDrone.Core/IndexerSearch/EpisodeSearchService.cs index 72a7948f7..c99622ed1 100644 --- a/src/NzbDrone.Core/IndexerSearch/EpisodeSearchService.cs +++ b/src/NzbDrone.Core/IndexerSearch/EpisodeSearchService.cs @@ -6,6 +6,7 @@ using NzbDrone.Core.Datastore; using NzbDrone.Core.Download; using NzbDrone.Core.Instrumentation.Extensions; using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Queue; using NzbDrone.Core.Tv; namespace NzbDrone.Core.IndexerSearch @@ -15,16 +16,19 @@ namespace NzbDrone.Core.IndexerSearch private readonly ISearchForNzb _nzbSearchService; private readonly IDownloadApprovedReports _downloadApprovedReports; private readonly IEpisodeService _episodeService; + private readonly IQueueService _queueService; private readonly Logger _logger; public MissingEpisodeSearchService(ISearchForNzb nzbSearchService, IDownloadApprovedReports downloadApprovedReports, IEpisodeService episodeService, + IQueueService queueService, Logger logger) { _nzbSearchService = nzbSearchService; _downloadApprovedReports = downloadApprovedReports; _episodeService = episodeService; + _queueService = queueService; _logger = logger; } @@ -53,13 +57,15 @@ namespace NzbDrone.Core.IndexerSearch FilterExpression = v => v.Monitored == true && v.Series.Monitored == true }).Records.ToList(); + var missing = episodes.Where(e => _queueService.GetQueue().Select(q => q.Episode.Id).Contains(e.Id)); + _logger.ProgressInfo("Performing missing search for {0} episodes", episodes.Count); var downloadedCount = 0; //Limit requests to indexers at 100 per minute using (var rateGate = new RateGate(100, TimeSpan.FromSeconds(60))) { - foreach (var episode in episodes) + foreach (var episode in missing) { rateGate.WaitToProceed(); var decisions = _nzbSearchService.EpisodeSearch(episode); From 3f4c1a16f8340420123d3a296ea4008208c3f5d5 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Wed, 9 Apr 2014 17:15:13 -0700 Subject: [PATCH 03/25] Health check results are stored in memory and updated as required --- src/NzbDrone.Api/Health/HealthModule.cs | 6 +- .../{HistoryResource.cs => HealthResource.cs} | 3 +- src/NzbDrone.Api/NzbDrone.Api.csproj | 2 +- .../Checks/DownloadClientCheckFixture.cs | 4 +- .../Checks/DroneFactoryCheckFixture.cs | 4 +- .../Checks/HealthCheckFixtureExtentions.cs | 9 ++- .../HealthCheck/Checks/IndexerCheckFixture.cs | 8 +-- .../Checks/MonoVersionCheckFixture.cs | 16 ++--- .../Download/FailedDownloadService.cs | 2 +- .../HealthCheck/Checks/DownloadClientCheck.cs | 10 +-- .../HealthCheck/Checks/DroneFactoryCheck.cs | 12 ++-- .../HealthCheck/Checks/IndexerCheck.cs | 10 +-- .../HealthCheck/Checks/MonoVersionCheck.cs | 26 +++++-- .../HealthCheck/Checks/UpdateCheck.cs | 18 +++-- src/NzbDrone.Core/HealthCheck/HealthCheck.cs | 15 +++- .../HealthCheck/HealthCheckBase.cs | 36 ++++++++++ ...ckEvent.cs => HealthCheckCompleteEvent.cs} | 2 +- .../HealthCheck/HealthCheckService.cs | 68 ++++++++++++++----- .../HealthCheck/IProvideHealthCheck.cs | 7 +- src/NzbDrone.Core/Jobs/TaskManager.cs | 2 +- src/NzbDrone.Core/NzbDrone.Core.csproj | 3 +- 21 files changed, 188 insertions(+), 75 deletions(-) rename src/NzbDrone.Api/Health/{HistoryResource.cs => HealthResource.cs} (78%) create mode 100644 src/NzbDrone.Core/HealthCheck/HealthCheckBase.cs rename src/NzbDrone.Core/HealthCheck/{TriggerHealthCheckEvent.cs => HealthCheckCompleteEvent.cs} (63%) diff --git a/src/NzbDrone.Api/Health/HealthModule.cs b/src/NzbDrone.Api/Health/HealthModule.cs index e658a9a0b..a876925d6 100644 --- a/src/NzbDrone.Api/Health/HealthModule.cs +++ b/src/NzbDrone.Api/Health/HealthModule.cs @@ -8,7 +8,7 @@ using NzbDrone.Core.Messaging.Events; namespace NzbDrone.Api.Health { public class HealthModule : NzbDroneRestModuleWithSignalR, - IHandle + IHandle { private readonly IHealthCheckService _healthCheckService; @@ -21,10 +21,10 @@ namespace NzbDrone.Api.Health private List GetHealth() { - return ToListResource(_healthCheckService.PerformHealthCheck); + return ToListResource(_healthCheckService.Results); } - public void Handle(TriggerHealthCheckEvent message) + public void Handle(HealthCheckCompleteEvent message) { BroadcastResourceChange(ModelAction.Sync); } diff --git a/src/NzbDrone.Api/Health/HistoryResource.cs b/src/NzbDrone.Api/Health/HealthResource.cs similarity index 78% rename from src/NzbDrone.Api/Health/HistoryResource.cs rename to src/NzbDrone.Api/Health/HealthResource.cs index ab490b449..a5bec7c06 100644 --- a/src/NzbDrone.Api/Health/HistoryResource.cs +++ b/src/NzbDrone.Api/Health/HealthResource.cs @@ -2,12 +2,11 @@ using NzbDrone.Api.REST; using NzbDrone.Core.HealthCheck; - namespace NzbDrone.Api.Health { public class HealthResource : RestResource { - public HealthCheckResultType Type { get; set; } + public HealthCheckResult Type { get; set; } public String Message { get; set; } } } diff --git a/src/NzbDrone.Api/NzbDrone.Api.csproj b/src/NzbDrone.Api/NzbDrone.Api.csproj index ba75c4bfa..b1e81be9c 100644 --- a/src/NzbDrone.Api/NzbDrone.Api.csproj +++ b/src/NzbDrone.Api/NzbDrone.Api.csproj @@ -136,7 +136,7 @@ - + diff --git a/src/NzbDrone.Core.Test/HealthCheck/Checks/DownloadClientCheckFixture.cs b/src/NzbDrone.Core.Test/HealthCheck/Checks/DownloadClientCheckFixture.cs index 6d0b613df..9b0982976 100644 --- a/src/NzbDrone.Core.Test/HealthCheck/Checks/DownloadClientCheckFixture.cs +++ b/src/NzbDrone.Core.Test/HealthCheck/Checks/DownloadClientCheckFixture.cs @@ -37,7 +37,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks } [Test] - public void should_return_null_when_download_client_returns() + public void should_return_ok_when_download_client_returns() { var downloadClient = Mocker.GetMock(); @@ -48,7 +48,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks .Setup(s => s.GetDownloadClient()) .Returns(downloadClient.Object); - Subject.Check().Should().BeNull(); + Subject.Check().ShouldBeOk(); } } } diff --git a/src/NzbDrone.Core.Test/HealthCheck/Checks/DroneFactoryCheckFixture.cs b/src/NzbDrone.Core.Test/HealthCheck/Checks/DroneFactoryCheckFixture.cs index 6fead9ed1..c3a9ef0c3 100644 --- a/src/NzbDrone.Core.Test/HealthCheck/Checks/DroneFactoryCheckFixture.cs +++ b/src/NzbDrone.Core.Test/HealthCheck/Checks/DroneFactoryCheckFixture.cs @@ -57,11 +57,11 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks } [Test] - public void should_return_null_when_no_issues_found() + public void should_return_ok_when_no_issues_found() { GivenDroneFactoryFolder(true); - Subject.Check().Should().BeNull(); + Subject.Check().ShouldBeOk(); } } } diff --git a/src/NzbDrone.Core.Test/HealthCheck/Checks/HealthCheckFixtureExtentions.cs b/src/NzbDrone.Core.Test/HealthCheck/Checks/HealthCheckFixtureExtentions.cs index 3b8b358c8..fa1577974 100644 --- a/src/NzbDrone.Core.Test/HealthCheck/Checks/HealthCheckFixtureExtentions.cs +++ b/src/NzbDrone.Core.Test/HealthCheck/Checks/HealthCheckFixtureExtentions.cs @@ -5,14 +5,19 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks { public static class HealthCheckFixtureExtensions { + public static void ShouldBeOk(this Core.HealthCheck.HealthCheck result) + { + result.Type.Should().Be(HealthCheckResult.Ok); + } + public static void ShouldBeWarning(this Core.HealthCheck.HealthCheck result) { - result.Type.Should().Be(HealthCheckResultType.Warning); + result.Type.Should().Be(HealthCheckResult.Warning); } public static void ShouldBeError(this Core.HealthCheck.HealthCheck result) { - result.Type.Should().Be(HealthCheckResultType.Error); + result.Type.Should().Be(HealthCheckResult.Error); } } } diff --git a/src/NzbDrone.Core.Test/HealthCheck/Checks/IndexerCheckFixture.cs b/src/NzbDrone.Core.Test/HealthCheck/Checks/IndexerCheckFixture.cs index 9411c92d6..574b8dd99 100644 --- a/src/NzbDrone.Core.Test/HealthCheck/Checks/IndexerCheckFixture.cs +++ b/src/NzbDrone.Core.Test/HealthCheck/Checks/IndexerCheckFixture.cs @@ -39,7 +39,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks } [Test] - public void should_return_null_when_multiple_indexers_are_enabled() + public void should_return_ok_when_multiple_indexers_are_enabled() { var indexer1 = Mocker.GetMock(); indexer1.SetupGet(s => s.SupportsSearching).Returns(true); @@ -51,11 +51,11 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks .Setup(s => s.GetAvailableProviders()) .Returns(new List { indexer1.Object, indexer2.Object }); - Subject.Check().Should().BeNull(); + Subject.Check().ShouldBeOk(); } [Test] - public void should_return_null_when_indexer_supports_searching() + public void should_return_ok_when_indexer_supports_searching() { var indexer1 = Mocker.GetMock(); indexer1.SetupGet(s => s.SupportsSearching).Returns(true); @@ -64,7 +64,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks .Setup(s => s.GetAvailableProviders()) .Returns(new List { indexer1.Object }); - Subject.Check().Should().BeNull(); + Subject.Check().ShouldBeOk(); } } } diff --git a/src/NzbDrone.Core.Test/HealthCheck/Checks/MonoVersionCheckFixture.cs b/src/NzbDrone.Core.Test/HealthCheck/Checks/MonoVersionCheckFixture.cs index 5f968a90b..55d5bc25e 100644 --- a/src/NzbDrone.Core.Test/HealthCheck/Checks/MonoVersionCheckFixture.cs +++ b/src/NzbDrone.Core.Test/HealthCheck/Checks/MonoVersionCheckFixture.cs @@ -48,35 +48,35 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks } [Test] - public void should_return_null_when_mono_3_2() + public void should_return_ok_when_mono_3_2() { GivenOutput("3.2.0.1"); - Subject.Check().Should().BeNull(); + Subject.Check().ShouldBeOk(); } [Test] - public void should_return_null_when_mono_4_0() + public void should_return_ok_when_mono_4_0() { GivenOutput("4.0.0.0"); - Subject.Check().Should().BeNull(); + Subject.Check().ShouldBeOk(); } [Test] - public void should_return_null_when_mono_3_2_7() + public void should_return_ok_when_mono_3_2_7() { GivenOutput("3.2.7"); - Subject.Check().Should().BeNull(); + Subject.Check().ShouldBeOk(); } [Test] - public void should_return_null_when_mono_3_2_1() + public void should_return_ok_when_mono_3_2_1() { GivenOutput("3.2.1"); - Subject.Check().Should().BeNull(); + Subject.Check().ShouldBeOk(); } } } diff --git a/src/NzbDrone.Core/Download/FailedDownloadService.cs b/src/NzbDrone.Core/Download/FailedDownloadService.cs index 468e144c0..e5be96880 100644 --- a/src/NzbDrone.Core/Download/FailedDownloadService.cs +++ b/src/NzbDrone.Core/Download/FailedDownloadService.cs @@ -42,7 +42,7 @@ namespace NzbDrone.Core.Download _configService = configService; _logger = logger; - _failedDownloads = cacheManager.GetCache(GetType(), "queue"); + _failedDownloads = cacheManager.GetCache(GetType()); } public void MarkAsFailed(int historyId) diff --git a/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientCheck.cs index d92673851..b45ee036f 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/DownloadClientCheck.cs @@ -3,7 +3,7 @@ using NzbDrone.Core.Download; namespace NzbDrone.Core.HealthCheck.Checks { - public class DownloadClientCheck : IProvideHealthCheck + public class DownloadClientCheck : HealthCheckBase { private readonly IProvideDownloadClient _downloadClientProvider; @@ -12,13 +12,13 @@ namespace NzbDrone.Core.HealthCheck.Checks _downloadClientProvider = downloadClientProvider; } - public HealthCheck Check() + public override HealthCheck Check() { var downloadClient = _downloadClientProvider.GetDownloadClient(); if (downloadClient == null) { - return new HealthCheck(HealthCheckResultType.Warning, "No download client is available"); + return new HealthCheck(GetType(), HealthCheckResult.Warning, "No download client is available"); } try @@ -27,10 +27,10 @@ namespace NzbDrone.Core.HealthCheck.Checks } catch (Exception) { - return new HealthCheck(HealthCheckResultType.Error, "Unable to communicate with download client"); + return new HealthCheck(GetType(), HealthCheckResult.Error, "Unable to communicate with download client"); } - return null; + return new HealthCheck(GetType()); } } } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/DroneFactoryCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/DroneFactoryCheck.cs index 3100b9f80..b26cf3404 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/DroneFactoryCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/DroneFactoryCheck.cs @@ -6,7 +6,7 @@ using NzbDrone.Core.Configuration; namespace NzbDrone.Core.HealthCheck.Checks { - public class DroneFactoryCheck : IProvideHealthCheck + public class DroneFactoryCheck : HealthCheckBase { private readonly IConfigService _configService; private readonly IDiskProvider _diskProvider; @@ -17,18 +17,18 @@ namespace NzbDrone.Core.HealthCheck.Checks _diskProvider = diskProvider; } - public HealthCheck Check() + public override HealthCheck Check() { var droneFactoryFolder = _configService.DownloadedEpisodesFolder; if (droneFactoryFolder.IsNullOrWhiteSpace()) { - return new HealthCheck(HealthCheckResultType.Warning, "Drone factory folder is not configured"); + return new HealthCheck(GetType(), HealthCheckResult.Warning, "Drone factory folder is not configured"); } if (!_diskProvider.FolderExists(droneFactoryFolder)) { - return new HealthCheck(HealthCheckResultType.Error, "Drone factory folder does not exist"); + return new HealthCheck(GetType(), HealthCheckResult.Error, "Drone factory folder does not exist"); } try @@ -39,12 +39,12 @@ namespace NzbDrone.Core.HealthCheck.Checks } catch (Exception) { - return new HealthCheck(HealthCheckResultType.Error, "Unable to write to drone factory folder"); + return new HealthCheck(GetType(), HealthCheckResult.Error, "Unable to write to drone factory folder"); } //Todo: Unable to import one or more files/folders from - return null; + return new HealthCheck(GetType()); } } } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/IndexerCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/IndexerCheck.cs index b4566bbf8..077307b99 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/IndexerCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/IndexerCheck.cs @@ -3,7 +3,7 @@ using NzbDrone.Core.Indexers; namespace NzbDrone.Core.HealthCheck.Checks { - public class IndexerCheck : IProvideHealthCheck + public class IndexerCheck : HealthCheckBase { private readonly IIndexerFactory _indexerFactory; @@ -12,21 +12,21 @@ namespace NzbDrone.Core.HealthCheck.Checks _indexerFactory = indexerFactory; } - public HealthCheck Check() + public override HealthCheck Check() { var enabled = _indexerFactory.GetAvailableProviders(); if (!enabled.Any()) { - return new HealthCheck(HealthCheckResultType.Error, "No indexers are enabled"); + return new HealthCheck(GetType(), HealthCheckResult.Error, "No indexers are enabled"); } if (enabled.All(i => i.SupportsSearching == false)) { - return new HealthCheck(HealthCheckResultType.Warning, "Enabled indexers do not support searching"); + return new HealthCheck(GetType(), HealthCheckResult.Warning, "Enabled indexers do not support searching"); } - return null; + return new HealthCheck(GetType()); } } } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/MonoVersionCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/MonoVersionCheck.cs index ed24bf018..733d5e61a 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/MonoVersionCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/MonoVersionCheck.cs @@ -6,7 +6,7 @@ using NzbDrone.Common.Processes; namespace NzbDrone.Core.HealthCheck.Checks { - public class MonoVersionCheck : IProvideHealthCheck + public class MonoVersionCheck : HealthCheckBase { private readonly IProcessProvider _processProvider; private readonly Logger _logger; @@ -18,11 +18,11 @@ namespace NzbDrone.Core.HealthCheck.Checks _logger = logger; } - public HealthCheck Check() + public override HealthCheck Check() { if (!OsInfo.IsMono) { - return null; + return new HealthCheck(GetType()); } var output = _processProvider.StartAndCapture("mono", "--version"); @@ -38,12 +38,28 @@ namespace NzbDrone.Core.HealthCheck.Checks if (version >= new Version(3, 2)) { _logger.Debug("mono version is 3.2 or better: {0}", version.ToString()); - return null; + return new HealthCheck(GetType()); } } } - return new HealthCheck(HealthCheckResultType.Warning, "mono version is less than 3.2, upgrade for improved stability"); + return new HealthCheck(GetType(), HealthCheckResult.Warning, "mono version is less than 3.2, upgrade for improved stability"); + } + + public override bool CheckOnConfigChange + { + get + { + return false; + } + } + + public override bool CheckOnSchedule + { + get + { + return false; + } } } } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/UpdateCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/UpdateCheck.cs index c79bbf3fa..21b76bcc0 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/UpdateCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/UpdateCheck.cs @@ -6,7 +6,7 @@ using NzbDrone.Core.Update; namespace NzbDrone.Core.HealthCheck.Checks { - public class UpdateCheck : IProvideHealthCheck + public class UpdateCheck : HealthCheckBase { private readonly IDiskProvider _diskProvider; private readonly IAppFolderInfo _appFolderInfo; @@ -20,7 +20,7 @@ namespace NzbDrone.Core.HealthCheck.Checks } - public HealthCheck Check() + public override HealthCheck Check() { if (OsInfo.IsWindows) { @@ -32,7 +32,7 @@ namespace NzbDrone.Core.HealthCheck.Checks } catch (Exception) { - return new HealthCheck(HealthCheckResultType.Error, + return new HealthCheck(GetType(), HealthCheckResult.Error, "Unable to update, running from write-protected folder"); } } @@ -41,11 +41,19 @@ namespace NzbDrone.Core.HealthCheck.Checks { if (_checkUpdateService.AvailableUpdate() != null) { - return new HealthCheck(HealthCheckResultType.Warning, "New update is available"); + return new HealthCheck(GetType(), HealthCheckResult.Warning, "New update is available"); } } - return null; + return new HealthCheck(GetType()); + } + + public override bool CheckOnConfigChange + { + get + { + return false; + } } } } diff --git a/src/NzbDrone.Core/HealthCheck/HealthCheck.cs b/src/NzbDrone.Core/HealthCheck/HealthCheck.cs index bc543a292..183849ecb 100644 --- a/src/NzbDrone.Core/HealthCheck/HealthCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/HealthCheck.cs @@ -5,18 +5,27 @@ namespace NzbDrone.Core.HealthCheck { public class HealthCheck : ModelBase { - public HealthCheckResultType Type { get; set; } + public Type Source { get; set; } + public HealthCheckResult Type { get; set; } public String Message { get; set; } - public HealthCheck(HealthCheckResultType type, string message) + public HealthCheck(Type source) { + Source = source; + Type = HealthCheckResult.Ok; + } + + public HealthCheck(Type source, HealthCheckResult type, string message) + { + Source = source; Type = type; Message = message; } } - public enum HealthCheckResultType + public enum HealthCheckResult { + Ok = 0, Warning = 1, Error = 2 } diff --git a/src/NzbDrone.Core/HealthCheck/HealthCheckBase.cs b/src/NzbDrone.Core/HealthCheck/HealthCheckBase.cs new file mode 100644 index 000000000..a36303420 --- /dev/null +++ b/src/NzbDrone.Core/HealthCheck/HealthCheckBase.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.HealthCheck +{ + public abstract class HealthCheckBase : IProvideHealthCheck + { + public abstract HealthCheck Check(); + + public virtual bool CheckOnStartup + { + get + { + return true; + } + } + + public virtual bool CheckOnConfigChange + { + get + { + return true; + } + } + + public virtual bool CheckOnSchedule + { + get + { + return true; + } + } + } +} diff --git a/src/NzbDrone.Core/HealthCheck/TriggerHealthCheckEvent.cs b/src/NzbDrone.Core/HealthCheck/HealthCheckCompleteEvent.cs similarity index 63% rename from src/NzbDrone.Core/HealthCheck/TriggerHealthCheckEvent.cs rename to src/NzbDrone.Core/HealthCheck/HealthCheckCompleteEvent.cs index d9b3e3838..0e44bc36d 100644 --- a/src/NzbDrone.Core/HealthCheck/TriggerHealthCheckEvent.cs +++ b/src/NzbDrone.Core/HealthCheck/HealthCheckCompleteEvent.cs @@ -2,7 +2,7 @@ namespace NzbDrone.Core.HealthCheck { - public class TriggerHealthCheckEvent : IEvent + public class HealthCheckCompleteEvent : IEvent { } } diff --git a/src/NzbDrone.Core/HealthCheck/HealthCheckService.cs b/src/NzbDrone.Core/HealthCheck/HealthCheckService.cs index 6ad4bccc4..5658aa682 100644 --- a/src/NzbDrone.Core/HealthCheck/HealthCheckService.cs +++ b/src/NzbDrone.Core/HealthCheck/HealthCheckService.cs @@ -1,9 +1,13 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; using NLog; +using NzbDrone.Common.Cache; using NzbDrone.Core.Configuration.Events; using NzbDrone.Core.Download; using NzbDrone.Core.Indexers; +using NzbDrone.Core.Lifecycle; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.ThingiProvider.Events; @@ -12,56 +16,86 @@ namespace NzbDrone.Core.HealthCheck { public interface IHealthCheckService { - List PerformHealthCheck(); + List Results(); } public class HealthCheckService : IHealthCheckService, IExecute, + IHandleAsync, IHandleAsync, IHandleAsync>, IHandleAsync> { private readonly IEnumerable _healthChecks; private readonly IEventAggregator _eventAggregator; + private readonly ICacheManager _cacheManager; private readonly Logger _logger; - public HealthCheckService(IEnumerable healthChecks, IEventAggregator eventAggregator, Logger logger) + private readonly ICached _healthCheckResults; + + public HealthCheckService(IEnumerable healthChecks, + IEventAggregator eventAggregator, + ICacheManager cacheManager, + Logger logger) { _healthChecks = healthChecks; _eventAggregator = eventAggregator; + _cacheManager = cacheManager; _logger = logger; + + _healthCheckResults = _cacheManager.GetCache(GetType()); } - public List PerformHealthCheck() + public List Results() { - _logger.Trace("Checking health"); - var result = _healthChecks.Select(c => c.Check()).Where(c => c != null).ToList(); - - return result; + return _healthCheckResults.Values.ToList(); } - public void Execute(CheckHealthCommand message) + private void PerformHealthCheck(Func predicate) { - //Until we have stored health checks we should just trigger the complete event - //and let the clients check in - //Multiple connected clients means we're going to compute the health check multiple times - //Multiple checks feels a bit ugly, but means the most up to date information goes to the client - _eventAggregator.PublishEvent(new TriggerHealthCheckEvent()); + var results = _healthChecks.Where(predicate) + .Select(c => c.Check()) + .ToList(); + + foreach (var result in results) + { + if (result.Type == HealthCheckResult.Ok) + { + _healthCheckResults.Remove(result.Source.Name); + } + + else + { + _healthCheckResults.Set(result.Source.Name, result); + } + } + + _eventAggregator.PublishEvent(new HealthCheckCompleteEvent()); } public void HandleAsync(ConfigSavedEvent message) { - _eventAggregator.PublishEvent(new TriggerHealthCheckEvent()); + PerformHealthCheck(c => c.CheckOnConfigChange); } public void HandleAsync(ProviderUpdatedEvent message) { - _eventAggregator.PublishEvent(new TriggerHealthCheckEvent()); + PerformHealthCheck(c => c.CheckOnConfigChange); } public void HandleAsync(ProviderUpdatedEvent message) { - _eventAggregator.PublishEvent(new TriggerHealthCheckEvent()); + PerformHealthCheck(c => c.CheckOnConfigChange); + } + + public void HandleAsync(ApplicationStartedEvent message) + { + PerformHealthCheck(c => c.CheckOnStartup); + } + + public void Execute(CheckHealthCommand message) + { + PerformHealthCheck(c => c.CheckOnSchedule); } } } diff --git a/src/NzbDrone.Core/HealthCheck/IProvideHealthCheck.cs b/src/NzbDrone.Core/HealthCheck/IProvideHealthCheck.cs index 0c7838fb3..17035b0d7 100644 --- a/src/NzbDrone.Core/HealthCheck/IProvideHealthCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/IProvideHealthCheck.cs @@ -1,7 +1,12 @@ -namespace NzbDrone.Core.HealthCheck +using System; + +namespace NzbDrone.Core.HealthCheck { public interface IProvideHealthCheck { HealthCheck Check(); + Boolean CheckOnStartup { get; } + Boolean CheckOnConfigChange { get; } + Boolean CheckOnSchedule { get; } } } diff --git a/src/NzbDrone.Core/Jobs/TaskManager.cs b/src/NzbDrone.Core/Jobs/TaskManager.cs index d4680655e..4c7daba7e 100644 --- a/src/NzbDrone.Core/Jobs/TaskManager.cs +++ b/src/NzbDrone.Core/Jobs/TaskManager.cs @@ -50,10 +50,10 @@ namespace NzbDrone.Core.Jobs { new ScheduledTask{ Interval = 1, TypeName = typeof(TrackedCommandCleanupCommand).FullName}, new ScheduledTask{ Interval = 1, TypeName = typeof(CheckForFailedDownloadCommand).FullName}, - new ScheduledTask{ Interval = 5, TypeName = typeof(CheckHealthCommand).FullName}, new ScheduledTask{ Interval = 1*60, TypeName = typeof(ApplicationUpdateCommand).FullName}, new ScheduledTask{ Interval = 1*60, TypeName = typeof(TrimLogCommand).FullName}, new ScheduledTask{ Interval = 3*60, TypeName = typeof(UpdateSceneMappingCommand).FullName}, + new ScheduledTask{ Interval = 6*60, TypeName = typeof(CheckHealthCommand).FullName}, new ScheduledTask{ Interval = 12*60, TypeName = typeof(RefreshSeriesCommand).FullName}, new ScheduledTask{ Interval = 24*60, TypeName = typeof(HousekeepingCommand).FullName}, diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 5578d03f1..9ec718804 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -280,7 +280,8 @@ - + + From 073b4961973a2f21323c7d0b7a2f35db4b40dad5 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Wed, 9 Apr 2014 21:37:52 -0700 Subject: [PATCH 04/25] Update series logging improvements --- src/NzbDrone.Core/Tv/SeriesService.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core/Tv/SeriesService.cs b/src/NzbDrone.Core/Tv/SeriesService.cs index 920ad701e..ed51da640 100644 --- a/src/NzbDrone.Core/Tv/SeriesService.cs +++ b/src/NzbDrone.Core/Tv/SeriesService.cs @@ -196,8 +196,10 @@ namespace NzbDrone.Core.Tv public List UpdateSeries(List series) { + _logger.Debug("Updating {0} series", series.Count); foreach (var s in series) { + _logger.Trace("Updating: {0}", s.Title); if (!s.RootFolderPath.IsNullOrWhiteSpace()) { var folderName = new DirectoryInfo(s.Path).Name; @@ -210,8 +212,7 @@ namespace NzbDrone.Core.Tv _logger.Trace("Not changing path for: {0}", s.Title); } } - - _logger.Debug("Updating {0} series", series.Count); + _seriesRepository.UpdateMany(series); _logger.Debug("{0} series updated", series.Count); From 946406f45676fbda221b69ae05cf5dda99b8444e Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 10 Apr 2014 08:56:18 -0700 Subject: [PATCH 05/25] Rescan all series via RescanSeriesCommand --- .../MediaFiles/Commands/RescanSeriesCommand.cs | 2 +- src/NzbDrone.Core/MediaFiles/DiskScanService.cs | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/NzbDrone.Core/MediaFiles/Commands/RescanSeriesCommand.cs b/src/NzbDrone.Core/MediaFiles/Commands/RescanSeriesCommand.cs index 4ff2450b0..4523b1deb 100644 --- a/src/NzbDrone.Core/MediaFiles/Commands/RescanSeriesCommand.cs +++ b/src/NzbDrone.Core/MediaFiles/Commands/RescanSeriesCommand.cs @@ -4,7 +4,7 @@ namespace NzbDrone.Core.MediaFiles.Commands { public class RescanSeriesCommand : Command { - public int SeriesId { get; set; } + public int? SeriesId { get; set; } public override bool SendUpdatesToClient { diff --git a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs index 65f986878..6e86e1077 100644 --- a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs +++ b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs @@ -111,9 +111,21 @@ namespace NzbDrone.Core.MediaFiles public void Execute(RescanSeriesCommand message) { - var series = _seriesService.GetSeries(message.SeriesId); + if (message.SeriesId.HasValue) + { + var series = _seriesService.GetSeries(message.SeriesId.Value); + Scan(series); + } - Scan(series); + else + { + var allSeries = _seriesService.GetAllSeries(); + + foreach (var series in allSeries) + { + Scan(series); + } + } } } } \ No newline at end of file From d8aae8f8ffcb2d995d0e28b54aa2cb54f5954a1d Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 10 Apr 2014 10:18:51 -0700 Subject: [PATCH 06/25] Fixed: Removed validation to ensure series path exists when updating a series --- src/NzbDrone.Api/Series/SeriesModule.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/NzbDrone.Api/Series/SeriesModule.cs b/src/NzbDrone.Api/Series/SeriesModule.cs index dc251fae2..cf80835ce 100644 --- a/src/NzbDrone.Api/Series/SeriesModule.cs +++ b/src/NzbDrone.Api/Series/SeriesModule.cs @@ -58,7 +58,6 @@ namespace NzbDrone.Api.Series .Cascade(CascadeMode.StopOnFirstFailure) .IsValidPath() .SetValidator(rootFolderValidator) - .SetValidator(pathExistsValidator) .SetValidator(seriesPathValidator) .SetValidator(droneFactoryValidator); From 64ea525f7932d7c0a25ca83094ad17acb4201079 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 10 Apr 2014 16:21:14 -0700 Subject: [PATCH 07/25] Added test to confirm Release Group: Cyphanix parses properly --- .../ParserTests/ReleaseGroupParserFixture.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs index e5b3ba71f..cc3c3e7ec 100644 --- a/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/ReleaseGroupParserFixture.cs @@ -1,8 +1,5 @@ -using System; -using System.Linq; using FluentAssertions; using NUnit.Framework; -using NzbDrone.Core.Parser; using NzbDrone.Core.Test.Framework; namespace NzbDrone.Core.Test.ParserTests @@ -20,6 +17,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("The Office - S01E01 - Pilot [HTDV-480p]", "DRONE")] [TestCase("The Office - S01E01 - Pilot [HTDV-720p]", "DRONE")] [TestCase("The Office - S01E01 - Pilot [HTDV-1080p]", "DRONE")] + [TestCase("The.Walking.Dead.S04E13.720p.WEB-DL.AAC2.0.H.264-Cyphanix", "Cyphanix")] public void should_parse_release_group(string title, string expected) { Parser.Parser.ParseReleaseGroup(title).Should().Be(expected); From 187724f74c2510b9f178125ea0e5f3a418244617 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 10 Apr 2014 17:25:46 -0700 Subject: [PATCH 08/25] Missing search now searches for episodes not in queue --- src/NzbDrone.Core/IndexerSearch/EpisodeSearchService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/IndexerSearch/EpisodeSearchService.cs b/src/NzbDrone.Core/IndexerSearch/EpisodeSearchService.cs index c99622ed1..0a33c001e 100644 --- a/src/NzbDrone.Core/IndexerSearch/EpisodeSearchService.cs +++ b/src/NzbDrone.Core/IndexerSearch/EpisodeSearchService.cs @@ -57,7 +57,7 @@ namespace NzbDrone.Core.IndexerSearch FilterExpression = v => v.Monitored == true && v.Series.Monitored == true }).Records.ToList(); - var missing = episodes.Where(e => _queueService.GetQueue().Select(q => q.Episode.Id).Contains(e.Id)); + var missing = episodes.Where(e => !_queueService.GetQueue().Select(q => q.Episode.Id).Contains(e.Id)); _logger.ProgressInfo("Performing missing search for {0} episodes", episodes.Count); var downloadedCount = 0; From fe3351e7ac0cafefdfc4a97b3ca5c0b1a4ba3b6c Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 10 Apr 2014 18:14:03 -0700 Subject: [PATCH 09/25] New: Optional disable RSS Sync (set interval to zero) --- src/NzbDrone.Api/Config/IndexerConfigModule.cs | 4 +++- .../Settings/Indexers/Options/IndexerOptionsViewTemplate.html | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Api/Config/IndexerConfigModule.cs b/src/NzbDrone.Api/Config/IndexerConfigModule.cs index 10df7f2ae..aa9c8d5bf 100644 --- a/src/NzbDrone.Api/Config/IndexerConfigModule.cs +++ b/src/NzbDrone.Api/Config/IndexerConfigModule.cs @@ -9,7 +9,9 @@ namespace NzbDrone.Api.Config public IndexerConfigModule(IConfigService configService) : base(configService) { - SharedValidator.RuleFor(c => c.RssSyncInterval).InclusiveBetween(10, 120); + SharedValidator.RuleFor(c => c.RssSyncInterval) + .InclusiveBetween(10, 120) + .When(c => c.RssSyncInterval > 0); } } } \ No newline at end of file diff --git a/src/UI/Settings/Indexers/Options/IndexerOptionsViewTemplate.html b/src/UI/Settings/Indexers/Options/IndexerOptionsViewTemplate.html index 3dd1338e1..074bd219c 100644 --- a/src/UI/Settings/Indexers/Options/IndexerOptionsViewTemplate.html +++ b/src/UI/Settings/Indexers/Options/IndexerOptionsViewTemplate.html @@ -13,10 +13,11 @@
- + +
From ceb06378ada466c3796177d6f1376101fe5edcaf Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 10 Apr 2014 18:32:19 -0700 Subject: [PATCH 10/25] Fixed: Daily series won't get treated as specials during sample checks --- .../MediaFiles/EpisodeImport/SampleServiceFixture.cs | 9 +++++++++ .../MediaFiles/EpisodeImport/SampleService.cs | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/SampleServiceFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/SampleServiceFixture.cs index 8e9bcc81a..17fa05811 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/SampleServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/SampleServiceFixture.cs @@ -123,6 +123,15 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport ShouldBeTrue(); } + [Test] + public void should_not_treat_daily_episode_a_special() + { + GivenRuntime(600); + _series.SeriesType = SeriesTypes.Daily; + _localEpisode.Episodes[0].SeasonNumber = 0; + ShouldBeFalse(); + } + private void ShouldBeTrue() { Subject.IsSample(_localEpisode.Series, diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/SampleService.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/SampleService.cs index cf59efdfb..12b3ed26c 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/SampleService.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/SampleService.cs @@ -36,7 +36,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport public bool IsSample(Series series, QualityModel quality, string path, long size, int seasonNumber) { - if (seasonNumber == 0) + if (seasonNumber == 0 && series.SeriesType == SeriesTypes.Standard) { _logger.Debug("Special, skipping sample check"); return false; From 000c172553a874d6a790c6237dd0b55d29c27218 Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Sat, 12 Apr 2014 17:00:08 +0200 Subject: [PATCH 11/25] Fixed: Moved main database cleanup to daily housekeeping to prevent windows service startup failure. --- src/NzbDrone.Core/Datastore/DbFactory.cs | 6 ------ src/NzbDrone.Core/Housekeeping/HousekeepingService.cs | 9 ++++++++- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/NzbDrone.Core/Datastore/DbFactory.cs b/src/NzbDrone.Core/Datastore/DbFactory.cs index 6d0e4a615..df7889d83 100644 --- a/src/NzbDrone.Core/Datastore/DbFactory.cs +++ b/src/NzbDrone.Core/Datastore/DbFactory.cs @@ -78,12 +78,6 @@ namespace NzbDrone.Core.Datastore return dataMapper; }); - - if (migrationType == MigrationType.Main) - { - db.Vacuum(); - } - return db; } } diff --git a/src/NzbDrone.Core/Housekeeping/HousekeepingService.cs b/src/NzbDrone.Core/Housekeeping/HousekeepingService.cs index 26725a2c9..b48e953e0 100644 --- a/src/NzbDrone.Core/Housekeeping/HousekeepingService.cs +++ b/src/NzbDrone.Core/Housekeeping/HousekeepingService.cs @@ -4,6 +4,7 @@ using NLog; using NzbDrone.Core.Lifecycle; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Housekeeping { @@ -11,11 +12,13 @@ namespace NzbDrone.Core.Housekeeping { private readonly IEnumerable _housekeepers; private readonly Logger _logger; + private readonly IDatabase _mainDb; - public HousekeepingService(IEnumerable housekeepers, Logger logger) + public HousekeepingService(IEnumerable housekeepers, Logger logger, IDatabase mainDb) { _housekeepers = housekeepers; _logger = logger; + _mainDb = mainDb; } private void Clean() @@ -33,6 +36,10 @@ namespace NzbDrone.Core.Housekeeping _logger.ErrorException("Error running housekeeping task: " + housekeeper.GetType().FullName, ex); } } + + // Vacuuming the log db isn't needed since that's done hourly at the TrimLogCommand. + _logger.Debug("Compressing main database after housekeeping"); + _mainDb.Vacuum(); } public void Execute(HousekeepingCommand message) From 0f75a9008a7c2329cd0798d3414b560e4ead5710 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 11 Apr 2014 23:04:01 -0700 Subject: [PATCH 12/25] New: Get series images via the API (3rd party app support) --- .../MediaCovers/MediaCoverModule.cs | 39 +++++++++++++++++++ src/NzbDrone.Api/NzbDrone.Api.csproj | 1 + 2 files changed, 40 insertions(+) create mode 100644 src/NzbDrone.Api/MediaCovers/MediaCoverModule.cs diff --git a/src/NzbDrone.Api/MediaCovers/MediaCoverModule.cs b/src/NzbDrone.Api/MediaCovers/MediaCoverModule.cs new file mode 100644 index 000000000..7b0cc4ea9 --- /dev/null +++ b/src/NzbDrone.Api/MediaCovers/MediaCoverModule.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Nancy; +using Nancy.Responses; +using NzbDrone.Common; +using NzbDrone.Common.Disk; +using NzbDrone.Common.EnvironmentInfo; + +namespace NzbDrone.Api.MediaCovers +{ + public class MediaCoverModule : NzbDroneApiModule + { + private const string MEDIA_COVER_ROUTE = @"/(?\d+)/(?(.+)\.(jpg|png|gif))"; + + private readonly IAppFolderInfo _appFolderInfo; + private readonly IDiskProvider _diskProvider; + + public MediaCoverModule(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider) : base("MediaCover") + { + _appFolderInfo = appFolderInfo; + _diskProvider = diskProvider; + + Get[MEDIA_COVER_ROUTE] = options => GetMediaCover(options.seriesId, options.filename); + } + + private Response GetMediaCover(int seriesId, string filename) + { + var filePath = Path.Combine(_appFolderInfo.GetAppDataPath(), "MediaCover", seriesId.ToString(), filename); + + if (!_diskProvider.FileExists(filePath)) + return new NotFoundResponse(); + + return new StreamResponse(() => File.OpenRead(filePath), MimeTypes.GetMimeType(filePath)); + } + } +} diff --git a/src/NzbDrone.Api/NzbDrone.Api.csproj b/src/NzbDrone.Api/NzbDrone.Api.csproj index b1e81be9c..e6d6a32c9 100644 --- a/src/NzbDrone.Api/NzbDrone.Api.csproj +++ b/src/NzbDrone.Api/NzbDrone.Api.csproj @@ -140,6 +140,7 @@ + From 192e79d2ff59f2b3dda88718209cb0dfaec4daab Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 12 Apr 2014 13:49:41 -0700 Subject: [PATCH 13/25] New: Automatic search for missing episodes if RSS Sync hasn't been run recently --- .../IndexerSearch/EpisodeSearchService.cs | 35 +++++++++++++++---- src/NzbDrone.Core/Indexers/RssSyncService.cs | 15 ++++++-- src/NzbDrone.Core/Jobs/Scheduler.cs | 2 +- .../Messaging/Commands/Command.cs | 1 + .../Messaging/Commands/CommandExecutor.cs | 7 ++++ .../Messaging/Commands/ICommandExecutor.cs | 3 ++ 6 files changed, 54 insertions(+), 9 deletions(-) diff --git a/src/NzbDrone.Core/IndexerSearch/EpisodeSearchService.cs b/src/NzbDrone.Core/IndexerSearch/EpisodeSearchService.cs index 0a33c001e..049677f78 100644 --- a/src/NzbDrone.Core/IndexerSearch/EpisodeSearchService.cs +++ b/src/NzbDrone.Core/IndexerSearch/EpisodeSearchService.cs @@ -11,7 +11,12 @@ using NzbDrone.Core.Tv; namespace NzbDrone.Core.IndexerSearch { - public class MissingEpisodeSearchService : IExecute, IExecute + public interface IEpisodeSearchService + { + void MissingEpisodesAiredAfter(DateTime dateTime); + } + + public class MissingEpisodeSearchService : IEpisodeSearchService, IExecute, IExecute { private readonly ISearchForNzb _nzbSearchService; private readonly IDownloadApprovedReports _downloadApprovedReports; @@ -32,6 +37,26 @@ namespace NzbDrone.Core.IndexerSearch _logger = logger; } + public void MissingEpisodesAiredAfter(DateTime dateTime) + { + var missing = _episodeService.EpisodesBetweenDates(dateTime, DateTime.UtcNow) + .Where(e => !e.HasFile && + !_queueService.GetQueue().Select(q => q.Episode.Id).Contains(e.Id)) + .ToList(); + + var downloadedCount = 0; + _logger.Info("Searching for {0} missing episodes since last RSS Sync", missing.Count); + + foreach (var episode in missing) + { + var decisions = _nzbSearchService.EpisodeSearch(episode); + var downloaded = _downloadApprovedReports.DownloadApproved(decisions); + downloadedCount += downloaded.Count; + } + + _logger.ProgressInfo("Completed search for {0} episodes. {1} reports downloaded.", missing.Count, downloadedCount); + } + public void Execute(EpisodeSearchCommand message) { foreach (var episodeId in message.EpisodeIds) @@ -57,9 +82,9 @@ namespace NzbDrone.Core.IndexerSearch FilterExpression = v => v.Monitored == true && v.Series.Monitored == true }).Records.ToList(); - var missing = episodes.Where(e => !_queueService.GetQueue().Select(q => q.Episode.Id).Contains(e.Id)); + var missing = episodes.Where(e => !_queueService.GetQueue().Select(q => q.Episode.Id).Contains(e.Id)).ToList(); - _logger.ProgressInfo("Performing missing search for {0} episodes", episodes.Count); + _logger.ProgressInfo("Performing missing search for {0} episodes", missing.Count); var downloadedCount = 0; //Limit requests to indexers at 100 per minute @@ -71,12 +96,10 @@ namespace NzbDrone.Core.IndexerSearch var decisions = _nzbSearchService.EpisodeSearch(episode); var downloaded = _downloadApprovedReports.DownloadApproved(decisions); downloadedCount += downloaded.Count; - - _logger.ProgressInfo("Episode search completed. {0} reports downloaded.", downloaded.Count); } } - _logger.ProgressInfo("Completed missing search for {0} episodes. {1} reports downloaded.", episodes.Count, downloadedCount); + _logger.ProgressInfo("Completed missing search for {0} episodes. {1} reports downloaded.", missing.Count, downloadedCount); } } } diff --git a/src/NzbDrone.Core/Indexers/RssSyncService.cs b/src/NzbDrone.Core/Indexers/RssSyncService.cs index 8958d8e76..07b26bfff 100644 --- a/src/NzbDrone.Core/Indexers/RssSyncService.cs +++ b/src/NzbDrone.Core/Indexers/RssSyncService.cs @@ -1,7 +1,9 @@ -using System.Linq; +using System; +using System.Linq; using NLog; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; +using NzbDrone.Core.IndexerSearch; using NzbDrone.Core.Instrumentation.Extensions; using NzbDrone.Core.Messaging.Commands; @@ -17,16 +19,19 @@ namespace NzbDrone.Core.Indexers private readonly IFetchAndParseRss _rssFetcherAndParser; private readonly IMakeDownloadDecision _downloadDecisionMaker; private readonly IDownloadApprovedReports _downloadApprovedReports; + private readonly IEpisodeSearchService _episodeSearchService; private readonly Logger _logger; public RssSyncService(IFetchAndParseRss rssFetcherAndParser, IMakeDownloadDecision downloadDecisionMaker, IDownloadApprovedReports downloadApprovedReports, + IEpisodeSearchService episodeSearchService, Logger logger) { _rssFetcherAndParser = rssFetcherAndParser; _downloadDecisionMaker = downloadDecisionMaker; _downloadApprovedReports = downloadApprovedReports; + _episodeSearchService = episodeSearchService; _logger = logger; } @@ -45,6 +50,12 @@ namespace NzbDrone.Core.Indexers public void Execute(RssSyncCommand message) { Sync(); + + if (message.LastExecutionTime.HasValue && DateTime.UtcNow.Subtract(message.LastExecutionTime.Value).TotalHours > 3) + { + _logger.Info("RSS Sync hasn't run since: {0}. Searching for any missing episodes since then.", message.LastExecutionTime.Value); + _episodeSearchService.MissingEpisodesAiredAfter(message.LastExecutionTime.Value.AddDays(-1)); + } } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/Jobs/Scheduler.cs b/src/NzbDrone.Core/Jobs/Scheduler.cs index 869b5b10a..42c1acef6 100644 --- a/src/NzbDrone.Core/Jobs/Scheduler.cs +++ b/src/NzbDrone.Core/Jobs/Scheduler.cs @@ -43,7 +43,7 @@ namespace NzbDrone.Core.Jobs try { - _commandExecutor.PublishCommand(task.TypeName); + _commandExecutor.PublishCommand(task.TypeName, task.LastExecution); } catch (Exception e) { diff --git a/src/NzbDrone.Core/Messaging/Commands/Command.cs b/src/NzbDrone.Core/Messaging/Commands/Command.cs index 516464381..9c8b64b30 100644 --- a/src/NzbDrone.Core/Messaging/Commands/Command.cs +++ b/src/NzbDrone.Core/Messaging/Commands/Command.cs @@ -35,6 +35,7 @@ namespace NzbDrone.Core.Messaging.Commands public string Message { get; private set; } public string Name { get; private set; } + public DateTime? LastExecutionTime { get; set; } protected Command() { diff --git a/src/NzbDrone.Core/Messaging/Commands/CommandExecutor.cs b/src/NzbDrone.Core/Messaging/Commands/CommandExecutor.cs index 7ccdad4d7..52534d5a5 100644 --- a/src/NzbDrone.Core/Messaging/Commands/CommandExecutor.cs +++ b/src/NzbDrone.Core/Messaging/Commands/CommandExecutor.cs @@ -49,8 +49,15 @@ namespace NzbDrone.Core.Messaging.Commands } public void PublishCommand(string commandTypeName) + { + PublishCommand(commandTypeName, null); + } + + public void PublishCommand(string commandTypeName, DateTime? lastExecutionTime) { dynamic command = GetCommand(commandTypeName); + command.LastExecutionTime = lastExecutionTime; + PublishCommand(command); } diff --git a/src/NzbDrone.Core/Messaging/Commands/ICommandExecutor.cs b/src/NzbDrone.Core/Messaging/Commands/ICommandExecutor.cs index 45d300fcd..d456f6511 100644 --- a/src/NzbDrone.Core/Messaging/Commands/ICommandExecutor.cs +++ b/src/NzbDrone.Core/Messaging/Commands/ICommandExecutor.cs @@ -1,9 +1,12 @@ +using System; + namespace NzbDrone.Core.Messaging.Commands { public interface ICommandExecutor { void PublishCommand(TCommand command) where TCommand : Command; void PublishCommand(string commandTypeName); + void PublishCommand(string commandTypeName, DateTime? lastEecutionTime); Command PublishCommandAsync(TCommand command) where TCommand : Command; Command PublishCommandAsync(string commandTypeName); } From fd531eda36cbc863dae42700a6cce30f73dcc96e Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 12 Apr 2014 13:58:19 -0700 Subject: [PATCH 14/25] Fixed broken test --- src/NzbDrone.Api/Commands/CommandResource.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/NzbDrone.Api/Commands/CommandResource.cs b/src/NzbDrone.Api/Commands/CommandResource.cs index 86c1f4b15..fc5492adf 100644 --- a/src/NzbDrone.Api/Commands/CommandResource.cs +++ b/src/NzbDrone.Api/Commands/CommandResource.cs @@ -12,5 +12,6 @@ namespace NzbDrone.Api.Commands public DateTime StateChangeTime { get; set; } public Boolean SendUpdatesToClient { get; set; } public CommandStatus State { get; set; } + public DateTime? LastExecutionTime { get; set; } } } \ No newline at end of file From 63022626f1f55ad35044ad0f82a9beb16d7130db Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 13 Apr 2014 13:25:47 -0700 Subject: [PATCH 15/25] Fixed: Next airing will only include monitored episodes --- src/NzbDrone.Core/SeriesStats/SeriesStatisticsRepository.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/SeriesStats/SeriesStatisticsRepository.cs b/src/NzbDrone.Core/SeriesStats/SeriesStatisticsRepository.cs index 09244dc06..6bb4c63d3 100644 --- a/src/NzbDrone.Core/SeriesStats/SeriesStatisticsRepository.cs +++ b/src/NzbDrone.Core/SeriesStats/SeriesStatisticsRepository.cs @@ -56,7 +56,7 @@ namespace NzbDrone.Core.SeriesStats SeriesId, SUM(CASE WHEN (Monitored = 1 AND AirdateUtc <= @currentDate) OR EpisodeFileId > 0 THEN 1 ELSE 0 END) AS EpisodeCount, SUM(CASE WHEN EpisodeFileId > 0 THEN 1 ELSE 0 END) AS EpisodeFileCount, - MIN(CASE WHEN AirDateUtc < @currentDate OR EpisodeFileId > 0 THEN NULL ELSE AirDateUtc END) AS NextAiringString + MIN(CASE WHEN AirDateUtc < @currentDate OR EpisodeFileId > 0 OR Monitored = 0 THEN NULL ELSE AirDateUtc END) AS NextAiringString FROM Episodes"; } From af758133470aa40dc13960c9f0d320285d75cedb Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 13 Apr 2014 13:59:49 -0700 Subject: [PATCH 16/25] Fixed: ctrl, alt and cmd won't trigger searching on add series --- src/UI/AddSeries/AddSeriesView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/UI/AddSeries/AddSeriesView.js b/src/UI/AddSeries/AddSeriesView.js index 951d3431b..7ac62beb4 100644 --- a/src/UI/AddSeries/AddSeriesView.js +++ b/src/UI/AddSeries/AddSeriesView.js @@ -57,7 +57,7 @@ define( this.$el.addClass(this.className); - this.ui.seriesSearch.keyup(function () { + this.ui.seriesSearch.keypress(function () { self.searchResult.close(); self._abortExistingSearch(); self.throttledSearch({ From 612ca492813f8cf90c8196cc29975cdedbd4a78f Mon Sep 17 00:00:00 2001 From: Cyberlane Date: Tue, 8 Apr 2014 23:44:23 +0100 Subject: [PATCH 17/25] New: Alternative titles on Series Details UI --- src/NzbDrone.Api/Series/SeriesModule.cs | 22 +++++++++ src/NzbDrone.Api/Series/SeriesResource.cs | 1 + .../DataAugmentation/Scene/SceneMapping.cs | 3 +- .../Scene/SceneMappingRepository.cs | 7 ++- .../Scene/SceneMappingService.cs | 16 +++++- .../048_add_title_to_scenemappings.cs | 18 +++++++ src/NzbDrone.Core/NzbDrone.Core.csproj | 1 + src/UI/Series/Details/InfoViewTemplate.html | 49 +++++++++++-------- 8 files changed, 93 insertions(+), 24 deletions(-) create mode 100644 src/NzbDrone.Core/Datastore/Migration/048_add_title_to_scenemappings.cs diff --git a/src/NzbDrone.Api/Series/SeriesModule.cs b/src/NzbDrone.Api/Series/SeriesModule.cs index cf80835ce..c69bcd5fc 100644 --- a/src/NzbDrone.Api/Series/SeriesModule.cs +++ b/src/NzbDrone.Api/Series/SeriesModule.cs @@ -13,6 +13,7 @@ using NzbDrone.Api.Validation; using NzbDrone.Api.Mapping; using NzbDrone.Core.Tv.Events; using NzbDrone.Core.Validation.Paths; +using NzbDrone.Core.DataAugmentation.Scene; namespace NzbDrone.Api.Series { @@ -27,11 +28,13 @@ namespace NzbDrone.Api.Series private readonly ICommandExecutor _commandExecutor; private readonly ISeriesService _seriesService; private readonly ISeriesStatisticsService _seriesStatisticsService; + private readonly ISceneMappingService _sceneMappingService; private readonly IMapCoversToLocal _coverMapper; public SeriesModule(ICommandExecutor commandExecutor, ISeriesService seriesService, ISeriesStatisticsService seriesStatisticsService, + ISceneMappingService sceneMappingService, IMapCoversToLocal coverMapper, RootFolderValidator rootFolderValidator, PathExistsValidator pathExistsValidator, @@ -44,6 +47,8 @@ namespace NzbDrone.Api.Series _commandExecutor = commandExecutor; _seriesService = seriesService; _seriesStatisticsService = seriesStatisticsService; + _sceneMappingService = sceneMappingService; + _coverMapper = coverMapper; GetResourceAll = AllSeries; @@ -67,6 +72,21 @@ namespace NzbDrone.Api.Series PostValidator.RuleFor(s => s.TvdbId).GreaterThan(0).SetValidator(seriesExistsValidator); } + private void PopulateAlternativeTitles(List resources) + { + foreach (var resource in resources) + { + PopulateAlternativeTitles(resource); + } + } + + private void PopulateAlternativeTitles(SeriesResource resource) + { + var mapping = _sceneMappingService.FindByTvdbid(resource.TvdbId); + if (mapping == null) return; + resource.AlternativeTitles = mapping.Select(x => x.Title).Distinct().ToList(); + } + private SeriesResource GetSeries(int id) { var series = _seriesService.GetSeries(id); @@ -80,6 +100,7 @@ namespace NzbDrone.Api.Series var resource = series.InjectTo(); MapCoversToLocal(resource); FetchAndLinkSeriesStatistics(resource); + PopulateAlternativeTitles(resource); return resource; } @@ -91,6 +112,7 @@ namespace NzbDrone.Api.Series MapCoversToLocal(seriesResources.ToArray()); LinkSeriesStatistics(seriesResources, seriesStats); + PopulateAlternativeTitles(seriesResources); return seriesResources; } diff --git a/src/NzbDrone.Api/Series/SeriesResource.cs b/src/NzbDrone.Api/Series/SeriesResource.cs index 3e09fd3cf..6fdff13df 100644 --- a/src/NzbDrone.Api/Series/SeriesResource.cs +++ b/src/NzbDrone.Api/Series/SeriesResource.cs @@ -15,6 +15,7 @@ namespace NzbDrone.Api.Series //View Only public String Title { get; set; } + public List AlternativeTitles { get; set; } public Int32 SeasonCount { diff --git a/src/NzbDrone.Core/DataAugmentation/Scene/SceneMapping.cs b/src/NzbDrone.Core/DataAugmentation/Scene/SceneMapping.cs index 07bdfc065..a29518718 100644 --- a/src/NzbDrone.Core/DataAugmentation/Scene/SceneMapping.cs +++ b/src/NzbDrone.Core/DataAugmentation/Scene/SceneMapping.cs @@ -5,7 +5,8 @@ namespace NzbDrone.Core.DataAugmentation.Scene { public class SceneMapping : ModelBase { - [JsonProperty("title")] + public string Title { get; set; } + public string ParseTerm { get; set; } [JsonProperty("searchTitle")] diff --git a/src/NzbDrone.Core/DataAugmentation/Scene/SceneMappingRepository.cs b/src/NzbDrone.Core/DataAugmentation/Scene/SceneMappingRepository.cs index b3075e88f..f59e3a64b 100644 --- a/src/NzbDrone.Core/DataAugmentation/Scene/SceneMappingRepository.cs +++ b/src/NzbDrone.Core/DataAugmentation/Scene/SceneMappingRepository.cs @@ -1,12 +1,13 @@ using NzbDrone.Core.Datastore; using NzbDrone.Core.Messaging.Events; +using System.Collections.Generic; namespace NzbDrone.Core.DataAugmentation.Scene { public interface ISceneMappingRepository : IBasicRepository { - + List FindByTvdbid(int tvdbId); } public class SceneMappingRepository : BasicRepository, ISceneMappingRepository @@ -16,5 +17,9 @@ namespace NzbDrone.Core.DataAugmentation.Scene { } + public List FindByTvdbid(int tvdbId) + { + return Query.Where(x => x.TvdbId == tvdbId); + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/DataAugmentation/Scene/SceneMappingService.cs b/src/NzbDrone.Core/DataAugmentation/Scene/SceneMappingService.cs index bbe1e08a2..42137b1ac 100644 --- a/src/NzbDrone.Core/DataAugmentation/Scene/SceneMappingService.cs +++ b/src/NzbDrone.Core/DataAugmentation/Scene/SceneMappingService.cs @@ -6,6 +6,7 @@ using NzbDrone.Core.Lifecycle; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Parser; +using System.Collections.Generic; namespace NzbDrone.Core.DataAugmentation.Scene { @@ -13,6 +14,7 @@ namespace NzbDrone.Core.DataAugmentation.Scene { string GetSceneName(int tvdbId); Nullable GetTvDbId(string cleanName); + List FindByTvdbid(int tvdbId); } public class SceneMappingService : ISceneMappingService, @@ -24,6 +26,7 @@ namespace NzbDrone.Core.DataAugmentation.Scene private readonly Logger _logger; private readonly ICached _getSceneNameCache; private readonly ICached _gettvdbIdCache; + private readonly ICached> _findbytvdbIdCache; public SceneMappingService(ISceneMappingRepository repository, ISceneMappingProxy sceneMappingProxy, ICacheManager cacheManager, Logger logger) { @@ -32,6 +35,7 @@ namespace NzbDrone.Core.DataAugmentation.Scene _getSceneNameCache = cacheManager.GetCache(GetType(), "scene_name"); _gettvdbIdCache = cacheManager.GetCache(GetType(), "tvdb_id"); + _findbytvdbIdCache = cacheManager.GetCache>(GetType(), "find_tvdb_id"); _logger = logger; } @@ -54,6 +58,11 @@ namespace NzbDrone.Core.DataAugmentation.Scene return mapping.TvdbId; } + public List FindByTvdbid(int tvdbId) + { + return _findbytvdbIdCache.Find(tvdbId.ToString()); + } + private void UpdateMappings() { _logger.Info("Updating Scene mapping"); @@ -68,7 +77,7 @@ namespace NzbDrone.Core.DataAugmentation.Scene foreach (var sceneMapping in mappings) { - sceneMapping.ParseTerm = sceneMapping.ParseTerm.CleanSeriesTitle(); + sceneMapping.ParseTerm = sceneMapping.Title.CleanSeriesTitle(); } _repository.InsertMany(mappings); @@ -92,12 +101,17 @@ namespace NzbDrone.Core.DataAugmentation.Scene _gettvdbIdCache.Clear(); _getSceneNameCache.Clear(); + _findbytvdbIdCache.Clear(); foreach (var sceneMapping in mappings) { _getSceneNameCache.Set(sceneMapping.TvdbId.ToString(), sceneMapping); _gettvdbIdCache.Set(sceneMapping.ParseTerm.CleanSeriesTitle(), sceneMapping); } + foreach (var sceneMapping in mappings.GroupBy(x => x.TvdbId)) + { + _findbytvdbIdCache.Set(sceneMapping.Key.ToString(), sceneMapping.ToList()); + } } diff --git a/src/NzbDrone.Core/Datastore/Migration/048_add_title_to_scenemappings.cs b/src/NzbDrone.Core/Datastore/Migration/048_add_title_to_scenemappings.cs new file mode 100644 index 000000000..89e099605 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/048_add_title_to_scenemappings.cs @@ -0,0 +1,18 @@ +using NzbDrone.Core.Datastore.Migration.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using FluentMigrator; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(48)] + public class add_title_to_scenemappings : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Alter.Table("SceneMappings").AddColumn("Title").AsString().Nullable(); + } + } +} diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 9ec718804..f7de87066 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -193,6 +193,7 @@ + diff --git a/src/UI/Series/Details/InfoViewTemplate.html b/src/UI/Series/Details/InfoViewTemplate.html index bef8be3df..25d203f5b 100644 --- a/src/UI/Series/Details/InfoViewTemplate.html +++ b/src/UI/Series/Details/InfoViewTemplate.html @@ -1,25 +1,32 @@ -{{qualityProfile qualityProfileId}} -{{network}} -{{runtime}} minutes -{{path}} -{{#if_eq status compare="continuing"}} - Continuing -{{else}} - Ended -{{/if_eq}} +
+ {{qualityProfile qualityProfileId}} + {{network}} + {{runtime}} minutes + {{path}} + {{#if_eq status compare="continuing"}} + Continuing + {{else}} + Ended + {{/if_eq}} - - Trakt + + Trakt - {{#if imdbId}} - IMDB - {{/if}} + {{#if imdbId}} + IMDB + {{/if}} - {{#if tvdbId}} - TVDB - {{/if}} + {{#if tvdbId}} + TVDB + {{/if}} - {{#if tvRageId}} - TVRage - {{/if}} - \ No newline at end of file + {{#if tvRageId}} + TVRage + {{/if}} + +
+
+ {{#each alternativeTitles}} + {{this}} + {{/each}} +
\ No newline at end of file From 5bc820efedc769386fe722a0d863b7dae1359c19 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 14 Apr 2014 22:07:43 -0700 Subject: [PATCH 18/25] Fixed: Plex server authentication --- .../NotificationTests/PlexProviderTest.cs | 73 ----------- .../Notifications/Plex/PlexError.cs | 9 ++ .../Notifications/Plex/PlexException.cs | 19 +++ .../Notifications/Plex/PlexSection.cs | 26 ++++ .../Notifications/Plex/PlexServerProxy.cs | 121 ++++++++++++++++++ .../Notifications/Plex/PlexServerSettings.cs | 2 +- .../Notifications/Plex/PlexService.cs | 30 ++--- .../Notifications/Plex/PlexUser.cs | 11 ++ src/NzbDrone.Core/NzbDrone.Core.csproj | 5 + 9 files changed, 202 insertions(+), 94 deletions(-) create mode 100644 src/NzbDrone.Core/Notifications/Plex/PlexError.cs create mode 100644 src/NzbDrone.Core/Notifications/Plex/PlexException.cs create mode 100644 src/NzbDrone.Core/Notifications/Plex/PlexSection.cs create mode 100644 src/NzbDrone.Core/Notifications/Plex/PlexServerProxy.cs create mode 100644 src/NzbDrone.Core/Notifications/Plex/PlexUser.cs diff --git a/src/NzbDrone.Core.Test/NotificationTests/PlexProviderTest.cs b/src/NzbDrone.Core.Test/NotificationTests/PlexProviderTest.cs index bb1dea1f0..90b0d2c5f 100644 --- a/src/NzbDrone.Core.Test/NotificationTests/PlexProviderTest.cs +++ b/src/NzbDrone.Core.Test/NotificationTests/PlexProviderTest.cs @@ -33,79 +33,6 @@ namespace NzbDrone.Core.Test.NotificationTests }; } - [Test] - public void GetSectionKeys_should_return_single_section_key_when_only_one_show_section() - { - var response = ""; - Stream stream = new MemoryStream(ASCIIEncoding.Default.GetBytes(response)); - - Mocker.GetMock().Setup(s => s.DownloadStream("http://localhost:32400/library/sections", null)) - .Returns(stream); - - var result = Mocker.Resolve().GetSectionKeys(new PlexServerSettings { Host = "localhost", Port = 32400 }); - - - result.Should().HaveCount(1); - result.First().Should().Be(5); - } - - [Test] - public void GetSectionKeys_should_return_single_section_key_when_only_one_show_section_with_other_sections() - { - - - var response = ""; - Stream stream = new MemoryStream(ASCIIEncoding.Default.GetBytes(response)); - - Mocker.GetMock().Setup(s => s.DownloadStream("http://localhost:32400/library/sections", null)) - .Returns(stream); - - - var result = Mocker.Resolve().GetSectionKeys(new PlexServerSettings { Host = "localhost", Port = 32400 }); - - - result.Should().HaveCount(1); - result.First().Should().Be(5); - } - - [Test] - public void GetSectionKeys_should_return_multiple_section_keys_when_there_are_multiple_show_sections() - { - - - var response = ""; - Stream stream = new MemoryStream(ASCIIEncoding.Default.GetBytes(response)); - - Mocker.GetMock().Setup(s => s.DownloadStream("http://localhost:32400/library/sections", null)) - .Returns(stream); - - - var result = Mocker.Resolve().GetSectionKeys(new PlexServerSettings { Host = "localhost", Port = 32400 }); - - - result.Should().HaveCount(2); - result.First().Should().Be(5); - result.Last().Should().Be(6); - } - - [Test] - public void UpdateSection_should_update_section() - { - - - var response = ""; - Stream stream = new MemoryStream(ASCIIEncoding.Default.GetBytes(response)); - - Mocker.GetMock().Setup(s => s.DownloadString("http://localhost:32400/library/sections/5/refresh")) - .Returns(response); - - - Mocker.Resolve().UpdateSection(new PlexServerSettings { Host = "localhost", Port = 32400 }, 5); - - - - } - [Test] public void Notify_should_send_notification() { diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexError.cs b/src/NzbDrone.Core/Notifications/Plex/PlexError.cs new file mode 100644 index 000000000..78e733050 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Plex/PlexError.cs @@ -0,0 +1,9 @@ +using System; + +namespace NzbDrone.Core.Notifications.Plex +{ + public class PlexError + { + public String Error { get; set; } + } +} diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexException.cs b/src/NzbDrone.Core/Notifications/Plex/PlexException.cs new file mode 100644 index 000000000..5447c563b --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Plex/PlexException.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NzbDrone.Common.Exceptions; + +namespace NzbDrone.Core.Notifications.Plex +{ + public class PlexException : NzbDroneException + { + public PlexException(string message) : base(message) + { + } + + public PlexException(string message, params object[] args) : base(message, args) + { + } + } +} diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexSection.cs b/src/NzbDrone.Core/Notifications/Plex/PlexSection.cs new file mode 100644 index 000000000..d43fb53f6 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Plex/PlexSection.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace NzbDrone.Core.Notifications.Plex +{ + public class PlexSection + { + public Int32 Id { get; set; } + public String Path { get; set; } + } + + public class PlexDirectory + { + public String Type { get; set; } + + [JsonProperty("_children")] + public List Sections { get; set; } + } + + public class PlexMediaContainer + { + [JsonProperty("_children")] + public List Directories { get; set; } + } +} diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexServerProxy.cs b/src/NzbDrone.Core/Notifications/Plex/PlexServerProxy.cs new file mode 100644 index 000000000..e0cf3e061 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Plex/PlexServerProxy.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Newtonsoft.Json.Linq; +using NzbDrone.Common; +using NzbDrone.Common.Cache; +using NzbDrone.Common.Serializer; +using RestSharp; + +namespace NzbDrone.Core.Notifications.Plex +{ + public interface IPlexServerProxy + { + List GetTvSections(PlexServerSettings settings); + void Update(int sectionId, PlexServerSettings settings); + } + + public class PlexServerProxy : IPlexServerProxy + { + private readonly ICached _authCache; + + public PlexServerProxy(ICacheManager cacheManager) + { + _authCache = cacheManager.GetCache(GetType(), "authCache"); + } + + public List GetTvSections(PlexServerSettings settings) + { + var request = GetPlexServerRequest("library/sections", Method.GET, settings); + var client = GetPlexServerClient(settings); + + var response = client.Execute(request); + + return Json.Deserialize(response.Content) + .Directories + .Where(d => d.Type == "show") + .SelectMany(d => d.Sections) + .ToList(); + } + + public void Update(int sectionId, PlexServerSettings settings) + { + var resource = String.Format("library/sections/{2}/refresh"); + var request = GetPlexServerRequest(resource, Method.GET, settings); + var client = GetPlexServerClient(settings); + + var response = client.Execute(request); + } + + private String Authenticate(string username, string password) + { + var request = GetMyPlexRequest("users/sign_in.json", Method.POST); + var client = GetMyPlexClient(username, password); + + var response = client.Execute(request); + CheckForError(response.Content); + + var user = Json.Deserialize(JObject.Parse(response.Content).SelectToken("user").ToString()); + + _authCache.Set(username, user.AuthenticationToken); + + return user.AuthenticationToken; + } + + private RestClient GetMyPlexClient(string username, string password) + { + var client = new RestClient("https://my.plexapp.com"); + client.Authenticator = new HttpBasicAuthenticator(username, password); + + return client; + } + + private RestRequest GetMyPlexRequest(string resource, Method method) + { + var request = new RestRequest(resource, method); + request.AddHeader("X-Plex-Platform", "Windows"); + request.AddHeader("X-Plex-Platform-Version", "7"); + request.AddHeader("X-Plex-Provides", "player"); + request.AddHeader("X-Plex-Client-Identifier", "AB6CCCC7-5CF5-4523-826A-B969E0FFD8A0"); + request.AddHeader("X-Plex-Product", "PlexWMC"); + request.AddHeader("X-Plex-Version", "0"); + + return request; + + } + + private RestClient GetPlexServerClient(PlexServerSettings settings) + { + return new RestClient(String.Format("http://{0}:{1}", settings.Host, settings.Port)); + } + + private RestRequest GetPlexServerRequest(string resource, Method method, PlexServerSettings settings) + { + var request = new RestRequest(resource, method); + request.AddHeader("Accept", "application/json"); + + if (!settings.Username.IsNullOrWhiteSpace()) + { + request.AddParameter("X-Plex-Token", GetAuthenticationToken(settings.Username, settings.Password)); + } + + return request; + } + + private string GetAuthenticationToken(string username, string password) + { + return _authCache.Get(username, () => Authenticate(username, password)); + } + + private void CheckForError(string response) + { + var error = Json.Deserialize(response); + + if (error != null && !error.Error.IsNullOrWhiteSpace()) + { + throw new PlexException(error.Error); + } + } + } +} diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexServerSettings.cs b/src/NzbDrone.Core/Notifications/Plex/PlexServerSettings.cs index 272d0efcc..89be29eb0 100644 --- a/src/NzbDrone.Core/Notifications/Plex/PlexServerSettings.cs +++ b/src/NzbDrone.Core/Notifications/Plex/PlexServerSettings.cs @@ -33,7 +33,7 @@ namespace NzbDrone.Core.Notifications.Plex [FieldDefinition(2, Label = "Username")] public String Username { get; set; } - [FieldDefinition(3, Label = "Password")] + [FieldDefinition(3, Label = "Password", Type = FieldType.Password)] public String Password { get; set; } [FieldDefinition(4, Label = "Update Library", Type = FieldType.Checkbox)] diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexService.cs b/src/NzbDrone.Core/Notifications/Plex/PlexService.cs index 0d4c6608b..a27b55d3a 100644 --- a/src/NzbDrone.Core/Notifications/Plex/PlexService.cs +++ b/src/NzbDrone.Core/Notifications/Plex/PlexService.cs @@ -18,11 +18,13 @@ namespace NzbDrone.Core.Notifications.Plex public class PlexService : IPlexService, IExecute, IExecute { private readonly IHttpProvider _httpProvider; + private readonly IPlexServerProxy _plexServerProxy; private readonly Logger _logger; - public PlexService(IHttpProvider httpProvider, Logger logger) + public PlexService(IHttpProvider httpProvider, IPlexServerProxy plexServerProxy, Logger logger) { _httpProvider = httpProvider; + _plexServerProxy = plexServerProxy; _logger = logger; } @@ -45,7 +47,7 @@ namespace NzbDrone.Core.Notifications.Plex { _logger.Debug("Sending Update Request to Plex Server"); var sections = GetSectionKeys(settings); - sections.ForEach(s => UpdateSection(settings, s)); + sections.ForEach(s => UpdateSection(s, settings)); } catch(Exception ex) @@ -55,26 +57,21 @@ namespace NzbDrone.Core.Notifications.Plex } } - public List GetSectionKeys(PlexServerSettings settings) + private List GetSectionKeys(PlexServerSettings settings) { _logger.Debug("Getting sections from Plex host: {0}", settings.Host); - var url = String.Format("http://{0}:{1}/library/sections", settings.Host, settings.Port); - var xmlStream = _httpProvider.DownloadStream(url, GetCredentials(settings)); - var xDoc = XDocument.Load(xmlStream); - var mediaContainer = xDoc.Descendants("MediaContainer").FirstOrDefault(); - var directories = mediaContainer.Descendants("Directory").Where(x => x.Attribute("type").Value == "show"); - return directories.Select(d => Int32.Parse(d.Attribute("key").Value)).ToList(); + return _plexServerProxy.GetTvSections(settings).Select(s => s.Id).ToList(); } - public void UpdateSection(PlexServerSettings settings, int key) + private void UpdateSection(int key, PlexServerSettings settings) { _logger.Debug("Updating Plex host: {0}, Section: {1}", settings.Host, key); - var url = String.Format("http://{0}:{1}/library/sections/{2}/refresh", settings.Host, settings.Port, key); - _httpProvider.DownloadString(url, GetCredentials(settings)); + + _plexServerProxy.Update(key, settings); } - public string SendCommand(string host, int port, string command, string username, string password) + private string SendCommand(string host, int port, string command, string username, string password) { var url = String.Format("http://{0}:{1}/xbmcCmds/xbmcHttp?command={2}", host, port, command); @@ -86,13 +83,6 @@ namespace NzbDrone.Core.Notifications.Plex return _httpProvider.DownloadString(url); } - private NetworkCredential GetCredentials(PlexServerSettings settings) - { - if (settings.Username.IsNullOrWhiteSpace()) return null; - - return new NetworkCredential(settings.Username, settings.Password); - } - public void Execute(TestPlexClientCommand message) { _logger.Debug("Sending Test Notifcation to Plex Client: {0}", message.Host); diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexUser.cs b/src/NzbDrone.Core/Notifications/Plex/PlexUser.cs new file mode 100644 index 000000000..4ca9a642a --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Plex/PlexUser.cs @@ -0,0 +1,11 @@ +using System; +using Newtonsoft.Json; + +namespace NzbDrone.Core.Notifications.Plex +{ + public class PlexUser + { + [JsonProperty("authentication_token")] + public String AuthenticationToken { get; set; } + } +} diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 9ec718804..a27327dba 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -377,6 +377,11 @@ + + + + + From dade3bb2140309edc480b55a1b47925cc40db5f9 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Tue, 15 Apr 2014 07:16:49 -0700 Subject: [PATCH 19/25] New: Examples for Series and Season folder format --- src/NzbDrone.Api/Config/NamingConfigModule.cs | 3 +++ .../Config/NamingSampleResource.cs | 2 ++ .../Organizer/FileNameBuilder.cs | 27 ++++++++++++++----- .../Organizer/FilenameSampleService.cs | 15 ++++++++--- .../MediaManagement/Naming/NamingView.js | 6 ++++- .../Naming/NamingViewTemplate.html | 20 ++++++++++++-- 6 files changed, 60 insertions(+), 13 deletions(-) diff --git a/src/NzbDrone.Api/Config/NamingConfigModule.cs b/src/NzbDrone.Api/Config/NamingConfigModule.cs index eb41ef9e9..a571dd400 100644 --- a/src/NzbDrone.Api/Config/NamingConfigModule.cs +++ b/src/NzbDrone.Api/Config/NamingConfigModule.cs @@ -92,6 +92,9 @@ namespace NzbDrone.Api.Config ? "Invalid format" : dailyEpisodeSampleResult.Filename; + sampleResource.SeriesFolderExample = _filenameSampleService.GetSeriesFolderSample(nameSpec); + sampleResource.SeasonFolderExample = _filenameSampleService.GetSeasonFolderSample(nameSpec); + return sampleResource.AsResponse(); } diff --git a/src/NzbDrone.Api/Config/NamingSampleResource.cs b/src/NzbDrone.Api/Config/NamingSampleResource.cs index 7f6d0e99e..56ff031d2 100644 --- a/src/NzbDrone.Api/Config/NamingSampleResource.cs +++ b/src/NzbDrone.Api/Config/NamingSampleResource.cs @@ -5,5 +5,7 @@ public string SingleEpisodeExample { get; set; } public string MultiEpisodeExample { get; set; } public string DailyEpisodeExample { get; set; } + public string SeriesFolderExample { get; set; } + public string SeasonFolderExample { get; set; } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index d3caa5729..c09bc0c6d 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -18,6 +18,8 @@ namespace NzbDrone.Core.Organizer string BuildFilePath(Series series, int seasonNumber, string fileName, string extension); BasicNamingConfig GetBasicNamingConfig(NamingConfig nameSpec); string GetSeriesFolder(string seriesTitle); + string GetSeriesFolder(string seriesTitle, NamingConfig namingConfig); + string GetSeasonFolder(string seriesTitle, int seasonNumber, NamingConfig namingConfig); } public class FileNameBuilder : IBuildFileNames @@ -171,11 +173,7 @@ namespace NzbDrone.Core.Organizer else { var nameSpec = _namingConfigService.GetConfig(); - var tokenValues = new Dictionary(FilenameBuilderTokenEqualityComparer.Instance); - tokenValues.Add("{Series Title}", series.Title); - - seasonFolder = ReplaceSeasonTokens(nameSpec.SeasonFolderFormat, seasonNumber); - seasonFolder = ReplaceTokens(seasonFolder, tokenValues); + seasonFolder = GetSeasonFolder(series.Title, seasonNumber, nameSpec); } seasonFolder = CleanFilename(seasonFolder); @@ -233,14 +231,29 @@ namespace NzbDrone.Core.Organizer } public string GetSeriesFolder(string seriesTitle) + { + var namingConfig = _namingConfigService.GetConfig(); + + return GetSeriesFolder(seriesTitle, namingConfig); + } + + public string GetSeriesFolder(string seriesTitle, NamingConfig namingConfig) { seriesTitle = CleanFilename(seriesTitle); - var nameSpec = _namingConfigService.GetConfig(); var tokenValues = new Dictionary(FilenameBuilderTokenEqualityComparer.Instance); tokenValues.Add("{Series Title}", seriesTitle); - return ReplaceTokens(nameSpec.SeriesFolderFormat, tokenValues); + return ReplaceTokens(namingConfig.SeriesFolderFormat, tokenValues); + } + + public string GetSeasonFolder(string seriesTitle, int seasonNumber, NamingConfig namingConfig) + { + var tokenValues = new Dictionary(FilenameBuilderTokenEqualityComparer.Instance); + tokenValues.Add("{Series Title}", seriesTitle); + + var seasonFolder = ReplaceSeasonTokens(namingConfig.SeasonFolderFormat, seasonNumber); + return ReplaceTokens(seasonFolder, tokenValues); } public static string CleanFilename(string name) diff --git a/src/NzbDrone.Core/Organizer/FilenameSampleService.cs b/src/NzbDrone.Core/Organizer/FilenameSampleService.cs index 668a20966..2857bdd3a 100644 --- a/src/NzbDrone.Core/Organizer/FilenameSampleService.cs +++ b/src/NzbDrone.Core/Organizer/FilenameSampleService.cs @@ -1,9 +1,6 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; using NzbDrone.Core.MediaFiles; -using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; @@ -14,6 +11,8 @@ namespace NzbDrone.Core.Organizer SampleResult GetStandardSample(NamingConfig nameSpec); SampleResult GetMultiEpisodeSample(NamingConfig nameSpec); SampleResult GetDailySample(NamingConfig nameSpec); + String GetSeriesFolderSample(NamingConfig nameSpec); + String GetSeasonFolderSample(NamingConfig nameSpec); } public class FilenameSampleService : IFilenameSampleService @@ -123,6 +122,16 @@ namespace NzbDrone.Core.Organizer return result; } + public string GetSeriesFolderSample(NamingConfig nameSpec) + { + return _buildFileNames.GetSeriesFolder(_standardSeries.Title, nameSpec); + } + + public string GetSeasonFolderSample(NamingConfig nameSpec) + { + return _buildFileNames.GetSeasonFolder(_standardSeries.Title, _episode1.SeasonNumber, nameSpec); + } + private string BuildSample(List episodes, Series series, EpisodeFile episodeFile, NamingConfig nameSpec) { try diff --git a/src/UI/Settings/MediaManagement/Naming/NamingView.js b/src/UI/Settings/MediaManagement/Naming/NamingView.js index ab0c91a7b..071339d59 100644 --- a/src/UI/Settings/MediaManagement/Naming/NamingView.js +++ b/src/UI/Settings/MediaManagement/Naming/NamingView.js @@ -19,7 +19,9 @@ define( multiEpisodeExample : '.x-multi-episode-example', dailyEpisodeExample : '.x-daily-episode-example', namingTokenHelper : '.x-naming-token-helper', - multiEpisodeStyle : '.x-multi-episode-style' + multiEpisodeStyle : '.x-multi-episode-style', + seriesFolderExample : '.x-series-folder-example', + seasonFolderExample : '.x-season-folder-example' }, events: { @@ -66,6 +68,8 @@ define( this.ui.singleEpisodeExample.html(this.namingSampleModel.get('singleEpisodeExample')); this.ui.multiEpisodeExample.html(this.namingSampleModel.get('multiEpisodeExample')); this.ui.dailyEpisodeExample.html(this.namingSampleModel.get('dailyEpisodeExample')); + this.ui.seriesFolderExample.html(this.namingSampleModel.get('seriesFolderExample')); + this.ui.seasonFolderExample.html(this.namingSampleModel.get('seasonFolderExample')); }, _addToken: function (e) { diff --git a/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.html b/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.html index c72a6524e..df3917db9 100644 --- a/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.html +++ b/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.html @@ -103,7 +103,7 @@
- +
+ +
+ + +
+ +
+
+ +
+ + +
+ +
+
From 6aaa3c573fc2599cbc968fafebd522016b102f5f Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Tue, 15 Apr 2014 23:21:59 +0200 Subject: [PATCH 20/25] Fixed: Hashed releases should be parsed more accurately. --- .../NzbDrone.Core.Test.csproj | 1 + .../ParserTests/CrapParserFixture.cs | 1 + .../ParserTests/HashedReleasesFixture.cs | 20 +++++++++++++++++++ src/NzbDrone.Core/Parser/Parser.cs | 19 ++++++++++++++---- 4 files changed, 37 insertions(+), 4 deletions(-) create mode 100644 src/NzbDrone.Core.Test/ParserTests/HashedReleasesFixture.cs diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 022d01d8d..016d07e16 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -191,6 +191,7 @@ + diff --git a/src/NzbDrone.Core.Test/ParserTests/CrapParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/CrapParserFixture.cs index 9ed19f36f..a304518f1 100644 --- a/src/NzbDrone.Core.Test/ParserTests/CrapParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/CrapParserFixture.cs @@ -28,6 +28,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("ce39afb7da6cf7c04eba3090f0a309f609883862")] [TestCase("THIS SHOULD NEVER PARSE")] [TestCase("Vh1FvU3bJXw6zs8EEUX4bMo5vbbMdHghxHirc.mkv")] + [TestCase("0e895c37245186812cb08aab1529cf8ee389dd05.mkv")] public void should_not_parse_crap(string title) { Parser.Parser.ParseTitle(title).Should().BeNull(); diff --git a/src/NzbDrone.Core.Test/ParserTests/HashedReleasesFixture.cs b/src/NzbDrone.Core.Test/ParserTests/HashedReleasesFixture.cs new file mode 100644 index 000000000..4f810789c --- /dev/null +++ b/src/NzbDrone.Core.Test/ParserTests/HashedReleasesFixture.cs @@ -0,0 +1,20 @@ +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.ParserTests +{ + [TestFixture] + public class hashedReleasesFixture : CoreTest + { + [TestCase(@"C:\Test\Some.Hashed.Release.S01E01.720p.WEB-DL.AAC2.0.H.264-Mercury\0e895c3724.mkv", "somehashedrelease", "WEBDL-720p", "Mercury")] + [TestCase(@"C:\Test\0e895c3724\Some.Hashed.Release.S01E01.720p.WEB-DL.AAC2.0.H.264-Mercury.mkv", "somehashedrelease", "WEBDL-720p", "Mercury")] + public void should_properly_parse_hashed_releases(string path, string title, string quality, string releaseGroup) + { + var result = Parser.Parser.ParsePath(path); + result.SeriesTitle.Should().Be(title); + result.Quality.ToString().Should().Be(quality); + result.ReleaseGroup.Should().Be(releaseGroup); + } + } +} diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 0df8b6d6d..470ee4516 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; using NLog; +using NzbDrone.Common; using NzbDrone.Common.Instrumentation; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Tv; @@ -46,7 +47,7 @@ namespace NzbDrone.Core.Parser RegexOptions.IgnoreCase | RegexOptions.Compiled), //Episodes without a title, Single (S01E05, 1x05) AND Multi (S01E04E05, 1x04x05, etc) - new Regex(@"^(?:S?(?(?\d{2,3}(?!\d+)))+)", + new Regex(@"^(?:S?(?(?\d{2,3}(?!\d+)))+(?![\da-z]))", RegexOptions.IgnoreCase | RegexOptions.Compiled), //Episodes with a title, Single episodes (S01E05, 1x05, etc) & Multi-episode (S01E05E06, S01E05-06, S01E05 E06, etc) @@ -91,7 +92,7 @@ namespace NzbDrone.Core.Parser RegexOptions.IgnoreCase | RegexOptions.Compiled), //Episodes with a title, Single episodes (S01E05, 1x05, etc) & Multi-episode (S01E05E06, S01E05-06, S01E05 E06, etc) - new Regex(@"^(?.+?)(?:(\W|_)+S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]|\W[ex]|_){1,2}(?<episode>\d{4}(?!\d+|i|p)))+)\W?(?!\\)", + new Regex(@"^(?<title>.+?)(?:(\W|_)+S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]|\W[ex]|_){1,2}(?<episode>\d{4}(?!\d+|i|p)))+(?![\da-z]))\W?(?!\\)", RegexOptions.IgnoreCase | RegexOptions.Compiled), //Anime - Title Absolute Episode Number @@ -126,6 +127,12 @@ namespace NzbDrone.Core.Parser var result = ParseTitle(fileInfo.Name); + if (result == null) + { + Logger.Debug("Attempting to parse episode info using directory path. {0}", fileInfo.Directory.Name); + result = ParseTitle(fileInfo.Directory.Name + fileInfo.Extension); + } + if (result == null) { Logger.Debug("Attempting to parse episode info using full path. {0}", fileInfo.FullName); @@ -138,8 +145,6 @@ namespace NzbDrone.Core.Parser return null; } - result.ReleaseGroup = ParseReleaseGroup(fileInfo.Name.Replace(fileInfo.Extension, "")); - return result; } @@ -239,6 +244,12 @@ namespace NzbDrone.Core.Parser const string defaultReleaseGroup = "DRONE"; title = title.Trim(); + + if (!title.ContainsInvalidPathChars() && MediaFiles.MediaFileExtensions.Extensions.Contains(Path.GetExtension(title).ToLower())) + { + title = Path.GetFileNameWithoutExtension(title).Trim(); + } + var index = title.LastIndexOf('-'); if (index < 0) From 14554b49bc1fbdac8809f6300b95e25775400585 Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Tue, 15 Apr 2014 14:50:18 -0700 Subject: [PATCH 21/25] HashedReleaseFixture uses OS agnostic paths --- .../NzbDrone.Core.Test.csproj | 2 +- .../ParserTests/HashedReleaseFixture.cs | 38 +++++++++++++++++++ .../ParserTests/HashedReleasesFixture.cs | 20 ---------- 3 files changed, 39 insertions(+), 21 deletions(-) create mode 100644 src/NzbDrone.Core.Test/ParserTests/HashedReleaseFixture.cs delete mode 100644 src/NzbDrone.Core.Test/ParserTests/HashedReleasesFixture.cs diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 016d07e16..22a8d5042 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -191,7 +191,7 @@ <Compile Include="ParserTests\NormalizeTitleFixture.cs" /> <Compile Include="ParserTests\CrapParserFixture.cs" /> <Compile Include="ParserTests\DailyEpisodeParserFixture.cs" /> - <Compile Include="ParserTests\HashedReleasesFixture.cs" /> + <Compile Include="ParserTests\HashedReleaseFixture.cs" /> <Compile Include="ParserTests\SingleEpisodeParserFixture.cs" /> <Compile Include="ParserTests\PathParserFixture.cs" /> <Compile Include="ParserTests\MultiEpisodeParserFixture.cs" /> diff --git a/src/NzbDrone.Core.Test/ParserTests/HashedReleaseFixture.cs b/src/NzbDrone.Core.Test/ParserTests/HashedReleaseFixture.cs new file mode 100644 index 000000000..ea17fda0d --- /dev/null +++ b/src/NzbDrone.Core.Test/ParserTests/HashedReleaseFixture.cs @@ -0,0 +1,38 @@ +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.ParserTests +{ + [TestFixture] + public class HashedReleaseFixture : CoreTest + { + public static object[] HashedReleaseParserCases = + { + new object[] + { + @"C:\Test\Some.Hashed.Release.S01E01.720p.WEB-DL.AAC2.0.H.264-Mercury\0e895c3724.mkv".AsOsAgnostic(), + "somehashedrelease", + "WEBDL-720p", + "Mercury" + }, + new object[] + { + @"C:\Test\0e895c3724\Some.Hashed.Release.S01E01.720p.WEB-DL.AAC2.0.H.264-Mercury.mkv".AsOsAgnostic(), + "somehashedrelease", + "WEBDL-720p", + "Mercury" + } + }; + + [Test, TestCaseSource("HashedReleaseParserCases")] + public void should_properly_parse_hashed_releases(string path, string title, string quality, string releaseGroup) + { + var result = Parser.Parser.ParsePath(path); + result.SeriesTitle.Should().Be(title); + result.Quality.ToString().Should().Be(quality); + result.ReleaseGroup.Should().Be(releaseGroup); + } + } +} diff --git a/src/NzbDrone.Core.Test/ParserTests/HashedReleasesFixture.cs b/src/NzbDrone.Core.Test/ParserTests/HashedReleasesFixture.cs deleted file mode 100644 index 4f810789c..000000000 --- a/src/NzbDrone.Core.Test/ParserTests/HashedReleasesFixture.cs +++ /dev/null @@ -1,20 +0,0 @@ -using FluentAssertions; -using NUnit.Framework; -using NzbDrone.Core.Test.Framework; - -namespace NzbDrone.Core.Test.ParserTests -{ - [TestFixture] - public class hashedReleasesFixture : CoreTest - { - [TestCase(@"C:\Test\Some.Hashed.Release.S01E01.720p.WEB-DL.AAC2.0.H.264-Mercury\0e895c3724.mkv", "somehashedrelease", "WEBDL-720p", "Mercury")] - [TestCase(@"C:\Test\0e895c3724\Some.Hashed.Release.S01E01.720p.WEB-DL.AAC2.0.H.264-Mercury.mkv", "somehashedrelease", "WEBDL-720p", "Mercury")] - public void should_properly_parse_hashed_releases(string path, string title, string quality, string releaseGroup) - { - var result = Parser.Parser.ParsePath(path); - result.SeriesTitle.Should().Be(title); - result.Quality.ToString().Should().Be(quality); - result.ReleaseGroup.Should().Be(releaseGroup); - } - } -} From bc17466dbc4cbf6738343e27c0e746f2c2adafae Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Tue, 15 Apr 2014 16:53:08 -0700 Subject: [PATCH 22/25] Fixed: Parsing files that contain the date along with a season and episode --- .../ParserTests/SingleEpisodeParserFixture.cs | 1 + src/NzbDrone.Core/Parser/Parser.cs | 13 +++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/NzbDrone.Core.Test/ParserTests/SingleEpisodeParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/SingleEpisodeParserFixture.cs index baca66d12..fa6626113 100644 --- a/src/NzbDrone.Core.Test/ParserTests/SingleEpisodeParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/SingleEpisodeParserFixture.cs @@ -82,6 +82,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("24.S01E01", "24", 1, 1)] [TestCase("Homeland - 2x12 - The Choice [HDTV-1080p].mkv", "Homeland", 2, 12)] [TestCase("Homeland - 2x4 - New Car Smell [HDTV-1080p].mkv", "Homeland", 2, 4)] + [TestCase("Top Gear - 06x11 - 2005.08.07", "Top Gear", 6, 11)] public void should_parse_single_episode(string postTitle, string title, int seasonNumber, int episodeNumber) { var result = Parser.Parser.ParseTitle(postTitle); diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 470ee4516..bd22aa501 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -17,10 +17,6 @@ namespace NzbDrone.Core.Parser private static readonly Regex[] ReportTitleRegex = new[] { - //Episodes with airdate - new Regex(@"^(?<title>.+?)?\W*(?<airyear>\d{4})\W+(?<airmonth>[0-1][0-9])\W+(?<airday>[0-3][0-9])", - RegexOptions.IgnoreCase | RegexOptions.Compiled), - //Anime - Absolute Episode Number + Title + Season+Episode //Todo: This currently breaks series that start with numbers // new Regex(@"^(?:(?<absoluteepisode>\d{2,3})(?:_|-|\s|\.)+)+(?<title>.+?)(?:\W|_)+(?:S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]|\W[ex]){1,2}(?<episode>\d{2}(?!\d+)))+)", @@ -51,11 +47,11 @@ namespace NzbDrone.Core.Parser RegexOptions.IgnoreCase | RegexOptions.Compiled), //Episodes with a title, Single episodes (S01E05, 1x05, etc) & Multi-episode (S01E05E06, S01E05-06, S01E05 E06, etc) - new Regex(@"^(?<title>.+?)(?:(\W|_)+S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:(?:\-|[ex]|\W[ex]|_){1,2}(?<episode>\d{2,3}(?!\d+)))+)\W?(?!\\)", + new Regex(@"^(?<title>.+?)(?:(\W|_)+S?(?<season>(?<!\d+)\d{1,2}(?!\d+))(?:[ex]|\W[ex]|_){1,2}(?<episode>\d{2,3}(?!\d+))(?:(?:\-|[ex]|\W[ex]|_){1,2}(?<episode>\d{2,3}(?!\d+)))*)\W?(?!\\)", RegexOptions.IgnoreCase | RegexOptions.Compiled), //Episodes with a title, Single episodes (S01E05, 1x05, etc) & Multi-episode (S01E05E06, S01E05-06, S01E05 E06, etc) - new Regex(@"^(?<title>.+?)(?:\W+S?(?<season>(?<!\d+)(?:\d{1,2}|\d{4})(?!\d+))(?:(?:\-|[ex]|\W[ex]){1,2}(?<episode>\d{2,3}(?!\d+)))+)", + new Regex(@"^(?<title>.+?)(?:\W+S?(?<season>(?<!\d+)(?:\d{1,2}|\d{4})(?!\d+))(?:[ex]|\W[ex]){1,2}(?<episode>\d{2,3}(?!\d+))(?:(?:\-|[ex]|\W[ex]){1,2}(?<episode>\d{2,3}(?!\d+)))*)", RegexOptions.IgnoreCase | RegexOptions.Compiled), //Episodes with single digit episode number (S01E1, S01E5E6, etc) @@ -82,6 +78,10 @@ namespace NzbDrone.Core.Parser new Regex(@"^(?<title>.+?)\W(?:S|Season)\W?(?<season>\d{1,2}(?!\d+))(\W+|_|$)(?<extras>EXTRAS|SUBPACK)?(?!\\)", RegexOptions.IgnoreCase | RegexOptions.Compiled), + //Episodes with airdate + new Regex(@"^(?<title>.+?)?\W*(?<airyear>\d{4})\W+(?<airmonth>[0-1][0-9])\W+(?<airday>[0-3][0-9])", + RegexOptions.IgnoreCase | RegexOptions.Compiled), + //Supports 1103/1113 naming new Regex(@"^(?<title>.+?)?(?:\W(?<season>(?<!\d+|\(|\[|e|x)\d{2})(?<episode>(?<!e|x)\d{2}(?!p|i|\d+|\)|\]|\W\d+)))+(\W+|_|$)(?!\\)", RegexOptions.IgnoreCase | RegexOptions.Compiled), @@ -163,6 +163,7 @@ namespace NzbDrone.Core.Parser if (match.Count != 0) { + Console.WriteLine(regex); Logger.Trace(regex); try { From a2a2ad38b02f050bc501df7ceb9ba1e75d90b83d Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Wed, 16 Apr 2014 00:01:04 -0700 Subject: [PATCH 23/25] Fixed broken test --- .../Scene/SceneMappingProxyFixture.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/NzbDrone.Core.Test/DataAugmentationFixture/Scene/SceneMappingProxyFixture.cs b/src/NzbDrone.Core.Test/DataAugmentationFixture/Scene/SceneMappingProxyFixture.cs index cef5a5d0b..e2d18dc89 100644 --- a/src/NzbDrone.Core.Test/DataAugmentationFixture/Scene/SceneMappingProxyFixture.cs +++ b/src/NzbDrone.Core.Test/DataAugmentationFixture/Scene/SceneMappingProxyFixture.cs @@ -1,3 +1,4 @@ +using System; using System.Net; using FluentAssertions; using Newtonsoft.Json; @@ -25,8 +26,8 @@ namespace NzbDrone.Core.Test.DataAugmentationFixture.Scene mappings.Should().NotBeEmpty(); - mappings.Should().NotContain(c => string.IsNullOrWhiteSpace(c.SearchTerm)); - mappings.Should().NotContain(c => string.IsNullOrWhiteSpace(c.ParseTerm)); + mappings.Should().NotContain(c => String.IsNullOrWhiteSpace(c.SearchTerm)); + mappings.Should().NotContain(c => String.IsNullOrWhiteSpace(c.Title)); mappings.Should().NotContain(c => c.TvdbId == 0); } From 40c2c0b533a87787d0be81b67bff02bee484b364 Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Wed, 16 Apr 2014 10:09:57 -0700 Subject: [PATCH 24/25] Fixed updating for plex server --- src/NzbDrone.Core/Notifications/Plex/PlexServerProxy.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NzbDrone.Core/Notifications/Plex/PlexServerProxy.cs b/src/NzbDrone.Core/Notifications/Plex/PlexServerProxy.cs index e0cf3e061..e94091e7b 100644 --- a/src/NzbDrone.Core/Notifications/Plex/PlexServerProxy.cs +++ b/src/NzbDrone.Core/Notifications/Plex/PlexServerProxy.cs @@ -41,7 +41,7 @@ namespace NzbDrone.Core.Notifications.Plex public void Update(int sectionId, PlexServerSettings settings) { - var resource = String.Format("library/sections/{2}/refresh"); + var resource = String.Format("library/sections/{0}/refresh", sectionId); var request = GetPlexServerRequest(resource, Method.GET, settings); var client = GetPlexServerClient(settings); From fc540067c2a79ad08be983a3fc795ad5bd8dc49e Mon Sep 17 00:00:00 2001 From: Mark McDowall <markus.mcd5@gmail.com> Date: Thu, 17 Apr 2014 11:26:48 -0700 Subject: [PATCH 25/25] Series and Season folder format validation/error handling --- src/NzbDrone.Api/Config/NamingConfigModule.cs | 12 +++++++++--- src/NzbDrone.Core/Organizer/FileNameValidation.cs | 10 ++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/NzbDrone.Api/Config/NamingConfigModule.cs b/src/NzbDrone.Api/Config/NamingConfigModule.cs index a571dd400..36876a406 100644 --- a/src/NzbDrone.Api/Config/NamingConfigModule.cs +++ b/src/NzbDrone.Api/Config/NamingConfigModule.cs @@ -4,6 +4,7 @@ using System.Linq; using FluentValidation; using FluentValidation.Results; using Nancy.Responses; +using NzbDrone.Common; using NzbDrone.Core.Organizer; using Nancy.ModelBinding; using NzbDrone.Api.Mapping; @@ -39,6 +40,7 @@ namespace NzbDrone.Api.Config SharedValidator.RuleFor(c => c.StandardEpisodeFormat).ValidEpisodeFormat(); SharedValidator.RuleFor(c => c.DailyEpisodeFormat).ValidDailyEpisodeFormat(); SharedValidator.RuleFor(c => c.SeriesFolderFormat).ValidSeriesFolderFormat(); + SharedValidator.RuleFor(c => c.SeasonFolderFormat).ValidSeasonFolderFormat(); } private void UpdateNamingConfig(NamingConfigResource resource) @@ -72,7 +74,6 @@ namespace NzbDrone.Api.Config private JsonResponse<NamingSampleResource> GetExamples(NamingConfigResource config) { - //TODO: Validate that the format is valid var nameSpec = config.InjectTo<NamingConfig>(); var sampleResource = new NamingSampleResource(); @@ -92,8 +93,13 @@ namespace NzbDrone.Api.Config ? "Invalid format" : dailyEpisodeSampleResult.Filename; - sampleResource.SeriesFolderExample = _filenameSampleService.GetSeriesFolderSample(nameSpec); - sampleResource.SeasonFolderExample = _filenameSampleService.GetSeasonFolderSample(nameSpec); + sampleResource.SeriesFolderExample = nameSpec.SeriesFolderFormat.IsNullOrWhiteSpace() + ? "Invalid format" + : _filenameSampleService.GetSeriesFolderSample(nameSpec); + + sampleResource.SeasonFolderExample = nameSpec.SeasonFolderFormat.IsNullOrWhiteSpace() + ? "Invalid format" + : _filenameSampleService.GetSeasonFolderSample(nameSpec); return sampleResource.AsResponse(); } diff --git a/src/NzbDrone.Core/Organizer/FileNameValidation.cs b/src/NzbDrone.Core/Organizer/FileNameValidation.cs index 9a291e05c..bd554d46a 100644 --- a/src/NzbDrone.Core/Organizer/FileNameValidation.cs +++ b/src/NzbDrone.Core/Organizer/FileNameValidation.cs @@ -1,4 +1,5 @@ using System; +using System.Text.RegularExpressions; using FluentValidation; using FluentValidation.Validators; @@ -6,6 +7,9 @@ namespace NzbDrone.Core.Organizer { public static class FileNameValidation { + private static readonly Regex SeasonFolderRegex = new Regex(@"(\{season(\:\d+)?\})", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + public static IRuleBuilderOptions<T, string> ValidEpisodeFormat<T>(this IRuleBuilder<T, string> ruleBuilder) { ruleBuilder.SetValidator(new NotEmptyValidator(null)); @@ -23,6 +27,12 @@ namespace NzbDrone.Core.Organizer ruleBuilder.SetValidator(new NotEmptyValidator(null)); return ruleBuilder.SetValidator(new RegularExpressionValidator(FileNameBuilder.SeriesTitleRegex)).WithMessage("Must contain series title"); } + + public static IRuleBuilderOptions<T, string> ValidSeasonFolderFormat<T>(this IRuleBuilder<T, string> ruleBuilder) + { + ruleBuilder.SetValidator(new NotEmptyValidator(null)); + return ruleBuilder.SetValidator(new RegularExpressionValidator(SeasonFolderRegex)).WithMessage("Must contain season number"); + } } public class ValidDailyEpisodeFormatValidator : PropertyValidator