test(e2e): add transcode endpoint e2e tests and clean up test helpers
Add comprehensive e2e tests for getTranscodeDecision and getTranscodeStream endpoints covering direct play, transcoding, error handling, and round-trip token validation. Refactor buildPostReq to reuse buildReq for auth params, remove unused WAV/AAC test tracks, and consolidate duplicate test assertions.
This commit is contained in:
@@ -284,6 +284,9 @@ func (ffs *FakeFS) parseFile(filePath string) (*metadata.Info, error) {
|
|||||||
p.AudioProperties.BitDepth = getInt("bitdepth")
|
p.AudioProperties.BitDepth = getInt("bitdepth")
|
||||||
p.AudioProperties.SampleRate = getInt("samplerate")
|
p.AudioProperties.SampleRate = getInt("samplerate")
|
||||||
p.AudioProperties.Channels = getInt("channels")
|
p.AudioProperties.Channels = getInt("channels")
|
||||||
|
if codec, ok := data["codec"].(string); ok {
|
||||||
|
p.AudioProperties.Codec = codec
|
||||||
|
}
|
||||||
for k, v := range data {
|
for k, v := range data {
|
||||||
p.Tags[k] = []string{fmt.Sprintf("%v", v)}
|
p.Tags[k] = []string{fmt.Sprintf("%v", v)}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package e2e
|
package e2e
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
@@ -55,6 +56,7 @@ type _t = map[string]any
|
|||||||
|
|
||||||
var template = storagetest.Template
|
var template = storagetest.Template
|
||||||
var track = storagetest.Track
|
var track = storagetest.Track
|
||||||
|
var file = storagetest.File
|
||||||
|
|
||||||
// MusicBrainz ID constants for test data (valid UUID v4 values)
|
// MusicBrainz ID constants for test data (valid UUID v4 values)
|
||||||
const (
|
const (
|
||||||
@@ -122,6 +124,9 @@ func buildTestFS() storagetest.FakeFS {
|
|||||||
popTrack := template(_t{"albumartist": "Various", "artist": "Various", "album": "Pop", "year": 2020, "genre": "Pop"})
|
popTrack := template(_t{"albumartist": "Various", "artist": "Various", "album": "Pop", "year": 2020, "genre": "Pop"})
|
||||||
cowboyBebop := template(_t{"albumartist": "シートベルツ", "artist": "シートベルツ", "album": "COWBOY BEBOP", "year": 1998, "genre": "Jazz"})
|
cowboyBebop := template(_t{"albumartist": "シートベルツ", "artist": "シートベルツ", "album": "COWBOY BEBOP", "year": 1998, "genre": "Jazz"})
|
||||||
|
|
||||||
|
// Template for diverse-format transcode test tracks
|
||||||
|
tcBase := _t{"albumartist": "Test Artist", "artist": "Test Artist", "album": "Transcode Formats", "year": 2024, "genre": "Test"}
|
||||||
|
|
||||||
return createFS(fstest.MapFS{
|
return createFS(fstest.MapFS{
|
||||||
// Rock / The Beatles / Abbey Road (with MBIDs)
|
// Rock / The Beatles / Abbey Road (with MBIDs)
|
||||||
// Note: "musicbrainz_trackid" is an alias for the musicbrainz_recordingid tag (populates MbzRecordingID),
|
// Note: "musicbrainz_trackid" is an alias for the musicbrainz_recordingid tag (populates MbzRecordingID),
|
||||||
@@ -140,6 +145,33 @@ func buildTestFS() storagetest.FakeFS {
|
|||||||
"Pop/01 - Standalone Track.mp3": popTrack(track(1, "Standalone Track")),
|
"Pop/01 - Standalone Track.mp3": popTrack(track(1, "Standalone Track")),
|
||||||
// CJK / シートベルツ / COWBOY BEBOP (Japanese artist, for CJK search tests)
|
// CJK / シートベルツ / COWBOY BEBOP (Japanese artist, for CJK search tests)
|
||||||
"CJK/シートベルツ/COWBOY BEBOP/01 - プラチナ・ジェット.mp3": cowboyBebop(track(1, "プラチナ・ジェット")),
|
"CJK/シートベルツ/COWBOY BEBOP/01 - プラチナ・ジェット.mp3": cowboyBebop(track(1, "プラチナ・ジェット")),
|
||||||
|
|
||||||
|
// Diverse audio format tracks for transcode e2e tests
|
||||||
|
"Test/Transcode Formats/01 - TC FLAC Standard.flac": file(tcBase, _t{
|
||||||
|
"title": "TC FLAC Standard", "track": 1, "suffix": "flac",
|
||||||
|
"bitrate": 900, "samplerate": 44100, "bitdepth": 16, "channels": 2, "duration": int64(240),
|
||||||
|
}),
|
||||||
|
"Test/Transcode Formats/02 - TC FLAC HiRes.flac": file(tcBase, _t{
|
||||||
|
"title": "TC FLAC HiRes", "track": 2, "suffix": "flac",
|
||||||
|
"bitrate": 3000, "samplerate": 96000, "bitdepth": 24, "channels": 2, "duration": int64(180),
|
||||||
|
}),
|
||||||
|
"Test/Transcode Formats/03 - TC ALAC Track.m4a": file(tcBase, _t{
|
||||||
|
"title": "TC ALAC Track", "track": 3, "suffix": "m4a",
|
||||||
|
"bitrate": 900, "samplerate": 44100, "bitdepth": 16, "channels": 2, "duration": int64(200),
|
||||||
|
}),
|
||||||
|
"Test/Transcode Formats/04 - TC DSD Track.dsf": file(tcBase, _t{
|
||||||
|
"title": "TC DSD Track", "track": 4, "suffix": "dsf",
|
||||||
|
"bitrate": 5645, "samplerate": 2822400, "bitdepth": 1, "channels": 2, "duration": int64(300),
|
||||||
|
}),
|
||||||
|
"Test/Transcode Formats/05 - TC Opus Track.opus": file(tcBase, _t{
|
||||||
|
"title": "TC Opus Track", "track": 5, "suffix": "opus",
|
||||||
|
"bitrate": 128, "samplerate": 48000, "bitdepth": 0, "channels": 2, "duration": int64(210),
|
||||||
|
}),
|
||||||
|
"Test/Transcode Formats/06 - TC MKA Opus.mka": file(tcBase, _t{
|
||||||
|
"title": "TC MKA Opus", "track": 6, "suffix": "mka", "codec": "opus",
|
||||||
|
"bitrate": 128, "samplerate": 48000, "bitdepth": 0, "channels": 2, "duration": int64(220),
|
||||||
|
}),
|
||||||
|
|
||||||
// _empty folder (directory with no audio)
|
// _empty folder (directory with no audio)
|
||||||
"_empty/.keep": &fstest.MapFile{Data: []byte{}, ModTime: time.Now()},
|
"_empty/.keep": &fstest.MapFile{Data: []byte{}, ModTime: time.Now()},
|
||||||
})
|
})
|
||||||
@@ -207,6 +239,30 @@ func buildReq(user model.User, endpoint string, params ...string) *http.Request
|
|||||||
return httptest.NewRequest("GET", "/"+endpoint+"?"+q.Encode(), nil)
|
return httptest.NewRequest("GET", "/"+endpoint+"?"+q.Encode(), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// buildPostReq creates a POST request with a JSON body and Subsonic auth params in the query string.
|
||||||
|
func buildPostReq(user model.User, endpoint string, body string, params ...string) *http.Request {
|
||||||
|
getReq := buildReq(user, endpoint, params...)
|
||||||
|
r := httptest.NewRequest("POST", getReq.URL.RequestURI(), bytes.NewReader([]byte(body)))
|
||||||
|
r.Header.Set("Content-Type", "application/json")
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// doPostReq makes a POST round-trip as admin and returns the parsed Subsonic response.
|
||||||
|
func doPostReq(endpoint string, body string, params ...string) *responses.Subsonic {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r := buildPostReq(adminUser, endpoint, body, params...)
|
||||||
|
router.ServeHTTP(w, r)
|
||||||
|
return parseJSONResponse(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// doRawPostReq makes a POST round-trip as admin and returns the raw recorder.
|
||||||
|
func doRawPostReq(endpoint string, body string, params ...string) *httptest.ResponseRecorder {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r := buildPostReq(adminUser, endpoint, body, params...)
|
||||||
|
router.ServeHTTP(w, r)
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
// parseJSONResponse parses the JSON response body into a Subsonic response struct.
|
// parseJSONResponse parses the JSON response body into a Subsonic response struct.
|
||||||
func parseJSONResponse(w *httptest.ResponseRecorder) *responses.Subsonic {
|
func parseJSONResponse(w *httptest.ResponseRecorder) *responses.Subsonic {
|
||||||
Expect(w.Code).To(Equal(http.StatusOK))
|
Expect(w.Code).To(Equal(http.StatusOK))
|
||||||
@@ -411,6 +467,7 @@ func setupTestDB() {
|
|||||||
})
|
})
|
||||||
conf.Server.MusicFolder = "fake:///music"
|
conf.Server.MusicFolder = "fake:///music"
|
||||||
conf.Server.DevExternalScanner = false
|
conf.Server.DevExternalScanner = false
|
||||||
|
conf.Server.DevEnableMediaFileProbe = false
|
||||||
|
|
||||||
// Restore DB to golden state (no scan needed)
|
// Restore DB to golden state (no scan needed)
|
||||||
restoreDB()
|
restoreDB()
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ var _ = Describe("Album List Endpoints", func() {
|
|||||||
|
|
||||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||||
Expect(resp.AlbumList).ToNot(BeNil())
|
Expect(resp.AlbumList).ToNot(BeNil())
|
||||||
Expect(resp.AlbumList.Album).To(HaveLen(6))
|
Expect(resp.AlbumList.Album).To(HaveLen(7))
|
||||||
})
|
})
|
||||||
|
|
||||||
It("type=alphabeticalByName sorts albums by name", func() {
|
It("type=alphabeticalByName sorts albums by name", func() {
|
||||||
@@ -27,14 +27,15 @@ var _ = Describe("Album List Endpoints", func() {
|
|||||||
|
|
||||||
Expect(resp.AlbumList).ToNot(BeNil())
|
Expect(resp.AlbumList).ToNot(BeNil())
|
||||||
albums := resp.AlbumList.Album
|
albums := resp.AlbumList.Album
|
||||||
Expect(albums).To(HaveLen(6))
|
Expect(albums).To(HaveLen(7))
|
||||||
// Verify alphabetical order: Abbey Road, COWBOY BEBOP, Help!, IV, Kind of Blue, Pop
|
// Verify alphabetical order: Abbey Road, COWBOY BEBOP, Help!, IV, Kind of Blue, Pop, Transcode Formats
|
||||||
Expect(albums[0].Title).To(Equal("Abbey Road"))
|
Expect(albums[0].Title).To(Equal("Abbey Road"))
|
||||||
Expect(albums[1].Title).To(Equal("COWBOY BEBOP"))
|
Expect(albums[1].Title).To(Equal("COWBOY BEBOP"))
|
||||||
Expect(albums[2].Title).To(Equal("Help!"))
|
Expect(albums[2].Title).To(Equal("Help!"))
|
||||||
Expect(albums[3].Title).To(Equal("IV"))
|
Expect(albums[3].Title).To(Equal("IV"))
|
||||||
Expect(albums[4].Title).To(Equal("Kind of Blue"))
|
Expect(albums[4].Title).To(Equal("Kind of Blue"))
|
||||||
Expect(albums[5].Title).To(Equal("Pop"))
|
Expect(albums[5].Title).To(Equal("Pop"))
|
||||||
|
Expect(albums[6].Title).To(Equal("Transcode Formats"))
|
||||||
})
|
})
|
||||||
|
|
||||||
It("type=alphabeticalByArtist sorts albums by artist name", func() {
|
It("type=alphabeticalByArtist sorts albums by artist name", func() {
|
||||||
@@ -42,22 +43,23 @@ var _ = Describe("Album List Endpoints", func() {
|
|||||||
|
|
||||||
Expect(resp.AlbumList).ToNot(BeNil())
|
Expect(resp.AlbumList).ToNot(BeNil())
|
||||||
albums := resp.AlbumList.Album
|
albums := resp.AlbumList.Album
|
||||||
Expect(albums).To(HaveLen(6))
|
Expect(albums).To(HaveLen(7))
|
||||||
// Articles like "The" are stripped for sorting, so "The Beatles" sorts as "Beatles"
|
// Articles like "The" are stripped for sorting, so "The Beatles" sorts as "Beatles"
|
||||||
// Non-compilations first: Beatles (x2), Led Zeppelin, Miles Davis, then compilations: Various, then CJK: シートベルツ
|
// Non-compilations first: Beatles (x2), Led Zeppelin, Miles Davis, Test Artist, then compilations: Various, then CJK: シートベルツ
|
||||||
Expect(albums[0].Artist).To(Equal("The Beatles"))
|
Expect(albums[0].Artist).To(Equal("The Beatles"))
|
||||||
Expect(albums[1].Artist).To(Equal("The Beatles"))
|
Expect(albums[1].Artist).To(Equal("The Beatles"))
|
||||||
Expect(albums[2].Artist).To(Equal("Led Zeppelin"))
|
Expect(albums[2].Artist).To(Equal("Led Zeppelin"))
|
||||||
Expect(albums[3].Artist).To(Equal("Miles Davis"))
|
Expect(albums[3].Artist).To(Equal("Miles Davis"))
|
||||||
Expect(albums[4].Artist).To(Equal("Various"))
|
Expect(albums[4].Artist).To(Equal("Test Artist"))
|
||||||
Expect(albums[5].Artist).To(Equal("シートベルツ"))
|
Expect(albums[5].Artist).To(Equal("Various"))
|
||||||
|
Expect(albums[6].Artist).To(Equal("シートベルツ"))
|
||||||
})
|
})
|
||||||
|
|
||||||
It("type=random returns albums", func() {
|
It("type=random returns albums", func() {
|
||||||
resp := doReq("getAlbumList", "type", "random")
|
resp := doReq("getAlbumList", "type", "random")
|
||||||
|
|
||||||
Expect(resp.AlbumList).ToNot(BeNil())
|
Expect(resp.AlbumList).ToNot(BeNil())
|
||||||
Expect(resp.AlbumList.Album).To(HaveLen(6))
|
Expect(resp.AlbumList.Album).To(HaveLen(7))
|
||||||
})
|
})
|
||||||
|
|
||||||
It("type=byGenre filters by genre parameter", func() {
|
It("type=byGenre filters by genre parameter", func() {
|
||||||
@@ -188,7 +190,7 @@ var _ = Describe("Album List Endpoints", func() {
|
|||||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||||
Expect(resp.AlbumList2).ToNot(BeNil())
|
Expect(resp.AlbumList2).ToNot(BeNil())
|
||||||
albums := resp.AlbumList2.Album
|
albums := resp.AlbumList2.Album
|
||||||
Expect(albums).To(HaveLen(6))
|
Expect(albums).To(HaveLen(7))
|
||||||
// Verify AlbumID3 format fields
|
// Verify AlbumID3 format fields
|
||||||
Expect(albums[0].Name).To(Equal("Abbey Road"))
|
Expect(albums[0].Name).To(Equal("Abbey Road"))
|
||||||
Expect(albums[0].Id).ToNot(BeEmpty())
|
Expect(albums[0].Id).ToNot(BeEmpty())
|
||||||
@@ -199,7 +201,7 @@ var _ = Describe("Album List Endpoints", func() {
|
|||||||
resp := doReq("getAlbumList2", "type", "newest")
|
resp := doReq("getAlbumList2", "type", "newest")
|
||||||
|
|
||||||
Expect(resp.AlbumList2).ToNot(BeNil())
|
Expect(resp.AlbumList2).ToNot(BeNil())
|
||||||
Expect(resp.AlbumList2.Album).To(HaveLen(6))
|
Expect(resp.AlbumList2.Album).To(HaveLen(7))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -244,7 +246,7 @@ var _ = Describe("Album List Endpoints", func() {
|
|||||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||||
Expect(resp.RandomSongs).ToNot(BeNil())
|
Expect(resp.RandomSongs).ToNot(BeNil())
|
||||||
Expect(resp.RandomSongs.Songs).ToNot(BeEmpty())
|
Expect(resp.RandomSongs.Songs).ToNot(BeEmpty())
|
||||||
Expect(resp.RandomSongs.Songs).To(HaveLen(7))
|
Expect(resp.RandomSongs.Songs).To(HaveLen(10))
|
||||||
})
|
})
|
||||||
|
|
||||||
It("respects size parameter", func() {
|
It("respects size parameter", func() {
|
||||||
|
|||||||
@@ -288,7 +288,7 @@ var _ = Describe("Browsing Endpoints", func() {
|
|||||||
|
|
||||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||||
Expect(resp.Genres).ToNot(BeNil())
|
Expect(resp.Genres).ToNot(BeNil())
|
||||||
Expect(resp.Genres.Genre).To(HaveLen(3))
|
Expect(resp.Genres.Genre).To(HaveLen(4))
|
||||||
})
|
})
|
||||||
|
|
||||||
It("includes correct genre names", func() {
|
It("includes correct genre names", func() {
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ var _ = Describe("Multi-Library Support", Ordered, func() {
|
|||||||
resp := doReqWithUser(adminWithLibs, "getAlbumList", "type", "alphabeticalByName", "musicFolderId", fmt.Sprintf("%d", lib.ID))
|
resp := doReqWithUser(adminWithLibs, "getAlbumList", "type", "alphabeticalByName", "musicFolderId", fmt.Sprintf("%d", lib.ID))
|
||||||
|
|
||||||
Expect(resp.AlbumList).ToNot(BeNil())
|
Expect(resp.AlbumList).ToNot(BeNil())
|
||||||
Expect(resp.AlbumList.Album).To(HaveLen(6))
|
Expect(resp.AlbumList.Album).To(HaveLen(7))
|
||||||
for _, a := range resp.AlbumList.Album {
|
for _, a := range resp.AlbumList.Album {
|
||||||
Expect(a.Title).ToNot(Equal("Symphony No. 9"))
|
Expect(a.Title).ToNot(Equal("Symphony No. 9"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,9 +115,9 @@ var _ = Describe("Search Endpoints", func() {
|
|||||||
|
|
||||||
Expect(resp.Status).To(Equal(responses.StatusOK))
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||||
Expect(resp.SearchResult3).ToNot(BeNil())
|
Expect(resp.SearchResult3).ToNot(BeNil())
|
||||||
Expect(resp.SearchResult3.Artist).To(HaveLen(5))
|
Expect(resp.SearchResult3.Artist).To(HaveLen(6))
|
||||||
Expect(resp.SearchResult3.Album).To(HaveLen(6))
|
Expect(resp.SearchResult3.Album).To(HaveLen(7))
|
||||||
Expect(resp.SearchResult3.Song).To(HaveLen(7))
|
Expect(resp.SearchResult3.Song).To(HaveLen(13))
|
||||||
})
|
})
|
||||||
|
|
||||||
It("finds across all entity types simultaneously", func() {
|
It("finds across all entity types simultaneously", func() {
|
||||||
|
|||||||
@@ -0,0 +1,464 @@
|
|||||||
|
package e2e
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/navidrome/navidrome/server/subsonic/responses"
|
||||||
|
. "github.com/onsi/ginkgo/v2"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client profile JSON bodies for getTranscodeDecision requests.
|
||||||
|
// All bitrate values are in bps (per OpenSubsonic spec).
|
||||||
|
const (
|
||||||
|
// mp3OnlyClient can direct-play mp3 and transcode to mp3
|
||||||
|
mp3OnlyClient = `{
|
||||||
|
"name": "test-mp3-only",
|
||||||
|
"directPlayProfiles": [
|
||||||
|
{"containers": ["mp3"], "audioCodecs": ["mp3"], "protocols": ["http"]}
|
||||||
|
],
|
||||||
|
"transcodingProfiles": [
|
||||||
|
{"container": "mp3", "audioCodec": "mp3", "protocol": "http"}
|
||||||
|
]
|
||||||
|
}`
|
||||||
|
|
||||||
|
// flacAndMp3Client can direct-play flac and mp3, transcode to mp3
|
||||||
|
flacAndMp3Client = `{
|
||||||
|
"name": "test-flac-mp3",
|
||||||
|
"directPlayProfiles": [
|
||||||
|
{"containers": ["flac"], "audioCodecs": ["flac"], "protocols": ["http"]},
|
||||||
|
{"containers": ["mp3"], "audioCodecs": ["mp3"], "protocols": ["http"]}
|
||||||
|
],
|
||||||
|
"transcodingProfiles": [
|
||||||
|
{"container": "mp3", "audioCodec": "mp3", "protocol": "http"}
|
||||||
|
]
|
||||||
|
}`
|
||||||
|
|
||||||
|
// universalClient can direct-play most formats
|
||||||
|
universalClient = `{
|
||||||
|
"name": "test-universal",
|
||||||
|
"directPlayProfiles": [
|
||||||
|
{"containers": ["mp3"], "audioCodecs": ["mp3"], "protocols": ["http"]},
|
||||||
|
{"containers": ["flac"], "audioCodecs": ["flac"], "protocols": ["http"]},
|
||||||
|
{"containers": ["m4a"], "audioCodecs": ["alac", "aac"], "protocols": ["http"]},
|
||||||
|
{"containers": ["opus", "ogg"], "audioCodecs": ["opus"], "protocols": ["http"]},
|
||||||
|
{"containers": ["wav"], "audioCodecs": ["pcm"], "protocols": ["http"]},
|
||||||
|
{"containers": ["dsf"], "audioCodecs": ["dsd"], "protocols": ["http"]}
|
||||||
|
],
|
||||||
|
"transcodingProfiles": [
|
||||||
|
{"container": "mp3", "audioCodec": "mp3", "protocol": "http"}
|
||||||
|
]
|
||||||
|
}`
|
||||||
|
|
||||||
|
// bitrateCapClient has maxAudioBitrate set to 320000 bps (320 kbps)
|
||||||
|
bitrateCapClient = `{
|
||||||
|
"name": "test-bitrate-cap",
|
||||||
|
"maxAudioBitrate": 320000,
|
||||||
|
"directPlayProfiles": [
|
||||||
|
{"containers": ["mp3"], "audioCodecs": ["mp3"], "protocols": ["http"]},
|
||||||
|
{"containers": ["flac"], "audioCodecs": ["flac"], "protocols": ["http"]}
|
||||||
|
],
|
||||||
|
"transcodingProfiles": [
|
||||||
|
{"container": "mp3", "audioCodec": "mp3", "protocol": "http"}
|
||||||
|
]
|
||||||
|
}`
|
||||||
|
|
||||||
|
// opusTranscodeClient can direct-play mp3, transcode to opus
|
||||||
|
opusTranscodeClient = `{
|
||||||
|
"name": "test-opus-transcode",
|
||||||
|
"directPlayProfiles": [
|
||||||
|
{"containers": ["mp3"], "audioCodecs": ["mp3"], "protocols": ["http"]}
|
||||||
|
],
|
||||||
|
"transcodingProfiles": [
|
||||||
|
{"container": "opus", "audioCodec": "opus", "protocol": "http"}
|
||||||
|
]
|
||||||
|
}`
|
||||||
|
|
||||||
|
// flacOnlyClient can direct-play flac, transcode to flac (no mp3 support at all)
|
||||||
|
flacOnlyClient = `{
|
||||||
|
"name": "test-flac-only",
|
||||||
|
"directPlayProfiles": [
|
||||||
|
{"containers": ["flac"], "audioCodecs": ["flac"], "protocols": ["http"]}
|
||||||
|
],
|
||||||
|
"transcodingProfiles": [
|
||||||
|
{"container": "flac", "audioCodec": "flac", "protocol": "http"}
|
||||||
|
]
|
||||||
|
}`
|
||||||
|
|
||||||
|
// maxTranscodeBitrateClient has maxTranscodingAudioBitrate set
|
||||||
|
maxTranscodeBitrateClient = `{
|
||||||
|
"name": "test-max-transcode-bitrate",
|
||||||
|
"maxTranscodingAudioBitrate": 192000,
|
||||||
|
"directPlayProfiles": [
|
||||||
|
{"containers": ["mp3"], "audioCodecs": ["mp3"], "protocols": ["http"]}
|
||||||
|
],
|
||||||
|
"transcodingProfiles": [
|
||||||
|
{"container": "mp3", "audioCodec": "mp3", "protocol": "http"}
|
||||||
|
]
|
||||||
|
}`
|
||||||
|
|
||||||
|
// dsdToFlacClient can direct-play mp3, transcode to flac
|
||||||
|
dsdToFlacClient = `{
|
||||||
|
"name": "test-dsd-to-flac",
|
||||||
|
"directPlayProfiles": [
|
||||||
|
{"containers": ["mp3"], "audioCodecs": ["mp3"], "protocols": ["http"]}
|
||||||
|
],
|
||||||
|
"transcodingProfiles": [
|
||||||
|
{"container": "flac", "audioCodec": "flac", "protocol": "http"}
|
||||||
|
]
|
||||||
|
}`
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = Describe("Transcode Endpoints", Ordered, func() {
|
||||||
|
// Track IDs resolved in BeforeAll
|
||||||
|
var (
|
||||||
|
mp3TrackID string // Come Together (mp3, 320kbps)
|
||||||
|
flacTrackID string // TC FLAC Standard (flac, 900kbps)
|
||||||
|
flacHiResTrackID string // TC FLAC HiRes (flac, 3000kbps)
|
||||||
|
alacTrackID string // TC ALAC Track (m4a, alac)
|
||||||
|
dsdTrackID string // TC DSD Track (dsf, dsd)
|
||||||
|
opusTrackID string // TC Opus Track (opus, 128kbps)
|
||||||
|
mkaOpusTrackID string // TC MKA Opus (mka, opus via codec tag)
|
||||||
|
)
|
||||||
|
|
||||||
|
BeforeAll(func() {
|
||||||
|
setupTestDB()
|
||||||
|
|
||||||
|
songs, err := ds.MediaFile(ctx).GetAll()
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
byTitle := map[string]string{}
|
||||||
|
for _, s := range songs {
|
||||||
|
byTitle[s.Title] = s.ID
|
||||||
|
}
|
||||||
|
ensureGetTrackID := func(title string) string {
|
||||||
|
id := byTitle[title]
|
||||||
|
Expect(id).ToNot(BeEmpty())
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
mp3TrackID = ensureGetTrackID("Come Together")
|
||||||
|
flacTrackID = ensureGetTrackID("TC FLAC Standard")
|
||||||
|
flacHiResTrackID = ensureGetTrackID("TC FLAC HiRes")
|
||||||
|
alacTrackID = ensureGetTrackID("TC ALAC Track")
|
||||||
|
dsdTrackID = ensureGetTrackID("TC DSD Track")
|
||||||
|
opusTrackID = ensureGetTrackID("TC Opus Track")
|
||||||
|
mkaOpusTrackID = ensureGetTrackID("TC MKA Opus")
|
||||||
|
})
|
||||||
|
|
||||||
|
Describe("getTranscodeDecision", func() {
|
||||||
|
Describe("error cases", func() {
|
||||||
|
It("returns 405 for GET request", func() {
|
||||||
|
w := doRawReq("getTranscodeDecision", "mediaId", mp3TrackID, "mediaType", "song")
|
||||||
|
Expect(w.Code).To(Equal(http.StatusMethodNotAllowed))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns error when mediaId is missing", func() {
|
||||||
|
resp := doPostReq("getTranscodeDecision", mp3OnlyClient, "mediaType", "song")
|
||||||
|
Expect(resp.Status).To(Equal(responses.StatusFailed))
|
||||||
|
Expect(resp.Error).ToNot(BeNil())
|
||||||
|
Expect(resp.Error.Code).To(Equal(responses.ErrorMissingParameter))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns error when mediaType is missing", func() {
|
||||||
|
resp := doPostReq("getTranscodeDecision", mp3OnlyClient, "mediaId", mp3TrackID)
|
||||||
|
Expect(resp.Status).To(Equal(responses.StatusFailed))
|
||||||
|
Expect(resp.Error).ToNot(BeNil())
|
||||||
|
Expect(resp.Error.Code).To(Equal(responses.ErrorMissingParameter))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns error for unsupported mediaType", func() {
|
||||||
|
resp := doPostReq("getTranscodeDecision", mp3OnlyClient, "mediaId", mp3TrackID, "mediaType", "video")
|
||||||
|
Expect(resp.Status).To(Equal(responses.StatusFailed))
|
||||||
|
Expect(resp.Error).ToNot(BeNil())
|
||||||
|
Expect(resp.Error.Code).To(Equal(responses.ErrorGeneric))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns error for invalid JSON body", func() {
|
||||||
|
resp := doPostReq("getTranscodeDecision", "{invalid-json", "mediaId", mp3TrackID, "mediaType", "song")
|
||||||
|
Expect(resp.Status).To(Equal(responses.StatusFailed))
|
||||||
|
Expect(resp.Error).ToNot(BeNil())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns error for empty JSON body", func() {
|
||||||
|
w := doRawPostReq("getTranscodeDecision", "", "mediaId", mp3TrackID, "mediaType", "song")
|
||||||
|
Expect(w.Code).To(Equal(http.StatusOK)) // Subsonic errors are returned as 200 with error status
|
||||||
|
resp := parseJSONResponse(w)
|
||||||
|
Expect(resp.Status).To(Equal(responses.StatusFailed))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns error for non-existent media ID", func() {
|
||||||
|
resp := doPostReq("getTranscodeDecision", mp3OnlyClient, "mediaId", "non-existent-id", "mediaType", "song")
|
||||||
|
Expect(resp.Status).To(Equal(responses.StatusFailed))
|
||||||
|
Expect(resp.Error).ToNot(BeNil())
|
||||||
|
Expect(resp.Error.Code).To(Equal(responses.ErrorDataNotFound))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns error for invalid protocol in body", func() {
|
||||||
|
invalidBody := `{
|
||||||
|
"directPlayProfiles": [
|
||||||
|
{"containers": ["mp3"], "audioCodecs": ["mp3"], "protocols": ["invalid-protocol"]}
|
||||||
|
]
|
||||||
|
}`
|
||||||
|
resp := doPostReq("getTranscodeDecision", invalidBody, "mediaId", mp3TrackID, "mediaType", "song")
|
||||||
|
Expect(resp.Status).To(Equal(responses.StatusFailed))
|
||||||
|
Expect(resp.Error).ToNot(BeNil())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns error for invalid comparison operator in body", func() {
|
||||||
|
invalidBody := `{
|
||||||
|
"directPlayProfiles": [
|
||||||
|
{"containers": ["mp3"], "audioCodecs": ["mp3"], "protocols": ["http"]}
|
||||||
|
],
|
||||||
|
"codecProfiles": [{
|
||||||
|
"type": "AudioCodec", "name": "mp3",
|
||||||
|
"limitations": [{"name": "audioBitrate", "comparison": "InvalidOp", "values": ["320000"]}]
|
||||||
|
}]
|
||||||
|
}`
|
||||||
|
resp := doPostReq("getTranscodeDecision", invalidBody, "mediaId", mp3TrackID, "mediaType", "song")
|
||||||
|
Expect(resp.Status).To(Equal(responses.StatusFailed))
|
||||||
|
Expect(resp.Error).ToNot(BeNil())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Describe("direct play decisions", func() {
|
||||||
|
It("allows MP3 direct play when client supports mp3", func() {
|
||||||
|
resp := doPostReq("getTranscodeDecision", mp3OnlyClient, "mediaId", mp3TrackID, "mediaType", "song")
|
||||||
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||||
|
Expect(resp.TranscodeDecision).ToNot(BeNil())
|
||||||
|
Expect(resp.TranscodeDecision.CanDirectPlay).To(BeTrue())
|
||||||
|
Expect(resp.TranscodeDecision.TranscodeStream).To(BeNil())
|
||||||
|
Expect(resp.TranscodeDecision.TranscodeParams).ToNot(BeEmpty())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("allows FLAC direct play when client supports flac", func() {
|
||||||
|
resp := doPostReq("getTranscodeDecision", flacAndMp3Client, "mediaId", flacTrackID, "mediaType", "song")
|
||||||
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||||
|
Expect(resp.TranscodeDecision).ToNot(BeNil())
|
||||||
|
Expect(resp.TranscodeDecision.CanDirectPlay).To(BeTrue())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("allows ALAC direct play via m4a container + alac codec matching", func() {
|
||||||
|
resp := doPostReq("getTranscodeDecision", universalClient, "mediaId", alacTrackID, "mediaType", "song")
|
||||||
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||||
|
Expect(resp.TranscodeDecision).ToNot(BeNil())
|
||||||
|
Expect(resp.TranscodeDecision.CanDirectPlay).To(BeTrue())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("allows Opus direct play when client supports opus", func() {
|
||||||
|
resp := doPostReq("getTranscodeDecision", universalClient, "mediaId", opusTrackID, "mediaType", "song")
|
||||||
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||||
|
Expect(resp.TranscodeDecision).ToNot(BeNil())
|
||||||
|
Expect(resp.TranscodeDecision.CanDirectPlay).To(BeTrue())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("denies direct play when container mismatches", func() {
|
||||||
|
// mp3OnlyClient cannot play FLAC container
|
||||||
|
resp := doPostReq("getTranscodeDecision", mp3OnlyClient, "mediaId", flacTrackID, "mediaType", "song")
|
||||||
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||||
|
Expect(resp.TranscodeDecision).ToNot(BeNil())
|
||||||
|
Expect(resp.TranscodeDecision.CanDirectPlay).To(BeFalse())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("denies direct play when codec mismatches", func() {
|
||||||
|
// MKA container with opus codec — client only supports mp3
|
||||||
|
resp := doPostReq("getTranscodeDecision", mp3OnlyClient, "mediaId", mkaOpusTrackID, "mediaType", "song")
|
||||||
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||||
|
Expect(resp.TranscodeDecision).ToNot(BeNil())
|
||||||
|
Expect(resp.TranscodeDecision.CanDirectPlay).To(BeFalse())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("denies direct play when maxAudioBitrate exceeded", func() {
|
||||||
|
// bitrateCapClient caps at 320kbps, FLAC is 900kbps
|
||||||
|
resp := doPostReq("getTranscodeDecision", bitrateCapClient, "mediaId", flacTrackID, "mediaType", "song")
|
||||||
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||||
|
Expect(resp.TranscodeDecision).ToNot(BeNil())
|
||||||
|
Expect(resp.TranscodeDecision.CanDirectPlay).To(BeFalse())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Describe("transcode decisions", func() {
|
||||||
|
It("transcodes FLAC to MP3 when client only supports MP3", func() {
|
||||||
|
resp := doPostReq("getTranscodeDecision", mp3OnlyClient, "mediaId", flacTrackID, "mediaType", "song")
|
||||||
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||||
|
Expect(resp.TranscodeDecision).ToNot(BeNil())
|
||||||
|
Expect(resp.TranscodeDecision.CanDirectPlay).To(BeFalse())
|
||||||
|
Expect(resp.TranscodeDecision.CanTranscode).To(BeTrue())
|
||||||
|
Expect(resp.TranscodeDecision.TranscodeStream).ToNot(BeNil())
|
||||||
|
Expect(resp.TranscodeDecision.TranscodeStream.Container).To(Equal("mp3"))
|
||||||
|
Expect(resp.TranscodeDecision.TranscodeStream.Codec).To(Equal("mp3"))
|
||||||
|
Expect(resp.TranscodeDecision.TranscodeParams).ToNot(BeEmpty())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("transcodes FLAC hi-res to Opus with correct sample rate", func() {
|
||||||
|
resp := doPostReq("getTranscodeDecision", opusTranscodeClient, "mediaId", flacHiResTrackID, "mediaType", "song")
|
||||||
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||||
|
Expect(resp.TranscodeDecision).ToNot(BeNil())
|
||||||
|
Expect(resp.TranscodeDecision.CanTranscode).To(BeTrue())
|
||||||
|
Expect(resp.TranscodeDecision.TranscodeStream).ToNot(BeNil())
|
||||||
|
Expect(resp.TranscodeDecision.TranscodeStream.Codec).To(Equal("opus"))
|
||||||
|
// Opus always outputs 48000 Hz
|
||||||
|
Expect(resp.TranscodeDecision.TranscodeStream.AudioSamplerate).To(Equal(int32(48000)))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("transcodes DSD to FLAC with normalized sample rate and bit depth", func() {
|
||||||
|
resp := doPostReq("getTranscodeDecision", dsdToFlacClient, "mediaId", dsdTrackID, "mediaType", "song")
|
||||||
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||||
|
Expect(resp.TranscodeDecision).ToNot(BeNil())
|
||||||
|
Expect(resp.TranscodeDecision.CanTranscode).To(BeTrue())
|
||||||
|
Expect(resp.TranscodeDecision.TranscodeStream).ToNot(BeNil())
|
||||||
|
Expect(resp.TranscodeDecision.TranscodeStream.Codec).To(Equal("flac"))
|
||||||
|
// DSD sample rate normalized: 2822400 / 8 = 352800
|
||||||
|
Expect(resp.TranscodeDecision.TranscodeStream.AudioSamplerate).To(Equal(int32(352800)))
|
||||||
|
// DSD 1-bit → 24-bit PCM
|
||||||
|
Expect(resp.TranscodeDecision.TranscodeStream.AudioBitdepth).To(Equal(int32(24)))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("refuses lossy to lossless transcoding: MP3 to FLAC", func() {
|
||||||
|
// flacOnlyClient can't direct-play mp3, and lossy→lossless transcode is rejected
|
||||||
|
resp := doPostReq("getTranscodeDecision", flacOnlyClient, "mediaId", mp3TrackID, "mediaType", "song")
|
||||||
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||||
|
Expect(resp.TranscodeDecision).ToNot(BeNil())
|
||||||
|
// MP3 is lossy, FLAC is lossless — should not allow transcoding
|
||||||
|
Expect(resp.TranscodeDecision.CanTranscode).To(BeFalse())
|
||||||
|
Expect(resp.TranscodeDecision.CanDirectPlay).To(BeFalse())
|
||||||
|
Expect(resp.TranscodeDecision.TranscodeParams).To(BeEmpty())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("caps transcode bitrate via maxTranscodingAudioBitrate", func() {
|
||||||
|
resp := doPostReq("getTranscodeDecision", maxTranscodeBitrateClient, "mediaId", flacTrackID, "mediaType", "song")
|
||||||
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||||
|
Expect(resp.TranscodeDecision).ToNot(BeNil())
|
||||||
|
Expect(resp.TranscodeDecision.CanTranscode).To(BeTrue())
|
||||||
|
Expect(resp.TranscodeDecision.TranscodeStream).ToNot(BeNil())
|
||||||
|
// maxTranscodingAudioBitrate is 192000 bps = 192 kbps → response in bps
|
||||||
|
Expect(resp.TranscodeDecision.TranscodeStream.AudioBitrate).To(Equal(int32(192000)))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Describe("response structure", func() {
|
||||||
|
It("has correct sourceStream details", func() {
|
||||||
|
resp := doPostReq("getTranscodeDecision", universalClient, "mediaId", flacTrackID, "mediaType", "song")
|
||||||
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||||
|
Expect(resp.TranscodeDecision).ToNot(BeNil())
|
||||||
|
src := resp.TranscodeDecision.SourceStream
|
||||||
|
Expect(src).ToNot(BeNil())
|
||||||
|
Expect(src.Container).To(Equal("flac"))
|
||||||
|
Expect(src.Codec).To(Equal("flac"))
|
||||||
|
// AudioBitrate is in bps: 900 kbps * 1000 = 900000 bps
|
||||||
|
Expect(src.AudioBitrate).To(Equal(int32(900000)))
|
||||||
|
Expect(src.AudioSamplerate).To(Equal(int32(44100)))
|
||||||
|
Expect(src.AudioChannels).To(Equal(int32(2)))
|
||||||
|
Expect(src.Protocol).To(Equal("http"))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("reports audioBitrate in bps (kbps * 1000)", func() {
|
||||||
|
resp := doPostReq("getTranscodeDecision", universalClient, "mediaId", mp3TrackID, "mediaType", "song")
|
||||||
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||||
|
src := resp.TranscodeDecision.SourceStream
|
||||||
|
Expect(src).ToNot(BeNil())
|
||||||
|
// MP3 is 320 kbps → 320000 bps
|
||||||
|
Expect(src.AudioBitrate).To(Equal(int32(320000)))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Describe("getTranscodeStream", func() {
|
||||||
|
Describe("error cases", func() {
|
||||||
|
It("returns 400 when mediaId is missing", func() {
|
||||||
|
w := doRawReq("getTranscodeStream", "mediaType", "song", "transcodeParams", "some-token")
|
||||||
|
Expect(w.Code).To(Equal(http.StatusBadRequest))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns 400 when mediaType is missing", func() {
|
||||||
|
w := doRawReq("getTranscodeStream", "mediaId", mp3TrackID, "transcodeParams", "some-token")
|
||||||
|
Expect(w.Code).To(Equal(http.StatusBadRequest))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns 400 when transcodeParams is missing", func() {
|
||||||
|
w := doRawReq("getTranscodeStream", "mediaId", mp3TrackID, "mediaType", "song")
|
||||||
|
Expect(w.Code).To(Equal(http.StatusBadRequest))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns 400 for unsupported mediaType", func() {
|
||||||
|
w := doRawReq("getTranscodeStream", "mediaId", mp3TrackID, "mediaType", "video", "transcodeParams", "some-token")
|
||||||
|
Expect(w.Code).To(Equal(http.StatusBadRequest))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns 410 for malformed token", func() {
|
||||||
|
w := doRawReq("getTranscodeStream", "mediaId", mp3TrackID, "mediaType", "song", "transcodeParams", "invalid-token")
|
||||||
|
Expect(w.Code).To(Equal(http.StatusGone))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns 410 for stale token (media file updated after token issued)", func() {
|
||||||
|
// Get a valid decision token
|
||||||
|
resp := doPostReq("getTranscodeDecision", mp3OnlyClient, "mediaId", mp3TrackID, "mediaType", "song")
|
||||||
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||||
|
Expect(resp.TranscodeDecision).ToNot(BeNil())
|
||||||
|
token := resp.TranscodeDecision.TranscodeParams
|
||||||
|
Expect(token).ToNot(BeEmpty())
|
||||||
|
|
||||||
|
// Save original UpdatedAt and restore after test
|
||||||
|
mf, err := ds.MediaFile(ctx).Get(mp3TrackID)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
originalUpdatedAt := mf.UpdatedAt
|
||||||
|
|
||||||
|
// Update the media file's UpdatedAt to simulate a change after token issuance
|
||||||
|
mf.UpdatedAt = time.Now().Add(time.Hour)
|
||||||
|
Expect(ds.MediaFile(ctx).Put(mf)).To(Succeed())
|
||||||
|
|
||||||
|
// Attempt to stream with the now-stale token
|
||||||
|
w := doRawReq("getTranscodeStream", "mediaId", mp3TrackID, "mediaType", "song", "transcodeParams", token)
|
||||||
|
Expect(w.Code).To(Equal(http.StatusGone))
|
||||||
|
|
||||||
|
// Restore original UpdatedAt
|
||||||
|
mf.UpdatedAt = originalUpdatedAt
|
||||||
|
Expect(ds.MediaFile(ctx).Put(mf)).To(Succeed())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Describe("round-trip: decision then stream", func() {
|
||||||
|
It("streams direct play for MP3", func() {
|
||||||
|
// Get decision
|
||||||
|
resp := doPostReq("getTranscodeDecision", mp3OnlyClient, "mediaId", mp3TrackID, "mediaType", "song")
|
||||||
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||||
|
Expect(resp.TranscodeDecision.CanDirectPlay).To(BeTrue())
|
||||||
|
token := resp.TranscodeDecision.TranscodeParams
|
||||||
|
Expect(token).ToNot(BeEmpty())
|
||||||
|
|
||||||
|
// Stream using the token
|
||||||
|
w := doRawReq("getTranscodeStream", "mediaId", mp3TrackID, "mediaType", "song", "transcodeParams", token)
|
||||||
|
Expect(w.Code).To(Equal(http.StatusOK))
|
||||||
|
// Direct play: format should be "raw" or empty
|
||||||
|
Expect(spy.LastRequest.Format).To(BeElementOf("raw", ""))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("streams transcoded FLAC to MP3", func() {
|
||||||
|
// Get decision
|
||||||
|
resp := doPostReq("getTranscodeDecision", mp3OnlyClient, "mediaId", flacTrackID, "mediaType", "song")
|
||||||
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||||
|
Expect(resp.TranscodeDecision.CanTranscode).To(BeTrue())
|
||||||
|
token := resp.TranscodeDecision.TranscodeParams
|
||||||
|
Expect(token).ToNot(BeEmpty())
|
||||||
|
|
||||||
|
// Stream using the token
|
||||||
|
w := doRawReq("getTranscodeStream", "mediaId", flacTrackID, "mediaType", "song", "transcodeParams", token)
|
||||||
|
Expect(w.Code).To(Equal(http.StatusOK))
|
||||||
|
Expect(spy.LastRequest.Format).To(Equal("mp3"))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("passes offset through to stream request", func() {
|
||||||
|
// Get decision
|
||||||
|
resp := doPostReq("getTranscodeDecision", mp3OnlyClient, "mediaId", mp3TrackID, "mediaType", "song")
|
||||||
|
Expect(resp.Status).To(Equal(responses.StatusOK))
|
||||||
|
token := resp.TranscodeDecision.TranscodeParams
|
||||||
|
Expect(token).ToNot(BeEmpty())
|
||||||
|
|
||||||
|
// Stream with offset
|
||||||
|
w := doRawReq("getTranscodeStream", "mediaId", mp3TrackID, "mediaType", "song",
|
||||||
|
"transcodeParams", token, "offset", "30")
|
||||||
|
Expect(w.Code).To(Equal(http.StatusOK))
|
||||||
|
Expect(spy.LastRequest.Offset).To(Equal(30))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user