Phonograph is a production Progressive Web App focused on audio playback, currently centered on podcasts. It combines a modern React frontend, a resilient browser playback engine, and Netlify function proxies for third-party APIs.
Production URL: https://phonograph.app
User manual: https://phonograph.app/docs/
Deliver a first-class, fast, and reliable audio experience on the web with strong mobile/PWA behavior.
Evolve Phonograph from a podcast-first player into a flexible audio platform where additional audio experiences can be integrated behind a shared playback and offline-capable core.
- Discover podcasts through curated and searchable sources.
- Save podcasts into a personal library and revisit episodes quickly.
- Play audio with playlist controls and per-podcast playback preferences.
- Run as a PWA with service worker registration and cached assets.
- Proxy third-party APIs through Netlify Functions for consistency and deployment control.
Phonograph is organized around a client runtime and a lightweight serverless proxy layer.
- Entry point:
src/index.tsx- Boots React 19, internationalization wrapper, router, and service worker registration.
- App shell:
src/App.tsx- Sets routes (
/discover,/library,/podcast,/playlist,/settings). - Lazily loads heavy views for better startup performance.
- Initializes playback engine, event wiring, and background worker refresh.
- Sets routes (
- UI system: Material UI (
@mui/material) with app theming insrc/theme.ts.
- State store: Zustand store in
src/store/appStore.ts.- Reducer-style dispatch for predictable state transitions.
- Persists app state in
localStorage.
- Reducer logic:
src/reducer.ts- Handles library updates, playback status, queue changes, and settings updates.
- Playback control:
src/engine/player.ts+src/engine/events.ts- Drives
HTMLAudioElementbehavior. - Integrates Media Session actions for OS-level play/pause support.
- Tracks progress, buffering, completion, and per-podcast skip/speed preferences.
- Drives
- Podcast engine wrapper:
src/engine/index.ts+src/podcast.ts. - Core feature areas:
- Discovery:
src/podcast/Discovery/ - Library:
src/podcast/Library.tsx - Episode/podcast detail views:
src/podcast/PodcastView/ - Settings/import support:
src/podcast/Settings.tsx,src/podcast/opmlImporter.ts
- Discovery:
- Service worker registration:
src/serviceworker/index.ts - Worker refresh loop:
src/serviceworker/worker.ts- Uses the podcast engine in a worker context to refresh library metadata in the background.
- Build-time SW patching:
swGenerator.js- Rewrites the generated
service-worker.jsprecache list from actualdist/assets.
- Rewrites the generated
Located in lambda/ and routed via netlify.toml:
findCast.ts: RSS fetch proxyfindFinal.ts: URL resolution proxyapple.tsandappleSearch.ts: Apple/iTunes search and metadata proxyinglistenNotesProxy.ts: Listen Notes API proxy with selective CDN caching
This layer avoids exposing provider details directly in the client and centralizes API behavior.
- Frontend: React 19, React Router v5, Material UI v6
- Build tooling: Vite 5, TypeScript 5
- State management: Zustand
- Audio/podcast utilities:
podcastsuite,audioqueue - Serverless runtime: Netlify Functions (
@netlify/functions) - Testing: Vitest
- Deployment: Netlify
src/
core/ # Shared UI + playback controls
engine/ # Audio engine wiring and events
i18n/ # Internationalization wrappers/messages
platform/ # Runtime abstraction (web + tauri adapters)
podcast/ # Podcast discovery/library/view features
serviceworker/ # SW registration + web worker logic
store/ # Zustand store
App.tsx
index.tsx
lambda/ # Netlify Functions
src-tauri/ # Tauri desktop host (Rust + tauri.conf)
public/ # Static assets copied to dist/
dist/ # Production build output
docs/ # Planning and migration docs
manuals/ # User-facing manual source (VitePress)
- Node.js 20+
- Yarn
- Rust toolchain (
rustup) - Tauri CLI (installed via
yarn install)
yarn installlistennotes=YOUR_API_KEY yarn devThis starts:
- Vite app on
http://localhost:1234 - Netlify functions on
http://localhost:9999
yarn startyarn desktop:devBuild desktop bundles:
yarn desktop:build- Pushing a semver tag (for example
v1.3.24) triggers.github/workflows/desktop-release-macos.yml. - The workflow publishes two stable assets to the GitHub release:
Phonograph-macOS-Apple-Silicon.dmgPhonograph-macOS-Intel.dmg
- The app exposes an in-product download screen at
/downloadand a shortcut from Settings → Desktop App.
yarn build
yarn serveBuild manuals only:
yarn docs:manuals:buildRun manuals locally:
yarn docs:manuals:devBuild pipeline (yarn build) includes:
- Fetching discovery JSON assets (
filegetter.sh) - Vite production build
- SEO/static file copy
- Service worker precache rewrite
- User manual static build to
/dist/docs - Netlify function build
LISTEN_NOTES_API_KEY(preferred)LISTENNOTES/listennotes(backward-compatible alternatives)
These are used for discovery/proxy calls that depend on Listen Notes.
Run tests with:
yarn testRun coverage locally with:
yarn test:coverageValidate the pull-request changed-file coverage gate (70% minimum across lines/functions/branches/statements):
COVERAGE_BASE_REF=origin/master yarn coverage:changedRun lint checks with:
yarn lint
yarn lint:errorsRun the local quality gate sequence with:
yarn qualityPR CI now enforces a dual-surface quality matrix:
web-quality: typecheck, lint, test, and production web bundle build.desktop-compile: desktop compile smoke validation whensrc-tauri/is present.desktop-package-smoke: unsigned desktop package smoke build and artifact upload for QA.merge-gate: fails when any required web/desktop check fails.
Desktop smoke artifacts are published as desktop-unsigned-<run_id> in the Actions run.
Framework and linting standards are documented in docs/framework-best-practices.md.
Current repository includes targeted tests for reducers, engine events, app store behavior, and podcast utilities.
- Continue hardening offline playback and sync behavior.
- Keep improving provider abstraction in function proxies.
- Expand audio-platform extensibility beyond podcasts.
- Continue TypeScript and test coverage improvements where needed.