
Noir Player Desktop (Rust, GPUI Kit)
Desktop rebuild of NoirPlayer in Rust with GPUI Kit: multi-folder library scanning, a 5-band graphic equalizer, a dockable queue, and the same red-on-black identity as the Android app, packaged for Windows, macOS, and Linux.
Problem
NoirPlayer started as an Android app, and Android habits don't map cleanly onto a desktop: no navigation rail, no dockable queue, no keyboard shortcuts, no reason to hide an equalizer behind a single "presets" button when there's room for five full sliders on screen. Rather than wrap the Flutter app in a webview shell, Noir Player Desktop is a from-scratch Rust rebuild on GPUI Kit, built for a mouse and keyboard from the layout up while keeping the mobile app's red-on-black identity intact.
Architecture
The UI never touches audio decoding or file metadata directly. Playback commands go to Rodio, which owns decoding (including the ALAC and AIFF feature flags) and drives the 5-band equalizer. Library state comes from Lofty, which scans the system Music folder plus any user-added folders recursively, reads embedded tags and artwork, and hands back a deduplicated, grouped track list. The two paths don't cross: swapping the playback backend or the tag reader would touch one module each, not the UI.
Tech decisions & trade-offs
Rust and GPUI Kit instead of a Flutter desktop target or a webview shell
Flutter ships a desktop target and Tauri would have let the existing web-adjacent skills carry over, but neither gives a genuinely native window on all three platforms without a runtime in between. GPUI Kit is Zed's UI framework, exposed as a Rust crate: no embedded browser, no VM, a real navigation rail and dockable queue instead of a mobile layout stretched wide. The cost is platform-specific build requirements the mobile app never had — Xcode and Metal tooling on macOS, Vulkan and Wayland/X11 libraries on Linux — each pinned to a documented recipe in CI rather than discovered by trial and error.
Discover downloads and validates before it plays, and never streams
The desktop Discover tab reuses the mobile app's three-hop idea (Last.fm for search and charts, the YouTube Data API to find a matching video, a RapidAPI endpoint to resolve audio) but changes the delivery model: it downloads the complete file into a local cache, validates it (rejecting anything over three hours or that fails to decode within a deadline), and only then plays it — never streams partial audio. That trade lands closer to the local-first library than to a jukebox: cached audio behaves exactly like a scanned file, at the cost of a wait before the first note plays. It's opt-in by construction — Discover reads its three API keys from a gitignored .env at runtime, and with none set the tab simply reports what's missing instead of doing nothing silently.
File-path playlists over copying audio
Playlists, favourites, and history store the local path to a track, not the audio itself — the same trade-off the mobile app made. A moved file breaks its reference until the next rescan; the win is that a multi-gigabyte library costs nothing extra to organize into playlists, and nothing is duplicated on disk. The queue, the current track, and playback position are treated differently: they're intentionally not persisted, so every launch starts from a clean transport rather than resuming a stale queue from a previous session.
Packaging by platform, not by shortcut
Each platform gets its native installer format rather than one generic bundle: WiX MSI and NSIS on Windows (added starting at v1.2.0), a .dmg on macOS built and validated on an actual Apple Silicon runner, and .deb/.rpm/.pkg.tar.zst on Linux with dependencies generated from the real ELF binary rather than copied from a template. The Windows installer path pins tauri-bundler as a packaging-only dependency — it builds the MSI and NSIS installers without pulling any Tauri or WebView runtime into the shipped application. CI runs cargo fmt, clippy -D warnings, and the full test suite on all three targets before a release tag is allowed to build, and every asset ships with a SHA-256 sidecar.
