From b7a38d773e9c05c359e20ce38b09a5981902d656 Mon Sep 17 00:00:00 2001 From: luke Date: Tue, 6 Oct 2026 20:09:27 +0000 Subject: [PATCH] Fix Kavita notification sending title prefix instead of folder path Kavita's library scan-folder endpoint expects an absolute directory path within a configured library. Previously, Chaptarr was sending $"{header} - {message}" (e.g. "Chaptarr - Book Downloaded - /media/Books/Author/Title"), causing Kavita's DirectoryService to fail with a 500 ArgumentOutOfRangeException when resolving parent directories back to root. This change passes the directory path directly without the branded title prefix, guards against empty book file lists, and corrects the log message. --- .../Notifications/Kavita/KavitaFixture.cs | 158 ++++++++++++++++++ .../Notifications/Kavita/Kavita.cs | 22 ++- .../Notifications/Kavita/KavitaService.cs | 2 +- .../Kavita/KavitaServiceProxy.cs | 2 +- 4 files changed, 175 insertions(+), 9 deletions(-) create mode 100644 src/Chaptarr.Core.Test/Notifications/Kavita/KavitaFixture.cs diff --git a/src/Chaptarr.Core.Test/Notifications/Kavita/KavitaFixture.cs b/src/Chaptarr.Core.Test/Notifications/Kavita/KavitaFixture.cs new file mode 100644 index 00000000..cc266a01 --- /dev/null +++ b/src/Chaptarr.Core.Test/Notifications/Kavita/KavitaFixture.cs @@ -0,0 +1,158 @@ +using System.Collections.Generic; +using System.IO; +using FluentValidation.Results; +using NLog; +using NUnit.Framework; +using NzbDrone.Core.Books; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Notifications; +using NzbDrone.Core.Notifications.Kavita; +using NzbDrone.Core.ThingiProvider; + +namespace Chaptarr.Core.Test.Notifications.Kavita +{ + [TestFixture] + public class KavitaFixture + { + private FakeKavitaService _kavitaService; + private NzbDrone.Core.Notifications.Kavita.Kavita _subject; + + [SetUp] + public void SetUp() + { + _kavitaService = new FakeKavitaService(); + _subject = new NzbDrone.Core.Notifications.Kavita.Kavita(_kavitaService, LogManager.GetCurrentClassLogger()) + { + Definition = new NotificationDefinition + { + Settings = new KavitaSettings + { + Host = "kavita", + Port = 5000, + ApiKey = "test-key", + Notify = true + } + } + }; + } + + [Test] + public void should_send_parent_folder_path_on_release_import() + { + var filePath = Path.Combine(Path.GetTempPath(), "Books", "Author", "Title", "book.epub"); + var expectedFolder = Path.GetDirectoryName(filePath); + + var message = new BookDownloadMessage + { + BookFiles = new List + { + new BookFile { Path = filePath } + } + }; + + _subject.OnReleaseImport(message); + + Assert.That(_kavitaService.NotifiedFolders, Has.Count.EqualTo(1)); + Assert.That(_kavitaService.NotifiedFolders[0], Is.EqualTo(expectedFolder)); + } + + [Test] + public void should_not_notify_on_release_import_with_empty_book_files() + { + var message = new BookDownloadMessage + { + BookFiles = new List() + }; + + Assert.DoesNotThrow(() => _subject.OnReleaseImport(message)); + Assert.That(_kavitaService.NotifiedFolders, Is.Empty); + } + + [Test] + public void should_send_parent_folder_path_on_book_delete() + { + var filePath = Path.Combine(Path.GetTempPath(), "Books", "Author", "Title", "book.epub"); + var expectedFolder = Path.GetDirectoryName(filePath); + + var book = new Book + { + BookFiles = new List + { + new BookFile { Path = filePath } + } + }; + var message = new BookDeleteMessage(book, true); + + _subject.OnBookDelete(message); + + Assert.That(_kavitaService.NotifiedFolders, Has.Count.EqualTo(1)); + Assert.That(_kavitaService.NotifiedFolders[0], Is.EqualTo(expectedFolder)); + } + + [Test] + public void should_send_parent_folder_path_on_book_file_delete() + { + var filePath = Path.Combine(Path.GetTempPath(), "Books", "Author", "Title", "book.epub"); + var expectedFolder = Path.GetDirectoryName(filePath); + + var message = new BookFileDeleteMessage + { + BookFile = new BookFile { Path = filePath } + }; + + _subject.OnBookFileDelete(message); + + Assert.That(_kavitaService.NotifiedFolders, Has.Count.EqualTo(1)); + Assert.That(_kavitaService.NotifiedFolders[0], Is.EqualTo(expectedFolder)); + } + + [Test] + public void should_send_parent_folder_path_on_book_retag() + { + var filePath = Path.Combine(Path.GetTempPath(), "Books", "Author", "Title", "book.epub"); + var expectedFolder = Path.GetDirectoryName(filePath); + + var message = new BookRetagMessage + { + BookFile = new BookFile { Path = filePath } + }; + + _subject.OnBookRetag(message); + + Assert.That(_kavitaService.NotifiedFolders, Has.Count.EqualTo(1)); + Assert.That(_kavitaService.NotifiedFolders[0], Is.EqualTo(expectedFolder)); + } + + [Test] + public void should_not_notify_when_notify_setting_is_disabled() + { + var settings = (KavitaSettings)_subject.Definition.Settings; + settings.Notify = false; + + var filePath = Path.Combine(Path.GetTempPath(), "Books", "Author", "Title", "book.epub"); + var message = new BookFileDeleteMessage + { + BookFile = new BookFile { Path = filePath } + }; + + _subject.OnBookFileDelete(message); + + Assert.That(_kavitaService.NotifiedFolders, Is.Empty); + } + + private class FakeKavitaService : IKavitaService + { + public List NotifiedFolders { get; } = new List(); + + public void Notify(KavitaSettings settings, string folderPath) + { + NotifiedFolders.Add(folderPath); + } + + public ValidationFailure Test(KavitaSettings settings, string message) + { + return null; + } + } + } +} diff --git a/src/NzbDrone.Core/Notifications/Kavita/Kavita.cs b/src/NzbDrone.Core/Notifications/Kavita/Kavita.cs index 93bbab8c..4bb323ff 100644 --- a/src/NzbDrone.Core/Notifications/Kavita/Kavita.cs +++ b/src/NzbDrone.Core/Notifications/Kavita/Kavita.cs @@ -24,15 +24,23 @@ public Kavita(IKavitaService kavitaService, Logger logger) public override void OnReleaseImport(BookDownloadMessage message) { var allPaths = message.BookFiles.Select(v => v.Path).Distinct(); - var path = Directory.GetParent(allPaths.First())?.FullName; - Notify(Settings, BOOK_DOWNLOADED_TITLE_BRANDED, path); + var firstPath = allPaths.FirstOrDefault(); + if (firstPath != null) + { + var path = Directory.GetParent(firstPath)?.FullName; + Notify(Settings, BOOK_DOWNLOADED_TITLE_BRANDED, path); + } } public override void OnBookDelete(BookDeleteMessage deleteMessage) { var allPaths = deleteMessage.Book.BookFiles.Select(v => v.Path).Distinct(); - var path = Directory.GetParent(allPaths.First())?.FullName; - Notify(Settings, BOOK_FILE_DELETED_TITLE_BRANDED, path); + var firstPath = allPaths.FirstOrDefault(); + if (firstPath != null) + { + var path = Directory.GetParent(firstPath)?.FullName; + Notify(Settings, BOOK_FILE_DELETED_TITLE_BRANDED, path); + } } public override void OnBookFileDelete(BookFileDeleteMessage message) @@ -60,14 +68,14 @@ private void Notify(KavitaSettings settings, string header, string message) { try { - if (Settings.Notify) + if (Settings.Notify && !string.IsNullOrWhiteSpace(message)) { - _kavitaService.Notify(Settings, $"{header} - {message}"); + _kavitaService.Notify(Settings, message); } } catch (SocketException ex) { - var logMessage = $"Unable to connect to Subsonic Host: {Settings.Host}:{Settings.Port}"; + var logMessage = $"Unable to connect to Kavita Host: {Settings.Host}:{Settings.Port}"; _logger.Debug(ex, logMessage); } } diff --git a/src/NzbDrone.Core/Notifications/Kavita/KavitaService.cs b/src/NzbDrone.Core/Notifications/Kavita/KavitaService.cs index 63983648..bc736b64 100644 --- a/src/NzbDrone.Core/Notifications/Kavita/KavitaService.cs +++ b/src/NzbDrone.Core/Notifications/Kavita/KavitaService.cs @@ -6,7 +6,7 @@ namespace NzbDrone.Core.Notifications.Kavita; public interface IKavitaService { - void Notify(KavitaSettings settings, string message); + void Notify(KavitaSettings settings, string folderPath); ValidationFailure Test(KavitaSettings settings, string message); } diff --git a/src/NzbDrone.Core/Notifications/Kavita/KavitaServiceProxy.cs b/src/NzbDrone.Core/Notifications/Kavita/KavitaServiceProxy.cs index 71c4ba6d..03be71d3 100644 --- a/src/NzbDrone.Core/Notifications/Kavita/KavitaServiceProxy.cs +++ b/src/NzbDrone.Core/Notifications/Kavita/KavitaServiceProxy.cs @@ -11,7 +11,7 @@ namespace NzbDrone.Core.Notifications.Kavita; public interface IKavitaServiceProxy { string GetBaseUrl(KavitaSettings settings, string relativePath = null); - void Notify(KavitaSettings settings, string message); + void Notify(KavitaSettings settings, string folderPath); string GetToken(KavitaSettings settings); }