mirror of
https://github.com/navidrome/navidrome.git
synced 2025-04-15 03:30:39 +03:00
Adds Lyrics Support to Subsonic API (#1379)
* Add function 'isSynced' that identifies if lyrics are synced or not and add tests for the same * implement 'getLyrics' which returns lyrics if they exist Signed-off-by: Dheeraj Lalwani <lalwanidheeraj1234@gmail.com> * remove timestamps frorom the the lyrics if they are synced, fix filters & clean up code Signed-off-by: Dheeraj Lalwani <lalwanidheeraj1234@gmail.com> * add snapshot tests for the 'Lyrics' response & add some clean up Signed-off-by: Dheeraj Lalwani <lalwanidheeraj1234@gmail.com> * add tests for 'GetLyrics' function Signed-off-by: Dheeraj Lalwani <lalwanidheeraj1234@gmail.com> * update the snapshot test & the test for 'GetLyrics' function Signed-off-by: Dheeraj Lalwani <lalwanidheeraj1234@gmail.com>
This commit is contained in:
parent
3214783ce9
commit
5621551dd0
@ -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")
|
||||
|
@ -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": ""}},
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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"}}
|
@ -0,0 +1 @@
|
||||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0"><lyrics artist="Rick Astley" title="Never Gonna Give You Up">Never gonna give you up
				Never gonna let you down
				Never gonna run around and desert you
				Never gonna say goodbye</lyrics></subsonic-response>
|
@ -0,0 +1 @@
|
||||
{"status":"ok","version":"1.8.0","type":"navidrome","serverVersion":"v0.0.0","lyrics":{"value":""}}
|
@ -0,0 +1 @@
|
||||
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0"><lyrics></lyrics></subsonic-response>
|
@ -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"`
|
||||
}
|
||||
|
@ -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())
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
})
|
||||
|
Loading…
x
Reference in New Issue
Block a user