1
0
mirror of https://github.com/Radarr/Radarr.git synced 2024-09-11 20:12:41 +02:00

New: HealthCheck and Status for Movies Deleted from TMDb

This commit is contained in:
Qstick 2020-02-25 23:18:49 -05:00
parent ea9494019e
commit 0c44ee5f88
18 changed files with 505 additions and 202 deletions

View File

@ -190,6 +190,7 @@ export const SCORE = fasUserPlus;
export const SEARCH = fasSearch; export const SEARCH = fasSearch;
export const MOVIE_CONTINUING = fasPlay; export const MOVIE_CONTINUING = fasPlay;
export const SERIES_ENDED = fasStop; export const SERIES_ENDED = fasStop;
export const MOVIE_DELETED = fasExclamationTriangle;
export const SETTINGS = fasCogs; export const SETTINGS = fasCogs;
export const SHUTDOWN = fasPowerOff; export const SHUTDOWN = fasPowerOff;
export const SORT = fasSort; export const SORT = fasSort;

View File

@ -13,7 +13,7 @@ import MovieTitleLink from 'Movie/MovieTitleLink';
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector'; import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal'; import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
import MovieStatusCell from './MovieStatusCell'; import MovieStatusCell from './MovieStatusCell';
import MovieStatusConnector from 'Movie/MovieStatusConnector'; import MovieFileStatusConnector from 'Movie/MovieFileStatusConnector';
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell'; import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
import styles from './MovieIndexRow.css'; import styles from './MovieIndexRow.css';
@ -278,7 +278,7 @@ class MovieIndexRow extends Component {
key={name} key={name}
className={styles[name]} className={styles[name]}
> >
<MovieStatusConnector <MovieFileStatusConnector
movieId={id} movieId={id}
/> />
</VirtualTableRowCell> </VirtualTableRowCell>

View File

@ -3,6 +3,7 @@ import React from 'react';
import { icons } from 'Helpers/Props'; import { icons } from 'Helpers/Props';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import VirtualTableRowCell from 'Components/Table/Cells/TableRowCell'; import VirtualTableRowCell from 'Components/Table/Cells/TableRowCell';
import { getMovieStatusDetails } from 'Movie/MovieStatus';
import styles from './MovieStatusCell.css'; import styles from './MovieStatusCell.css';
function MovieStatusCell(props) { function MovieStatusCell(props) {
@ -14,6 +15,8 @@ function MovieStatusCell(props) {
...otherProps ...otherProps
} = props; } = props;
const statusDetails = getMovieStatusDetails(status);
return ( return (
<Component <Component
className={className} className={className}
@ -25,32 +28,12 @@ function MovieStatusCell(props) {
title={monitored ? 'Movie is monitored' : 'Movie is unmonitored'} title={monitored ? 'Movie is monitored' : 'Movie is unmonitored'}
/> />
{ <Icon
status === 'announced' ? className={styles.statusIcon}
<Icon name={statusDetails.icon}
className={styles.statusIcon} title={`${statusDetails.title}: ${statusDetails.message}`}
name={icons.ANNOUNCED} />
title={'Movie is announced'}
/> : null
}
{
status === 'inCinemas' ?
<Icon
className={styles.statusIcon}
name={icons.IN_CINEMAS}
title={'Movie is in Cinemas'}
/> : null
}
{
status === 'released' ?
<Icon
className={styles.statusIcon}
name={icons.MOVIE_FILE}
title={'Movie is released'}
/> : null
}
</Component> </Component>
); );
} }

View File

@ -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 (
<div className={styles.center}>
<QueueDetails
{...queueItem}
progressBar={
<ProgressBar
title={`Movie is downloading - ${progress.toFixed(1)}% ${queueItem.title}`}
progress={progress}
kind={kinds.PURPLE}
size={sizes.MEDIUM}
/>
}
/>
</div>
);
}
if (grabbed) {
return (
<div className={styles.center}>
<Icon
name={icons.DOWNLOADING}
title="Movie is downloading"
/>
</div>
);
}
if (hasMovieFile) {
const quality = movieFile.quality;
return (
<div className={styles.center}>
<MovieQuality
title={quality.quality.name}
size={movieFile.size}
quality={quality}
isMonitored={monitored}
isCutoffNotMet={movieFile.qualityCutoffNotMet}
/>
</div>
);
}
if (!monitored) {
return (
<div className={styles.center}>
<Label
title="Announced"
kind={kinds.WARNING}
>
Not Monitored
</Label>
</div>
);
}
if (hasReleased) {
return (
<div className={styles.center}>
<Label
title="Movie Available, but Missing"
kind={kinds.DANGER}
>
Missing
</Label>
</div>
);
}
return (
<div className={styles.center}>
<Label
title="Announced"
kind={kinds.INFO}
>
Not Available
</Label>
</div>
);
}
MovieFileStatus.propTypes = {
inCinemas: PropTypes.string,
isAvailable: PropTypes.bool,
monitored: PropTypes.bool.isRequired,
grabbed: PropTypes.bool,
queueItem: PropTypes.object,
movieFile: PropTypes.object
};
export default MovieFileStatus;

