* fix(servers): probe gated servers over native reqwest so custom headers connect Adding a server behind a header gate (Cloudflare Access, Pangolin service tokens) failed at the connect step (#1216). The connect probe ran in the WebView (axios); a non-safelisted header such as Authorization / CF-Access-* makes the browser send a CORS preflight OPTIONS first, and that preflight carries no token — so the gate rejects it and the real request never leaves. Streaming and covers already worked because they go through Rust (reqwest), which never preflights. Route the header-bearing connect probe through a new Tauri command `probe_server_connection` that runs the Subsonic ping over native reqwest with the per-server header context (endpoints + apply rule), reusing the same resolver as the data plane. Header-less servers keep the lightweight WebView path unchanged. - Rust: add `probe_server_connection` (+ `ServerProbeResult`) in app_api/network, register in collect_commands!/generate_handler!, regenerate specta bindings. - Frontend: `pingWithCredentialsForProfile` calls the command when the profile has custom headers; add `serverHttpContextWireForProbe` helper. - Tests: SubsonicClient sends the gate header on ping via http_context (and misses the gated matcher without it); frontend probe routing + WebView fallback; result mapping unit tests. * docs(changelog): note gated-server connect fix (#1272) * fix(servers): keep add form open and surface the reason when connect fails The add-server flow closed the form the instant you pressed Add and, on failure, left only a small status dot — so a bad password, a rejected gate header, or an unreachable host all looked the same ("nothing happened"). - The connect probe now returns a short failure reason (the server's own error message, an HTTP status, or a transport error) via ServerProbeResult and the axios fallback; no secrets or header values are included. - ServersTab keeps the Add form open until the connect test succeeds and shows the reason in a toast on failure (edit flow surfaces it too). - AddServerForm validates the server address and username before probing, with clear per-field messages instead of a silent no-op. - New i18n keys (serverUrlRequired, serverUsernameRequired, serverConnectFailedReason) added across all shipped locales. * fix(servers): route all WebView Subsonic REST through native proxy for gated servers Once a header-gated server (Cloudflare Access, Pangolin, …) connected, every view that still fetched over axios in the WebView came up empty — Main stage, New Releases, Random Albums, Statistics, search, genres — because a non-safelisted gate header makes the WebView send a CORS preflight the gate rejects. Only the Rust-routed paths (streaming, covers, ping) worked. - Add a generic `subsonic_proxy_request` Tauri command backed by `SubsonicClient::send_raw`: it runs the request natively (no preflight), applies the per-server gate header via ServerHttpContext, and returns the untouched JSON body for the WebView to parse as it does an axios response. Supports GET and form-POST (OpenSubsonic formPost) with a clamped timeout. - `api`, `apiWithCredentials`, `apiPostFormWithCredentials` (and thus `apiForServer`/`apiPostFormForServer`) now switch to the native proxy exactly when a request would carry gate headers; header-less servers keep the lightweight WebView axios path unchanged. - Tests: wiremock coverage for `send_raw` (GET gate header + form-POST body); frontend routing tests that gated `api()` calls hit the proxy and header-less ones stay on axios; updated the OpenSubsonic extensions test for the new path. * fix(servers): apply gate header to media paths via URL fallback Streaming, prefetch and artist-info fetches returned 403 on a gated server: apply_for_http_url resolved custom headers strictly by server_ref and attached nothing when the caller's ref (e.g. the audio engine's playback server id) didn't match the registry key. Add a resolve_context helper that falls back to matching the request URL against the server's registered endpoints — the same fallback covers and Navidrome browse already relied on — and route the audio engine, apply_for_http_url and apply_optional_registry_headers through it. Endpoint matching only hits a configured gated server and still honours the apply-to LAN/public rule, so non-gated servers stay untouched. Also pool the native proxy's reqwest clients by timeout bucket so the extra gated-server browse traffic reuses keep-alive connections instead of opening a fresh pool per request (which surfaced as spurious timeouts / 499s on fast endpoints). * fix(servers): register gate headers before probe/bind so native paths work Root cause of the persistent 403s behind a header gate: the native ServerHttpRegistry was populated only at the end of bindIndexedServer, after ensureConnectUrlResolved + the bind session. When that probe was slow, hung, or reported the server briefly offline, the sync never ran, leaving the registry empty — so every Rust-initiated request (stream, cover, prefetch, fanart getArtistInfo2, Navidrome /auth/login) resolved no header and the gate returned 403. The inline-context proxy path kept working, which is why browse succeeded while media/covers failed. Register the header context up front: before the reachability probe in bindIndexedServer, and authoritatively for all servers at the start of bootstrapAllIndexedServers (once React has mounted and IPC is ready). Add concise sync/sync_all diagnostics (endpoint URLs + header count, never values) so gated-server registration is observable in logs. * fix(servers): retry gated-server covers that 403'd during header-registry gap Covers fetched while the native gate-header registry was momentarily empty (e.g. a dev restart before the header sync landed) got a 403, which the cover fetcher classified as a permanent "cover missing" and wrote a 30-minute `.fetch-failed` marker for — so on a gated server most artwork stayed blank long after the gate started answering 200. - Treat a gate-style 401/403 (plus 408/425/429) on cover art as a transient, retryable hiccup rather than a permanent miss, so the short retry loop can ride out a brief registry gap. Genuine 404/410/400 stay permanent. - On (re)bind of a gated server, once its header is registered, clear that server's stale `.fetch-failed` markers and kick a backfill pass so the covers re-download — same rationale as the URL-change retry. * refactor(servers): funnel gate-header application through one helper Collapse the remaining bespoke header-application variants onto the single `apply_optional_registry_headers` / `resolve_context` entry point so gated-server header logic lives in exactly one place: - cover_cache fetch and Navidrome `/auth/login` used older `apply_for_http_url` / `get_for_server_url` + `apply_server_headers_for_http_url` combos; both now call the shared helper. - The audio engine's `apply_playback_request_headers` delegates to it instead of re-implementing the resolve+apply. - Drop the now-unused `ServerHttpRegistry::apply_for_http_url` / `apply_for_base_url` methods. Behaviour is unchanged: a non-gated server (no registry match) leaves every request untouched, exactly as before. This is purely removing duplicate ways to do the same thing so new native paths have one obvious hook. * chore(servers): drop server-http registry debug logging Remove the `[server-http] sync/sync_all` eprintln diagnostics added while tracing the header-registry timing bug. They printed to stderr on every sync (startup + each persist-rehydrate) and echoed endpoint URLs; the behaviour is verified and covered by tests now, so the noise is no longer warranted. The dev-gated `[connect-probe]`/`[subsonic-proxy]` failure logs stay. * docs(changelog): point gated-server entry at PR #1273 * fix(servers): await gate-header sync before probe/bind bindIndexedServer registered the per-server gate headers with a fire-and-forget `void syncServerHttpContextForProfile(...)`, so a direct bind (add / enable a single server) could run the native reachability probe and bind session while the header IPC was still in flight — the registry was empty and native paths 403'd behind the gate, the exact race this branch fixes. Await the sync before probing/binding; the stale-cover retry stays off the critical path. * fix(servers): reclaim LAN from a sticky public endpoint For a dual-address profile, the first endpoint that answered after launch stuck for the whole session: a public sticky endpoint was always tried first and kept answering, so the LAN-first order was never re-run and the connection never upgraded back to LAN. pickReachableBaseUrl now makes a short, no-retry, bounded quick-probe of the higher-priority LAN endpoint before honouring a public sticky entry, so a laptop returning to the LAN upgrades on the next reachability tick while staying remote costs only one bounded probe (never the full retry cushion). * fix(servers): refresh LAN/public badge on active-server switch; credit #1273 The connection badge read the endpoint kind from the last probe and never re-probed when the active server changed, so switching between a LAN-only and a public server left the badge stuck on the old server's classification until the 120-s tick. useConnectionStatus now resets the kind and re-probes when activeServerId changes (mount still deferred to the polling effect). Also adds the settings credits line for the gated-server work (PR #1273). * fix(servers): LAN reclaim uses the probe's own timeout, not a 3s race The reclaim quick-probe raced pingWithCredentialsForProfile against a fixed 3s timer, but the underlying probe (native reqwest for gated servers, axios otherwise) runs on a 15s timeout — so a LAN endpoint answering between 3s and 15s was wrongly treated as unreachable and the session stayed pinned to the public sticky endpoint. Reclaim now does a single, no-retry probe on the LAN endpoint using the normal ping timeout: a slow-but-reachable LAN still upgrades, while a dead one still costs only one attempt (not the full retry cushion) before falling through to the sticky sequence.
A modern desktop client for self-hosted music libraries
Fast. Native. Beautiful. Built for people who actually care about their music collection.
Psysonic is built primarily for Navidrome and also works with Gonic, Airsonic, LMS and other Subsonic-compatible servers, depending on the features supported by your server.
Available languages: English, German, Spanish, French, Norwegian Bokmål, Dutch, Romanian, Russian, Chinese, Japanese, Hungarian and Polish.
More translations are added over time.
No telemetry • Native performance • Navidrome-first • Community driven
What is Psysonic?
Psysonic is a desktop music client for self-hosted music libraries. It is designed for people who want the freedom of their own server without giving up the comfort, polish and speed of a modern music app.
It is built with Rust, Tauri v2 and React, with a strong focus on responsiveness, customization, practical music-library workflows and a user interface that does not require a manual before you can press play.
Psysonic is optimized first and foremost for Navidrome, and it leans into that on purpose: instead of being one more generic Subsonic client, it is the Navidrome-first desktop client that does things no other client does. Other Subsonic-compatible servers can work well too, but advanced features may depend on server-side support.
⭐ Key Features
These are the things that set Psysonic apart. To our knowledge, no comparable self-hosted desktop client ships them.
🪐 Orbit — Shared Listening
Listen together, in sync, over your own server.
Orbit brings real-time synchronized group listening into Psysonic. Start a session, invite people with a link, and everyone hears the same thing at the same time — with host-controlled playback, a shared queue and guest song suggestions.
The clever part: Orbit rides entirely on your own Navidrome. There is no external relay, no third-party service and no extra accounts. The session lives on your server, where it belongs. It is built for real-world music sharing without turning your self-hosted setup into a social-media circus.
⚡ Local Library — Instant, and Almost Offline
A local index of your whole collection, so the app stays fast no matter what your connection does.
Psysonic keeps a local library of your collection's metadata right on your machine. Because the app already knows your tracks inside out, browsing, searching and starting playback are instant — even a 500 MB FLAC starts the moment you hit play, because nothing has to be fetched or parsed first.
It also means the connection to your server stops being a bottleneck. Even on a slow, flaky or distant link, Psysonic stays responsive and behaves almost like an offline player, whatever the network is doing.
And it's the foundation everything else is built on: the local library is what makes on-device analysis, smart audio and snappy navigation possible in the first place.
🧠 On-Device Audio Analysis
One of the most powerful things Psysonic does — entirely on your own machine.
Built on top of the local library, Psysonic analyzes your tracks locally — loudness, waveform and tempo — with no cloud service and no required server-side plugin. That analysis is what powers content-aware AutoDJ transitions, LUFS-based loudness normalization and playback-speed control.
This is a deep, genuinely useful layer that most clients simply don't have, and because it runs locally, it works exactly the same whether you're fully online or barely connected.
🎧 AutoDJ — Content-Aware Crossfade
A DJ that listens to the music, not a stopwatch.
Most players do fixed-time crossfade: blend the last N seconds into the next N seconds, dead air and all. AutoDJ uses Psysonic's own audio analysis to trim the silence at the edges of a track and blend out of the actual music — for transitions that sound deliberate instead of mechanical. It is a standalone playback mode with smooth skip/interrupt handling and a configurable overlap.
🔗 Navidrome-Native, Deeply
Not a generic Subsonic client wearing a Navidrome hat.
Psysonic binds Navidrome's native capabilities directly: server-side smart-playlist create/edit, playback reporting and OpenSubsonic capability probing. Most clients in this space stay Subsonic-generic. Psysonic goes deeper, so Navidrome users get the features their server can actually deliver.
🎨 Community Theme Store
A real marketplace for themes — installable and schedulable.
Beyond a big set of built-in themes, Psysonic has a first-party theme registry: browse community themes, install them in-app, and let the Theme Scheduler switch looks automatically between day and night.
Built to be trusted
We take an enterprise-grade approach to development — continuously improving our automated testing and maintaining strict contracts between the backend and the frontend. Releases are cut from green CI, not vibes.
✨ More Highlights
Features that go well beyond the basics. Not all of these are unique to Psysonic, but few clients bring this many together.
Audio & Loudness
- LUFS-based Smart Loudness Normalization
- ReplayGain support and loudness-aware playback
- 10-band Equalizer with presets
- AutoEQ headphone correction
- Per-device EQ and output optimization
- Adjustable playback speed
Lyrics & Listening
- Synced lyrics with seek support, from multiple providers (YouLy+, LRCLIB, NetEase)
- Auto-scrolling sidebar lyrics and a fullscreen lyric mode
- Last.fm scrobbling, similar artists, loved tracks and listening stats
- Smart Radio sessions and an Infinite Queue
- AudioMuse-AI support for sonic-similarity discovery (requires an AudioMuse-AI server)
Artwork & Visuals
- Optional external artist imagery via fanart.tv — opt-in, shown on the artist page, fullscreen player and home hero (Navidrome stays the canonical cover-art source)
- Cover art surfaced across the app, OS media controls and Discord Rich Presence
Library & Playlists
- Smart Playlists
- Drag & drop playlist management
- Multi-select bulk actions
Sharing
- Magic Strings sharing for albums, artists and queues
- Navidrome user-management helpers for fast account sharing
Offline, Sync & Deployment
- Offline playback and downloads
- USB / portable sync
- LAN / remote auto-switching
- Custom HTTP headers for reverse-proxy-gated servers (e.g. Cloudflare Access, Pangolin)
- Backup and restore settings
- In-app auto updater
Personalization & Accessibility
- Font customization and zoom controls
- Keybind remapping
- Colorblind-friendly theme options
- Keyboard-friendly navigation
Power-User Extras
- CLI controls
✅ The Basics, Done Right
The things you simply expect from a serious music player — and Psysonic does them well.
- Gapless playback and crossfade
- Fast search across large libraries
- Browse albums, artists, tracks and genres
- Ratings
- Queue management
- Keyboard navigation
- Media key support
- Low memory usage and native performance compared to heavy web-first clients
- Built for large self-hosted collections
Platforms
| OS | Support |
|---|---|
| Windows | Native installer / WinGet |
| macOS | Signed DMG |
| Linux | AppImage / DEB / RPM / AUR (psysonic, psysonic-bin) / NixOS |
Install
Linux
curl -fsSL https://raw.githubusercontent.com/Psychotoxical/psysonic/main/scripts/install.sh | sudo bash
Linux builds are also available through GitHub Releases, AUR and Cachix/Nix.
AppImage runs under X11/XWayland — it pins
GDK_BACKEND=x11for a stable WebKitGTK stack. For a native-Wayland launch, use the.deb,.rpm, AUR, or Nix packages, which follow your session's display server.
Windows
Download the latest installer from the GitHub Releases.
or,
install via Windows Package Manager (WinGet):
winget install Psysonic
You can also browse and install it on winstall.app.
macOS
Download the signed DMG from the GitHub Releases.
Development
Contributor expectations (PRs, CI, Tauri boundary, UI): CONTRIBUTING.md.
git clone https://github.com/Psychotoxical/psysonic.git
cd psysonic
npm install
npm run tauri:dev
Build release:
npm run tauri:build
Privacy
Psysonic is built for self-hosted music collections. Your library is yours.
- No telemetry
- No spyware nonsense
- No analytics harvesting
- No hidden tracking
See TELEMETRY.md for the telemetry stance and PRIVACY.md for how each opt-in integration handles data.
Reviews
Community & Support
Join the community, report bugs, suggest features, share themes and help shape the future of Psysonic.
License
Psysonic is licensed under the GNU GPL v3.0.
Forks and Attribution
Psysonic is free and open-source software under the GPLv3. You are welcome to fork it, modify it and build upon it under the terms of the license.
If you publish a modified or rebranded version, please make it clear that your project is based on Psysonic and preserve proper attribution to the original project.
That is not about preventing forks. Forks are part of open source. It is about being honest with users and contributors about where the work comes from.
Features, design work and implementation ideas developed in Psysonic should not be presented as unrelated original work in downstream projects.
Own your music. Enjoy the client too.
Psysonic brings a modern desktop experience to self-hosted music libraries.

