Gpodder
A gPodder podcast library as swamp data, and a pipeline from new downloads to notes: transcripts, Claude's summaries and chapters, Markdown for a personal knowledge base, a weekly listening audit and feed health checks.
Three model types. @beddari/gpodder/library reads gPodder's SQLite database read-only: subscriptions without their credentials, episodes with their files, show notes as Markdown and feed chapters; downloads returns what was downloaded since its last run, by a cursor; listening records what was played in a window; feedHealth fetches every feed, probes its newest enclosures and records incidents (dead, moved, stale, not a feed, no published transcripts, downloads gone or untranscribed), each error and warning also logged. @beddari/gpodder/transcriber turns an episode into timed segments and WebVTT with whisper.cpp on the machine or any OpenAI-compatible transcription endpoint. @beddari/gpodder/enricher has Claude read the show notes and transcript and return a schema-checked summary, topics, timestamped chapters, key points, cleaned notes and mentions, and exports every enriched episode as a Markdown note with front matter, e.g. into an Obsidian vault or a notes repository.
Three workflows wire them: @beddari/gpodder-ingest (hourly, or from the gPodder hook in the README right after a download), @beddari/gpodder-feed-health (daily) and @beddari/gpodder-weekly-listening (Sundays), with a listening report and a feed health report. Nothing ever writes to gPodder.
First release: gPodder library as data, whisper.cpp/OpenAI transcription, Claude enrichment with Markdown export, weekly listening and feed health workflows and reports.
Global Arguments
| Argument | Type | Description |
|---|---|---|
| home? | string | gPodder's home directory, holding Database. Default: $GPODDER_HOME, else ~/Library/Application Support/gPodder |
| downloads? | string | gPodder's download directory. Default: $GPODDER_DOWNLOAD_DIR, else <home>/Downloads |
| busyTimeoutMs? | coerce.number | How long a read waits for a gPodder that is writing its database |
| httpTimeoutMs? | coerce.number | Per-request timeout for feeds and enclosures, in milliseconds |
| concurrency? | coerce.number | Feeds fetched at once by feedHealth |
| maxFeedBytes? | coerce.number | Bytes of a feed read at most; a larger feed is read up to here |
| Argument | Type | Description |
|---|---|---|
| state? | enum | downloaded: files on disk; new: gPodder's new episodes; all: every episode |
| podcastIds? | array | Only these podcasts; every podcast when omitted |
| limit? | coerce.number | The newest this many, by publication date |
| Argument | Type | Description |
|---|---|---|
| backfill? | coerce.number | On the first run only: also return this many of the newest downloads instead of taking them all as seen |
| limit? | coerce.number | New downloads returned at most per run; the rest stay new for the next |
| pendingFor? | array | Definitions, e.g. a transcriber and an enricher, whose record of an episode (episodeId and guid) means it is done. |
| maxAttempts? | coerce.number | How often an episode is returned before it is given up on |
| Argument | Type | Description |
|---|---|---|
| days? | coerce.number | Length of the window, ending at until |
| until? | string | End of the window, ISO 8601; now when omitted |
| outputDir? | string | Also write the summary as <outputDir>/<YYYY-Www>.md, e.g. into a notes repository; |
| Argument | Type | Description |
|---|---|---|
| podcastIds? | array | Only these podcasts; every subscription when omitted |
| includePaused? | boolean | Also check subscriptions gPodder has paused |
| enclosures? | coerce.number | Newest enclosures probed per feed, with HEAD; 0 probes none |
| staleDays? | coerce.number | A feed whose newest episode is older than this is stale |
| transcriptTag? | boolean | Report feeds that publish no <podcast:transcript>, as info |
| checkDownloads? | boolean | Report downloaded episodes whose file is gone |
| transcriber? | string | Name of a @beddari/gpodder/transcriber definition: report downloaded episodes it has not transcribed |
| failOn? | enum | Fail the method, after recording everything, on an incident of at least this severity |
Resources
Global Arguments
| Argument | Type | Description |
|---|---|---|
| library? | string | Name of the @beddari/gpodder/library definition whose episode records are transcribed |
| engine? | enum | whisper-cpp: whisper.cpp on this machine, audio never leaves it. |
| language? | string | Spoken language as an ISO 639-1 code, e.g. en or nb; auto lets the model detect it |
| whisperBinary? | string | whisper.cpp's command-line program (whisper-cli; main in older builds) |
| whisperModel? | string | whisper.cpp model file, e.g. ~/models/ggml-large-v3-turbo.bin; required by whisper-cpp. |
| ffmpegBinary? | string | ffmpeg, which decodes the episode for whisper.cpp |
| threads? | coerce.number | whisper.cpp threads |
| apiUrl? | string | Base URL of the OpenAI-compatible API, without /audio/transcriptions |
| apiKey? | string | API key for the openai engine, e.g. ${{ vault.get("<vault>", "openai-api-key") }}; |
| apiModel? | string | Transcription model on that endpoint; it must return verbose_json segments for timestamps |
| maxUploadBytes? | coerce.number | Largest file the openai engine sends; OpenAI's limit is 25 MB |
| timeoutMs? | coerce.number | How long one transcription may take, decoding included |
| Argument | Type | Description |
|---|---|---|
| episodeId | coerce.number | gPodder's id of a downloaded episode, as in the library's episode-<id> records |
| force? | boolean | Transcribe again even when this episode's file was already transcribed with this engine and model |
Resources
Files
Global Arguments
| Argument | Type | Description |
|---|---|---|
| library? | string | Name of the @beddari/gpodder/library definition holding the episode records |
| transcriber? | string | Name of the @beddari/gpodder/transcriber definition holding the transcripts |
| apiKey? | string | Anthropic API key, e.g. ${{ vault.get("<vault>", "anthropic-api-key") }}. |
| baseUrl? | string | Anthropic API base URL, for a proxy or gateway |
| model? | string | Claude model id |
| effort? | enum | How hard Claude thinks; medium suits summarising, high gives finer chapters on long episodes |
| maxTokens? | coerce.number | Ceiling on Claude's answer, thinking included |
| fallbacks? | boolean | On a policy refusal, let the API retry on another model (server-side fallback, Claude API only) |
| topics? | array | A fixed vocabulary of topics; when given, every topic is one of these |
| outputLanguage? | string | Language of summaries and chapter titles, e.g. English or Norwegian; the episode's own when omitted |
| exportDir? | string | Where export writes notes when its outputDir is not given, e.g. ~/Notes/Podcasts; with neither, export writes nothing |
| maxInputChars? | coerce.number | Largest prompt sent; an episode over it fails rather than being cut |
| Argument | Type | Description |
|---|---|---|
| episodeId | coerce.number | gPodder's id of the episode, as in the library's episode-<id> records |
| requireTranscript? | boolean | Fail when the transcriber has no transcript of the episode, instead of working from the show notes |
| force? | boolean | Ask Claude again even when this episode was enriched from the same transcript with the same model |
| Argument | Type | Description |
|---|---|---|
| outputDir? | string | Directory the notes go in, one subdirectory per podcast, e.g. ~/Notes/Podcasts; the definition's exportDir when omitted. |
| since? | string | Only episodes published on or after this date, ISO 8601 |
| includeTranscript? | boolean | Append the timed transcript to each note |
| includeAudioUrl? | boolean | Put the episode's media URL in each note's front matter. Off by default: a private feed's media URLs |
Resources
Turn gPodder's new downloads into notes. Finds the episodes downloaded since the last run (the library's downloads method and its cursor), transcribes each one, has Claude summarise it and mark its chapters, and writes every enriched episode as a Markdown note into the enricher's exportDir. Uses three definitions by name: gpodder (@beddari/gpodder/library), gpodder-transcriber (@beddari/gpodder/transcriber) and gpodder-enricher (@beddari/gpodder/enricher); create them first. Each job takes only
Daily health check of every gPodder subscription. Fetches each feed and probes its newest enclosures, records one feedHealth record per feed and one healthCheck with every incident (unreachable or failing feeds, feeds not RSS or Atom, permanent redirects, stale and empty feeds, dead enclosures, feeds without published transcripts, downloads whose file is gone, and downloads not transcribed when a transcriber is named), logs each error and warning, and renders the feed health report. Uses the gpo
Every Sunday evening, what was played in gPodder that week: per podcast and per episode, finished or how far, recorded as the week's listening record and rendered by the listening report. With outputDir, the week's page is also written there as YYYY-Www.md, e.g. into a private notes repository. Uses the gpodder definition (@beddari/gpodder/library). gPodder knows positions reached, not time spent, and only what its player reports.
A library's feed health check: incidents by severity
What was played in a library's listening window, by podcast and by episode
- Has README or module doc2/2earned
- README has a code example1/1earned
- README is substantive1/1earned
- Most symbols documented1/1earned
- No slow types (deprecated)1/1earned
- Dependencies pass trust audit2/2earned
- Docs keep secrets in a vault1/1earned
- Has description1/1earned
- Platform support declared (or universal)2/2earned
- License2/2earned
- Verified public repository2/2earned