summaryrefslogtreecommitdiff
path: root/README.md
blob: 615774480240e9d15828747250100e4a44538532 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# 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 "<playlist url>" \
    --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 <url>` | 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 <url>` | 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).