feat: store word-by-word and translated lyrics
This commit is contained in:
1 parent
bec757c7a5
commit
e2364b171e
9 files changed
+45
-21
No files matched your search
@@ -0,0 +1,4 @@
|
||||
-- +goose Up
|
||||
ALTER TABLE tracks
|
||||
ADD COLUMN lyric_words text,
|
||||
ADD COLUMN lyric_trans text;
|
||||
@@ -5,7 +5,7 @@ UPDATE rooms SET term = term + 1 WHERE id = @id RETURNING *;
|
||||
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 = @room_id
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
-- 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 = @source AND track_id = @track_id;
|
||||
|
||||
-- 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 (@source, @track_id, @title, @artist, @album, @cover_url, @cover_key, @page_url, @duration_ms, @gain, @lyric, @media_key, @media_size)
|
||||
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 (@source, @track_id, @title, @artist, @album, @cover_url, @cover_key, @page_url, @duration_ms, @gain, @lyric, @lyric_words, @lyric_trans, @media_key, @media_size)
|
||||
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();
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user