Sign inSign up

cdzombak/papercast

By cdzombak

•Updated about 1 month ago

Generate a personal podcast of your Instapaper reading queue

Image
0

375

cdzombak/papercast repository overview

⁠papercast

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.

⁠Requirements

  • A paid Instapaper subscription (the Instapaper API⁠ requires one) and an Instapaper OAuth application. Apply for a consumer key/secret here⁠.
  • A Google Cloud project with the Cloud Text-to-Speech API enabled. Create a service account with access scoped to the Text-to-Speech API only, download its JSON key, and point tts.google_service_account_key_path at it (or set GOOGLE_APPLICATION_CREDENTIALS yourself). Pick voices from the Chirp 3 HD voice list⁠.
  • ffmpeg and ffprobe on PATH. (The Docker image includes them.)

⁠Installation

Every installation method except Docker requires ffmpeg; the Homebrew formula and Debian package pull it in for you.

⁠macOS via Homebrew
brew install cdzombak/oss/papercast
⁠Debian via Apt repository

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
⁠Manual installation from build artifacts

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.

⁠Docker image

Multi-architecture images are published to Docker Hub and GHCR; see Docker⁠ below.

⁠Build and install locally
git clone https://github.com/cdzombak/papercast.git
cd papercast
make build

cp out/papercast $INSTALL_DIR

⁠Setup

⁠1. Configure

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-login
  • database: path to the SQLite database
  • processing: minimum article length, retry interval, and attempt limit
  • llm: optional LLM-generated episode descriptions (disabled by default)
  • tts: Google service account key, voice list, and synthesis settings
  • feed: podcast metadata and the public base URL
  • archiver: optional per-episode "Archive or Delete this article in Instapaper" link to a papercast-archiver⁠ instance
  • output: directory where the feed and MP3s are written

Every 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.

⁠2. Log in to Instapaper

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.

⁠3. Run from cron

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: success
  • 1: 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)
⁠4. Serve the output

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.

⁠Docker

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

⁠Other commands

  • list-articles: sync with Instapaper, list each article's ID, source, and title, and exit
  • debug -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 exit
  • help [command]: print usage for papercast or a single command

Every command accepts -config (default ./config.yaml) and -log-level (debug, info (default), warn, or error); papercast <command> -h shows a command's flags.

⁠Behavior notes

  • Articles shorter than processing.min_words words are skipped permanently and never retried.
  • Deleting or archiving an article in Instapaper removes its episode from the feed and its MP3 from disk on the next run. papercast never writes to Instapaper.
  • A voice is picked at random from tts.voices for each article and kept for that article, so retries sound consistent.
  • Every episode description begins with a link to the article. When LLM descriptions are disabled or a request fails, that link is the whole description. Failed descriptions are retried on later runs when the LLM is enabled, and the feed is updated once one succeeds.

⁠Building from source

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

⁠License

MIT; see LICENSE⁠ in this repo.

⁠Author

Chris Dzombak

Tag summary

Content type

Image

Digest

sha256:0633d6900…

Size

207.9 MB

Last updated

about 1 month ago

docker pull cdzombak/papercast