The Nuxt version is being rewritten in Go. This is the design that came out of it, split by how each part ages: - CONTEXT.md glossary, English identifiers and Finnish UI wording - docs/spec.md behaviour: rules, submission pipeline, routes, API contract, schema - docs/decisions.md why, append-only - docs/later.md deliberately not in v1 testdata/ytdlp-noose.json is a real yt-dlp dump used to test metadata prefill against a video with no track, artist or album tags.
5.1 KiB
Levyraati
A private music review club. Members submit songs — by file upload or YouTube link — and review each other's picks on a 1–100 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/later.md | Deliberately not in v1, with the reasoning kept |
Branches
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 reachesmainby merge at release time.
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 |
|---|---|---|
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 |
STORAGE_DIR |
./storage |
Audio, avatars, in-flight conversions |
SECURE_COOKIES |
true |
Set false for local development over plain HTTP |
Local development
docker compose up -d postgres
export DATABASE_URL=postgres://levyraati:levyraati@localhost:5432/levyraati
export ADMIN_PASSWORD=dev SECURE_COOKIES=false
go run .
Requires Go 1.22+, plus ffmpeg, ffprobe, and yt-dlp on PATH.
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 is installed with
pip install -U yt-dlp at image build time, 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./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.