From 4d74ee73bd2809d9b067c69d82ba637aa2f25d49 Mon Sep 17 00:00:00 2001 From: Mika Cohen Date: Wed, 22 Apr 2026 16:16:36 -0600 Subject: [PATCH] Reuse stable waiting tracked downloads --- .../TrackedDownloadServiceFixture.cs | 134 ++++++++++++++++++ .../TrackedDownloadService.cs | 28 +++- 2 files changed, 161 insertions(+), 1 deletion(-) diff --git a/src/NzbDrone.Core.Test/Download/TrackedDownloads/TrackedDownloadServiceFixture.cs b/src/NzbDrone.Core.Test/Download/TrackedDownloads/TrackedDownloadServiceFixture.cs index 54544923ff..edea0e3acd 100644 --- a/src/NzbDrone.Core.Test/Download/TrackedDownloads/TrackedDownloadServiceFixture.cs +++ b/src/NzbDrone.Core.Test/Download/TrackedDownloads/TrackedDownloadServiceFixture.cs @@ -40,6 +40,140 @@ private void GivenDownloadHistory() }); } + private static DownloadClientDefinition CreateDownloadClient() + { + return new DownloadClientDefinition() + { + Id = 1, + Protocol = DownloadProtocol.Usenet + }; + } + + private static DownloadClientItem CreateDownloadItem(DownloadItemStatus status) + { + return new DownloadClientItem() + { + Title = "A Movie 1998", + DownloadId = "35238", + Category = "radarr", + TotalSize = 1000, + RemainingSize = 500, + Status = status, + DownloadClientInfo = new DownloadClientItemClientInfo + { + Id = 1, + Type = "NZBGet", + Name = "NZBGet", + Protocol = DownloadProtocol.Usenet + } + }; + } + + private void GivenTrackedDownloadCanBeMapped() + { + Mocker.GetMock() + .Setup(s => s.FindByDownloadId(It.IsAny())) + .Returns(new List()); + + Mocker.GetMock() + .Setup(s => s.Map(It.IsAny(), It.IsAny(), It.IsAny(), null)) + .Returns(new RemoteMovie + { + Release = new ReleaseInfo { Title = "A Movie 1998" }, + Movie = new Movie() { Id = 3 }, + ParsedMovieInfo = new ParsedMovieInfo() + { + MovieTitles = new List { "A Movie" }, + Year = 1998 + } + }); + } + + [Test] + public void should_reuse_stable_queued_downloading_tracked_download() + { + GivenTrackedDownloadCanBeMapped(); + + var client = CreateDownloadClient(); + var item = CreateDownloadItem(DownloadItemStatus.Queued); + var updatedItem = CreateDownloadItem(DownloadItemStatus.Queued); + updatedItem.RemainingSize = 250; + + var trackedDownload = Subject.TrackDownload(client, item); + var refreshedTrackedDownload = Subject.TrackDownload(client, updatedItem); + + refreshedTrackedDownload.Should().BeSameAs(trackedDownload); + refreshedTrackedDownload.DownloadItem.Should().BeSameAs(updatedItem); + + Mocker.GetMock() + .Verify(s => s.FindByDownloadId(It.IsAny()), Times.Once()); + + Mocker.GetMock() + .Verify(s => s.Map(It.IsAny(), It.IsAny(), It.IsAny(), null), Times.Once()); + } + + [Test] + public void should_reuse_stable_paused_downloading_tracked_download() + { + GivenTrackedDownloadCanBeMapped(); + + var client = CreateDownloadClient(); + var item = CreateDownloadItem(DownloadItemStatus.Paused); + var updatedItem = CreateDownloadItem(DownloadItemStatus.Paused); + updatedItem.RemainingSize = 250; + + var trackedDownload = Subject.TrackDownload(client, item); + var refreshedTrackedDownload = Subject.TrackDownload(client, updatedItem); + + refreshedTrackedDownload.Should().BeSameAs(trackedDownload); + refreshedTrackedDownload.DownloadItem.Should().BeSameAs(updatedItem); + + Mocker.GetMock() + .Verify(s => s.FindByDownloadId(It.IsAny()), Times.Once()); + + Mocker.GetMock() + .Verify(s => s.Map(It.IsAny(), It.IsAny(), It.IsAny(), null), Times.Once()); + } + + [Test] + public void should_reprocess_when_waiting_download_starts_downloading() + { + GivenTrackedDownloadCanBeMapped(); + + var client = CreateDownloadClient(); + var item = CreateDownloadItem(DownloadItemStatus.Queued); + var updatedItem = CreateDownloadItem(DownloadItemStatus.Downloading); + + Subject.TrackDownload(client, item); + Subject.TrackDownload(client, updatedItem); + + Mocker.GetMock() + .Verify(s => s.FindByDownloadId(It.IsAny()), Times.Exactly(2)); + + Mocker.GetMock() + .Verify(s => s.Map(It.IsAny(), It.IsAny(), It.IsAny(), null), Times.Exactly(2)); + } + + [Test] + public void should_reprocess_when_waiting_download_identity_changes() + { + GivenTrackedDownloadCanBeMapped(); + + var client = CreateDownloadClient(); + var item = CreateDownloadItem(DownloadItemStatus.Queued); + var updatedItem = CreateDownloadItem(DownloadItemStatus.Queued); + updatedItem.TotalSize = 2000; + + Subject.TrackDownload(client, item); + Subject.TrackDownload(client, updatedItem); + + Mocker.GetMock() + .Verify(s => s.FindByDownloadId(It.IsAny()), Times.Exactly(2)); + + Mocker.GetMock() + .Verify(s => s.Map(It.IsAny(), It.IsAny(), It.IsAny(), null), Times.Exactly(2)); + } + [Test] public void should_track_downloads_using_the_source_title_if_it_cannot_be_found_using_the_download_title() { diff --git a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs index 96d28c07e4..b009066c00 100644 --- a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs +++ b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs @@ -96,7 +96,7 @@ public TrackedDownload TrackDownload(DownloadClientDefinition downloadClient, Do { var existingItem = Find(downloadItem.DownloadId); - if (existingItem != null && existingItem.State != TrackedDownloadState.Downloading) + if (existingItem != null && CanReuseTrackedDownload(existingItem, downloadItem)) { LogItemChange(existingItem, existingItem.DownloadItem, downloadItem); @@ -221,6 +221,32 @@ public void UpdateTrackable(List trackedDownloads) } } + private static bool CanReuseTrackedDownload(TrackedDownload existingItem, DownloadClientItem downloadItem) + { + if (existingItem.State != TrackedDownloadState.Downloading) + { + return true; + } + + return IsStableWaitingDownload(downloadItem) && + HasSameDownloadIdentity(existingItem.DownloadItem, downloadItem); + } + + private static bool IsStableWaitingDownload(DownloadClientItem downloadItem) + { + return downloadItem.Status == DownloadItemStatus.Queued || + downloadItem.Status == DownloadItemStatus.Paused; + } + + private static bool HasSameDownloadIdentity(DownloadClientItem existingItem, DownloadClientItem downloadItem) + { + return existingItem.DownloadId == downloadItem.DownloadId && + existingItem.Title == downloadItem.Title && + existingItem.Category == downloadItem.Category && + existingItem.TotalSize == downloadItem.TotalSize && + existingItem.DownloadClientInfo?.Id == downloadItem.DownloadClientInfo?.Id; + } + private void UpdateCachedItem(TrackedDownload trackedDownload) { var parsedMovieInfo = Parser.Parser.ParseMovieTitle(trackedDownload.DownloadItem.Title);