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:
@@ -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 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](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).
|
||||
Reference in New Issue
Block a user