diff --git a/src/Chaptarr.Core.Test/Download/QBittorrentDuplicateHandlingFixture.cs b/src/Chaptarr.Core.Test/Download/QBittorrentDuplicateHandlingFixture.cs index 239a4824..c5b96cb3 100644 --- a/src/Chaptarr.Core.Test/Download/QBittorrentDuplicateHandlingFixture.cs +++ b/src/Chaptarr.Core.Test/Download/QBittorrentDuplicateHandlingFixture.cs @@ -43,12 +43,19 @@ private class TestProxy : IQBittorrentProxy public QBittorrentPreferences GetConfig(QBittorrentSettings settings) => new QBittorrentPreferences { DhtEnabled = true }; - public List GetTorrents(QBittorrentSettings settings, string category = null) => throw new NotImplementedException(); + public Dictionary> HashesByCategory { get; } = new(StringComparer.OrdinalIgnoreCase); + public bool TorrentLoaded { get; set; } + + public List GetTorrents(QBittorrentSettings settings, string category = null) + { + var hashes = category != null && HashesByCategory.TryGetValue(category, out var found) ? found : new List(); + return hashes.ConvertAll(hash => new QBittorrentTorrent { Hash = hash }); + } public bool IsTorrentLoaded(string hash, QBittorrentSettings settings) { LoadedHashes.Add(hash); - return false; + return TorrentLoaded; } public QBittorrentTorrentProperties GetTorrentProperties(string hash, QBittorrentSettings settings) => throw new NotImplementedException(); @@ -242,11 +249,84 @@ public void should_surface_non_conflict_errors_unchanged() Assert.Multiple(() => { Assert.That(exception, Is.SameAs(original)); + Assert.That(proxy.Labels, Is.Empty); + }); + } + + [Test] + public void should_continue_with_existing_torrent_already_in_a_chaptarr_category() + { + var proxy = new TestProxy { AddException = new DownloadClientException("qBittorrent rejected the torrent add request (response: Fails.).") }; + proxy.HashesByCategory["ebooks"] = new List { "abcdef1234" }; + var client = CreateClient(proxy); + + var result = client.AddTorrentFile(CreateRemoteBook(), "ABCDEF1234"); + + Assert.Multiple(() => + { + Assert.That(result, Is.EqualTo("ABCDEF1234")); Assert.That(proxy.LoadedHashes, Is.Empty); Assert.That(proxy.Labels, Is.Empty); }); } + [Test] + public void should_move_existing_torrent_back_from_post_import_category() + { + var proxy = new TestProxy { AddException = new DownloadClientException("qBittorrent rejected the torrent add request (response: Fails.).") }; + proxy.HashesByCategory["ebooks-imported"] = new List { "abcdef1234" }; + var client = CreateClient(proxy); + + client.AddMagnet(CreateRemoteBook(), "ABCDEF1234"); + + Assert.That(proxy.Labels, Is.EqualTo(new[] { ("abcdef1234", "ebooks") })); + } + + [Test] + public void should_reject_without_adopting_when_existing_torrent_is_outside_chaptarr_categories() + { + var proxy = new TestProxy + { + AddException = new DownloadClientException("qBittorrent rejected the torrent add request (response: Fails.)."), + TorrentLoaded = true + }; + var client = CreateClient(proxy); + + var exception = Assert.Throws(() => client.AddTorrentFile(CreateRemoteBook(), "ABCDEF1234")); + + Assert.Multiple(() => + { + Assert.That(exception.Message, Is.EqualTo("qBittorrent already has this torrent outside Chaptarr's categories")); + Assert.That(proxy.Labels, Is.Empty); + }); + } + + private static TestQBittorrent CreateClient(TestProxy proxy) + { + return new TestQBittorrent(new TestProxySelector(proxy, new Version(2, 11, 0)), new CacheManager(), LogManager.GetCurrentClassLogger()) + { + Definition = new DownloadClientDefinition + { + Name = "qBittorrent", + Settings = new QBittorrentSettings + { + EbookCategory = "ebooks", + AudiobookCategory = "audiobooks", + EbookImportedCategory = "ebooks-imported" + } + } + }; + } + + private static RemoteBook CreateRemoteBook() + { + return new RemoteBook + { + Books = new List { new Book { MediaType = BookMediaType.Ebook } }, + Release = new TorrentInfo() + }; + } + private static DownloadClientException ConflictException() { var request = new HttpRequest("http://localhost/api/v2/torrents/add"); diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs index fa2b9151..14d8feb6 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs @@ -91,6 +91,57 @@ public bool ShouldPreserveItemAfterImport(DownloadClientItem downloadClientItem) Settings.EbookImportedCategory); } + private bool TryAdoptExistingTorrent(RemoteBook remoteBook, string hash, string category, DownloadClientException addException) + { + if (hash.IsNullOrWhiteSpace()) + { + return false; + } + + string existingCategory; + bool existsElsewhere; + + try + { + existingCategory = new[] { Settings.AudiobookCategory, Settings.EbookCategory, Settings.AudiobookImportedCategory, Settings.EbookImportedCategory } + .Where(c => c.IsNotNullOrWhiteSpace()) + .Distinct(StringComparer.InvariantCultureIgnoreCase) + .FirstOrDefault(c => Proxy.GetTorrents(Settings, c).Any(t => hash.Equals(t.Hash, StringComparison.OrdinalIgnoreCase))); + + existsElsewhere = existingCategory == null && Proxy.IsTorrentLoaded(hash.ToLower(), Settings); + } + catch (Exception ex) + { + _logger.Debug(ex, "Unable to check whether torrent {0} already exists in qBittorrent", hash); + return false; + } + + if (existsElsewhere) + { + throw new DownloadClientRejectedReleaseException(remoteBook.Release, "qBittorrent already has this torrent outside Chaptarr's categories", addException); + } + + if (existingCategory == null) + { + return false; + } + + if (category.IsNotNullOrWhiteSpace() && !category.Equals(existingCategory, StringComparison.InvariantCultureIgnoreCase)) + { + try + { + Proxy.SetTorrentLabel(hash.ToLower(), category, Settings); + } + catch (DownloadClientException ex) + { + _logger.Warn(ex, "Failed to move existing torrent {0} from category \"{1}\" to \"{2}\" in qBittorrent.", hash, existingCategory, category); + } + } + + _logger.Info("qBittorrent already has torrent {0} in Chaptarr category \"{1}\"; continuing with the existing download instead of failing the grab", hash, existingCategory); + return true; + } + protected override string AddFromMagnetLink(RemoteBook remoteBook, string hash, string magnetLink) { if (!Proxy.GetConfig(Settings).DhtEnabled && !magnetLink.Contains("&tr=")) @@ -113,6 +164,13 @@ protected override string AddFromMagnetLink(RemoteBook remoteBook, string hash, { throw new DownloadClientRejectedReleaseException(remoteBook.Release, "qBittorrent rejected the magnet link due to a conflict", ex); } + catch (DownloadClientException ex) + { + if (!TryAdoptExistingTorrent(remoteBook, hash, category, ex)) + { + throw; + } + } if ((!addHasSetShareLimits && setShareLimits) || moveToTop || forceStart) { @@ -178,6 +236,13 @@ protected override string AddFromTorrentFile(RemoteBook remoteBook, string hash, { throw new DownloadClientRejectedReleaseException(remoteBook.Release, "qBittorrent rejected the torrent file due to a conflict", ex); } + catch (DownloadClientException ex) + { + if (!TryAdoptExistingTorrent(remoteBook, hash, category, ex)) + { + throw; + } + } if ((!addHasSetShareLimits && setShareLimits) || moveToTop || forceStart) {