Synced LRC highlights the playing line, keeps it centred and seeks on click.
Plain text scrolls continuously with a nudge knob instead — a highlight on
guessed timings turns guaranteed drift into what looks like a bug. Seuraa
kappaletta turns following off without losing the highlight, and scrolling by
hand turns it off too.
Fixes the scroll landing in the wrong place (offsetTop measured from a
different coordinate space than the box it was applied to) and the fader
shifting the deck sideways at score 100 (auto-sized grid columns plus a
readout spanning both).
Lyrics are suggested at submission and never imposed. The conversion worker
makes one LRCLIB lookup with whatever metadata exists, and the waiting page
has a Hae sanoitukset button that re-queries with whatever title and artist
are currently typed — which is the case that matters, since our metadata comes
from ID3 tags and YouTube uploaders. Neither path overwrites typed text.
- lyrics text on both submissions and songs, copied across at publish. Nothing
has launched, so the column goes into 001_init.sql rather than a migration
- The lock does not cover lyrics: it freezes what the song claims to be, and
nobody reviewed the lyrics. So the submitter can still fix them afterwards,
or paste them for an old song a year later
- The review strip gained a second pane: lyrics on the left, review on the
right, so following the words costs no scrolling. No lyrics means no pane,
not an empty one. Below 1024px the panes stack
- LRC timestamps are stored but stripped for reading — they belong to the
player, not the reader
- The lyrics box is capped and scrolls inside itself, so a long song cannot
stretch the strip past the screen
Fixes a real bug found on the way: saveMetadata cleared any field the request
did not carry, so publishing wiped the lyrics the worker had just fetched.
Fields absent from a request now keep their stored value.
The client identifies itself to LRCLIB as "levyraati" and nothing more.
Tests cover cleanLyrics keeping line breaks, and fetchLyrics against a local
server: synced beats plain, instrumentals and wrong-length takes are skipped,
and a miss is empty with no error.
The app is about operating something — playing a track and setting a level on
it — but every screen looked like a form. One metaphor now does three jobs.
- Reviewing: a vertical fader beside the text, so the two things you do at
once stop being a screen apart. Native range input, so keyboard, focus and
form submission are unchanged; on mobile it lies down and the ticks reverse
- The reveal: everyone's scores as a row of channels. The silhouette of that
row is the spread, which the stats page can only tell you as a number
- Profiles: given versus received as two faders, the one comparison that says
something about a person
The player is now a transport: play/pause, a range input for seeking so arrow
keys come free, and a stereo level meter driven by a real AnalyserNode. It is
progressive enhancement — the page ships native audio controls and the script
takes over, so no JS means the browser's own player. The meter is dark until
audio actually plays and stops when it does; reduced motion skips it entirely.
Also: hidden scores are hatched rather than blank, the nav carries the queue
count, "Seuraava jonossa" keeps the loop going after a review, leaderboards
gained level bars and a range bar where divisive is the point, durations read
3:54, both lists can get back to the start, and the admin invite table lists
unused codes instead of silently truncating at 50.
Slogan restored from the original app, three decades on.
Step 6. The surfaces around the review loop.
- Nine leaderboards, ordered and limited in SQL, each with a deterministic
tie-break so a tied board doesn't reshuffle between reloads. Min 3 reviews
to qualify, for reviewer boards too
- Profiles show counts and history-wide averages and the member's songs,
never a list of their reviews — per-song opinion stays gated
- Avatars: 5MB in, 256px JPEG out, ffmpeg's re-encode being the validation.
No upload still means initials, and avatars are public
- Changing your own password requires the current one and drops your other
sessions
- Palaute: free text plus the page you were on, carried in a footer link, and
the user agent from the header. Reporters see their own; the admin resolves
them with a timestamp rather than a status enum
- Admin gained the song list with delete, the reports page, and an open-report
count on the dashboard
Two theme fixes the screenshots caught: leaderboard ranks need a CSS counter
because display:grid suppresses list markers, and count-based boards were
printing 3.0 where they mean 3.
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.