diff --git a/src/Chaptarr.Api.V1/Author/AuthorController.cs b/src/Chaptarr.Api.V1/Author/AuthorController.cs index 08ee39ea..dff10f6e 100644 --- a/src/Chaptarr.Api.V1/Author/AuthorController.cs +++ b/src/Chaptarr.Api.V1/Author/AuthorController.cs @@ -213,6 +213,8 @@ public AuthorController(IBroadcastSignalRMessage signalRBroadcaster, PostValidator.RuleFor(s => s.ForeignAuthorId).NotEmpty().SetValidator(authorExistsValidator); PutValidator.RuleFor(s => s.Path).IsValidPath(); + PutValidator.RuleFor(s => s.AudiobookPath).IsValidPath().When(s => s.AudiobookPath.IsNotNullOrWhiteSpace()); + PutValidator.RuleFor(s => s.EbookPath).IsValidPath().When(s => s.EbookPath.IsNotNullOrWhiteSpace()); } private bool IsImportActive() diff --git a/src/Chaptarr.Api.V1/Author/AuthorResource.cs b/src/Chaptarr.Api.V1/Author/AuthorResource.cs index 091988f9..6b8f4c10 100644 --- a/src/Chaptarr.Api.V1/Author/AuthorResource.cs +++ b/src/Chaptarr.Api.V1/Author/AuthorResource.cs @@ -67,6 +67,10 @@ public class AuthorResource : RestResource // Readarr/Seerr compatibility (monitor mode string, e.g. "none") public string MonitorNewItems { get; set; } public string Folder { get; set; } + // Writable per-media author paths. The edit UI submits these; AudiobookFolder/ + // EbookFolder below remain the read-only display values set by the controller. + public string AudiobookPath { get; set; } + public string EbookPath { get; set; } public string AudiobookFolder { get; set; } public string EbookFolder { get; set; } public List Genres { get; set; } @@ -131,6 +135,8 @@ public static AuthorResource ToResource(this NzbDrone.Core.Books.Author model, R Images = displayImages.JsonClone(), Path = model.Path, + AudiobookPath = model.AudiobookPath, + EbookPath = model.EbookPath, AudiobookQualityProfileId = model.AudiobookQualityProfileId, EbookQualityProfileId = model.EbookQualityProfileId, MetadataProfileId = model.MetadataProfileId, @@ -463,6 +469,8 @@ public static NzbDrone.Core.Books.Author ToModel(this AuthorResource resource, R //AlternateTitles Path = resource.Path, + AudiobookPath = resource.AudiobookPath, + EbookPath = resource.EbookPath, AudiobookQualityProfileId = audiobookQualityProfileId, EbookQualityProfileId = ebookQualityProfileId, MetadataProfileId = resource.MetadataProfileId, @@ -568,6 +576,16 @@ private static void PreserveStoredAuthorStateForFacadeUpdate(AuthorResource reso updatedAuthor.Path = storedAuthor.Path; } + if (resource.AudiobookPath == null) + { + updatedAuthor.AudiobookPath = storedAuthor.AudiobookPath; + } + + if (resource.EbookPath == null) + { + updatedAuthor.EbookPath = storedAuthor.EbookPath; + } + if (resource.AddOptions == null) { updatedAuthor.AddOptions = storedAuthor.AddOptions; diff --git a/src/Chaptarr.Core.Test/Books/AuthorResourceMapperFixture.cs b/src/Chaptarr.Core.Test/Books/AuthorResourceMapperFixture.cs index 21df66dc..77a60632 100644 --- a/src/Chaptarr.Core.Test/Books/AuthorResourceMapperFixture.cs +++ b/src/Chaptarr.Core.Test/Books/AuthorResourceMapperFixture.cs @@ -99,6 +99,53 @@ public void should_not_wipe_tri_state_monitoring_fields_when_not_provided_on_put Assert.That(updated.EbookSettingsManuallyOverridden, Is.False); } + [Test] + public void should_apply_media_paths_on_put_update() + { + var existing = new NzbDrone.Core.Books.Author + { + Id = 1, + Name = "J.R.R. Tolkien", + AudiobookPath = "/audiobooks/J.R.R. Tolkien", + EbookPath = "/ebooks/J.R.R. Tolkien" + }; + + var resource = new AuthorResource + { + Id = 1, + AuthorName = "J.R.R. Tolkien", + AudiobookPath = "/audiobooks/J. R. R. Tolkien" + }; + + var updated = resource.ToModel(existing); + + Assert.That(updated.AudiobookPath, Is.EqualTo("/audiobooks/J. R. R. Tolkien")); + Assert.That(updated.EbookPath, Is.EqualTo("/ebooks/J.R.R. Tolkien"), "an omitted media path must keep its stored value"); + } + + [Test] + public void should_not_wipe_media_paths_when_not_provided_on_put_update() + { + var existing = new NzbDrone.Core.Books.Author + { + Id = 1, + Name = "J.R.R. Tolkien", + AudiobookPath = "/audiobooks/J.R.R. Tolkien", + EbookPath = "/ebooks/J.R.R. Tolkien" + }; + + var resource = new AuthorResource + { + Id = 1, + AuthorName = "J.R.R. Tolkien" + }; + + var updated = resource.ToModel(existing); + + Assert.That(updated.AudiobookPath, Is.EqualTo("/audiobooks/J.R.R. Tolkien")); + Assert.That(updated.EbookPath, Is.EqualTo("/ebooks/J.R.R. Tolkien")); + } + [Test] public void should_apply_per_type_metadata_profiles_on_put_update() { diff --git a/src/NzbDrone.Core/Books/Model/Author.cs b/src/NzbDrone.Core/Books/Model/Author.cs index 18658aa4..47d4fcde 100644 --- a/src/NzbDrone.Core/Books/Model/Author.cs +++ b/src/NzbDrone.Core/Books/Model/Author.cs @@ -328,6 +328,11 @@ public override void ApplyChanges(Author other) AddOptions = other.AddOptions; AudiobookRootFolderPath = other.AudiobookRootFolderPath; EbookRootFolderPath = other.EbookRootFolderPath; + + // Per-media author paths: null means the caller did not provide a value, + // so keep the stored one (an explicit clear arrives as an empty string). + AudiobookPath = other.AudiobookPath ?? AudiobookPath; + EbookPath = other.EbookPath ?? EbookPath; Monitored = other.Monitored; // TRI-STATE MONITORING SYSTEM - Copy only when explicitly provided (not null). // This prevents partial updates from wiping existing monitoring values.