Chapters / cue points for long-form tracks (Audiobooks with chapters, multi track performances, podcasts with sections)?

Hi all,

A class of content doesn’t fit Mopidy’s flat-track model well: one audio
file with an internal tracklist
Continuous live mixes, long classical works, podcasts or audiobooks with chapter
markers. The data for the internal positions usually already exists (CUE
sheets, container chapter atoms, yt-dlp chapters, Podcasting 2.0 chapters),
but Mopidy has nowhere to put it — so clients can’t show “what’s playing now
in this 2-hour mix” or let you jump between segments.

What’s interesting is the data is already reachable underneath Mopidy —
GStreamer emits a TOC (GstToc) for chaptered files, and mpv exposes
chapter-list — it’s just never surfaced through Mopidy’s model or API.

Before writing anything heavy, I wanted to gauge whether this is something
you’d consider in core. The shape I have in mind mirrors the existing
get_images() API:

core.library.get_chapters(uris)            -> {uri: [Chapter, ...]}
backend.LibraryProvider.get_chapters(uris) -> {uri: [Chapter]}   # defaults to {}

with a small Chapter model (start ms, optional length, name,
artists, image). Key choices:

  • Not a field on Track — chapters can be large or need a network fetch
    to enumerate, so keep them lazy/opt-in, the same as images.
  • No tracklist auto-splitting — chapters are the non-destructive
    representation; backends that want CUE->tracks can still do that separately.
  • No MPD-protocol changes — out of scope (MPD has no chapter concept). The
    native JSON-RPC frontend exposes get_chapters for free, so web/native
    clients (Iris etc.) can use it.

I put together a working PoC rather than just describe it:

  • core branch — Chapter model + get_chapters mirroring get_images, with
    tests: GitHub - davidj4tech/mopidy at poc/library-get-chapters · GitHub
  • a Mopidy-Mpv backend that populates it straight from mpv’s chapter-list
    (container + yt-dlp chapters). Verified end-to-end: a chaptered file →
    get_chapters[{start: 0, name: "Intro"}, {start: 318000, name: "Bonobo – Kerala"}].

Is a chapters / cue-points API something you’d be open to in core? Happy to
take feedback on the model (naming chapters vs cuepoints, whether
artists/image belong in v1), and to turn it into a proper PR or move this
wherever you’d prefer it discussed.

Thanks!

Following up on my own post since it’s been quiet — I went ahead and built a proof of concept, and I’ve made a call on the open questions so there’s something concrete to accept or veto rather than a list of choices.

Proof of concept: GitHub - davidj4tech/mopidy at poc/library-get-chapters · GitHub

It mirrors get_images one-for-one: a Chapter model plus library.get_chapters(uris) -> {uri: [Chapter]}, backend-provided, lazy, per-URI, not on the Track model, no tracklist splitting, no MPD change. The JSON-RPC frontend exposes core.library.get_chapters for free. Tests pass, mirroring the get_images tests (model, backend default, and core controller). I also have a Mopidy-Mpv backend get_chapters that populates real chapters from mpv’s chapter-list (which folds in yt-dlp’s YouTube chapters), so the mpv-backend + Iris slice works end to end.

Decisions on the open questions:

  1. Name: chapters / Chapter, not “cue points”. mpv, Matroska, MP4, Ogg, yt-dlp and Podcasting 2.0 all say “chapters” — match the ecosystem.
  2. Fields (v1): start (required, ms), length (optional), name (optional), artists (optional, reusing the existing Artist model). The artists field is what makes DJ mixes useful — each segment is a different artist — and it adds no new concept.
  3. image / external link: deferred to a follow-up. They’re Podcasting-2.0-shaped and carry their own discussion; leaving them out keeps the first merge small. The model is additive, so they slot in later.
  4. end vs length: start is the only required field; length stays optional. Chapters are contiguous so end is derivable from the next start; length covers the trailing chapter and sources (M4B) that give it natively.
  5. chapter_changed event / seek helpers: Phase 2, not v1. Seeking is already playback.seek(chapter.start) and “current chapter” is derived client-side, so v1 stays purely additive.

The one thing I’d genuinely like a maintainer steer on before I open a PR: is LibraryController.get_chapters the right seam? If the get_images parallel is acceptable, I think this is a small, self-contained addition and I’m happy to do the PR + the Iris side. If you’d rather it live elsewhere (or not in core at all), I’d rather hear that now than after a PR.