113 lines
5.1 KiB
Markdown
113 lines
5.1 KiB
Markdown
|
|
# 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 · <book>" when it plays one.
|