Release 2026.08.01-1
Lyrics, versioning, and the song page's fact line. - Lyrics are suggested at submission and never imposed: the worker makes one LRCLIB lookup, and Hae sanoitukset re-queries with whatever title and artist are typed. Neither overwrites what the submitter wrote. They live on the submission, travel to the song at publish, and stay editable after the song locks — the lock freezes what a song claims to be, and nobody reviewed the lyrics - The review strip reads and writes side by side: lyrics left, review right. Synced LRC highlights the playing line and seeks on click; plain text scrolls continuously with a nudge knob. Following can be turned off - CalVer YYYY.MM.DD-N, injected from the git tag with -ldflags, shown in the footer, the startup log and /healthz - The song page's metadata became four labelled cells instead of one flat run of five different kinds of fact Fixes: publishing wiped lyrics the worker had just fetched (a request that omitted a field cleared it), lyric auto-scroll landed in the wrong place, and the fader shifted the deck sideways at score 100.
This commit is contained in:
@@ -84,6 +84,7 @@ type submission struct {
|
||||
Artist string
|
||||
Genre string
|
||||
Description string
|
||||
Lyrics string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
@@ -376,6 +377,21 @@ func (a *app) process(subID int64, sourceURL, src string) {
|
||||
return
|
||||
}
|
||||
slog.Info("conversion ready", "ctx", "submissions", "submission", subID)
|
||||
|
||||
// One automatic lyrics attempt, after the audio is safe. It runs on whatever metadata exists,
|
||||
// so it covers well-tagged music; the Hae sanoitukset button on the waiting page is what
|
||||
// covers everything else, once the submitter has fixed the title and artist.
|
||||
var title, artist string
|
||||
if err := a.pool.QueryRow(ctx,
|
||||
`select coalesce(title, ''), coalesce(artist, '') from submissions where id = $1`,
|
||||
subID).Scan(&title, &artist); err != nil {
|
||||
return
|
||||
}
|
||||
seconds := 0
|
||||
if meta, err := probe(ctx, out); err == nil {
|
||||
seconds = int(meta.Duration.Seconds())
|
||||
}
|
||||
a.autoFetchLyrics(ctx, subID, title, artist, seconds)
|
||||
}
|
||||
|
||||
func (a *app) setStatus(ctx context.Context, subID int64, status, msg string) {
|
||||
@@ -399,10 +415,10 @@ func (a *app) loadSubmission(w http.ResponseWriter, r *http.Request) *submission
|
||||
err = a.pool.QueryRow(r.Context(), `
|
||||
select id, user_id, status, status_msg, source_url, coalesce(tmp_path, ''),
|
||||
coalesce(title, ''), coalesce(artist, ''), coalesce(genre, ''),
|
||||
coalesce(description, ''), created_at
|
||||
coalesce(description, ''), coalesce(lyrics, ''), created_at
|
||||
from submissions where id = $1`, id).
|
||||
Scan(&s.ID, &s.UserID, &s.Status, &s.StatusMsg, &s.SourceURL, &s.TmpPath,
|
||||
&s.Title, &s.Artist, &s.Genre, &s.Description, &s.CreatedAt)
|
||||
&s.Title, &s.Artist, &s.Genre, &s.Description, &s.Lyrics, &s.CreatedAt)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
http.NotFound(w, r)
|
||||
return nil
|
||||
@@ -444,19 +460,34 @@ func (a *app) submissionStatus(w http.ResponseWriter, r *http.Request) {
|
||||
// no save button: HTMX posts here after a pause in typing, and pressing Julkaise posts the same
|
||||
// fields to publish, so a browser without JS loses nothing.
|
||||
func (a *app) saveMetadata(ctx context.Context, subID int64, r *http.Request) error {
|
||||
// r.Form is only populated once the body has been parsed, and the check below reads it.
|
||||
if err := r.ParseForm(); err != nil {
|
||||
return err
|
||||
}
|
||||
genre := r.FormValue("genre")
|
||||
if genre != "" && !validGenre(genre) {
|
||||
return fmt.Errorf("unknown genre %q", genre)
|
||||
}
|
||||
// A field the request does not carry keeps its stored value. Without this, any post that omits
|
||||
// a field silently clears it — which is exactly how a publish request wiped lyrics that the
|
||||
// worker had just fetched.
|
||||
has := func(field string) bool { _, ok := r.Form[field]; return ok }
|
||||
|
||||
_, err := a.pool.Exec(ctx, `
|
||||
update submissions set title = nullif($2, ''), artist = nullif($3, ''),
|
||||
genre = nullif($4, ''), description = nullif($5, '')
|
||||
update submissions set
|
||||
title = case when $2 then nullif($3, '') else title end,
|
||||
artist = case when $4 then nullif($5, '') else artist end,
|
||||
genre = case when $6 then nullif($7, '') else genre end,
|
||||
description = case when $8 then nullif($9, '') else description end,
|
||||
lyrics = case when $10 then nullif($11, '') else lyrics end
|
||||
where id = $1`,
|
||||
subID,
|
||||
clean(r.FormValue("title"), maxTitle),
|
||||
clean(r.FormValue("artist"), maxArtist),
|
||||
genre,
|
||||
clean(r.FormValue("description"), maxDescription))
|
||||
has("title"), clean(r.FormValue("title"), maxTitle),
|
||||
has("artist"), clean(r.FormValue("artist"), maxArtist),
|
||||
has("genre"), genre,
|
||||
has("description"), clean(r.FormValue("description"), maxDescription),
|
||||
// Line breaks are the whole point of lyrics, so they survive rather than being cleaned away.
|
||||
has("lyrics"), cleanLyrics(r.FormValue("lyrics")))
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -536,10 +567,11 @@ func (a *app) publish(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var songID int64
|
||||
err = tx.QueryRow(r.Context(), `
|
||||
insert into songs (title, artist, genre, description, audio_file, duration_seconds,
|
||||
insert into songs (title, artist, genre, description, lyrics, audio_file, duration_seconds,
|
||||
source_url, submitted_by)
|
||||
values ($1, $2, $3, $4, '', $5, $6, $7) returning id`,
|
||||
values ($1, $2, $3, $4, $5, '', $6, $7, $8) returning id`,
|
||||
title, artist, genre, nilIfEmpty(clean(s.Description, maxDescription)),
|
||||
nilIfEmpty(cleanLyrics(s.Lyrics)),
|
||||
int(meta.Duration.Seconds()), s.SourceURL, s.UserID).Scan(&songID)
|
||||
if err != nil {
|
||||
slog.Error("insert song", "ctx", "songs", "error", err, "submission", s.ID)
|
||||
|
||||
Reference in New Issue
Block a user