A modern, gorgeous, and blazing fast desktop client for Subsonic API compatible music servers (Navidrome, Gonic, etc.).
-
-
-
-
-
-
-
-
-
-
-
-
-> [!WARNING]
-> **Psysonic is under heavy active development.** Bugs and rough edges are to be expected. We reserve the right to change, remove, or rework existing features at any time without prior notice.
-
----
+# Psysonic
Have questions, ideas, or just want to hang out? Come chat in our Discord server!
+
+
+## The Ultimate Desktop Client for Self-Hosted Music Libraries
+
+**Fast. Beautiful. Native. Feature-packed.**
+Built primarily for **Navidrome**. Also compatible with **Gonic**, **Airsonic**, **LMS** and other Subsonic-compatible servers with partial feature support.
+
+
+
+
+
+
+
+**No telemetry β’ Native performance β’ Massive feature set β’ Community driven**
+
---
-Psysonic is a beautiful desktop music player built completely from the ground up for the modern era. Utilizing **Tauri v2** and **React**, it offers a native-feeling, lightweight, and incredibly fast experience with a stunning UI inspired by the [Catppuccin](https://github.com/catppuccin/catppuccin) and [Nord](https://www.nordtheme.com/) aesthetics.
-
-Designed specifically for users hosting their own music via Navidrome or other Subsonic API servers, Psysonic aims to be the best way to interact with your personal library.
-
-

-## β¨ Features
+---
-- π¨ **Wide Theme Selection**: Dozens of themes across 8 groups β Open Source Classics (Catppuccin, Nord, Gruvbox, Nightfox, Dracula), Operating Systems, Games, Movies, Series, Social Media, and Psysonic originals. Glassmorphism effects, micro-animations, and a time-based **Theme Scheduler** for automatic day/night switching.
-- β‘ **Native Performance**: Built with Rust & Tauri β native audio engine (rodio), minimal RAM usage, no Electron overhead.
-- π΅ **Last.fm Integration**: Scrobbling, Now Playing, love/unlove, Similar Artists, and top stats β no Navidrome config needed.
-- π€ **Synchronized Lyrics**: Auto-scrolling synced lyrics with click-to-seek in the sidebar and fullscreen player, powered by LRCLIB and your Navidrome server.
-- π» **Radio & Infinite Queue**: Smart Radio sessions from any song or artist, built-in Internet Radio (ICY/HLS), and an Infinite Queue that silently refills when the queue runs out.
-- ποΈ **Advanced Audio**: 10-band graphic EQ with custom presets, **AutoEQ** headphone correction, Replay Gain, gapless playback, and crossfade.
-- γ°οΈ **10 Seekbar Styles**: Waveform, Bar, Thick Bar, Segmented, Line+Dot, Neon, Pulse Wave, Particle Trail, Liquid Fill, and Retro Tape.
-- π₯οΈ **Fullscreen Player**: Album art, animated synced lyrics overlay, and artist image in a dedicated fullscreen view.
-- π **Playlists & Library**: Full playlist management with drag-and-drop reorder and smart suggestions. Genre browsing, Random Mix, Advanced Search, ratings (1β5 stars), and multi-select actions.
-- πΎ **Device Sync**: Export your library to a USB drive or portable device using a configurable filename template.
-- π₯οΈ **CLI Control**: Control playback, switch servers, manage the queue, and more directly from the command line.
-- β¨οΈ **Customization**: Configurable keybindings, UI fonts, global zoom slider, system tray, backup & restore, and in-app auto-update.
-- π **8 Languages**: English, German, French, Dutch, Spanish, Chinese, Norwegian, Russian.
-- π₯οΈ **Cross-Platform**: Windows, macOS, and Linux (Arch AUR, .deb, .rpm, NixOS flake).
-- βοΈ **NixOS / flakes**: First-class flake package with a public **Cachix** binary cache (`psysonic.cachix.org`) β `nix run github:Psychotoxical/psysonic` or add to your system config. See the [NixOS install guide](./nixos-install.md).
+> [!WARNING]
+> Psysonic is under heavy active development. Bugs and rough edges are to be expected. We reserve the right to change, remove, or rework existing features at any time without prior notice.
-## πΊοΈ Roadmap
+## Server Compatibility
-### π Planned
-- [ ] Theme contrast & legibility audit β systematic review of text/background contrast ratios across all themes
-- [ ] Accessibility (a11y) β keyboard navigation, screen reader support, ARIA labels
-- [ ] More languages
+**Psysonic is optimized first and foremost for Navidrome.**
+
+Many advanced functions integrate directly with Navidrome APIs for the best possible experience. Other Subsonic-compatible servers generally work well, but some features may be limited depending on server capabilities.
+
+## Why Psysonic?
+
+Most Subsonic clients feel like web wrappers.
+
+**Psysonic does not.**
+
+It is a true desktop experience built with **Rust**, **Tauri v2**, and **React** for users who care about speed, aesthetics, customization, and serious music library management.
+
+If you host your own music, this is what the premium experience should feel like.
---
-## π₯ Installation
+# Core Features
-Navigate to the [Releases](https://github.com/Psychotoxical/psysonic/releases) page and download the installer for your operating system.
+## Playback Engine
-### π§ Linux
+* Gapless playback
+* Crossfade
+* ReplayGain support
+* Smart Loudness Normalization
+* Infinite Queue
+* Smart Radio sessions
+* High responsiveness with low memory usage
+
+## Audio Tools
+
+* 10-band Equalizer
+* Presets
+* AutoEQ headphone correction
+* Per-device optimization
+
+## Library Power
+
+* Lightning-fast search
+* Albums / Artists / Tracks / Genres
+* Ratings system
+* Multi-select bulk actions
+* Drag & drop playlist management
+* Huge library friendly
+
+## Lyrics & Discovery
+
+* Synced lyrics with seek support
+* Auto-scroll sidebar lyrics
+* Fullscreen lyric mode
+* Last.fm scrobbling
+* Similar artists / love tracks / stats
+
+## Personalization
+
+* Huge theme collection
+* Catppuccin / Nord inspired styles
+* Glassmorphism effects
+* Font customization
+* Zoom controls
+* Keybind remapping
+* Theme Scheduler (day/night auto switch)
+
+## Power User Extras
+
+* CLI controls
+* USB / portable sync
+* Backup & restore settings
+* In-app auto updater
+* LAN / remote auto switching
+
+---
+
+# Orbit (Upcoming)
+
+## Listen Together. In Sync. Soon.
+
+Currently in final development and testing. Orbit will introduce synchronized shared listening sessions directly inside Psysonic.
+
+* Host-controlled playback
+* Join via link
+* Shared listening sessions
+* Guest song suggestions
+* Real-time queue interaction
+
+**Rolling out in an upcoming release. Community feedback will help shape the final experience.**
+
+---
+
+# Platforms
+
+| OS | Support |
+| ------- | ---------------------------------------- |
+| Windows | Native Installer *(certificate pending)* |
+| macOS | Signed DMG |
+| Linux | AppImage / DEB / RPM / AUR / NixOS |
+
+Supports **8 languages** and growing.
+
+---
+
+# Install
+
+## Linux
-**Quick Install (Recommended):**
```bash
curl -fsSL https://raw.githubusercontent.com/Psychotoxical/psysonic/main/scripts/install.sh | sudo bash
```
-**Manual Installation:**
-- **Ubuntu / Debian**: `.deb` from GitHub Releases
-- **Fedora / RHEL**: `.rpm` from GitHub Releases
-- **Any distro (portable)**: `.AppImage` from GitHub Releases β `chmod +x` and run, no install required
+## Windows
-**Arch Linux (AUR):**
+Download the latest installer from Releases.
-| Package | Type | Description |
-| :--- | :--- | :--- |
-| [**psysonic**](https://aur.archlinux.org/packages/psysonic) | **Source** | Builds from source using your system's native **WebKitGTK** (no bundled libs, no EGL/Mesa compatibility issues). |
-| [**psysonic-bin**](https://aur.archlinux.org/packages/psysonic-bin) | **Binary** | Pre-compiled version for faster installation. |
+> SmartScreen warnings may appear until the code-signing certificate is active.
-> [!TIP]
-> The AUR binary package is kindly provided and maintained by [**kilyabin**](https://github.com/kilyabin).
+## macOS
-**βοΈ NixOS (flakes):**
-- `nix run github:Psychotoxical/psysonic` β one-shot launch
-- Full guide: [`nixos-install.md`](./nixos-install.md) *(contributed by [@cucadmuh](https://github.com/cucadmuh), PR [#209](https://github.com/Psychotoxical/psysonic/pull/209))*
+Download the signed DMG from Releases.
-### π macOS
+---
-- **macOS**: `.dmg` (Universal or Apple Silicon) β **signed with an Apple Developer ID and notarized by Apple**. Gatekeeper opens it with a single click, no `xattr` workaround required.
-
-> [!NOTE]
-> Since **v1.40.0**, macOS builds include an in-app auto-updater: click **Install now** in the update notification and the signed `.app.tar.gz` is fetched, verified against the bundled minisign public key, replaced in place, and the app relaunches β all in one step.
-
-### πͺ Windows
-
-- **Windows**: `.exe` (NSIS installer)
-
-> [!WARNING]
-> **SmartScreen Note:**
-> Windows SmartScreen might show a warning because the installer isn't signed with an expensive developer certificate. Click on **"More info"** and then **"Run anyway"**.
-
-## π Getting Started
-
-1. Download and install Psysonic.
-2. Open the app and enter your Subsonic/Navidrome server details (URL, Username, Password).
-3. If applicable, you can provide both an external URL and a local LAN IP. Psysonic allows you to quickly toggle between them in the Settings.
-4. Enjoy your music!
-
-## π οΈ Development
-
-If you want to build Psysonic from source or contribute to the project:
-
-### Prerequisites
-- [Node.js](https://nodejs.org/) (v18+)
-- [Rust](https://www.rust-lang.org/) (v1.75+)
-- **`cmake`** β required to compile the bundled libopus (Opus audio support). Install it before running `cargo build` or `npm run tauri:build`:
- - Linux: `sudo apt install cmake` / `sudo pacman -S cmake`
- - macOS: `brew install cmake`
- - Windows: [cmake.org/download](https://cmake.org/download/) or `winget install cmake`
-- OS-specific build dependencies for Tauri (see the [Tauri prerequisites guide](https://tauri.app/v2/guides/getting-started/prerequisites)).
-
-### Setup
+# Development
```bash
-# Clone the repository
git clone https://github.com/Psychotoxical/psysonic.git
cd psysonic
-
-# Install node dependencies
npm install
-
-# Run in development mode
npm run tauri:dev
+```
-# Build for production
+Build release:
+
+```bash
npm run tauri:build
```
-## π€ Contributing
+---
-Contributions are completely welcome! Whether it is translating the app into a new language, fixing a bug, or proposing a new feature.
+# Privacy First
-1. Fork the Project
-2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
-3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
-4. Push to the Branch (`git push origin feature/AmazingFeature`)
-5. Open a Pull Request
+* No telemetry
+* No spyware nonsense
+* No analytics harvesting
+* Your library stays yours
-## π License
+---
-Distributed under the **GNU General Public License v3.0**. See `LICENSE` for more information.
+# Community
-This means: you are free to use, study, and modify Psysonic. If you distribute a modified version, you must release it under the same GPL v3 license and keep the original copyright notice intact. You may **not** incorporate this code into proprietary software.
+Join Discord, report bugs, suggest features, share themes, shape the future.
-## π Privacy
+---
-Psysonic contains no telemetry or analytics. All third-party integrations (Last.fm, LRCLIB, Discord) are opt-in. See [PRIVACY.md](PRIVACY.md) for full details.
+# License
+
+GNU GPL v3.0
+
+---
+
+
+
+## Stop using boring music clients.
+
+## Use Psysonic.
+
+
From 9048e9426e5dbb55c0dc2ef5473144a7f1e345ce Mon Sep 17 00:00:00 2001
From: Frank Stellmacher <171614930+Psychotoxical@users.noreply.github.com>
Date: Sat, 25 Apr 2026 20:37:53 +0200
Subject: [PATCH 2/7] Update README.md
---
README.md | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/README.md b/README.md
index d8cb2652..ef6a155f 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@ Built primarily for **Navidrome**. Also compatible with **Gonic**, **Airsonic**,
-
+
@@ -119,11 +119,11 @@ Currently in final development and testing. Orbit will introduce synchronized sh
# Platforms
-| OS | Support |
-| ------- | ---------------------------------------- |
-| Windows | Native Installer *(certificate pending)* |
-| macOS | Signed DMG |
-| Linux | AppImage / DEB / RPM / AUR / NixOS |
+| OS | Support |
+| ------- | --------------------------------------------------------------- |
+| Windows | Native Installer *(certificate pending)* |
+| macOS | Signed DMG |
+| Linux | AppImage / DEB / RPM / AUR (`psysonic`, `psysonic-bin`) / NixOS |
Supports **8 languages** and growing.
From 2fae1b4c0ea96bf005d23d567b688260764ba671 Mon Sep 17 00:00:00 2001
From: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com>
Date: Sat, 25 Apr 2026 22:17:16 +0200
Subject: [PATCH 3/7] chore(tauri): don't auto-open devtools on launch
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Removes the #[cfg(debug_assertions)] open_devtools() block from setup().
DevTools remain available in dev builds via the standard WebKit shortcut
(Ctrl+Shift+I) and right-click β Inspect, but no longer pop up automatically
on every dev launch. Less visual clutter when iterating without needing the
inspector.
Production behaviour is unchanged β devtools stay hard-stripped from
release builds via Tauri's default `windows[].devtools=false` for release.
Co-Authored-By: Claude Opus 4.7 (1M context)
---
src-tauri/src/lib.rs | 8 --------
1 file changed, 8 deletions(-)
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
index 1f586d83..a6cc1202 100644
--- a/src-tauri/src/lib.rs
+++ b/src-tauri/src/lib.rs
@@ -3621,14 +3621,6 @@ pub fn run() {
}
}
- #[cfg(debug_assertions)]
- {
- use tauri::Manager;
- if let Some(win) = app.get_webview_window("main") {
- win.open_devtools();
- }
- }
-
// ββ System tray βββββββββββββββββββββββββββββββββββββββββββββββ
// Always build on startup when possible; the frontend calls toggle_tray_icon(false)
// immediately after load if the user has disabled the tray icon.
From d31291a463586072dbcb501ab74ba9324f99aa13 Mon Sep 17 00:00:00 2001
From: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com>
Date: Sat, 25 Apr 2026 23:57:44 +0200
Subject: [PATCH 4/7] perf(genres): replace icon cards with tag-cloud pills
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The previous genre grid mounted ~60 Lucide SVG cards per page (with
Watermark icon, gradient bg, infinite scroll) and froze the WebKitGTK
renderer for several seconds on libraries with many genres.
The new layout flows all genres as compact pills with log-scaled font
size based on albumCount β one -equivalent button per genre, no
SVGs, no pagination needed. Pill colour is dimly tinted by the same
deterministic hash-to-CTP palette used before; text picks up the genre
colour on hover only, so the page reads calmly at rest.
Co-Authored-By: Claude Opus 4.7 (1M context)
---
src/pages/Genres.tsx | 130 ++++++++++----------------------------
src/styles/components.css | 72 +++++++++------------
2 files changed, 64 insertions(+), 138 deletions(-)
diff --git a/src/pages/Genres.tsx b/src/pages/Genres.tsx
index 66b44e72..99f1656c 100644
--- a/src/pages/Genres.tsx
+++ b/src/pages/Genres.tsx
@@ -1,36 +1,10 @@
-import React, { useEffect, useMemo, useRef, useState } from 'react';
+import { useEffect, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
-import {
- Headphones, Zap, Music2, Music, Cpu, Mic, Radio, Cloud,
- Leaf, Heart, Sun, Flame, Film, Globe, BookOpen, Podcast, Star,
- Tags, type LucideIcon,
-} from 'lucide-react';
+import { Tags } from 'lucide-react';
import { getGenres, SubsonicGenre } from '../api/subsonic';
import { APP_MAIN_SCROLL_VIEWPORT_ID } from '../constants/appScroll';
-function getGenreIcon(name: string): LucideIcon {
- const n = name.toLowerCase();
- if (/ambient|drone|new age/.test(n)) return Cloud;
- if (/metal|hardcore|thrash|death|grind|doom/.test(n)) return Zap;
- if (/rock/.test(n)) return Radio;
- if (/jazz/.test(n)) return Music2;
- if (/classical|orchestra|chamber|baroque|opera|symphon/.test(n)) return Music;
- if (/electronic|techno|edm|house|trance|electro|synth/.test(n)) return Cpu;
- if (/hip.?hop|rap/.test(n)) return Mic;
- if (/pop/.test(n)) return Star;
- if (/folk|country|bluegrass|americana/.test(n)) return Leaf;
- if (/blues/.test(n)) return Music2;
- if (/soul|r.?b|funk|gospel/.test(n)) return Heart;
- if (/reggae|ska|dub/.test(n)) return Sun;
- if (/punk/.test(n)) return Flame;
- if (/soundtrack|score|ost|film|movie|cinema/.test(n)) return Film;
- if (/world|latin|afro|celtic|tribal|traditional/.test(n)) return Globe;
- if (/audiobook|spoken|hΓΆrbuch|speech|comedy/.test(n)) return BookOpen;
- if (/podcast/.test(n)) return Podcast;
- return Headphones;
-}
-
const CTP_COLORS = [
'var(--ctp-rosewater)', 'var(--ctp-flamingo)', 'var(--ctp-pink)', 'var(--ctp-mauve)',
'var(--ctp-red)', 'var(--ctp-maroon)', 'var(--ctp-peach)', 'var(--ctp-yellow)',
@@ -45,63 +19,39 @@ function genreColor(name: string): string {
}
const SCROLL_KEY = 'genres-scroll';
-const VISIBLE_KEY = 'genres-visible';
-const PAGE_SIZE = 60;
+const FONT_MIN_REM = 0.78;
+const FONT_MAX_REM = 1.7;
export default function Genres() {
const { t } = useTranslation();
const navigate = useNavigate();
const [rawGenres, setRawGenres] = useState([]);
const [loading, setLoading] = useState(true);
- const [visibleCount, setVisibleCount] = useState(() => {
- // Restore the previous visibleCount when navigating back from a detail
- // page so scroll position lines up with rendered cards.
- const saved = sessionStorage.getItem(VISIBLE_KEY);
- return saved ? Math.max(PAGE_SIZE, parseInt(saved, 10)) : PAGE_SIZE;
- });
- const observerTarget = useRef(null);
useEffect(() => {
getGenres()
.then(data => setRawGenres(data))
.finally(() => setLoading(false));
- }, []); // getGenres is not folder-scoped β no dep on musicLibraryFilterVersion
+ }, []);
- // Memoised sort β without this the page re-sorted 500+ entries on every
- // unrelated re-render (e.g. theme change, sidebar toggle).
const genres = useMemo(
() => [...rawGenres].sort((a, b) => b.albumCount - a.albumCount),
[rawGenres],
);
- const visible = useMemo(() => genres.slice(0, visibleCount), [genres, visibleCount]);
- const hasMore = visibleCount < genres.length;
+ // Log-scale font sizing β flattens the long tail (a 1000-album genre and a
+ // 50-album genre look distinct, but a 1-album genre still has a readable size).
+ const maxLog = useMemo(() => {
+ if (genres.length === 0) return 1;
+ return Math.log(Math.max(2, genres[0].albumCount));
+ }, [genres]);
- // Infinite scroll β render the next batch when the user is ~1.5 screens
- // away from the sentinel, so the rest of the watermarks never block first
- // paint of the page.
- useEffect(() => {
- if (!hasMore) return;
- const root = document.getElementById(APP_MAIN_SCROLL_VIEWPORT_ID);
- const observer = new IntersectionObserver(
- entries => { if (entries[0].isIntersecting) setVisibleCount(c => c + PAGE_SIZE); },
- {
- root: root instanceof HTMLElement ? root : null,
- rootMargin: '1500px',
- },
- );
- if (observerTarget.current) observer.observe(observerTarget.current);
- return () => observer.disconnect();
- }, [hasMore]);
-
- // Restore scroll position after genres are rendered
useEffect(() => {
if (loading || genres.length === 0) return;
const saved = sessionStorage.getItem(SCROLL_KEY);
if (!saved) return;
const pos = parseInt(saved, 10);
sessionStorage.removeItem(SCROLL_KEY);
- sessionStorage.removeItem(VISIBLE_KEY);
requestAnimationFrame(() => {
const el = document.getElementById(APP_MAIN_SCROLL_VIEWPORT_ID);
if (el) el.scrollTop = pos;
@@ -110,10 +60,7 @@ export default function Genres() {
const handleGenreClick = (genreValue: string) => {
const el = document.getElementById(APP_MAIN_SCROLL_VIEWPORT_ID);
- if (el) {
- sessionStorage.setItem(SCROLL_KEY, String(el.scrollTop));
- sessionStorage.setItem(VISIBLE_KEY, String(visibleCount));
- }
+ if (el) sessionStorage.setItem(SCROLL_KEY, String(el.scrollTop));
navigate(`/genres/${encodeURIComponent(genreValue)}`);
};
@@ -132,36 +79,29 @@ export default function Genres() {
{loading &&