Skip to content

Commit f9a867f

Browse files
authored
feat: clarify feed generator (#4)
2 parents 26e88ff + 0b2543e commit f9a867f

3 files changed

Lines changed: 66 additions & 75 deletions

File tree

.pre-commit-config.yaml

Lines changed: 10 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,26 @@
1+
minimum_pre_commit_version: "4.2.0"
2+
13
repos:
24
- repo: https://github.com/pre-commit/pre-commit-hooks
35
rev: v5.0.0
46
hooks:
5-
- id: check-added-large-files
6-
args: ["--maxkb=100"]
7+
- id: check-json
78
- id: check-merge-conflict
9+
- id: check-symlinks
810
- id: check-yaml
9-
- id: check-json
1011
- id: end-of-file-fixer
11-
exclude_types: ["image"]
1212
- id: trailing-whitespace
1313
exclude_types: ["image"]
1414
- id: requirements-txt-fixer
15-
files: (requirements|constraints).*\.(txt|in)$
16-
1715
- repo: https://github.com/astral-sh/ruff-pre-commit
18-
rev: v0.9.5
16+
rev: v0.11.8
1917
hooks:
2018
- id: ruff
2119
args: [--fix]
2220
- id: ruff-format
23-
24-
- repo: https://github.com/pre-commit/mirrors-clang-format
25-
rev: v19.1.7
26-
hooks:
27-
- id: clang-format
28-
"types_or": [c++, c]
29-
30-
- repo: https://github.com/Lucas-C/pre-commit-hooks
31-
rev: v1.5.5
32-
hooks:
33-
- id: forbid-tabs
34-
exclude: ".gitmodules|Makefile"
35-
36-
- repo: https://github.com/rhysd/actionlint
37-
rev: v1.7.7
38-
hooks:
39-
- id: actionlint
40-
41-
- repo: https://github.com/shellcheck-py/shellcheck-py # TODO add this
42-
rev: v0.10.0.1
43-
hooks:
44-
- id: shellcheck
45-
exclude: build_tools/credential-helper # This is an autogenerated file
46-
47-
- repo: local
21+
- repo: https://github.com/pre-commit/mirrors-prettier
22+
rev: v3.0.0
4823
hooks:
49-
- id: buildifier
50-
name: buildifier
51-
entry: buildifier
52-
language: golang
53-
additional_dependencies: [
54-
# v7.1.2
55-
"github.com/bazelbuild/buildtools/buildifier@1429e15ae755a6762d0edf9198062dc6ed04408d",
56-
]
57-
files: '^(.*/)?(BUILD\.bazel|BUILD|WORKSPACE|WORKSPACE\.bazel|WORKSPACE\.bzlmod|MODULE\.bazel)$|\.BUILD$|\.bzl$'
58-
language_version: "1.16"
59-
- id: build_file_names
60-
name: Check Bazel file names
61-
entry: Files should be named BUILD.bazel instead of BUILD
62-
language: fail
63-
files: "BUILD$"
24+
- id: prettier
25+
args: [--write]
26+
types_or: [yaml, json, markdown, css, html, javascript, ts, tsx]

README.md

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,27 +17,27 @@
1717

1818
Create a `feeds.jsonl` file with the following format:
1919

20-
``` sh
20+
```sh
2121
{"url": "https://pod.url1.com/FFFFF", "name": "Podname XYZ"}
2222
{"url": "https://pod.url2.com/FFFFF", "name": "Podname ABC"}
2323
{"url": "https://pod.url3.com/FFFFF", "name": "Podname DEF"}
2424
```
2525

2626
### Download podcasts
2727

28-
Next, run the script to download all the missing episodes and metadata. By default it will be stored under the `pods` directory, with one sub-directory per podcast.
28+
Next, run the script to download all the missing episodes and metadata. By default it will be stored under the `pods` directory, with one sub-directory per podcast.
2929
We recommend if you plan on serving this over the web to use a different directory, such as `/srv/www/petit-pois/pods`.
3030

31-
``` sh
31+
```sh
3232
python3 download_podcasts.py \
3333
--archive_dir /srv/www/petit-pois/pods
3434
```
3535

3636
### Generate podcast feed tokens (optional)
3737

38-
Again, if you're interested serving, we don't want expose the podcast to just anyone, so we need to create a token for each podcast. This is done by running the `generate_tokens.py` script:
38+
Again, if you're interested serving, we don't want expose the podcast to just anyone, so we need to create a token for each podcast. This is done by running the `generate_tokens.py` script:
3939

40-
``` sh
40+
```sh
4141
sudo python3 generate_token_map.py \
4242
--archive_dir /srv/www/petit-pois/pods \
4343
--map_file /etc/nginx/podcast_tokens.map
@@ -47,15 +47,16 @@ sudo python3 generate_token_map.py \
4747

4848
Next, run the script to generate the feeds, with the optional inclusion of a token map file:
4949

50-
``` sh
50+
```sh
5151
python3 generate_feeds.py \
5252
--archive_dir /srv/www/petit-pois/pods \
53-
--base_url http://yourdomain.com/pods
53+
--base_url http://pods.yourdomain.com/ \
54+
--map_file /etc/nginx/podcast_tokens.map
5455
```
5556

5657
Now, each podcast will have a `archive.xml` file in its directory.
5758

58-
If you want to serve the files using a web-server, there are a few options. The next section gives an example using Nginx.
59+
If you want to serve the files using a web-server, there are a few options. The next section gives an example using Nginx.
5960

6061
## ⚠️ Disclaimer
6162

@@ -73,14 +74,13 @@ Before archiving or sharing anything, it’s a good idea to:
7374

7475
Install Nginx:
7576

76-
``` sh
77+
```sh
7778
sudo apt update && sudo apt install nginx
7879
```
7980

8081
Create a config file (e.g., `/etc/nginx/sites-available/petit-pois`):
8182

82-
83-
``` sh
83+
```sh
8484
map $secure_token $podcast_dir {
8585
default "";
8686
include /etc/nginx/podcast_tokens.map;
@@ -120,7 +120,7 @@ server {
120120
}
121121

122122

123-
###### 🔐 TLS CONFIG (UNCHANGED) ######
123+
###### 🔐 TLS CONFIG ######
124124
listen 443 ssl; # managed by Certbot
125125
ssl_certificate /etc/letsencrypt/live/podcasts.archive.example.com/fullchain.pem; # managed by Certbot
126126
ssl_certificate_key /etc/letsencrypt/live/podcasts.archive.example.com/privkey.pem; # managed by Certbot
@@ -142,14 +142,14 @@ server {
142142
143143
Enable the site and restart nginx:
144144
145-
``` sh
145+
```sh
146146
sudo ln -s /etc/nginx/sites-available/petit-pois /etc/nginx/sites-enabled/
147147
sudo nginx -t && sudo systemctl reload nginx
148148
```
149149
150150
Further information on Nginx and web server configuration is outwith the scope of this guide.
151151
152-
# Bootstrap a Podcast from Local Files (Advanced)
152+
# Bootstrap a Podcast from Local Files
153153
154154
In some cases you may already have local MP3 files (or partial archives) and want to generate a valid podcast feed without downloading from an RSS source.
155155
For this, use `bootstrap_local_podcast.py`.
@@ -165,14 +165,13 @@ Use `bootstrap_local_podcast.py` if:
165165
- The original feed no longer exists
166166
- You want a complete historical feed, even with gaps
167167
168-
169168
### Input: Episode Metadata JSONL
170169
171170
The bootstrap script consumes a JSONL (JSON-per-line) file describing episodes.
172171
173172
Each line represents one episode, with the following format:
174173
175-
``` sh
174+
```sh
176175
{"episode":"Episode Title","date":"YYYY-MM-DD","file":"audio_file.mp3"}
177176
```
178177
@@ -183,7 +182,7 @@ When an episode has `"file": null`, this means the episode is missing.
183182
184183
You can then run:
185184
186-
``` sh
185+
```sh
187186
python3 bootstrap_local_podcast.py \
188187
--jsonl metadata.jsonl \
189188
--podcast_dir path/to/archive

generate_feeds.py

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -150,21 +150,50 @@ def generate_rss_for_podcast(
150150
rss_path = os.path.join(podcast_dir, "archive.xml")
151151
tree = ET.ElementTree(rss)
152152
tree.write(rss_path, encoding="utf-8", xml_declaration=True)
153+
# With the nginx config, only /secure/<token>/<filename> is reachable.
154+
# If there's no token, the feed won't be accessible over HTTP.
155+
url_prefix = f"{base_url}/secure/{token}" if token else None
156+
feed_url = f"{url_prefix}/archive.xml"
153157
print(f"✅ Generated RSS: {rss_path}")
158+
print("token", token)
159+
return feed_url
154160

155161

156-
def generate_all_feeds(archive_root: str, base_url: str, map_file: str = None):
162+
def generate_all_feeds(archive_root: str, base_url: str, map_file: os.PathLike = None):
163+
if map_file is None:
164+
print("⚠️ No token map file provided. RSS feeds will not be obfuscated.")
157165
token_map = load_token_map(map_file) if map_file else {}
158-
for dir_name in os.listdir(archive_root):
166+
print("token_map", token_map, map_file)
167+
results = []
168+
169+
for dir_name in sorted(os.listdir(archive_root)):
159170
dir_path = os.path.join(archive_root, dir_name)
160-
if os.path.isdir(dir_path):
161-
token = next(
162-
(t for t, folder in token_map.items() if folder == dir_name), None
163-
)
164-
if not token:
165-
print(f"⚠️ No token found for {dir_name}.")
166-
title = dir_name.replace("_", " ")
167-
generate_rss_for_podcast(dir_path, title, base_url, token)
171+
if not os.path.isdir(dir_path):
172+
continue
173+
174+
token = next((t for t, folder in token_map.items() if folder == dir_name), None)
175+
176+
title = dir_name.replace("_", " ")
177+
feed_url = generate_rss_for_podcast(dir_path, title, base_url, token)
178+
179+
if feed_url:
180+
results.append((title, feed_url))
181+
182+
# ---- Print table at the end ----
183+
if results:
184+
title_width = max(len(r[0]) for r in results)
185+
url_width = max(len(r[1]) for r in results)
186+
187+
print("\n" + "─" * (title_width + url_width + 5))
188+
print("Generated Podcast Feeds")
189+
print("─" * (title_width + url_width + 5))
190+
print(f"{'Podcast'.ljust(title_width)} {'Feed URL'.ljust(url_width)}")
191+
print(f"{'-' * title_width} {'-' * url_width}")
192+
193+
for title, url in results:
194+
print(f"{title.ljust(title_width)} {url}")
195+
196+
print()
168197

169198

170199
if __name__ == "__main__":
@@ -181,7 +210,7 @@ def generate_all_feeds(archive_root: str, base_url: str, map_file: str = None):
181210
)
182211
parser.add_argument(
183212
"--map_file",
184-
help="Optional path to token map (JSON format). Enables secure token-based URLs.",
213+
help="Optional path to token map (nginx-formatted map). Enables secureish token-based URLs.",
185214
)
186215

187216
args = parser.parse_args()

0 commit comments

Comments
 (0)