- TypeScript 39.8%
- Python 35.2%
- Svelte 23.5%
- CSS 0.7%
- JavaScript 0.3%
- Other 0.4%
- Suppress spurious 'Empty src attribute' audio_error during blob-fetch src swap (loadAndPlayDirect intentionally clears src; the resulting MEDIA_ELEMENT_ERROR was firing ~10x/day on every blob track). - Clear loadStart on stall/error + add 60s sanity cap, fixing the ms=1378601 (23 min) load_timing readings on stalled podcasts. - Add final_ms (last-attempt only) to request_complete/4xx/5xx/timeout events; existing cumulative ms preserved for backward compat. - Plumb optional level through the SW->client telemetry bridge and downgrade pwa.sw_cache_warm_fail from ERROR to WARN. - Move podcast.progress_save and radio.icy_metadata into VERBOSE_EVENTS (set localStorage.sonus_telemetry_verbose=1 to re-enable). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| .claude | ||
| .forgejo/workflows | ||
| backend | ||
| deploy | ||
| frontend | ||
| .env.example | ||
| .gitignore | ||
| .pre-commit-config.yaml | ||
| CLAUDE.md | ||
| Makefile | ||
| PODCASTS_PLAN.md | ||
| README.md | ||
| REFACTOR-PHASE1.md | ||
| REFACTOR-PHASE2.md | ||
Sonus
A self-hosted, single-user audio orchestrator. Wraps Navidrome (music) behind a single PWA, and drops an AI DJ personality into your personal listening session — intro breaks, ducked transitions between tracks, and periodic weather/news reports.
Architecture at a glance
┌──────────────────────┐ ┌──────────────────────────┐
│ SvelteKit PWA │──HTTPS──▶│ FastAPI backend │
│ (glass UI, lyrics, │ │ - Navidrome proxy │
│ now-playing, …) │ │ - DJEngine (OpenRouter │
└──────────────────────┘ │ + Kokoro / Fish TTS) │
│ - Internet radio proxy │
│ - SQLite (WAL) │
└──────────────────────────┘
All audio playback happens in the browser via <audio> and the Web Audio API.
The backend proxies Navidrome streams with full HTTP Range support for
scrubbing, and can also proxy internet radio stations (parsing ICY metadata for
now-playing info). DJ breaks are generated server-side (LLM script → TTS audio)
and mixed client-side.
Repository layout
sonus/
├── backend/ # FastAPI + SQLAlchemy + async pipelines
│ └── sonus/
│ ├── api/ # Route modules (auth, playback, dj, stations, …)
│ ├── auth/ # Session management (cookie + itsdangerous)
│ ├── dj/ # AI DJ engine, prompts, weather, news
│ │ └── tts/ # TTS providers (Kokoro, Fish Speech)
│ ├── media/ # Navidrome client and proxy
│ ├── models/ # SQLAlchemy models
│ └── radio/ # ICY metadata parsing, cover art lookup
├── frontend/ # SvelteKit + Tailwind PWA
├── deploy/
│ ├── media/ # docker-compose for Kokoro TTS
│ ├── systemd/ # sonus-backend.service
│ └── nginx/ # reverse-proxy config
├── .pre-commit-config.yaml
├── Makefile
└── CLAUDE.md # guidance for future Claude Code sessions
Getting started (dev)
Prerequisites: Python 3.12+, Node 20+, a reachable Navidrome instance.
# Install everything
make install
# Copy and edit the env file
cp .env.example .env
$EDITOR .env
# Run backend + frontend concurrently
make dev # http://localhost:5173
The frontend dev server proxies /api to the backend on port 8000.
Log in with your Navidrome credentials.
Tooling
| Task | Command |
|---|---|
| Lint everything | make lint |
| Format everything | make fmt |
| Type-check | make typecheck |
| Backend tests | make test-backend |
| Frontend tests | make test-frontend |
| E2E (Playwright) | make e2e |
| Pre-commit (all) | make pre-commit |
| Regenerate API types | make generate-types |
Backend uses ruff + mypy --strict + pytest. Frontend uses prettier +
eslint + svelte-check + vitest + Playwright. Pre-commit wires them
together so git commit rejects anything that wouldn't pass CI.
API type pipeline
All API endpoint responses are typed with Pydantic response_model. FastAPI
generates an OpenAPI schema from these models, and openapi-typescript
generates TypeScript types from that schema into
frontend/src/lib/api-types.generated.ts. The frontend api.ts re-exports
friendly type aliases (e.g. Track, AlbumDetail) from the generated file.
Pydantic models (schemas.py)
→ FastAPI OpenAPI JSON (openapi.json)
→ openapi-typescript (api-types.generated.ts)
→ type aliases in api.ts
Run make generate-types after changing any backend response model. CI verifies
the generated types are up to date.
Features
- Library browsing — albums, artists, playlists, search, liked songs
- AI DJ — intro breaks, ducked transitions, weather reports (Open-Meteo), news segments (configurable RSS feed), all probability-gated by a chattiness slider
- Internet radio — add stations by stream URL; backend proxies the stream and parses ICY metadata for now-playing display with cover art lookup
- AI radio — prompt-driven playlist generation via AudioMuse (optional)
- Lyrics — synced and plain lyrics from LRCLIB, cached in SQLite
- Discovery — personalized recommendations
- Listening sessions — tracks what you play for the home page activity feed
- Crossfade — configurable 0–12s crossfade between tracks
- Aurral integration — upcoming releases shelf and deep-link song requests
- PWA — installable, works offline for cached content
Deployment (Proxmox LXC, Tailscale funnel or public reverse-proxy)
Single LXC container. Hybrid layout:
- Bare-metal systemd: FastAPI backend (
deploy/systemd/sonus-backend.service). - Docker Compose: Kokoro TTS (
deploy/media/docker-compose.yml). - Nginx: serves the built SvelteKit bundle and reverse-proxies
/apito the backend (deploy/nginx/sonus.conf).
Public-internet exposure checklist
Sonus is single-user / friends-and-family by design, but if you publish it on the open web (e.g. via a Caddy/nginx + Let's Encrypt setup instead of a Tailscale funnel) make sure all of the following are true:
SONUS_SECRET_KEYis a long random string (≥32 bytes); rotate if leaked.SONUS_COOKIE_SECURE=trueand the site is served exclusively over HTTPS.SONUS_FRONTEND_ORIGINmatches the public origin exactly (CORS allow-list).- The Navidrome service account (
SONUS_NAVIDROME_USERNAME/_PASSWORD) is scoped to read-only library access — it is the fallback identity for background tasks like cover-art lookup. SONUS_NEWS_FEED_URLis set to a feed you trust (the response is parsed and surfaced to the LLM prompt).- The reverse proxy enforces TLS, the existing nginx rate-limit zones are active, and the host firewall blocks anything other than 80/443.
- A backup cron (see below) is wired up before the first user logs in.
SQLite backup
The state lives in a single SQLite file (default
./data/sonus.db). Use deploy/scripts/backup-sqlite.sh to snapshot it via
the WAL-safe sqlite3 .backup API:
# crontab -e
0 4 * * * /opt/sonus/deploy/scripts/backup-sqlite.sh
Backups land in /var/backups/sonus/sonus-<UTC-timestamp>.db.gz (paths and
retention configurable via SONUS_DB, SONUS_BACKUP_DIR, SONUS_KEEP_DAYS).
To restore: gunzip -c sonus-….db.gz > sonus.db and restart the backend.
Notes on external services
- Aurral does not expose a public REST API. The backend builds a deep link
to its search UI so the user lands with a pre-filled query. The
AurralClientalso fetches upcoming releases for the home page shelf. - AudioMuse (optional) powers AI radio mode — prompt-driven playlist
generation. Disabled when
SONUS_AUDIOMUSE_URLis empty. - LRCLIB is the lyrics source; results are cached in SQLite.
- Open-Meteo powers the DJ's weather segments — no API key needed.
- OpenRouter provides the LLM for DJ script generation.