fix(transcoding): adapt transcode claims to struct-based auth.Claims

Updated transcode token handling to use the struct-based auth.Claims
introduced on master, replacing the previous map[string]any approach.
Extended auth.Claims with transcoding-specific fields (MediaID, DirectPlay,
UpdatedAt, Channels, SampleRate, BitDepth) and added float64 fallback in
ClaimsFromToken for numeric claims that lose their Go type during JWT
string serialization. Also added the missing lyrics parameter to all
subsonic.New() calls in test files.
This commit is contained in:
Deluan 2026-03-06 16:31:55 -05:00
parent 004ebea7d0
commit 5af03e1feb
8 changed files with 94 additions and 62 deletions

View File

@ -21,6 +21,14 @@ type Claims struct {
ID string // "id" - artwork/mediafile ID
Format string // "f" - audio format
BitRate int // "b" - audio bitrate
// Transcoding claims
MediaID string // "mid" - media file ID
DirectPlay bool // "dp" - direct play flag
UpdatedAt int64 // "ua" - source file updated-at (Unix seconds)
Channels int // "ch" - target channels
SampleRate int // "sr" - target sample rate (Hz)
BitDepth int // "bd" - target bit depth
}
// ToMap converts Claims to a map[string]any for use with TokenAuth.Encode().
@ -54,6 +62,24 @@ func (c Claims) ToMap() map[string]any {
if c.BitRate != 0 {
m["b"] = c.BitRate
}
if c.MediaID != "" {
m["mid"] = c.MediaID
}
if c.DirectPlay {
m["dp"] = c.DirectPlay
}
if c.UpdatedAt != 0 {
m["ua"] = c.UpdatedAt
}
if c.Channels != 0 {
m["ch"] = c.Channels
}
if c.SampleRate != 0 {
m["sr"] = c.SampleRate
}
if c.BitDepth != 0 {
m["bd"] = c.BitDepth
}
return m
}
@ -86,9 +112,43 @@ func ClaimsFromToken(token jwt.Token) Claims {
if err := token.Get("f", &f); err == nil {
c.Format = f
}
var b int
if err := token.Get("b", &b); err == nil {
c.BitRate = b
if err := token.Get("b", &c.BitRate); err != nil {
var bf float64
if err := token.Get("b", &bf); err == nil {
c.BitRate = int(bf)
}
}
var mid string
if err := token.Get("mid", &mid); err == nil {
c.MediaID = mid
}
var dp bool
if err := token.Get("dp", &dp); err == nil {
c.DirectPlay = dp
}
if err := token.Get("ua", &c.UpdatedAt); err != nil {
var uaf float64
if err := token.Get("ua", &uaf); err == nil {
c.UpdatedAt = int64(uaf)
}
}
if err := token.Get("ch", &c.Channels); err != nil {
var chf float64
if err := token.Get("ch", &chf); err == nil {
c.Channels = int(chf)
}
}
if err := token.Get("sr", &c.SampleRate); err != nil {
var srf float64
if err := token.Get("sr", &srf); err == nil {
c.SampleRate = int(srf)
}
}
if err := token.Get("bd", &c.BitDepth); err != nil {
var bdf float64
if err := token.Get("bd", &bdf); err == nil {
c.BitDepth = int(bdf)
}
}
return c
}

View File

@ -15,16 +15,6 @@ import (
const (
tokenTTL = 12 * time.Hour
defaultBitrate = 256 // kbps
// JWT claim keys for transcode params tokens
claimMediaID = "mid" // Media file ID
claimDirectPlay = "dp" // Direct play flag (bool)
claimUpdatedAt = "ua" // Source file updated-at (Unix seconds)
claimFormat = "fmt" // Target transcoding format
claimBitrate = "br" // Target bitrate (kbps)
claimChannels = "ch" // Target channels
claimSampleRate = "sr" // Target sample rate (Hz)
claimBitDepth = "bd" // Target bit depth
)
func NewDecider(ds model.DataStore) Decider {
@ -308,22 +298,22 @@ func (s *deciderService) applyCodecLimitations(ctx context.Context, sourceBitrat
func (s *deciderService) CreateTranscodeParams(decision *Decision) (string, error) {
exp := time.Now().Add(tokenTTL)
claims := map[string]any{
claimMediaID: decision.MediaID,
claimDirectPlay: decision.CanDirectPlay,
claimUpdatedAt: decision.SourceUpdatedAt.Truncate(time.Second).Unix(),
claims := auth.Claims{
MediaID: decision.MediaID,
DirectPlay: decision.CanDirectPlay,
UpdatedAt: decision.SourceUpdatedAt.Truncate(time.Second).Unix(),
}
if decision.CanTranscode && decision.TargetFormat != "" {
claims[claimFormat] = decision.TargetFormat
claims[claimBitrate] = decision.TargetBitrate
claims.Format = decision.TargetFormat
claims.BitRate = decision.TargetBitrate
if decision.TargetChannels > 0 {
claims[claimChannels] = decision.TargetChannels
claims.Channels = decision.TargetChannels
}
if decision.TargetSampleRate > 0 {
claims[claimSampleRate] = decision.TargetSampleRate
claims.SampleRate = decision.TargetSampleRate
}
if decision.TargetBitDepth > 0 {
claims[claimBitDepth] = decision.TargetBitDepth
claims.BitDepth = decision.TargetBitDepth
}
}
return auth.CreateExpiringPublicToken(exp, claims)
@ -335,43 +325,25 @@ func (s *deciderService) ParseTranscodeParams(token string) (*Params, error) {
return nil, err
}
params := &Params{}
// Required claims
mid, ok := claims[claimMediaID].(string)
if !ok || mid == "" {
if claims.MediaID == "" {
return nil, fmt.Errorf("%w: invalid transcode token: missing media ID", ErrTokenInvalid)
}
params.MediaID = mid
dp, ok := claims[claimDirectPlay].(bool)
if !ok {
return nil, fmt.Errorf("%w: invalid transcode token: missing direct play flag", ErrTokenInvalid)
}
params.DirectPlay = dp
// Optional claims (legitimately absent for direct-play tokens)
if f, ok := claims[claimFormat].(string); ok {
params.TargetFormat = f
}
if br, ok := claims[claimBitrate].(float64); ok {
params.TargetBitrate = int(br)
}
if ch, ok := claims[claimChannels].(float64); ok {
params.TargetChannels = int(ch)
}
if sr, ok := claims[claimSampleRate].(float64); ok {
params.TargetSampleRate = int(sr)
}
if bd, ok := claims[claimBitDepth].(float64); ok {
params.TargetBitDepth = int(bd)
}
ua, ok := claims[claimUpdatedAt].(float64)
if !ok {
if claims.UpdatedAt == 0 {
return nil, fmt.Errorf("%w: invalid transcode token: missing source timestamp", ErrTokenInvalid)
}
params.SourceUpdatedAt = time.Unix(int64(ua), 0)
params := &Params{
MediaID: claims.MediaID,
DirectPlay: claims.DirectPlay,
TargetFormat: claims.Format,
TargetBitrate: claims.BitRate,
TargetChannels: claims.Channels,
TargetSampleRate: claims.SampleRate,
TargetBitDepth: claims.BitDepth,
SourceUpdatedAt: time.Unix(claims.UpdatedAt, 0),
}
return params, nil
}

View File

@ -27,7 +27,7 @@ var _ = Describe("Album Lists", func() {
ds = &tests.MockDataStore{}
auth.Init(ds)
mockRepo = ds.Album(ctx).(*tests.MockAlbumRepo)
router = New(ds, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
router = New(ds, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
w = httptest.NewRecorder()
})

View File

@ -27,7 +27,7 @@ var _ = Describe("MediaAnnotationController", func() {
ds = &tests.MockDataStore{}
playTracker = &fakePlayTracker{}
eventBroker = &fakeEventBroker{}
router = New(ds, nil, nil, nil, nil, nil, nil, eventBroker, nil, playTracker, nil, nil, nil, nil)
router = New(ds, nil, nil, nil, nil, nil, nil, eventBroker, nil, playTracker, nil, nil, nil, nil, nil)
})
Describe("Scrobble", func() {

View File

@ -19,7 +19,7 @@ var _ = Describe("GetOpenSubsonicExtensions", func() {
)
BeforeEach(func() {
router = subsonic.New(nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
router = subsonic.New(nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
w = httptest.NewRecorder()
r = httptest.NewRequest("GET", "/getOpenSubsonicExtensions?f=json", nil)
})

View File

@ -24,7 +24,7 @@ var _ = Describe("buildPlaylist", func() {
BeforeEach(func() {
ds = &tests.MockDataStore{}
router = New(ds, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
router = New(ds, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
ctx = context.Background()
})
@ -224,7 +224,7 @@ var _ = Describe("UpdatePlaylist", func() {
BeforeEach(func() {
ds = &tests.MockDataStore{}
playlists = &fakePlaylists{}
router = New(ds, nil, nil, nil, nil, nil, nil, nil, playlists, nil, nil, nil, nil, nil)
router = New(ds, nil, nil, nil, nil, nil, nil, nil, playlists, nil, nil, nil, nil, nil, nil)
})
It("clears the comment when parameter is empty", func() {

View File

@ -21,7 +21,7 @@ var _ = Describe("Search", func() {
ds = &tests.MockDataStore{}
auth.Init(ds)
router = New(ds, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
router = New(ds, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
// Get references to the mock repositories so we can inspect their Options
mockAlbumRepo = ds.Album(nil).(*tests.MockAlbumRepo)

View File

@ -28,7 +28,7 @@ var _ = Describe("Transcode endpoints", func() {
mockMFRepo = &tests.MockMediaFileRepo{}
ds = &tests.MockDataStore{MockedMediaFile: mockMFRepo}
mockTD = &mockTranscodeDecision{}
router = New(ds, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, mockTD)
router = New(ds, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, mockTD)
w = httptest.NewRecorder()
})
@ -260,7 +260,7 @@ var _ = Describe("Transcode endpoints", func() {
It("builds correct StreamRequest for direct play", func() {
fakeStreamer := &fakeMediaStreamer{}
router = New(ds, nil, fakeStreamer, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, mockTD)
router = New(ds, nil, fakeStreamer, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, mockTD)
mockTD.validateParams = &transcode.Params{MediaID: "song-1", DirectPlay: true}
mockTD.validateMF = &model.MediaFile{ID: "song-1"}
@ -278,7 +278,7 @@ var _ = Describe("Transcode endpoints", func() {
It("builds correct StreamRequest for transcoding", func() {
fakeStreamer := &fakeMediaStreamer{}
router = New(ds, nil, fakeStreamer, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, mockTD)
router = New(ds, nil, fakeStreamer, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, mockTD)
mockTD.validateParams = &transcode.Params{
MediaID: "song-2",
DirectPlay: false,