As a page
Earhorn Music — GNU/Linux
A UPnP/DLNA music player for GNU/Linux desktops. Point it at the server already running on your NAS, browse and queue with mouse and keyboard, and play through your computer's own speakers via libvlc — or send the queue to a Chromecast or a UPnP renderer. Same library you stream to on the TV, same MinimServer-aware browse tree, no cloud account, no telemetry.
Website · Download · Browse the code · All releases · Tip jar
---
Why Earhorn on the desktop?
Earhorn already exists for Android TV, phone, and Android Auto in the android/ folder next to this one. This is the same product on the surface where you already keep your headphones plugged in — a laptop running Debian, an old workstation wired into a stereo, a homelab box doing double duty. Same UPnP discovery, same library, same classical-listener metadata defaults, rendered in a Compose Multiplatform window.
- Plays through your computer. libvlc via VLCJ — FLAC, MP3, WAV, AAC, ALAC, DSD, Opus, anything libvlc handles, which is essentially everything.
- No cloud, no account. SSDP discovery on your local network. Your server, your speakers, your files. The only routine internet traffic is the lyrics lookup, and one switch turns it off (see Privacy).
- Same browse tree as the Android build. MinimServer's
indexTagsstructure renders faithfully; synthetic entries (>> Show All,[folder view],[untagged]) get distinct sage-green italic typography. - Classical-listener defaults. 28 configurable metadata fields. Defaults surface Composer, Conductor, Work, Performer, Ensemble, sample rate, bit depth. Toggle what you care about.
- Drag-to-reorder queue. Real-time visual reorder; the renderer's playback never restarts when the currently-playing track stays put.
- Lyrics, album art, album notes. Time-synced lyrics from lrclib.net with server-side
.lrcfallback. Coil-loaded album art.AlbumInfoLoaderreads ID3 / Vorbis comments and sibling.txtnotes. - Cast and UPnP renderers. Cast the queue to a Chromecast, Chromecast Audio or speaker group, drive a UPnP renderer on your network, or follow what another app is playing on a Cast device without taking it over.
- Zero telemetry. No analytics, no crash reporting. Network traffic is your LAN (discovery, your server, the devices you pick) plus lrclib.net for lyrics (on by default, one switch to turn off). [
PRIVACY.md](PRIVACY.md) lists every connection.
Screenshots
Main view Search Random Browse view Now Playing Settings 1 Settings 2
Status
Feature-complete enough for daily use as a music player; some features of the Android sibling (Android Auto, local MediaStore browse) are out of scope by design. See the Limitations section.
What this is
- A native desktop UPnP/DLNA music player for GNU/Linux.
- A drop-in alternative to Android Earhorn for users who want the library on their computer's speakers.
- The result of a Kotlin Multiplatform port: the entire
shared/module (domain model, UPnP layer, ViewModels, theme tokens) is the same code that powers the Android build. Only the renderer (VLCJ vs Media3) and the screens (Compose Multiplatform vs Compose Android) differ.
What this is not
- Not a music ripper, transcoder, library manager, or tagger. Earhorn renders what the server sends. Run MinimServer, Plex, or any other DLNA-compliant server and let it own the library.
- Not an Android Auto / TV / phone app. Use the Android app (
android/, next to this folder) for those surfaces. - Not a Windows or macOS app today. The build pipeline produces
.deband.AppImageonly. The codebase is Compose Multiplatform; cross-compiling to other JVM platforms is technically possible but not packaged here.
Install
Three options, easiest first.
.AppImage (any GNU/Linux distro)
Download earhorn-<version>-x86_64.AppImage from the download page. Mark executable, run.
chmod +x earhorn-<version>-x86_64.AppImage
./earhorn-<version>-x86_64.AppImage
Verify the download: save SHA256SUMS from the same page next to it, then:
sha256sum -c SHA256SUMS --ignore-missing
The AppImage bundles a JDK runtime. The host needs only libvlc (sudo apt install vlc on Debian/Ubuntu; equivalent elsewhere).
.deb (Debian, Ubuntu, Linux Mint, Pop!_OS)
sudo apt install ./earhorn_<version>_amd64.deb
Installs to /opt/earhorn, registers a .desktop entry, drops hicolor icons at 48 / 64 / 128 / 256 / 512, and writes AppStream metainfo so software centers list it properly. The first launch after install may need an AppStream cache refresh:
sudo appstreamcli refresh-cache --force
From source
sudo apt install openjdk-17-jdk vlc dpkg-dev
# download earhorn-<version>.zip from https://faveat.software/earhorn/#download and unzip it, then:
cd earhorn-<version>/desktop
./gradlew :desktop:run # run from source
./gradlew :desktop:packageDeb # build .deb
./gradlew :desktop:packageAppImage # build .AppImage (requires appimagetool on PATH)
Outputs land under desktop/build/compose/binaries/main/.
Requirements: JDK 17 or newer, libvlc available on the system (sudo apt install vlc), and for AppImage builds, appimagetool on PATH (download from the AppImageKit continuous build and put it somewhere on your PATH).
Usage
First launch
Earhorn scans the LAN with SSDP and lists the UPnP/DLNA servers it finds. Pick one. If no server appears, see Network notes below.
Browsing
- Toolbar dropdowns drive a hierarchical browse: Albums / Artists / Composers / Genres / Tracks / etc. — whatever your server advertises.
- Click a row to drill in. The breadcrumb above the list collapses when it gets too long; click the collapsed entries to jump.
- Right-click a track or container for "Play now", "Play next", "Add to queue".
- Dice button picks 42 random albums or tracks. The Party button in the breadcrumb row is "Party mode" — load 42 random tracks and start playing immediately.
Now Playing
- Album art — click for a full-screen lightbox. The drag handle below the hero row resizes the art square; pull it up to give the queue more vertical room on short windows.
- Queue — drag the handle on a row to reorder. The currently-playing track keeps playing through the reorder; only an explicit "skip" stops audio.
- Metadata sidebar — Composer, Conductor, Work, Performer, etc. Defaults to classical-listener tags; configurable in Settings.
Keyboard shortcuts
| Key | Action |
|---|---|
Space | Play / Pause (suppressed while typing in any text field) |
Ctrl+← / Ctrl+→ | Previous / next track |
Ctrl+K | Search |
Ctrl+1 / Ctrl+2 / Ctrl+3 | Library / Random / Playlists |
Ctrl+L | Lyrics for the current track |
Ctrl+, | Settings |
Ctrl+R | Refresh devices |
Ctrl+Q | Quit |
Esc / Alt+← | Back: closes an open menu or dialog first, then overlays, then the browse trail |
Media keys work through the desktop's media controls (MPRIS).
Features
| Feature | Status |
|---|---|
| UPnP/DLNA discovery (SSDP) | ✓ |
| ContentDirectory browse | ✓ |
| Search (with history) | ✓ |
| Local playback via libvlc | ✓ |
| Drag-to-reorder queue | ✓ |
Time-synced lyrics (lrclib.net + server-side .lrc) | ✓ |
Album notes (ID3 / Vorbis / sibling .txt) | ✓ |
| Random picker (Albums / Tracks) | ✓ |
| Party mode (42 random tracks, instant play) | ✓ |
| ReplayGain | ✓ local playback — track or album gain from the file's tags, with a pre-amp |
| Gapless playback | ✓ (libvlc native) |
| Save / load M3U playlists | ✓ |
| Sleep timer | ✓ |
| Google Cast sender (Chromecast, Chromecast Audio, speaker groups) | ✓ |
| Follow a Cast session another app is driving | ✓ |
| Control a UPnP renderer (AVTransport) | ✓ |
| Act as a UPnP renderer for other apps (LAN broadcast) | ✓ — off by default |
| Local device-side library | n/a — use a UPnP server |
| Android Auto | n/a |
| TV / leanback layout | n/a — use the Android build |
Privacy
Earhorn makes the minimum network requests required for its features. There are no analytics, no crash reporting, no telemetry.
- SSDP multicast to UDP 1900 and mDNS to UDP 5353 — server, renderer and Chromecast discovery on your LAN.
- HTTP to your UPnP server — track streams, browse responses, album art, sibling
.lrc/.txtfiles. - Devices you pick — UPnP renderer commands, and TLS to Cast devices (port 8009) when you cast to or follow one.
- musicnow — while following a Cast device, a status request about every 12 s to the musicnow address in Settings, only if you enter one (empty by default).
- **HTTPS to
lrclib.net— time-synced lyrics for the track in the lyrics view. On by default**; Settings → Lyrics → Allow external service calls turns it off. Sends the track title, artist, album and duration, plus a User-Agent naming Earhorn. No identifiers, no account. - Cover art published by others — when following a Cast device, the artwork address the sending app chose (often an internet server) is fetched.
- Listening — a UPnP event port (always), a stream proxy while casting to a speaker group, and, only if you turn on LAN broadcast, a renderer any device on your network can control.
Auditable: the source is published at faveat.software/earhorn, GPLv3+. See [PRIVACY.md](PRIVACY.md) for the full list, including what is stored on disk.
Limitations
- GNU/Linux only. The build pipeline produces
.deband.AppImage. macOS / Windows are out of scope. - x86_64 only. ARM64 builds are technically possible (libvlc supports it, Compose Multiplatform supports it) but not packaged today.
- No device-side library indexer. Earhorn doesn't scan local files into a library — point it at a UPnP server and let the server own that.
- Single-server browse. Switching servers takes one tap; cross-server search/browse is not a feature.
Compatibility
| Tested | Notes |
|---|---|
| Ubuntu 24.04 LTS (Plasma, GNOME) | Primary target |
| Debian 12 | .deb install path |
| Pop!_OS 22.04 | .deb install path |
| Fedora 40 (KDE) | .AppImage path |
| Arch (KDE) | .AppImage path |
| openSUSE Tumbleweed | .AppImage path |
JDK requirement: 17 or newer. The bundled JDK in the packaged .deb and .AppImage is 21. Source builds run on either.
Tested servers: MinimServer 2.x, Plex Media Server (DLNA mode), Universal Media Server, Jellyfin (DLNA plugin).
Tech stack
| Layer | What |
|---|---|
| Build | Gradle 8.x, JDK 17+, Kotlin 2.0.21 |
| UI | Compose Multiplatform 1.7 |
| Architecture | Kotlin Multiplatform (shared/ common across Android + Desktop) |
| Media | libvlc via VLCJ |
| UPnP / DLNA | jUPnP 3.0.3 |
| Image loading | Coil 3 |
| DI | Koin |
| Drag-to-reorder | sh.calvin.reorderable 2.4.3 |
| Packaging | Compose jpackage + custom Gradle tasks for AppStream / hicolor / .AppImage post-processing |
Project layout
earhorn-gnu-linux/
├── shared/ ← KMP library shared with the Android sibling
│ └── src/
│ ├── commonMain/ ← domain model, theme tokens
│ ├── jvmCommonMain/ ← UPnP, ViewModels, lyrics, playlist store
│ └── desktopMain/ ← VLCJ renderer, CastStreamProxy, desktop entry points
│
├── desktop/ ← Compose Multiplatform desktop application
│ └── src/jvmMain/
│ ├── kotlin/dev/stave/desktop/
│ │ ├── Main.kt ← application entry, Compose window setup,
│ │ │ WindowExceptionHandlerFactory installation
│ │ ├── screens/ ← AboutScreen, NowPlayingPane, RandomPickerScreen,
│ │ │ ServerSwitcher, SettingsScreen, AlbumGrid, …
│ │ ├── ui/StaveApp.kt ← top-level app shell, navigation stack
│ │ ├── window/StaveMenuBar.kt ← AWT menu bar wiring
│ │ ├── shortcuts/Keybindings.kt ← keyboard accelerator handling
│ │ └── coil/ ← Coil image loader integration
│ └── resources/
│ ├── icon.png ← steampunk-ear dock icon
│ ├── earhorn.png ← in-app brand badge
│ ├── earhorn_gramophone_glow.webp
│ └── icons/earhorn-{48,64,128,256,512}.png
│
├── build.gradle.kts ← plugin aliases
├── settings.gradle.kts ← module declarations
├── gradle/libs.versions.toml ← version catalog
├── README.md ← this file
├── DESIGN.md ← internal architecture / port notes
├── CHANGELOG.md
├── CONTRIBUTING.md
├── PRIVACY.md
├── SECURITY.md
└── LICENSE
Network notes
UPnP discovery uses SSDP multicast on UDP 1900. Most issues are network-level rather than code-level:
- VPN clients (NetGuard, ProtonVPN, Tailscale, …) often route all traffic through a tunnel that hides the LAN. Add a split-tunnel exclusion for your LAN subnet or temporarily disable the VPN.
- Docker / KVM / libvirt bridges appear as separate interfaces. Earhorn's local-IP picker filters obvious virtuals (
docker0,br-*,veth*,virbr*,vmnet*,tun*,tap*,zt*) — if your setup has differently-named virtuals, extend the filter inCastStreamProxy.getLocalIp(). - Firewalls — multicast UDP 1900 inbound, plus the ephemeral UDP ports the server uses for unicast responses, must be allowed. UFW:
sudo ufw allow from 192.168.0.0/16 to any port 1900 proto udp. - Wi-Fi multicast — some access points drop multicast on Wi-Fi to save airtime. Look for "IGMP Snooping" or "Multicast Forwarding" in the AP settings.
Building from source
See Install → From source for the basic invocation. For ongoing development:
./gradlew :desktop:run # iterative run
./gradlew :shared:compileKotlinDesktop # type-check shared
./gradlew clean :desktop:packageDeb :desktop:packageAppImage # full release build
Configuration cache is enabled. If you change a build.gradle.kts and Gradle complains about cache invalidation, that's expected — let it rebuild the cache.
The .deb build runs an enhanceDeb finalizer that patches .desktop files, installs hicolor icons, writes AppStream metainfo, and rewrites the DEBIAN/control Description block. The .AppImage build similarly post-processes the jpackage app-image directory into a real AppImage via appimagetool. Both flows are in desktop/build.gradle.kts and are documented inline.
See [DESIGN.md](DESIGN.md) for the deeper architecture / port write-up.
Reporting bugs
Email [foundry@faveat.software](mailto:foundry@faveat.software). Include:
- Distro + version (
lsb_release -a) - Java version (
java -version) - UPnP server name + version
- How to reproduce, and what you expected to happen instead
- For crashes, the stack trace from stderr (run
./gradlew :desktop:runfrom a terminal to capture it, or run the AppImage //opt/earhorn/bin/Earhornfrom a terminal)
The app installs a WindowExceptionHandlerFactory and a CoroutineExceptionHandler on all ViewModels, so most unexpected exceptions are caught, logged to stderr with full stack trace, and the app keeps running. If you see a "[Earhorn]" tagged stack trace in your terminal during a reproduction, that's the gold — include it.
Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md). The short version: small focused PRs, comments explaining why not what, no force pushes to long-lived branches, KMP shared code goes in shared/.
License
GPLv3 or later. See [LICENSE](LICENSE).
A Faveat Software project.
If Earhorn GNU-Linux is useful to you, consider gifting a nice, frosty beverage.


