Generate a personal podcast of your Instapaper reading queue
375
papercast turns your unread Instapaper articles into a podcast. On each run it syncs your Instapaper "unread" folder, narrates each new article with Google's Chirp 3 HD text-to-speech, and writes MP3s plus an RSS feed to a directory for your own webserver to serve. Articles you archive or delete in Instapaper are removed from the feed on the next run. Episode descriptions can optionally be generated by an LLM via any OpenAI-compatible API.
It's designed to run from cron; nothing is interactive after the one-time Instapaper login.
tts.google_service_account_key_path at it (or set GOOGLE_APPLICATION_CREDENTIALS yourself). Pick voices from the Chirp 3 HD voice list.PATH. (The Docker image includes them.)Every installation method except Docker requires ffmpeg; the Homebrew formula and Debian package pull it in for you.
brew install cdzombak/oss/papercast
Install my Debian repository if you haven't already:
sudo apt-get install ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://dist.cdzombak.net/deb.key | sudo gpg --dearmor -o /etc/apt/keyrings/dist-cdzombak-net.gpg
sudo chmod 0644 /etc/apt/keyrings/dist-cdzombak-net.gpg
echo -e "deb [signed-by=/etc/apt/keyrings/dist-cdzombak-net.gpg] https://dist.cdzombak.net/deb/oss any oss\n" | sudo tee -a /etc/apt/sources.list.d/dist-cdzombak-net.list > /dev/null
sudo apt update
Then install papercast via apt:
sudo apt install papercast
Pre-built binaries for Linux and macOS (amd64 and arm64) are attached to every GitHub Release. Debian packages for each release are published as well.
Multi-architecture images are published to Docker Hub and GHCR; see Docker below.
git clone https://github.com/cdzombak/papercast.git
cd papercast
make build
cp out/papercast $INSTALL_DIR
Copy config.example.yaml to config.yaml and edit it. The file is organized into sections:
instapaper: your OAuth consumer key/secret, plus where to store the token from papercast instapaper-logindatabase: path to the SQLite databaseprocessing: minimum article length, retry interval, and attempt limitllm: optional LLM-generated episode descriptions (disabled by default)tts: Google service account key, voice list, and synthesis settingsfeed: podcast metadata and the public base URLarchiver: optional per-episode "Archive or Delete this article in Instapaper" link to a papercast-archiver instanceoutput: directory where the feed and MP3s are writtenEvery field is explained by comments in the example file.
Fill in the whole file before going any further. Every mode of operation, including -instapaper-login and -list-articles, validates the entire config at startup and exits if anything is missing.
Run once, interactively:
papercast instapaper-login -config /path/to/config.yaml
This prompts for your Instapaper username and password, exchanges them via xAuth for a permanent OAuth token, and stores that token (with 0600 permissions) at instapaper.credentials_path. Your password is not stored.
Run papercast generate -config … periodically. Overlap protection is your job; on Linux, flock handles it:
*/30 * * * * flock -n /tmp/papercast.lock papercast generate -config /etc/papercast/config.yaml
papercast is idempotent: each run picks up where the last left off, retrying failed articles up to processing.max_attempts times, spaced at least processing.retry_interval apart.
Exit codes:
0: success1: fatal error, or a failure affecting all articles (e.g. Instapaper sync failed, or every article failed processing)2: invalid command line (unknown command or flag)8: partial success (some articles failed, others succeeded)Point any webserver at output.dir; it contains feed.xml and the episode MP3s. Subscribe to <base_url>/feed.xml in your podcast app.
papercast stages work-in-progress files in a .papercast-work subdirectory of the output directory. It contains nothing secret, but you may want to block it from being served.
Multi-architecture images are published to Docker Hub and GHCR. The image includes ffmpeg. Mount your config (read-only), the directory holding your Instapaper and Google credentials, the database directory, and the output directory:
docker run --rm \
-v /etc/papercast/config.yaml:/config.yaml:ro \
-v /etc/papercast/data:/data \
-v /var/www/papercast:/srv/papercast \
cdzombak/papercast:1 \
generate -config /config.yaml
(This assumes your config points the credentials and database paths under /data and the output directory at /srv/papercast, as the example config does.)
The image sets no UID/GID; pass --user if you care about the ownership of the output files.
The one-time Instapaper login works the same way, with an interactive terminal:
docker run --rm -it \
-v /etc/papercast/config.yaml:/config.yaml:ro \
-v /etc/papercast/data:/data \
cdzombak/papercast:1 \
instapaper-login -config /config.yaml
list-articles: sync with Instapaper, list each article's ID, source, and title, and exitdebug -id <article-id>: reprocess a single article and write a chunk-by-chunk debug HTML file (the exact text/SSML sent to the TTS API, with playable audio for each chunk) plus the assembled MP3 into the work directory. Nothing is published, and the run doesn't count against the article's retry budget.version: print the version and exithelp [command]: print usage for papercast or a single commandEvery command accepts -config (default ./config.yaml) and -log-level (debug, info (default), warn, or error); papercast <command> -h shows a command's flags.
processing.min_words words are skipped permanently and never retried.tts.voices for each article and kept for that article, so retries sound consistent.make build # build for the current platform to ./out
make all # cross-compile for macOS and Linux (amd64, arm64)
make package # build all binaries + .deb packages (requires fpm)
make test # run the test suite
make lint # lint (requires golangci-lint)
make build-docker # build a Docker image for the current machine
MIT; see LICENSE in this repo.
Chris Dzombak
Content type
Image
Digest
sha256:0633d6900…
Size
207.9 MB
Last updated
about 1 month ago
docker pull cdzombak/papercast