diff --git a/server/subsonic/api.go b/server/subsonic/api.go
index 78e21e4df..cc4b4d7e2 100644
--- a/server/subsonic/api.go
+++ b/server/subsonic/api.go
@@ -146,6 +146,7 @@ func (api *Router) routes() http.Handler {
withThrottle := r.With(middleware.ThrottleBacklog(maxRequests, consts.RequestThrottleBacklogLimit, consts.RequestThrottleBacklogTimeout))
h(withThrottle, "getAvatar", c.GetAvatar)
h(withThrottle, "getCoverArt", c.GetCoverArt)
+ h(withThrottle, "getLyrics", c.GetLyrics)
})
r.Group(func(r chi.Router) {
c := initStreamController(api)
@@ -155,7 +156,6 @@ func (api *Router) routes() http.Handler {
})
// Not Implemented (yet?)
- h501(r, "getLyrics")
h501(r, "jukeboxControl")
h501(r, "getAlbumInfo", "getAlbumInfo2")
h501(r, "getShares", "createShare", "updateShare", "deleteShare")
diff --git a/server/subsonic/filter/filters.go b/server/subsonic/filter/filters.go
index da6e339d1..8f05c42c4 100644
--- a/server/subsonic/filter/filters.go
+++ b/server/subsonic/filter/filters.go
@@ -111,3 +111,11 @@ func SongsByRandom(genre string, fromYear, toYear int) Options {
func Starred() Options {
return Options{Sort: "starred_at", Order: "desc", Filters: squirrel.Eq{"starred": true}}
}
+
+func SongsWithLyrics(artist, title string) Options {
+ return Options{
+ Sort: "updated_at",
+ Order: "desc",
+ Filters: squirrel.And{squirrel.Eq{"artist": artist, "title": title}, squirrel.NotEq{"lyrics": ""}},
+ }
+}
diff --git a/server/subsonic/media_retrieval.go b/server/subsonic/media_retrieval.go
index 55f9c2407..30a6bf3a6 100644
--- a/server/subsonic/media_retrieval.go
+++ b/server/subsonic/media_retrieval.go
@@ -3,6 +3,7 @@ package subsonic
import (
"io"
"net/http"
+ "regexp"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/consts"
@@ -10,6 +11,7 @@ import (
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/resources"
+ "github.com/navidrome/navidrome/server/subsonic/filter"
"github.com/navidrome/navidrome/server/subsonic/responses"
"github.com/navidrome/navidrome/utils"
"github.com/navidrome/navidrome/utils/gravatar"
@@ -78,3 +80,42 @@ func (c *MediaRetrievalController) GetCoverArt(w http.ResponseWriter, r *http.Re
return nil, err
}
+
+const TIMESTAMP_REGEX string = `(\[([0-9]{1,2}:)?([0-9]{1,2}:)([0-9]{1,2})(\.[0-9]{1,2})?\])`
+
+func isSynced(rawLyrics string) bool {
+ r := regexp.MustCompile(TIMESTAMP_REGEX)
+ // Eg: [04:02:50.85]
+ // [02:50.85]
+ // [02:50]
+ return r.MatchString(rawLyrics)
+}
+
+func (c *MediaRetrievalController) GetLyrics(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) {
+ artist := utils.ParamString(r, "artist")
+ title := utils.ParamString(r, "title")
+ response := newResponse()
+ lyrics := responses.Lyrics{}
+ response.Lyrics = &lyrics
+ media_files, err := c.ds.MediaFile(r.Context()).GetAll(filter.SongsWithLyrics(artist, title))
+
+ if err != nil {
+ return nil, err
+ }
+
+ if len(media_files) == 0 {
+ return response, nil
+ }
+
+ lyrics.Artist = artist
+ lyrics.Title = title
+
+ if isSynced(media_files[0].Lyrics) {
+ r := regexp.MustCompile(TIMESTAMP_REGEX)
+ lyrics.Value = r.ReplaceAllString(media_files[0].Lyrics, "")
+ } else {
+ lyrics.Value = media_files[0].Lyrics
+ }
+
+ return response, nil
+}
diff --git a/server/subsonic/media_retrieval_test.go b/server/subsonic/media_retrieval_test.go
index 705e52d18..b98075991 100644
--- a/server/subsonic/media_retrieval_test.go
+++ b/server/subsonic/media_retrieval_test.go
@@ -7,6 +7,7 @@ import (
"io"
"net/http/httptest"
+ "github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/tests"
. "github.com/onsi/ginkgo"
@@ -15,12 +16,17 @@ import (
var _ = Describe("MediaRetrievalController", func() {
var controller *MediaRetrievalController
+ var ds model.DataStore
+ mockRepo := &mockedMediaFile{}
var artwork *fakeArtwork
var w *httptest.ResponseRecorder
BeforeEach(func() {
+ ds = &tests.MockDataStore{
+ MockedMediaFile: mockRepo,
+ }
artwork = &fakeArtwork{}
- controller = NewMediaRetrievalController(artwork, &tests.MockDataStore{})
+ controller = NewMediaRetrievalController(artwork, ds)
w = httptest.NewRecorder()
})
@@ -60,6 +66,41 @@ var _ = Describe("MediaRetrievalController", func() {
Expect(err).To(MatchError("weird error"))
})
})
+
+ Describe("GetLyrics", func() {
+ It("should return data for given artist & title", func() {
+ r := newGetRequest("artist=Rick+Astley", "title=Never+Gonna+Give+You+Up")
+ mockRepo.SetData(model.MediaFiles{
+ {
+ ID: "1",
+ Artist: "Rick Astley",
+ Title: "Never Gonna Give You Up",
+ Lyrics: "[00:18.80]We're no strangers to love\n[00:22.80]You know the rules and so do I",
+ },
+ })
+ response, err := controller.GetLyrics(w, r)
+ if err != nil {
+ log.Error("You're missing something.", err)
+ }
+ 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"))
+ })
+ It("should return empty subsonic response if the record corresponding to the given artist & title is not found", func() {
+ r := newGetRequest("artist=Dheeraj", "title=Rinkiya+Ke+Papa")
+ mockRepo.SetData(model.MediaFiles{})
+ response, err := controller.GetLyrics(w, r)
+ if err != nil {
+ log.Error("You're missing something.", err)
+ }
+ Expect(err).To(BeNil())
+ Expect(response.Lyrics.Artist).To(Equal(""))
+ Expect(response.Lyrics.Title).To(Equal(""))
+ Expect(response.Lyrics.Value).To(Equal(""))
+
+ })
+ })
})
type fakeArtwork struct {
@@ -77,3 +118,36 @@ func (c *fakeArtwork) Get(ctx context.Context, id string, size int) (io.ReadClos
c.recvSize = size
return io.NopCloser(bytes.NewReader([]byte(c.data))), nil
}
+
+var _ = Describe("isSynced", func() {
+ It("returns false if lyrics contain no timestamps", func() {
+ Expect(isSynced("Just in case my car goes off the highway")).To(Equal(false))
+ Expect(isSynced("[02.50] Just in case my car goes off the highway")).To(Equal(false))
+ })
+ It("returns false if lyrics is an empty string", func() {
+ Expect(isSynced("")).To(Equal(false))
+ })
+ It("returns true if lyrics contain timestamps", func() {
+ Expect(isSynced(`NF Real Music
+ [00:00] ksdjjs
+ [00:00.85] JUST LIKE YOU
+ [00:00.85] Just in case my car goes off the highway`)).To(Equal(true))
+ Expect(isSynced("[04:02:50.85] Never gonna give you up")).To(Equal(true))
+ Expect(isSynced("[02:50.85] Never gonna give you up")).To(Equal(true))
+ Expect(isSynced("[02:50] Never gonna give you up")).To(Equal(true))
+ })
+
+})
+
+type mockedMediaFile struct {
+ model.MediaFileRepository
+ data model.MediaFiles
+}
+
+func (m *mockedMediaFile) SetData(mfs model.MediaFiles) {
+ m.data = mfs
+}
+
+func (m *mockedMediaFile) GetAll(options ...model.QueryOptions) (model.MediaFiles, error) {
+ return m.data, nil
+}
diff --git a/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses Lyrics with data should match .JSON b/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses Lyrics with data should match .JSON
new file mode 100644
index 000000000..bd15ee945
--- /dev/null
+++ b/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses Lyrics with data should match .JSON
@@ -0,0 +1 @@
+{"status":"ok","version":"1.8.0","type":"navidrome","serverVersion":"v0.0.0","lyrics":{"artist":"Rick Astley","title":"Never Gonna Give You Up","value":"Never gonna give you up\n\t\t\t\tNever gonna let you down\n\t\t\t\tNever gonna run around and desert you\n\t\t\t\tNever gonna say goodbye"}}
diff --git a/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses Lyrics with data should match .XML b/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses Lyrics with data should match .XML
new file mode 100644
index 000000000..92f3a0012
--- /dev/null
+++ b/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses Lyrics with data should match .XML
@@ -0,0 +1 @@
+Never gonna give you up
Never gonna let you down
Never gonna run around and desert you
Never gonna say goodbye
diff --git a/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses Lyrics without data should match .JSON b/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses Lyrics without data should match .JSON
new file mode 100644
index 000000000..5bb964d93
--- /dev/null
+++ b/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses Lyrics without data should match .JSON
@@ -0,0 +1 @@
+{"status":"ok","version":"1.8.0","type":"navidrome","serverVersion":"v0.0.0","lyrics":{"value":""}}
diff --git a/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses Lyrics without data should match .XML b/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses Lyrics without data should match .XML
new file mode 100644
index 000000000..3a2ae01a4
--- /dev/null
+++ b/server/subsonic/responses/.snapshots/responses-snapshotMatcher-Match-Responses Lyrics without data should match .XML
@@ -0,0 +1 @@
+
diff --git a/server/subsonic/responses/responses.go b/server/subsonic/responses/responses.go
index c88c02845..3b9461f1c 100644
--- a/server/subsonic/responses/responses.go
+++ b/server/subsonic/responses/responses.go
@@ -46,6 +46,7 @@ type Subsonic struct {
PlayQueue *PlayQueue `xml:"playQueue,omitempty" json:"playQueue,omitempty"`
Bookmarks *Bookmarks `xml:"bookmarks,omitempty" json:"bookmarks,omitempty"`
ScanStatus *ScanStatus `xml:"scanStatus,omitempty" json:"scanStatus,omitempty"`
+ Lyrics *Lyrics `xml:"lyrics,omitempty" json:"lyrics,omitempty"`
}
type JsonWrapper struct {
@@ -346,3 +347,9 @@ type ScanStatus struct {
FolderCount int64 `xml:"folderCount,attr" json:"folderCount"`
LastScan *time.Time `xml:"lastScan,attr,omitempty" json:"lastScan,omitempty"`
}
+
+type Lyrics struct {
+ Artist string `xml:"artist,omitempty,attr" json:"artist,omitempty"`
+ Title string `xml:"title,omitempty,attr" json:"title,omitempty"`
+ Value string `xml:",chardata" json:"value"`
+}
diff --git a/server/subsonic/responses/responses_test.go b/server/subsonic/responses/responses_test.go
index 4ac52d513..c442ad93c 100644
--- a/server/subsonic/responses/responses_test.go
+++ b/server/subsonic/responses/responses_test.go
@@ -561,4 +561,37 @@ var _ = Describe("Responses", func() {
})
})
})
+
+ Describe("Lyrics", func() {
+ BeforeEach(func() {
+ response.Lyrics = &Lyrics{}
+ })
+
+ Context("without data", func() {
+ It("should match .XML", func() {
+ Expect(xml.Marshal(response)).To(MatchSnapshot())
+ })
+ It("should match .JSON", func() {
+ Expect(json.Marshal(response)).To(MatchSnapshot())
+ })
+ })
+
+ Context("with data", func() {
+ BeforeEach(func() {
+ response.Lyrics.Artist = "Rick Astley"
+ response.Lyrics.Title = "Never Gonna Give You Up"
+ response.Lyrics.Value = `Never gonna give you up
+ Never gonna let you down
+ Never gonna run around and desert you
+ Never gonna say goodbye`
+ })
+ It("should match .XML", func() {
+ Expect(xml.Marshal(response)).To(MatchSnapshot())
+ })
+ It("should match .JSON", func() {
+ Expect(json.Marshal(response)).To(MatchSnapshot())
+ })
+
+ })
+ })
})