LaunchedEffect(state.albumSort) { scrollToItem(0) } feuerte nicht nur beim
Sortierwechsel, sondern auch beim ersten Compose nach der Rueckkehr aus einem
Album — die im ViewModel gehaltene Position wurde also sofort ueberschrieben.
Das Hochscrollen sitzt jetzt in setAlbumSort() und laeuft nur bei echtem
Wechsel; dazu zwei ungenutzte remember*State-Importe entfernt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
NovaSonic
A self-hosted music streaming PWA and Android app for Navidrome / Subsonic-compatible servers.
Built with Vite + vanilla ES modules (web) and Jetpack Compose + Media3/ExoPlayer (Android).
Features
- Library — Stream from Navidrome / Subsonic; browse artists, albums, playlists, genres, starred, and random; search; star + context menu on every row.
- Playback — Queue with shuffle, repeat, and radio mode (endless similar songs); gapless-style preload; lock-screen / notification controls; Navidrome scrobbling.
- Lyrics — Synced and plain lyrics via lrclib.net, auto-fetched and cached per session.
- Artist pages — Hero card, bio / similar artists / top songs, Ticketmaster events, and a Deezer cross-reference showing owned vs. missing tracks. Same-name artists can be pinned (Deezer search or a pasted Deezer link/id) to fix the wrong-discography and wrong-photo problem; a pinned artist's similar list comes from Deezer.
- Playlists — Create, rename, delete, add/remove tracks, and upload a cover (Web + Android).
- Importer & downloads — Paste a Spotify / YouTube / YouTube Music / SoundCloud link to download via yt-dlp, with Deezer metadata enrichment and duplicate detection.
- Metadata editor (web) — Edit tags + cover art; files are renamed/moved and Navidrome is rescanned on save.
- Offline (Android) — Download albums, playlists, or single songs to private storage; an Offline tab to play and manage them (works without a server connection); downloaded tracks play from disk on every screen.
- Listening stats — A separate, self-hosted scrobble service records timestamped plays (multi-user; Navidrome only keeps a play count). A statistics view (Web + Android, from Settings) shows totals, top artists/tracks/albums with cover art, a day/month/year play series, and a 24h listening clock, plus a Musik-Match that compares two users' taste. Runs in addition to Navidrome/Last.fm scrobbling.
- Audiobookshelf — Browse and stream audiobooks with chapter navigation, speed control, sleep timer, and progress sync.
Web is an installable PWA (themes, German UI); Android is native Jetpack Compose + Media3/ExoPlayer with a mini player, media/ABS notifications, and Android Auto. See CHANGELOG.md for the full web/Android parity matrix.
Project Structure
NovaSonic/
├── src/
│ ├── player/
│ │ ├── player.js # Playback, queue, transport, media session, preload
│ │ ├── nowplaying.js # Now Playing overlay + synced lyrics
│ │ └── scrobbleLog.js # Play-log capture + offline queue → scrobble service
│ ├── views/
│ │ ├── detail.js # Artist / album detail pages
│ │ ├── views.js # Navigation, page loaders, search
│ │ ├── events.js # Ticketmaster concert events
│ │ ├── home.js # Home page
│ │ ├── search.js # Search, artist/album navigation
│ │ ├── playlists.js # Playlist management
│ │ ├── spotify.js # Spotify importer
│ │ ├── youtube.js # YouTube importer
│ │ ├── soundcloud.js # SoundCloud importer
│ │ ├── navigation.js # Client-side router
│ │ └── abs.js # Audiobookshelf integration
│ ├── ui/
│ │ ├── ui.js # Modals, sidebar, context menu
│ │ ├── components.js # Song rows, cards, shared UI
│ │ ├── downloads.js # Download modal + job tracker
│ │ ├── metadata.js # Metadata editor
│ │ ├── modals.js # Song info, events modals
│ │ ├── notify.js # Toast notifications
│ │ ├── stats.js # Listening statistics view (overlay)
│ │ └── settings.js # Settings page
│ ├── state.js # Shared mutable state (S object)
│ ├── utils.js # Helpers (md5, esc, formatDur, toast)
│ ├── api.js # Subsonic API, Deezer API, cover art
│ └── main.js # App init, audio wiring, window globals
├── backend/
│ ├── novasonic-server.js # Express API: downloads, metadata, events proxy, ABS proxy
│ └── lib/
│ ├── config.js # Env var → config object
│ ├── jobs.js # In-memory download job store
│ ├── deezer.js # Deezer metadata: track/album/artist lookups
│ ├── metadata.js # Artwork, filename helpers, duplicate detection
│ ├── ytdlp.js # yt-dlp download + ffmpeg metadata embedding
│ ├── downloader.js # Unified downloadTrack / downloadAlbumTracks
│ └── romaji.js # Japanese → romaji search index
├── scrobble/ # Isolated listening-stats service (own container + DB)
│ └── scrobble-server.js # node:sqlite play-log: ingest / history / stats / health
├── android/ # Native Android app (Jetpack Compose + Media3)
│ └── app/src/main/kotlin/com/novasonic/app/
│ ├── api/ # SubsonicClient, AbsClient, LrcLibClient, ItunesClient (Deezer-backed)
│ ├── data/ # PrefsStore (DataStore), MusicRepository, StarState
│ ├── offline/ # Room DB, DAO, OfflineRepository, DownloadWorker (offline downloads)
│ ├── player/ # PlayerController, MusicService (Media3)
│ ├── ui/
│ │ ├── screen/ # HomeScreen, NowPlayingScreen, AbsScreen, SettingsScreen, …
│ │ ├── component/ # MiniPlayer, SongRow, SongContextSheet, …
│ │ └── theme/ # Color, Type, Theme
│ └── viewmodel/ # ViewModel per screen
├── public/
│ ├── sw.js # Service worker
│ ├── manifest.json # PWA manifest
│ └── config.js # Runtime config (generated by Docker entrypoint)
├── index.html
├── styles.css
├── vite.config.js
├── Dockerfile
├── docker-compose.yml # Standard: zieht die fertigen Images aus der Registry
├── docker-compose.dev.yml # Variante: baut lokal aus den Dockerfiles
├── nginx.conf
└── docker-entrypoint.sh # Injects NAVIDROME_URL / BACKEND_URL / SCROBBLE_URL at runtime
Setup
1. Clone and configure
git clone <repo>
cd NovaSonic
cp .env.example .env
Edit .env:
NAVIDROME_URL=https://music.yourdomain.com
BACKEND_URL=https://novasonic-api.yourdomain.com
SCROBBLE_URL=https://novasonic-scrobble.yourdomain.com # listening-stats service
MUSIC_PATH=/srv/navidrome/music
AUDIO_FMT=mp3 # mp3 | flac | opus | best
AUDIO_QUALITY=0 # 0 = best (yt-dlp)
MAX_PARALLEL=2
TICKETMASTER_API_KEY= # optional — free key from developer.ticketmaster.com
2. Start
docker compose pull
docker compose up -d
Zieht die drei fertigen Images aus der Registry — nichts wird auf dem Server gebaut. Dafür in der
.env REGISTRY (und optional TAG) setzen; die Images heißen
$REGISTRY/novasonic-frontend|-backend|-scrobble. Neue Stände lädt man von der Dev-Maschine mit
npm run push-images hoch (bzw. docker build -t $REGISTRY/<name>:latest <context> && docker push …
— Kontexte: . (frontend), backend/, scrobble/).
Selbst bauen statt ziehen (keine Registry nötig):
docker compose -f docker-compose.dev.yml up -d --build
Baut lokal aus den Dockerfiles; die Deps stecken in den Images, also sind Restarts sofort da und
Rebuilds nach einem git pull dank Layer-Cache schnell.
Frontend on port 8080, backend on port 3700, scrobble service on port 3710. Route each through your reverse proxy. Important: the scrobble subdomain must not sit behind an SSO / forward-auth gateway — it authenticates itself via Navidrome (ping.view), and a gateway in front redirects the app's API calls to a login page.
3. First launch
- Enter your Navidrome server URL, username, and password
- Optionally add a Last.fm API key in Settings for bios and recommendations
- Optionally connect Audiobookshelf in Settings
- Optionally add the NovaSonic backend URL in Settings to enable downloads
- Optionally add the NovaSonic scrobble URL in Settings to record listening stats
Environment Variables
Frontend container
| Variable | Description |
|---|---|
NAVIDROME_URL |
Pre-fills the Navidrome server URL on the login screen |
BACKEND_URL |
Pre-fills the download backend URL on the login screen |
SCROBBLE_URL |
Pre-fills the listening-stats (scrobble) service URL on the login screen |
Backend container
| Variable | Default | Description |
|---|---|---|
PORT |
3700 |
Port the backend listens on |
MUSIC_PATH |
/music |
Absolute path to your Navidrome music folder |
AUDIO_FMT |
mp3 |
Download format (mp3, flac, opus, best) |
AUDIO_QUALITY |
0 |
yt-dlp audio quality (0 = best) |
MAX_PARALLEL |
2 |
Max concurrent download jobs |
TICKETMASTER_API_KEY |
— | Free key from developer.ticketmaster.com |
Scrobble container
| Variable | Default | Description |
|---|---|---|
PORT |
3710 |
Port the scrobble service listens on |
DATA_PATH |
/data |
Where the play-log SQLite DB is stored (own volume) |
NAVIDROME_URL |
— | Used to verify user credentials via ping.view (multi-user auth) |
Development
npm install
npm run dev # Vite dev server with HMR
npm run build # Production build → dist/
npm run preview # Preview the production build locally