From 0274778679a8fd485a651eea9d293463528244fd Mon Sep 17 00:00:00 2001
From: Mark McDowall <mark@mcdowall.ca>
Date: Sat, 19 Aug 2023 21:49:17 -0700
Subject: [PATCH] Fixed: Don't reimport the same file from the same release
 unless grabbed again

Closes #5625
---
 .../AlreadyImportedSpecificationFixture.cs    | 129 ++++++++++++++++++
 .../AlreadyImportedSpecification.cs           |  29 ++--
 2 files changed, 147 insertions(+), 11 deletions(-)
 create mode 100644 src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/AlreadyImportedSpecificationFixture.cs

diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/AlreadyImportedSpecificationFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/AlreadyImportedSpecificationFixture.cs
new file mode 100644
index 000000000..9e276d909
--- /dev/null
+++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeImport/Specifications/AlreadyImportedSpecificationFixture.cs
@@ -0,0 +1,129 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using FizzWare.NBuilder;
+using FluentAssertions;
+using Moq;
+using NUnit.Framework;
+using NzbDrone.Core.Download;
+using NzbDrone.Core.History;
+using NzbDrone.Core.MediaFiles.EpisodeImport.Specifications;
+using NzbDrone.Core.Parser.Model;
+using NzbDrone.Core.Test.Framework;
+using NzbDrone.Core.Tv;
+using NzbDrone.Test.Common;
+
+namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Specifications
+{
+    [TestFixture]
+    public class AlreadyImportedSpecificationFixture : CoreTest<AlreadyImportedSpecification>
+    {
+        private Series _series;
+        private Episode _episode;
+        private LocalEpisode _localEpisode;
+        private DownloadClientItem _downloadClientItem;
+
+        [SetUp]
+        public void Setup()
+        {
+            _series = Builder<Series>.CreateNew()
+                                     .With(s => s.SeriesType = SeriesTypes.Standard)
+                                     .With(s => s.Path = @"C:\Test\TV\30 Rock".AsOsAgnostic())
+                                     .Build();
+
+            _episode = Builder<Episode>.CreateNew()
+                .With(e => e.SeasonNumber = 1)
+                .With(e => e.AirDateUtc = DateTime.UtcNow)
+                .Build();
+
+            _localEpisode = new LocalEpisode
+                                {
+                                    Path = @"C:\Test\Unsorted\30 Rock\30.rock.s01e01.avi".AsOsAgnostic(),
+                                    Episodes = new List<Episode> { _episode },
+                                    Series = _series
+                                };
+
+            _downloadClientItem = Builder<DownloadClientItem>.CreateNew()
+                .Build();
+        }
+
+        private void GivenHistory(List<EpisodeHistory> history)
+        {
+            Mocker.GetMock<IHistoryService>()
+                .Setup(s => s.FindByEpisodeId(It.IsAny<int>()))
+                .Returns(history);
+        }
+
+        [Test]
+        public void should_accepted_if_download_client_item_is_null()
+        {
+            Subject.IsSatisfiedBy(_localEpisode, null).Accepted.Should().BeTrue();
+        }
+
+        [Test]
+        public void should_accept_if_episode_does_not_have_file()
+        {
+            _episode.EpisodeFileId = 0;
+
+            Subject.IsSatisfiedBy(_localEpisode, _downloadClientItem).Accepted.Should().BeTrue();
+        }
+
+        [Test]
+        public void should_accept_if_episode_has_not_been_imported()
+        {
+            var history = Builder<EpisodeHistory>.CreateListOfSize(1)
+                .All()
+                .With(h => h.EpisodeId = _episode.Id)
+                .With(h => h.EventType = EpisodeHistoryEventType.Grabbed)
+                .Build()
+                .ToList();
+
+            GivenHistory(history);
+
+            Subject.IsSatisfiedBy(_localEpisode, _downloadClientItem).Accepted.Should().BeTrue();
+        }
+
+        [Test]
+        public void should_accept_if_episode_was_grabbed_after_being_imported()
+        {
+            var history = Builder<EpisodeHistory>.CreateListOfSize(3)
+                .All()
+                .With(h => h.EpisodeId = _episode.Id)
+                .TheFirst(1)
+                .With(h => h.EventType = EpisodeHistoryEventType.Grabbed)
+                .With(h => h.Date = DateTime.UtcNow)
+                .TheNext(1)
+                .With(h => h.EventType = EpisodeHistoryEventType.DownloadFolderImported)
+                .With(h => h.Date = DateTime.UtcNow.AddDays(-1))
+                .TheNext(1)
+                .With(h => h.EventType = EpisodeHistoryEventType.Grabbed)
+                .With(h => h.Date = DateTime.UtcNow.AddDays(-2))
+                .Build()
+                .ToList();
+
+            GivenHistory(history);
+
+            Subject.IsSatisfiedBy(_localEpisode, _downloadClientItem).Accepted.Should().BeTrue();
+        }
+
+        [Test]
+        public void should_reject_if_episode_imported_after_being_grabbed()
+        {
+            var history = Builder<EpisodeHistory>.CreateListOfSize(2)
+                .All()
+                .With(h => h.EpisodeId = _episode.Id)
+                .TheFirst(1)
+                .With(h => h.EventType = EpisodeHistoryEventType.DownloadFolderImported)
+                .With(h => h.Date = DateTime.UtcNow.AddDays(-1))
+                .TheNext(1)
+                .With(h => h.EventType = EpisodeHistoryEventType.Grabbed)
+                .With(h => h.Date = DateTime.UtcNow.AddDays(-2))
+                .Build()
+                .ToList();
+
+            GivenHistory(history);
+
+            Subject.IsSatisfiedBy(_localEpisode, _downloadClientItem).Accepted.Should().BeTrue();
+        }
+    }
+}
diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/AlreadyImportedSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/AlreadyImportedSpecification.cs
index e1ec23a40..891cef89a 100644
--- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/AlreadyImportedSpecification.cs
+++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/AlreadyImportedSpecification.cs
@@ -39,8 +39,11 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
                 }
 
                 var episodeHistory = _historyService.FindByEpisodeId(episode.Id);
