From 0c44ee5f8893a2c0cea1d9fcc862dc5777ddfb54 Mon Sep 17 00:00:00 2001 From: Qstick Date: Tue, 25 Feb 2020 23:18:49 -0500 Subject: [PATCH] New: HealthCheck and Status for Movies Deleted from TMDb --- frontend/src/Helpers/Props/icons.js | 1 + .../src/Movie/Index/Table/MovieIndexRow.js | 4 +- .../src/Movie/Index/Table/MovieStatusCell.js | 33 +--- .../{MovieStatus.css => MovieFileStatus.css} | 0 frontend/src/Movie/MovieFileStatus.js | 124 +++++++++++++++ ...nnector.js => MovieFileStatusConnector.js} | 10 +- frontend/src/Movie/MovieStatus.js | 144 ++++-------------- .../Checks/RemovedMovieCheckFixture.cs | 75 +++++++++ .../HealthCheck/HealthCheckServiceFixture.cs | 81 ++++++++++ .../MovieTests/RefreshMovieServiceFixture.cs | 60 ++++++-- .../Exceptions/MovieNotFoundExceptions.cs | 24 +-- .../HealthCheck/Checks/RemovedMovieCheck.cs | 48 ++++++ .../HealthCheck/EventDrivenHealthCheck.cs | 33 +++- .../HealthCheck/HealthCheckService.cs | 32 ++-- .../HealthCheck/ICheckOnCondition.cs | 7 + .../MetadataSource/SkyHook/SkyHookProxy.cs | 2 +- src/NzbDrone.Core/Movies/MovieStatusType.cs | 3 +- .../Movies/RefreshMovieService.cs | 26 +++- 18 files changed, 505 insertions(+), 202 deletions(-) rename frontend/src/Movie/{MovieStatus.css => MovieFileStatus.css} (100%) create mode 100644 frontend/src/Movie/MovieFileStatus.js rename frontend/src/Movie/{MovieStatusConnector.js => MovieFileStatusConnector.js} (83%) create mode 100644 src/NzbDrone.Core.Test/HealthCheck/Checks/RemovedMovieCheckFixture.cs create mode 100644 src/NzbDrone.Core.Test/HealthCheck/HealthCheckServiceFixture.cs create mode 100644 src/NzbDrone.Core/HealthCheck/Checks/RemovedMovieCheck.cs create mode 100644 src/NzbDrone.Core/HealthCheck/ICheckOnCondition.cs diff --git a/frontend/src/Helpers/Props/icons.js b/frontend/src/Helpers/Props/icons.js index fa7eb85c7..25e4c4627 100644 --- a/frontend/src/Helpers/Props/icons.js +++ b/frontend/src/Helpers/Props/icons.js @@ -190,6 +190,7 @@ export const SCORE = fasUserPlus; export const SEARCH = fasSearch; export const MOVIE_CONTINUING = fasPlay; export const SERIES_ENDED = fasStop; +export const MOVIE_DELETED = fasExclamationTriangle; export const SETTINGS = fasCogs; export const SHUTDOWN = fasPowerOff; export const SORT = fasSort; diff --git a/frontend/src/Movie/Index/Table/MovieIndexRow.js b/frontend/src/Movie/Index/Table/MovieIndexRow.js index 933d65302..5ecf54ebc 100644 --- a/frontend/src/Movie/Index/Table/MovieIndexRow.js +++ b/frontend/src/Movie/Index/Table/MovieIndexRow.js @@ -13,7 +13,7 @@ import MovieTitleLink from 'Movie/MovieTitleLink'; import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector'; import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal'; import MovieStatusCell from './MovieStatusCell'; -import MovieStatusConnector from 'Movie/MovieStatusConnector'; +import MovieFileStatusConnector from 'Movie/MovieFileStatusConnector'; import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell'; import styles from './MovieIndexRow.css'; @@ -278,7 +278,7 @@ class MovieIndexRow extends Component { key={name} className={styles[name]} > - diff --git a/frontend/src/Movie/Index/Table/MovieStatusCell.js b/frontend/src/Movie/Index/Table/MovieStatusCell.js index 7865c7d4f..ee5a2b9b0 100644 --- a/frontend/src/Movie/Index/Table/MovieStatusCell.js +++ b/frontend/src/Movie/Index/Table/MovieStatusCell.js @@ -3,6 +3,7 @@ import React from 'react'; import { icons } from 'Helpers/Props'; import Icon from 'Components/Icon'; import VirtualTableRowCell from 'Components/Table/Cells/TableRowCell'; +import { getMovieStatusDetails } from 'Movie/MovieStatus'; import styles from './MovieStatusCell.css'; function MovieStatusCell(props) { @@ -14,6 +15,8 @@ function MovieStatusCell(props) { ...otherProps } = props; + const statusDetails = getMovieStatusDetails(status); + return ( - { - status === 'announced' ? - : null - } + - { - status === 'inCinemas' ? - : null - } - - { - status === 'released' ? - : null - } ); } diff --git a/frontend/src/Movie/MovieStatus.css b/frontend/src/Movie/MovieFileStatus.css similarity index 100% rename from frontend/src/Movie/MovieStatus.css rename to frontend/src/Movie/MovieFileStatus.css diff --git a/frontend/src/Movie/MovieFileStatus.js b/frontend/src/Movie/MovieFileStatus.js new file mode 100644 index 000000000..310c77cb6 --- /dev/null +++ b/frontend/src/Movie/MovieFileStatus.js @@ -0,0 +1,124 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { icons, kinds, sizes } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import ProgressBar from 'Components/ProgressBar'; +import QueueDetails from 'Activity/Queue/QueueDetails'; +import MovieQuality from 'Movie/MovieQuality'; +import Label from 'Components/Label'; +import styles from './MovieFileStatus.css'; + +function MovieFileStatus(props) { + const { + inCinemas, + isAvailable, + monitored, + grabbed, + queueItem, + movieFile + } = props; + + const hasMovieFile = !!movieFile; + const isQueued = !!queueItem; + const hasReleased = isAvailable && inCinemas; + + if (isQueued) { + const { + sizeleft, + size + } = queueItem; + + const progress = (100 - sizeleft / size * 100); + + return ( +
+ + } + /> +
+ ); + } + + if (grabbed) { + return ( +
+ +
+ ); + } + + if (hasMovieFile) { + const quality = movieFile.quality; + + return ( +
+ +
+ ); + } + + if (!monitored) { + return ( +
+ +
+ ); + } + + if (hasReleased) { + return ( +
+ +
+ ); + } + + return ( +
+ +
+ ); +} + +MovieFileStatus.propTypes = { + inCinemas: PropTypes.string, + isAvailable: PropTypes.bool, + monitored: PropTypes.bool.isRequired, + grabbed: PropTypes.bool, + queueItem: PropTypes.object, + movieFile: PropTypes.object +}; + +export default MovieFileStatus; diff --git a/frontend/src/Movie/MovieStatusConnector.js b/frontend/src/Movie/MovieFileStatusConnector.js similarity index 83% rename from frontend/src/Movie/MovieStatusConnector.js rename to frontend/src/Movie/MovieFileStatusConnector.js index c3d721faa..4812f94b4 100644 --- a/frontend/src/Movie/MovieStatusConnector.js +++ b/frontend/src/Movie/MovieFileStatusConnector.js @@ -5,7 +5,7 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import createMovieSelector from 'Store/Selectors/createMovieSelector'; import createQueueItemSelector from 'Store/Selectors/createQueueItemSelector'; -import MovieStatus from './MovieStatus'; +import MovieFileStatus from './MovieFileStatus'; function createMapStateToProps() { return createSelector( @@ -30,22 +30,22 @@ function createMapStateToProps() { const mapDispatchToProps = { }; -class MovieStatusConnector extends Component { +class MovieFileStatusConnector extends Component { // // Render render() { return ( - ); } } -MovieStatusConnector.propTypes = { +MovieFileStatusConnector.propTypes = { movieId: PropTypes.number.isRequired }; -export default connect(createMapStateToProps, mapDispatchToProps)(MovieStatusConnector); +export default connect(createMapStateToProps, mapDispatchToProps)(MovieFileStatusConnector); diff --git a/frontend/src/Movie/MovieStatus.js b/frontend/src/Movie/MovieStatus.js index c7dfc39ab..3df5f695a 100644 --- a/frontend/src/Movie/MovieStatus.js +++ b/frontend/src/Movie/MovieStatus.js @@ -1,124 +1,32 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import { icons, kinds, sizes } from 'Helpers/Props'; -import Icon from 'Components/Icon'; -import ProgressBar from 'Components/ProgressBar'; -import QueueDetails from 'Activity/Queue/QueueDetails'; -import MovieQuality from 'Movie/MovieQuality'; -import Label from 'Components/Label'; -import styles from './MovieStatus.css'; +import { icons } from 'Helpers/Props'; -function MovieStatus(props) { - const { - inCinemas, - isAvailable, - monitored, - grabbed, - queueItem, - movieFile - } = props; +export function getMovieStatusDetails(status) { - const hasMovieFile = !!movieFile; - const isQueued = !!queueItem; - const hasReleased = isAvailable && inCinemas; + let statusDetails = { + icon: icons.ANNOUNCED, + title: 'Announced', + message: 'Movie is announced' + }; - if (isQueued) { - const { - sizeleft, - size - } = queueItem; - - const progress = (100 - sizeleft / size * 100); - - return ( -
- - } - /> -
- ); + if (status === 'deleted') { + statusDetails = { + icon: icons.MOVIE_DELETED, + title: 'Deleted', + message: 'Movie was deleted from TMDb' + }; + } else if (status === 'inCinemas') { + statusDetails = { + icon: icons.IN_CINEMAS, + title: 'In Cinemas', + message: 'Movie is in Cinemas' + }; + } else if (status === 'released') { + statusDetails = { + icon: icons.MOVIE_FILE, + title: 'Released', + message: 'Movie is released' + }; } - if (grabbed) { - return ( -
- -
- ); - } - - if (hasMovieFile) { - const quality = movieFile.quality; - - return ( -
- -
- ); - } - - if (!monitored) { - return ( -
- -
- ); - } - - if (hasReleased) { - return ( -
- -
- ); - } - - return ( -
- -
- ); + return statusDetails; } - -MovieStatus.propTypes = { - inCinemas: PropTypes.string, - isAvailable: PropTypes.bool, - monitored: PropTypes.bool.isRequired, - grabbed: PropTypes.bool, - queueItem: PropTypes.object, - movieFile: PropTypes.object -}; - -export default MovieStatus; diff --git a/src/NzbDrone.Core.Test/HealthCheck/Checks/RemovedMovieCheckFixture.cs b/src/NzbDrone.Core.Test/HealthCheck/Checks/RemovedMovieCheckFixture.cs new file mode 100644 index 000000000..887f3d99b --- /dev/null +++ b/src/NzbDrone.Core.Test/HealthCheck/Checks/RemovedMovieCheckFixture.cs @@ -0,0 +1,75 @@ +using System.Collections.Generic; +using FizzWare.NBuilder; +using NUnit.Framework; +using NzbDrone.Core.HealthCheck.Checks; +using NzbDrone.Core.Movies; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.HealthCheck.Checks +{ + [TestFixture] + public class RemovedSeriesCheckFixture : CoreTest + { + private void GivenMovie(int amount, int deleted) + { + List movie; + + if (amount == 0) + { + movie = new List(); + } + else if (deleted == 0) + { + movie = Builder.CreateListOfSize(amount) + .All() + .With(v => v.Status = MovieStatusType.Released) + .BuildList(); + } + else + { + movie = Builder.CreateListOfSize(amount) + .All() + .With(v => v.Status = MovieStatusType.Released) + .Random(deleted) + .With(v => v.Status = MovieStatusType.Deleted) + .BuildList(); + } + + Mocker.GetMock() + .Setup(v => v.GetAllMovies()) + .Returns(movie); + } + + [Test] + public void should_return_error_if_movie_no_longer_on_tmdb() + { + GivenMovie(4, 1); + + Subject.Check().ShouldBeError(); + } + + [Test] + public void should_return_error_if_multiple_movie_no_longer_on_tmdb() + { + GivenMovie(4, 2); + + Subject.Check().ShouldBeError(); + } + + [Test] + public void should_return_ok_if_all_movie_still_on_tmdb() + { + GivenMovie(4, 0); + + Subject.Check().ShouldBeOk(); + } + + [Test] + public void should_return_ok_if_no_movie_exist() + { + GivenMovie(0, 0); + + Subject.Check().ShouldBeOk(); + } + } +} diff --git a/src/NzbDrone.Core.Test/HealthCheck/HealthCheckServiceFixture.cs b/src/NzbDrone.Core.Test/HealthCheck/HealthCheckServiceFixture.cs new file mode 100644 index 000000000..fcf9d58c8 --- /dev/null +++ b/src/NzbDrone.Core.Test/HealthCheck/HealthCheckServiceFixture.cs @@ -0,0 +1,81 @@ +using System.Collections.Generic; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Common.Cache; +using NzbDrone.Common.Messaging; +using NzbDrone.Core.HealthCheck; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.HealthCheck +{ + public class HealthCheckServiceFixture : CoreTest + { + private FakeHealthCheck _healthCheck; + + [SetUp] + public void SetUp() + { + _healthCheck = new FakeHealthCheck(); + + Mocker.SetConstant>(new[] { _healthCheck }); + Mocker.SetConstant(Mocker.Resolve()); + } + + [Test] + public void should_not_execute_conditional() + { + Subject.HandleAsync(new FakeEvent()); + + _healthCheck.Executed.Should().BeFalse(); + } + + [Test] + public void should_execute_conditional() + { + Subject.HandleAsync(new FakeEvent() { ShouldExecute = true }); + + _healthCheck.Executed.Should().BeTrue(); + } + + [Test] + public void should_execute_unconditional() + { + Subject.HandleAsync(new FakeEvent2()); + + _healthCheck.Executed.Should().BeTrue(); + } + } + + public class FakeEvent : IEvent + { + public bool ShouldExecute { get; set; } + } + + public class FakeEvent2 : IEvent + { + public bool ShouldExecute { get; set; } + } + + [CheckOn(typeof(FakeEvent))] + [CheckOn(typeof(FakeEvent2))] + public class FakeHealthCheck : IProvideHealthCheck, ICheckOnCondition + { + public bool CheckOnStartup => false; + public bool CheckOnSchedule => false; + + public bool Executed { get; set; } + public bool Checked { get; set; } + + public Core.HealthCheck.HealthCheck Check() + { + Executed = true; + + return new Core.HealthCheck.HealthCheck(GetType()); + } + + public bool ShouldCheckOnEvent(FakeEvent message) + { + return message.ShouldExecute; + } + } +} diff --git a/src/NzbDrone.Core.Test/MovieTests/RefreshMovieServiceFixture.cs b/src/NzbDrone.Core.Test/MovieTests/RefreshMovieServiceFixture.cs index c5cc267e8..adcd01c3f 100644 --- a/src/NzbDrone.Core.Test/MovieTests/RefreshMovieServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MovieTests/RefreshMovieServiceFixture.cs @@ -1,3 +1,6 @@ +using System; +using System.Collections.Generic; +using System.Linq; using FizzWare.NBuilder; using Moq; using NUnit.Framework; @@ -6,6 +9,7 @@ using NzbDrone.Core.MetadataSource; using NzbDrone.Core.Movies; using NzbDrone.Core.Movies.Commands; +using NzbDrone.Core.Movies.Credits; using NzbDrone.Core.Profiles; using NzbDrone.Core.Test.Framework; using NzbDrone.Test.Common; @@ -13,7 +17,6 @@ namespace NzbDrone.Core.Test.MovieTests { [TestFixture] - [Ignore("Weird moq errors")] public class RefreshMovieServiceFixture : CoreTest { private Movie _movie; @@ -22,36 +25,37 @@ public class RefreshMovieServiceFixture : CoreTest public void Setup() { _movie = Builder.CreateNew() - .Build(); + .With(s => s.Status = MovieStatusType.Released) + .Build(); Mocker.GetMock() .Setup(s => s.GetMovie(_movie.Id)) .Returns(_movie); Mocker.GetMock() - .Setup(s => s.GetMovieInfo(It.IsAny(), It.IsAny(), false)) - .Callback(p => { throw new MovieNotFoundException(p.ToString()); }); + .Setup(s => s.GetMovieInfo(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((i, p, b) => { throw new MovieNotFoundException(i); }); } private void GivenNewMovieInfo(Movie movie) { Mocker.GetMock() - .Setup(s => s.GetMovieInfo(_movie.ImdbId)) - .Returns(movie); + .Setup(s => s.GetMovieInfo(_movie.TmdbId, It.IsAny(), It.IsAny())) + .Returns(new Tuple>(movie, new List())); } [Test] - public void should_update_tvrage_id_if_changed() + public void should_update_imdb_id_if_changed() { - var newSeriesInfo = _movie.JsonClone(); - newSeriesInfo.ImdbId = _movie.ImdbId + 1; + var newMovieInfo = _movie.JsonClone(); + newMovieInfo.ImdbId = _movie.ImdbId + 1; - GivenNewMovieInfo(newSeriesInfo); + GivenNewMovieInfo(newMovieInfo); Subject.Execute(new RefreshMovieCommand(_movie.Id)); Mocker.GetMock() - .Verify(v => v.UpdateMovie(It.Is(s => s.ImdbId == newSeriesInfo.ImdbId))); + .Verify(v => v.UpdateMovie(It.Is>(s => s.First().ImdbId == newMovieInfo.ImdbId), true)); } [Test] @@ -60,7 +64,7 @@ public void should_log_error_if_tmdb_id_not_found() Subject.Execute(new RefreshMovieCommand(_movie.Id)); Mocker.GetMock() - .Verify(v => v.UpdateMovie(It.IsAny()), Times.Never()); + .Verify(v => v.UpdateMovie(It.Is(s => s.Status == MovieStatusType.Deleted)), Times.Once()); ExceptionVerification.ExpectedErrors(1); } @@ -68,17 +72,41 @@ public void should_log_error_if_tmdb_id_not_found() [Test] public void should_update_if_tmdb_id_changed() { - var newSeriesInfo = _movie.JsonClone(); - newSeriesInfo.TmdbId = _movie.TmdbId + 1; + var newMovieInfo = _movie.JsonClone(); + newMovieInfo.TmdbId = _movie.TmdbId + 1; - GivenNewMovieInfo(newSeriesInfo); + GivenNewMovieInfo(newMovieInfo); Subject.Execute(new RefreshMovieCommand(_movie.Id)); Mocker.GetMock() - .Verify(v => v.UpdateMovie(It.Is(s => s.TmdbId == newSeriesInfo.TmdbId))); + .Verify(v => v.UpdateMovie(It.Is>(s => s.First().TmdbId == newMovieInfo.TmdbId), true)); ExceptionVerification.ExpectedWarns(1); } + + [Test] + public void should_mark_as_deleted_if_tmdb_id_not_found() + { + Subject.Execute(new RefreshMovieCommand(_movie.Id)); + + Mocker.GetMock() + .Verify(v => v.UpdateMovie(It.Is(s => s.Status == MovieStatusType.Deleted)), Times.Once()); + + ExceptionVerification.ExpectedErrors(1); + } + + [Test] + public void should_not_remark_as_deleted_if_tmdb_id_not_found() + { + _movie.Status = MovieStatusType.Deleted; + + Subject.Execute(new RefreshMovieCommand(_movie.Id)); + + Mocker.GetMock() + .Verify(v => v.UpdateMovie(It.IsAny()), Times.Never()); + + ExceptionVerification.ExpectedErrors(1); + } } } diff --git a/src/NzbDrone.Core/Exceptions/MovieNotFoundExceptions.cs b/src/NzbDrone.Core/Exceptions/MovieNotFoundExceptions.cs index c2345bd93..5018b2889 100644 --- a/src/NzbDrone.Core/Exceptions/MovieNotFoundExceptions.cs +++ b/src/NzbDrone.Core/Exceptions/MovieNotFoundExceptions.cs @@ -1,27 +1,33 @@ -using NzbDrone.Common.Exceptions; +using NzbDrone.Common.Exceptions; namespace NzbDrone.Core.Exceptions { public class MovieNotFoundException : NzbDroneException { - public string ImdbId { get; set; } + public int TmdbMovieId { get; set; } - public MovieNotFoundException(string imdbid) - : base(string.Format("Movie with imdbid {0} was not found, it may have been removed from IMDb.", imdbid)) + public MovieNotFoundException(int tmdbMovieId) + : base(string.Format("Movie with tmdbId {0} was not found, it may have been removed from TMDb.", tmdbMovieId)) { - ImdbId = imdbid; + TmdbMovieId = tmdbMovieId; } - public MovieNotFoundException(string imdbid, string message, params object[] args) + public MovieNotFoundException(string imdbId) + : base(string.Format("Movie with IMDBId {0} was not found, it may have been removed from TMDb.", imdbId)) + { + TmdbMovieId = 0; + } + + public MovieNotFoundException(int tmdbMovieId, string message, params object[] args) : base(message, args) { - ImdbId = imdbid; + TmdbMovieId = tmdbMovieId; } - public MovieNotFoundException(string imdbid, string message) + public MovieNotFoundException(int tmdbMovieId, string message) : base(message) { - ImdbId = imdbid; + TmdbMovieId = tmdbMovieId; } } } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/RemovedMovieCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/RemovedMovieCheck.cs new file mode 100644 index 000000000..fdf31a8e5 --- /dev/null +++ b/src/NzbDrone.Core/HealthCheck/Checks/RemovedMovieCheck.cs @@ -0,0 +1,48 @@ +using System.Linq; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Movies; +using NzbDrone.Core.Movies.Events; + +namespace NzbDrone.Core.HealthCheck.Checks +{ + [CheckOn(typeof(MovieUpdatedEvent))] + [CheckOn(typeof(MovieDeletedEvent), CheckOnCondition.FailedOnly)] + public class RemovedSeriesCheck : HealthCheckBase, ICheckOnCondition, ICheckOnCondition + { + private readonly IMovieService _movieService; + + public RemovedSeriesCheck(IMovieService movieService) + { + _movieService = movieService; + } + + public override HealthCheck Check() + { + var deletedMovie = _movieService.GetAllMovies().Where(v => v.Status == MovieStatusType.Deleted).ToList(); + + if (deletedMovie.Empty()) + { + return new HealthCheck(GetType()); + } + + var movieText = deletedMovie.Select(s => $"{s.Title} (tmdbid {s.TmdbId})").Join(", "); + + if (deletedMovie.Count == 1) + { + return new HealthCheck(GetType(), HealthCheckResult.Error, $"Movie {movieText} was removed from TMDb"); + } + + return new HealthCheck(GetType(), HealthCheckResult.Error, $"Movie {movieText} were removed from TMDb"); + } + + public bool ShouldCheckOnEvent(MovieDeletedEvent message) + { + return message.Movie.Status == MovieStatusType.Deleted; + } + + public bool ShouldCheckOnEvent(MovieUpdatedEvent message) + { + return message.Movie.Status == MovieStatusType.Deleted; + } + } +} diff --git a/src/NzbDrone.Core/HealthCheck/EventDrivenHealthCheck.cs b/src/NzbDrone.Core/HealthCheck/EventDrivenHealthCheck.cs index 0b55c1ff2..32dc48bff 100644 --- a/src/NzbDrone.Core/HealthCheck/EventDrivenHealthCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/EventDrivenHealthCheck.cs @@ -1,14 +1,45 @@ +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 761e9f7d0..24b112207 100644 --- a/src/NzbDrone.Core/HealthCheck/HealthCheckService.cs +++ b/src/NzbDrone.Core/HealthCheck/HealthCheckService.cs @@ -3,7 +3,6 @@ using System.Linq; using NLog; using NzbDrone.Common.Cache; -using NzbDrone.Common.Extensions; using NzbDrone.Common.Messaging; using NzbDrone.Common.Reflection; using NzbDrone.Core.Lifecycle; @@ -25,7 +24,7 @@ public class HealthCheckService : IHealthCheckService, 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; @@ -54,10 +53,16 @@ public List Results() 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()); } @@ -111,7 +116,7 @@ public void HandleAsync(IEvent message) return; } - EventDrivenHealthCheck[] checks; + IEventDrivenHealthCheck[] checks; if (!_eventDrivenHealthChecks.TryGetValue(message.GetType(), out checks)) { return; @@ -122,23 +127,10 @@ public void HandleAsync(IEvent message) 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)) - { - filteredChecks.Add(eventDrivenHealthCheck.HealthCheck); - continue; - } - - if (eventDrivenHealthCheck.Condition == CheckOnCondition.SuccessfulOnly && - healthCheckResults.None(r => r.Source == healthCheckType)) + if (eventDrivenHealthCheck.ShouldExecute(message, previouslyFailed)) { 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..921f1d3e8 --- /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/MetadataSource/SkyHook/SkyHookProxy.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs index 9707c9237..d1e98ddcf 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs @@ -100,7 +100,7 @@ public Tuple> GetMovieInfo(int tmdbId, Profile profile, bool if (response.StatusCode == HttpStatusCode.NotFound) { - throw new MovieNotFoundException("Movie not found."); + throw new MovieNotFoundException(tmdbId); } if (response.StatusCode != HttpStatusCode.OK) diff --git a/src/NzbDrone.Core/Movies/MovieStatusType.cs b/src/NzbDrone.Core/Movies/MovieStatusType.cs index 2e856e0b5..a4c7ec9bf 100644 --- a/src/NzbDrone.Core/Movies/MovieStatusType.cs +++ b/src/NzbDrone.Core/Movies/MovieStatusType.cs @@ -1,7 +1,8 @@ -namespace NzbDrone.Core.Movies +namespace NzbDrone.Core.Movies { public enum MovieStatusType { + Deleted = -1, TBA = 0, //Nothing yet announced, only rumors, but still IMDb page (this might not be used) Announced = 1, //Movie is announced but Cinema date is in the future or unknown InCinemas = 2, //Been in Cinemas for less than 3 months (since TMDB lacks complete information) diff --git a/src/NzbDrone.Core/Movies/RefreshMovieService.cs b/src/NzbDrone.Core/Movies/RefreshMovieService.cs index 4a8327ac3..a6f13719f 100644 --- a/src/NzbDrone.Core/Movies/RefreshMovieService.cs +++ b/src/NzbDrone.Core/Movies/RefreshMovieService.cs @@ -63,13 +63,31 @@ private void RefreshMovieInfo(Movie movie) { _logger.ProgressInfo("Updating Info for {0}", movie.Title); - var tuple = _movieInfo.GetMovieInfo(movie.TmdbId, movie.Profile, movie.HasPreDBEntry); + Movie movieInfo; + List credits; - var movieInfo = tuple.Item1; + try + { + var tuple = _movieInfo.GetMovieInfo(movie.TmdbId, movie.Profile, movie.HasPreDBEntry); + movieInfo = tuple.Item1; + credits = tuple.Item2; + } + catch (MovieNotFoundException) + { + if (movie.Status != MovieStatusType.Deleted) + { + movie.Status = MovieStatusType.Deleted; + _movieService.UpdateMovie(movie); + _logger.Debug("Movie marked as deleted on tmdb for {0}", movie.Title); + _eventAggregator.PublishEvent(new MovieUpdatedEvent(movie)); + } + + throw; + } if (movie.TmdbId != movieInfo.TmdbId) { - _logger.Warn("Movie '{0}' (tvdbid {1}) was replaced with '{2}' (tvdbid {3}), because the original was a duplicate.", movie.Title, movie.TmdbId, movieInfo.Title, movieInfo.TmdbId); + _logger.Warn("Movie '{0}' (TmdbId {1}) was replaced with '{2}' (TmdbId {3}), because the original was a duplicate.", movie.Title, movie.TmdbId, movieInfo.Title, movieInfo.TmdbId); movie.TmdbId = movieInfo.TmdbId; } @@ -139,7 +157,7 @@ private void RefreshMovieInfo(Movie movie) } _movieService.UpdateMovie(new List { movie }, true); - _creditService.UpdateCredits(tuple.Item2, movie); + _creditService.UpdateCredits(credits, movie); try {