pcast is a local-first command-line podcast manager and player written in Go.
It subscribes to standard RSS/Atom podcast feeds, keeps a durable local episode catalog, reports episodes discovered since the previous check, and streams episodes through an installed media player.
Designed for humans and automation: deterministic behavior, stable local IDs, structured JSON, no prompts, documented exit codes, and strict stdout/stderr separation.
Download a release archive for your platform from GitHub Releases, verify the SHA-256 checksum, and place pcast on your PATH.
go install github.com/Keldrik/pcast/cmd/pcast@latestRequires Go 1.24+. Builds are CGO-free.
git clone <repo-url>
cd pcast-cli
go build -o pcast ./cmd/pcastpcast add https://example.com/podcast.xml --name example
pcast list
pcast latest
pcast episodes example
pcast episode 42
pcast play 42
pcast mark 42 unplayed
pcast remove example| Command | Purpose |
|---|---|
pcast add <feed-url> [--name <alias>] |
Subscribe and baseline current episodes |
pcast remove <podcast> |
Delete a subscription and local episodes |
pcast list |
List subscriptions (local only) |
pcast latest [podcast] |
Check feeds and show newly discovered episodes |
pcast episodes [podcast] [flags] |
Query the cached catalog |
pcast episode <id> |
Show one episode in detail |
pcast play <id> |
Stream through an external player |
pcast mark <id> played|unplayed |
Correct listening state |
pcast doctor |
Local diagnostics |
pcast version |
Build metadata |
Global flags:
--json— machine-readable JSON on stdout (errors on stderr)--data-dir <path>— override storage location--help,--version
pcast episodes [podcast] [--limit N] [--offset N] [--all]
[--played|--unplayed] [--new] [--query TEXT]
--played and --unplayed are mutually exclusive. --all cannot be combined with --limit/--offset.
pcast play <id> [--player EXE] [--player-arg ARG]... [--no-mark-played]
Player resolution order:
--playerPCAST_PLAYER- first of
mpv,vlc,ffplayonPATH - platform opener (
open/xdg-open/ Windows URL handler)
The executable is invoked directly (never through a shell). Repeatable --player-arg values are passed literally before the enclosure URL.
Examples:
pcast play 42 --player mpv --player-arg=--audio-pitch-correction=no --player-arg=--speed=1.25
PCAST_PLAYER=vlc pcast play 42Podcast selectors resolve exactly, in order:
- Digits only → podcast ID
- Value beginning with
http://orhttps://→ normalized feed URL (submitted or resolved) - Anything else → case-insensitive alias, then exact title
- Multiple title matches →
ambiguous_selectorwith candidate IDs
Aliases are trimmed, case-insensitively unique, non-empty, and cannot be digits-only.
Episode commands always take a numeric episode ID. Podcast and episode IDs are separate namespaces.
- On
add, current feed items become the baseline. They appear inepisodesbut are not returned by the nextlatest. - A new episode is one first discovered after the baseline, regardless of publication date.
latesthas at-least-once delivery: episodes are acknowledged only after output is written successfully. An interrupted process may repeat an episode; it should not silently lose one.- Episodes that disappear from a remote feed are never deleted locally.
- If some feeds fail,
lateststill prints successful results, setspartial: true, writes diagnostics to stderr, and exits4.
Success envelope (stdout):
{
"schema_version": 1,
"command": "list",
"data": { }
}Error envelope (stderr only; stdout empty):
{
"schema_version": 1,
"error": {
"code": "not_found",
"message": "no podcast matches \"daily\"",
"details": {}
}
}Field names are snake_case. Timestamps are UTC RFC3339 or null. Collections are always arrays.
| Status | Meaning |
|---|---|
| 0 | Success (including empty results and idempotent ops) |
| 1 | Unexpected internal error or stdout write failure |
| 2 | Invalid command, argument, flag, or conflicting options |
| 3 | Not found or ambiguous selector |
| 4 | Network/feed failure; also partial latest |
| 5 | Storage, migration, lock, or local config failure |
| 6 | Player unavailable or playback failed |
Stable error codes include: invalid_argument, not_found, ambiguous_selector, feed_unavailable, invalid_feed, storage_error, lock_unavailable, player_unavailable, player_failed, internal_error.
Resolution order:
--data-dirPCAST_HOME- Platform default:
- macOS:
~/Library/Application Support/pcast - Linux:
$XDG_DATA_HOME/pcastor~/.local/share/pcast - Windows:
%LOCALAPPDATA%\pcast
- macOS:
Files:
<data-dir>/pcast.db
<data-dir>/pcast.lock
Backup: copy pcast.db while no mutating command is running (or stop all pcast processes).
Reset: delete the data directory.
Tests and automation should always set --data-dir or PCAST_HOME to a temporary path—never the user's real library.
- Fully local: no accounts, telemetry, or cloud sync.
- Network is used only by
add,latest, and media playback (the external player fetches the enclosure URL). - Private/authenticated feeds are not supported in v1.
- URL userinfo is rejected. Sensitive-looking query keys are redacted in diagnostics.
Install one of mpv, vlc, or ffplay, or rely on the platform opener. Interactive controls work because foreground players inherit stdin/stderr. Player stdout is kept off pcast JSON stdout.
An Agent Skills-compatible skill lives at skills/pcast/. It teaches agents how to drive pcast with --json, selectors, exit codes, and safe workflows.
The skill never installs pcast. It only runs the CLI when a pcast binary is already on PATH.
Install the skill into an agent that supports the open skills format (examples):
# from this repo
npx skills add ./skills/pcast
# or copy/symlink
ln -s "$(pwd)/skills/pcast" ~/.agents/skills/pcastProject-local discovery paths are also linked at .agents/skills/pcast and .cursor/skills/pcast.
See agents.md for contributor rules and project.md for the full behavior contract.
gofmt -w $(find . -name '*.go')
go test ./...
go test -race ./...
go vet ./...
staticcheck ./... # when installed
./scripts/smoke.sh ./pcastModule path: github.com/Keldrik/pcast
License: MIT