diora-web/docs/youtube.md
mrwnslz 3e843aea9f
Some checks failed
Test / e2e (push) Waiting to run
Build and push Docker image / build (push) Successful in 15s
Test / test (push) Has been cancelled
Abo umziehen: Feed-Adresse ändern, alles behalten
POST /podcasts/feeds/<id>/move/ gibt einem Abo eine neue Adresse und
behält Einstellungen, Folgen, Stellen, Gespielt- und Favoriten-Markierungen
und die Warteschlange. Bei einer YouTube-Adresse werden Folgen, deren Datei
nach ihrem Video benannt ist (podsync-artige Server), zu diesem Video, damit
der Kanal-Feed sie wiederfindet statt sie doppelt anzulegen.

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

55 lines
3.4 KiB
Markdown

# YouTube channels as podcasts
A YouTube channel (or playlist) can be subscribed to like a podcast. Only its
sound is played, and **the server never downloads or streams a video or its
sound**: each app finds the audio stream itself.
## Server (diora-web, `podcasts/youtube.py`)
- `POST /podcasts/feeds/add/` takes a YouTube address as `rss_url`: a channel
(`/@name`, `/channel/UC…`, `/c/…`, `/user/…`), a playlist (`?list=…`), a video
(subscribes its channel) or the feed itself. The server reads the channel page
once (feed address, name, picture) and stores the feed
`https://www.youtube.com/feeds/videos.xml?channel_id=UC…` (or `?playlist_id=…`).
`GET /podcasts/search/?q=<YouTube address>` answers with that one channel.
- From then on the feed is refreshed like any other. It carries the latest 15
videos. **Shorts are left out.** There is no duration (`duration_seconds` 0)
until a player knows it: `POST /podcasts/progress/save/` takes an optional
`duration_seconds` (number, 1 s … 7 days), stored only while the episode has none.
- An episode from such a feed has
- `audio_url` = `https://www.youtube.com/watch?v=<11-character id>`. This is
**the** marker: a client recognises a YouTube episode by this prefix;
- `guid` = `yt:video:<id>`, `artwork_url` = the video's thumbnail,
`description` = the video description as HTML (links, line breaks; timestamps
like `00:00 Intro` work as usual).
- Nothing on the server touches the sound of such an episode: no ID3 chapters,
no transcripts, and `/podcasts/episodes/<id>/silences/` answers
`{"ready": true, "spans": [], "error": "youtube"}` (skip silence is up to the
app, as on Android anyway).
- `POST /podcasts/feeds/<id>/move/ {"rss_url": …}` gives an existing subscription a
new address and keeps everything (settings, episodes, places, played and favourite
marks, queue). Moved to a YouTube address, every episode whose file is named after
its video (`…/<11-character id>.mp3`, as podsync-like servers name them) becomes
that video (`guid` `yt:video:<id>`, `audio_url` its `watch?v=` page), so the
channel's feed finds it again instead of adding it twice. Used to move channels
that came through such a server (it downloaded the sound on the server) to the
channel itself.
## Apps
A client that sees a `watch?v=` address resolves it **itself, on the device,
to an audio-only stream** (no video track), and plays or downloads that.
| Client | How |
|---------|-----|
| Android | NewPipeExtractor: the original audio track, progressive, opus before m4a, ≤ 160 kbit/s; resolved when the stream is opened and once more on a 403; downloads in 10 MB ranges (in one go YouTube throttles to ~50 KB/s) |
| Desktop | yt-dlp in the main process (`-f bestaudio`), Electron itself as its JavaScript runtime; yt-dlp is fetched from its GitHub release on first use and kept up to date (`src/main/youtube.js`) |
| Browser | can't (YouTube doesn't let a web page read its pages, and a stream address only works for the device that asked for it): the episode is listed but says it plays in the app |
Stream addresses are bound to the device's IP address and run out after a few
hours: resolve shortly before playing, and again when the player reports an
error (podcasts-web.js reloads after a media error anyway).
Progress, queue, played state etc. work as for any episode: they belong to the
episode on the server, not to the stream address.