Skip to content

feat: write description, genre, media type and gapless tags (#208) - #419

Merged
kikootwo merged 6 commits into
mainfrom
reland/397-tag-details
Oct 20, 2026
Merged

kikootwo merged 6 commits into
mainfrom
reland/397-tag-details

Conversation

@kikootwo

@kikootwo kikootwo commented Oct 20, 2026 •

Copy link
Copy Markdown
Owner

Linked issue

Part of #208. Re-lands #397 by @N0cky, who wrote the original change and is credited as co-author on these commits. #397 gets closed once this merges.

What changed

With metadata tagging on (the existing metadata_tagging_enabled switch; no new settings), imported files now also carry the book's description, genres and, on MP4, the audiobook media type and gapless flag:

M4B/M4A/MP4 MP3 FLAC
Description (Audible summary) desc + ldes TXXX:description comment (the Vorbis DESCRIPTION field, so an existing one is replaced, not doubled)
Genre (top-level genres, joined with ; ) ©gen TCON GENRE
Media type: Audiobook stik=2 n/a n/a
Gapless pgap=1 n/a n/a
  • Descriptions: line breaks, tabs and other whitespace runs become single spaces, control characters are dropped, and each value is capped at 16 KiB of UTF-8 at a word boundary with an ellipsis. Raw text is cut to a bound before any processing, and the cleanup is a single linear pass, so a description made of hundreds of thousands of spaces is handled in under a millisecond.
  • Genres: primaryGenres on the Audible book: Audnexus genres of type genre, or the top of each catalog ladder. / and ; inside a genre name become &, because Audiobookshelf splits genre tags on both. The existing display list is unchanged.
  • One lookup per import: the organize job looks up the Audible details once, starting as soon as the tagging switch is read, and shares the result with the existing missing-series backfill (src/lib/processors/organize-tag-details.ts). When the lookup only serves the tags, it gives up after 15 s: the stored description is used (bounded and HTML-stripped like the lookup's), with no genres.
  • Errors: a failed ffmpeg tag run reports the tail of ffmpeg's error output, never the command line, so descriptions don't end up in job logs.
  • Text helpers live in src/lib/utils/metadata-text.ts. Docs: documentation/phase3/file-organization.md and documentation/integrations/audible.md.
  • Ebooks are unchanged.

Publisher, subtitle and language (the rest of #208) can build on the ASIN post-step from #404.

Checks

  • On this head, with main merged in: the full unit suite in the Node 24 test container passes (296 files, 3,952 passed, 5 skipped), and so do the image build, the smoke test and the end-to-end spec below (3/3).
  • End to end on a real Audiobookshelf (regressions/208-asin-tag.spec.ts, three imports): with tagging on, Audiobookshelf reads the ASIN, the description and the genres as separate genres (for example Mystery, Thriller & Suspense and Science Fiction & Fantasy) from a single M4B and from a chapter merge; with tagging off, no description is written. Smoke: pass.
  • Tests with real ffmpeg 9.0.2 round-trip the M4B tags (description, genre, media type, gapless, ASIN, moov first) and a 200 KB MP3 description. FLAC is checked at the ffmpeg level: exactly one DESCRIPTION after a rewrite.
  • Timing tests: 200,000 spaces, and a 1 MB mixed-whitespace description, each clean in well under a second.
  • The new and changed tests fail against main.

Risk and rollback

  • With tagging on (the default), each import with an ASIN makes one extra Audible details lookup; if Audible is slow, it is skipped after 15 s and the import carries on.
  • Rollback: revert this PR.

Release note

Imported audiobooks now carry their description and genres as tags (and, for M4B, the audiobook media type), which Audiobookshelf reads when it scans. With metadata tagging on (the default), each import with an ASIN makes one extra Audible details lookup; if Audible is slow it is skipped after 15 s.

🤖 Generated with Claude Code

kikootwo and others added 6 commits October 20, 2026 14:14
Re-land of #397 on top of the merged ASIN post-step (#404).

- Tagger: description (MP4 desc + ldes, MP3 TXXX description, FLAC
  comment = the Vorbis DESCRIPTION field, so a source DESCRIPTION is
  replaced, not doubled), genre (top-level genres joined with "; ",
  "/" in a name becomes " & "), and on MP4 media_type=2 (stik) and
  gapless_playback=1 (pgap), all before -movflags +faststart.
- Descriptions: line breaks and tabs become spaces, other C0 controls
  are dropped, and each value is capped at 16 KiB UTF-8 at a word
  boundary with an ellipsis.
- Processor: one Audible details lookup per import, shared with the
  series backfill; when it only serves the tags it gives up after
  15 s (stored description, no genres). The tagging switch is read
  once and handed to the lookup and organize().
- AudibleService: primaryGenres (top of each category ladder /
  Audnexus type genre).

Co-authored-by: Tobias Bunk <github@bunk.at>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Tobias Bunk <github@bunk.at>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Description: the raw text is cut to 4x the 16 KiB cap (surrogate
  safe) before any work, then non-whitespace C0 controls are dropped
  and each whitespace run (incl. VT, FF, U+2028/U+2029) becomes one
  space in a single linear pass. The old pattern was quadratic on long
  space runs (200,000 spaces: about a minute).
- Genres: split on '/' and ';', trim, join with ' & ' (no regex
  backtracking; both are Audiobookshelf separators).
- The stored description fallback is bounded and HTML-stripped like
  the lookup path.
- A failed ffmpeg run reports the stderr tail, never the argv.
- The tag details lookup starts right after the switch read and is
  awaited just before organize(); a no-op handler keeps it handled.
- capUtf8Bytes clamps tiny limits; catalog primaryGenres keeps only
  non-blank strings; docs updated.
- The #208 spec checks Audiobookshelf's book genres and description.

Co-authored-by: Tobias Bunk <github@bunk.at>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Tobias Bunk <github@bunk.at>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- metadata-tagger.ts was over the 400-line limit: the value sanitizers,
  raw bound, byte cap and ffmpeg failure text move unchanged into
  src/lib/utils/metadata-text.ts (no behaviour change); their direct
  tests move to tests/utils/metadata-text.test.ts.
- The unhandled-rejection test now makes resolveTagDetails reject, so
  it fails without the processor's no-op catch.

Co-authored-by: Tobias Bunk <github@bunk.at>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Tobias Bunk <github@bunk.at>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kikootwo
kikootwo merged commit 024da94 into main Oct 20, 2026
1 check passed
@kikootwo
kikootwo deleted the reland/397-tag-details branch October 20, 2026 20:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant