https://github.com/daniel-alexander4/hespera
Local-first media app for your music, TV, movies, and photos — automatic metadata matching, seekable ffmpeg transcoding, and a remote-friendly UI in one Go binary
https://github.com/daniel-alexander4/hespera
ffmpeg golang hls media-library media-server movies music-player musicbrainz photos self-hosted sqlite tmdb tv-shows
Last synced: about 8 hours ago
JSON representation
Local-first media app for your music, TV, movies, and photos — automatic metadata matching, seekable ffmpeg transcoding, and a remote-friendly UI in one Go binary
- Host: GitHub
- URL: https://github.com/daniel-alexander4/hespera
- Owner: daniel-alexander4
- License: gpl-3.0
- Created: 2026-07-04T17:38:32.000Z (5 days ago)
- Default Branch: main
- Last Pushed: 2026-07-04T17:56:46.000Z (5 days ago)
- Last Synced: 2026-07-04T19:14:47.038Z (5 days ago)
- Topics: ffmpeg, golang, hls, media-library, media-server, movies, music-player, musicbrainz, photos, self-hosted, sqlite, tmdb, tv-shows
- Language: Go
- Size: 16.3 MB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# Hespera
A local app for your **Music, TV, Movies, and Photos**, with automatic metadata
matching. Written in Go: a single static binary that opens a chromeless app
window on your machine (loopback-only — a single-machine app, not a network
server). SQLite storage, server-rendered HTML, no external services required to
run. A headless server mode is also available if you want to reach it from
other devices.
Licensed under the [GNU AGPL v3](LICENSE); third-party attributions in
[`THIRD_PARTY_LICENSES.md`](THIRD_PARTY_LICENSES.md).
## Features
- **Music** — MusicBrainz matching with Cover Art Archive covers, artist bios
and images, synced karaoke-style lyrics (LRCLIB), persistent playlists and
one-click instant mixes, shuffle by era or popularity, duplicate detection,
and a per-track tag editor that writes back to your files.
- **TV & Movies** — TMDB matching (posters, backdrops, cast, collections,
related titles), direct play when your browser can handle the file and
**seekable on-demand HLS transcoding** when it can't, embedded +
on-demand OpenSubtitles subtitles, **skip-intro detected by audio
fingerprinting**, scrub-preview thumbnails, per-episode screen-capture
thumbnails, Up Next auto-advance, and watched/resume tracking.
- **Photos** — a capture-date timeline built from EXIF (with a folders view
and year filters); home-video clips play through the same engine as TV.
- **Library care** — a filesystem watcher auto-scans new media, corruption
detection with lossless container auto-repair, loudness leveling, and jobs
interrupted by a shutdown resume automatically on the next launch.
- **Couch-friendly** — the whole UI drives with arrow keys or a TV remote,
scales itself to the physical display size, and honors hardware media keys.
- **Local-first** — one binary, SQLite, your files stay yours: no accounts,
no telemetry, and external services are used only to *fetch* metadata.
## Install
Hespera ships as a self-contained binary — the web UI assets are embedded, so
there is no directory to deploy alongside it. Builds are available for **Linux,
macOS, and Windows** (amd64 + arm64).
### Debian / Ubuntu (recommended)
```sh
./install.sh
```
Builds Hespera, packages a `.deb`, and installs it — placing `hespera` and
`hescli` in `/usr/bin`, an app-menu entry + icon, and pulling the runtime
dependencies (**ffmpeg**, **openssh-client**) via apt. No background service is
installed; launch **Hespera** from your app menu (or run `hespera`) and it opens
an app window:
```sh
hespera
```
### Other platforms (macOS, Windows, other Linux)
Build the binaries (or grab one from `dist/` after `./build.sh`) and run the
`hespera` binary directly — it's fully self-contained. **ffmpeg must be on your
PATH** for TV/movie playback (transcoding and tag recovery); music and metadata
work without it.
- macOS: `brew install ffmpeg`
- Windows: `winget install ffmpeg` (or `choco install ffmpeg`)
```sh
./hespera---
```
## Build from source
Requires Go 1.23+.
```sh
make build # local ./bin/hespera + ./bin/hescli (quick dev build)
make dist # cross-compile all platforms + .deb packages into dist/
make install # build, package, and install on this Debian/Ubuntu machine
make test # go test ./...
make release # build + publish dist/ as GitHub release v (needs gh + nfpm)
```
`build.sh` produces one cgo-free static `hespera` binary per OS/arch in `dist/`,
plus `.deb` packages for Linux amd64/arm64 (needs
[`nfpm`](https://github.com/goreleaser/nfpm): `go install
github.com/goreleaser/nfpm/v2/cmd/nfpm@latest`).
## Run
```sh
hespera
```
That's it — Hespera opens an app window automatically, bound to a random
loopback port (so it never collides with anything else). The window is a
Chromium-family app-mode window (Chrome, Chromium, Edge, or Brave — on Linux
one of these must be installed; Hespera deliberately won't hand the app
window to a non-Chromium default browser). On first run, the
window walks you through pointing it at your media folder and adding a library;
you can also set the media folder under **Libraries** or with
`HESPERA_MEDIA_ROOT`. It stores its database, caches, and downloaded artwork in
a per-user data directory (`~/.config/hespera` on Linux, the equivalent on
macOS/Windows).
`HESPERA_NO_BROWSER=1` runs **server mode** instead: no window, binds
`HESPERA_LISTEN` (default `127.0.0.1:8080` — loopback only). To reach it from
other devices, opt in explicitly with `HESPERA_LISTEN=:8080`.
### Serving your household
To let other devices in the house use Hespera (phones, laptops, a TV
browser), run server mode on the machine that holds the media, started at
boot by a systemd **user** unit:
```ini
# ~/.config/systemd/user/hespera.service
[Unit]
Description=Hespera media server
[Service]
Environment=HESPERA_NO_BROWSER=1
Environment=HESPERA_LISTEN=:8080
ExecStart=/usr/bin/hespera
Restart=on-failure
[Install]
WantedBy=default.target
```
```sh
systemctl --user daemon-reload && systemctl --user enable --now hespera
loginctl enable-linger $USER # keep it running when logged out
sudo ufw allow from 192.168.1.0/24 to any port 8080 proto tcp # your LAN subnet
```
Devices then browse `http://:8080`. Phones and laptops get the
right UI scale automatically; a TV browser can pin the 10-foot scale once
with `?scale=tv` (it persists per browser). On the server's own screen,
**just launch Hespera from the app menu as usual** — when a running instance
is detected (the service), the launcher *attaches*: it opens the same
chromeless app window onto it instead of starting a second copy, and exits.
Stop the service and the icon goes back to launching a standalone app.
Notes for shared use: Hespera has one household-wide state — watched marks,
resume positions, and playlists are shared by everyone (there are no user
profiles); the security posture below applies (trusted network only); and
the topbar power button only works from the server machine itself, so a
phone can't shut the family server down.
### Security posture
Hespera has **no authentication layer, by design** — it is a single-machine
media app, and in app mode it is only reachable from your own computer.
That means anyone who can reach the port in server mode has *full* access:
not just playback, but the tag editor (writes into your music files), the
integrity auto-repair (the one path that rewrites media files), settings,
and shutdown. The built-in CSRF guard stops hostile web pages, not direct
network peers. Hence the loopback default: exposing Hespera to a network is
an explicit choice, and should only be made on a network you trust end to
end. For anything beyond that (shared LAN, remote access), put a reverse
proxy with authentication in front (Caddy `basic_auth`, nginx `auth_basic`,
Tailscale, etc.) — that is the supported pattern, not an app-level login.
### Performance: sharing a disk with another media server
Hespera runs all of its background work — library scans, integrity checks,
loudness analysis, thumbnail and preview generation — at **idle I/O priority**
(and nice 19), so a long scan yields the disk to anything that needs it right
now, like Plex or Jellyfin streaming a movie from the same drive.
One catch: the kernel only enforces I/O priorities when the disk's scheduler
supports them, and the default on most distros (`mq-deadline`) ignores them.
Check yours (replace `sdb` with your media disk):
```sh
cat /sys/block/sdb/queue/scheduler # [mq-deadline] → priorities are ignored
```
For a spinning disk (external USB drives especially), switch it to `bfq`:
```sh
# apply now
sudo modprobe bfq
echo bfq | sudo tee /sys/block/sdb/queue/scheduler
# make it stick across reboots
echo bfq | sudo tee /etc/modules-load.d/bfq.conf
echo 'ACTION=="add|change", KERNEL=="sdb", ATTR{queue/scheduler}="bfq"' \
| sudo tee /etc/udev/rules.d/60-media-disk.rules
```
With `bfq`, Hespera's background jobs still use the disk's full speed when it
is otherwise idle — they only step aside under contention. On NVMe/SSD media
disks this tuning rarely matters; it is for rotational disks shared with
playback.
### Configuration
Day-to-day settings (media folder, API keys, feature toggles, subtitle
defaults) live in the in-app **Settings** page. Environment variables cover
the rest — all `HESPERA_`-prefixed:
| Variable | Default | Purpose |
|----------|---------|---------|
| `HESPERA_NO_BROWSER` | (unset) | Set → **server mode**: no app window, honors `HESPERA_LISTEN`. Unset → app mode (chromeless window on a random loopback port) |
| `HESPERA_LISTEN` | `127.0.0.1:8080` | Server-mode listen address — loopback by default; LAN exposure is an explicit opt-in (`:8080`) |
| `HESPERA_DATA_DIR` | per-user config dir | Database, caches, artwork |
| `HESPERA_DB_PATH` | `/hespera.sqlite` | Database path |
| `HESPERA_MEDIA_ROOT` | home dir | Media root (the path-containment boundary; also settable in Settings → Libraries) |
| `HESPERA_TMDB_API_KEY` | | TMDB key for TV/movie matching (also settable in Settings) |
| `HESPERA_FANARTTV_API_KEY` | | Optional fanart.tv key — artist image backfill |
| `HESPERA_THEAUDIODB_API_KEY` | | Optional TheAudioDB key — artist bio/image backfill |
| `HESPERA_LASTFM_API_KEY` | | Optional Last.fm key — popularity blend for shuffles |
| `HESPERA_OPENSUBTITLES_API_KEY` | | Optional OpenSubtitles key — on-demand subtitle search |
| `HESPERA_OPENSUBTITLES_USER_AGENT` | `Hespera v1.0` | OpenSubtitles registered consumer UA |
| `HESPERA_LOG_LEVEL` | `info` | Log level (`debug`/`info`/`warn`/`error`). Per-request access logs are emitted at `debug`; the default `info` keeps request serving off the log path. Set `debug` to see per-request logs |
| `HESPERA_FFMPEG_CONCURRENCY` | 4 | Max concurrent ffmpeg/ffprobe processes |
| `HESPERA_HLS_ENCODER` | `software` | HLS video encoder: `software` (libx264) or `vaapi` (opt-in hardware encode) |
| `HESPERA_HLS_SEGMENT_CONCURRENCY` | 1 | Max concurrent HLS segment transcodes (keeps prefetch bursts off every core) |
| `HESPERA_FFMPEG_ACQUIRE_TIMEOUT` | 2s | How long foreground ffmpeg work waits for a slot |
| `HESPERA_TV_HLS_CACHE_MAX_BYTES` | 20GiB | HLS transcode cache budget |
| `HESPERA_TV_CACHE_MAX_AGE` | 72h | HLS cache entry max age |
| `HESPERA_TRICKPLAY_CACHE_MAX_BYTES` | 10GiB | Scrub-preview sprite cache budget |