Skip to content

Repository files navigation

ShadowCast — English Podcast Shadow Reader

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.

Features

  • 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 persons metadata 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

Setup

The local development proxy reads credentials from .env.local:

PODCAST_INDEX_KEY=your_key
PODCAST_INDEX_SECRET=your_secret

Authentication 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:8788

For 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 dev

Open 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:remote

Optional cost-control variable for Pages Functions:

# Max transcription jobs per user per rolling 24 hours (default 5)
TRANSCRIPTION_DAILY_LIMIT=5

Run unit tests (transcript parsers, dictation diff):

npm test

Important security note

PodcastIndex 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, and Authorization
  • backend forwards the request to https://api.podcastindex.org/api/1.0/...

Do not ship PodcastIndex secrets in static frontend bundles.

Practice workflow

  1. Search for a podcast topic, e.g. news, science, or english learning.
  2. Open a podcast feed.
  3. Choose an episode marked CC when available.
  4. Use the Episode Shadow Room:
    • slow playback to 0.8×
    • listen to one cue
    • press R or click Repeat
    • speak along on the next pass
    • enable cue loop for difficult phrases

Keyboard shortcuts

Key Action
Previous transcript cue
Next transcript cue
R Repeat active cue
Space Play / pause
, Back 5 seconds
. Forward 5 seconds

API endpoints used

  • GET /search/byterm
  • GET /podcasts/byfeedid
  • GET /episodes/byfeedid
  • GET /episodes/byid
  • GET /recent/data

Notes

  • 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 persons metadata is used as fallback context.
  • Transcript fetching goes through /api/transcript?url=... during development to avoid cross-origin failures.

About

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages