feat: store word-by-word and translated lyrics

This commit is contained in:
DiheChen committed 2026-11-05 01:02:25 +08:00
1 parent bec757c7a5
commit e2364b171e
9 files changed
+45 -21

No files matched your search

@@ -92,7 +92,7 @@ const roomEntries = `-- name: RoomEntries :many
SELECT e.entry_id, e.slot, e.source, e.track_id, e.title, e.artist, e.album, e.cover_url, e.cover_key, e.page_url, e.meta_duration_ms,
e.by_member_id, e.by_name, e.by_avatar_hash, e.auto, e.likes, e.round, e.added_at, e.started_at,
t.title AS track_title, t.artist AS track_artist, t.album AS track_album, t.cover_url AS track_cover_url,
t.cover_key AS track_cover_key, t.page_url AS track_page_url, t.duration_ms, t.gain, t.lyric
t.cover_key AS track_cover_key, t.page_url AS track_page_url, t.duration_ms, t.gain, t.lyric, t.lyric_words, t.lyric_trans
FROM room_entries e
LEFT JOIN tracks t ON t.source = e.source AND t.track_id = e.track_id
WHERE e.room_id = $1
@@ -128,6 +128,8 @@ type RoomEntriesRow struct {
DurationMs *int64
Gain *float64
Lyric *string
LyricWords *string
LyricTrans *string
}
func (q *Queries) RoomEntries(ctx context.Context, roomID string) ([]RoomEntriesRow, error) {
@@ -168,6 +170,8 @@ func (q *Queries) RoomEntries(ctx context.Context, roomID string) ([]RoomEntries
&i.DurationMs,
&i.Gain,
&i.Lyric,
&i.LyricWords,
&i.LyricTrans,
); err != nil {
return nil, err
}
@@ -10,7 +10,7 @@ import (
)
const preparedTrack = `-- name: PreparedTrack :one
SELECT title, artist, album, cover_url, cover_key, page_url, duration_ms, gain, lyric, media_key, media_size FROM tracks
SELECT title, artist, album, cover_url, cover_key, page_url, duration_ms, gain, lyric, lyric_words, lyric_trans, media_key, media_size FROM tracks
WHERE source = $1 AND track_id = $2
`
@@ -29,6 +29,8 @@ type PreparedTrackRow struct {
DurationMs int64
Gain float64
Lyric *string
LyricWords *string
LyricTrans *string
MediaKey *string
MediaSize *int64
}
@@ -46,6 +48,8 @@ func (q *Queries) PreparedTrack(ctx context.Context, arg PreparedTrackParams) (P
&i.DurationMs,
&i.Gain,
&i.Lyric,
&i.LyricWords,
&i.LyricTrans,
&i.MediaKey,
&i.MediaSize,
)
@@ -53,12 +57,13 @@ func (q *Queries) PreparedTrack(ctx context.Context, arg PreparedTrackParams) (P
}
const savePreparedTrack = `-- name: SavePreparedTrack :exec
INSERT INTO tracks (source, track_id, title, artist, album, cover_url, cover_key, page_url, duration_ms, gain, lyric, media_key, media_size)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
INSERT INTO tracks (source, track_id, title, artist, album, cover_url, cover_key, page_url, duration_ms, gain, lyric, lyric_words, lyric_trans, media_key, media_size)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
ON CONFLICT (source, track_id) DO UPDATE SET
title = excluded.title, artist = excluded.artist, album = excluded.album, cover_url = excluded.cover_url,
cover_key = excluded.cover_key, page_url = excluded.page_url, duration_ms = excluded.duration_ms, gain = excluded.gain,
lyric = excluded.lyric, media_key = excluded.media_key, media_size = excluded.media_size,
lyric = excluded.lyric, lyric_words = excluded.lyric_words, lyric_trans = excluded.lyric_trans,
media_key = excluded.media_key, media_size = excluded.media_size,
prepared_at = now()
`
@@ -74,6 +79,8 @@ type SavePreparedTrackParams struct {
DurationMs int64
Gain float64
Lyric *string
LyricWords *string
LyricTrans *string
MediaKey *string
MediaSize *int64
}
@@ -92,6 +99,8 @@ func (q *Queries) SavePreparedTrack(ctx context.Context, arg SavePreparedTrackPa
arg.DurationMs,
arg.Gain,
arg.Lyric,
arg.LyricWords,
arg.LyricTrans,
arg.MediaKey,
arg.MediaSize,
)
+1 -1
View File
@@ -153,7 +153,7 @@ func entryOf(r query.RoomEntriesRow) room.Entry {
// 条目中仍是点歌时的元数据,比如链接曲目的标题仍是链接本身。
if r.DurationMs != nil {
e.Ready = true
e.Meta, e.Prep = prepared(*r.TrackTitle, orEmpty(r.TrackArtist), orEmpty(r.TrackAlbum), orEmpty(r.TrackCoverURL), orEmpty(r.TrackCoverKey), *r.TrackPageURL, *r.DurationMs, *r.Gain, orEmpty(r.Lyric))
e.Meta, e.Prep = prepared(*r.TrackTitle, orEmpty(r.TrackArtist), orEmpty(r.TrackAlbum), orEmpty(r.TrackCoverURL), orEmpty(r.TrackCoverKey), *r.TrackPageURL, *r.DurationMs, *r.Gain, orEmpty(r.Lyric), orEmpty(r.LyricWords), orEmpty(r.LyricTrans))
}
return e
}
@@ -72,7 +72,7 @@ func TestSavedSnapshotIsRestored(t *testing.T) {
id := createRoom(t, store, port.NewRoom{Kind: room.Persistent, Name: "大厅"})
term, _ := takeRoom(t, store, id)
playing := track.Prepared{Duration: 3 * time.Minute, Gain: 0.8, Lyric: "[00:00.00]歌词"}
playing := track.Prepared{Duration: 3 * time.Minute, Gain: 0.8, Lyric: "[00:00.00]歌词", LyricWords: "[0,500](0,250,0)歌(250,250,0)词", LyricTrans: "[00:00.00]Lyrics"}
queued := track.Prepared{Duration: 2*time.Minute + 500*time.Millisecond, Gain: 1.2}
link := track.Track{Source: track.Link, ID: "https://example.com/a.mp3"}
meta := track.Meta{Title: "歌", Artist: "人", Album: "专辑", CoverURL: "https://p.example/c.jpg", PageURL: "https://music.163.com/#/song?id=1", Duration: playing.Duration}
+5 -3
View File
@@ -23,7 +23,7 @@ func (s *Store) PreparedTrack(ctx context.Context, t track.Track) (port.Prepared
if err != nil {
return port.PreparedTrack{}, false, fmt.Errorf("读取曲目 %s %s 的准备结果:%w", t.Source, t.ID, err)
}
meta, result := prepared(row.Title, orEmpty(row.Artist), orEmpty(row.Album), orEmpty(row.CoverURL), orEmpty(row.CoverKey), row.PageURL, row.DurationMs, row.Gain, orEmpty(row.Lyric))
meta, result := prepared(row.Title, orEmpty(row.Artist), orEmpty(row.Album), orEmpty(row.CoverURL), orEmpty(row.CoverKey), row.PageURL, row.DurationMs, row.Gain, orEmpty(row.Lyric), orEmpty(row.LyricWords), orEmpty(row.LyricTrans))
return port.PreparedTrack{Meta: meta, Result: result, MediaKey: orEmpty(row.MediaKey), MediaSize: orZero(row.MediaSize)}, true, nil
}
@@ -40,6 +40,8 @@ func (s *Store) SavePreparedTrack(ctx context.Context, t track.Track, p port.Pre
DurationMs: p.Result.Duration.Milliseconds(),
Gain: p.Result.Gain,
Lyric: nullable(p.Result.Lyric),
LyricWords: nullable(p.Result.LyricWords),
LyricTrans: nullable(p.Result.LyricTrans),
}
if p.MediaKey != "" {
params.MediaKey, params.MediaSize = &p.MediaKey, &p.MediaSize
@@ -50,10 +52,10 @@ func (s *Store) SavePreparedTrack(ctx context.Context, t track.Track, p port.Pre
return nil
}
func prepared(title, artist, album, coverURL, coverKey, pageURL string, durationMs int64, gain float64, lyric string) (track.Meta, track.Prepared) {
func prepared(title, artist, album, coverURL, coverKey, pageURL string, durationMs int64, gain float64, lyric, lyricWords, lyricTrans string) (track.Meta, track.Prepared) {
d := time.Duration(durationMs) * time.Millisecond
return track.Meta{Title: title, Artist: artist, Album: album, CoverURL: coverURL, CoverKey: track.ObjectKey(coverKey), PageURL: pageURL, Duration: d},
track.Prepared{Duration: d, Gain: gain, Lyric: lyric}
track.Prepared{Duration: d, Gain: gain, Lyric: lyric, LyricWords: lyricWords, LyricTrans: lyricTrans}
}
func orZero(p *int64) int64 {
+10 -6
View File
@@ -21,7 +21,10 @@ func TestPreparedTrackRoundTrip(t *testing.T) {
Title: "晴天", Artist: "周杰伦", Album: "叶惠美", CoverURL: "https://y.gtimg.cn/c.jpg",
PageURL: "https://y.qq.com/n/ryqq/songDetail/0039MnYb0qxYhV", Duration: 269 * time.Second,
},
Result: track.Prepared{Duration: 269500 * time.Millisecond, Gain: 0.75, Lyric: "[00:01.00]故事的小黄花"},
Result: track.Prepared{
Duration: 269500 * time.Millisecond, Gain: 0.75, Lyric: "[00:01.00]故事的小黄花",
LyricWords: "[1000,500](1000,250,0)故(1250,250,0)事", LyricTrans: "[00:01.00]The story",
},
}
if err := store.SavePreparedTrack(t.Context(), song, saved); err != nil {
t.Fatal(err)
@@ -40,12 +43,12 @@ func TestPreparedTrackRoundTrip(t *testing.T) {
func TestSavePreparedTrackOverwritesResult(t *testing.T) {
store, pool := newStore(t)
song := netease("1")
addTrack(t, store, song, track.Meta{Title: "旧"}, track.Prepared{Duration: time.Minute, Gain: 1})
addTrack(t, store, song, track.Meta{Title: "旧"}, track.Prepared{Duration: time.Minute, Gain: 1, LyricWords: "旧逐字", LyricTrans: "旧翻译"})
id := createRoom(t, store, port.NewRoom{Kind: room.Persistent, Name: "大厅"})
term, snap := takeRoom(t, store, id)
saveRoom(t, store, term, snap, room.RecordPlay{Entry: room.Entry{ID: 1, Track: song, By: alice}, StartedAt: t0, EndedAt: t0.Add(time.Minute)})
again := port.PreparedTrack{Meta: track.Meta{Title: "新"}, Result: track.Prepared{Duration: 2 * time.Minute, Gain: 0.5, Lyric: "词"}}
again := port.PreparedTrack{Meta: track.Meta{Title: "新"}, Result: track.Prepared{Duration: 2 * time.Minute, Gain: 0.5, Lyric: "词", LyricWords: "新逐字", LyricTrans: "新翻译"}}
addTrack(t, store, song, again.Meta, again.Result)
got, _, err := store.PreparedTrack(t.Context(), song)
if err != nil {
@@ -75,13 +78,14 @@ func TestPreparedTrackStoresMissingValuesAsNull(t *testing.T) {
}
var missing string
err := pool.QueryRow(t.Context(), `SELECT concat_ws(',', CASE WHEN artist IS NULL THEN 'artist' END, CASE WHEN album IS NULL THEN 'album' END,
CASE WHEN cover_url IS NULL THEN 'cover_url' END, CASE WHEN cover_key IS NULL THEN 'cover_key' END, CASE WHEN lyric IS NULL THEN 'lyric' END)
CASE WHEN cover_url IS NULL THEN 'cover_url' END, CASE WHEN cover_key IS NULL THEN 'cover_key' END, CASE WHEN lyric IS NULL THEN 'lyric' END,
CASE WHEN lyric_words IS NULL THEN 'lyric_words' END, CASE WHEN lyric_trans IS NULL THEN 'lyric_trans' END)
FROM tracks WHERE track_id = '1'`).Scan(&missing)
if err != nil {
t.Fatal(err)
}
if missing != "artist,album,cover_url,cover_key,lyric" {
t.Errorf("为 NULL 的列 = %s,期望 artist,album,cover_url,cover_key,lyric", missing)
if want := "artist,album,cover_url,cover_key,lyric,lyric_words,lyric_trans"; missing != want {
t.Errorf("为 NULL 的列 = %s,期望 %s", missing, want)
}
if got, ok, err := store.PreparedTrack(t.Context(), song); err != nil || !ok || got != saved {
t.Errorf("读出 %#v, %v, %v\n期望 %#v", got, ok, err, saved)