EARHORNMUSIC UPnP · Android TV · Phones · Android Auto · GNU/Linux
Release 0.5.0

README.md

Each line, with the release that last changed it.

0.5.01# Earhorn Music — GNU/Linux
2
3**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.
4
5[![License: GPL v3+](https://img.shields.io/badge/License-GPLv3+-blue.svg)](LICENSE)
6[![Kotlin](https://img.shields.io/badge/Kotlin-2.0.21-7F52FF.svg)](https://kotlinlang.org)
7[![Compose Multiplatform](https://img.shields.io/badge/Compose_MP-1.7-4285F4.svg)](https://www.jetbrains.com/lp/compose-multiplatform/)
8
9[**Website**](https://faveat.software/earhorn/) · [**Download**](https://faveat.software/earhorn/#download) · [**Browse the code**](https://faveat.software/earhorn/browse/) · [**All releases**](https://faveat.software/earhorn/browse/history.html) · [**Tip jar**](https://ko-fi.com/faveat)
10
11---
12
13## Why Earhorn on the desktop?
14
15Earhorn 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.
16
17- **Plays through your computer.** libvlc via VLCJ — FLAC, MP3, WAV, AAC, ALAC, DSD, Opus, anything libvlc handles, which is essentially everything.
18- **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](#privacy)).
19- **Same browse tree as the Android build.** MinimServer's `indexTags` structure renders faithfully; synthetic entries (`>> Show All`, `[folder view]`, `[untagged]`) get distinct sage-green italic typography.
20- **Classical-listener defaults.** 28 configurable metadata fields. Defaults surface Composer, Conductor, Work, Performer, Ensemble, sample rate, bit depth. Toggle what you care about.
21- **Drag-to-reorder queue.** Real-time visual reorder; the renderer's playback never restarts when the currently-playing track stays put.
22- **Lyrics, album art, album notes.** Time-synced lyrics from lrclib.net with server-side `.lrc` fallback. Coil-loaded album art. `AlbumInfoLoader` reads ID3 / Vorbis comments and sibling `.txt` notes.
23- **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.
24- **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.
25
26## Screenshots
27
28[![Main view](docs/screenshots/main.png)](docs/screenshots/main.png)
29[![Search](docs/screenshots/search.png)](docs/screenshots/search.png)
30[![Random](docs/screenshots/random.png)](docs/screenshots/random.png)
31[![Browse view](docs/screenshots/about.png)](docs/screenshots/about.png)
32[![Now Playing](docs/screenshots/user-guide.png)](docs/screenshots/user-guide.png)
33[![Settings 1](docs/screenshots/settings1.png)](docs/screenshots/settings1.png)
34[![Settings 2](docs/screenshots/settings2.png)](docs/screenshots/settings2.png)
35
36## Status
37
38Feature-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](#limitations) section.
39
40## What this is
41
42- A native desktop UPnP/DLNA music player for GNU/Linux.
43- A drop-in alternative to Android Earhorn for users who want the library on their computer's speakers.
44- 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.
45
46## What this is not
47
48- **Not a music ripper, transcoder, library manager, or tagger.** Earhorn renders what the server sends. Run [MinimServer](https://minimserver.com/), [Plex](https://www.plex.tv/), or any other DLNA-compliant server and let it own the library.
49- **Not an Android Auto / TV / phone app.** Use the Android app (`android/`, next to this folder) for those surfaces.
50- **Not a Windows or macOS app today.** The build pipeline produces `.deb` and `.AppImage` only. The codebase is Compose Multiplatform; cross-compiling to other JVM platforms is technically possible but not packaged here.
51
52## Install
53
54Three options, easiest first.
55
56### `.AppImage` (any GNU/Linux distro)
57
58Download `earhorn-<version>-x86_64.AppImage` from the [download page](https://faveat.software/earhorn/#download). Mark executable, run.
59
60```bash
61chmod +x earhorn-<version>-x86_64.AppImage
62./earhorn-<version>-x86_64.AppImage
63```
64
65Verify the download: save `SHA256SUMS` from the same page next to it, then:
66
67```bash
68sha256sum -c SHA256SUMS --ignore-missing
69```
70
71The AppImage bundles a JDK runtime. The host needs only libvlc (`sudo apt install vlc` on Debian/Ubuntu; equivalent elsewhere).
72
73### `.deb` (Debian, Ubuntu, Linux Mint, Pop!_OS)
74
75```bash
76sudo apt install ./earhorn_<version>_amd64.deb
77```
78
79Installs 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:
80
81```bash
82sudo appstreamcli refresh-cache --force
83```
84
85### From source
86
87```bash
88sudo apt install openjdk-17-jdk vlc dpkg-dev
89# download earhorn-<version>.zip from https://faveat.software/earhorn/#download and unzip it, then:
90cd earhorn-<version>/desktop
91./gradlew :desktop:run # run from source
92./gradlew :desktop:packageDeb # build .deb
93./gradlew :desktop:packageAppImage # build .AppImage (requires appimagetool on PATH)
94```
95
96Outputs land under `desktop/build/compose/binaries/main/`.
97
98Requirements: 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](https://github.com/AppImage/appimagetool/releases) and put it somewhere on your `PATH`).
99
100## Usage
101
102### First launch
103
104Earhorn scans the LAN with SSDP and lists the UPnP/DLNA servers it finds. Pick one. If no server appears, see [Network notes](#network-notes) below.
105
106### Browsing
107
108- **Toolbar dropdowns** drive a hierarchical browse: Albums / Artists / Composers / Genres / Tracks / etc. — whatever your server advertises.
109- **Click a row** to drill in. The breadcrumb above the list collapses when it gets too long; click the collapsed entries to jump.
110- **Right-click a track or container** for "Play now", "Play next", "Add to queue".
111- **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.
112
113### Now Playing
114
115- **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.
116- **Queue** — drag the handle on a row to reorder. The currently-playing track keeps playing through the reorder; only an explicit "skip" stops audio.
117- **Metadata sidebar** — Composer, Conductor, Work, Performer, etc. Defaults to classical-listener tags; configurable in Settings.
118
119### Keyboard shortcuts
120
121| Key | Action |
122|-----|--------|
123| `Space` | Play / Pause (suppressed while typing in any text field) |
124| `Ctrl+←` / `Ctrl+→` | Previous / next track |
125| `Ctrl+K` | Search |
126| `Ctrl+1` / `Ctrl+2` / `Ctrl+3` | Library / Random / Playlists |
127| `Ctrl+L` | Lyrics for the current track |
128| `Ctrl+,` | Settings |
129| `Ctrl+R` | Refresh devices |
130| `Ctrl+Q` | Quit |
131| `Esc` / `Alt+←` | Back: closes an open menu or dialog first, then overlays, then the browse trail |
132
133Media keys work through the desktop's media controls (MPRIS).
134
135## Features
136
137| Feature | Status |
138|---------|--------|
139| UPnP/DLNA discovery (SSDP) | ✓ |
140| ContentDirectory browse | ✓ |
141| Search (with history) | ✓ |
142| Local playback via libvlc | ✓ |
143| Drag-to-reorder queue | ✓ |
144| Time-synced lyrics (lrclib.net + server-side `.lrc`) | ✓ |
145| Album notes (ID3 / Vorbis / sibling `.txt`) | ✓ |
146| Random picker (Albums / Tracks) | ✓ |
147| Party mode (42 random tracks, instant play) | ✓ |
148| ReplayGain | ✓ local playback — track or album gain from the file's tags, with a pre-amp |
149| Gapless playback | ✓ (libvlc native) |
150| Save / load M3U playlists | ✓ |
151| Sleep timer | ✓ |
152| Google Cast sender (Chromecast, Chromecast Audio, speaker groups) | ✓ |
153| Follow a Cast session another app is driving | ✓ |
154| Control a UPnP renderer (AVTransport) | ✓ |
155| Act as a UPnP renderer for other apps (LAN broadcast) | ✓ — off by default |
156| Local device-side library | n/a — use a UPnP server |
157| Android Auto | n/a |
158| TV / leanback layout | n/a — use the Android build |
159
160## Privacy
161
162Earhorn makes the minimum network requests required for its features. There are no analytics, no crash reporting, no telemetry.
163
164- **SSDP multicast** to UDP 1900 and **mDNS** to UDP 5353 — server, renderer and Chromecast discovery on your LAN.
165- **HTTP to your UPnP server** — track streams, browse responses, album art, sibling `.lrc` / `.txt` files.
166- **Devices you pick** — UPnP renderer commands, and TLS to Cast devices (port 8009) when you cast to or follow one.
167- **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).
168- **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.
169- **Cover art published by others** — when following a Cast device, the artwork address the sending app chose (often an internet server) is fetched.
170- **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.
171
172Auditable: the source is published at [faveat.software/earhorn](https://faveat.software/earhorn/browse/), GPLv3+. See [`PRIVACY.md`](PRIVACY.md) for the full list, including what is stored on disk.
173
174## Limitations
175
176- **GNU/Linux only.** The build pipeline produces `.deb` and `.AppImage`. macOS / Windows are out of scope.
177- **x86_64 only.** ARM64 builds are technically possible (libvlc supports it, Compose Multiplatform supports it) but not packaged today.
178- **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.
179- **Single-server browse.** Switching servers takes one tap; cross-server search/browse is not a feature.
180
181## Compatibility
182
183| Tested | Notes |
184|--------|-------|
185| Ubuntu 24.04 LTS (Plasma, GNOME) | Primary target |
186| Debian 12 | `.deb` install path |
187| Pop!_OS 22.04 | `.deb` install path |
188| Fedora 40 (KDE) | `.AppImage` path |
189| Arch (KDE) | `.AppImage` path |
190| openSUSE Tumbleweed | `.AppImage` path |
191
192JDK requirement: 17 or newer. The bundled JDK in the packaged `.deb` and `.AppImage` is 21. Source builds run on either.
193
194Tested servers: MinimServer 2.x, Plex Media Server (DLNA mode), Universal Media Server, Jellyfin (DLNA plugin).
195
196## Tech stack
197
198| Layer | What |
199|-------|------|
200| Build | Gradle 8.x, JDK 17+, Kotlin 2.0.21 |
201| UI | Compose Multiplatform 1.7 |
202| Architecture | Kotlin Multiplatform (`shared/` common across Android + Desktop) |
203| Media | libvlc via VLCJ |
204| UPnP / DLNA | jUPnP 3.0.3 |
205| Image loading | Coil 3 |
206| DI | Koin |
207| Drag-to-reorder | sh.calvin.reorderable 2.4.3 |
208| Packaging | Compose `jpackage` + custom Gradle tasks for AppStream / hicolor / .AppImage post-processing |
209
210## Project layout
211
212```
213earhorn-gnu-linux/
214├── shared/ ← KMP library shared with the Android sibling
215│ └── src/
216│ ├── commonMain/ ← domain model, theme tokens
217│ ├── jvmCommonMain/ ← UPnP, ViewModels, lyrics, playlist store
218│ └── desktopMain/ ← VLCJ renderer, CastStreamProxy, desktop entry points
219│
220├── desktop/ ← Compose Multiplatform desktop application
221│ └── src/jvmMain/
222│ ├── kotlin/dev/stave/desktop/
223│ │ ├── Main.kt ← application entry, Compose window setup,
224│ │ │ WindowExceptionHandlerFactory installation
225│ │ ├── screens/ ← AboutScreen, NowPlayingPane, RandomPickerScreen,
226│ │ │ ServerSwitcher, SettingsScreen, AlbumGrid, …
227│ │ ├── ui/StaveApp.kt ← top-level app shell, navigation stack
228│ │ ├── window/StaveMenuBar.kt ← AWT menu bar wiring
229│ │ ├── shortcuts/Keybindings.kt ← keyboard accelerator handling
230│ │ └── coil/ ← Coil image loader integration
231│ └── resources/
232│ ├── icon.png ← steampunk-ear dock icon
233│ ├── earhorn.png ← in-app brand badge
234│ ├── earhorn_gramophone_glow.webp
235│ └── icons/earhorn-{48,64,128,256,512}.png
236│
237├── build.gradle.kts ← plugin aliases
238├── settings.gradle.kts ← module declarations
239├── gradle/libs.versions.toml ← version catalog
240├── README.md ← this file
241├── DESIGN.md ← internal architecture / port notes
242├── CHANGELOG.md
243├── CONTRIBUTING.md
244├── PRIVACY.md
245├── SECURITY.md
246└── LICENSE
247```
248
249## Network notes
250
251UPnP discovery uses SSDP multicast on UDP 1900. Most issues are network-level rather than code-level:
252
253- **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.
254- **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 in `CastStreamProxy.getLocalIp()`.
255- **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`.
256- **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.
257
258## Building from source
259
260See [Install → From source](#from-source) for the basic invocation. For ongoing development:
261
262```bash
263./gradlew :desktop:run # iterative run
264./gradlew :shared:compileKotlinDesktop # type-check shared
265./gradlew clean :desktop:packageDeb :desktop:packageAppImage # full release build
266```
267
268Configuration cache is enabled. If you change a `build.gradle.kts` and Gradle complains about cache invalidation, that's expected — let it rebuild the cache.
269
270The `.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.
271
272See [`DESIGN.md`](DESIGN.md) for the deeper architecture / port write-up.
273
274## Reporting bugs
275
276Email [`foundry@faveat.software`](mailto:foundry@faveat.software). Include:
277
278- Distro + version (`lsb_release -a`)
279- Java version (`java -version`)
280- UPnP server name + version
281- How to reproduce, and what you expected to happen instead
282- For crashes, the stack trace from stderr (run `./gradlew :desktop:run` from a terminal to capture it, or run the AppImage / `/opt/earhorn/bin/Earhorn` from a terminal)
283
284The 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.
285
286## Contributing
287
288See [`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/`.
289
290## License
291
292GPLv3 or later. See [`LICENSE`](LICENSE).
293
294A [Faveat Software](https://faveat.software) project.
295
296If Earhorn GNU-Linux is useful to you, consider [gifting a nice, frosty beverage](https://ko-fi.com/faveat).