From d6d524e624b20949a5ea4179c8fd645003ae2c0f Mon Sep 17 00:00:00 2001 From: "kay.one" Date: Sun, 28 Apr 2013 17:39:17 -0700 Subject: [PATCH] decision engine now reports it's own errors rather than just dying. --- NzbDrone.Api/ErrorManagement/ErrorPipeline.cs | 8 +- NzbDrone.Api/Indexers/ReleaseModule.cs | 1 - .../Scene/SceneMappingServiceFixture.cs | 6 +- .../Scene/SceneMappingService.cs | 77 ++++++++----------- .../Scene/UpdateSceneMappingCommand.cs | 8 ++ .../DecisionEngine/DownloadDecisionMaker.cs | 65 ++++++++++------ .../Search/SingleEpisodeMatchSpecification.cs | 45 +++++++++++ .../IndexerSearch/NzbSearchService.cs | 4 +- .../Implementations/UpdateSceneMappingsJob.cs | 33 -------- NzbDrone.Core/NzbDrone.Core.csproj | 3 +- NzbDrone.Core/Tv/Series.cs | 8 -- NzbDrone.Core/Tv/SeriesRepository.cs | 6 -- NzbDrone.Core/Tv/SeriesService.cs | 12 ++- NzbDrone.Core/Tv/SeriesTypes.cs | 9 +++ .../ReleaseIntegrationTest.cs | 19 ++++- 15 files changed, 178 insertions(+), 126 deletions(-) create mode 100644 NzbDrone.Core/DataAugmentation/Scene/UpdateSceneMappingCommand.cs create mode 100644 NzbDrone.Core/DecisionEngine/Specifications/Search/SingleEpisodeMatchSpecification.cs delete mode 100644 NzbDrone.Core/Jobs/Implementations/UpdateSceneMappingsJob.cs create mode 100644 NzbDrone.Core/Tv/SeriesTypes.cs diff --git a/NzbDrone.Api/ErrorManagement/ErrorPipeline.cs b/NzbDrone.Api/ErrorManagement/ErrorPipeline.cs index e5c099fc6..98539dfd1 100644 --- a/NzbDrone.Api/ErrorManagement/ErrorPipeline.cs +++ b/NzbDrone.Api/ErrorManagement/ErrorPipeline.cs @@ -31,8 +31,8 @@ namespace NzbDrone.Api.ErrorManagement if (validationException != null) { _logger.Warn("Invalid request {0}", validationException.Message); - - + + return validationException.Errors.AsResponse(HttpStatusCode.BadRequest); } @@ -42,8 +42,8 @@ namespace NzbDrone.Api.ErrorManagement return new ErrorModel() { - Message = exception.Message, - Description = exception.ToString() + Message = exception.Message, + Description = exception.ToString() }.AsResponse(HttpStatusCode.InternalServerError); } } diff --git a/NzbDrone.Api/Indexers/ReleaseModule.cs b/NzbDrone.Api/Indexers/ReleaseModule.cs index 0d6912a2e..b8398c7a6 100644 --- a/NzbDrone.Api/Indexers/ReleaseModule.cs +++ b/NzbDrone.Api/Indexers/ReleaseModule.cs @@ -59,7 +59,6 @@ namespace NzbDrone.Api.Indexers public Int32 SeasonNumber { get; set; } public Language Language { get; set; } public DateTime? AirDate { get; set; } - public String OriginalString { get; set; } public String SeriesTitle { get; set; } public int[] EpisodeNumbers { get; set; } public Boolean Approved { get; set; } diff --git a/NzbDrone.Core.Test/DataAugmentationFixture/Scene/SceneMappingServiceFixture.cs b/NzbDrone.Core.Test/DataAugmentationFixture/Scene/SceneMappingServiceFixture.cs index 7509d08c2..ebfec9944 100644 --- a/NzbDrone.Core.Test/DataAugmentationFixture/Scene/SceneMappingServiceFixture.cs +++ b/NzbDrone.Core.Test/DataAugmentationFixture/Scene/SceneMappingServiceFixture.cs @@ -29,7 +29,7 @@ namespace NzbDrone.Core.Test.DataAugmentationFixture.Scene { Mocker.GetMock().Setup(c => c.Fetch()).Returns(_fakeMappings); - Subject.UpdateMappings(); + Subject.Execute(new UpdateSceneMappingCommand()); AssertMappingUpdated(); } @@ -42,7 +42,7 @@ namespace NzbDrone.Core.Test.DataAugmentationFixture.Scene Mocker.GetMock().Setup(c => c.Fetch()).Throws(new WebException()); - Subject.UpdateMappings(); + Subject.Execute(new UpdateSceneMappingCommand()); AssertNoUpdate(); @@ -56,7 +56,7 @@ namespace NzbDrone.Core.Test.DataAugmentationFixture.Scene Mocker.GetMock().Setup(c => c.Fetch()).Returns(new List()); - Subject.UpdateMappings(); + Subject.Execute(new UpdateSceneMappingCommand()); AssertNoUpdate(); diff --git a/NzbDrone.Core/DataAugmentation/Scene/SceneMappingService.cs b/NzbDrone.Core/DataAugmentation/Scene/SceneMappingService.cs index f7f547369..7198d8392 100644 --- a/NzbDrone.Core/DataAugmentation/Scene/SceneMappingService.cs +++ b/NzbDrone.Core/DataAugmentation/Scene/SceneMappingService.cs @@ -3,60 +3,33 @@ using System.Linq; using NLog; using NzbDrone.Common.Messaging; using NzbDrone.Core.Lifecycle; -using NzbDrone.Core.Tv; namespace NzbDrone.Core.DataAugmentation.Scene { public interface ISceneMappingService { - void UpdateMappings(); - string GetSceneName(int seriesId, int seasonNumber = -1); + string GetSceneName(int tvdbId, int seasonNumber = -1); Nullable GetTvDbId(string cleanName); - string GetCleanName(int tvdbId); } - public class SceneMappingService : ISceneMappingService,IHandleAsync + public class SceneMappingService : ISceneMappingService, + IHandleAsync, + IExecute { private readonly ISceneMappingRepository _repository; private readonly ISceneMappingProxy _sceneMappingProxy; - private readonly ISeriesService _seriesService; private readonly Logger _logger; - public SceneMappingService(ISceneMappingRepository repository, ISceneMappingProxy sceneMappingProxy, ISeriesService seriesService, Logger logger) + public SceneMappingService(ISceneMappingRepository repository, ISceneMappingProxy sceneMappingProxy, Logger logger) { _repository = repository; _sceneMappingProxy = sceneMappingProxy; - _seriesService = seriesService; _logger = logger; } - public void UpdateMappings() + public string GetSceneName(int tvdbId, int seasonNumber = -1) { - try - { - var mappings = _sceneMappingProxy.Fetch(); - - if (mappings.Any()) - { - _repository.Purge(); - _repository.InsertMany(mappings); - } - else - { - _logger.Warn("Received empty list of mapping. will not update."); - } - } - catch (Exception ex) - { - _logger.ErrorException("Failed to Update Scene Mappings:", ex); - } - } - - public string GetSceneName(int seriesId, int seasonNumber = -1) - { - var tvDbId = _seriesService.FindByTvdbId(seriesId).TvdbId; - - var mapping = _repository.FindByTvdbId(tvDbId); + var mapping = _repository.FindByTvdbId(tvdbId); if (mapping == null) return null; @@ -76,15 +49,6 @@ namespace NzbDrone.Core.DataAugmentation.Scene } - public string GetCleanName(int tvdbId) - { - var mapping = _repository.FindByTvdbId(tvdbId); - - if (mapping == null) return null; - - return mapping.CleanTitle; - } - public void HandleAsync(ApplicationStartedEvent message) { if (!_repository.HasItems()) @@ -92,5 +56,32 @@ namespace NzbDrone.Core.DataAugmentation.Scene UpdateMappings(); } } + + private void UpdateMappings() + { + try + { + var mappings = _sceneMappingProxy.Fetch(); + + if (mappings.Any()) + { + _repository.Purge(); + _repository.InsertMany(mappings); + } + else + { + _logger.Warn("Received empty list of mapping. will not update."); + } + } + catch (Exception ex) + { + _logger.ErrorException("Failed to Update Scene Mappings:", ex); + } + } + + public void Execute(UpdateSceneMappingCommand message) + { + UpdateMappings(); + } } } diff --git a/NzbDrone.Core/DataAugmentation/Scene/UpdateSceneMappingCommand.cs b/NzbDrone.Core/DataAugmentation/Scene/UpdateSceneMappingCommand.cs new file mode 100644 index 000000000..68356ab6e --- /dev/null +++ b/NzbDrone.Core/DataAugmentation/Scene/UpdateSceneMappingCommand.cs @@ -0,0 +1,8 @@ +using NzbDrone.Common.Messaging; + +namespace NzbDrone.Core.DataAugmentation.Scene +{ + public class UpdateSceneMappingCommand : ICommand + { + } +} \ No newline at end of file diff --git a/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs b/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs index 61bc581fc..8a86ed28c 100644 --- a/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs +++ b/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using NLog; using NzbDrone.Core.DecisionEngine.Specifications.Search; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser; @@ -18,38 +19,26 @@ namespace NzbDrone.Core.DecisionEngine { private readonly IEnumerable _specifications; private readonly IParsingService _parsingService; + private readonly Logger _logger; - public DownloadDecisionMaker(IEnumerable specifications, IParsingService parsingService) + public DownloadDecisionMaker(IEnumerable specifications, IParsingService parsingService, Logger logger) { _specifications = specifications; _parsingService = parsingService; + _logger = logger; } public List GetRssDecision(IEnumerable reports) { - return GetDecisions(reports, GetGeneralRejectionReasons).ToList(); + return GetDecisions(reports).ToList(); } public List GetSearchDecision(IEnumerable reports, SearchDefinitionBase searchDefinitionBase) { - return GetDecisions(reports, remoteEpisode => - { - var generalReasons = GetGeneralRejectionReasons(remoteEpisode); - var searchReasons = GetSearchRejectionReasons(remoteEpisode, searchDefinitionBase); - return generalReasons.Union(searchReasons); - }).ToList(); - } - - - private IEnumerable GetGeneralRejectionReasons(RemoteEpisode report) - { - return _specifications - .OfType() - .Where(spec => !spec.IsSatisfiedBy(report)) - .Select(spec => spec.RejectionReason); + return GetDecisions(reports).ToList(); } - private IEnumerable GetDecisions(IEnumerable reports, Func> decisionCallback) + private IEnumerable GetDecisions(IEnumerable reports, SearchDefinitionBase searchDefinition = null) { foreach (var report in reports) { @@ -62,7 +51,7 @@ namespace NzbDrone.Core.DecisionEngine if (remoteEpisode.Series != null) { - yield return new DownloadDecision(remoteEpisode, decisionCallback(remoteEpisode).ToArray()); + yield return GetDecisionForReport(remoteEpisode, searchDefinition); } else { @@ -72,12 +61,40 @@ namespace NzbDrone.Core.DecisionEngine } } - private IEnumerable GetSearchRejectionReasons(RemoteEpisode report, SearchDefinitionBase searchDefinitionBase) + private DownloadDecision GetDecisionForReport(RemoteEpisode remoteEpisode, SearchDefinitionBase searchDefinition = null) { - return _specifications - .OfType() - .Where(spec => !spec.IsSatisfiedBy(report, searchDefinitionBase)) - .Select(spec => spec.RejectionReason); + var reasons = _specifications.Select(c => EvaluateSpec(c, remoteEpisode, searchDefinition)) + .Where(c => !string.IsNullOrWhiteSpace(c)); + + return new DownloadDecision(remoteEpisode, reasons.ToArray()); + } + + private string EvaluateSpec(IRejectWithReason spec, RemoteEpisode remoteEpisode, SearchDefinitionBase searchDefinitionBase = null) + { + try + { + var searchSpecification = spec as IDecisionEngineSearchSpecification; + if (searchSpecification != null && searchDefinitionBase != null) + { + if (!searchSpecification.IsSatisfiedBy(remoteEpisode, searchDefinitionBase)) + { + return spec.RejectionReason; + } + } + + var generalSpecification = spec as IDecisionEngineSpecification; + if (generalSpecification != null && !generalSpecification.IsSatisfiedBy(remoteEpisode)) + { + return spec.RejectionReason; + } + } + catch (Exception e) + { + _logger.ErrorException("Couldn't evaluate decision", e); + return string.Format("{0}: {1}", spec.GetType().Name, e.Message); + } + + return null; } } } \ No newline at end of file diff --git a/NzbDrone.Core/DecisionEngine/Specifications/Search/SingleEpisodeMatchSpecification.cs b/NzbDrone.Core/DecisionEngine/Specifications/Search/SingleEpisodeMatchSpecification.cs new file mode 100644 index 000000000..2a8495492 --- /dev/null +++ b/NzbDrone.Core/DecisionEngine/Specifications/Search/SingleEpisodeMatchSpecification.cs @@ -0,0 +1,45 @@ +using System.Linq; +using NLog; +using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.DecisionEngine.Specifications.Search +{ + public class SingleEpisodeMatchSpecification : IDecisionEngineSpecification + { + private readonly Logger _logger; + + public SingleEpisodeMatchSpecification(Logger logger) + { + _logger = logger; + } + + public string RejectionReason + { + get + { + return "Episode doesn't match"; + } + } + + public bool IsSatisfiedBy(RemoteEpisode remoteEpisode, SearchDefinitionBase searchDefinitionBase) + { + var singleEpisodeSpec = searchDefinitionBase as SingleEpisodeSearchDefinition; + if (singleEpisodeSpec == null) return true; + + if (singleEpisodeSpec.SeasonNumber != remoteEpisode.ParsedEpisodeInfo.SeasonNumber) + { + _logger.Trace("Season number does not match searched season number, skipping."); + return false; + } + + if (!remoteEpisode.Episodes.Select(c => c.EpisodeNumber).Contains(singleEpisodeSpec.EpisodeNumber)) + { + _logger.Trace("Episode number does not match searched episode number, skipping."); + return false; + } + + return true; + } + } +} \ No newline at end of file diff --git a/NzbDrone.Core/IndexerSearch/NzbSearchService.cs b/NzbDrone.Core/IndexerSearch/NzbSearchService.cs index c952d46d2..f69c0b39b 100644 --- a/NzbDrone.Core/IndexerSearch/NzbSearchService.cs +++ b/NzbDrone.Core/IndexerSearch/NzbSearchService.cs @@ -101,8 +101,10 @@ namespace NzbDrone.Core.IndexerSearch { var spec = new TSpec(); + var tvdbId = _seriesService.GetSeries(seriesId).TvdbId; + spec.SeriesId = seriesId; - spec.SceneTitle = _sceneMapping.GetSceneName(seriesId, seasonNumber); + spec.SceneTitle = _sceneMapping.GetSceneName(tvdbId, seasonNumber); return spec; } diff --git a/NzbDrone.Core/Jobs/Implementations/UpdateSceneMappingsJob.cs b/NzbDrone.Core/Jobs/Implementations/UpdateSceneMappingsJob.cs deleted file mode 100644 index b043a18a6..000000000 --- a/NzbDrone.Core/Jobs/Implementations/UpdateSceneMappingsJob.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Linq; -using NzbDrone.Core.DataAugmentation; -using NzbDrone.Core.DataAugmentation.Scene; -using NzbDrone.Core.Model.Notification; - -namespace NzbDrone.Core.Jobs.Implementations -{ - public class UpdateSceneMappingsJob : IJob - { - private readonly SceneMappingService _sceneNameMappingService; - - public UpdateSceneMappingsJob(SceneMappingService sceneNameMappingService) - { - _sceneNameMappingService = sceneNameMappingService; - } - - public string Name - { - get { return "Update Scene Mappings"; } - } - - public TimeSpan DefaultInterval - { - get { return TimeSpan.FromHours(6); } - } - - public virtual void Start(ProgressNotification notification, dynamic options) - { - _sceneNameMappingService.UpdateMappings(); - } - } -} \ No newline at end of file diff --git a/NzbDrone.Core/NzbDrone.Core.csproj b/NzbDrone.Core/NzbDrone.Core.csproj index 2f5a5f0ec..b7fd0c76a 100644 --- a/NzbDrone.Core/NzbDrone.Core.csproj +++ b/NzbDrone.Core/NzbDrone.Core.csproj @@ -186,6 +186,7 @@ + @@ -295,7 +296,6 @@ - @@ -394,6 +394,7 @@ + diff --git a/NzbDrone.Core/Tv/Series.cs b/NzbDrone.Core/Tv/Series.cs index 3f5231687..56780427c 100644 --- a/NzbDrone.Core/Tv/Series.cs +++ b/NzbDrone.Core/Tv/Series.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using Marr.Data; using NzbDrone.Core.Datastore; -using NzbDrone.Core.MetadataSource.Trakt; using NzbDrone.Core.Model; using NzbDrone.Core.Qualities; using NzbDrone.Core.RootFolders; @@ -10,13 +9,6 @@ using NzbDrone.Core.RootFolders; namespace NzbDrone.Core.Tv { - public enum SeriesTypes - { - Standard = 0, - Daily = 1, - Anime = 2, - } - public class Series : ModelBase { public Series() diff --git a/NzbDrone.Core/Tv/SeriesRepository.cs b/NzbDrone.Core/Tv/SeriesRepository.cs index 1ca5c2d56..9d17552d2 100644 --- a/NzbDrone.Core/Tv/SeriesRepository.cs +++ b/NzbDrone.Core/Tv/SeriesRepository.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using NzbDrone.Core.Datastore; -using NzbDrone.Core.SeriesStats; namespace NzbDrone.Core.Tv { @@ -13,7 +12,6 @@ namespace NzbDrone.Core.Tv Series FindByTitle(string cleanTitle); Series FindByTvdbId(int tvdbId); void SetSeriesType(int seriesId, SeriesTypes seriesTypes); - void SetTvRageId(int seriesId, int tvRageId); } public class SeriesRepository : BasicRepository, ISeriesRepository @@ -48,9 +46,5 @@ namespace NzbDrone.Core.Tv SetFields(new Series { Id = seriesId, SeriesType = seriesType }, s => s.SeriesType); } - public void SetTvRageId(int seriesId, int tvRageId) - { - SetFields(new Series { Id = seriesId, TvRageId = tvRageId }, s => s.TvRageId); - } } } \ No newline at end of file diff --git a/NzbDrone.Core/Tv/SeriesService.cs b/NzbDrone.Core/Tv/SeriesService.cs index cd1d780b5..f51401ca3 100644 --- a/NzbDrone.Core/Tv/SeriesService.cs +++ b/NzbDrone.Core/Tv/SeriesService.cs @@ -8,6 +8,7 @@ using NzbDrone.Common; using NzbDrone.Common.EnsureThat; using NzbDrone.Common.Messaging; using NzbDrone.Core.Configuration; +using NzbDrone.Core.DataAugmentation.Scene; using NzbDrone.Core.Datastore; using NzbDrone.Core.MetadataSource; using NzbDrone.Core.Model; @@ -42,18 +43,20 @@ namespace NzbDrone.Core.Tv private readonly IConfigService _configService; private readonly IProvideSeriesInfo _seriesInfoProxy; private readonly IMessageAggregator _messageAggregator; + private readonly ISceneMappingService _sceneMappingService; private readonly IRootFolderService _rootFolderService; private readonly DiskProvider _diskProvider; private readonly Logger _logger; public SeriesService(ISeriesRepository seriesRepository, IConfigService configServiceService, - IProvideSeriesInfo seriesInfoProxy, IMessageAggregator messageAggregator, + IProvideSeriesInfo seriesInfoProxy, IMessageAggregator messageAggregator, ISceneMappingService sceneMappingService, IRootFolderService rootFolderService, DiskProvider diskProvider, Logger logger) { _seriesRepository = seriesRepository; _configService = configServiceService; _seriesInfoProxy = seriesInfoProxy; _messageAggregator = messageAggregator; + _sceneMappingService = sceneMappingService; _rootFolderService = rootFolderService; _diskProvider = diskProvider; _logger = logger; @@ -146,6 +149,13 @@ namespace NzbDrone.Core.Tv public Series FindByTitle(string title) { + var tvdbId = _sceneMappingService.GetTvDbId(title); + + if (tvdbId.HasValue) + { + return FindByTvdbId(tvdbId.Value); + } + return _seriesRepository.FindByTitle(Parser.Parser.NormalizeTitle(title)); } diff --git a/NzbDrone.Core/Tv/SeriesTypes.cs b/NzbDrone.Core/Tv/SeriesTypes.cs new file mode 100644 index 000000000..176ff7655 --- /dev/null +++ b/NzbDrone.Core/Tv/SeriesTypes.cs @@ -0,0 +1,9 @@ +namespace NzbDrone.Core.Tv +{ + public enum SeriesTypes + { + Standard = 0, + Daily = 1, + Anime = 2, + } +} \ No newline at end of file diff --git a/NzbDrone.Integration.Test/ReleaseIntegrationTest.cs b/NzbDrone.Integration.Test/ReleaseIntegrationTest.cs index df01d6be6..4c8b16103 100644 --- a/NzbDrone.Integration.Test/ReleaseIntegrationTest.cs +++ b/NzbDrone.Integration.Test/ReleaseIntegrationTest.cs @@ -1,5 +1,6 @@ using FluentAssertions; using NUnit.Framework; +using NzbDrone.Api.Indexers; namespace NzbDrone.Integration.Test { @@ -9,9 +10,25 @@ namespace NzbDrone.Integration.Test [Test] public void should_only_have_unknown_series_releases() { - Releases.All().Should().OnlyContain(c => c.Rejections.Contains("Unknown Series")); + var releases = Releases.All(); + + releases.Should().OnlyContain(c => c.Rejections.Contains("Unknown Series")); + releases.Should().OnlyContain(c=>BeValidRelease(c)); } + + private bool BeValidRelease(ReleaseResource releaseResource) + { + releaseResource.Age.Should().BeGreaterOrEqualTo(-1); + releaseResource.Title.Should().NotBeBlank(); + releaseResource.NzbInfoUrl.Should().NotBeBlank(); + releaseResource.NzbUrl.Should().NotBeBlank(); + releaseResource.SeriesTitle.Should().NotBeBlank(); + releaseResource.Size.Should().BeGreaterThan(0); + + return true; + } + } } \ No newline at end of file