refactor(transcode): rename Decider methods and unexport Params type

Rename ResolveStream → ResolveRequest and ValidateTranscodeParams →
ResolveRequestFromToken for clarity and consistency. The new
ResolveRequestFromToken returns a StreamRequest directly (instead of
the intermediate Params type), eliminating manual Params→StreamRequest
conversion in callers. Unexport Params to params since it is now only
used internally for JWT token parsing.
This commit is contained in:
Deluan 2026-03-08 20:25:20 -04:00
parent 3259040db0
commit d5038c3194
7 changed files with 72 additions and 72 deletions

View File

@ -460,9 +460,9 @@ func buildLegacyClientInfo(mf *model.MediaFile, reqFormat string, reqBitRate int
return ci
}
// ResolveStream uses MakeDecision to resolve legacy Subsonic stream parameters
// ResolveRequest uses MakeDecision to resolve legacy Subsonic stream parameters
// into a fully specified StreamRequest.
func (s *deciderService) ResolveStream(ctx context.Context, mf *model.MediaFile, reqFormat string, reqBitRate int, offset int) StreamRequest {
func (s *deciderService) ResolveRequest(ctx context.Context, mf *model.MediaFile, reqFormat string, reqBitRate int, offset int) StreamRequest {
var req StreamRequest
req.ID = mf.ID
req.Offset = offset
@ -503,7 +503,7 @@ func (s *deciderService) CreateTranscodeParams(decision *Decision) (string, erro
return auth.EncodeToken(decision.toClaimsMap())
}
func (s *deciderService) parseTranscodeParams(tokenStr string) (*Params, error) {
func (s *deciderService) parseTranscodeParams(tokenStr string) (*params, error) {
token, err := auth.DecodeAndVerifyToken(tokenStr)
if err != nil {
return nil, err
@ -511,25 +511,34 @@ func (s *deciderService) parseTranscodeParams(tokenStr string) (*Params, error)
return paramsFromToken(token)
}
func (s *deciderService) ValidateTranscodeParams(ctx context.Context, token string, mediaID string) (*Params, *model.MediaFile, error) {
params, err := s.parseTranscodeParams(token)
func (s *deciderService) ResolveRequestFromToken(ctx context.Context, token string, mediaID string, offset int) (StreamRequest, *model.MediaFile, error) {
p, err := s.parseTranscodeParams(token)
if err != nil {
return nil, nil, errors.Join(ErrTokenInvalid, err)
return StreamRequest{}, nil, errors.Join(ErrTokenInvalid, err)
}
if params.MediaID != mediaID {
return nil, nil, fmt.Errorf("%w: token mediaID %q does not match %q", ErrTokenInvalid, params.MediaID, mediaID)
if p.MediaID != mediaID {
return StreamRequest{}, nil, fmt.Errorf("%w: token mediaID %q does not match %q", ErrTokenInvalid, p.MediaID, mediaID)
}
mf, err := s.ds.MediaFile(ctx).Get(mediaID)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return nil, nil, ErrMediaNotFound
return StreamRequest{}, nil, ErrMediaNotFound
}
return nil, nil, err
return StreamRequest{}, nil, err
}
if !mf.UpdatedAt.Truncate(time.Second).Equal(params.SourceUpdatedAt) {
if !mf.UpdatedAt.Truncate(time.Second).Equal(p.SourceUpdatedAt) {
log.Info(ctx, "Transcode token is stale", "mediaID", mediaID,
"tokenUpdatedAt", params.SourceUpdatedAt, "fileUpdatedAt", mf.UpdatedAt)
return nil, nil, ErrTokenStale
"tokenUpdatedAt", p.SourceUpdatedAt, "fileUpdatedAt", mf.UpdatedAt)
return StreamRequest{}, nil, ErrTokenStale
}
return params, mf, nil
req := StreamRequest{ID: mediaID, Offset: offset}
if !p.DirectPlay && p.TargetFormat != "" {
req.Format = p.TargetFormat
req.BitRate = p.TargetBitrate
req.SampleRate = p.TargetSampleRate
req.BitDepth = p.TargetBitDepth
req.Channels = p.TargetChannels
}
return req, mf, nil
}

View File

@ -1252,7 +1252,7 @@ var _ = Describe("Decider", func() {
})
})
Describe("ValidateTranscodeParams", func() {
Describe("ResolveRequestFromToken", func() {
var (
mockMFRepo *tests.MockMediaFileRepo
sourceTime time.Time
@ -1275,35 +1275,35 @@ var _ = Describe("Decider", func() {
return token
}
It("returns params and media file for valid token", func() {
It("returns stream request and media file for valid token", func() {
mockMFRepo.SetData(model.MediaFiles{
{ID: "song-1", UpdatedAt: sourceTime},
})
token := createTokenForMedia("song-1", sourceTime)
params, mf, err := svc.ValidateTranscodeParams(ctx, token, "song-1")
req, mf, err := svc.ResolveRequestFromToken(ctx, token, "song-1", 0)
Expect(err).ToNot(HaveOccurred())
Expect(params.MediaID).To(Equal("song-1"))
Expect(params.DirectPlay).To(BeTrue())
Expect(req.ID).To(Equal("song-1"))
Expect(req.Format).To(BeEmpty()) // direct play has no target format
Expect(mf.ID).To(Equal("song-1"))
})
It("returns ErrTokenInvalid for invalid token", func() {
_, _, err := svc.ValidateTranscodeParams(ctx, "bad-token", "song-1")
_, _, err := svc.ResolveRequestFromToken(ctx, "bad-token", "song-1", 0)
Expect(err).To(MatchError(ContainSubstring(ErrTokenInvalid.Error())))
})
It("returns ErrTokenInvalid when mediaID does not match token", func() {
token := createTokenForMedia("song-1", sourceTime)
_, _, err := svc.ValidateTranscodeParams(ctx, token, "song-2")
_, _, err := svc.ResolveRequestFromToken(ctx, token, "song-2", 0)
Expect(err).To(MatchError(ContainSubstring(ErrTokenInvalid.Error())))
})
It("returns ErrMediaNotFound when media file does not exist", func() {
token := createTokenForMedia("gone-id", sourceTime)
_, _, err := svc.ValidateTranscodeParams(ctx, token, "gone-id")
_, _, err := svc.ResolveRequestFromToken(ctx, token, "gone-id", 0)
Expect(err).To(MatchError(ErrMediaNotFound))
})
@ -1314,7 +1314,7 @@ var _ = Describe("Decider", func() {
})
token := createTokenForMedia("song-1", sourceTime)
_, _, err := svc.ValidateTranscodeParams(ctx, token, "song-1")
_, _, err := svc.ResolveRequestFromToken(ctx, token, "song-1", 0)
Expect(err).To(MatchError(ErrTokenStale))
})
})

View File

@ -37,9 +37,9 @@ type StreamRequest struct {
// Decider is the core service interface for making transcoding decisions
type Decider interface {
MakeDecision(ctx context.Context, mf *model.MediaFile, clientInfo *ClientInfo, opts DecisionOptions) (*Decision, error)
ResolveStream(ctx context.Context, mf *model.MediaFile, reqFormat string, reqBitRate int, offset int) StreamRequest
CreateTranscodeParams(decision *Decision) (string, error)
ValidateTranscodeParams(ctx context.Context, token string, mediaID string) (*Params, *model.MediaFile, error)
ResolveRequestFromToken(ctx context.Context, token string, mediaID string, offset int) (StreamRequest, *model.MediaFile, error)
ResolveRequest(ctx context.Context, mf *model.MediaFile, reqFormat string, reqBitRate int, offset int) StreamRequest
}
// ClientInfo represents client playback capabilities.
@ -175,9 +175,9 @@ type StreamDetails struct {
IsLossless bool
}
// Params contains the parameters extracted from a transcode token.
// params contains the parameters extracted from a transcode token.
// TargetBitrate is in kilobits per second (kbps).
type Params struct {
type params struct {
MediaID string
DirectPlay bool
TargetFormat string
@ -190,8 +190,8 @@ type Params struct {
// paramsFromToken extracts and validates Params from a parsed JWT token.
// Returns an error if required claims (media ID, source timestamp) are missing.
func paramsFromToken(token jwt.Token) (*Params, error) {
var p Params
func paramsFromToken(token jwt.Token) (*params, error) {
var p params
var mid string
if err := token.Get("mid", &mid); err == nil {
p.MediaID = mid

View File

@ -243,7 +243,7 @@ func (n noopDecider) MakeDecision(context.Context, *model.MediaFile, *transcode.
return nil, nil
}
func (n noopDecider) ResolveStream(context.Context, *model.MediaFile, string, int, int) transcode.StreamRequest {
func (n noopDecider) ResolveRequest(context.Context, *model.MediaFile, string, int, int) transcode.StreamRequest {
return transcode.StreamRequest{Format: "raw"}
}
@ -251,8 +251,8 @@ func (n noopDecider) CreateTranscodeParams(*transcode.Decision) (string, error)
return "", nil
}
func (n noopDecider) ValidateTranscodeParams(context.Context, string, string) (*transcode.Params, *model.MediaFile, error) {
return nil, nil, nil
func (n noopDecider) ResolveRequestFromToken(context.Context, string, string, int) (transcode.StreamRequest, *model.MediaFile, error) {
return transcode.StreamRequest{}, nil, nil
}
// noopArchiver implements core.Archiver

View File

@ -65,7 +65,7 @@ func (api *Router) Stream(w http.ResponseWriter, r *http.Request) (*responses.Su
return nil, err
}
streamReq := api.transcodeDecision.ResolveStream(ctx, mf, format, maxBitRate, timeOffset)
streamReq := api.transcodeDecision.ResolveRequest(ctx, mf, format, maxBitRate, timeOffset)
stream, err := api.streamer.DoStream(ctx, mf, streamReq)
if err != nil {
return nil, err
@ -135,7 +135,7 @@ func (api *Router) Download(w http.ResponseWriter, r *http.Request) (*responses.
switch v := entity.(type) {
case *model.MediaFile:
streamReq := api.transcodeDecision.ResolveStream(ctx, v, format, maxBitRate, 0)
streamReq := api.transcodeDecision.ResolveRequest(ctx, v, format, maxBitRate, 0)
stream, err := api.streamer.DoStream(ctx, v, streamReq)
if err != nil {
return nil, err

View File

@ -343,8 +343,8 @@ func (api *Router) GetTranscodeStream(w http.ResponseWriter, r *http.Request) (*
return nil, nil
}
// Validate the token, mediaID match, file existence, and freshness
params, mf, err := api.transcodeDecision.ValidateTranscodeParams(ctx, transcodeParamsToken, mediaID)
// Validate the token and resolve streaming parameters
streamReq, mf, err := api.transcodeDecision.ResolveRequestFromToken(ctx, transcodeParamsToken, mediaID, p.IntOr("offset", 0))
if err != nil {
switch {
case errors.Is(err, transcode.ErrMediaNotFound):
@ -358,16 +358,6 @@ func (api *Router) GetTranscodeStream(w http.ResponseWriter, r *http.Request) (*
return nil, nil
}
// Build streaming parameters from the token
streamReq := transcode.StreamRequest{ID: mediaID, Offset: p.IntOr("offset", 0)}
if !params.DirectPlay && params.TargetFormat != "" {
streamReq.Format = params.TargetFormat
streamReq.BitRate = params.TargetBitrate // Already in kbps, matching the streamer
streamReq.SampleRate = params.TargetSampleRate
streamReq.BitDepth = params.TargetBitDepth
streamReq.Channels = params.TargetChannels
}
// Create stream (use DoStream to avoid duplicate DB fetch)
stream, err := api.streamer.DoStream(ctx, mf, streamReq)
if err != nil {

View File

@ -231,7 +231,7 @@ var _ = Describe("Transcode endpoints", func() {
})
It("returns 410 for invalid token", func() {
mockTD.validateErr = transcode.ErrTokenInvalid
mockTD.resolveErr = transcode.ErrTokenInvalid
r := newGetRequest("mediaId=123", "mediaType=song", "transcodeParams=bad-token")
resp, err := router.GetTranscodeStream(w, r)
Expect(err).ToNot(HaveOccurred())
@ -240,7 +240,7 @@ var _ = Describe("Transcode endpoints", func() {
})
It("returns 410 when mediaId doesn't match token", func() {
mockTD.validateErr = transcode.ErrTokenInvalid
mockTD.resolveErr = transcode.ErrTokenInvalid
r := newGetRequest("mediaId=wrong-id", "mediaType=song", "transcodeParams=valid-token")
resp, err := router.GetTranscodeStream(w, r)
Expect(err).ToNot(HaveOccurred())
@ -249,7 +249,7 @@ var _ = Describe("Transcode endpoints", func() {
})
It("returns 404 when media file not found", func() {
mockTD.validateErr = transcode.ErrMediaNotFound
mockTD.resolveErr = transcode.ErrMediaNotFound
r := newGetRequest("mediaId=gone-id", "mediaType=song", "transcodeParams=valid-token")
resp, err := router.GetTranscodeStream(w, r)
Expect(err).ToNot(HaveOccurred())
@ -258,7 +258,7 @@ var _ = Describe("Transcode endpoints", func() {
})
It("returns 410 when media file has changed (stale token)", func() {
mockTD.validateErr = transcode.ErrTokenStale
mockTD.resolveErr = transcode.ErrTokenStale
r := newGetRequest("mediaId=song-1", "mediaType=song", "transcodeParams=stale-token")
resp, err := router.GetTranscodeStream(w, r)
Expect(err).ToNot(HaveOccurred())
@ -269,8 +269,8 @@ 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, nil, mockTD)
mockTD.validateParams = &transcode.Params{MediaID: "song-1", DirectPlay: true}
mockTD.validateMF = &model.MediaFile{ID: "song-1"}
mockTD.resolvedReq = transcode.StreamRequest{ID: "song-1"}
mockTD.resolvedMF = &model.MediaFile{ID: "song-1"}
r := newGetRequest("mediaId=song-1", "mediaType=song", "transcodeParams=valid-token")
_, _ = router.GetTranscodeStream(w, r)
@ -287,16 +287,15 @@ 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, nil, mockTD)
mockTD.validateParams = &transcode.Params{
MediaID: "song-2",
DirectPlay: false,
TargetFormat: "mp3",
TargetBitrate: 256,
TargetSampleRate: 44100,
TargetBitDepth: 16,
TargetChannels: 2,
mockTD.resolvedReq = transcode.StreamRequest{
ID: "song-2",
Format: "mp3",
BitRate: 256,
SampleRate: 44100,
BitDepth: 16,
Channels: 2,
}
mockTD.validateMF = &model.MediaFile{ID: "song-2"}
mockTD.resolvedMF = &model.MediaFile{ID: "song-2"}
r := newGetRequest("mediaId=song-2", "mediaType=song", "transcodeParams=valid-token", "offset=10")
_, _ = router.GetTranscodeStream(w, r)
@ -365,12 +364,12 @@ func newJSONPostRequest(queryParams string, jsonBody string) *http.Request {
// mockTranscodeDecision is a test double for transcode.Decider
type mockTranscodeDecision struct {
decision *transcode.Decision
token string
tokenErr error
validateParams *transcode.Params
validateMF *model.MediaFile
validateErr error
decision *transcode.Decision
token string
tokenErr error
resolvedReq transcode.StreamRequest
resolvedMF *model.MediaFile
resolveErr error
}
func (m *mockTranscodeDecision) MakeDecision(_ context.Context, _ *model.MediaFile, _ *transcode.ClientInfo, _ transcode.DecisionOptions) (*transcode.Decision, error) {
@ -380,7 +379,7 @@ func (m *mockTranscodeDecision) MakeDecision(_ context.Context, _ *model.MediaFi
return &transcode.Decision{}, nil
}
func (m *mockTranscodeDecision) ResolveStream(_ context.Context, _ *model.MediaFile, _ string, _ int, _ int) transcode.StreamRequest {
func (m *mockTranscodeDecision) ResolveRequest(_ context.Context, _ *model.MediaFile, _ string, _ int, _ int) transcode.StreamRequest {
return transcode.StreamRequest{Format: "raw"}
}
@ -388,11 +387,13 @@ func (m *mockTranscodeDecision) CreateTranscodeParams(_ *transcode.Decision) (st
return m.token, m.tokenErr
}
func (m *mockTranscodeDecision) ValidateTranscodeParams(_ context.Context, _ string, _ string) (*transcode.Params, *model.MediaFile, error) {
if m.validateErr != nil {
return nil, nil, m.validateErr
func (m *mockTranscodeDecision) ResolveRequestFromToken(_ context.Context, _ string, _ string, offset int) (transcode.StreamRequest, *model.MediaFile, error) {
if m.resolveErr != nil {
return transcode.StreamRequest{}, nil, m.resolveErr
}
return m.validateParams, m.validateMF, nil
req := m.resolvedReq
req.Offset = offset
return req, m.resolvedMF, nil
}
// fakeMediaStreamer captures the StreamRequest and returns a sentinel error,