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
+137
View File
@@ -0,0 +1,137 @@
# Later
Ideas with their reasoning, kept so the thinking isn't redone from scratch. **None of this is v1
work**, and none of it should shape v1 code beyond what the API contract already allows.
---
## Email: password reset and announcements
Wanted, blocked on not having a reliable SMTP server. Members already have addresses in their profile
(decisions 31), so the data is there when the transport is.
Two things to remember when it happens:
- **v1 cannot verify addresses.** Some of them will be typos, and a password reset to a typo'd
address is a locked-out member. The admin already resets passwords, so that is the fallback — but
the reset flow should not assume the address works.
- Announcements need an opt-out, or they become the reason someone stops reading them.
---
## Lyrics with scaled autoscroll
Fetch lyrics and scroll them in time with the audio.
- **LRCLIB** (`lrclib.net`) is a community database with no API key, and its responses include
`syncedLyrics` — real LRC with `[mm:ss.xx]` per-line timestamps — alongside `plainLyrics`. Query by
track, artist and duration, all of which are already on the song row. So a decent share of songs
need no faked timing at all.
- **Synced hit** → highlight the current line properly. **Plain hit or manual paste** → distribute
lines evenly across `duration_seconds` and scroll the block *continuously without highlighting a
line*. Highlighting makes every second of drift read as a bug, and drift is guaranteed — intros and
outros alone break a uniform mapping.
- The Web Animations API does the whole thing including seeking: build the scroll animation with
`duration_seconds`, `pause()` it, and bind `play`/`pause`/`seeked` on the audio element. No timers,
no drift accumulation.
- **Leave a nudge knob** — a ±10 s offset slider, remembered per song in `localStorage`. Uniform
distribution models a song no real song obeys, and one drag while listening beats any heuristic.
- Storage: one nullable `lyrics text` column. LRC or plain — tell them apart by whether the first
line starts with `[`, so no second column and no flag. Fetched best-effort in the publish worker.
- **Add a paste box to the submitter's edit form.** The genre list contains *Finnish*,
*Experimental* and *Just Plain Weird*; LRCLIB will miss nearly all of it, and for those songs the
textarea is the entire feature.
- Copyright posture is the same as the YouTube note: private app, ten people, written down
deliberately.
---
## AI features (Gemma-class local model)
The constraint that shapes every idea: **E2B-class multimodal models are speech models.** The audio
encoder targets ASR and spoken-audio QA. Music is out of distribution — genre calls are near
coin-flips, "describe this track" produces beige copy, and singing over instrumentation is a worst
case for ASR. Encoders also work in ~30 s windows, so a four-minute song is a chunk loop, and on CPU
beside Postgres and ffmpeg that is minutes per submission.
So the ideas that use the model to be *correct* are the weak ones, and the idea that uses it to be
*entertaining* is the strong one:
- **A bot reviewer** — a member account that listens to each published song and posts a score and a
few sentences in character. The model's weaknesses become the feature; nobody files a bug because
the robot was wrong about a friend's doom metal demo. It is a `users` row and a `reviews` row, no
new domain concepts, and the reveal rule already makes its take something you unlock by reviewing —
which quietly rewards the behaviour the whole app exists for. Needs: a sharp persona, a
~3-sentence cap, exclusion from the reviewer leaderboards and from the ≥3-review threshold.
- Weak: genre suggestion (buys one click, and a confident wrong default is worse than an empty
dropdown), auto-drafted introductions (same objection as YouTube descriptions), "is this actually
music" screening (ffmpeg already proves it is audio, and the admin can delete).
- If **duplicate detection** is ever the real want, that is Chromaprint/AcoustID, not an LLM.
- Architecture, when it happens: an Ollama or llama.cpp sidecar called over HTTP from the worker with
a timeout — treated exactly like ffmpeg and yt-dlp, an external process allowed to fail. Go's side
is `net/http` and `encoding/json`, so the dependency budget survives. Never block publish on
inference.
---
## Native client
Deferred; the shape depends entirely on what it is *for*.
- **Launcher icon and media keys** → a PWA. `manifest.json` plus ~10 lines of
`navigator.mediaSession` gets media-key control, and on Linux the browser exposes it over MPRIS, so
the song appears in the GNOME/KDE media widget with artwork. Zero packaging.
- **Playback surviving navigation** → a webview shell, and this is *not* free even then: a webview is
one browser context, so in-app navigation tears down `<audio>` exactly like a tab does. The fixes
are an outer shell page with the app in a same-origin `<iframe>`, fetch-based navigation, or audio
played by the native layer with the webview as UI only.
- **Offline listening** → the only motivation nothing else covers, and a genuinely different project:
download endpoint, local cache management, sync.
- **Not Electron.** It bundles Chromium and Node — ~150 MB on disk, 200400 MB idle RAM, and Node
back in the build, which is the exact dependency surface the rewrite exists to escape. The
system-webview equivalents are ~510 MB: **Wails** (Go, same language as the server) is the pick,
with Tauri as the Rust alternative.
- Packaging inverts with that choice: a plain Go binary → **AppImage** (one `appimagetool` step, one
file to hand out). A webview shell links WebKitGTK, which AppImage bundles badly and the GNOME
**Flatpak** runtime provides for free.
- Worth settling first: the usage pattern this app implies is opening a song, listening, and
reviewing it right there — one page, playback never interrupted. If that is the real behaviour, the
entire rationale reduces to wanting a launcher icon, and that is a `.desktop` file with
`chromium --app=…`.
---
## Custom audio player
Native `<audio controls>` is plain, but it is also correct — keyboard-operable, screen-reader
labelled, media-key aware. Replacing it is cheap and purely client-side: drop the `controls`
attribute, keep the same element, and drive it through `play()`, `pause()`, `currentTime`,
`duration`, and `timeupdate`. Playback, buffering, seeking and Range requests keep working untouched,
and the server changes not at all.
Two things keep it cheap and both are already in v1: the player lives in one template partial, and
`duration_seconds` is on the song row. Skip precomputed waveform peaks — if a SoundCloud-style
waveform is ever wanted, backfilling a couple hundred `.ogg` files is a ten-line script.
When building it, re-earn what the native controls gave away for free: keyboard operation, visible
focus states, and `aria-label`s on the buttons. That is the part custom players routinely drop, and
it is not optional.
---
## Stats not built
Wanted at some point, deliberately out of v1: Top 10 by genre, average given versus received per
member, a reviewer agreement matrix, genre distribution, activity over time. The agreement matrix in
particular is an O(n²) query and a UI nobody has asked for twice.
Also here: pruning the count-based leaderboards once the queue has drained and they are all ties
(decisions 35).
---
## Filters on the browse list
`/songs` is newest-first with no filters. Once there are a couple of hundred songs, "which ones
haven't I reviewed" and "show me the metal" become real questions — one `WHERE` each, additive to the
API contract as query parameters. Worth doing when the scrolling annoys someone, not before.