-                var lastImported = episodeHistory.FirstOrDefault(h => h.EventType == EpisodeHistoryEventType.DownloadFolderImported);
-                var lastGrabbed = episodeHistory.FirstOrDefault(h => h.EventType == EpisodeHistoryEventType.Grabbed);
+                var lastImported = episodeHistory.FirstOrDefault(h =>
+                    h.DownloadId == downloadClientItem.DownloadId &&
+                    h.EventType == EpisodeHistoryEventType.DownloadFolderImported);
+                var lastGrabbed = episodeHistory.FirstOrDefault(h =>
+                    h.DownloadId == downloadClientItem.DownloadId && h.EventType == EpisodeHistoryEventType.Grabbed);
 
                 if (lastImported == null)
                 {
@@ -48,17 +51,21 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport.Specifications
                     continue;
                 }
 
-                // If the release was grabbed again after importing don't reject it
-                if (lastGrabbed != null && lastGrabbed.Date.After(lastImported.Date))
+                if (lastGrabbed != null)
                 {
-                    _logger.Trace("Episode file was grabbed again after importing");
-                    continue;
-                }
+                    // If the release was grabbed again after importing don't reject it
+                    if (lastGrabbed.Date.After(lastImported.Date))
+                    {
+                        _logger.Trace("Episode file was grabbed again after importing");
+                        continue;
+                    }
 
-                if (lastImported.DownloadId == downloadClientItem.DownloadId)
-                {
-                    _logger.Debug("Episode file previously imported at {0}", lastImported.Date);
-                    return Decision.Reject("Episode file already imported at {0}", lastImported.Date.ToLocalTime());
+                    // If the release was imported after the last grab reject it
+                    if (lastImported.Date.After(lastGrabbed.Date))
+                    {
+                        _logger.Debug("Episode file previously imported at {0}", lastImported.Date);
+                        return Decision.Reject("Episode file already imported at {0}", lastImported.Date.ToLocalTime());
+                    }
                 }
             }