From 7b0802cfd65d04d58283460f160e15791a03dafe Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 7 Feb 2021 20:25:44 -0800 Subject: [PATCH] Fixed: Errors loading queue after episodes in series are removed Closes #3565 --- .../TrackedDownloadServiceFixture.cs | 114 +++++++++++++++++- .../Books/Events/BookInfoRefreshedEvent.cs | 5 +- .../Books/Services/RefreshAuthorService.cs | 4 +- .../Services/RefreshEntityServiceBase.cs | 4 +- .../TrackedDownloadService.cs | 52 +++++++- 5 files changed, 169 insertions(+), 10 deletions(-) diff --git a/src/NzbDrone.Core.Test/Download/TrackedDownloads/TrackedDownloadServiceFixture.cs b/src/NzbDrone.Core.Test/Download/TrackedDownloads/TrackedDownloadServiceFixture.cs index a35125562..2bbe1511c 100644 --- a/src/NzbDrone.Core.Test/Download/TrackedDownloads/TrackedDownloadServiceFixture.cs +++ b/src/NzbDrone.Core.Test/Download/TrackedDownloads/TrackedDownloadServiceFixture.cs @@ -129,12 +129,124 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads .Returns(default(RemoteBook)); // handle deletion event - Subject.Handle(new BookDeletedEvent(remoteBook.Books.First(), false, false)); + Subject.Handle(new BookInfoRefreshedEvent(remoteBook.Author, new List(), new List(), remoteBook.Books)); // verify download has null remote book var trackedDownloads = Subject.GetTrackedDownloads(); trackedDownloads.Should().HaveCount(1); trackedDownloads.First().RemoteBook.Should().BeNull(); } + + [Test] + public void should_not_throw_when_processing_deleted_episodes() + { + GivenDownloadHistory(); + + var remoteEpisode = new RemoteBook + { + Author = new Author() { Id = 5 }, + Books = new List { new Book { Id = 4 } }, + ParsedBookInfo = new ParsedBookInfo() + { + BookTitle = "TV Series" + } + }; + + Mocker.GetMock() + .Setup(s => s.Map(It.IsAny(), It.IsAny(), It.IsAny>())) + .Returns(default(RemoteBook)); + + Mocker.GetMock() + .Setup(s => s.FindByDownloadId(It.IsAny())) + .Returns(new List()); + + var client = new DownloadClientDefinition() + { + Id = 1, + Protocol = DownloadProtocol.Torrent + }; + + var item = new DownloadClientItem() + { + Title = "TV Series - S01E01", + DownloadId = "12345", + DownloadClientInfo = new DownloadClientItemClientInfo + { + Id = 1, + Type = "Blackhole", + Name = "Blackhole Client", + Protocol = DownloadProtocol.Torrent + } + }; + + Subject.TrackDownload(client, item); + Subject.GetTrackedDownloads().Should().HaveCount(1); + + Mocker.GetMock() + .Setup(s => s.Map(It.IsAny(), It.IsAny(), It.IsAny>())) + .Returns(default(RemoteBook)); + + Subject.Handle(new BookInfoRefreshedEvent(remoteEpisode.Author, new List(), new List(), remoteEpisode.Books)); + + var trackedDownloads = Subject.GetTrackedDownloads(); + trackedDownloads.Should().HaveCount(1); + trackedDownloads.First().RemoteBook.Should().BeNull(); + } + + [Test] + public void should_not_throw_when_processing_deleted_series() + { + GivenDownloadHistory(); + + var remoteEpisode = new RemoteBook + { + Author = new Author() { Id = 5 }, + Books = new List { new Book { Id = 4 } }, + ParsedBookInfo = new ParsedBookInfo() + { + BookTitle = "TV Series", + } + }; + + Mocker.GetMock() + .Setup(s => s.Map(It.IsAny(), It.IsAny(), It.IsAny>())) + .Returns(default(RemoteBook)); + + Mocker.GetMock() + .Setup(s => s.FindByDownloadId(It.IsAny())) + .Returns(new List()); + + var client = new DownloadClientDefinition() + { + Id = 1, + Protocol = DownloadProtocol.Torrent + }; + + var item = new DownloadClientItem() + { + Title = "TV Series - S01E01", + DownloadId = "12345", + DownloadClientInfo = new DownloadClientItemClientInfo + { + Id = 1, + Type = "Blackhole", + Name = "Blackhole Client", + Protocol = DownloadProtocol.Torrent + } + }; + + Subject.TrackDownload(client, item); + Subject.GetTrackedDownloads().Should().HaveCount(1); + + Mocker.GetMock() + .Setup(s => s.Map(It.IsAny(), It.IsAny(), It.IsAny>())) + .Returns(default(RemoteBook)); + + Subject.Handle(new AuthorDeletedEvent(remoteEpisode.Author, true, true)); + + var trackedDownloads = Subject.GetTrackedDownloads(); + trackedDownloads.Should().HaveCount(1); + trackedDownloads.First().RemoteBook.Should().BeNull(); + } } } diff --git a/src/NzbDrone.Core/Books/Events/BookInfoRefreshedEvent.cs b/src/NzbDrone.Core/Books/Events/BookInfoRefreshedEvent.cs index 8a4a11946..76a539a07 100644 --- a/src/NzbDrone.Core/Books/Events/BookInfoRefreshedEvent.cs +++ b/src/NzbDrone.Core/Books/Events/BookInfoRefreshedEvent.cs @@ -8,13 +8,16 @@ namespace NzbDrone.Core.Books.Events { public Author Author { get; set; } public ReadOnlyCollection Added { get; private set; } + public ReadOnlyCollection Updated { get; private set; } + public ReadOnlyCollection Removed { get; private set; } - public BookInfoRefreshedEvent(Author author, IList added, IList updated) + public BookInfoRefreshedEvent(Author author, IList added, IList updated, IList removed) { Author = author; Added = new ReadOnlyCollection(added); Updated = new ReadOnlyCollection(updated); + Removed = new ReadOnlyCollection(removed); } } } diff --git a/src/NzbDrone.Core/Books/Services/RefreshAuthorService.cs b/src/NzbDrone.Core/Books/Services/RefreshAuthorService.cs index 85d284634..750e269be 100644 --- a/src/NzbDrone.Core/Books/Services/RefreshAuthorService.cs +++ b/src/NzbDrone.Core/Books/Services/RefreshAuthorService.cs @@ -297,9 +297,9 @@ namespace NzbDrone.Core.Books _eventAggregator.PublishEvent(new AuthorRefreshCompleteEvent(entity)); } - protected override void PublishChildrenUpdatedEvent(Author entity, List newChildren, List updateChildren) + protected override void PublishChildrenUpdatedEvent(Author entity, List newChildren, List updateChildren, List deleteChildren) { - _eventAggregator.PublishEvent(new BookInfoRefreshedEvent(entity, newChildren, updateChildren)); + _eventAggregator.PublishEvent(new BookInfoRefreshedEvent(entity, newChildren, updateChildren, deleteChildren)); } private void Rescan(List authorIds, bool isNew, CommandTrigger trigger, bool infoUpdated) diff --git a/src/NzbDrone.Core/Books/Services/RefreshEntityServiceBase.cs b/src/NzbDrone.Core/Books/Services/RefreshEntityServiceBase.cs index 3405f5237..e11a7e52c 100644 --- a/src/NzbDrone.Core/Books/Services/RefreshEntityServiceBase.cs +++ b/src/NzbDrone.Core/Books/Services/RefreshEntityServiceBase.cs @@ -109,7 +109,7 @@ namespace NzbDrone.Core.Books { } - protected virtual void PublishChildrenUpdatedEvent(TEntity entity, List newChildren, List updateChildren) + protected virtual void PublishChildrenUpdatedEvent(TEntity entity, List newChildren, List updateChildren, List deleteChildren) { } @@ -290,7 +290,7 @@ namespace NzbDrone.Core.Books // now trigger updates var updated = RefreshChildren(sortedChildren, remoteChildren, remoteData, forceChildRefresh, forceUpdateFileTags, lastUpdate); - PublishChildrenUpdatedEvent(entity, sortedChildren.Added, sortedChildren.Updated); + PublishChildrenUpdatedEvent(entity, sortedChildren.Added, sortedChildren.Updated, sortedChildren.Deleted); return updated; } } diff --git a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs index bf5de6f98..455281779 100644 --- a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs +++ b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs @@ -14,7 +14,7 @@ using NzbDrone.Core.Parser; namespace NzbDrone.Core.Download.TrackedDownloads { - public interface ITrackedDownloadService : IHandle + public interface ITrackedDownloadService { TrackedDownload Find(string downloadId); void StopTracking(string downloadId); @@ -24,7 +24,9 @@ namespace NzbDrone.Core.Download.TrackedDownloads void UpdateTrackable(List trackedDownloads); } - public class TrackedDownloadService : ITrackedDownloadService + public class TrackedDownloadService : ITrackedDownloadService, + IHandle, + IHandle { private readonly IParsingService _parsingService; private readonly IHistoryService _historyService; @@ -238,6 +240,13 @@ namespace NzbDrone.Core.Download.TrackedDownloads } } + private void UpdateCachedItem(TrackedDownload trackedDownload) + { + var parsedEpisodeInfo = Parser.Parser.ParseBookTitle(trackedDownload.DownloadItem.Title); + + trackedDownload.RemoteBook = parsedEpisodeInfo == null ? null : _parsingService.Map(parsedEpisodeInfo, 0, new[] { 0 }); + } + private static TrackedDownloadState GetStateFromHistory(DownloadHistoryEventType eventType) { switch (eventType) @@ -255,9 +264,44 @@ namespace NzbDrone.Core.Download.TrackedDownloads } } - public void Handle(BookDeletedEvent message) + public void Handle(BookInfoRefreshedEvent message) { - UpdateBookCache(message.Book.Id); + var needsToUpdate = false; + + foreach (var episode in message.Removed) + { + var cachedItems = _cache.Values.Where(t => + t.RemoteBook?.Books != null && + t.RemoteBook.Books.Any(e => e.Id == episode.Id)) + .ToList(); + + if (cachedItems.Any()) + { + needsToUpdate = true; + } + + cachedItems.ForEach(UpdateCachedItem); + } + + if (needsToUpdate) + { + _eventAggregator.PublishEvent(new TrackedDownloadRefreshedEvent(GetTrackedDownloads())); + } + } + + public void Handle(AuthorDeletedEvent message) + { + var cachedItems = _cache.Values.Where(t => + t.RemoteBook?.Author != null && + t.RemoteBook.Author.Id == message.Author.Id) + .ToList(); + + if (cachedItems.Any()) + { + cachedItems.ForEach(UpdateCachedItem); + + _eventAggregator.PublishEvent(new TrackedDownloadRefreshedEvent(GetTrackedDownloads())); + } } } }