mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-31 07:30:32 +00:00
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:
parent
3259040db0
commit
d5038c3194
@ -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
|
||||
}
|
||||
|
||||
@ -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))
|
||||
})
|
||||
})
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user