Skip to content
pynezzPublic

About

spor is a self-hosted simple music streaming service for sharing self-made music (godspeed AI)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

5 Commits

Folders and files

Repository files navigation

spor

A self-hosted music library in one Go binary. It serves your own tracks (audio and video) from a directory, signed with Ed25519. Share links preview properly in Signal, Messenger and Discord. The server has no dependencies, no database and no accounts.

Full documentation: docs/ (publishing, server setup, configuration, logs, troubleshooting). See PLAN.md for the design.

Quick start

On the workstation:

spor keygen                          # ~/.config/spor/spor.key + spor.pub
mkdir -p library/neon-fjord
cp ~/Downloads/neon-fjord.mp3 library/neon-fjord/track.mp3
cp ~/Downloads/cover.jpg      library/neon-fjord/cover.jpg
cat > library/neon-fjord/meta.toml <<'EOF'
title = "Neon Fjord"
date = "2026-09-30"        # published
created = "2024-05"        # originally made: YYYY, YYYY-MM or YYYY-MM-DD (optional)
style = "synthwave"
description = "First one out."
visibility = "public"   # public | unlisted | draft
EOF
spor sign library/neon-fjord         # writes peaks.bin (ffmpeg) and track.sig
spor verify -pub ~/.config/spor/spor.pub library/

Publish in one step (sign, rsync, SIGHUP, print the share link):

export SPOR_REMOTE=root@vps:/srv/spor/library
spor publish library/neon-fjord

Or let scripts/add-track.sh do all of it from a single file. It reads title, genre and the original creation date from the tags (the publish date is today), takes the cover from an image next to the file or the embedded artwork, transcodes WAV/AIFF to opus + mp3, writes meta.toml and signs:

scripts/add-track.sh ~/Downloads/neon-fjord.mp3                  # -> library/<slug from title>, signed
scripts/add-track.sh -s synthwave -v unlisted -p ~/Exports/demo.wav   # -p: publish to $SPOR_REMOTE
scripts/add-track.sh -h                                          # all options (title, slug, cover, lyrics, ...)

Track directory

neon-fjord/            # directory name = slug = /t/neon-fjord
  track.mp3            # audio: mp3, m4a, opus, ogg, webm, flac (several = <source> fallbacks)
  track.mp4            # video (optional, alone or with audio)
  cover.jpg            # jpg/png; for video, sign grabs a frame if missing
  lyrics.txt           # optional
  meta.toml            # optional (meta.json also works)
  peaks.bin            # waveform, generated by `spor sign` when ffmpeg is present
  track.sig            # signed manifest; the directory is ignored without a valid one

Every file in track.sig is hashed and signed. If a listed file is missing or changed, the whole track is rejected. Files that are not listed are ignored and logged. For unlisted tracks, spor sign adds a random 128-bit token to meta.toml and prints the ?k= link.

Server

spor serve -config /etc/spor/spor.toml
# or: spor serve -library lib -cache cache -pub spor.pub -url https://lytt.txtv.no -listen 127.0.0.1:8740

The server only needs the public key. It verifies once at scan time and caches hashes in cache/state.json, keyed by (dev, inode, size, mtime). It rescans on SIGHUP and every rescan_interval. Media is content-addressed (/m/<sha256>.<ext>, immutable), served with sendfile and Range support. Bytes that change after verification are refused until the next scan.

Route
/ library UI
/t/<slug>[?k=token] share page with OpenGraph tags, works without JS
/api/library.json public tracks, pre-gzipped, ETag
/api/sig/<slug>, /api/keys manifest and public keys for in-browser verification
/m/…, /c/…, /p/… media, cover thumbnails (320/1200), waveform peaks
POST /api/play/<slug> append-only play log (cache/plays.log, no IPs, no cookies)
POST /api/like/<slug> thumbs up; header X-Spor-Session: <32 hex>; 409 if that session already liked it (cache/likes.log)
GET /api/stats, /api/stats/<slug> play and like counts, liked for the requesting session

The site is Norwegian (bokmål) for everyone, crawlers included; ?lang=en gives the English strings. Likes are one per browser session per track: the browser keeps a random session id in localStorage and the server stores a hash of it. That stops double clicks, not someone determined to inflate a count. Set likes = false to turn them off.

The footer shows web/static/brand.png and two buttons: suno_url (the artist profile, default https://suno.com/@skumx) and referral_url (a Suno referral link; hidden until set). Both must be https; an empty value hides the button. spor serve -suno URL -referral URL sets them without a config file. The full-size original artwork is kept in assets/ so it is not embedded in the binary.

Deploy (AlmaLinux VPS, native)

git clone https://github.com/pynezz/spor && cd spor
sudo GO="$(command -v go)" deploy/install.sh   # builds with Go >= 1.25, installs binary, unit and config
sudo cp spor.pub /etc/spor/spor.pub
sudo systemctl enable --now spor

Or download a binary from Releases and run install.sh's steps by hand. Add deploy/spor.snippet to Caddy. The systemd unit runs as a DynamicUser with the library read-only, state in /var/lib/spor, the public key via LoadCredential= and a 128 MB memory cap.

Where this differs from the plan

  • gzip, not brotli, for precompressed text, since Go's stdlib has no brotli encoder. Caddy can still apply zstd/br on top for HTML.
  • systemd unit instead of Quadlet, since the VPS builds and runs the binary natively.
  • peaks.bin and the video cover are made at sign time on the workstation and signed with everything else, so the server does not need ffmpeg. If ffmpeg is on the server, it fills in missing peaks and covers itself.
  • Config and meta.toml use a tiny built-in flat-TOML parser, so there are still zero dependencies.
  • Fonts are bundled (Inter, Inter Tight; SIL OFL, licenses in web/static/fonts/) because the CSP allows no third-party origins.

About

spor is a self-hosted simple music streaming service for sharing self-made music (godspeed AI)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages