Add OS Lyrics extension (#2656)
* draft commit * time to fight pipeline * round 2 changes * remove unnecessary line * fight taglib. again * make taglib work again??? * add id3 tags * taglib 1.12 vs 1.13 * use int instead for windows * store as json now * add migration, more tests * support repeated line, multiline * fix ms and support .m, .mm, .mmm * address some concerns, make cpp a bit safer * separate responses from model * remove [:] * Add trace log * Try to unblock pipeline * Fix merge errors * Fix SIGSEGV error (proper handling of empty frames) * Add fallback artist/title to structured lyrics * Rename conflicting named vars * Fix tests * Do we still need ffmpeg in the pipeline? * Revert "Do we still need ffmpeg in the pipeline?" Yes we do. This reverts commit 87df7f6df79bccee83f48c4b7a8118a7636a5e66. * Does this passes now, with a newer ffmpeg version? * Revert "Does this passes now, with a newer ffmpeg version?" No, it does not :( This reverts commit 372eb4b0ae05d9ffe98078e9bc4e56a9b2921f32. * My OCD made me do it :P --------- Co-authored-by: Deluan Quintão <deluan@navidrome.org>
This commit is contained in:
@@ -142,6 +142,7 @@ func (api *Router) routes() http.Handler {
|
||||
r.Group(func(r chi.Router) {
|
||||
hr(r, "getAvatar", api.GetAvatar)
|
||||
h(r, "getLyrics", api.GetLyrics)
|
||||
h(r, "getLyricsBySongId", api.GetLyricsBySongId)
|
||||
})
|
||||
r.Group(func(r chi.Router) {
|
||||
// configure request throttling
|
||||
|
||||
@@ -323,3 +323,45 @@ func buildAlbumID3(ctx context.Context, album model.Album) responses.AlbumID3 {
|
||||
dir.OriginalReleaseDate = toItemDate(album.OriginalDate)
|
||||
return dir
|
||||
}
|
||||
|
||||
func buildStructuredLyric(mf *model.MediaFile, lyrics model.Lyrics) responses.StructuredLyric {
|
||||
lines := make([]responses.Line, len(lyrics.Line))
|
||||
|
||||
for i, line := range lyrics.Line {
|
||||
lines[i] = responses.Line{
|
||||
Start: line.Start,
|
||||
Value: line.Value,
|
||||
}
|
||||
}
|
||||
|
||||
structured := responses.StructuredLyric{
|
||||
DisplayArtist: lyrics.DisplayArtist,
|
||||
DisplayTitle: lyrics.DisplayTitle,
|
||||
Lang: lyrics.Lang,
|
||||
Line: lines,
|
||||
Offset: lyrics.Offset,
|
||||
Synced: lyrics.Synced,
|
||||
}
|
||||
|
||||
if structured.DisplayArtist == "" {
|
||||
structured.DisplayArtist = mf.Artist
|
||||
}
|
||||
if structured.DisplayTitle == "" {
|
||||
structured.DisplayTitle = mf.Title
|
||||
}
|
||||
|
||||
return structured
|
||||
}
|
||||
|
||||
func buildLyricsList(mf *model.MediaFile, lyricsList model.LyricList) *responses.LyricsList {
|
||||
lyricList := make(responses.StructuredLyrics, len(lyricsList))
|
||||
|
||||
for i, lyrics := range lyricsList {
|
||||
lyricList[i] = buildStructuredLyric(mf, lyrics)
|
||||
}
|
||||
|
||||
res := &responses.LyricsList{
|
||||
StructuredLyrics: lyricList,
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
@@ -90,16 +89,6 @@ func (api *Router) GetCoverArt(w http.ResponseWriter, r *http.Request) (*respons
|
||||
return nil, err
|
||||
}
|
||||
|
||||
const timeStampRegex 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(timeStampRegex)
|
||||
// Eg: [04:02:50.85]
|
||||
// [02:50.85]
|
||||
// [02:50]
|
||||
return r.MatchString(rawLyrics)
|
||||
}
|
||||
|
||||
func (api *Router) GetLyrics(r *http.Request) (*responses.Subsonic, error) {
|
||||
p := req.Params(r)
|
||||
artist, _ := p.String("artist")
|
||||
@@ -117,15 +106,46 @@ func (api *Router) GetLyrics(r *http.Request) (*responses.Subsonic, error) {
|
||||
return response, nil
|
||||
}
|
||||
|
||||
structuredLyrics, err := mediaFiles[0].StructuredLyrics()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(structuredLyrics) == 0 {
|
||||
return response, nil
|
||||
}
|
||||
|
||||
lyrics.Artist = artist
|
||||
lyrics.Title = title
|
||||
|
||||
if isSynced(mediaFiles[0].Lyrics) {
|
||||
r := regexp.MustCompile(timeStampRegex)
|
||||
lyrics.Value = r.ReplaceAllString(mediaFiles[0].Lyrics, "")
|
||||
} else {
|
||||
lyrics.Value = mediaFiles[0].Lyrics
|
||||
lyricsText := ""
|
||||
for _, line := range structuredLyrics[0].Line {
|
||||
lyricsText += line.Value + "\n"
|
||||
}
|
||||
|
||||
lyrics.Value = lyricsText
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (api *Router) GetLyricsBySongId(r *http.Request) (*responses.Subsonic, error) {
|
||||
id, err := req.Params(r).String("id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mediaFile, err := api.ds.MediaFile(r.Context()).Get(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lyrics, err := mediaFile.StructuredLyrics()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response := newResponse()
|
||||
response.LyricsList = buildLyricsList(mediaFile, lyrics)
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package subsonic
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http/httptest"
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
"github.com/navidrome/navidrome/core/artwork"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/server/subsonic/responses"
|
||||
"github.com/navidrome/navidrome/tests"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
@@ -72,12 +74,18 @@ var _ = Describe("MediaRetrievalController", func() {
|
||||
Describe("GetLyrics", func() {
|
||||
It("should return data for given artist & title", func() {
|
||||
r := newGetRequest("artist=Rick+Astley", "title=Never+Gonna+Give+You+Up")
|
||||
lyrics, _ := model.ToLyrics("eng", "[00:18.80]We're no strangers to love\n[00:22.80]You know the rules and so do I")
|
||||
lyricsJson, err := json.Marshal(model.LyricList{
|
||||
*lyrics,
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
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",
|
||||
Lyrics: string(lyricsJson),
|
||||
},
|
||||
})
|
||||
response, err := router.GetLyrics(r)
|
||||
@@ -87,7 +95,7 @@ var _ = Describe("MediaRetrievalController", func() {
|
||||
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"))
|
||||
Expect(response.Lyrics.Value).To(Equal("We're no strangers to love\nYou know the rules and so do I\n"))
|
||||
})
|
||||
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")
|
||||
@@ -100,7 +108,143 @@ var _ = Describe("MediaRetrievalController", func() {
|
||||
Expect(response.Lyrics.Artist).To(Equal(""))
|
||||
Expect(response.Lyrics.Title).To(Equal(""))
|
||||
Expect(response.Lyrics.Value).To(Equal(""))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("getLyricsBySongId", 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"
|
||||
const metadata = "[ar:Rick Astley]\n[ti:That one song]\n[offset:-100]"
|
||||
var times = []int64{18800, 22801}
|
||||
|
||||
compareResponses := func(actual *responses.LyricsList, expected responses.LyricsList) {
|
||||
Expect(actual).ToNot(BeNil())
|
||||
Expect(actual.StructuredLyrics).To(HaveLen(len(expected.StructuredLyrics)))
|
||||
for i, realLyric := range actual.StructuredLyrics {
|
||||
expectedLyric := expected.StructuredLyrics[i]
|
||||
|
||||
Expect(realLyric.DisplayArtist).To(Equal(expectedLyric.DisplayArtist))
|
||||
Expect(realLyric.DisplayTitle).To(Equal(expectedLyric.DisplayTitle))
|
||||
Expect(realLyric.Lang).To(Equal(expectedLyric.Lang))
|
||||
Expect(realLyric.Synced).To(Equal(expectedLyric.Synced))
|
||||
|
||||
if expectedLyric.Offset == nil {
|
||||
Expect(realLyric.Offset).To(BeNil())
|
||||
} else {
|
||||
Expect(*realLyric.Offset).To(Equal(*expectedLyric.Offset))
|
||||
}
|
||||
|
||||
Expect(realLyric.Line).To(HaveLen(len(expectedLyric.Line)))
|
||||
for j, realLine := range realLyric.Line {
|
||||
expectedLine := expectedLyric.Line[j]
|
||||
Expect(realLine.Value).To(Equal(expectedLine.Value))
|
||||
|
||||
if expectedLine.Start == nil {
|
||||
Expect(realLine.Start).To(BeNil())
|
||||
} else {
|
||||
Expect(*realLine.Start).To(Equal(*expectedLine.Start))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
It("should return mixed lyrics", func() {
|
||||
r := newGetRequest("id=1")
|
||||
synced, _ := model.ToLyrics("eng", syncedLyrics)
|
||||
unsynced, _ := model.ToLyrics("xxx", unsyncedLyrics)
|
||||
lyricsJson, err := json.Marshal(model.LyricList{
|
||||
*synced, *unsynced,
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
mockRepo.SetData(model.MediaFiles{
|
||||
{
|
||||
ID: "1",
|
||||
Artist: "Rick Astley",
|
||||
Title: "Never Gonna Give You Up",
|
||||
Lyrics: string(lyricsJson),
|
||||
},
|
||||
})
|
||||
|
||||
response, err := router.GetLyricsBySongId(r)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
compareResponses(response.LyricsList, responses.LyricsList{
|
||||
StructuredLyrics: responses.StructuredLyrics{
|
||||
{
|
||||
Lang: "eng",
|
||||
DisplayArtist: "Rick Astley",
|
||||
DisplayTitle: "Never Gonna Give You Up",
|
||||
Synced: true,
|
||||
Line: []responses.Line{
|
||||
{
|
||||
Start: ×[0],
|
||||
Value: "We're no strangers to love",
|
||||
},
|
||||
{
|
||||
Start: ×[1],
|
||||
Value: "You know the rules and so do I",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Lang: "xxx",
|
||||
DisplayArtist: "Rick Astley",
|
||||
DisplayTitle: "Never Gonna Give You Up",
|
||||
Synced: false,
|
||||
Line: []responses.Line{
|
||||
{
|
||||
Value: "We're no strangers to love",
|
||||
},
|
||||
{
|
||||
Value: "You know the rules and so do I",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
It("should parse lrc metadata", func() {
|
||||
r := newGetRequest("id=1")
|
||||
synced, _ := model.ToLyrics("eng", metadata+"\n"+syncedLyrics)
|
||||
lyricsJson, err := json.Marshal(model.LyricList{
|
||||
*synced,
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
mockRepo.SetData(model.MediaFiles{
|
||||
{
|
||||
ID: "1",
|
||||
Artist: "Rick Astley",
|
||||
Title: "Never Gonna Give You Up",
|
||||
Lyrics: string(lyricsJson),
|
||||
},
|
||||
})
|
||||
|
||||
response, err := router.GetLyricsBySongId(r)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
offset := int64(-100)
|
||||
compareResponses(response.LyricsList, responses.LyricsList{
|
||||
StructuredLyrics: responses.StructuredLyrics{
|
||||
{
|
||||
DisplayArtist: "Rick Astley",
|
||||
DisplayTitle: "That one song",
|
||||
Lang: "eng",
|
||||
Synced: true,
|
||||
Line: []responses.Line{
|
||||
{
|
||||
Start: ×[0],
|
||||
Value: "We're no strangers to love",
|
||||
},
|
||||
{
|
||||
Start: ×[1],
|
||||
Value: "You know the rules and so do I",
|
||||
},
|
||||
},
|
||||
Offset: &offset,
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -122,26 +266,6 @@ func (c *fakeArtwork) GetOrPlaceholder(_ context.Context, id string, size int) (
|
||||
return io.NopCloser(bytes.NewReader([]byte(c.data))), time.Time{}, 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] First line
|
||||
[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
|
||||
@@ -154,3 +278,12 @@ func (m *mockedMediaFile) SetData(mfs model.MediaFiles) {
|
||||
func (m *mockedMediaFile) GetAll(...model.QueryOptions) (model.MediaFiles, error) {
|
||||
return m.data, nil
|
||||
}
|
||||
|
||||
func (m *mockedMediaFile) Get(id string) (*model.MediaFile, error) {
|
||||
for _, mf := range m.data {
|
||||
if mf.ID == id {
|
||||
return &mf, nil
|
||||
}
|
||||
}
|
||||
return nil, model.ErrNotFound
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ func (api *Router) GetOpenSubsonicExtensions(_ *http.Request) (*responses.Subson
|
||||
response.OpenSubsonicExtensions = &responses.OpenSubsonicExtensions{
|
||||
{Name: "transcodeOffset", Versions: []int32{1}},
|
||||
{Name: "formPost", Versions: []int32{1}},
|
||||
{Name: "songLyrics", Versions: []int32{1}},
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"status": "ok",
|
||||
"version": "1.8.0",
|
||||
"type": "navidrome",
|
||||
"serverVersion": "v0.0.0",
|
||||
"openSubsonic": true,
|
||||
"lyricsList": {
|
||||
"structuredLyrics": [
|
||||
{
|
||||
"displayArtist": "Rick Astley",
|
||||
"displayTitle": "Never Gonna Give You Up",
|
||||
"lang": "eng",
|
||||
"line": [
|
||||
{
|
||||
"start": 18800,
|
||||
"value": "We're no strangers to love"
|
||||
},
|
||||
{
|
||||
"start": 22801,
|
||||
"value": "You know the rules and so do I"
|
||||
}
|
||||
],
|
||||
"offset": 100,
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"displayArtist": "Rick Astley",
|
||||
"displayTitle": "Never Gonna Give You Up",
|
||||
"lang": "xxx",
|
||||
"line": [
|
||||
{
|
||||
"value": "We're no strangers to love"
|
||||
},
|
||||
{
|
||||
"value": "You know the rules and so do I"
|
||||
}
|
||||
],
|
||||
"offset": 100,
|
||||
"synced": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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 displayArtist="Rick Astley" displayTitle="Never Gonna Give You Up" lang="eng" offset="100" synced="true">
|
||||
<line start="18800">
|
||||
<value>We're no strangers to love</value>
|
||||
</line>
|
||||
<line start="22801">
|
||||
<value>You know the rules and so do I</value>
|
||||
</line>
|
||||
</structuredLyrics>
|
||||
<structuredLyrics displayArtist="Rick Astley" displayTitle="Never Gonna Give You Up" lang="xxx" offset="100" synced="false">
|
||||
<line>
|
||||
<value>We're no strangers to love</value>
|
||||
</line>
|
||||
<line>
|
||||
<value>You know the rules and so do I</value>
|
||||
</line>
|
||||
</structuredLyrics>
|
||||
</lyricsList>
|
||||
</subsonic-response>
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"status": "ok",
|
||||
"version": "1.8.0",
|
||||
"type": "navidrome",
|
||||
"serverVersion": "v0.0.0",
|
||||
"openSubsonic": true,
|
||||
"lyricsList": {}
|
||||
}
|
||||
+3
@@ -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>
|
||||
@@ -58,6 +58,7 @@ type Subsonic struct {
|
||||
JukeboxPlaylist *JukeboxPlaylist `xml:"jukeboxPlaylist,omitempty" json:"jukeboxPlaylist,omitempty"`
|
||||
|
||||
OpenSubsonicExtensions *OpenSubsonicExtensions `xml:"openSubsonicExtensions,omitempty" json:"openSubsonicExtensions,omitempty"`
|
||||
LyricsList *LyricsList `xml:"lyricsList,omitempty" json:"lyricsList,omitempty"`
|
||||
}
|
||||
|
||||
type JsonWrapper struct {
|
||||
@@ -446,6 +447,26 @@ type JukeboxPlaylist struct {
|
||||
JukeboxStatus
|
||||
Entry []Child `xml:"entry,omitempty" json:"entry,omitempty"`
|
||||
}
|
||||
|
||||
type Line struct {
|
||||
Start *int64 `xml:"start,attr,omitempty" json:"start,omitempty"`
|
||||
Value string `xml:"value" json:"value"`
|
||||
}
|
||||
|
||||
type StructuredLyric struct {
|
||||
DisplayArtist string `xml:"displayArtist,attr,omitempty" json:"displayArtist,omitempty"`
|
||||
DisplayTitle string `xml:"displayTitle,attr,omitempty" json:"displayTitle,omitempty"`
|
||||
Lang string `xml:"lang,attr" json:"lang"`
|
||||
Line []Line `xml:"line" json:"line"`
|
||||
Offset *int64 `xml:"offset,attr,omitempty" json:"offset,omitempty"`
|
||||
Synced bool `xml:"synced,attr" json:"synced"`
|
||||
}
|
||||
|
||||
type StructuredLyrics []StructuredLyric
|
||||
type LyricsList struct {
|
||||
StructuredLyrics []StructuredLyric `xml:"structuredLyrics,omitempty" json:"structuredLyrics,omitempty"`
|
||||
}
|
||||
|
||||
type OpenSubsonicExtension struct {
|
||||
Name string `xml:"name,attr" json:"name"`
|
||||
Versions []int32 `xml:"versions" json:"versions"`
|
||||
|
||||
@@ -796,4 +796,69 @@ var _ = Describe("Responses", func() {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Describe("LyricsList", 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{18800, 22801}
|
||||
offset := int64(100)
|
||||
|
||||
response.LyricsList.StructuredLyrics = StructuredLyrics{
|
||||
{
|
||||
Lang: "eng",
|
||||
DisplayArtist: "Rick Astley",
|
||||
DisplayTitle: "Never Gonna Give You Up",
|
||||
Offset: &offset,
|
||||
Synced: true,
|
||||
Line: []Line{
|
||||
{
|
||||
Start: ×[0],
|
||||
Value: "We're no strangers to love",
|
||||
},
|
||||
{
|
||||
Start: ×[1],
|
||||
Value: "You know the rules and so do I",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Lang: "xxx",
|
||||
DisplayArtist: "Rick Astley",
|
||||
DisplayTitle: "Never Gonna Give You Up",
|
||||
Offset: &offset,
|
||||
Synced: false,
|
||||
Line: []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())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user