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

12 KiB
Raw Permalink Blame History

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 · " 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.