Skip to content

feat(locallibrary): give a set's recording chapters, one per track - #273

Merged
botandbang merged 5 commits into
mainfrom
bnb/set-chapters
Oct 29, 2026
Merged

botandbang merged 5 commits into
mainfrom
bnb/set-chapters

Conversation

@botandbang

Copy link
Copy Markdown
Contributor

What and why

Builds on the --upload change. A set's recording now carries its tracklist as chapters, so podcast apps and audio players show what's playing and can jump between tracks.

Added:

  • crates/locallibrary/src/sets/chapters.rs turns a set's tracks into chapters: one for each track with a start time, sorted by start, each ending where the next begins and the last where the recording ends. A track without a start time, starting at the same second as an earlier track, or starting after the recording ends gets no chapter, and a note says which and why.
  • sets export SET --format chapters writes Podcasting 2.0 JSON chapters (version 1.2.0, with the set's name as title). -o FILE.json picks the format from the extension. As a format for publishing, it names a track marked unnamed or hidden by the profile ID - ID.
  • sets publish SET --upload puts the same chapters in the file it sends, naming tracks the way the set's record does (--hide included). When ffmpeg encodes the recording, it's given the chapters as an FFMETADATA file (-f ffmetadata -i FILE -map_chapters 1, values escaped as ffmpeg's docs require), so the M4A has MP4 chapters and keeps the recording's own tags. An MP3 that goes up as it is goes up as a temporary copy whose ID3v2 tag gains a CHAP frame per chapter and a CTOC listing them in order, written with lofty with the tag's other frames kept; the copy is deleted after the upload, and the MP3 itself isn't touched. If the tag would push the file over the size limit, the MP3 goes up as it is, with a warning. An AAC or M4A file that goes up as it is gets no chapters, since adding them would need ffmpeg.
  • The upload plan lists the chapters (chapters in its JSON, a Chapters line in the text) and notes the tracks that have none.

Changed:

  • SetUploadRequest takes hide. The upload plan also watches the profile's preferences, since chapter names depend on them.
  • sets::export::shown_name names a track or writes ID - ID, shared by the export and the upload.
  • SetFormat gains chapters, so apps/locallibrary/docs/rpc/ is regenerated.
  • apps/locallibrary/docs/sets.md, apps/locallibrary/docs/commands.md, the sets export help and apps/locallibrary/CHANGELOG.md describe the format and the chapters in uploads.

How it was tested

  • sets::chapters has a unit test and a Hegel property over drawn tracklists and lengths: chapters are ordered and don't overlap, each ends at the next start or the recording's end, each timed track before the end has a chapter named for the first track at that start, and every other track is in a note.
  • sets::export checks the exact JSON for a set, including a track left out without a start time, and for an empty set.
  • publish::recording checks the exact FFMETADATA output with every escape, and a Hegel property that escaping round-trips. The stand-in ffmpeg test draws chapters: ffmpeg gets the metadata file with -map_chapters 1, an MP3 that goes up as it is goes as a copy whose ID3 chapters read back correctly with the same audio after the tag, and a copy pushed over the limit falls back to the original with a warning.
  • crates/locallibrary/tests/it/publish/recordings.rs uploads an MP3 to the mock data server with a hidden track and reads the chapters back from the uploaded blob.
  • The CLI's tests check the dry run's chapters (--hide included), ffmpeg's arguments, and the chapters format and .json in props_cli_sets.rs.
  • Each new test was checked by breaking the code it covers. By hand with ffmpeg 9.0.2: an encode with the same arguments and a metadata file with escaped titles, and lofty's tagging of a real MP3; ffprobe -show_chapters read the expected chapters, titles and tags from both.
  • Local: fmt, workspace clippy with -D warnings, nextest for locallibrary (2140) and locallibrary-cli (1071).

Checklist

  • Tests cover the change
  • For the website: Storybook stories cover any new UI states
  • For locallibrary: apps/locallibrary/CHANGELOG.md has a line under Unreleased, if people using it will notice
  • The READMEs, docs/ and --help still say what the code does (apps/locallibrary/docs/rpc/ too, for a sidecar change)

`sets export SET --format chapters` writes Podcasting 2.0 JSON chapters
(version 1.2.0: the set's name and a startTime and title for each track
with a start time, in the order they play), and `-o FILE.json` picks it.
It's a publishing format, so a track marked unnamed or hidden by the
profile is `ID - ID`, and a note names each track left out.

`sets publish SET --upload` puts the same chapters in the file it sends,
named as the set's record names them, `--hide` included, each ending
where the next starts and the last where the recording ends. ffmpeg gets
them as an FFMETADATA file (`-f ffmetadata -i FILE -map_chapters 1`,
values escaped as its docs say), so the M4A it encodes has MP4 chapters
and keeps the recording's own tags. An MP3 that goes up as it is goes up
as a temporary copy whose ID3v2 tag gains a CHAP frame per chapter and a
CTOC listing them (written with lofty, the other frames kept); if the
tag tips it over the limit, the MP3 goes as it is with a warning. An AAC
or M4A file that goes up as it is gets no chapters, since adding them
would need ffmpeg. The plan lists the chapters and why any track has
none, and reads the profile's choices along with the sets.

sets::chapters builds the chapters for both: tracks without a start
time, starting with an earlier track or after the recording ends get
none.
@github-actions github-actions Bot added documentation Improvements or additions to documentation locallibrary locallibrary and the loclib-* crates locallibrary: cli The locallibrary command line locallibrary: sidecar The JSON-RPC sidecar and the api module it serves locallibrary: core The core crate, the data model and rekordbox's formats locallibrary: publishing The Atmosphere account, and the sets and profile published to it tests Tests, fixtures and test harnesses labels Oct 28, 2026
bnb
bnb previously approved these changes Oct 29, 2026
Base automatically changed from bnb/pds-recordings to main October 29, 2026 17:49
@botandbang
botandbang dismissed bnb’s stale review October 29, 2026 17:49

The base branch was changed.

@botandbang
botandbang marked this pull request as ready for review October 29, 2026 19:54
@github-actions

Copy link
Copy Markdown

Size

Compared with main. KiB are 1,024 bytes.

This pull request main Change
Client JavaScript 620.5 KiB 635.1 KiB -14.7 KiB (-2.3%)
Client JavaScript, brotli 204.9 KiB 209.5 KiB -4.6 KiB (-2.2%)
Client CSS 87.8 KiB 88.6 KiB -750 B (-0.8%)
Client CSS, brotli 12.6 KiB 12.7 KiB -151 B (-1.2%)

@github-actions

Copy link
Copy Markdown

Rust coverage

From run 537. Both are held to the thresholds in .cargo/config.toml.

Tests Lines Functions Regions
All 97.46% (81787/83922) 96.57% (13034/13497) 95.76% (179825/187793)
Hegel property tests alone 95.19% (79886/83922) 93.74% (12652/13497) 93.29% (175198/187793)

@botandbang
botandbang merged commit cc1e7a9 into main Oct 29, 2026
66 checks passed
@botandbang
botandbang deleted the bnb/set-chapters branch October 29, 2026 20:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation locallibrary: cli The locallibrary command line locallibrary: core The core crate, the data model and rekordbox's formats locallibrary: publishing The Atmosphere account, and the sets and profile published to it locallibrary: sidecar The JSON-RPC sidecar and the api module it serves locallibrary locallibrary and the loclib-* crates tests Tests, fixtures and test harnesses

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants