add migration, more tests

This commit is contained in:
Kendall Garner 2023-12-09 21:58:50 -08:00
commit 40d29f2cb1
No known key found for this signature in database
GPG key ID: 18D2767419676C87
8 changed files with 261 additions and 16 deletions

View file

@ -0,0 +1,77 @@
package migrations
import (
"context"
"database/sql"
"encoding/json"
"github.com/navidrome/navidrome/model"
"github.com/pressly/goose/v3"
)
func init() {
goose.AddMigrationContext(upAlterLyricColumn, downAlterLyricColumn)
}
func upAlterLyricColumn(ctx context.Context, tx *sql.Tx) error {
_, err := tx.ExecContext(ctx, `alter table media_file rename COLUMN lyrics TO lyrics_old`)
if err != nil {
return err
}
_, err = tx.ExecContext(ctx, `alter table media_file add lyrics JSONB default '[]';`)
if err != nil {
return err
}
stmt, err := tx.Prepare(`update media_file SET lyrics = ? where id = ?`)
if err != nil {
return err
}
rows, err := tx.Query(`select id, lyrics_old FROM media_file WHERE lyrics_old <> '';`)
if err != nil {
return err
}
var id, lyrics string
for rows.Next() {
err = rows.Scan(&id, &lyrics)
if err != nil {
return err
}
lyrics, err := model.ToLyrics("xxx", lyrics)
if err != nil {
return err
}
text, err := json.Marshal(model.Lyrics{*lyrics})
if err != nil {
return err
}
_, err = stmt.Exec(string(text[:]), id)
if err != nil {
return err
}
}
err = rows.Err()
if err != nil {
return err
}
_, err = tx.ExecContext(ctx, `ALTER TABLE media_file DROP COLUMN lyrics_old;`)
if err != nil {
return err
}
notice(tx, "A full rescan will be performed to pick up additional lyrics (existing lyrics have been preserved)")
return forceFullRescan(tx)
}
func downAlterLyricColumn(ctx context.Context, tx *sql.Tx) error {
// This code is executed when the migration is rolled back.
return nil
}

View file

