feat(transcoding): add enums for protocol, comparison operators, limitations, and codec profiles in transcode decision logic

Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Deluan 2026-02-05 18:17:48 -05:00
parent 07e2f699da
commit 2e00479a8b
4 changed files with 250 additions and 74 deletions

View File

@ -64,6 +64,34 @@ type Limitation struct {
Required bool
}
// Protocol values (OpenSubsonic spec enum)
const (
ProtocolHTTP = "http"
ProtocolHLS = "hls"
)
// Comparison operators (OpenSubsonic spec enum)
const (
ComparisonEquals = "Equals"
ComparisonNotEquals = "NotEquals"
ComparisonLessThanEqual = "LessThanEqual"
ComparisonGreaterThanEqual = "GreaterThanEqual"
)
// Limitation names (OpenSubsonic spec enum)
const (
LimitationAudioChannels = "audioChannels"
LimitationAudioBitrate = "audioBitrate"
LimitationAudioProfile = "audioProfile"
LimitationAudioSamplerate = "audioSamplerate"
LimitationAudioBitdepth = "audioBitdepth"
)
// Codec profile types (OpenSubsonic spec enum)
const (
CodecProfileTypeAudio = "AudioCodec"
)
// Decision represents the internal decision result.
// All bitrate values are in kilobits per second (kbps).
type Decision struct {
@ -84,6 +112,7 @@ type Decision struct {
type StreamDetails struct {
Container string
Codec string
Profile string // Audio profile (e.g., "LC", "HE-AAC"). Empty until scanner support is added.
Bitrate int
SampleRate int
BitDepth int
@ -179,7 +208,7 @@ func (s *transcodeDecisionService) MakeDecision(ctx context.Context, mf *model.M
// or a typed reason string if it doesn't match.
func (s *transcodeDecisionService) checkDirectPlayProfile(mf *model.MediaFile, sourceBitrate int, profile *DirectPlayProfile, clientInfo *ClientInfo) string {
// Check protocol (only http for now)
if len(profile.Protocols) > 0 && !containsIgnoreCase(profile.Protocols, "http") {
if len(profile.Protocols) > 0 && !containsIgnoreCase(profile.Protocols, ProtocolHTTP) {
return "protocol not supported"
}
@ -200,7 +229,7 @@ func (s *transcodeDecisionService) checkDirectPlayProfile(mf *model.MediaFile, s
// Check codec-specific limitations
for _, codecProfile := range clientInfo.CodecProfiles {
if strings.EqualFold(codecProfile.Type, "AudioCodec") && matchesCodec(mf.AudioCodec(), []string{codecProfile.Name}) {
if strings.EqualFold(codecProfile.Type, CodecProfileTypeAudio) && matchesCodec(mf.AudioCodec(), []string{codecProfile.Name}) {
if reason := checkLimitations(mf, sourceBitrate, codecProfile.Limitations); reason != "" {
return reason
}
@ -214,31 +243,37 @@ func (s *transcodeDecisionService) checkDirectPlayProfile(mf *model.MediaFile, s
// Returns "" if all limitations pass, or a typed reason string for the first failure.
func checkLimitations(mf *model.MediaFile, sourceBitrate int, limitations []Limitation) string {
for _, lim := range limitations {
switch strings.ToLower(lim.Name) {
case "audiochannels":
if strings.EqualFold(lim.Name, LimitationAudioChannels) {
if !checkIntLimitation(mf.Channels, lim.Comparison, lim.Values) {
if lim.Required {
return "audio channels not supported"
}
}
case "audiosamplerate":
} else if strings.EqualFold(lim.Name, LimitationAudioSamplerate) {
if !checkIntLimitation(mf.SampleRate, lim.Comparison, lim.Values) {
if lim.Required {
return "audio samplerate not supported"
}
}
case "audiobitrate":
} else if strings.EqualFold(lim.Name, LimitationAudioBitrate) {
if !checkIntLimitation(sourceBitrate, lim.Comparison, lim.Values) {
if lim.Required {
return "audio bitrate not supported"
}
}
case "audiobitdepth":
} else if strings.EqualFold(lim.Name, LimitationAudioBitdepth) {
if !checkIntLimitation(mf.BitDepth, lim.Comparison, lim.Values) {
if lim.Required {
return "audio bitdepth not supported"
}
}
} else if strings.EqualFold(lim.Name, LimitationAudioProfile) {
// TODO: populate source profile when MediaFile has audio profile info
if !checkStringLimitation("", lim.Comparison, lim.Values) {
if lim.Required {
return "audio profile not supported"
}
}
}
}
return ""
@ -257,7 +292,7 @@ const (
// Returns nil if the profile cannot produce a valid output.
func (s *transcodeDecisionService) computeTranscodedStream(ctx context.Context, mf *model.MediaFile, sourceBitrate int, profile *TranscodingProfile, clientInfo *ClientInfo) *StreamDetails {
// Check protocol (only http for now)
if profile.Protocol != "" && !strings.EqualFold(profile.Protocol, "http") {
if profile.Protocol != "" && !strings.EqualFold(profile.Protocol, ProtocolHTTP) {
return nil
}
@ -324,7 +359,7 @@ func (s *transcodeDecisionService) computeTranscodedStream(ctx context.Context,
// Apply codec profile limitations to the TARGET codec (#4)
targetCodec := ts.Codec
for _, codecProfile := range clientInfo.CodecProfiles {
if !strings.EqualFold(codecProfile.Type, "AudioCodec") {
if !strings.EqualFold(codecProfile.Type, CodecProfileTypeAudio) {
continue
}
if !matchesCodec(targetCodec, []string{codecProfile.Name}) {
@ -333,7 +368,7 @@ func (s *transcodeDecisionService) computeTranscodedStream(ctx context.Context,
for _, lim := range codecProfile.Limitations {
result := applyLimitation(sourceBitrate, &lim, ts)
// For lossless codecs, adjusting bitrate is not valid
if strings.EqualFold(lim.Name, "audiobitrate") && targetIsLossless && result == adjustAdjusted {
if strings.EqualFold(lim.Name, LimitationAudioBitrate) && targetIsLossless && result == adjustAdjusted {
return nil
}
if result == adjustCannotFit {
@ -348,22 +383,22 @@ func (s *transcodeDecisionService) computeTranscodedStream(ctx context.Context,
// applyLimitation adjusts a transcoded stream parameter to satisfy the limitation.
// Returns the adjustment result.
func applyLimitation(sourceBitrate int, lim *Limitation, ts *StreamDetails) adjustResult {
switch strings.ToLower(lim.Name) {
case "audiochannels":
current := ts.Channels
return applyIntLimitation(lim.Comparison, lim.Values, current, func(v int) { ts.Channels = v })
case "audiobitrate":
if strings.EqualFold(lim.Name, LimitationAudioChannels) {
return applyIntLimitation(lim.Comparison, lim.Values, ts.Channels, func(v int) { ts.Channels = v })
} else if strings.EqualFold(lim.Name, LimitationAudioBitrate) {
current := ts.Bitrate
if current == 0 {
current = sourceBitrate
}
return applyIntLimitation(lim.Comparison, lim.Values, current, func(v int) { ts.Bitrate = v })
case "audiosamplerate":
} else if strings.EqualFold(lim.Name, LimitationAudioSamplerate) {
return applyIntLimitation(lim.Comparison, lim.Values, ts.SampleRate, func(v int) { ts.SampleRate = v })
case "audiobitdepth":
} else if strings.EqualFold(lim.Name, LimitationAudioBitdepth) {
if ts.BitDepth > 0 {
return applyIntLimitation(lim.Comparison, lim.Values, ts.BitDepth, func(v int) { ts.BitDepth = v })
}
} else if strings.EqualFold(lim.Name, LimitationAudioProfile) {
// TODO: implement when audio profile data is available
}
return adjustNone
}
@ -375,8 +410,7 @@ func applyIntLimitation(comparison string, values []string, current int, setter
return adjustNone
}
switch strings.ToLower(comparison) {
case "lessthanequal":
if strings.EqualFold(comparison, ComparisonLessThanEqual) {
limit, ok := parseInt(values[0])
if !ok {
return adjustNone
@ -386,8 +420,7 @@ func applyIntLimitation(comparison string, values []string, current int, setter
}
setter(limit)
return adjustAdjusted
case "greaterthanequal":
} else if strings.EqualFold(comparison, ComparisonGreaterThanEqual) {
limit, ok := parseInt(values[0])
if !ok {
return adjustNone
@ -397,8 +430,7 @@ func applyIntLimitation(comparison string, values []string, current int, setter
}
// Cannot upscale
return adjustCannotFit
case "equals":
} else if strings.EqualFold(comparison, ComparisonEquals) {
// Check if current value matches any allowed value
for _, v := range values {
if limit, ok := parseInt(v); ok && current == limit {
@ -421,8 +453,7 @@ func applyIntLimitation(comparison string, values []string, current int, setter
return adjustAdjusted
}
return adjustCannotFit
case "notequals":
} else if strings.EqualFold(comparison, ComparisonNotEquals) {
for _, v := range values {
if limit, ok := parseInt(v); ok && current == limit {
return adjustCannotFit
@ -573,36 +604,56 @@ func checkIntLimitation(value int, comparison string, values []string) bool {
return true
}
switch strings.ToLower(comparison) {
case "lessthanequal":
if strings.EqualFold(comparison, ComparisonLessThanEqual) {
limit, ok := parseInt(values[0])
if !ok {
return true
}
return value <= limit
case "greaterthanequal":
} else if strings.EqualFold(comparison, ComparisonGreaterThanEqual) {
limit, ok := parseInt(values[0])
if !ok {
return true
}
return value >= limit
case "equals":
} else if strings.EqualFold(comparison, ComparisonEquals) {
for _, v := range values {
if limit, ok := parseInt(v); ok && value == limit {
return true
}
}
return false
case "notequals":
} else if strings.EqualFold(comparison, ComparisonNotEquals) {
for _, v := range values {
if limit, ok := parseInt(v); ok && value == limit {
return false
}
}
return true
default:
}
return true
}
// checkStringLimitation checks a string value against a limitation.
// Only Equals and NotEquals comparisons are meaningful for strings.
// LessThanEqual/GreaterThanEqual are not applicable and always pass.
func checkStringLimitation(value string, comparison string, values []string) bool {
if strings.EqualFold(comparison, ComparisonEquals) {
for _, v := range values {
if strings.EqualFold(value, v) {
return true
}
}
return false
} else if strings.EqualFold(comparison, ComparisonNotEquals) {
for _, v := range values {
if strings.EqualFold(value, v) {
return false
}
}
return true
}
return true
}
func parseInt(s string) (int, bool) {

View File

@ -331,10 +331,10 @@ var _ = Describe("TranscodeDecision", func() {
},
CodecProfiles: []CodecProfile{
{
Type: "AudioCodec",
Type: CodecProfileTypeAudio,
Name: "mp3",
Limitations: []Limitation{
{Name: "audioBitrate", Comparison: "LessThanEqual", Values: []string{"320"}, Required: true},
{Name: LimitationAudioBitrate, Comparison: ComparisonLessThanEqual, Values: []string{"320"}, Required: true},
},
},
},
@ -353,10 +353,10 @@ var _ = Describe("TranscodeDecision", func() {
},
CodecProfiles: []CodecProfile{
{
Type: "AudioCodec",
Type: CodecProfileTypeAudio,
Name: "mp3",
Limitations: []Limitation{
{Name: "audioBitrate", Comparison: "LessThanEqual", Values: []string{"320"}, Required: false},
{Name: LimitationAudioBitrate, Comparison: ComparisonLessThanEqual, Values: []string{"320"}, Required: false},
},
},
},
@ -374,10 +374,10 @@ var _ = Describe("TranscodeDecision", func() {
},
CodecProfiles: []CodecProfile{
{
Type: "AudioCodec",
Type: CodecProfileTypeAudio,
Name: "flac",
Limitations: []Limitation{
{Name: "audioChannels", Comparison: "Equals", Values: []string{"1", "2"}, Required: true},
{Name: LimitationAudioChannels, Comparison: ComparisonEquals, Values: []string{"1", "2"}, Required: true},
},
},
},
@ -395,10 +395,10 @@ var _ = Describe("TranscodeDecision", func() {
},
CodecProfiles: []CodecProfile{
{
Type: "AudioCodec",
Type: CodecProfileTypeAudio,
Name: "flac",
Limitations: []Limitation{
{Name: "audioChannels", Comparison: "Equals", Values: []string{"1", "2"}, Required: true},
{Name: LimitationAudioChannels, Comparison: ComparisonEquals, Values: []string{"1", "2"}, Required: true},
},
},
},
@ -408,6 +408,50 @@ var _ = Describe("TranscodeDecision", func() {
Expect(decision.CanDirectPlay).To(BeFalse())
})
It("rejects direct play when audioProfile limitation fails (required)", func() {
mf := &model.MediaFile{ID: "1", Suffix: "m4a", Codec: "AAC", BitRate: 256, Channels: 2, SampleRate: 44100}
ci := &ClientInfo{
DirectPlayProfiles: []DirectPlayProfile{
{Containers: []string{"m4a"}, AudioCodecs: []string{"aac"}, Protocols: []string{"http"}},
},
CodecProfiles: []CodecProfile{
{
Type: CodecProfileTypeAudio,
Name: "aac",
Limitations: []Limitation{
{Name: LimitationAudioProfile, Comparison: ComparisonEquals, Values: []string{"LC"}, Required: true},
},
},
},
}
// Source profile is empty (not yet populated from scanner), so Equals("LC") fails
decision, err := svc.MakeDecision(ctx, mf, ci)
Expect(err).ToNot(HaveOccurred())
Expect(decision.CanDirectPlay).To(BeFalse())
Expect(decision.TranscodeReasons).To(ContainElement("audio profile not supported"))
})
It("allows direct play when audioProfile limitation is optional", func() {
mf := &model.MediaFile{ID: "1", Suffix: "m4a", Codec: "AAC", BitRate: 256, Channels: 2, SampleRate: 44100}
ci := &ClientInfo{
DirectPlayProfiles: []DirectPlayProfile{
{Containers: []string{"m4a"}, AudioCodecs: []string{"aac"}, Protocols: []string{"http"}},
},
CodecProfiles: []CodecProfile{
{
Type: CodecProfileTypeAudio,
Name: "aac",
Limitations: []Limitation{
{Name: LimitationAudioProfile, Comparison: ComparisonEquals, Values: []string{"LC"}, Required: false},
},
},
},
}
decision, err := svc.MakeDecision(ctx, mf, ci)
Expect(err).ToNot(HaveOccurred())
Expect(decision.CanDirectPlay).To(BeTrue())
})
It("rejects direct play due to samplerate limitation", func() {
mf := &model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 96000, BitDepth: 24}
ci := &ClientInfo{
@ -416,10 +460,10 @@ var _ = Describe("TranscodeDecision", func() {
},
CodecProfiles: []CodecProfile{
{
Type: "AudioCodec",
Type: CodecProfileTypeAudio,
Name: "flac",
Limitations: []Limitation{
{Name: "audioSamplerate", Comparison: "LessThanEqual", Values: []string{"48000"}, Required: true},
{Name: LimitationAudioSamplerate, Comparison: ComparisonLessThanEqual, Values: []string{"48000"}, Required: true},
},
},
},
@ -441,10 +485,10 @@ var _ = Describe("TranscodeDecision", func() {
},
CodecProfiles: []CodecProfile{
{
Type: "AudioCodec",
Type: CodecProfileTypeAudio,
Name: "mp3",
Limitations: []Limitation{
{Name: "audioBitrate", Comparison: "LessThanEqual", Values: []string{"96"}, Required: true},
{Name: LimitationAudioBitrate, Comparison: ComparisonLessThanEqual, Values: []string{"96"}, Required: true},
},
},
},
@ -464,10 +508,10 @@ var _ = Describe("TranscodeDecision", func() {
},
CodecProfiles: []CodecProfile{
{
Type: "AudioCodec",
Type: CodecProfileTypeAudio,
Name: "mp3",
Limitations: []Limitation{
{Name: "audioChannels", Comparison: "LessThanEqual", Values: []string{"2"}, Required: true},
{Name: LimitationAudioChannels, Comparison: ComparisonLessThanEqual, Values: []string{"2"}, Required: true},
},
},
},
@ -487,10 +531,10 @@ var _ = Describe("TranscodeDecision", func() {
},
CodecProfiles: []CodecProfile{
{
Type: "AudioCodec",
Type: CodecProfileTypeAudio,
Name: "mp3",
Limitations: []Limitation{
{Name: "audioSamplerate", Comparison: "LessThanEqual", Values: []string{"48000"}, Required: true},
{Name: LimitationAudioSamplerate, Comparison: ComparisonLessThanEqual, Values: []string{"48000"}, Required: true},
},
},
},
@ -510,10 +554,10 @@ var _ = Describe("TranscodeDecision", func() {
},
CodecProfiles: []CodecProfile{
{
Type: "AudioCodec",
Type: CodecProfileTypeAudio,
Name: "mp3",
Limitations: []Limitation{
{Name: "audioSamplerate", Comparison: "GreaterThanEqual", Values: []string{"96000"}, Required: true},
{Name: LimitationAudioSamplerate, Comparison: ComparisonGreaterThanEqual, Values: []string{"96000"}, Required: true},
},
},
},

View File

@ -2,6 +2,7 @@ package subsonic
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
@ -97,7 +98,7 @@ func (r *clientInfoRequest) toCore() *core.ClientInfo {
Required: lim.Required,
}
// Convert audioBitrate limitation values from bps to kbps
if strings.EqualFold(lim.Name, "audioBitrate") {
if strings.EqualFold(lim.Name, core.LimitationAudioBitrate) {
coreLim.Values = convertBitrateValues(lim.Values)
}
coreCP.Limitations = append(coreCP.Limitations, coreLim)
@ -132,6 +133,59 @@ func convertBitrateValues(bpsValues []string) []string {
return result
}
// validate checks that all enum fields in the request contain valid values per the OpenSubsonic spec.
func (r *clientInfoRequest) validate() error {
for _, dp := range r.DirectPlayProfiles {
for _, p := range dp.Protocols {
if !isValidProtocol(p) {
return fmt.Errorf("invalid protocol: %s", p)
}
}
}
for _, tp := range r.TranscodingProfiles {
if tp.Protocol != "" && !isValidProtocol(tp.Protocol) {
return fmt.Errorf("invalid protocol: %s", tp.Protocol)
}
}
for _, cp := range r.CodecProfiles {
if !isValidCodecProfileType(cp.Type) {
return fmt.Errorf("invalid codec profile type: %s", cp.Type)
}
for _, lim := range cp.Limitations {
if !isValidLimitationName(lim.Name) {
return fmt.Errorf("invalid limitation name: %s", lim.Name)
}
if !isValidComparison(lim.Comparison) {
return fmt.Errorf("invalid comparison: %s", lim.Comparison)
}
}
}
return nil
}
func isValidProtocol(p string) bool {
return strings.EqualFold(p, core.ProtocolHTTP) || strings.EqualFold(p, core.ProtocolHLS)
}
func isValidCodecProfileType(t string) bool {
return strings.EqualFold(t, core.CodecProfileTypeAudio)
}
func isValidLimitationName(n string) bool {
return strings.EqualFold(n, core.LimitationAudioChannels) ||
strings.EqualFold(n, core.LimitationAudioBitrate) ||
strings.EqualFold(n, core.LimitationAudioProfile) ||
strings.EqualFold(n, core.LimitationAudioSamplerate) ||
strings.EqualFold(n, core.LimitationAudioBitdepth)
}
func isValidComparison(c string) bool {
return strings.EqualFold(c, core.ComparisonEquals) ||
strings.EqualFold(c, core.ComparisonNotEquals) ||
strings.EqualFold(c, core.ComparisonLessThanEqual) ||
strings.EqualFold(c, core.ComparisonGreaterThanEqual)
}
// GetTranscodeDecision handles the OpenSubsonic getTranscodeDecision endpoint.
// It receives client capabilities and returns a decision on whether to direct play or transcode.
func (api *Router) GetTranscodeDecision(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) {
@ -159,13 +213,16 @@ func (api *Router) GetTranscodeDecision(w http.ResponseWriter, r *http.Request)
return nil, newError(responses.ErrorGeneric, "mediaType '%s' is not yet supported", mediaType)
}
// Parse ClientInfo from request body
// Parse and validate ClientInfo from request body (required per OpenSubsonic spec)
var clientInfoReq clientInfoRequest
if r.Body != nil {
if err := json.NewDecoder(r.Body).Decode(&clientInfoReq); err != nil {
log.Debug(ctx, "Failed to parse client info from body", err)
// Continue with empty client info - will likely result in no compatible profile
}
if r.Body == nil {
return nil, newError(responses.ErrorMissingParameter, "missing required JSON request body")
}
if err := json.NewDecoder(r.Body).Decode(&clientInfoReq); err != nil {
return nil, newError(responses.ErrorGeneric, "invalid JSON request body")
}
if err := clientInfoReq.validate(); err != nil {
return nil, newError(responses.ErrorGeneric, "%v", err)
}
clientInfo := clientInfoReq.toCore()
@ -200,6 +257,7 @@ func (api *Router) GetTranscodeDecision(w http.ResponseWriter, r *http.Request)
Container: decision.SourceStream.Container,
Codec: decision.SourceStream.Codec,
AudioBitrate: int32(kbpsToBps(decision.SourceStream.Bitrate)),
AudioProfile: decision.SourceStream.Profile,
AudioSamplerate: int32(decision.SourceStream.SampleRate),
AudioBitdepth: int32(decision.SourceStream.BitDepth),
AudioChannels: int32(decision.SourceStream.Channels),
@ -212,6 +270,7 @@ func (api *Router) GetTranscodeDecision(w http.ResponseWriter, r *http.Request)
Container: decision.TranscodeStream.Container,
Codec: decision.TranscodeStream.Codec,
AudioBitrate: int32(kbpsToBps(decision.TranscodeStream.Bitrate)),
AudioProfile: decision.TranscodeStream.Profile,
AudioSamplerate: int32(decision.TranscodeStream.SampleRate),
AudioBitdepth: int32(decision.TranscodeStream.BitDepth),
AudioChannels: int32(decision.TranscodeStream.Channels),

View File

@ -66,26 +66,48 @@ var _ = Describe("Transcode endpoints", func() {
Expect(err).To(HaveOccurred())
})
It("handles empty body gracefully", func() {
mockMFRepo.SetData(model.MediaFiles{
{ID: "song-1", Suffix: "mp3", Codec: "MP3", BitRate: 320, Channels: 2, SampleRate: 44100},
})
mockTD.decision = &core.Decision{
MediaID: "song-1",
CanDirectPlay: false,
SourceStream: core.StreamDetails{
Container: "mp3", Codec: "mp3", Bitrate: 320,
SampleRate: 44100, Channels: 2,
},
}
mockTD.token = "empty-body-token"
It("returns error when body is empty", func() {
r := newJSONPostRequest("mediaId=song-1&mediaType=song", "")
resp, err := router.GetTranscodeDecision(w, r)
_, err := router.GetTranscodeDecision(w, r)
Expect(err).To(HaveOccurred())
})
Expect(err).ToNot(HaveOccurred())
Expect(resp.TranscodeDecision).ToNot(BeNil())
Expect(resp.TranscodeDecision.TranscodeParams).To(Equal("empty-body-token"))
It("returns error when body contains invalid JSON", func() {
r := newJSONPostRequest("mediaId=song-1&mediaType=song", "not-json{{{")
_, err := router.GetTranscodeDecision(w, r)
Expect(err).To(HaveOccurred())
})
It("returns error for invalid protocol in direct play profile", func() {
body := `{"directPlayProfiles":[{"containers":["mp3"],"audioCodecs":["mp3"],"protocols":["ftp"]}]}`
r := newJSONPostRequest("mediaId=song-1&mediaType=song", body)
_, err := router.GetTranscodeDecision(w, r)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("invalid protocol"))
})
It("returns error for invalid comparison operator", func() {
body := `{"codecProfiles":[{"type":"AudioCodec","name":"mp3","limitations":[{"name":"audioBitrate","comparison":"InvalidOp","values":["320"]}]}]}`
r := newJSONPostRequest("mediaId=song-1&mediaType=song", body)
_, err := router.GetTranscodeDecision(w, r)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("invalid comparison"))
})
It("returns error for invalid limitation name", func() {
body := `{"codecProfiles":[{"type":"AudioCodec","name":"mp3","limitations":[{"name":"unknownField","comparison":"Equals","values":["320"]}]}]}`
r := newJSONPostRequest("mediaId=song-1&mediaType=song", body)
_, err := router.GetTranscodeDecision(w, r)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("invalid limitation name"))
})
It("returns error for invalid codec profile type", func() {
body := `{"codecProfiles":[{"type":"VideoCodec","name":"mp3"}]}`
r := newJSONPostRequest("mediaId=song-1&mediaType=song", body)
_, err := router.GetTranscodeDecision(w, r)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("invalid codec profile type"))
})
It("returns a valid decision response", func() {