View File

@ -5,7 +5,7 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import createMovieSelector from 'Store/Selectors/createMovieSelector'; import createMovieSelector from 'Store/Selectors/createMovieSelector';
import createQueueItemSelector from 'Store/Selectors/createQueueItemSelector'; import createQueueItemSelector from 'Store/Selectors/createQueueItemSelector';
import MovieStatus from './MovieStatus'; import MovieFileStatus from './MovieFileStatus';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
@ -30,22 +30,22 @@ function createMapStateToProps() {
const mapDispatchToProps = { const mapDispatchToProps = {
}; };
class MovieStatusConnector extends Component { class MovieFileStatusConnector extends Component {
// //
// Render // Render
render() { render() {
return ( return (
<MovieStatus <MovieFileStatus
{...this.props} {...this.props}
/> />
); );
} }
} }
MovieStatusConnector.propTypes = { MovieFileStatusConnector.propTypes = {
movieId: PropTypes.number.isRequired movieId: PropTypes.number.isRequired
}; };
export default connect(createMapStateToProps, mapDispatchToProps)(MovieStatusConnector); export default connect(createMapStateToProps, mapDispatchToProps)(MovieFileStatusConnector);

View File

@ -1,124 +1,32 @@
import PropTypes from 'prop-types'; import { icons } from 'Helpers/Props';
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';
function MovieStatus(props) { export function getMovieStatusDetails(status) {
const {
inCinemas,
isAvailable,
monitored,
grabbed,
queueItem,
movieFile
} = props;
const hasMovieFile = !!movieFile; let statusDetails = {
const isQueued = !!queueItem; icon: icons.ANNOUNCED,
const hasReleased = isAvailable && inCinemas; title: 'Announced',
message: 'Movie is announced'
};
if (isQueued) { if (status === 'deleted') {
const { statusDetails = {
sizeleft, icon: icons.MOVIE_DELETED,
size title: 'Deleted',
} = queueItem; message: 'Movie was deleted from TMDb'
};
const progress = (100 - sizeleft / size * 100); } else if (status === 'inCinemas') {
statusDetails = {
return ( icon: icons.IN_CINEMAS,
<div className={styles.center}> title: 'In Cinemas',
<QueueDetails message: 'Movie is in Cinemas'
{...queueItem} };
progressBar={ } else if (status === 'released') {
<ProgressBar statusDetails = {
title={`Movie is downloading - ${progress.toFixed(1)}% ${queueItem.title}`} icon: icons.MOVIE_FILE,
progress={progress} title: 'Released',
kind={kinds.PURPLE} message: 'Movie is released'
size={sizes.MEDIUM} };
/>
}
/>
</div>
);
} }
if (grabbed) { return statusDetails;
return (
<div className={styles.center}>
<Icon
name={icons.DOWNLOADING}
title="Movie is downloading"
/>
</div>
);
}
if (hasMovieFile) {
const quality = movieFile.quality;
return (
<div className={styles.center}>
<MovieQuality
title={quality.quality.name}
size={movieFile.size}
quality={quality}
isMonitored={monitored}
isCutoffNotMet={movieFile.qualityCutoffNotMet}
/>
</div>
);
}
if (!monitored) {
return (
<div className={styles.center}>
<Label
title="Announced"
kind={kinds.WARNING}
>
Not Monitored
</Label>
</div>
);
}
if (hasReleased) {
return (
<div className={styles.center}>
<Label
title="Movie Available, but Missing"
kind={kinds.DANGER}
>
Missing
</Label>
</div>
);
}
return (
<div className={styles.center}>
<Label
title="Announced"
kind={kinds.INFO}
>
Not Available
</Label>
</div>
);
} }
MovieStatus.propTypes = {
inCinemas: PropTypes.string,
isAvailable: PropTypes.bool,
monitored: PropTypes.bool.isRequired,
grabbed: PropTypes.bool,
queueItem: PropTypes.object,
movieFile: PropTypes.object
};
export default MovieStatus;

View File

@ -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<RemovedSeriesCheck>
{
private void GivenMovie(int amount, int deleted)
{
List<Movie> movie;
if (amount == 0)
{
movie = new List<Movie>();
}
else if (deleted == 0)
{
movie = Builder<Movie>.CreateListOfSize(amount)
.All()
.With(v => v.Status = MovieStatusType.Released)
.BuildList();
}
else
{
movie = Builder<Movie>.CreateListOfSize(amount)
.All()
.With(v => v.Status = MovieStatusType.Released)
.Random(deleted)
.With(v => v.Status = MovieStatusType.Deleted)
.BuildList();
}
Mocker.GetMock<IMovieService>()
.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();
}
}
}

