Self-hosted single-user audio orchestrator — Navidrome + Audiobookshelf unified behind one PWA with an AI DJ
  • TypeScript 39.8%
  • Python 35.2%
  • Svelte 23.5%
  • CSS 0.7%
  • JavaScript 0.3%
  • Other 0.4%
Find a file
maks 9443f378fb
All checks were successful
CI / Backend (lint + type-check + test) (push) Successful in 1m48s
CI / Frontend (lint + check + test) (push) Successful in 4m29s
telemetry: trim noise and fix load_timing / request ms semantics
- 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>
2026-05-19 07:39:37 +00:00
.claude Formatting fixes and bump mypy pre-commit pin to v1.20.1 2026-04-25 19:37:12 +02:00
.forgejo/workflows Revert runner label back to linux-amd64 2026-05-17 09:32:50 +02:00
backend Add extensive frontend telemetry to journald 2026-05-15 18:28:32 +02:00
deploy Add extensive frontend telemetry to journald 2026-05-15 18:28:32 +02:00
frontend telemetry: trim noise and fix load_timing / request ms semantics 2026-05-19 07:39:37 +00:00
.env.example Move DJ TTS to the browser via Web Speech API 2026-05-03 12:19:52 +02:00
.gitignore Add Kokoro browser TTS and podcast episode downloads 2026-05-15 09:58:23 +00:00
.pre-commit-config.yaml Pin ruff-pre-commit to v0.15.10 to match venv ruff 2026-04-25 19:40:25 +02:00
CLAUDE.md Move DJ TTS to the browser via Web Speech API 2026-05-03 12:19:52 +02:00
Makefile Move DJ TTS to the browser via Web Speech API 2026-05-03 12:19:52 +02:00
PODCASTS_PLAN.md Remove Audiobookshelf mentions from docs and comments 2026-05-02 22:30:01 +02:00
README.md Remove Audiobookshelf mentions from docs and comments 2026-05-02 22:30:01 +02:00
REFACTOR-PHASE1.md Refactor phase 1: extract shared utilities 2026-04-16 09:58:09 +02:00
REFACTOR-PHASE2.md Refactor phase 2: split backend god files 2026-04-16 10:09:08 +02:00

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 012s 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 /api to 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_KEY is a long random string (≥32 bytes); rotate if leaked.
  • SONUS_COOKIE_SECURE=true and the site is served exclusively over HTTPS.
  • SONUS_FRONTEND_ORIGIN matches 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_URL is 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 AurralClient also fetches upcoming releases for the home page shelf.
  • AudioMuse (optional) powers AI radio mode — prompt-driven playlist generation. Disabled when SONUS_AUDIOMUSE_URL is 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.