diff --git a/src/NzbDrone.Core.Test/TvTests/EpisodeServiceTests/HandleEpisodeFileDeletedFixture.cs b/src/NzbDrone.Core.Test/TvTests/EpisodeServiceTests/HandleEpisodeFileDeletedFixture.cs index 96b5002ff..723917287 100644 --- a/src/NzbDrone.Core.Test/TvTests/EpisodeServiceTests/HandleEpisodeFileDeletedFixture.cs +++ b/src/NzbDrone.Core.Test/TvTests/EpisodeServiceTests/HandleEpisodeFileDeletedFixture.cs @@ -61,7 +61,7 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeServiceTests Subject.Handle(new EpisodeFileDeletedEvent(_episodeFile, DeleteMediaFileReason.MissingFromDisk)); Mocker.GetMock() - .Verify(v => v.Update(It.Is(e => e.EpisodeFileId == 0)), Times.Once()); + .Verify(v => v.ClearFileId(It.IsAny(), It.IsAny()), Times.Once()); } [Test] @@ -72,7 +72,7 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeServiceTests Subject.Handle(new EpisodeFileDeletedEvent(_episodeFile, DeleteMediaFileReason.MissingFromDisk)); Mocker.GetMock() - .Verify(v => v.Update(It.Is(e => e.EpisodeFileId == 0)), Times.Exactly(2)); + .Verify(v => v.ClearFileId(It.IsAny(), It.IsAny()), Times.Exactly(2)); } [Test] @@ -87,7 +87,7 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeServiceTests Subject.Handle(new EpisodeFileDeletedEvent(_episodeFile, DeleteMediaFileReason.MissingFromDisk)); Mocker.GetMock() - .Verify(v => v.Update(It.Is(e => e.Monitored == false)), Times.Once()); + .Verify(v => v.ClearFileId(It.IsAny(), true), Times.Once()); } [Test] @@ -102,7 +102,7 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeServiceTests Subject.Handle(new EpisodeFileDeletedEvent(_episodeFile, DeleteMediaFileReason.Upgrade)); Mocker.GetMock() - .Verify(v => v.Update(It.Is(e => e.Monitored == true)), Times.Once()); + .Verify(v => v.ClearFileId(It.IsAny(), false), Times.Once()); } [Test] @@ -117,7 +117,7 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeServiceTests Subject.Handle(new EpisodeFileDeletedEvent(_episodeFile, DeleteMediaFileReason.Upgrade)); Mocker.GetMock() - .Verify(v => v.Update(It.Is(e => e.Monitored == true)), Times.Once()); + .Verify(v => v.ClearFileId(It.IsAny(), false), Times.Once()); } } } diff --git a/src/NzbDrone.Core/Datastore/BasicRepository.cs b/src/NzbDrone.Core/Datastore/BasicRepository.cs index 38f8d5c16..9f24b4f28 100644 --- a/src/NzbDrone.Core/Datastore/BasicRepository.cs +++ b/src/NzbDrone.Core/Datastore/BasicRepository.cs @@ -279,24 +279,24 @@ namespace NzbDrone.Core.Datastore .Take(pagingSpec.PageSize); } - protected void ModelCreated(TModel model) + protected void ModelCreated(TModel model, bool forcePublish = false) { - PublishModelEvent(model, ModelAction.Created); + PublishModelEvent(model, ModelAction.Created, forcePublish); } - protected void ModelUpdated(TModel model) + protected void ModelUpdated(TModel model, bool forcePublish = false) { - PublishModelEvent(model, ModelAction.Updated); + PublishModelEvent(model, ModelAction.Updated, forcePublish); } - protected void ModelDeleted(TModel model) + protected void ModelDeleted(TModel model, bool forcePublish = false) { - PublishModelEvent(model, ModelAction.Deleted); + PublishModelEvent(model, ModelAction.Deleted, forcePublish); } - private void PublishModelEvent(TModel model, ModelAction action) + private void PublishModelEvent(TModel model, ModelAction action, bool forcePublish) { - if (PublishModelEvents) + if (PublishModelEvents || forcePublish) { _eventAggregator.PublishEvent(new ModelEvent(model, action)); } diff --git a/src/NzbDrone.Core/Datastore/Events/ModelEvent.cs b/src/NzbDrone.Core/Datastore/Events/ModelEvent.cs index 72ff55296..af07997ec 100644 --- a/src/NzbDrone.Core/Datastore/Events/ModelEvent.cs +++ b/src/NzbDrone.Core/Datastore/Events/ModelEvent.cs @@ -2,13 +2,22 @@ namespace NzbDrone.Core.Datastore.Events { - public class ModelEvent : IEvent + public class ModelEvent : IEvent + where TModel : ModelBase { + public int ModelId { get; set; } public TModel Model { get; set; } public ModelAction Action { get; set; } + public ModelEvent(int modelId, ModelAction action) + { + ModelId = modelId; + Action = action; + } + public ModelEvent(TModel model, ModelAction action) { + ModelId = model.Id; Model = model; Action = action; } diff --git a/src/NzbDrone.Core/Datastore/Extensions/MappingExtensions.cs b/src/NzbDrone.Core/Datastore/Extensions/MappingExtensions.cs index 4d635024a..10e6fdd59 100644 --- a/src/NzbDrone.Core/Datastore/Extensions/MappingExtensions.cs +++ b/src/NzbDrone.Core/Datastore/Extensions/MappingExtensions.cs @@ -1,4 +1,7 @@ -using System.Reflection; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; using Marr.Data; using Marr.Data.Mapping; using NzbDrone.Common.Reflection; @@ -59,5 +62,10 @@ namespace NzbDrone.Core.Datastore.Extensions return false; } + + public static List QueryScalar(this IDataMapper dataMapper, string sql) + { + return dataMapper.ExecuteReader(sql, reader => (TModel)Convert.ChangeType(reader.GetValue(0), typeof(TModel))).ToList(); + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Tv/EpisodeRepository.cs b/src/NzbDrone.Core/Tv/EpisodeRepository.cs index c268807c9..a8e1a703a 100644 --- a/src/NzbDrone.Core/Tv/EpisodeRepository.cs +++ b/src/NzbDrone.Core/Tv/EpisodeRepository.cs @@ -30,7 +30,8 @@ namespace NzbDrone.Core.Tv void SetMonitoredFlat(Episode episode, bool monitored); void SetMonitoredBySeason(int seriesId, int seasonNumber, bool monitored); void SetMonitored(IEnumerable ids, bool monitored); - void SetFileId(int episodeId, int fileId); + void SetFileId(Episode episode, int fileId); + void ClearFileId(Episode episode, bool unmonitor); } public class EpisodeRepository : BasicRepository, IEpisodeRepository @@ -163,6 +164,8 @@ namespace NzbDrone.Core.Tv { episode.Monitored = monitored; SetFields(episode, p => p.Monitored); + + ModelUpdated(episode, true); } public void SetMonitoredBySeason(int seriesId, int seasonNumber, bool monitored) @@ -173,30 +176,39 @@ namespace NzbDrone.Core.Tv mapper.AddParameter("seasonNumber", seasonNumber); mapper.AddParameter("monitored", monitored); - const string sql = "UPDATE Episodes " + - "SET Monitored = @monitored " + - "WHERE SeriesId = @seriesId " + - "AND SeasonNumber = @seasonNumber"; + var sqlUpdate = $"UPDATE Episodes SET Monitored = @monitored WHERE SeriesId = @seriesId AND SeasonNumber = @seasonNumber AND Monitored != @monitored"; - mapper.ExecuteNonQuery(sql); + mapper.ExecuteNonQuery(sqlUpdate); } public void SetMonitored(IEnumerable ids, bool monitored) { - var mapper = _database.GetDataMapper(); - + var mapper = DataMapper; + mapper.AddParameter("monitored", monitored); - var sql = "UPDATE Episodes " + - "SET Monitored = @monitored " + - $"WHERE Id IN ({string.Join(", ", ids)})"; + var sqlUpdate = $"UPDATE Episodes SET Monitored = @monitored WHERE Id IN ({string.Join(", ", ids)}) AND Monitored != @monitored"; - mapper.ExecuteNonQuery(sql); + mapper.ExecuteNonQuery(sqlUpdate); } - public void SetFileId(int episodeId, int fileId) + public void SetFileId(Episode episode, int fileId) { - SetFields(new Episode { Id = episodeId, EpisodeFileId = fileId }, episode => episode.EpisodeFileId); + episode.EpisodeFileId = fileId; + + SetFields(episode, ep => ep.EpisodeFileId); + + ModelUpdated(episode, true); + } + + public void ClearFileId(Episode episode, bool unmonitor) + { + episode.EpisodeFileId = 0; + episode.Monitored &= !unmonitor; + + SetFields(episode, ep => ep.EpisodeFileId, ep => ep.Monitored); + + ModelUpdated(episode, true); } private SortBuilder GetMissingEpisodesQuery(PagingSpec pagingSpec, DateTime currentTime, int startingSeasonNumber) diff --git a/src/NzbDrone.Core/Tv/EpisodeService.cs b/src/NzbDrone.Core/Tv/EpisodeService.cs index e9bf45507..27406c396 100644 --- a/src/NzbDrone.Core/Tv/EpisodeService.cs +++ b/src/NzbDrone.Core/Tv/EpisodeService.cs @@ -220,14 +220,7 @@ namespace NzbDrone.Core.Tv foreach (var episode in GetEpisodesByFileId(message.EpisodeFile.Id)) { _logger.Debug("Detaching episode {0} from file.", episode.Id); - episode.EpisodeFileId = 0; - - if (message.Reason != DeleteMediaFileReason.Upgrade && _configService.AutoUnmonitorPreviouslyDownloadedEpisodes) - { - episode.Monitored = false; - } - - UpdateEpisode(episode); + _episodeRepository.ClearFileId(episode, message.Reason != DeleteMediaFileReason.Upgrade && _configService.AutoUnmonitorPreviouslyDownloadedEpisodes); } } @@ -235,7 +228,7 @@ namespace NzbDrone.Core.Tv { foreach (var episode in message.EpisodeFile.Episodes.Value) { - _episodeRepository.SetFileId(episode.Id, message.EpisodeFile.Id); + _episodeRepository.SetFileId(episode, message.EpisodeFile.Id); _logger.Debug("Linking [{0}] > [{1}]", message.EpisodeFile.RelativePath, episode); } } diff --git a/src/Sonarr.Api.V3/Episodes/EpisodeModule.cs b/src/Sonarr.Api.V3/Episodes/EpisodeModule.cs index 1c4807669..919d6981f 100644 --- a/src/Sonarr.Api.V3/Episodes/EpisodeModule.cs +++ b/src/Sonarr.Api.V3/Episodes/EpisodeModule.cs @@ -69,7 +69,9 @@ namespace Sonarr.Api.V3.Episodes var resource = Request.Body.FromJson(); _episodeService.SetEpisodeMonitored(id, resource.Monitored); - return ResponseWithCode(MapToResource(_episodeService.GetEpisode(id), false, false, false), HttpStatusCode.Accepted); + resource = MapToResource(_episodeService.GetEpisode(id), false, false, false); + + return ResponseWithCode(resource, HttpStatusCode.Accepted); } private object SetEpisodesMonitored() @@ -77,10 +79,18 @@ namespace Sonarr.Api.V3.Episodes var includeImages = Request.GetBooleanQueryParameter("includeImages", false); var resource = Request.Body.FromJson(); - _episodeService.SetMonitored(resource.EpisodeIds, resource.Monitored); + if (resource.EpisodeIds.Count == 1) + { + _episodeService.SetEpisodeMonitored(resource.EpisodeIds.First(), resource.Monitored); + } + else + { + _episodeService.SetMonitored(resource.EpisodeIds, resource.Monitored); + } - return ResponseWithCode(MapToResource(_episodeService.GetEpisodes(resource.EpisodeIds), false, false, includeImages) - , HttpStatusCode.Accepted); + var resources = MapToResource(_episodeService.GetEpisodes(resource.EpisodeIds), false, false, includeImages); + + return ResponseWithCode(resources, HttpStatusCode.Accepted); } } } diff --git a/src/Sonarr.Api.V3/Episodes/EpisodeModuleWithSignalR.cs b/src/Sonarr.Api.V3/Episodes/EpisodeModuleWithSignalR.cs index 6271f8f5e..6ff27de59 100644 --- a/src/Sonarr.Api.V3/Episodes/EpisodeModuleWithSignalR.cs +++ b/src/Sonarr.Api.V3/Episodes/EpisodeModuleWithSignalR.cs @@ -56,6 +56,13 @@ namespace Sonarr.Api.V3.Episodes var resource = MapToResource(episode, true, true, true); return resource; } + + protected override EpisodeResource GetResourceByIdForBroadcast(int id) + { + var episode = _episodeService.GetEpisode(id); + var resource = MapToResource(episode, false, false, false); + return resource; + } protected EpisodeResource MapToResource(Episode episode, bool includeSeries, bool includeEpisodeFile, bool includeImages) { diff --git a/src/Sonarr.Http/SonarrRestModuleWithSignalR.cs b/src/Sonarr.Http/SonarrRestModuleWithSignalR.cs index 7774dc395..e95db189b 100644 --- a/src/Sonarr.Http/SonarrRestModuleWithSignalR.cs +++ b/src/Sonarr.Http/SonarrRestModuleWithSignalR.cs @@ -23,6 +23,11 @@ namespace Sonarr.Http _signalRBroadcaster = signalRBroadcaster; } + protected virtual TResource GetResourceByIdForBroadcast(int id) + { + return GetResourceById(id); + } + public void Handle(ModelEvent message) { if (!_signalRBroadcaster.IsConnected) return; @@ -32,7 +37,7 @@ namespace Sonarr.Http BroadcastResourceChange(message.Action); } - BroadcastResourceChange(message.Action, message.Model.Id); + BroadcastResourceChange(message.Action, message.ModelId); } protected void BroadcastResourceChange(ModelAction action, int id) @@ -45,7 +50,7 @@ namespace Sonarr.Http } else { - var resource = GetResourceById(id); + var resource = GetResourceByIdForBroadcast(id); BroadcastResourceChange(action, resource); } }