From 5af03e1feb057a1639b3ca38e733527c3007765b Mon Sep 17 00:00:00 2001 From: Deluan Date: Fri, 6 Mar 2026 16:31:55 -0500 Subject: [PATCH] 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. --- core/auth/claims.go | 66 +++++++++++++++++++++- core/transcode/transcode.go | 72 ++++++++---------------- server/subsonic/album_lists_test.go | 2 +- server/subsonic/media_annotation_test.go | 2 +- server/subsonic/opensubsonic_test.go | 2 +- server/subsonic/playlists_test.go | 4 +- server/subsonic/searching_test.go | 2 +- server/subsonic/transcode_test.go | 6 +- 8 files changed, 94 insertions(+), 62 deletions(-) diff --git a/core/auth/claims.go b/core/auth/claims.go index ca496ae9a..30d5d57a3 100644 --- a/core/auth/claims.go +++ b/core/auth/claims.go @@ -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 } diff --git a/core/transcode/transcode.go b/core/transcode/transcode.go index fe246d89d..254d56424 100644 --- a/core/transcode/transcode.go +++ b/core/transcode/transcode.go @@ -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 } diff --git a/server/subsonic/album_lists_test.go b/server/subsonic/album_lists_test.go index aac2d63da..ae4ef9bb9 100644 --- a/server/subsonic/album_lists_test.go +++ b/server/subsonic/album_lists_test.go @@ -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() }) diff --git a/server/subsonic/media_annotation_test.go b/server/subsonic/media_annotation_test.go index 57809fbb6..fc767b0ff 100644 --- a/server/subsonic/media_annotation_test.go +++ b/server/subsonic/media_annotation_test.go @@ -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() { diff --git a/server/subsonic/opensubsonic_test.go b/server/subsonic/opensubsonic_test.go index 3ac294457..92d1c3e84 100644 --- a/server/subsonic/opensubsonic_test.go +++ b/server/subsonic/opensubsonic_test.go @@ -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) }) diff --git a/server/subsonic/playlists_test.go b/server/subsonic/playlists_test.go index 86c17b39c..41701b4de 100644 --- a/server/subsonic/playlists_test.go +++ b/server/subsonic/playlists_test.go @@ -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() { diff --git a/server/subsonic/searching_test.go b/server/subsonic/searching_test.go index d4b7e9702..ab40a726f 100644 --- a/server/subsonic/searching_test.go +++ b/server/subsonic/searching_test.go @@ -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) diff --git a/server/subsonic/transcode_test.go b/server/subsonic/transcode_test.go index 57c66e762..88c65d9ce 100644 --- a/server/subsonic/transcode_test.go +++ b/server/subsonic/transcode_test.go @@ -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,