TempleOS Desktop Docs

Podcast service (svcpodcast.py): protocol for the HolyC side

Edited 2026-10-27apps-host/hub/PODCAST.md

Service name podcast. Request REQ id podcast token op arg...; reply records are TAB separated (recs.py), all text ASCII. Ids: show f + 10 hex (from the feed URL), episode 10 hex. The guest never sends a feed URL except in subscribe_url. ? = optional. Sizes measured by tests/test_podcast.py.

Ops and replies#

  • categories -> K id label count x12.
  • directory CAT|all OFFSET LIMIT(max 40) ?QUERY -> DH total offset count cat, then D fid title publisher cat tone subscribed desc. 10 entries: 984 bytes (worst case about 1.9 KB). 108 shows, 12 categories (fixtures/podcasts/catalog.json).
  • subs -> S fid title publisher tone episodes unplayed fetched_epoch downloaded. subscribe FID, subscribe_url URL ?TITLE, unsubscribe FID reply with one S. No network, no approval. tone 0..11 = rose orange amber lime green teal cyan blue indigo violet purple pink (the app's cover colour).
  • refresh FID ?force -> R fid result new total skipped title note; result new same (304) cache (inside podcast_min_refresh_s) offline (saved list shown). Approval scope podcast:refresh:FID. No saved list and offline: ERR offline: cannot reach HOST (...).
  • episodes FID OFFSET LIMIT ?unplayed -> EH fid total offset count show publisher, then E eid yyyymmdd dur_s size_kb played downloaded pos_s title. Newest first, date 00000000 when unknown, title 72 chars. 10 episodes: 658 bytes (worst case about 1.4 KB). Needs a refresh first, else ERR.
  • episode EID -> X (same fields as E plus host) and up to 4 N text lines of 88 chars (show notes, tags removed).
  • mark EID played|unplayed -> E. downloads OFFSET LIMIT -> DLH total offset count, then DE (same fields as E). delete EID -> E.
  • download EID -> DL eid state done_kb total_kb error; state none run done err. Approval podcast:dl:EID (text names the title, size and host). Runs in a thread: poll dlstatus EID; dlcancel EID. Limit podcast_max_mb. afplay cannot stream a URL (checked), so play of an episode that is not downloaded asks the same approval, downloads, then starts playing (the reply holds DL then PS).
  • Playback (through the music service, afplay): play EID ?START_S (default: resume), pause resume toggle stop seek S next prev volume 0..100 speed 0.75|1|1.25|1.5|2 sleep MINUTES|eoe|0 autonext 0|1 status. All reply PS state eid fid pos_s dur_s speed sleep_s autonext title; state playing paused stopped loading; sleep_s is seconds left, -1 for end of episode, 0 off. Poll status about 4 times a second while the window is open (it also saves progress). Position is saved every 5 s, on pause, stop, seek and sleep; an episode within 3 s of its end is marked played, then the next downloaded unplayed one plays if autonext is 1 (default; it never starts a download by itself). Pause/stop/seek answer ERR nothing from Podcasts is playing when the music service plays something else.
  • cover FID|EID SIZE ?p4|565, SIZE 32 40 48 64 96 (565 only up to 64) -> V id fmt w h nlines palette note, then nlines x Z hex (96 hex digits per line, last shorter). V ... none with a note when there is no image or no Pillow (the guest paints the tone block). Approval podcast:img:HOST. Cached on the host. Sizes: 40x40 1743 bytes, 48x48 2468, 96x96 9596 (the guest reply buffer is 12000).
  • stats -> STATS requests user_agent.

Cover record (the guest has no image decoder)#

  • p4: palette = 64 hex digits = 16 colours of 4 digits each, the number is RGB565 (r5 g6 b5, F800 red). Data = rows top to bottom, left to right, two pixels per byte (left pixel = high nibble, index into the palette), 2 hex digits per byte, SIZE*SIZE/2 bytes. Concatenate the Z lines.
  • 565: no palette (empty field); data = SIZE*SIZE pixels, 4 hex digits each, the RGB565 number.
  • The image is centre-cropped to a square, resized to SIZE (Lanczos), p4 is a 16-colour median cut (no dithering).

Config (hub config.json; all optional)#

podcast_dir (default ~/.cache/kosmos-hostd/podcast: state.json, feeds/, audio/, covers/; outside the repo), podcast_max_mb 200, podcast_min_refresh_s 600, podcast_allow_loopback false (tests only), podcast_catalog (path), podcast_tick_s 0.5, contact (goes into the User-Agent KosmosTempleDesktop/0.1 podcast (contact)). Registered in hostd.py build_hub (two lines).

Security notes#

  • Network only after a one-time approval token naming the host (and size). Subscribing, browsing the directory, episode lists, status and play of a downloaded file are local.
  • Feeds: http and https only, no user name or password, at most 3 redirects (each checked again), no proxy, identity encoding, 2 MB cap, 15 s timeout. Hosts that are or resolve to this Mac or a private network are refused (a lookup happens only after the approval; DNS rebinding between check and connect is not covered).
  • XML: any DOCTYPE or ENTITY declaration and UTF-16 are refused before parsing (defusedxml is also used if installed), 100 episodes kept, titles cut to 300 chars. enclosure URLs that are not http/https (file://) are skipped and counted in skipped.
  • Files: names are made from the episode id plus an audio extension, never from the feed; reads re-check the name and the real path inside audio/ (symlinks out are refused); ids are matched against ^f[0-9a-f]{10}$ and ^[0-9a-f]{10}$. Downloads stop at podcast_max_mb, text/html answers are refused, partial files are removed.
  • Covers: 3 MB cap, JPEG PNG GIF WEBP only, Pillow decompression-bomb limit 25 million pixels.
  • The approval is a consent record for the UI, not a defence against a hostile guest (same as the other services).
TempleOS Desktop Docs. Built from the Markdown sources; offline, no external requests. Back to top