Repository navigation
Conversation
MP3 -> M4B conversion only ran for files arriving from a tracked download client, so a collection that predates Chaptarr could never be normalised to one format. ConvertBookGroupIfNeeded returned early whenever there was no DownloadClientItem, and nothing else could reach the converter. The download id was only ever a correlation key, so it is now synthesised from the book and edition when no download client owns the import. That alone lets a manual import convert. On top of it, a ConvertBookFiles command (and a bulk ConvertAuthor) hands existing BookFile rows back to the import pipeline as a library conversion, reusing ffmpeg detection, the workspace free-space check, the concurrency semaphore, tagging, chapters, destination naming and the recycle bin rather than duplicating any of it. A library conversion replaces exactly the files it converted. It does not inherit the manual-import behaviour of replacing every other file on the book, so converting an audiobook leaves the book's eBook and any second audiobook edition alone. It also bypasses quality gating, since the profile that wants everything as M4B is usually the one that disallows MP3. Selection is per book in the UI and expanded to the whole edition in the service: every convertible file of a book becomes one output file, so converting a subset would leave the book half MP3 and half M4B. - GET /api/v1/convert?authorId=&bookId= previews what would convert and why a file would be skipped - Convert Files toolbar action on the author and book detail pages - Detached conversion jobs stay limited to tracked downloads, which is the only path that has a way to resume the import Fixes Chaptarr#109 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GXPbRG8Geef4zMeMQgpQ8H
The footer select-all checkbox passed a custom className to CheckInput without composing the base input class, so the checkbox lost its size and rendered as a thin sliver. Compose from CheckInput.css the same way the Organize modal does, and fix the property order stylelint flagged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GXPbRG8Geef4zMeMQgpQ8H
|
Ty for this. One downside of this that I see is that I believe this will reset all stats for these books in downstrem consumers/libraries like ABS, etc. So for instance, if you're half way through your audiobook and convert it to m4b from mp3 in chaptarr, your progress is gone. But, if you use ABS (not sure, I'll have to check, or you can and reply that's helpful!) if you convert that same book it keeps/stores your progress? I'm sure there's ways around it, or some users just won't care, but something to consider. |
|
I will have to test that thoroughly, but my use of ABS is long as the directory doesn't change, causing it to re-index or create a new entry in ABS. I don't lose my progress. But I've done this inside of ABS using its ability to merge M4B. |
A library conversion wrote the M4B to the folder the naming scheme calls for. When a library predates Chaptarr, that is usually a different folder from the one the MP3s were in, and tools that key books by folder see a new book: Audiobookshelf marked the old item missing and created a new one with no listening progress. Converting in place, which is what Audiobookshelf's own M4B tool does, keeps the same item and the listener's position. Library conversions now write to the folder the source files share, or to their parent when they sit in disc subfolders (CD1, Disc 2), and apply naming to the file name only. Anything that does not form one book folder inside the author's root folder falls back to normal naming. Downloads and manual imports are unchanged, and Organize still moves a book to the naming scheme on request. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GXPbRG8Geef4zMeMQgpQ8H
|
Thanks, good catch. I tested it against Audiobookshelf, and you were right for one specific case. That case is now fixed in the PR. Short version: Audiobookshelf ties listening progress to the book's folder (its library item), not to the audio files inside it. Progress survives a conversion as long as the M4B stays in the folder the MP3s were in. It's lost when the M4B lands in a different folder, because ABS then sees a brand-new book, and the old one shows as missing. The PR originally put the converted M4B where Chaptarr's naming scheme says the book should live. For a library that predates Chaptarr (the main audience for #109), that's often a different folder name from the existing one, so progress was lost in exactly the case that matters most. I've pushed a fix: library conversions now happen in place, the same way ABS's own M4B tool does it. ResultsABS 2.36.1, same library folder as Chaptarr. Each book had progress set at 1:15 of 2:00 before converting:
So after the fix, converting in Chaptarr behaves the same as converting in ABS as far as progress goes. What changedNew commit: Keep library conversions in the book's existing folder
How it was tested
Caveats
(Tested with Claude Code, same as the rest of the PR.) |
|
ty for the dd. I'll take a look! |
A library conversion ran as a task with a single static message, so a long book gave no sign of how far along it was, and there was no way to stop it: the conversion handler ignored the task's cancellation token, so Cancel in System > Tasks had no effect. The converter already reports its progress to the conversion tracker, but from its own threads, where the task's status message cannot see it. While a book converts, poll the tracker and mirror the percentage into the task message, so it shows in the sidebar and in System > Tasks. The handlers now take the task's cancellation token and pass it to the import, which links it to the converter's own cancellation. Cancelling the task stops m4b-tool, cleans up the work folder, and leaves the book's original files untouched, since nothing is replaced until a conversion finishes. A multi-book run stops before starting the next book. The tracking id is now built by one shared helper, so the import pipeline and the conversion service cannot disagree on it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GXPbRG8Geef4zMeMQgpQ8H
|
Pushed one more commit: Show conversion progress and let a running conversion be cancelled I found this while testing on my own Synology library. A long conversion only showed "Converting 'Harvest' to M4B (1/1)", with no progress. Cancel in System → Tasks did nothing, because the conversion handler ignored the task's cancellation token. What changed
Tested in Docker with the real ffmpeg/m4b-tool, using a 2-hour, 6-file MP3 book:
The percentage jumps rather than creeping, since m4b-tool encodes several files in parallel, and it holds in the 90s while it merges and writes chapters. (Built and tested with Claude Code, same as the rest of the PR.) |
A multi-file conversion is merged into one M4B that has no part number, so the import names it "Title.m4b". The destination preview copied the first source file's part fields (1 of N), so it was named "Title - 01.m4b" (or "Title (1).m4b"). The conflict check before conversion looked at that path, found nothing, and a taken destination was only found after a full conversion. The preview now carries the same part values as the converted file. The check finds the real destination before any conversion runs, and the work folder output gets the final name. Fixes Chaptarr#284 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GXPbRG8Geef4zMeMQgpQ8H
Description
MP3 → M4B conversion only ran for files arriving from a tracked download client, so a
collection that predates Chaptarr could never be normalised to one format.
ImportApprovedBooks.ConvertBookGroupIfNeededreturned early whenever there was noDownloadClientItem, and nothing else could reach the converter.This implements both suggestions from #109:
The download id is now synthesised when no download client owns the import. It was
only ever a correlation key for progress tracking and the
.chaptarr-conversionsworkfolder, so it is derived from the book and edition instead (
chaptarr-local-{bookId}-{editionId}).Keeping it stable means a retried conversion still finds the artifact retained by the
previous attempt. On its own this makes manual import convert — see Behaviour changes.
A
ConvertBookFilescommand (plus a bulkConvertAuthor), scoped likeRenameFiles.BookFileConversionServicetakes existingBookFilerows, rebuilds import decisions withthe book and edition already known, and hands them back to the import pipeline flagged as a
library conversion. That reuses ffmpeg detection, the workspace free-space check, the
concurrency semaphore, tagging, chapter insertion, destination naming and the recycle bin
rather than duplicating any of it.
Also added:
GET /api/v1/convert?authorId=&bookId=— previews what would convert, and why a file wouldbe skipped (no conversion target on the profile, target not allowed by the profile, already
M4B, missing from disk, Calibre-managed).
modelled on the existing Organize/Retag modals.
Three design decisions worth reviewing:
was tempting, but
manualReplaceExistingsweeps in every other file on the book —converting an audiobook would have deleted the book's eBook. Hence the new
LocalBook.IsLibraryConversionflag, with replacement scoped to the conversion's own sourcepaths.
profile that disallows MP3, so gating would reject the library's own files before they
reached the converter. This matches how manual imports and forced grabs are already treated.
file, so converting 5 of 18 tracks would leave the book half MP3 and half M4B. The modal
selects whole books, and the service expands a partial
filesselection to the whole edition(pulling in only siblings that would convert anyway).
Detached conversion jobs remain limited to tracked downloads. They signal completion by pushing
ProcessMonitoredDownloadsCommand, which has no way to resume a library conversion, so manualimports and library conversions convert inline on the calling thread — the path manual imports
would already have taken.
Behaviour changes
directly from fix (1) and is what
ConvertToQualityIdreads like it promises, but it is achange for anyone currently relying on manual import bypassing conversion. Happy to gate it
behind a separate opt-in if you would rather it stayed opt-in.
DownloadedBooksScanof a folder with no tracked download now converts too, for the samereason — it is the other caller that passes a null
DownloadClientItem. This matches theprofile setting's own help text ("After download, convert compatible files…"). Note that the
hasRejectedTrackedDownloadDecisionsguard only applies to tracked downloads, so an untrackedfolder with some unmatched files will convert the files that did match.
DiskScanServicegoes throughImportOrchestratorandnever calls
ImportApprovedBooks.Import, so a scheduled rescan will not start converting anexisting library on its own.
ImportApprovedBooks.Importhas exactly three callers:ManualImportService,DownloadedBooksImportService, and the newBookFileConversionService.ConvertBookFiles/ConvertAuthordeclareRequiresDiskAccessandIsLongRunning, so alarge conversion run serialises against other disk commands (rescan, rename, download import)
the same way
RenameAuthordoes. That is deliberate — it is writing into the library — butit does mean a long run holds the disk lane.
On the "adjacent" point in #109:
ConvertMp3ToM4bis already mostly neutralised — migration 076clears it and
QualityProfileResourcederives it fromConvertToQualityId. Only the DB columnand two
en.jsonstrings linger. I left it alone rather than touch migrations in this PR.Fixes #109
Database Migration
NO. No schema changes, no new migration.
ConversionJobrows are unchanged — libraryconversions do not create them.
How was this tested?
Built and tested from source on Linux (.NET 10.0.401, Node 22):
dotnet build src/NzbDrone.Core/Chaptarr.Core.csproj— cleandotnet build src/Chaptarr.Api.V1/Chaptarr.Api.V1.csproj— cleandotnet test src/Chaptarr.Core.Test/Chaptarr.Core.Test.csproj— 3033 passed, 0 failed,including the 13 new tests
yarn build— compiles cleanyarn typecheck— cleanyarn check-translations— alltranslate()keys present inen.jsonyarn lint— no new errors introduced (verified per-file against the pre-change baseline;the new
frontend/src/Convert/files are lint-clean)stylelint— clean on the newfrontend/src/Convert/*.cssNew test fixture
BookFileConversionServiceFixture(13 tests) covers eligibility and theskip reasons, per-edition grouping, the
IsLibraryConversion/replaceExistingcontract,whole-book expansion from a partial selection, and not pulling in non-convertible siblings.
End to end, in Docker. Built the image with the repo's own
Dockerfile.buildfrom thisbranch, exported without
.git(the same as building from a GitHub tarball). The only changeswere two local build workarounds, neither part of this PR: my sandbox's proxy CA certificate,
and the
NuGetAuditworkaround described below. Ran it with a fresh config, and drove it through both the API and the real UI in headless Chromium. The imageincludes the real ffmpeg, m4b-tool and mp4v2 tools, so these are genuine conversions:
ConvertBookFilessent with only 1 of the 3 files selected.chaptarr-conversionsfolder left behind.filesToReplacenarrowing exists for.{"name":"ConvertBookFiles","authorId":1,"bookId":3,"files":[…4 ids]}; the book goes from 4 MP3 files to 1 M4B.bookIdand converts only the selected book.The UI run caught one bug of mine, a mis-styled select-all checkbox in the modal, fixed in the
second commit.
Notes for reviewers:
Dockerfile.buildfails ondevelopright now, independent of this PR.dotnet restorestops with
NU1902: Warning As Error: Package 'Microsoft.Build.Tasks.Git' 8.0.0 has a known moderate severity vulnerability(GHSA-23fw-v26w-5fgq). It comes in viaMicrosoft.SourceLink.GitHub8.0.0 plusTreatWarningsAsErrors, and it fails the same way onthe untouched
developcommit. To build for testing I added/p:NuGetAudit=falseto that onerestore line locally; it is not part of this PR. Bumping SourceLink probably fixes it, and is
worth its own PR.
AuthorService's 10-second author cache. That is pre-existing and affects everyprofile-dependent feature.
(
….mp3.chaptarr-upgrade~<guid>), so restoring one means renaming it.empty. Cosmetic; happy to add cleanup if you want it here.
memory on long books are the existing converter's behaviour, which this PR does not change.
Screenshots (UI changes only)
Convert Files on the book page, next to Preview Rename and Preview Retag. Before: 4 MP3 files.
The preview modal at author level: one convertible book, others already M4B with the reason shown.
After converting: the book has a single M4B.
A note on AI: taking you up on the disclosure paragraph in the template — this change was
written with Claude Code (Claude Agent SDK), using
Claude Opus 5 (
claude-opus-5) atxhighreasoning effort. The investigation, designdecisions, implementation, tests and this description were all AI-generated; I reviewed them
before opening the PR. Please scrutinise it as hard as you like — the three design decisions
called out above and the manual-import behaviour change are the places I'd start. The
end-to-end runs above used synthetic MP3s, so a real multi-hour audiobook is the remaining gap.