View File

@ -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<HealthCheckService>
{
private FakeHealthCheck _healthCheck;
[SetUp]
public void SetUp()
{
_healthCheck = new FakeHealthCheck();
Mocker.SetConstant<IEnumerable<IProvideHealthCheck>>(new[] { _healthCheck });
Mocker.SetConstant<ICacheManager>(Mocker.Resolve<CacheManager>());
}
[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<FakeEvent>
{
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;
}
}
}

View File

@ -1,3 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FizzWare.NBuilder; using FizzWare.NBuilder;
using Moq; using Moq;
using NUnit.Framework; using NUnit.Framework;
@ -6,6 +9,7 @@
using NzbDrone.Core.MetadataSource; using NzbDrone.Core.MetadataSource;
using NzbDrone.Core.Movies; using NzbDrone.Core.Movies;
using NzbDrone.Core.Movies.Commands; using NzbDrone.Core.Movies.Commands;
using NzbDrone.Core.Movies.Credits;
using NzbDrone.Core.Profiles; using NzbDrone.Core.Profiles;
using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Test.Framework;
using NzbDrone.Test.Common; using NzbDrone.Test.Common;
@ -13,7 +17,6 @@
namespace NzbDrone.Core.Test.MovieTests namespace NzbDrone.Core.Test.MovieTests
{ {
[TestFixture] [TestFixture]
[Ignore("Weird moq errors")]
public class RefreshMovieServiceFixture : CoreTest<RefreshMovieService> public class RefreshMovieServiceFixture : CoreTest<RefreshMovieService>
{ {
private Movie _movie; private Movie _movie;
@ -22,36 +25,37 @@ public class RefreshMovieServiceFixture : CoreTest<RefreshMovieService>
public void Setup() public void Setup()
{ {
_movie = Builder<Movie>.CreateNew() _movie = Builder<Movie>.CreateNew()
.Build(); .With(s => s.Status = MovieStatusType.Released)
.Build();
Mocker.GetMock<IMovieService>() Mocker.GetMock<IMovieService>()
.Setup(s => s.GetMovie(_movie.Id)) .Setup(s => s.GetMovie(_movie.Id))
.Returns(_movie); .Returns(_movie);
Mocker.GetMock<IProvideMovieInfo>() Mocker.GetMock<IProvideMovieInfo>()
.Setup(s => s.GetMovieInfo(It.IsAny<int>(), It.IsAny<Profile>(), false)) .Setup(s => s.GetMovieInfo(It.IsAny<int>(), It.IsAny<Profile>(), It.IsAny<bool>()))
.Callback<int>(p => { throw new MovieNotFoundException(p.ToString()); }); .Callback<int, Profile, bool>((i, p, b) => { throw new MovieNotFoundException(i); });
} }
private void GivenNewMovieInfo(Movie movie) private void GivenNewMovieInfo(Movie movie)
{ {
Mocker.GetMock<IProvideMovieInfo>() Mocker.GetMock<IProvideMovieInfo>()
.Setup(s => s.GetMovieInfo(_movie.ImdbId)) .Setup(s => s.GetMovieInfo(_movie.TmdbId, It.IsAny<Profile>(), It.IsAny<bool>()))
.Returns(movie); .Returns(new Tuple<Movie, List<Credit>>(movie, new List<Credit>()));
} }
[Test] [Test]
public void should_update_tvrage_id_if_changed() public void should_update_imdb_id_if_changed()
{ {
var newSeriesInfo = _movie.JsonClone(); var newMovieInfo = _movie.JsonClone();
newSeriesInfo.ImdbId = _movie.ImdbId + 1; newMovieInfo.ImdbId = _movie.ImdbId + 1;
GivenNewMovieInfo(newSeriesInfo); GivenNewMovieInfo(newMovieInfo);
Subject.Execute(new RefreshMovieCommand(_movie.Id)); Subject.Execute(new RefreshMovieCommand(_movie.Id));
Mocker.GetMock<IMovieService>() Mocker.GetMock<IMovieService>()
.Verify(v => v.UpdateMovie(It.Is<Movie>(s => s.ImdbId == newSeriesInfo.ImdbId))); .Verify(v => v.UpdateMovie(It.Is<List<Movie>>(s => s.First().ImdbId == newMovieInfo.ImdbId), true));
} }
[Test] [Test]
@ -60,7 +64,7 @@ public void should_log_error_if_tmdb_id_not_found()
Subject.Execute(new RefreshMovieCommand(_movie.Id)); Subject.Execute(new RefreshMovieCommand(_movie.Id));
Mocker.GetMock<IMovieService>() Mocker.GetMock<IMovieService>()
.Verify(v => v.UpdateMovie(It.IsAny<Movie>()), Times.Never()); .Verify(v => v.UpdateMovie(It.Is<Movie>(s => s.Status == MovieStatusType.Deleted)), Times.Once());
ExceptionVerification.ExpectedErrors(1); ExceptionVerification.ExpectedErrors(1);
} }
@ -68,17 +72,41 @@ public void should_log_error_if_tmdb_id_not_found()
[Test] [Test]
public void should_update_if_tmdb_id_changed() public void should_update_if_tmdb_id_changed()
{ {
var newSeriesInfo = _movie.JsonClone(); var newMovieInfo = _movie.JsonClone();
newSeriesInfo.TmdbId = _movie.TmdbId + 1; newMovieInfo.TmdbId = _movie.TmdbId + 1;
GivenNewMovieInfo(newSeriesInfo); GivenNewMovieInfo(newMovieInfo);
Subject.Execute(new RefreshMovieCommand(_movie.Id)); Subject.Execute(new RefreshMovieCommand(_movie.Id));
Mocker.GetMock<IMovieService>() Mocker.GetMock<IMovieService>()
.Verify(v => v.UpdateMovie(It.Is<Movie>(s => s.TmdbId == newSeriesInfo.TmdbId))); .Verify(v => v.UpdateMovie(It.Is<List<Movie>>(s => s.First().TmdbId == newMovieInfo.TmdbId), true));
ExceptionVerification.ExpectedWarns(1); ExceptionVerification.ExpectedWarns(1);
} }
[Test]
public void should_mark_as_deleted_if_tmdb_id_not_found()
{
Subject.Execute(new RefreshMovieCommand(_movie.Id));
Mocker.GetMock<IMovieService>()
.Verify(v => v.UpdateMovie(It.Is<Movie>(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<IMovieService>()
.Verify(v => v.UpdateMovie(It.IsAny<Movie>()), Times.Never());
ExceptionVerification.ExpectedErrors(1);
}
} }
} }

View File

@ -1,27 +1,33 @@
using NzbDrone.Common.Exceptions; using NzbDrone.Common.Exceptions;
namespace NzbDrone.Core.Exceptions namespace NzbDrone.Core.Exceptions
{ {
public class MovieNotFoundException : NzbDroneException public class MovieNotFoundException : NzbDroneException
{ {
public string ImdbId { get; set; } public int TmdbMovieId { get; set; }
public MovieNotFoundException(string imdbid) public MovieNotFoundException(int tmdbMovieId)
: base(string.Format("Movie with imdbid {0} was not found, it may have been removed from IMDb.", imdbid)) : 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) : base(message, args)
{ {
ImdbId = imdbid; TmdbMovieId = tmdbMovieId;
} }
public MovieNotFoundException(string imdbid, string message) public MovieNotFoundException(int tmdbMovieId, string message)
: base(message) : base(message)
{ {
ImdbId = imdbid; TmdbMovieId = tmdbMovieId;
} }
} }
} }

