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,