From 8a2a41fab0af4314872dfd25da9db07d7b66cdee Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Sat, 19 Oct 2019 17:15:28 +0200 Subject: [PATCH] New: Added health check warning to emphasis when a series was deleted instead of only logging it in System Events --- .../Checks/RemovedSeriesCheckFixture.cs | 77 +++++++++++++++++++ .../TvTests/RefreshSeriesServiceFixture.cs | 24 ++++++ .../HealthCheck/CheckOnAttribute.cs | 2 +- .../HealthCheck/Checks/RemovedSeriesCheck.cs | 53 +++++++++++++ .../HealthCheck/EventDrivenHealthCheck.cs | 34 +++++++- .../HealthCheck/HealthCheckService.cs | 32 ++++---- .../HealthCheck/ICheckOnCondition.cs | 7 ++ src/NzbDrone.Core/Tv/RefreshSeriesService.cs | 23 +++++- src/NzbDrone.Core/Tv/SeriesStatusType.cs | 4 +- 9 files changed, 231 insertions(+), 25 deletions(-) create mode 100644 src/NzbDrone.Core.Test/HealthCheck/Checks/RemovedSeriesCheckFixture.cs create mode 100644 src/NzbDrone.Core/HealthCheck/Checks/RemovedSeriesCheck.cs create mode 100644 src/NzbDrone.Core/HealthCheck/ICheckOnCondition.cs diff --git a/src/NzbDrone.Core.Test/HealthCheck/Checks/RemovedSeriesCheckFixture.cs b/src/NzbDrone.Core.Test/HealthCheck/Checks/RemovedSeriesCheckFixture.cs new file mode 100644 index 000000000..958c6ae5a --- /dev/null +++ b/src/NzbDrone.Core.Test/HealthCheck/Checks/RemovedSeriesCheckFixture.cs @@ -0,0 +1,77 @@ +using System.Collections.Generic; +using FizzWare.NBuilder; +using NUnit.Framework; +using NzbDrone.Core.HealthCheck.Checks; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Test.HealthCheck.Checks +{ + [TestFixture] + public class RemovedSeriesCheckFixture : CoreTest + { + private void GivenSeries(int amount, int deleted) + { + List series; + + if (amount == 0) + { + series = new List(); + } + else if (deleted == 0) + { + series = Builder.CreateListOfSize(amount) + .All() + .With(v => v.Status = SeriesStatusType.Continuing) + .BuildList(); + } + else + { + series = Builder.CreateListOfSize(amount) + .All() + .With(v => v.Status = SeriesStatusType.Continuing) + .Random(deleted) + .With(v => v.Status = SeriesStatusType.Deleted) + .BuildList(); + } + + Mocker.GetMock() + .Setup(v => v.GetAllSeries()) + .Returns(series); + } + + + + [Test] + public void should_return_error_if_series_no_longer_on_tvdb() + { + GivenSeries(4, 1); + + Subject.Check().ShouldBeError(); + } + + [Test] + public void should_return_error_if_multiple_series_no_longer_on_tvdb() + { + GivenSeries(4, 2); + + Subject.Check().ShouldBeError(); + } + + [Test] + public void should_return_ok_if_all_series_still_on_tvdb() + { + GivenSeries(4, 0); + + Subject.Check().ShouldBeOk(); + } + + [Test] + public void should_return_ok_if_no_series_exist() + { + GivenSeries(0, 0); + + Subject.Check().ShouldBeOk(); + } + } +} diff --git a/src/NzbDrone.Core.Test/TvTests/RefreshSeriesServiceFixture.cs b/src/NzbDrone.Core.Test/TvTests/RefreshSeriesServiceFixture.cs index 31c402927..dd7eb8e9a 100644 --- a/src/NzbDrone.Core.Test/TvTests/RefreshSeriesServiceFixture.cs +++ b/src/NzbDrone.Core.Test/TvTests/RefreshSeriesServiceFixture.cs @@ -123,6 +123,30 @@ namespace NzbDrone.Core.Test.TvTests ExceptionVerification.ExpectedErrors(1); } + [Test] + public void should_mark_as_deleted_if_tvdb_id_not_found() + { + Subject.Execute(new RefreshSeriesCommand(_series.Id)); + + Mocker.GetMock() + .Verify(v => v.UpdateSeries(It.Is(s => s.Status == SeriesStatusType.Deleted), It.IsAny()), Times.Once()); + + ExceptionVerification.ExpectedErrors(1); + } + + [Test] + public void should_not_remark_as_deleted_if_tvdb_id_not_found() + { + _series.Status = SeriesStatusType.Deleted; + + Subject.Execute(new RefreshSeriesCommand(_series.Id)); + + Mocker.GetMock() + .Verify(v => v.UpdateSeries(It.IsAny(), It.IsAny()), Times.Never()); + + ExceptionVerification.ExpectedErrors(1); + } + [Test] public void should_update_if_tvdb_id_changed() { diff --git a/src/NzbDrone.Core/HealthCheck/CheckOnAttribute.cs b/src/NzbDrone.Core/HealthCheck/CheckOnAttribute.cs index 7e7b9d259..dd1dcb3be 100644 --- a/src/NzbDrone.Core/HealthCheck/CheckOnAttribute.cs +++ b/src/NzbDrone.Core/HealthCheck/CheckOnAttribute.cs @@ -3,7 +3,7 @@ using System; namespace NzbDrone.Core.HealthCheck { [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] - public class CheckOnAttribute: Attribute + public class CheckOnAttribute : Attribute { public Type EventType { get; set; } public CheckOnCondition Condition { get; set; } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/RemovedSeriesCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/RemovedSeriesCheck.cs new file mode 100644 index 000000000..9376ce9ca --- /dev/null +++ b/src/NzbDrone.Core/HealthCheck/Checks/RemovedSeriesCheck.cs @@ -0,0 +1,53 @@ +using System.Linq; +using System.Text; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Tv; +using NzbDrone.Core.Tv.Events; + +namespace NzbDrone.Core.HealthCheck.Checks +{ + [CheckOn(typeof(SeriesUpdatedEvent))] + [CheckOn(typeof(SeriesDeletedEvent), CheckOnCondition.FailedOnly)] + public class RemovedSeriesCheck : HealthCheckBase, ICheckOnCondition, ICheckOnCondition + { + private readonly ISeriesService _seriesService; + private readonly Logger _logger; + + public RemovedSeriesCheck(ISeriesService seriesService, Logger logger) + { + _seriesService = seriesService; + _logger = logger; + } + + public override HealthCheck Check() + { + var deletedSeries = _seriesService.GetAllSeries().Where(v => v.Status == SeriesStatusType.Deleted).ToList(); + + if (deletedSeries.Empty()) + { + return new HealthCheck(GetType()); + } + + var seriesText = deletedSeries.Select(s => $"{s.Title} (tvdbid {s.TvdbId})").Join(", "); + + if (deletedSeries.Count() == 1) + { + + return new HealthCheck(GetType(), HealthCheckResult.Error, $"Series {seriesText} was removed from TheTVDB"); + } + + return new HealthCheck(GetType(), HealthCheckResult.Error, $"Series {seriesText} were removed from TheTVDB"); + } + + public bool ShouldCheckOnEvent(SeriesDeletedEvent deletedEvent) + { + return deletedEvent.Series.Status == SeriesStatusType.Deleted; + } + + public bool ShouldCheckOnEvent(SeriesUpdatedEvent updatedEvent) + { + return updatedEvent.Series.Status == SeriesStatusType.Deleted; + } + } +} diff --git a/src/NzbDrone.Core/HealthCheck/EventDrivenHealthCheck.cs b/src/NzbDrone.Core/HealthCheck/EventDrivenHealthCheck.cs index 0b55c1ff2..4f7e8cabe 100644 --- a/src/NzbDrone.Core/HealthCheck/EventDrivenHealthCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/EventDrivenHealthCheck.cs @@ -1,14 +1,46 @@ +using System; +using NzbDrone.Common.Messaging; + namespace NzbDrone.Core.HealthCheck { - public class EventDrivenHealthCheck + public interface IEventDrivenHealthCheck + { + IProvideHealthCheck HealthCheck { get; } + + bool ShouldExecute(IEvent message, bool previouslyFailed); + } + + public class EventDrivenHealthCheck : IEventDrivenHealthCheck { public IProvideHealthCheck HealthCheck { get; set; } public CheckOnCondition Condition { get; set; } + public ICheckOnCondition EventFilter { get; set; } public EventDrivenHealthCheck(IProvideHealthCheck healthCheck, CheckOnCondition condition) { HealthCheck = healthCheck; Condition = condition; + EventFilter = healthCheck as ICheckOnCondition; + } + + public bool ShouldExecute(IEvent message, bool previouslyFailed) + { + if (Condition == CheckOnCondition.SuccessfulOnly && previouslyFailed) + { + return false; + } + + if (Condition == CheckOnCondition.FailedOnly && !previouslyFailed) + { + return false; + } + + if (EventFilter == null && !EventFilter.ShouldCheckOnEvent((TEvent)message)) + { + return false; + } + + return true; } } } diff --git a/src/NzbDrone.Core/HealthCheck/HealthCheckService.cs b/src/NzbDrone.Core/HealthCheck/HealthCheckService.cs index f9c35d515..72df70d72 100644 --- a/src/NzbDrone.Core/HealthCheck/HealthCheckService.cs +++ b/src/NzbDrone.Core/HealthCheck/HealthCheckService.cs @@ -29,7 +29,7 @@ namespace NzbDrone.Core.HealthCheck private readonly IProvideHealthCheck[] _healthChecks; private readonly IProvideHealthCheck[] _startupHealthChecks; private readonly IProvideHealthCheck[] _scheduledHealthChecks; - private readonly Dictionary _eventDrivenHealthChecks; + private readonly Dictionary _eventDrivenHealthChecks; private readonly IEventAggregator _eventAggregator; private readonly ICacheManager _cacheManager; private readonly Logger _logger; @@ -58,10 +58,16 @@ namespace NzbDrone.Core.HealthCheck return _healthCheckResults.Values.ToList(); } - private Dictionary GetEventDrivenHealthChecks() + private Dictionary GetEventDrivenHealthChecks() { return _healthChecks - .SelectMany(h => h.GetType().GetAttributes().Select(a => Tuple.Create(a.EventType, new EventDrivenHealthCheck(h, a.Condition)))) + .SelectMany(h => h.GetType().GetAttributes().Select(a => + { + var eventDrivenType = typeof(EventDrivenHealthCheck<>).MakeGenericType(a.EventType); + var eventDriven = (IEventDrivenHealthCheck)Activator.CreateInstance(eventDrivenType, h, a.Condition); + + return Tuple.Create(a.EventType, eventDriven); + })) .GroupBy(t => t.Item1, t => t.Item2) .ToDictionary(g => g.Key, g => g.ToArray()); } @@ -86,7 +92,7 @@ namespace NzbDrone.Core.HealthCheck _eventAggregator.PublishEvent(new HealthCheckCompleteEvent()); } - + public void Execute(CheckHealthCommand message) { if (message.Trigger == CommandTrigger.Manual) @@ -111,7 +117,7 @@ namespace NzbDrone.Core.HealthCheck return; } - EventDrivenHealthCheck[] checks; + IEventDrivenHealthCheck[] checks; if (!_eventDrivenHealthChecks.TryGetValue(message.GetType(), out checks)) { return; @@ -122,26 +128,14 @@ namespace NzbDrone.Core.HealthCheck foreach (var eventDrivenHealthCheck in checks) { - if (eventDrivenHealthCheck.Condition == CheckOnCondition.Always) - { - filteredChecks.Add(eventDrivenHealthCheck.HealthCheck); - continue; - } - var healthCheckType = eventDrivenHealthCheck.HealthCheck.GetType(); + var previouslyFailed = healthCheckResults.Any(r => r.Source == healthCheckType); - if (eventDrivenHealthCheck.Condition == CheckOnCondition.FailedOnly && - healthCheckResults.Any(r => r.Source == healthCheckType)) + if (eventDrivenHealthCheck.ShouldExecute(message, previouslyFailed)) { filteredChecks.Add(eventDrivenHealthCheck.HealthCheck); continue; } - - if (eventDrivenHealthCheck.Condition == CheckOnCondition.SuccessfulOnly && - healthCheckResults.None(r => r.Source == healthCheckType)) - { - filteredChecks.Add(eventDrivenHealthCheck.HealthCheck); - } } diff --git a/src/NzbDrone.Core/HealthCheck/ICheckOnCondition.cs b/src/NzbDrone.Core/HealthCheck/ICheckOnCondition.cs new file mode 100644 index 000000000..fcd761dac --- /dev/null +++ b/src/NzbDrone.Core/HealthCheck/ICheckOnCondition.cs @@ -0,0 +1,7 @@ +namespace NzbDrone.Core.HealthCheck +{ + public interface ICheckOnCondition + { + bool ShouldCheckOnEvent(TEvent message); + } +} diff --git a/src/NzbDrone.Core/Tv/RefreshSeriesService.cs b/src/NzbDrone.Core/Tv/RefreshSeriesService.cs index 37ef69072..b2ad274c8 100644 --- a/src/NzbDrone.Core/Tv/RefreshSeriesService.cs +++ b/src/NzbDrone.Core/Tv/RefreshSeriesService.cs @@ -54,9 +54,26 @@ namespace NzbDrone.Core.Tv { _logger.ProgressInfo("Updating {0}", series.Title); - var tuple = _seriesInfo.GetSeriesInfo(series.TvdbId); + Series seriesInfo; + List episodes; - var seriesInfo = tuple.Item1; + try + { + var tuple = _seriesInfo.GetSeriesInfo(series.TvdbId); + seriesInfo = tuple.Item1; + episodes = tuple.Item2; + } + catch (SeriesNotFoundException) + { + if (series.Status != SeriesStatusType.Deleted) + { + series.Status = SeriesStatusType.Deleted; + _seriesService.UpdateSeries(series); + _logger.Debug("Series marked as deleted on tvdb for {0}", series.Title); + _eventAggregator.PublishEvent(new SeriesUpdatedEvent(series)); + } + throw; + } if (series.TvdbId != seriesInfo.TvdbId) { @@ -102,7 +119,7 @@ namespace NzbDrone.Core.Tv series.Seasons = UpdateSeasons(series, seriesInfo); _seriesService.UpdateSeries(series); - _refreshEpisodeService.RefreshEpisodeInfo(series, tuple.Item2); + _refreshEpisodeService.RefreshEpisodeInfo(series, episodes); _logger.Debug("Finished series refresh for {0}", series.Title); _eventAggregator.PublishEvent(new SeriesUpdatedEvent(series)); diff --git a/src/NzbDrone.Core/Tv/SeriesStatusType.cs b/src/NzbDrone.Core/Tv/SeriesStatusType.cs index acc9fbf81..722b88ab3 100644 --- a/src/NzbDrone.Core/Tv/SeriesStatusType.cs +++ b/src/NzbDrone.Core/Tv/SeriesStatusType.cs @@ -2,7 +2,9 @@ { public enum SeriesStatusType { + Deleted = -1, Continuing = 0, - Ended = 1 + Ended = 1, + Upcoming = 2 } }