View File

@ -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<MovieUpdatedEvent>, ICheckOnCondition<MovieDeletedEvent>
{
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;
}
}
}

View File

@ -1,14 +1,45 @@
using NzbDrone.Common.Messaging;
namespace NzbDrone.Core.HealthCheck namespace NzbDrone.Core.HealthCheck
{ {
public class EventDrivenHealthCheck public interface IEventDrivenHealthCheck
{
IProvideHealthCheck HealthCheck { get; }
bool ShouldExecute(IEvent message, bool previouslyFailed);
}
public class EventDrivenHealthCheck<TEvent> : IEventDrivenHealthCheck
{ {
public IProvideHealthCheck HealthCheck { get; set; } public IProvideHealthCheck HealthCheck { get; set; }
public CheckOnCondition Condition { get; set; } public CheckOnCondition Condition { get; set; }
public ICheckOnCondition<TEvent> EventFilter { get; set; }
public EventDrivenHealthCheck(IProvideHealthCheck healthCheck, CheckOnCondition condition) public EventDrivenHealthCheck(IProvideHealthCheck healthCheck, CheckOnCondition condition)
{ {
HealthCheck = healthCheck; HealthCheck = healthCheck;
Condition = condition; Condition = condition;
EventFilter = healthCheck as ICheckOnCondition<TEvent>;
}
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;
} }
} }
} }

