update with build script to configure base path and generate files dynamically
This commit is contained in:
parent
6f63c1a734
commit
6915aac092
10 changed files with 250 additions and 20 deletions
51
README.md
51
README.md
|
|
@ -1,20 +1,61 @@
|
|||
# ButterBox Content Pack Sample Podcast
|
||||
|
||||
A small, self-contained sample podcast webpage and RSS feed from [Guardian Project](https://guardianproject.info). This repository is intended as an example "content pack" — a minimal, portable bundle of everything needed to host and distribute a podcast: episode audio, show notes, a landing page, and a standards-compliant feed.
|
||||
A small, self-contained sample podcast webpage and RSS feed from [Guardian Project](https://guardianproject.info). This repository is intended as an example "content pack": a minimal, portable bundle of everything needed to host and distribute a podcast, including episode audio, show notes, a landing page, and a standards-compliant feed.
|
||||
|
||||
## What's inside
|
||||
|
||||
- **`index.html`** : A simple, styled landing page that lists each episode with inline audio playback.
|
||||
- **`podcast.xml`** : An RSS 2.0 feed with iTunes podcast extensions, suitable for submission to podcast directories and use in podcast players.
|
||||
- **`*.mp3`** : The three episode audio files.
|
||||
- **`*.md`** : Per-episode show notes in Markdown.
|
||||
- **`posts/*.md`** : Per-episode show notes in Markdown (YAML frontmatter holds the episode metadata).
|
||||
- **`posts/*.mp3`** : The episode audio files, colocated with their show notes.
|
||||
- **`build.sh`** : Regenerates `index.html` and `podcast.xml` from the contents of `posts/`.
|
||||
|
||||
## Using it
|
||||
|
||||
Because everything is relative-linked, you can serve this site from a folder on a USB drive plugged into a ButterBox device.
|
||||
Because everything in `index.html` is relative-linked, you can serve this site from a folder on a USB drive plugged into a ButterBox device.
|
||||
|
||||
To subscribe in a podcast client, point it at the hosted URL of `podcast.xml`.
|
||||
|
||||
## Building `index.html` and `podcast.xml`
|
||||
|
||||
Both output files are generated from the markdown posts in `posts/` by `build.sh`. Run the script any time you add, remove, or edit a post:
|
||||
|
||||
```sh
|
||||
./build.sh <baseURL>
|
||||
```
|
||||
|
||||
The single required argument is the **absolute URL prefix where the mp3 files will be served**. It is only used for the `<enclosure url="...">` entries in `podcast.xml`, because RSS enclosures must be absolute URLs so podcast clients can download them without knowing where the feed lives. Everything else in the generated files (the RSS `<link>`, the `<audio>` players in `index.html`, and the link to the feed itself) stays relative, so you can still move the whole bundle around and serve it from any path.
|
||||
|
||||
### Example
|
||||
|
||||
If the files will be served from a USB drive called `usb-butter` plugged into a ButterBox, inside a folder named `engardepodcast`, run:
|
||||
|
||||
```sh
|
||||
./build.sh http://butterbox.local/serve_file/media/usb-butter/engardepodcast/
|
||||
```
|
||||
|
||||
Note that `butterbox.local` in the URL must match the configured hostname of your ButterBox. `butterbox.local` is the default, but if you have changed yours to, for example, `someotherbox.local`, use that hostname instead.
|
||||
|
||||
The script will produce `podcast.xml` with enclosures like:
|
||||
|
||||
```
|
||||
http://butterbox.local/serve_file/media/usb-butter/engardepodcast/posts/ProofModeCaravan-English-Feb2024.mp3
|
||||
```
|
||||
|
||||
and an `index.html` whose audio elements still use the relative `posts/<file>.mp3` path.
|
||||
|
||||
### How it works
|
||||
|
||||
For each `posts/*.md` file (sorted newest-first by filename date prefix), the script:
|
||||
|
||||
1. Parses the YAML frontmatter (`title`, `date`, `description`, `file`, `voices`, `keywords`, `duration`/`length`, `explicit`).
|
||||
2. Looks up the referenced mp3 in `posts/` and reads its real byte size with `stat` for the RSS `length` attribute.
|
||||
3. Picks whichever of the `duration`/`length` frontmatter fields actually looks like `HH:MM[:SS]` for `<itunes:duration>` (the two fields are used inconsistently across the sample posts).
|
||||
4. Normalizes the `date` field into an RFC 822 `<pubDate>`.
|
||||
5. Emits one `<item>` block into `podcast.xml` and one `<article class="episode">` block into `index.html`.
|
||||
|
||||
Adding a new episode is just a matter of dropping a new `YYYY-MM-DD-slug.md` file and its `.mp3` into `posts/` and re-running `build.sh`.
|
||||
|
||||
## License
|
||||
|
||||
All content in this repository — audio, show notes, webpage, and feed — is released under [Creative Commons Zero (CC0 1.0)](https://creativecommons.org/publicdomain/zero/1.0/). You may copy, modify, distribute, and use the material for any purpose, including commercial, without asking permission.
|
||||
All content in this repository (audio, show notes, webpage, and feed) is released under [Creative Commons Zero (CC0 1.0)](https://creativecommons.org/publicdomain/zero/1.0/). You may copy, modify, distribute, and use the material for any purpose, including commercial, without asking permission.
|
||||
|
|
|
|||
189
build.sh
Executable file
189
build.sh
Executable file
|
|
@ -0,0 +1,189 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [ $# -ne 1 ]; then
|
||||
echo "Usage: $0 <baseURL>" >&2
|
||||
echo " <baseURL>: absolute URL prefix for mp3 enclosures" >&2
|
||||
echo " e.g. https://example.com/podcast" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BASE_URL="${1%/}"
|
||||
POSTS_DIR="posts"
|
||||
HTML_OUT="index.html"
|
||||
XML_OUT="podcast.xml"
|
||||
|
||||
PODCAST_TITLE="Guardian Project Podcast"
|
||||
PODCAST_DESC="A small sample podcast from Guardian Project covering decentralized web, privacy-preserving measurement, and content provenance for human rights."
|
||||
PODCAST_AUTHOR="Guardian Project"
|
||||
PODCAST_LINK="index.html"
|
||||
|
||||
# Extract a single frontmatter field from a markdown file
|
||||
fm_get() {
|
||||
awk -v key="$2" '
|
||||
/^---[[:space:]]*$/ { fm++; if (fm == 2) exit; next }
|
||||
fm == 1 {
|
||||
i = index($0, ":")
|
||||
if (i == 0) next
|
||||
k = substr($0, 1, i-1)
|
||||
v = substr($0, i+1)
|
||||
sub(/^[[:space:]]+/, "", k); sub(/[[:space:]]+$/, "", k)
|
||||
sub(/^[[:space:]]+/, "", v); sub(/[[:space:]]+$/, "", v)
|
||||
if (k != key) next
|
||||
if ((substr(v,1,1)=="\"" && substr(v,length(v))=="\"") || \
|
||||
(substr(v,1,1)=="\x27" && substr(v,length(v))=="\x27")) {
|
||||
v = substr(v, 2, length(v)-2)
|
||||
}
|
||||
print v
|
||||
exit
|
||||
}
|
||||
' "$1"
|
||||
}
|
||||
|
||||
# Escape text for safe use in HTML/XML element content and attributes
|
||||
esc() {
|
||||
local s="$1"
|
||||
# In bash 5.2+, & in the replacement means "matched text", so escape as \&.
|
||||
s="${s//&/\&}"
|
||||
s="${s//</\<}"
|
||||
s="${s//>/\>}"
|
||||
s="${s//\"/\"}"
|
||||
printf '%s' "$s"
|
||||
}
|
||||
|
||||
# Newest-first list of posts (filenames start with YYYY-MM-DD so sort works)
|
||||
mapfile -t POSTS < <(find "$POSTS_DIR" -maxdepth 1 -name '*.md' | sort -r)
|
||||
|
||||
if [ "${#POSTS[@]}" -eq 0 ]; then
|
||||
echo "No markdown files found in $POSTS_DIR/" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- podcast.xml header ---
|
||||
{
|
||||
cat <<EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd">
|
||||
<channel>
|
||||
<title>$(esc "$PODCAST_TITLE")</title>
|
||||
<link>$(esc "$PODCAST_LINK")</link>
|
||||
<language>en-us</language>
|
||||
<description>$(esc "$PODCAST_DESC")</description>
|
||||
<itunes:author>$(esc "$PODCAST_AUTHOR")</itunes:author>
|
||||
<itunes:summary>$(esc "$PODCAST_DESC")</itunes:summary>
|
||||
<itunes:explicit>no</itunes:explicit>
|
||||
EOF
|
||||
} > "$XML_OUT"
|
||||
|
||||
# --- index.html header ---
|
||||
{
|
||||
cat <<EOF
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>$(esc "$PODCAST_TITLE")</title>
|
||||
<style>
|
||||
body { font-family: system-ui, sans-serif; max-width: 720px; margin: 2em auto; padding: 0 1em; line-height: 1.5; color: #222; }
|
||||
h1 { border-bottom: 2px solid #333; padding-bottom: .3em; }
|
||||
.episode { border: 1px solid #ddd; border-radius: 6px; padding: 1em 1.2em; margin: 1.2em 0; background: #fafafa; }
|
||||
.episode h2 { margin: 0 0 .3em 0; font-size: 1.15em; }
|
||||
.meta { color: #666; font-size: .85em; margin-bottom: .6em; }
|
||||
audio { width: 100%; margin-top: .5em; }
|
||||
.feed-link { font-size: .9em; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>$(esc "$PODCAST_TITLE")</h1>
|
||||
<p>$(esc "$PODCAST_DESC") <span class="feed-link"><a href="podcast.xml">RSS feed</a></span></p>
|
||||
|
||||
EOF
|
||||
} > "$HTML_OUT"
|
||||
|
||||
# --- per-post items ---
|
||||
for post in "${POSTS[@]}"; do
|
||||
title=$(fm_get "$post" title)
|
||||
date_raw=$(fm_get "$post" date)
|
||||
desc=$(fm_get "$post" description)
|
||||
[ -z "$desc" ] && desc=$(fm_get "$post" summary)
|
||||
file=$(fm_get "$post" file)
|
||||
voices=$(fm_get "$post" voices)
|
||||
keywords=$(fm_get "$post" keywords)
|
||||
explicit=$(fm_get "$post" explicit)
|
||||
[ -z "$explicit" ] && explicit="no"
|
||||
|
||||
# Duration string is whichever of duration/length looks like HH:MM[:SS].
|
||||
# The two fields are used inconsistently across existing posts.
|
||||
dur_field=$(fm_get "$post" duration)
|
||||
len_field=$(fm_get "$post" length)
|
||||
if [[ "$dur_field" =~ ^[0-9]+:[0-9]+(:[0-9]+)?$ ]]; then
|
||||
duration="$dur_field"
|
||||
elif [[ "$len_field" =~ ^[0-9]+:[0-9]+(:[0-9]+)?$ ]]; then
|
||||
duration="$len_field"
|
||||
else
|
||||
duration=""
|
||||
fi
|
||||
|
||||
if [ -z "$file" ]; then
|
||||
echo "Warning: no file: field in $post, skipping" >&2
|
||||
continue
|
||||
fi
|
||||
mp3_path="$POSTS_DIR/$file"
|
||||
if [ ! -f "$mp3_path" ]; then
|
||||
echo "Warning: missing mp3 $mp3_path, skipping $post" >&2
|
||||
continue
|
||||
fi
|
||||
filesize=$(stat -c %s "$mp3_path")
|
||||
|
||||
date_short=$(printf '%s' "$date_raw" | awk '{print $1}')
|
||||
# Strip trailing numeric timezone (some posts use "-500" which date(1) rejects)
|
||||
date_norm=$(printf '%s' "$date_raw" | sed -E 's/[[:space:]]+[+-]?[0-9]{3,4}$//')
|
||||
pubdate=$(date -R -d "$date_norm" 2>/dev/null || date -R -d "$date_short")
|
||||
|
||||
guid="${file%.mp3}"
|
||||
|
||||
# podcast.xml item — enclosure is absolute, everything else is plain text
|
||||
{
|
||||
printf '\n <item>\n'
|
||||
printf ' <title>%s</title>\n' "$(esc "$title")"
|
||||
printf ' <description>%s</description>\n' "$(esc "$desc")"
|
||||
printf ' <pubDate>%s</pubDate>\n' "$pubdate"
|
||||
printf ' <enclosure url="%s/posts/%s" length="%s" type="audio/mpeg"/>\n' \
|
||||
"$BASE_URL" "$file" "$filesize"
|
||||
printf ' <guid isPermaLink="false">%s</guid>\n' "$(esc "$guid")"
|
||||
[ -n "$duration" ] && printf ' <itunes:duration>%s</itunes:duration>\n' "$duration"
|
||||
[ -n "$voices" ] && printf ' <itunes:author>%s</itunes:author>\n' "$(esc "$voices")"
|
||||
printf ' <itunes:explicit>%s</itunes:explicit>\n' "$(esc "$explicit")"
|
||||
[ -n "$keywords" ] && printf ' <itunes:keywords>%s</itunes:keywords>\n' "$(esc "$keywords")"
|
||||
printf ' </item>\n'
|
||||
} >> "$XML_OUT"
|
||||
|
||||
# index.html article — audio src is a relative path into posts/
|
||||
meta="$date_short"
|
||||
[ -n "$duration" ] && meta="$meta · $duration"
|
||||
[ -n "$voices" ] && meta="$meta · Voices: $(esc "$voices")"
|
||||
|
||||
{
|
||||
printf '<article class="episode">\n'
|
||||
printf ' <h2>%s</h2>\n' "$(esc "$title")"
|
||||
printf ' <div class="meta">%s</div>\n' "$meta"
|
||||
printf ' <p>%s</p>\n' "$(esc "$desc")"
|
||||
printf ' <audio controls preload="none" src="%s/%s"></audio>\n' "$POSTS_DIR" "$file"
|
||||
printf '</article>\n\n'
|
||||
} >> "$HTML_OUT"
|
||||
done
|
||||
|
||||
# --- close files ---
|
||||
cat >> "$XML_OUT" <<EOF
|
||||
|
||||
</channel>
|
||||
</rss>
|
||||
EOF
|
||||
|
||||
cat >> "$HTML_OUT" <<EOF
|
||||
</body>
|
||||
</html>
|
||||
EOF
|
||||
|
||||
echo "Wrote $XML_OUT and $HTML_OUT (baseURL: $BASE_URL)"
|
||||
14
index.html
14
index.html
|
|
@ -16,27 +16,27 @@
|
|||
</head>
|
||||
<body>
|
||||
<h1>Guardian Project Podcast</h1>
|
||||
<p>A small sample podcast from Guardian Project. <span class="feed-link"><a href="podcast.xml">RSS feed</a></span></p>
|
||||
<p>A small sample podcast from Guardian Project covering decentralized web, privacy-preserving measurement, and content provenance for human rights. <span class="feed-link"><a href="podcast.xml">RSS feed</a></span></p>
|
||||
|
||||
<article class="episode">
|
||||
<h2>ProofMode for Indigenous Rights in Mexico</h2>
|
||||
<div class="meta">2024-02-09 · 30:08 · Voices: Fabiola, Armando, Nicolas</div>
|
||||
<p>We invited team members of the "El Sur Resiste" movement to the Guardian Project podcast to share with us their experiences using ProofMode to document the negative impact of the Mayan Train development in southern states of Mexico.</p>
|
||||
<audio controls preload="none" src="ProofModeCaravan-English-Feb2024.mp3"></audio>
|
||||
<p>We invited team members of the "El Sur Resiste" movement to the Guardian Project podcast to share with us their experiences using ProofMode to document the negative impact of the Mayan Train development in southern states of Mexico.</p>
|
||||
<audio controls preload="none" src="posts/ProofModeCaravan-English-Feb2024.mp3"></audio>
|
||||
</article>
|
||||
|
||||
<article class="episode">
|
||||
<h2>Clean Insights and Privacy Preserving Measurement</h2>
|
||||
<div class="meta">2023-03-22 · 34:28 · Voices: John</div>
|
||||
<p>Introduction to Privacy Preserving Measurement and Clean Insights. We talk about the various kinds of privacy folks might want, then explain the techniques for achieving them in plain English.</p>
|
||||
<audio controls preload="none" src="cleaninsightsoverviewmarch2023.mp3"></audio>
|
||||
<p>Introduction to Privacy Preserving Measurement and Clean Insights</p>
|
||||
<audio controls preload="none" src="posts/cleaninsightsoverviewmarch2023.mp3"></audio>
|
||||
</article>
|
||||
|
||||
<article class="episode">
|
||||
<h2>Fabiola and David discuss IPFS, Filecoin and the Decentralized Web</h2>
|
||||
<div class="meta">2022-02-17 · 27:42 · Voices: Fabiola and David of Guardian Project</div>
|
||||
<p>A discussion covering details on decentralized web technologies that we are exploring at Guardian Project, including IPFS, IPNS, Filecoin, and ProofMode.</p>
|
||||
<audio controls preload="none" src="news-on-the-dweb.mp3"></audio>
|
||||
<p>A discussion covering details on decentralized web technologies that we are exploring at Guardian Project</p>
|
||||
<audio controls preload="none" src="posts/news-on-the-dweb.mp3"></audio>
|
||||
</article>
|
||||
|
||||
</body>
|
||||
|
|
|
|||
16
podcast.xml
16
podcast.xml
|
|
@ -11,9 +11,9 @@
|
|||
|
||||
<item>
|
||||
<title>ProofMode for Indigenous Rights in Mexico</title>
|
||||
<description>We invited team members of the "El Sur Resiste" movement to the Guardian Project podcast to share with us their experiences using ProofMode to document the negative impact of the Mayan Train development in southern states of Mexico.</description>
|
||||
<pubDate>Fri, 09 Feb 2024 10:00:00 +0000</pubDate>
|
||||
<enclosure url="ProofModeCaravan-English-Feb2024.mp3" length="14467565" type="audio/mpeg"/>
|
||||
<description>We invited team members of the "El Sur Resiste" movement to the Guardian Project podcast to share with us their experiences using ProofMode to document the negative impact of the Mayan Train development in southern states of Mexico.</description>
|
||||
<pubDate>Fri, 09 Feb 2024 10:00:00 -0500</pubDate>
|
||||
<enclosure url="http://butterbox.local/serve_file/media/usb-butter/engardepodcast/posts/ProofModeCaravan-English-Feb2024.mp3" length="14467565" type="audio/mpeg"/>
|
||||
<guid isPermaLink="false">ProofModeCaravan-English-Feb2024</guid>
|
||||
<itunes:duration>30:08</itunes:duration>
|
||||
<itunes:author>Fabiola, Armando, Nicolas</itunes:author>
|
||||
|
|
@ -23,9 +23,9 @@
|
|||
|
||||
<item>
|
||||
<title>Clean Insights and Privacy Preserving Measurement</title>
|
||||
<description>Introduction to Privacy Preserving Measurement and Clean Insights. We talk about the various kinds of privacy folks might want, then explain the techniques for achieving them in plain English.</description>
|
||||
<pubDate>Wed, 22 Mar 2023 12:30:00 -0500</pubDate>
|
||||
<enclosure url="cleaninsightsoverviewmarch2023.mp3" length="18165760" type="audio/mpeg"/>
|
||||
<description>Introduction to Privacy Preserving Measurement and Clean Insights</description>
|
||||
<pubDate>Wed, 22 Mar 2023 12:30:00 -0400</pubDate>
|
||||
<enclosure url="http://butterbox.local/serve_file/media/usb-butter/engardepodcast/posts/cleaninsightsoverviewmarch2023.mp3" length="18165760" type="audio/mpeg"/>
|
||||
<guid isPermaLink="false">cleaninsightsoverviewmarch2023</guid>
|
||||
<itunes:duration>34:28</itunes:duration>
|
||||
<itunes:author>John</itunes:author>
|
||||
|
|
@ -35,9 +35,9 @@
|
|||
|
||||
<item>
|
||||
<title>Fabiola and David discuss IPFS, Filecoin and the Decentralized Web</title>
|
||||
<description>A discussion covering details on decentralized web technologies that we are exploring at Guardian Project, including IPFS, IPNS, Filecoin, and ProofMode.</description>
|
||||
<description>A discussion covering details on decentralized web technologies that we are exploring at Guardian Project</description>
|
||||
<pubDate>Thu, 17 Feb 2022 12:30:00 -0500</pubDate>
|
||||
<enclosure url="news-on-the-dweb.mp3" length="4845568" type="audio/mpeg"/>
|
||||
<enclosure url="http://butterbox.local/serve_file/media/usb-butter/engardepodcast/posts/news-on-the-dweb.mp3" length="4845568" type="audio/mpeg"/>
|
||||
<guid isPermaLink="false">news-on-the-dweb</guid>
|
||||
<itunes:duration>27:42</itunes:duration>
|
||||
<itunes:author>Fabiola and David of Guardian Project</itunes:author>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue