Add the submission pipeline and the review loop

Steps 3 and 4 of the build order. A member can now upload a song, watch it
convert, publish it, and review what everyone else has published.

Pipeline:
- ffprobe reads tags synchronously at submit so prefill never races typing;
  ffmpeg converts to Opus in the background, two at a time
- ffmpeg succeeding is the validation — no container sniffing
- publish moves the file inside the transaction, so a song row and its .ogg
  appear together or neither does
- five submissions per rolling 24h, failures excluded

Reviews and the reveal rule:
- the queue is unreviewed songs only, oldest first, never your own
- other people's reviews and the average are withheld in the query, not the
  template — a hidden average is never sent
- 30 minutes to edit or delete your own review, enforced in the WHERE clause
- deleting the last review unlocks the song for its submitter again

The waiting page has one button: the metadata form autosaves after a pause in
typing, and Julkaise submits it and publishes in the same request, so nothing
is lost without JS.

Genres store an English code and render a Finnish label.
This commit is contained in:
Esa Kataja
2026-07-31 21:42:49 +03:00
parent 41c8a2914f
commit 80d3e36679
19 changed files with 2040 additions and 16 deletions
+25 -7
View File
@@ -96,10 +96,20 @@ zero reviews, it is editable again.
### 2.2 Genres
Fixed list, `text` column, validated app-side:
Fixed list, `text` column, validated app-side. The **stored value is the English code** and the
Finnish label is display only — the same split the statuses use, so rewording a genre never touches
a song row:
Rock, Metal, Punk, Blues, Jazz, Electronic, Hip Hop, Pop, Folk / Country, Classical, Soundtrack,
Experimental, Finnish, Just Plain Weird, Other
| Code | Label | | Code | Label |
|---|---|---|---|---|
| Rock | Rock | | Soundtrack | Elokuvamusiikki |
| Metal | Metal | | Experimental | Kokeellinen |
| Punk | Punk | | Classical | Klassinen |
| Blues | Blues | | Electronic | Elektroninen |
| Jazz | Jazz | | Hip Hop | Hip hop |
| Pop | Pop | | Finnish | Kotimainen |
| Folk / Country | Folk / Country | | Just Plain Weird | Ihan outoa |
| | | | Other | Muu |
---
@@ -201,9 +211,17 @@ States: `queued` → (`downloading`, URL only) → `converting` → `ready` | `f
Submitter-only. Live status plus the editable metadata form, so the wait is spent writing the
introduction rather than watching a spinner.
The button reads *Muunnetaan…* and is disabled until `status = 'ready'`, when it becomes
**Julkaise**. Publishing is always an explicit click — firing it automatically would race the
submitter mid-sentence.
**One button, at the bottom of the form: Julkaise**, disabled until `status = 'ready'`. Publishing
is always an explicit click — firing it automatically would race the submitter mid-sentence.
There is no separate save button: two buttons made it unclear which one committed the text.
- The metadata form **autosaves**`hx-post` on `input changed delay:1.2s` and on `change`,
answering with a quiet "Tallennettu 21.37" line and nothing else.
- Julkaise lives outside the form and is bound to it with the HTML `form=` attribute, so pressing
it submits the metadata *and* publishes in one request. The last keystrokes therefore arrive with
the click even if the autosave never fired — which is also what makes the page work with no JS at
all.
The live part is HTMX polling a fragment:
@@ -211,7 +229,7 @@ The live part is HTMX polling a fragment:
<!-- {{define "submission-status"}} — included by the page, returned alone by the poll -->
<div hx-get="/submit/{{.ID}}/status" hx-trigger="every 2s" hx-swap="outerHTML">
<p>{{.Label}}</p>
<button {{if not .Ready}}disabled{{end}}>Julkaise</button>
<button type="submit" form="meta" {{if not .Ready}}disabled{{end}}>Julkaise</button>
</div>
```