View File

@ -3,7 +3,6 @@
using System.Linq; using System.Linq;
using NLog; using NLog;
using NzbDrone.Common.Cache; using NzbDrone.Common.Cache;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Messaging; using NzbDrone.Common.Messaging;
using NzbDrone.Common.Reflection; using NzbDrone.Common.Reflection;
using NzbDrone.Core.Lifecycle; using NzbDrone.Core.Lifecycle;
@ -25,7 +24,7 @@ public class HealthCheckService : IHealthCheckService,
private readonly IProvideHealthCheck[] _healthChecks; private readonly IProvideHealthCheck[] _healthChecks;
private readonly IProvideHealthCheck[] _startupHealthChecks; private readonly IProvideHealthCheck[] _startupHealthChecks;
private readonly IProvideHealthCheck[] _scheduledHealthChecks; private readonly IProvideHealthCheck[] _scheduledHealthChecks;
private readonly Dictionary<Type, EventDrivenHealthCheck[]> _eventDrivenHealthChecks; private readonly Dictionary<Type, IEventDrivenHealthCheck[]> _eventDrivenHealthChecks;
private readonly IEventAggregator _eventAggregator; private readonly IEventAggregator _eventAggregator;
private readonly ICacheManager _cacheManager; private readonly ICacheManager _cacheManager;
private readonly Logger _logger; private readonly Logger _logger;
@ -54,10 +53,16 @@ public List<HealthCheck> Results()
return _healthCheckResults.Values.ToList(); return _healthCheckResults.Values.ToList();
} }
private Dictionary<Type, EventDrivenHealthCheck[]> GetEventDrivenHealthChecks() private Dictionary<Type, IEventDrivenHealthCheck[]> GetEventDrivenHealthChecks()
{ {
return _healthChecks return _healthChecks
.SelectMany(h => h.GetType().GetAttributes<CheckOnAttribute>().Select(a => Tuple.Create(a.EventType, new EventDrivenHealthCheck(h, a.Condition)))) .SelectMany(h => h.GetType().GetAttributes<CheckOnAttribute>().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) .GroupBy(t => t.Item1, t => t.Item2)
.ToDictionary(g => g.Key, g => g.ToArray()); .ToDictionary(g => g.Key, g => g.ToArray());
} }
@ -111,7 +116,7 @@ public void HandleAsync(IEvent message)
return; return;
} }
EventDrivenHealthCheck[] checks; IEventDrivenHealthCheck[] checks;
if (!_eventDrivenHealthChecks.TryGetValue(message.GetType(), out checks)) if (!_eventDrivenHealthChecks.TryGetValue(message.GetType(), out checks))
{ {
return; return;
@ -122,23 +127,10 @@ public void HandleAsync(IEvent message)
foreach (var eventDrivenHealthCheck in checks) foreach (var eventDrivenHealthCheck in checks)
{ {
if (eventDrivenHealthCheck.Condition == CheckOnCondition.Always)
{
filteredChecks.Add(eventDrivenHealthCheck.HealthCheck);
continue;
}
var healthCheckType = eventDrivenHealthCheck.HealthCheck.GetType(); var healthCheckType = eventDrivenHealthCheck.HealthCheck.GetType();
var previouslyFailed = healthCheckResults.Any(r => r.Source == healthCheckType);
if (eventDrivenHealthCheck.Condition == CheckOnCondition.FailedOnly && if (eventDrivenHealthCheck.ShouldExecute(message, previouslyFailed))
healthCheckResults.Any(r => r.Source == healthCheckType))
{
filteredChecks.Add(eventDrivenHealthCheck.HealthCheck);
continue;
}
if (eventDrivenHealthCheck.Condition == CheckOnCondition.SuccessfulOnly &&
healthCheckResults.None(r => r.Source == healthCheckType))
{ {
filteredChecks.Add(eventDrivenHealthCheck.HealthCheck); filteredChecks.Add(eventDrivenHealthCheck.HealthCheck);
} }

View File

@ -0,0 +1,7 @@
namespace NzbDrone.Core.HealthCheck
{
public interface ICheckOnCondition<TEvent>
{
bool ShouldCheckOnEvent(TEvent message);
}
}

View File

@ -100,7 +100,7 @@ public Tuple<Movie, List<Credit>> GetMovieInfo(int tmdbId, Profile profile, bool
if (response.StatusCode == HttpStatusCode.NotFound) if (response.StatusCode == HttpStatusCode.NotFound)
{ {
throw new MovieNotFoundException("Movie not found."); throw new MovieNotFoundException(tmdbId);
} }
if (response.StatusCode != HttpStatusCode.OK) if (response.StatusCode != HttpStatusCode.OK)

View File

@ -1,7 +1,8 @@
namespace NzbDrone.Core.Movies namespace NzbDrone.Core.Movies
{ {
public enum MovieStatusType public enum MovieStatusType
{ {
Deleted = -1,
TBA = 0, //Nothing yet announced, only rumors, but still IMDb page (this might not be used) 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 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) InCinemas = 2, //Been in Cinemas for less than 3 months (since TMDB lacks complete information)

View File

@ -63,13 +63,31 @@ private void RefreshMovieInfo(Movie movie)
{ {
_logger.ProgressInfo("Updating Info for {0}", movie.Title); _logger.ProgressInfo("Updating Info for {0}", movie.Title);
var tuple = _movieInfo.GetMovieInfo(movie.TmdbId, movie.Profile, movie.HasPreDBEntry); Movie movieInfo;
List<Credit> 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) 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; movie.TmdbId = movieInfo.TmdbId;
} }
@ -139,7 +157,7 @@ private void RefreshMovieInfo(Movie movie)
} }
_movieService.UpdateMovie(new List<Movie> { movie }, true); _movieService.UpdateMovie(new List<Movie> { movie }, true);
_creditService.UpdateCredits(tuple.Item2, movie); _creditService.UpdateCredits(credits, movie);
try try
{ {