ShadowCast is a React + TypeScript podcast player for English listening and speaking practice. It uses the PodcastIndex.org API to discover podcasts and episodes, then combines audio playback with synced transcripts for shadow reading.
- Search podcasts with PodcastIndex
/search/byterm - Browse podcast details and episode lists, with CC / duration filters
- Play podcast audio with custom controls, Media Session lock-screen support, and an installable PWA shell
- Parse transcripts from SRT, VTT, JSON, HTML, and plain text; export cues back to SRT
- Highlight the active transcript cue while audio plays
- Distinguish speakers when transcript labels or PodcastIndex
personsmetadata are available - Shadow reading controls:
- cue-by-cue navigation
- repeat current cue
- loop current cue or an arbitrary A-B range
- playback speed presets
- keyboard shortcuts
- Practice workbench (logged in):
- microphone recording with original-vs-you comparison (recordings stay in the browser)
- dictation mode that masks the transcript and diffs what you type against the cue
- click any word for a dictionary lookup and save it to a personal vocab book (CSV export for Anki)
- practice minutes, 7-day chart, and streaks in
/library - learning preferences (level, goal, daily target, default playback speed) synced to the account
- Cost and abuse controls: per-user daily transcription quota, login/registration rate limiting
- Vite dev proxy for PodcastIndex auth headers and transcript CORS
The local development proxy reads credentials from .env.local:
PODCAST_INDEX_KEY=your_key
PODCAST_INDEX_SECRET=your_secretAuthentication and account features also require these variables for Pages Functions:
AUTH_PASSWORD_PEPPER=long_random_password_pepper
AUTH_SESSION_SECRET=long_random_session_secret
AUTH_SESSION_DAYS=30
GITHUB_CLIENT_ID=your_github_oauth_client_id
GITHUB_CLIENT_SECRET=your_github_oauth_client_secret
APP_URL=http://localhost:8788For GitHub OAuth, configure the callback URL in the GitHub OAuth app as:
<APP_URL>/api/auth/github/callback
This file is intentionally ignored by git. The current project already includes a local .env.local for this workspace using the credentials you supplied.
Install dependencies and run:
npm install
npm run devOpen the Vite URL, usually:
http://localhost:5173
Apply D1 migrations when working with Pages Functions auth or transcripts (migration 0004 adds learning preferences, the vocab book, practice-day stats, and the rate-limit table — required after upgrading):
npm run db:migrate:local
npm run db:migrate:remoteOptional cost-control variable for Pages Functions:
# Max transcription jobs per user per rolling 24 hours (default 5)
TRANSCRIPTION_DAILY_LIMIT=5Run unit tests (transcript parsers, dictation diff):
npm testPodcastIndex authentication requires a secret. This app uses a Vite development proxy so the secret is not embedded in browser source code. For production deployment, add a server/API route that implements the same proxy behavior:
- browser calls your backend
/api/podcastindex/... - backend injects
X-Auth-Date,X-Auth-Key, andAuthorization - backend forwards the request to
https://api.podcastindex.org/api/1.0/...
Do not ship PodcastIndex secrets in static frontend bundles.
- Search for a podcast topic, e.g.
news,science, orenglish learning. - Open a podcast feed.
- Choose an episode marked
CCwhen available. - Use the Episode Shadow Room:
- slow playback to
0.8× - listen to one cue
- press
Ror clickRepeat - speak along on the next pass
- enable cue loop for difficult phrases
- slow playback to
| Key | Action |
|---|---|
← |
Previous transcript cue |
→ |
Next transcript cue |
R |
Repeat active cue |
Space |
Play / pause |
, |
Back 5 seconds |
. |
Forward 5 seconds |
GET /search/bytermGET /podcasts/byfeedidGET /episodes/byfeedidGET /episodes/byidGET /recent/data
- Transcript availability depends on each podcast feed. Not every episode exposes transcript metadata.
- Speaker labels are best-effort: transcript-embedded labels are used first, then PodcastIndex
personsmetadata is used as fallback context. - Transcript fetching goes through
/api/transcript?url=...during development to avoid cross-origin failures.