# podkit Turns a playlist or video URL into a private podcast feed you can subscribe to from any podcast app. One Go binary; the fetch step shells out to `yt-dlp`. ``` podkit all "" \ --dir ./audio \ --title "Character of Physical Law" \ --artist "Richard Feynman" \ --base-url "https://your-host/audio/" podkit serve --dir ./audio --addr :8080 --new-token ``` Point your podcast app at the feed URL that `serve` prints. That's it. ## Install ``` go build -o podkit . pip install --user yt-dlp # needed only for `podkit fetch` ``` `ffmpeg` must also be on PATH — yt-dlp uses it to extract audio. `podkit` checks for both **before** starting a long download rather than failing during post-processing, after the bytes are already on disk. ## Commands | Command | What it does | | --- | --- | | `podkit fetch ` | Download and extract audio | | `podkit tag` | Write ID3 tags derived from filenames | | `podkit feed` | Generate `feed.xml` | | `podkit serve` | Host the directory and feed over HTTP | | `podkit all ` | fetch → tag → feed | Each stage runs independently on purpose: the download is the slow part and shouldn't be repeated while you're adjusting titles or the base URL. ## Notes on the things that usually go wrong These are the parts a hand-rolled version of this pipeline tends to get wrong, and what `podkit` does instead. **Filenames in URLs.** Lecture titles contain spaces and ampersands. Pasted into an enclosure URL unescaped they produce both an invalid URL and invalid XML, and the feed won't load at all. `podkit` escapes the final path segment only — escaping the whole base would mangle the scheme and host. **Episode order.** `01, 02, … 10` sorts correctly as text only while the zero-padding is applied consistently; one unpadded file and lecture 10 lands after lecture 1. Order comes from the parsed track number, not the filename. Untracked files sort last rather than jumping to the front. **Play direction.** Podcasts default to newest-first, which starts a lecture course at the final lecture. The feed sets `itunes:type=serial` and gives every item a distinct ascending `pubDate`, so clients present it in order either way. **`guid`.** Marked `isPermaLink="false"`. Without that, a reader is entitled to treat the value as a URL and fetch it; these identifiers are filenames. **Required channel elements.** `description` and `link` are mandatory in practice — several clients silently reject a feed lacking them. **Non-audio files.** Cover art and the `.part` files an interrupted download leaves behind are skipped by extension, so they can't become episodes. **Resumability.** `fetch` keeps a download archive (`.podkit-archive`), so re-running skips what's already there. Tagging is idempotent, and re-tagging preserves artwork yt-dlp embedded rather than replacing the tag wholesale. **ID3 only on MP3.** ID3v2 is an MP3 convention; writing it into `.opus` or `.m4a` produces something no player reads, so other formats are left alone. ## "Private" `--token` means unlisted-and-unguessable, not authenticated. Podcast clients can't do interactive login, so a shared secret in the URL is the only mechanism that actually works with them — anyone holding the URL has access. Rejections return 404 rather than 401, so a prober learns nothing. For anything beyond a home network, put it behind a reverse proxy with TLS. `serve` speaks plain HTTP and is not intended to face the internet directly. ## Sourcing `podkit` takes a URL you supply and does nothing to find content. Whether you may download a given item is between you and whoever published it — check the source's terms. ## Tests ``` go test ./... ``` Covers the feed generator (URL escaping, ordering, XML well-formedness, non-audio filtering), the tagger (round-trips, `n/total` track frames, idempotency) and the server (token gating, path traversal, that the escaped URLs the generator emits actually resolve against the server that serves them).