@ -5,6 +5,7 @@ import (
"strconv"
"strings"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/utils"
)
@ -26,7 +27,7 @@ type Lyric struct {
const timeRegexString = `(\[(([0-9]{1,2}):)?([0-9]{1,2}):([0-9]{1,2})(\.([0-9]{1,3}))?\])`
var (
lineRegex = regexp.MustCompile(timeRegexString + "([^\n]+)")
lineRegex = regexp.MustCompile(timeRegexString + "([^\n]+)?")
lrcIdRegex = regexp.MustCompile(`\[(ar|ti|offset):([^\]]+)\]`)
)
@ -54,18 +55,18 @@ func ToLyrics(language, text string) (*Lyric, error) {
if idTag != nil {
switch idTag[1] {
case "ar":
artist = idTag[2]
artist = utils.SanitizeText(strings.TrimSpace(idTag[2]))
case "offset":
{
off, err := strconv.ParseInt(idTag[2], 10, 64)
off, err := strconv.ParseInt(strings.TrimSpace(idTag[2]), 10, 64)
if err != nil {
return nil, err
log.Warn("Error parsing offset", "offset", idTag[2], "error", err)
} else {
offset = &off
}
offset = &off
}
case "ti":
title = idTag[2]
title = utils.SanitizeText(strings.TrimSpace(idTag[2]))
}
continue
@ -74,9 +75,9 @@ func ToLyrics(language, text string) (*Lyric, error) {
syncedMatch := lineRegex.FindStringSubmatch(line)
if syncedMatch == nil {
synced = false
text = line
text = utils.SanitizeText(line)
} else {
var hours int64
var hours, millis int64
var err error
if syncedMatch[3] != "" {
@ -96,18 +97,20 @@ func ToLyrics(language, text string) (*Lyric, error) {
return nil, err
}
millis, err := strconv.ParseInt(syncedMatch[7], 10, 64)
if err != nil {
return nil, err
}
if syncedMatch[7] != "" {
millis, err = strconv.ParseInt(syncedMatch[7], 10, 64)
if err != nil {
return nil, err
}
if len(syncedMatch[7]) == 2 {
millis *= 10
if len(syncedMatch[7]) == 2 {
millis *= 10
}
}
timeInMillis := (((((hours * 60) + min) * 60) + sec) * 1000) + millis
time = &timeInMillis
text = syncedMatch[8]
text = utils.SanitizeText(syncedMatch[8])
}
} else {
text = line

38
model/lyrics_test.go Normal file
View file

@ -0,0 +1,38 @@
package model_test
import (
. "github.com/navidrome/navidrome/model"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("ToLyrics", func() {
num := int64(1551)
It("should parse tags with spaces", func() {
lyrics, err := ToLyrics("xxx", "[offset: 1551 ]\n[ti: A title ]\n[ar: An artist ]\n[00:00.00]Hi there")
Expect(err).ToNot(HaveOccurred())
Expect(lyrics.DisplayArtist).To(Equal("An artist"))
Expect(lyrics.DisplayTitle).To(Equal("A title"))
Expect(lyrics.Offset).To(Equal(&num))
})
It("Should ignore bad offset", func() {
lyrics, err := ToLyrics("xxx", "[offset: NotANumber ]\n[00:00.00]Hi there")
Expect(err).ToNot(HaveOccurred())
Expect(lyrics.Offset).To(BeNil())
})
It("should accept lines with no text and weird times", func() {
var a, b, c, d = int64(0), int64(10040), int64(40000), int64(1000 * 60 * 60)
lyrics, err := ToLyrics("xxx", "[00:00.00]Hi there\n\n\n[00:10.040] \n[00:40]Test\n[01:00:00]late")
Expect(err).ToNot(HaveOccurred())
Expect(lyrics.Synced).To(BeTrue())
Expect(lyrics.Line).To(Equal([]Line{
{Start: &a, Value: "Hi there"},
{Start: &b, Value: ""},
{Start: &c, Value: "Test"},
{Start: &d, Value: "late"},
}))
})
})

View file

@ -0,0 +1,37 @@
{
"status": "ok",
"version": "1.8.0",
"type": "navidrome",
"serverVersion": "v0.0.0",
"openSubsonic": true,
"lyricsList": {
"structuredLyrics": [
{
"lang": "eng",
"line": [
{
"start": 18800,
"value": "We're no strangers to love"
},
{
"start": 22801,
"value": "You know the rules and so do I"
}
],
"synced": true
},
{
"lang": "xxx",
"line": [
{
"value": "We're no strangers to love"
},
{
"value": "You know the rules and so do I"
}
],
"synced": false
}
]
}
}

View file

@ -0,0 +1,20 @@
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0" openSubsonic="true">
<lyricsList>
<structuredLyrics lang="eng" synced="true">
<line start="18800">
<value>We&#39;re no strangers to love</value>
</line>
<line start="22801">
<value>You know the rules and so do I</value>
</line>
</structuredLyrics>
<structuredLyrics lang="xxx" synced="false">
<line>
<value>We&#39;re no strangers to love</value>
</line>
<line>
<value>You know the rules and so do I</value>
</line>
</structuredLyrics>
</lyricsList>
</subsonic-response>

View file

@ -0,0 +1,8 @@
{
"status": "ok",
"version": "1.8.0",
"type": "navidrome",
"serverVersion": "v0.0.0",
"openSubsonic": true,
"lyricsList": {}
}

View file

@ -0,0 +1,3 @@
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0" openSubsonic="true">
<lyricsList></lyricsList>
</subsonic-response>

View file

@ -11,6 +11,7 @@ import (
"time"
"github.com/navidrome/navidrome/consts"
"github.com/navidrome/navidrome/model"
. "github.com/navidrome/navidrome/server/subsonic/responses"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
@ -795,4 +796,62 @@ var _ = Describe("Responses", func() {
})
})
})
Describe("getLyricsBySongId", func() {
BeforeEach(func() {
response.LyricsList = &LyricsList{}
})
Describe("without data", func() {
It("should match .XML", func() {
Expect(xml.MarshalIndent(response, "", " ")).To(MatchSnapshot())
})
It("should match .JSON", func() {
Expect(json.MarshalIndent(response, "", " ")).To(MatchSnapshot())
})
})
Describe("with data", func() {
BeforeEach(func() {
times := []int64{int64(18800), int64(22801)}
response.LyricsList.StructuredLyrics = model.Lyrics{
{
Lang: "eng",
Synced: true,
Line: []model.Line{
{
Start: &times[0],
Value: "We're no strangers to love",
},
{
Start: &times[1],
Value: "You know the rules and so do I",
},
},
},
{
Lang: "xxx",
Synced: false,
Line: []model.Line{
{
Value: "We're no strangers to love",
},
{
Value: "You know the rules and so do I",
},
},
},
}
})
It("should match .XML", func() {
Expect(xml.MarshalIndent(response, "", " ")).To(MatchSnapshot())
})
It("should match .JSON", func() {
Expect(json.MarshalIndent(response, "", " ")).To(MatchSnapshot())
})
})
})
})