diora-web/docs/audiobooks.md
mrwnslz 5be1b3fbaf
All checks were successful
Build and push Docker image / build (push) Successful in 15s
Test / test (push) Successful in 8m52s
Test / e2e (push) Successful in 1m59s
Hörbücher: Podcasts als Hörbuch markieren und in „Bücher“ hören
Ein Abo lässt sich als Hörbuch (oder Hörspiel) markieren (PodcastFeed.kind).
Es verlässt dann die Podcast-Welt: keine „Neu“-Folgen, keine Warteschlangen-
Automatik, nicht in „Folgen“. In der Bibliothek steht es neben den E-Books
(static/js/audiobooks.js, eine Datei für Web, Desktop und Android):
„Weiterhören“, ein Fortschritt pro Buch, die Kapitel in Reihenfolge,
Lesezeichen mit Notiz, Schlummer bis Kapitelende.

Server: /podcasts/audiobooks/ (+<id>/), /podcasts/bookmarks/…, Migration 0019.
Player (podcasts-web.js): podcastPlayBook spielt die Kapitel nacheinander
außerhalb der Warteschlange; ⏭ markiert nicht als gehört. Vertrag:
docs/audiobooks.md. native-podcast.js aus diora-android 7b1d500.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 13:05:00 +02:00

5.1 KiB

Audiobooks (Hörbücher & Hörspiele)

A podcast subscription can be marked as an audiobook. It then leaves the podcast world (no "Neu", no queue automation, not in the podcast lists) and shows up in the library ("Bücher") next to the e-books, where it is listened to like a book: its episodes are chapters in order, "Weiterhören" goes on where the last chapter stopped, the whole book has one progress, places can be kept as bookmarks with a note, and the sleep timer can stop at the end of a chapter.

Server (diora-web)

The mark

PodcastFeed.kind: "podcast" (default) or "audiobook". A feed setting like the others: in state.feeds[] (/podcasts/state/), set with POST /podcasts/feeds/<id>/settings/ {"kind": "audiobook"}.

Marked as an audiobook:

  • its episodes leave "Neu" (dismissed, not played); new ones never get there and are never queued, whatever new_episodes says;
  • it is left out of "Folgen" (/podcasts/episodes/ without feed_id);
  • nothing else changes: places, played marks, downloads, settings stay. Episodes of it already in the queue stay there until played or removed.

Chapters and progress

Chapter order: episode_number (if set), then pub_date, then the title in natural order ("Kapitel 2" before "Kapitel 10"), then id.

GET /podcasts/audiobooks/ → {"books": [book, …]}, the last listened first:

book = {
  "feed_id", "title", "author", "artwork_url",
  "chapters": 24, "done": 6,                 // played chapters
  "total_seconds": 32400, "heard_seconds": 11400,
  "next": {"episode_id", "index", "position_seconds"} | null,   // null: finished
  "last_played_at": ISO | null
}

heard_seconds = played chapters' durations + the places in the others. Durations the feed doesn't have count as 0 until a player reports them (progress/save with duration_seconds, see youtube.md).

GET /podcasts/audiobooks/<feed_id>/ → {"book": book, "chapters": [ch, …], "bookmarks": [bm, …]} with

ch = {   // the queue entry's shape, so it can be handed to the player as it is
  "episode__id", "episode__title", "episode__audio_url", "episode__duration_seconds",
  "episode__artwork_url", "episode__feed__id", "episode__feed__title",
  "episode__feed__artwork_url", "episode__pub_date", "position_seconds", "played",
  "index"
}
bm = {"id", "episode_id", "position_ms", "note", "created_at"}   // in chapter order, then place

Bookmarks

  • POST /podcasts/bookmarks/ {"episode_id", "position_ms", "note"} → {"ok", "bookmark": bm}
  • POST /podcasts/bookmarks/<id>/ {"note"} → {"ok", "bookmark": bm}
  • POST /podcasts/bookmarks/<id>/delete/ → {"ok"}

Any episode can have bookmarks; the library shows those of audiobooks.

The player (both hosts: podcasts-web.js and Android)

One new host call, next to podcastPlayEpisode:

podcastPlayBook(json) with json = {"feed_id", "chapters": [ch, …], "index", "position_ms"} (position_ms optional: else the chapter's own place)

  • plays chapters[index] without putting anything in the queue;
  • the book is what plays next: when a chapter ends it counts as played and the next chapter follows from its own place; after the last one playing stops; "next" (⏭, media keys) goes to the next chapter without marking the one left as played;
  • playing anything else (a queue episode, the radio's own handover aside) ends the book context; the book context survives a restart of the page/app;
  • the sleep timer needs nothing new: "end of chapter" is podcastSleep(-1) (end of the episode) or, for an episode with chapters of its own, the minutes until that chapter's end (podcastSleep(ms / 60000), fractions are fine).

playerState() (unchanged) tells the library which chapter plays where.

The library (diora-web static/js/audiobooks.js, everywhere)

One file for the web page, the desktop app and the Android app (like feeds.js; full flavour only, it needs the podcast host). It draws into #tab-books, above #book-list, in its own container #audiobook-shelf:

  • "Weiterhören": the audiobook listened to last (unless finished);
  • "Hörbücher": a card per audiobook — picture, title, author, "3 h 10 von 9 h · Kapitel 7/24" and a progress bar;
  • "+ Hörbuch": pick one of the podcast subscriptions to mark as an audiobook;
  • the book view: picture, progress, ▶ Weiterhören / Von vorn, the chapter list (tap: play it from its place; played ones ✓), bookmarks (tap: play from there; note editable; ✕), "+ Lesezeichen" (at the place where this book plays now), "Schlummer: Kapitelende", and ⋮ → "Wieder als Podcast", "Alles als ungehört".

It talks to the server with fetch and to the player through the host (window.DioraNative on Android, window.DioraPodcastHost elsewhere): podcastState, podcastFeedSettings, podcastPlayBook, playerState, podcastSleep, podcastMarkPlayedAll.

The podcast UI (native-podcast.js, diora-android)

  • feed settings sheet: "Als Hörbuch" (sets kind);
  • audiobook feeds left out of Abos, Neu, Folgen, search results of the subscriptions; a hint "In Bücher → Hörbücher" where one was moved;
  • the player shows a book chapter as "Kapitel 7/24 · " when it plays one.