Esa Kataja a9776c6dde Follow the song in the lyrics
Synced LRC highlights the playing line, keeps it centred and seeks on click.
Plain text scrolls continuously with a nudge knob instead — a highlight on
guessed timings turns guaranteed drift into what looks like a bug. Seuraa
kappaletta turns following off without losing the highlight, and scrolling by
hand turns it off too.

Fixes the scroll landing in the wrong place (offsetTop measured from a
different coordinate space than the box it was applied to) and the fader
shifting the deck sideways at score 100 (auto-sized grid columns plus a
readout spanning both).
2026-08-01 00:45:40 +03:00
2026-08-01 00:45:40 +03:00
2026-08-01 00:45:40 +03:00
2026-07-31 21:54:15 +03:00
2026-08-01 00:45:40 +03:00
2026-08-01 00:45:40 +03:00
2026-07-31 21:54:15 +03:00
2026-08-01 00:45:40 +03:00

Levyraati

A private music review club. Members submit songs — by file upload or YouTube link — and review each other's picks on a 1100 scale. Other people's reviews of a song stay hidden until you've written your own.

Invite-only, no public registration. Built for about ten friends.

Status: not built yet. This README describes the app defined in docs/spec.md; commands here are the intended interface, not a record of anything that runs today.

Docs

File What's in it
CONTEXT.md The glossary — every domain term, in English and Finnish
docs/spec.md What the app does: rules, pipeline, routes, API contract, schema
docs/decisions.md Why it is that way. Append-only
docs/theme.md The visual language: tokens, type, and what differs from the theme handoff
docs/later.md Deliberately not in v1, with the reasoning kept

Branches and releases

  • main — released code only. Every commit on it is something that ran in production, or is meant to. Tagged at each release.
  • dev — current development, and whatever nightly builds get made. Work happens here and reaches main by merge at release time.

Versions are CalVer: YYYY.MM.DD-N, where N is the build number for that day, starting at 1. The version is injected at build time, so no file in the repo carries it:

git switch main && git merge --no-ff dev
git tag 2026.07.31-1
VERSION=$(git describe --tags --exact-match) docker compose build app
docker compose up -d app

A plain go build reports dev, which is the honest answer for a local binary. The running version appears in the footer, in the startup log line, and in GET /healthz — so "what is actually deployed" is answerable without an SSH session.

The app never sends email — there is no verification, no password reset link, and no notifications. Members have an address because it is their login and because mail is a planned feature.

Stack

Go, Postgres, html/template, HTMX + Alpine. Audio is converted with ffmpeg and downloaded with yt-dlp. One binary, one origin — there is no separate frontend to deploy.

Go dependencies: pgx/v5 and golang.org/x/crypto. No Node, no npm, no bundler.

Running it

cp .env.example .env      # then edit — ADMIN_PASSWORD has no default and the app won't start without it
docker compose up -d

Migrations apply themselves at startup, before the server accepts connections. The first launch creates no users: log into the admin panel and mint an invite.

Configuration

Variable Default Notes
POSTGRES_PASSWORD Required by Compose. Used to build DATABASE_URL for the app
DATABASE_URL postgres://user:pass@postgres:5432/levyraati
ADMIN_USER admin Admin panel username
ADMIN_PASSWORD Required. No default; the app refuses to start without it
ADDR :8080 Member-facing listener
ADMIN_ADDR 127.0.0.1:8081 Admin listener. Keep it on loopback. Under Compose it binds :8081 inside the container and is published only to the host's loopback
STORAGE_DIR ./storage Audio, avatars, in-flight conversions
SECURE_COOKIES true Set false for local development over plain HTTP
PUBLIC_URL Public address of the member site, e.g. https://levyraati.example.com. Used to build invite links in the admin panel; unset gives relative links

Local development

docker compose up -d postgres
export DATABASE_URL="postgres://levyraati:$POSTGRES_PASSWORD@localhost:5432/levyraati"
export ADMIN_PASSWORD=dev SECURE_COOKIES=false
go run .

Requires Go 1.24+, plus ffmpeg, ffprobe, and yt-dlp on PATH.

Tests that need a database are skipped unless TEST_DATABASE_URL points at a throwaway one — the migration test drops and recreates the public schema, so never point it at anything you care about.

go test ./...

Templates, stylesheet, and migrations are embedded with embed.FS, so a rebuild is needed to see template changes. go build && ./levyraati is the loop.

Admin panel

The admin is not a user account. It exists only as ADMIN_USER / ADMIN_PASSWORD, authenticates with HTTP Basic Auth, and is bound to loopback so it is not reachable from the internet. Reach it through an SSH tunnel:

ssh -L 8081:127.0.0.1:8081 you@server
# then open http://localhost:8081

From there: mint invites, reset member passwords, ban members, delete songs, read issue reports.

Lost the admin password? Edit .env and docker compose restart app. There is no recovery endpoint and no recovery key — the credentials are the environment.

Operations

yt-dlp goes stale

yt-dlp needs regular updates to keep working against YouTube. It comes from Alpine's community repository, whose active branch tracks upstream closely, so rebuilding is how you update it:

docker compose build --no-cache app && docker compose up -d app

Rebuild monthly. When submissions start failing with download errors, this is the first thing to try. Failures are shown to the submitter with the yt-dlp error attached, so they're visible without reading logs.

Logs

docker compose logs -f app

JSON to stdout, nothing else. There is no log table and no log viewer in the app.

Backups

Two paths hold everything:

  • ./pgdata — the database. Postgres 18 stores it under a version subdirectory (18/docker), so the mount is /var/lib/postgresql, not /var/lib/postgresql/data
  • ./storage — audio files and avatars

Both are bind mounts. storage/tmp/ is in-flight conversions and is safe to skip; it's cleared on startup anyway.

docker compose exec postgres pg_dump -U levyraati levyraati | gzip > backup-$(date +%F).sql.gz

Database shell

docker compose exec postgres psql -U levyraati levyraati

Layout

Go source is flat at the repository root, one package. Beyond that: templates/ and static/ are embedded assets, migrations/ holds numbered .sql files applied in order at startup, and testdata/ holds the golden JSON files that guard the API contract, plus ytdlp-noose.json — a real yt-dlp -J dump of an ordinary upload, used to test metadata prefill against a video that has no track, artist or album at all.

Notes

Downloading audio from YouTube is against YouTube's terms of service. This is a private app among a handful of friends; the decision is deliberate rather than accidental.

Licence

MIT — see LICENSE.

S
Description
No description provided
Readme
282 KiB
Languages
Go 63.2%
CSS 16.4%
HTML 15.4%
JavaScript 4.6%
Dockerfile 0.4%