diora-web/docs/podcasts.md
mrwnslz f242605a97 Smarte Transkription: Vertrag und Prompt (docs/transcription.md), Abschnitt Transkripte in docs/podcasts.md, Podcast-Artikel in docs/ai.md
Transkript und Artikel nur auf Knopfdruck; der Server transkribiert mit Whisper (eingebaut oder eigene Instanz), das Gerät schreibt den Artikel mit dem eigenen Schlüssel, sofort oder beim nächsten Öffnen; Benachrichtigung über /feeds/notifications/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 09:16:16 +01:00

193 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Podcasts: Strom, Merkliste, Wiedergabeliste
The plan agreed on 2026-10-19 (with the user, from their listening history). It replaces
the inbox model ("Neu" to work through, "Folgen" beside it, one queue that is both a store
and the play order). Server: diora-web (podcasts/). The podcast UI is diora-android's
`web-native/static/native-podcast.js`, copied into `static/podcasts/` — built there.
## Why
- The subscriptions (about 540) are a self-chosen part of the podcast world: episodes to see
go by and dip into now and then — not a list to work through. "Neu" had 3360 unplayed
episodes; nobody works through that.
- The queue held 74 episodes (100+ hours): a store of "want to hear some day", yet the
player played it in order as if it were the play order.
- In AntennaPod "download automatically" and "add to queue automatically" were one thing
for the user: a regular podcast is one whose new episodes are on the phone. diora split
them (177 feeds downloading, 8 queueing).
- "Silent" subscriptions came from AntennaPod fetching every feed on the phone, slowly. The
server fetches all feeds anyway now (conditional, every 15 minutes).
## The lists
**Wiedergabeliste** — short, what plays next. The player goes through it in order and
**stops when it is empty** (it does not take from the Merkliste). Every episode in it is
downloaded. Filled by hand: ▶ on an episode (plays it, puts it first), or "In die
Wiedergabeliste" from the Merkliste or the Strom.
**Merkliste** — long, "want to hear". It may grow; it **never expires**. Grouped by podcast,
each podcast **collapsible** (newest first inside). New episodes of Stamm subscriptions land
here and are downloaded; so is what the user adds with +. An episode played from it moves
to the front of the Wiedergabeliste; finished, it leaves both.
When an episode leaves both lists (heard, removed), its download is deleted (setting
`delete_dequeue`). Downloads follow the lists, not the subscriptions.
## Subscriptions: three levels
| level | new episodes | today's mapping |
|---|---|---|
| **Stamm** | into the Merkliste and downloaded (per feed: or straight into the Wiedergabeliste — an option, used by none yet) | `new_episodes='queue'` (8) plus the regularly heard ones (about 50, list below) |
| **Strom** (default) | in the Strom; nothing to work through, nothing downloaded | `'inbox'`, and the former silent `'ignore'` ones |
| **Auf Abruf** | not in the Strom; on the podcast's page and in search | — (for exceptions) |
The per-feed "download automatically" switch goes: Stamm means it.
**Fetch interval** per subscription: *Automatisch* (by publishing rhythm, `interval_days`:
daily publishers hourly, rare ones daily), *stündlich*, *täglich*, *wöchentlich*. The former
silent subscriptions become Strom with *täglich*.
## The Strom (replaces "Neu" and "Folgen")
All episodes of all Strom and Stamm subscriptions, by period (Heute, Gestern, Diese Woche,
…). Within a period the podcasts are ordered by **how much the user has listened to them**
(episodes heard): every podcast with at least one heard episode before every one without —
those count for infinitely more (heard in the last 12 months, `heard_365`); among the never-heard ones, rarely publishing first.
Each podcast appears **once per period, always bundled**: its newest episode, and "+n ›" for the others. Episodes new since
they were last seen carry a **small mark** (no counts). Per episode: ▶ (Wiedergabeliste,
first), + (Merkliste), ★. Filters stay: Favoriten, Verlauf, Angefangen, Gehört.
Tabs: **Wiedergabeliste · Merkliste · Strom · Abos**.
## Favourite subscriptions
★ on a subscription (`PodcastFeed.favorite`): a mark and a filter (Abos: only the favourites;
episode lists: `filter=fave_feeds`), whatever its level — it puts nothing on the Merkliste by
itself. Started with the Stamm subscriptions marked.
## Stöbern (decided 2026-10-19)
Within the subscriptions:
- **Search** in titles and shownotes: `GET /podcasts/discover/search/?q=…&stream=1` (all
words, the last as a prefix; titles weigh more). SQLite FTS5 index, contentless
(`podcasts/discover.py`), filled after every refresh; `manage.py index_episodes` fills it.
- **Similar episodes** from the other podcasts: `GET /podcasts/discover/similar/<id>/`.
- **People** (who talks, who gives a talk), from episode titles only (`podcasts/people.py`):
`GET /podcasts/discover/people/` → `[{name, episodes, feeds, score}]`, the ones in the most
listened podcasts first; `GET /podcasts/discover/person/?name=` → their episodes.
- **Topics** of the Strom's last days, chips as for the news: `GET /podcasts/discover/topics/?days=7`
→ `[{label, ids}]`.
- **Forgotten**: `GET /podcasts/discover/forgotten/` → `{loved: [...], rare: [...]}` — unheard
episodes older than 60 days of the podcasts listened to most, and of the rare ones; the same
all day.
One controlled step out (Apple's episode search and fyyd; no key):
- **People elsewhere**: `GET /podcasts/discover/elsewhere/?name=` → episodes naming the person
in podcasts the user doesn't have (`{source, podcast, feed_url, title, audio_url, guid,
pub_date, duration_seconds, artwork_url, link}`). `POST /podcasts/discover/take/` with such an
item puts the episode on the Merkliste; its podcast becomes an *Auf Abruf* subscription.
- **Recommendations**: `GET /podcasts/discover/recommend/` → podcasts like the most listened ones
(fyyd), recommended for several first: `{title, feed_url, artwork_url, description, link,
because: [titles], count}`. Subscribing: the usual feeds/add/ (then *Auf Abruf* or Strom).
Show search stays on Apple (best coverage).
## Podcast-Radio (since 2026-10-25)
Listening without committing: one button plays the Strom like a station.
- **The rule**: the radio always plays the **newest** (publication date) Strom episode that is
neither played, nor started (normally), nor done for the radio. So an episode that comes out
while one plays is next (the one playing isn't interrupted); otherwise it goes back in time,
without limit. Auf Abruf podcasts and audiobooks aren't in the Strom, so not in the radio.
- **Mixed**: several episodes of one podcast in a row are mixed with the others waiting — the
next is the newest of *another* podcast than the one before, if there is one (within the
newest 200).
- What comes next is asked anew at every end and skip (so the newest really comes next); the
list fetched before is for "Danach: …" in the player and for going on offline.
- **Leaves no trace**: no progress (no Verlauf, no "Angefangen", no played mark, not counted in
`heard_365`), the Wiedergabeliste and Merkliste unchanged, nothing downloaded (it streams; a
downloaded file plays if there is one), the Strom's marks unchanged.
- **Done for the radio**: run to its end or skipped (⏭, notification, headset, car). Pausing
isn't: the radio keeps its episode and place for itself, ▶ goes on there.
- **Committing**: in the player while the radio plays, + (Merkliste) and "Normal weiterhören"
(first in the Wiedergabeliste at the current place; from then on it plays as usual, with
progress). Playing anything normally switches the radio off (as an audiobook does).
- **Server** (`podcasts/radio.py`): `PodcastRadioHeard(user, episode, at)`, kept apart from
progress and kept for good (pruned, an old episode would come back when the radio goes back
that far). `GET /podcasts/radio/?after=<playing>&skip=<ids>&limit=5` → the next episodes by
the rule (`{episodes: [...]}`, the rows of the episode lists; `after` isn't among them and its
podcast not first; `skip`: ones the device is done with whose /heard/ may still be on its
way); `POST /podcasts/radio/heard/ {"episode_id"}`. Shared by all devices. Filter `untouched`
(episode lists): neither played nor listened into.
- **Player sync**: a report carries `podcast_radio: true` while the radio plays
(`PlayerState.podcast_radio`); taking it over ("Hier abspielen", a transfer) or adopting it goes
on with the radio on that device.
- **Clients**: the web player (`podcasts-web.js`: an entry with `radio: true`, the radio's own
`podradio` in localStorage — next ones, its place, what it was done with lately; host
`podcastRadio()`, `podcastRadioCommit()`, `podcastRadioLater()`; `playerState().radio =
{next}`) and Android (`PodcastRadio.kt` beside `BookPlay.kt`; RadioService's `save()` /
`markPlayed()` / `followQueue()` leave radio items alone). UI (native-podcast.js): 📻 at the top
of the Strom, "Podcast-Radio · <podcast>" in the mini player and the player, there a box with
"Danach: …", + Merkliste and "Normal weiterhören"; ⏭ only skips.
- Later perhaps: a radio of one folder, a longest length.
## Moving over
- Today's queue (74) becomes the Merkliste; the Wiedergabeliste starts empty.
- Stamm: the 8 automatic subscriptions and the regularly heard ones (≥ 8 episodes a year or
≥ 3 in 90 days, 2026-10-19): Ones and Tooze, Die Neuen Zwanziger – Der Salon, Surplus,
The Deprogram, junge Welt – Tagesüberblick, Speakeasy-Bar, jwpod – Forschung, RLS
Audiodokumente, The Reaction Pod, Geschichten aus der Geschichte, Märkte Mächte Emissionen,
friendlyjordies, Second Thought, Neben der Spur, Forschungsquartett, GegenStandpunkt –
Artikel, Hörsaal, Hakim, Marxistische Vorträge, Blätter, Kontextfenster, New Left Review,
Kommunisten Kneipe, Jung & Naiv, HealthyGamerGG, Dekarldent, Daily Dose Of Dekarldent,
Eine Stunde History, Übertage, Eigentlich, Zwischen Zwei Deckeln, UNFASSBAR, Über Rechts,
TitosLeftLung, New Books in History, ARTEde, Irgendwas mit ARTE, Zwei wie Pech & Schwafel,
Geschichte der kommenden Welten, Vollbild, Zeitfragen, Zeitfragen. Feature, Die Filmanalyse,
Reaction, Wirtschaft am Mittag.
- Everything else: Strom; the silent ones with *täglich*.
## Order of work
1. Wiedergabeliste and Merkliste (server: a second list, moving the queue over, the player's
order; UI: two tabs, grouped Merkliste).
2. The three levels and the fetch interval; downloads following the lists.
3. The Strom replacing "Neu" and "Folgen".
4. Browsing — after more thought.
## API (server, since 2026-10-19)
- `PodcastSettings.lists_v2` turns the two lists on. `POST /podcasts/lists/ {"on": true}`
moves the queue into the Merkliste (the Wiedergabeliste starts empty); `{"on": false}` puts
the Merkliste back at the end of the queue. Off, everything works as before (apps not
updated yet).
- **Wiedergabeliste** = the queue as it was: `state.queue`, `/podcasts/queue/add|remove|reorder/`.
The player goes through it and stops at its end.
- **Merkliste** = `PodcastLater`: `state.later` (rows like the queue's, newest first, plus
`added_at`, `auto` — added by a Stamm feed — and `in_queue`), `POST /podcasts/later/add/` and
`/podcasts/later/remove/` `{"episode_ids": [...]}`. Heard (progress at the end, mark-played)
and "Folge löschen" take an episode off it. Episode lists carry `in_later`; filter `later`.
- **Levels**: `PodcastFeed.new_episodes` — `queue` = Stamm into the Merkliste (with lists_v2;
without it: the queue), `play` = Stamm into the Wiedergabeliste, `inbox` = Strom,
`ignore` = Auf Abruf. `auto_queue` stays true for queue and play.
- **Fetch interval**: `PodcastFeed.refresh_interval` — `''` (by rhythm: every `interval_days`
hours, within 15 minutes and 24 hours), `hourly`, `daily`, `weekly`; the cron's
`refresh_feeds` only fetches feeds that are due (`views.refresh_due`).
- **Strom**: `GET /podcasts/episodes/?filter=stream` (all but Auf Abruf; with `folder=`, `sort=`);
episodes carry `discovered_at`. Per feed in `state.feeds`: `heard_365` (episodes heard in the
last 12 months: played, or listened into for more than a minute) and `interval_days`.
`POST /podcasts/stream/seen/` → `{before, now}`; `state.stream_seen_at`. An episode is marked
when its `discovered_at` is after the time before the current look.
- Web host (`static/js/podcasts-web.js`, `DioraPodcastHost`): `podcastLaterAdd(json episodes)`,
`podcastLaterRemove(json ids)`, `podcastListsSwitch(on)`, `podcastStreamSeen()` (keeps the
previous time as `state.stream_seen_before` for the marks).
## Transcripts
An episode's Transkript tab shows one of two kinds. **From the feed**: `<podcast:transcript>`
(SRT, VTT, JSON, Podlove), fetched and parsed on first use and cached in `EpisodeTranscript`
(`podcasts/transcripts.py`, `GET /podcasts/episodes/<id>/transcript/`). **Made by Whisper**
(since 2026-11): only when the user asks, kept on `TranscriptionJob`; the feed's wins when both
exist. From a transcript the device can write an article that is pinned to the episode and shows
in Blogs. All of that is in docs/transcription.md.