# 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).