Add project docs, glossary, and licence

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.
This commit is contained in:
Esa Kataja
2026-07-31 19:37:22 +03:00
commit 2474b42175
7 changed files with 1252 additions and 0 deletions
+148
View File
@@ -0,0 +1,148 @@
# 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](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](CONTEXT.md) | The glossary — every domain term, in English and Finnish |
| [docs/spec.md](docs/spec.md) | What the app does: rules, pipeline, routes, API contract, schema |
| [docs/decisions.md](docs/decisions.md) | Why it is that way. Append-only |
| [docs/later.md](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
reaches `main` by 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
```sh
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
```sh
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:
```sh
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:
```sh
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
```sh
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.
```sh
docker compose exec postgres pg_dump -U levyraati levyraati | gzip > backup-$(date +%F).sql.gz
```
### Database shell
```sh
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](LICENSE).