mirror of
https://github.com/navidrome/navidrome.git
synced 2026-04-03 06:41:01 +00:00
feat:(server): support reading lyrics from filesystem (#2897)
* simplified lyrics handling * address initial feedback * add some trace and error logging * allow fallback lyrics * update nit * restore artist/title filter only
This commit is contained in:
parent
0d1f2bcc8a
commit
ec9f9aa243
@ -93,6 +93,7 @@ type configOptions struct {
|
|||||||
PID pidOptions
|
PID pidOptions
|
||||||
Inspect inspectOptions
|
Inspect inspectOptions
|
||||||
Subsonic subsonicOptions
|
Subsonic subsonicOptions
|
||||||
|
LyricsPriority string
|
||||||
|
|
||||||
Agents string
|
Agents string
|
||||||
LastFM lastfmOptions
|
LastFM lastfmOptions
|
||||||
@ -528,6 +529,8 @@ func init() {
|
|||||||
viper.SetDefault("inspect.backloglimit", consts.RequestThrottleBacklogLimit)
|
viper.SetDefault("inspect.backloglimit", consts.RequestThrottleBacklogLimit)
|
||||||
viper.SetDefault("inspect.backlogtimeout", consts.RequestThrottleBacklogTimeout)
|
viper.SetDefault("inspect.backlogtimeout", consts.RequestThrottleBacklogTimeout)
|
||||||
|
|
||||||
|
viper.SetDefault("lyricspriority", ".lrc,.txt,embedded")
|
||||||
|
|
||||||
// DevFlags. These are used to enable/disable debugging and incomplete features
|
// DevFlags. These are used to enable/disable debugging and incomplete features
|
||||||
viper.SetDefault("devlogsourceline", false)
|
viper.SetDefault("devlogsourceline", false)
|
||||||
viper.SetDefault("devenableprofiler", false)
|
viper.SetDefault("devenableprofiler", false)
|
||||||
|
|||||||
37
core/lyrics/lyrics.go
Normal file
37
core/lyrics/lyrics.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package lyrics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/navidrome/navidrome/conf"
|
||||||
|
"github.com/navidrome/navidrome/log"
|
||||||
|
"github.com/navidrome/navidrome/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetLyrics(ctx context.Context, mf *model.MediaFile) (model.LyricList, error) {
|
||||||
|
var lyricsList model.LyricList
|
||||||
|
var err error
|
||||||
|
|
||||||
|
for pattern := range strings.SplitSeq(strings.ToLower(conf.Server.LyricsPriority), ",") {
|
||||||
|
pattern = strings.TrimSpace(pattern)
|
||||||
|
switch {
|
||||||
|
case pattern == "embedded":
|
||||||
|
lyricsList, err = fromEmbedded(ctx, mf)
|
||||||
|
case strings.HasPrefix(pattern, "."):
|
||||||
|
lyricsList, err = fromExternalFile(ctx, mf, pattern)
|
||||||
|
default:
|
||||||
|
log.Error(ctx, "Invalid lyric pattern", "pattern", pattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Error(ctx, "error parsing lyrics", "source", pattern, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(lyricsList) > 0 {
|
||||||
|
return lyricsList, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
17
core/lyrics/lyrics_suite_test.go
Normal file
17
core/lyrics/lyrics_suite_test.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package lyrics_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/navidrome/navidrome/log"
|
||||||
|
"github.com/navidrome/navidrome/tests"
|
||||||
|
. "github.com/onsi/ginkgo/v2"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLyrics(t *testing.T) {
|
||||||
|
tests.Init(t, false)
|
||||||
|
log.SetLevel(log.LevelFatal)
|
||||||
|
RegisterFailHandler(Fail)
|
||||||
|
RunSpecs(t, "Lyrics Suite")
|
||||||
|
}
|
||||||
124
core/lyrics/lyrics_test.go
Normal file
124
core/lyrics/lyrics_test.go
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
package lyrics_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/navidrome/navidrome/conf"
|
||||||
|
"github.com/navidrome/navidrome/conf/configtest"
|
||||||
|
"github.com/navidrome/navidrome/core/lyrics"
|
||||||
|
"github.com/navidrome/navidrome/model"
|
||||||
|
"github.com/navidrome/navidrome/utils"
|
||||||
|
"github.com/navidrome/navidrome/utils/gg"
|
||||||
|
. "github.com/onsi/ginkgo/v2"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = Describe("sources", func() {
|
||||||
|
var mf model.MediaFile
|
||||||
|
var ctx context.Context
|
||||||
|
|
||||||
|
const badLyrics = "This is a set of lyrics\nThat is not good"
|
||||||
|
unsynced, _ := model.ToLyrics("xxx", badLyrics)
|
||||||
|
embeddedLyrics := model.LyricList{*unsynced}
|
||||||
|
|
||||||
|
syncedLyrics := model.LyricList{
|
||||||
|
model.Lyrics{
|
||||||
|
DisplayArtist: "Rick Astley",
|
||||||
|
DisplayTitle: "That one song",
|
||||||
|
Lang: "eng",
|
||||||
|
Line: []model.Line{
|
||||||
|
{
|
||||||
|
Start: gg.P(int64(18800)),
|
||||||
|
Value: "We're no strangers to love",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Start: gg.P(int64(22801)),
|
||||||
|
Value: "You know the rules and so do I",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Offset: gg.P(int64(-100)),
|
||||||
|
Synced: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
unsyncedLyrics := model.LyricList{
|
||||||
|
model.Lyrics{
|
||||||
|
Lang: "xxx",
|
||||||
|
Line: []model.Line{
|
||||||
|
{
|
||||||
|
Value: "We're no strangers to love",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Value: "You know the rules and so do I",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Synced: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
DeferCleanup(configtest.SetupConfig())
|
||||||
|
|
||||||
|
lyricsJson, _ := json.Marshal(embeddedLyrics)
|
||||||
|
|
||||||
|
mf = model.MediaFile{
|
||||||
|
Lyrics: string(lyricsJson),
|
||||||
|
Path: "tests/fixtures/test.mp3",
|
||||||
|
}
|
||||||
|
ctx = context.Background()
|
||||||
|
})
|
||||||
|
|
||||||
|
DescribeTable("Lyrics Priority", func(priority string, expected model.LyricList) {
|
||||||
|
conf.Server.LyricsPriority = priority
|
||||||
|
list, err := lyrics.GetLyrics(ctx, &mf)
|
||||||
|
Expect(err).To(BeNil())
|
||||||
|
Expect(list).To(Equal(expected))
|
||||||
|
},
|
||||||
|
Entry("embedded > lrc > txt", "embedded,.lrc,.txt", embeddedLyrics),
|
||||||
|
Entry("lrc > embedded > txt", ".lrc,embedded,.txt", syncedLyrics),
|
||||||
|
Entry("txt > lrc > embedded", ".txt,.lrc,embedded", unsyncedLyrics))
|
||||||
|
|
||||||
|
Context("Errors", func() {
|
||||||
|
var RegularUserContext = XContext
|
||||||
|
var isRegularUser = os.Getuid() != 0
|
||||||
|
if isRegularUser {
|
||||||
|
RegularUserContext = Context
|
||||||
|
}
|
||||||
|
|
||||||
|
RegularUserContext("run without root permissions", func() {
|
||||||
|
var accessForbiddenFile string
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
accessForbiddenFile = utils.TempFileName("access_forbidden-", ".mp3")
|
||||||
|
|
||||||
|
f, err := os.OpenFile(accessForbiddenFile, os.O_WRONLY|os.O_CREATE, 0222)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
mf.Path = accessForbiddenFile
|
||||||
|
|
||||||
|
DeferCleanup(func() {
|
||||||
|
Expect(f.Close()).To(Succeed())
|
||||||
|
Expect(os.Remove(accessForbiddenFile)).To(Succeed())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should fallback to embedded if an error happens when parsing file", func() {
|
||||||
|
conf.Server.LyricsPriority = ".mp3,embedded"
|
||||||
|
|
||||||
|
list, err := lyrics.GetLyrics(ctx, &mf)
|
||||||
|
Expect(err).To(BeNil())
|
||||||
|
Expect(list).To(Equal(embeddedLyrics))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should return nothing if error happens when trying to parse file", func() {
|
||||||
|
conf.Server.LyricsPriority = ".mp3"
|
||||||
|
|
||||||
|
list, err := lyrics.GetLyrics(ctx, &mf)
|
||||||
|
Expect(err).To(BeNil())
|
||||||
|
Expect(list).To(BeEmpty())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
51
core/lyrics/sources.go
Normal file
51
core/lyrics/sources.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package lyrics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"github.com/navidrome/navidrome/log"
|
||||||
|
"github.com/navidrome/navidrome/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func fromEmbedded(ctx context.Context, mf *model.MediaFile) (model.LyricList, error) {
|
||||||
|
if mf.Lyrics != "" {
|
||||||
|
log.Trace(ctx, "embedded lyrics found in file", "title", mf.Title)
|
||||||
|
return mf.StructuredLyrics()
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Trace(ctx, "no embedded lyrics for file", "path", mf.Title)
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fromExternalFile(ctx context.Context, mf *model.MediaFile, suffix string) (model.LyricList, error) {
|
||||||
|
basePath := mf.AbsolutePath()
|
||||||
|
ext := path.Ext(basePath)
|
||||||
|
|
||||||
|
externalLyric := basePath[0:len(basePath)-len(ext)] + suffix
|
||||||
|
|
||||||
|
contents, err := os.ReadFile(externalLyric)
|
||||||
|
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
log.Trace(ctx, "no lyrics found at path", "path", externalLyric)
|
||||||
|
return nil, nil
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
lyrics, err := model.ToLyrics("xxx", string(contents))
|
||||||
|
if err != nil {
|
||||||
|
log.Error(ctx, "error parsing lyric external file", "path", externalLyric, err)
|
||||||
|
return nil, err
|
||||||
|
} else if lyrics == nil {
|
||||||
|
log.Trace(ctx, "empty lyrics from external file", "path", externalLyric)
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Trace(ctx, "retrieved lyrics from external file", "path", externalLyric)
|
||||||
|
|
||||||
|
return model.LyricList{*lyrics}, nil
|
||||||
|
}
|
||||||
112
core/lyrics/sources_test.go
Normal file
112
core/lyrics/sources_test.go
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
package lyrics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/navidrome/navidrome/model"
|
||||||
|
"github.com/navidrome/navidrome/utils/gg"
|
||||||
|
. "github.com/onsi/ginkgo/v2"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = Describe("sources", func() {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
Describe("fromEmbedded", func() {
|
||||||
|
It("should return nothing for a media file with no lyrics", func() {
|
||||||
|
mf := model.MediaFile{}
|
||||||
|
lyrics, err := fromEmbedded(ctx, &mf)
|
||||||
|
|
||||||
|
Expect(err).To(BeNil())
|
||||||
|
Expect(lyrics).To(HaveLen(0))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should return lyrics for a media file with well-formatted lyrics", func() {
|
||||||
|
const syncedLyrics = "[00:18.80]We're no strangers to love\n[00:22.801]You know the rules and so do I"
|
||||||
|
const unsyncedLyrics = "We're no strangers to love\nYou know the rules and so do I"
|
||||||
|
|
||||||
|
synced, _ := model.ToLyrics("eng", syncedLyrics)
|
||||||
|
unsynced, _ := model.ToLyrics("xxx", unsyncedLyrics)
|
||||||
|
|
||||||
|
expectedList := model.LyricList{*synced, *unsynced}
|
||||||
|
lyricsJson, err := json.Marshal(expectedList)
|
||||||
|
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
mf := model.MediaFile{
|
||||||
|
Lyrics: string(lyricsJson),
|
||||||
|
}
|
||||||
|
|
||||||
|
lyrics, err := fromEmbedded(ctx, &mf)
|
||||||
|
Expect(err).To(BeNil())
|
||||||
|
Expect(lyrics).ToNot(BeNil())
|
||||||
|
Expect(lyrics).To(Equal(expectedList))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should return an error if somehow the JSON is bad", func() {
|
||||||
|
mf := model.MediaFile{Lyrics: "["}
|
||||||
|
lyrics, err := fromEmbedded(ctx, &mf)
|
||||||
|
|
||||||
|
Expect(lyrics).To(HaveLen(0))
|
||||||
|
Expect(err).ToNot(BeNil())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Describe("fromExternalFile", func() {
|
||||||
|
It("should return nil for lyrics that don't exist", func() {
|
||||||
|
mf := model.MediaFile{Path: "tests/fixtures/01 Invisible (RED) Edit Version.mp3"}
|
||||||
|
lyrics, err := fromExternalFile(ctx, &mf, ".lrc")
|
||||||
|
|
||||||
|
Expect(err).To(BeNil())
|
||||||
|
Expect(lyrics).To(HaveLen(0))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should return synchronized lyrics from a file", func() {
|
||||||
|
mf := model.MediaFile{Path: "tests/fixtures/test.mp3"}
|
||||||
|
lyrics, err := fromExternalFile(ctx, &mf, ".lrc")
|
||||||
|
|
||||||
|
Expect(err).To(BeNil())
|
||||||
|
Expect(lyrics).To(Equal(model.LyricList{
|
||||||
|
model.Lyrics{
|
||||||
|
DisplayArtist: "Rick Astley",
|
||||||
|
DisplayTitle: "That one song",
|
||||||
|
Lang: "eng",
|
||||||
|
Line: []model.Line{
|
||||||
|
{
|
||||||
|
Start: gg.P(int64(18800)),
|
||||||
|
Value: "We're no strangers to love",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Start: gg.P(int64(22801)),
|
||||||
|
Value: "You know the rules and so do I",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Offset: gg.P(int64(-100)),
|
||||||
|
Synced: true,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should return unsynchronized lyrics from a file", func() {
|
||||||
|
mf := model.MediaFile{Path: "tests/fixtures/test.mp3"}
|
||||||
|
lyrics, err := fromExternalFile(ctx, &mf, ".txt")
|
||||||
|
|
||||||
|
Expect(err).To(BeNil())
|
||||||
|
Expect(lyrics).To(Equal(model.LyricList{
|
||||||
|
model.Lyrics{
|
||||||
|
Lang: "xxx",
|
||||||
|
Line: []model.Line{
|
||||||
|
{
|
||||||
|
Value: "We're no strangers to love",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Value: "You know the rules and so do I",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Synced: false,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -32,7 +32,7 @@ var (
|
|||||||
// Should either be at the beginning of file, or beginning of line
|
// Should either be at the beginning of file, or beginning of line
|
||||||
syncRegex = regexp.MustCompile(`(^|\n)\s*` + timeRegexString)
|
syncRegex = regexp.MustCompile(`(^|\n)\s*` + timeRegexString)
|
||||||
timeRegex = regexp.MustCompile(timeRegexString)
|
timeRegex = regexp.MustCompile(timeRegexString)
|
||||||
lrcIdRegex = regexp.MustCompile(`\[(ar|ti|offset):([^]]+)]`)
|
lrcIdRegex = regexp.MustCompile(`\[(ar|ti|offset|lang):([^]]+)]`)
|
||||||
)
|
)
|
||||||
|
|
||||||
func (l Lyrics) IsEmpty() bool {
|
func (l Lyrics) IsEmpty() bool {
|
||||||
@ -72,6 +72,8 @@ func ToLyrics(language, text string) (*Lyrics, error) {
|
|||||||
switch idTag[1] {
|
switch idTag[1] {
|
||||||
case "ar":
|
case "ar":
|
||||||
artist = str.SanitizeText(strings.TrimSpace(idTag[2]))
|
artist = str.SanitizeText(strings.TrimSpace(idTag[2]))
|
||||||
|
case "lang":
|
||||||
|
language = str.SanitizeText(strings.TrimSpace(idTag[2]))
|
||||||
case "offset":
|
case "offset":
|
||||||
{
|
{
|
||||||
off, err := strconv.ParseInt(strings.TrimSpace(idTag[2]), 10, 64)
|
off, err := strconv.ParseInt(strings.TrimSpace(idTag[2]), 10, 64)
|
||||||
|
|||||||
@ -9,8 +9,9 @@ import (
|
|||||||
var _ = Describe("ToLyrics", func() {
|
var _ = Describe("ToLyrics", func() {
|
||||||
It("should parse tags with spaces", func() {
|
It("should parse tags with spaces", func() {
|
||||||
num := int64(1551)
|
num := int64(1551)
|
||||||
lyrics, err := ToLyrics("xxx", "[offset: 1551 ]\n[ti: A title ]\n[ar: An artist ]\n[00:00.00]Hi there")
|
lyrics, err := ToLyrics("xxx", "[lang: eng ]\n[offset: 1551 ]\n[ti: A title ]\n[ar: An artist ]\n[00:00.00]Hi there")
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(lyrics.Lang).To(Equal("eng"))
|
||||||
Expect(lyrics.Synced).To(BeTrue())
|
Expect(lyrics.Synced).To(BeTrue())
|
||||||
Expect(lyrics.DisplayArtist).To(Equal("An artist"))
|
Expect(lyrics.DisplayArtist).To(Equal("An artist"))
|
||||||
Expect(lyrics.DisplayTitle).To(Equal("A title"))
|
Expect(lyrics.DisplayTitle).To(Equal("A title"))
|
||||||
|
|||||||
@ -4,11 +4,13 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/navidrome/navidrome/log"
|
"github.com/navidrome/navidrome/log"
|
||||||
|
"github.com/navidrome/navidrome/tests"
|
||||||
. "github.com/onsi/ginkgo/v2"
|
. "github.com/onsi/ginkgo/v2"
|
||||||
. "github.com/onsi/gomega"
|
. "github.com/onsi/gomega"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSubsonicApi(t *testing.T) {
|
func TestSubsonicApi(t *testing.T) {
|
||||||
|
tests.Init(t, false)
|
||||||
log.SetLevel(log.LevelFatal)
|
log.SetLevel(log.LevelFatal)
|
||||||
RegisterFailHandler(Fail)
|
RegisterFailHandler(Fail)
|
||||||
RunSpecs(t, "Subsonic API Suite")
|
RunSpecs(t, "Subsonic API Suite")
|
||||||
|
|||||||
@ -108,12 +108,12 @@ func SongsByRandom(genre string, fromYear, toYear int) Options {
|
|||||||
return addDefaultFilters(options)
|
return addDefaultFilters(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
func SongWithLyrics(artist, title string) Options {
|
func SongWithArtistTitle(artist, title string) Options {
|
||||||
return addDefaultFilters(Options{
|
return addDefaultFilters(Options{
|
||||||
Sort: "updated_at",
|
Sort: "updated_at",
|
||||||
Order: "desc",
|
Order: "desc",
|
||||||
Max: 1,
|
Max: 1,
|
||||||
Filters: And{Eq{"artist": artist, "title": title}, NotEq{"lyrics": ""}},
|
Filters: And{Eq{"artist": artist, "title": title}},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/navidrome/navidrome/conf"
|
"github.com/navidrome/navidrome/conf"
|
||||||
"github.com/navidrome/navidrome/consts"
|
"github.com/navidrome/navidrome/consts"
|
||||||
|
"github.com/navidrome/navidrome/core/lyrics"
|
||||||
"github.com/navidrome/navidrome/log"
|
"github.com/navidrome/navidrome/log"
|
||||||
"github.com/navidrome/navidrome/model"
|
"github.com/navidrome/navidrome/model"
|
||||||
"github.com/navidrome/navidrome/resources"
|
"github.com/navidrome/navidrome/resources"
|
||||||
@ -95,9 +96,9 @@ func (api *Router) GetLyrics(r *http.Request) (*responses.Subsonic, error) {
|
|||||||
artist, _ := p.String("artist")
|
artist, _ := p.String("artist")
|
||||||
title, _ := p.String("title")
|
title, _ := p.String("title")
|
||||||
response := newResponse()
|
response := newResponse()
|
||||||
lyrics := responses.Lyrics{}
|
lyricsResponse := responses.Lyrics{}
|
||||||
response.Lyrics = &lyrics
|
response.Lyrics = &lyricsResponse
|
||||||
mediaFiles, err := api.ds.MediaFile(r.Context()).GetAll(filter.SongWithLyrics(artist, title))
|
mediaFiles, err := api.ds.MediaFile(r.Context()).GetAll(filter.SongWithArtistTitle(artist, title))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -107,7 +108,7 @@ func (api *Router) GetLyrics(r *http.Request) (*responses.Subsonic, error) {
|
|||||||
return response, nil
|
return response, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
structuredLyrics, err := mediaFiles[0].StructuredLyrics()
|
structuredLyrics, err := lyrics.GetLyrics(r.Context(), &mediaFiles[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -116,15 +117,15 @@ func (api *Router) GetLyrics(r *http.Request) (*responses.Subsonic, error) {
|
|||||||
return response, nil
|
return response, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
lyrics.Artist = artist
|
lyricsResponse.Artist = artist
|
||||||
lyrics.Title = title
|
lyricsResponse.Title = title
|
||||||
|
|
||||||
lyricsText := ""
|
lyricsText := ""
|
||||||
for _, line := range structuredLyrics[0].Line {
|
for _, line := range structuredLyrics[0].Line {
|
||||||
lyricsText += line.Value + "\n"
|
lyricsText += line.Value + "\n"
|
||||||
}
|
}
|
||||||
|
|
||||||
lyrics.Value = lyricsText
|
lyricsResponse.Value = lyricsText
|
||||||
|
|
||||||
return response, nil
|
return response, nil
|
||||||
}
|
}
|
||||||
@ -140,13 +141,13 @@ func (api *Router) GetLyricsBySongId(r *http.Request) (*responses.Subsonic, erro
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
lyrics, err := mediaFile.StructuredLyrics()
|
structuredLyrics, err := lyrics.GetLyrics(r.Context(), mediaFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
response := newResponse()
|
response := newResponse()
|
||||||
response.LyricsList = buildLyricsList(mediaFile, lyrics)
|
response.LyricsList = buildLyricsList(mediaFile, structuredLyrics)
|
||||||
|
|
||||||
return response, nil
|
return response, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,8 @@ import (
|
|||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/navidrome/navidrome/conf"
|
||||||
|
"github.com/navidrome/navidrome/conf/configtest"
|
||||||
"github.com/navidrome/navidrome/core/artwork"
|
"github.com/navidrome/navidrome/core/artwork"
|
||||||
"github.com/navidrome/navidrome/log"
|
"github.com/navidrome/navidrome/log"
|
||||||
"github.com/navidrome/navidrome/model"
|
"github.com/navidrome/navidrome/model"
|
||||||
@ -32,6 +34,8 @@ var _ = Describe("MediaRetrievalController", func() {
|
|||||||
artwork = &fakeArtwork{}
|
artwork = &fakeArtwork{}
|
||||||
router = New(ds, artwork, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
|
router = New(ds, artwork, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
|
||||||
w = httptest.NewRecorder()
|
w = httptest.NewRecorder()
|
||||||
|
DeferCleanup(configtest.SetupConfig())
|
||||||
|
conf.Server.LyricsPriority = "embedded,.lrc"
|
||||||
})
|
})
|
||||||
|
|
||||||
Describe("GetCoverArt", func() {
|
Describe("GetCoverArt", func() {
|
||||||
@ -109,6 +113,22 @@ var _ = Describe("MediaRetrievalController", func() {
|
|||||||
Expect(response.Lyrics.Title).To(Equal(""))
|
Expect(response.Lyrics.Title).To(Equal(""))
|
||||||
Expect(response.Lyrics.Value).To(Equal(""))
|
Expect(response.Lyrics.Value).To(Equal(""))
|
||||||
})
|
})
|
||||||
|
It("should return lyric file when finding mediafile with no embedded lyrics but present on filesystem", func() {
|
||||||
|
r := newGetRequest("artist=Rick+Astley", "title=Never+Gonna+Give+You+Up")
|
||||||
|
mockRepo.SetData(model.MediaFiles{
|
||||||
|
{
|
||||||
|
Path: "tests/fixtures/test.mp3",
|
||||||
|
ID: "1",
|
||||||
|
Artist: "Rick Astley",
|
||||||
|
Title: "Never Gonna Give You Up",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
response, err := router.GetLyrics(r)
|
||||||
|
Expect(err).To(BeNil())
|
||||||
|
Expect(response.Lyrics.Artist).To(Equal("Rick Astley"))
|
||||||
|
Expect(response.Lyrics.Title).To(Equal("Never Gonna Give You Up"))
|
||||||
|
Expect(response.Lyrics.Value).To(Equal("We're no strangers to love\nYou know the rules and so do I\n"))
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
Describe("getLyricsBySongId", func() {
|
Describe("getLyricsBySongId", func() {
|
||||||
|
|||||||
6
tests/fixtures/test.lrc
vendored
Normal file
6
tests/fixtures/test.lrc
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
[ar:Rick Astley]
|
||||||
|
[ti:That one song]
|
||||||
|
[offset:-100]
|
||||||
|
[lang:eng]
|
||||||
|
[00:18.80]We're no strangers to love
|
||||||
|
[00:22.801]You know the rules and so do I
|
||||||
2
tests/fixtures/test.txt
vendored
Normal file
2
tests/fixtures/test.txt
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
We're no strangers to love
|
||||||
|
You know the rules and so do I
|
||||||
Loading…
x
Reference in New Issue
Block a user