mirror of
https://github.com/magnus919/agent-skills.git
synced 2026-09-11 19:47:12 +03:00
Full skill-builder rebuild of transistor per issue #407: - scripts/transistor (renamed from transistor-cli): 19 commands covering the verified API surface - user probe (GET /v1; the /v1/user route does not exist), shows/episodes with corrected pagination[page]/pagination[per], the dedicated episode publish endpoint (PATCH /v1/episodes/:id/publish with episode[status]=draft|scheduled|published), authorize-upload flow, the three real analytics routes with downloads[] array summing, subscriber management incl. batch, and webhooks. Write bodies are form-encoded bracket keys exactly as documented; dry-run plans carry method/path/params/body; publish guard refuses audio-less episodes. Fixed stale claims: /analytics/show -> /v1/analytics/..., totals -> downloads arrays, pagination[limit] -> pagination[per], user email -> name/time_zone, dropped invented episodes_count/subscribers_count and POST /v1/shows (show creation is dashboard-only). - scripts/test_transistor.py: 50 offline tests (pytest + unittest green, proxy-trap clean) covering help, argument errors, dry-run plans, canned JSON:API compound-document parsing (data/attributes/relationships/ included[]), write-path body shapes, publish guard, create->audio-> publish pipeline, and HTTP error signatures. - references/: auth+JSON:API envelope with jq patterns, endpoint catalog, publish lifecycle with documented request/response shapes, gotchas field guide + worked recipes; all cited to live-verified sources. - evals/evals.json: 8 schema-v1 cases incl. two should-not-trigger probes. - SKILL.md rewritten (308 lines), README refreshed, root README blurb and generated catalogs synced (marketplace.json + llms.txt descriptions). Publish-body shape reconciliation: the contract's data.id+data.type JSON:API PATCH premise was falsified by current official docs (verified 2026-08-29) and the flimzy/transistor Go SDK; implemented reality escalated in handoff (see library/transistor-api-facts.md). Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
309 lines
14 KiB
Markdown
309 lines
14 KiB
Markdown
---
|
|
name: transistor
|
|
description: >-
|
|
Operate Transistor.fm podcast hosting from the terminal: verify API access,
|
|
browse shows and episodes with JSON:API-aware output, run the episode
|
|
publish lifecycle (create draft, attach audio, publish or schedule via the
|
|
dedicated publish endpoint), pull download analytics, and manage private
|
|
podcast subscribers and webhooks. Use when the user mentions Transistor,
|
|
Transistor.fm, podcast hosting, episode publishing, private podcast
|
|
subscribers, or podcast download analytics. Do not use this skill for other
|
|
podcast hosts (Buzzsprout, Libsyn, Megaphone, Spotify for Creators), for
|
|
editing or producing audio, or for feed/RSS parsing — the bundled CLI
|
|
manages a Transistor account through its v1 API and cannot create new
|
|
shows (dashboard-only).
|
|
license: MIT
|
|
compatibility: Requires TRANSISTOR_API_KEY env var (Account page -> API Access
|
|
at https://dashboard.transistor.fm/account), Python 3.8+, and `requests`.
|
|
Read commands need a working key; `--help` and `--dry-run` never do.
|
|
metadata:
|
|
tags: transistor, podcast, podcast-hosting, episodes, analytics, api-client
|
|
sources: https://developers.transistor.fm/, https://support.transistor.fm/
|
|
---
|
|
|
|
# transistor — Transistor.fm podcast hosting from the terminal
|
|
|
|
Drive a Transistor.fm account over its v1 JSON:API: shows, episodes, the
|
|
draft→publish lifecycle, per-day download analytics, private-podcast
|
|
subscribers, and webhooks. Responses are JSON:API documents; the bundled CLI
|
|
unwraps them (`--json`) while preserving the raw shapes agents need for jq.
|
|
Write commands are guarded: episode creation is always a draft, and
|
|
publishing goes through its own dedicated endpoint.
|
|
|
|
## Setup
|
|
|
|
1. Find your API key on the Transistor dashboard **Account page → API
|
|
Access** (https://dashboard.transistor.fm/account) and export it:
|
|
|
|
```bash
|
|
export TRANSISTOR_API_KEY="<API_KEY>"
|
|
```
|
|
|
|
2. Verify the key (GET /v1 — the authorization probe; there is no
|
|
/v1/user route):
|
|
|
|
```bash
|
|
transistor user # name and time zone
|
|
transistor user --json | jq '{id, name, time_zone}'
|
|
```
|
|
|
|
A key carries the dashboard role of its user (owner / admin / team member)
|
|
per podcast. `--help` and `--dry-run` work without credentials. Requests are
|
|
rate-limited to 10 per 10 seconds; the CLI dies with a clear 429 message
|
|
instead of hammering.
|
|
|
|
## Essential Commands
|
|
|
|
### user — authorization probe
|
|
|
|
```bash
|
|
transistor user # who does this key belong to?
|
|
transistor user --json
|
|
```
|
|
|
|
### shows / show — browse podcasts
|
|
|
|
```bash
|
|
transistor shows # newest-updated first
|
|
transistor shows --private --json # private podcasts only
|
|
transistor show --id <SHOW_ID_OR_SLUG> # full attributes incl. feed_url
|
|
transistor shows --page 1 --per 20 --json
|
|
```
|
|
|
|
Show ids and slugs are interchangeable on most show-scoped routes. Show
|
|
resources carry no counts fields — count via `episodes --show ... --json`,
|
|
then `meta.totalCount`.
|
|
|
|
### episodes / episode — browse episodes
|
|
|
|
```bash
|
|
transistor episodes # newest first, all shows
|
|
transistor episodes --show <SHOW_ID> --status draft # drafts for one show
|
|
transistor episodes --show <SHOW_ID> --per 50 --page 1 --json
|
|
transistor episode --id <EPISODE_ID> --include show # compound doc + parent show
|
|
```
|
|
|
|
`--include show` adds `included[]` (the JSON:API compound document); every
|
|
episode item in `--json` output already carries `show_id` resolved from
|
|
relationships. `--limit` works as an alias for `--per` for old scripts.
|
|
|
|
### episode-create / episode-update — drafts and metadata
|
|
|
|
```bash
|
|
transistor episode-create --show <SHOW_ID> --title "Ep 12: Roasting" \
|
|
--season 2 --number 4 --audio-url "https://uploads.example.com/ep12.mp3"
|
|
transistor episode-update --id <EPISODE_ID> --title "New title"
|
|
transistor episode-update --id <EPISODE_ID> --audio-url "<AUDIO_URL>" # attach audio
|
|
```
|
|
|
|
`episode-create` ALWAYS produces a draft (`status: "draft"`,
|
|
`published_at: null`) — it never publishes. `episode-update` changes
|
|
metadata or attaches audio and never touches publishing state.
|
|
|
|
### episode-publish — the lifecycle switch
|
|
|
|
```bash
|
|
transistor episode-publish --id <EPISODE_ID> # publish now
|
|
transistor episode-publish --id <EPISODE_ID> --status scheduled \
|
|
--published-at "2026-09-03 09:00:00" # schedule
|
|
transistor episode-publish --id <EPISODE_ID> --status draft # unpublish
|
|
```
|
|
|
|
Hits `PATCH /v1/episodes/<EPISODE_ID>/publish` with
|
|
`episode[status]=draft|scheduled|published` — the documented dedicated
|
|
endpoint. The CLI refuses to publish an episode whose `media_url` is still
|
|
empty (an unplayable item would hit every subscriber's feed); pass
|
|
`--force` to override.
|
|
|
|
### authorize-upload — local audio (max 5GB)
|
|
|
|
```bash
|
|
transistor authorize-upload --filename ep12.mp3 # plan only
|
|
transistor authorize-upload --filename ep12.mp3 --file ./ep12.mp3
|
|
```
|
|
|
|
Returns (and, with `--file`, performs) the signed PUT; the printed
|
|
`audio_url` is what you attach with `episode-create`/`episode-update`. The
|
|
signed URL expires (~600 s in the docs' example).
|
|
|
|
### analytics / episode-analytics — downloads per day
|
|
|
|
```bash
|
|
transistor analytics --show <SHOW_ID> # last 14 days
|
|
transistor analytics --show <SHOW_ID> \
|
|
--start-date 01-08-2026 --end-date 28-08-2026 --json
|
|
transistor episode-analytics --id <EPISODE_ID> --json
|
|
```
|
|
|
|
Dates are dd-mm-yyyy and must come in pairs. Analytics attributes are
|
|
per-day `downloads[]` arrays, not totals; the CLI sums them into
|
|
`downloads_total` and keeps the raw array.
|
|
|
|
### subscribers — private podcast audience
|
|
|
|
```bash
|
|
transistor subscribers --show <SHOW_ID> --json
|
|
transistor subscriber-create --show <SHOW_ID> --email "listener@example.com"
|
|
transistor subscriber-batch --show <SHOW_ID> --email "a@example.com" --email "b@example.com"
|
|
transistor subscriber-delete --show <SHOW_ID> --email "a@example.com" # or --id
|
|
```
|
|
|
|
### webhooks — push instead of poll
|
|
|
|
```bash
|
|
transistor webhooks --show <SHOW_ID>
|
|
transistor webhook-create --show <SHOW_ID> --event episode_published \
|
|
--url "https://example.com/hooks/transistor"
|
|
transistor webhook-delete --id <WEBHOOK_ID>
|
|
```
|
|
|
|
Events: `episode_created`, `episode_published`, `subscriber_created`,
|
|
`subscriber_deleted`. Cap: 50 per account. With a 10 req / 10 s limit,
|
|
webhooks beat polling for freshness.
|
|
|
|
## Global flags
|
|
|
|
```bash
|
|
transistor --json shows # flags work in any position
|
|
transistor --dry-run episodes --show <SHOW_ID> # request plan, zero network
|
|
transistor --force episode-publish --id <EPISODE_ID> # skip the audio guard
|
|
transistor --quiet shows # suppress non-essential output
|
|
transistor --verbose episodes # detailed stderr logging
|
|
```
|
|
|
|
`--dry-run` emits `{"dry_run": true, "method", "path", "params"}` (write
|
|
commands add the exact `body` that would be sent — bracket keys and all),
|
|
so you can verify a plan before touching the API. `--help` and `--dry-run`
|
|
never require credentials.
|
|
|
|
## Pipeline recipes
|
|
|
|
### Create, attach audio, publish (the core workflow)
|
|
|
|
```bash
|
|
export TRANSISTOR_API_KEY="<API_KEY>"
|
|
SHOW=$(transistor shows --json | jq -r '.shows[0].id') # string id
|
|
AUDIO=$(transistor authorize-upload --filename ep12.mp3 --file ./ep12.mp3 --json | jq -r '.audio_url')
|
|
EP=$(transistor episode-create --show "$SHOW" --title "Ep 12" \
|
|
--audio-url "$AUDIO" --json | jq -r '.id') # draft id
|
|
transistor episode-publish --id "$EP" # dedicated endpoint
|
|
transistor episode --id "$EP" --json | jq '{status, media_url, published_at}'
|
|
```
|
|
|
|
Each stage's output feeds the next: `shows` → string `id`,
|
|
`authorize-upload` → string `audio_url`, `episode-create` → string draft
|
|
`id`, `episode-publish` → final `status`. Stage 2 is skippable when the
|
|
audio already has a public URL (pass it straight to `episode-create`).
|
|
|
|
### Draft triage: what is not out yet?
|
|
|
|
```bash
|
|
transistor episodes --show <SHOW_ID> --status draft --json \
|
|
| jq -r '.episodes[] | [.id, .title, (if .media_url == "" then "no-audio" else "ready" end)] | @tsv'
|
|
# publish the ready ones (rate limit: 10 req / 10 s — add sleep 1 between calls)
|
|
```
|
|
|
|
### Weekly downloads report
|
|
|
|
```bash
|
|
transistor shows --json | jq -r '.shows[].id' | while read -r S; do
|
|
transistor analytics --show "$S" --json \
|
|
| jq -r --arg id "$S" '[$id, (.downloads_total|tostring)] | @tsv'
|
|
sleep 1
|
|
done
|
|
```
|
|
|
|
## JSON and jq
|
|
|
|
`--json` keys are stable snake_case wrappers around the JSON:API document:
|
|
`shows`/`episodes`/`subscribers`/`webhooks` (arrays with `meta` attached),
|
|
flat objects for single resources, `dry_run`/`method`/`path`/`params`/`body`
|
|
for plans. Attributes keep Transistor's own names — `status`, `season`,
|
|
`number`, `duration` (seconds), `media_url`, `share_url`, `published_at`,
|
|
`feed_url` — so jq selectors transfer directly to raw `curl` against
|
|
api.transistor.fm. Collection pagination surfaces as
|
|
`meta.currentPage`/`meta.totalPages`/`meta.totalCount`. Example:
|
|
`transistor episodes --show <SHOW_ID> --json | jq -r '.episodes[] |
|
|
[.id, .title, .status] | @tsv'`. For compound documents the CLI resolves
|
|
relationships (`show_id`) and prints `included` show summaries in human
|
|
mode; with raw curl, match `included[]` by `type` and
|
|
`relationships.show.data.id`.
|
|
|
|
## Known Gotchas
|
|
|
|
- **Publishing is a separate endpoint, never a side effect** — POST
|
|
/episodes and PATCH /episodes/:id cannot change `status`. If an episode
|
|
stays draft, the missing step is `PATCH /v1/episodes/<ID>/publish` with
|
|
`episode[status]=published`. (The pre-thickening CLI had no publish path
|
|
at all.)
|
|
- **The user probe is `GET /v1`** — `/v1/user` and `/v1/authorization` are
|
|
404s, and the user resource has no email attribute (name and time_zone
|
|
only).
|
|
- **Pagination is `pagination[page]` + `pagination[per]`** (defaults 0 and
|
|
10; docs' examples request page 1). `pagination[limit]` and
|
|
`page[number]` are silently ignored — loops using them re-read page 1
|
|
forever. Loop while `meta.currentPage < meta.totalPages`.
|
|
- **Show resources carry no counts** — derive episode/subscriber counts
|
|
from filtered listings' `meta.totalCount`.
|
|
- **Analytics are per-day arrays, not totals** — sum `attributes.downloads`
|
|
(the CLI provides `downloads_total`); date bounds are dd-mm-yyyy and
|
|
come in pairs; do not parse the row date format (docs' examples are
|
|
inconsistent between sections).
|
|
- **Show creation is dashboard-only** — there is no POST /v1/shows;
|
|
`show-update` is the only show write.
|
|
- **Rate limit 10 req / 10 s** — a 429 blocks access for 10 seconds. No
|
|
retry headers; back off, batch subscriber imports, cache responses, and
|
|
use webhooks for freshness. Transistor explicitly forbids using the API
|
|
as a website back end (parse the RSS feed for that).
|
|
- **`episode[published_at]` uses the show's time zone** (a show attribute),
|
|
not UTC; scheduling and backdating both ride the publish endpoint.
|
|
- **Audio processing is asynchronous** — watch `audio_processing` /
|
|
`processing_failure` after attaching audio; publishing an unprocessed or
|
|
failed file pushes silence to subscribers.
|
|
- **Signed upload URLs expire** (~600 s in the docs' example): authorize,
|
|
PUT with the returned `content_type`, attach promptly.
|
|
- **Error bodies are not formally specified** — the CLI handles JSON:API
|
|
`errors[]` arrays and bare `{"message": ...}` objects, flattening either
|
|
to one stderr line; 401 (bad key), 403 (role), 404 (bad id/route), 429
|
|
(rate limit) have distinct hints.
|
|
|
|
## When to use
|
|
|
|
Use this skill for anything that reads or drives a Transistor.fm account
|
|
through its API: verifying API access, browsing shows/episodes (including
|
|
drafts and compound documents), running the episode lifecycle
|
|
(create → attach audio → publish/schedule/unpublish), pulling download
|
|
analytics windows, importing or revoking private-podcast subscribers, and
|
|
registering webhooks.
|
|
|
|
## When not to use
|
|
|
|
Do not use this skill for other podcast hosts (Buzzsprout, Libsyn,
|
|
Megaphone, Spotify for Creators — use their own APIs/tooling); for audio
|
|
production or editing (ffmpeg and DAW territory); for generic RSS feed
|
|
parsing or website rendering (parse the feed XML directly — Transistor says
|
|
the API is not a back-end data source); for creating new shows (the API
|
|
cannot — the dashboard does); or for platform-level distribution questions
|
|
(Apple/Spotify submission is a dashboard and RSS concern).
|
|
|
|
## Reference Files
|
|
|
|
| File | Use it for |
|
|
| ---- | ---------- |
|
|
| [references/auth-and-basics.md](references/auth-and-basics.md) | x-api-key auth, key location and role scoping, the JSON:API envelope (data/attributes/relationships/included[]) with jq patterns, pagination params, error surfaces |
|
|
| [references/endpoint-catalog.md](references/endpoint-catalog.md) | Every route's method, path, and parameters (shows, episodes, publish, uploads, analytics, subscribers, webhooks) plus routes that do not exist |
|
|
| [references/episode-publish-lifecycle.md](references/episode-publish-lifecycle.md) | The draft/scheduled/published state machine, the exact publish request/response shapes, create→audio→publish recipes in CLI and curl, authorize-upload detour |
|
|
| [references/gotchas-and-recipes.md](references/gotchas-and-recipes.md) | Symptom → cause → fix field guide (404 user route, silent pagination, 429 storms...) and multi-step workflows (bulk scheduling, analytics reports, subscriber import, webhooks) |
|
|
|
|
## Available Scripts and Prerequisites
|
|
|
|
- `scripts/transistor` — the bundled Python CLI (`--json`, `--dry-run`,
|
|
`--force`, `--quiet`, `--verbose`, `--help` everywhere). Imports only the
|
|
standard library and `requests`; sends write bodies exactly as documented
|
|
(bracket-key form fields).
|
|
- `scripts/test_transistor.py` — offline test suite (pytest + unittest
|
|
compatible); all HTTP mocked with canned JSON:API documents, zero network
|
|
egress, no live-call cases (Transistor is a keyed API).
|
|
- Requires Python 3.8+, `requests`, and `TRANSISTOR_API_KEY` for live
|
|
commands (Account page → API Access). No service is started by this skill.
|