diff --git a/core/archiver_test.go b/core/archiver_test.go index b0cf13ae1..4ded2eba4 100644 --- a/core/archiver_test.go +++ b/core/archiver_test.go @@ -44,7 +44,7 @@ var _ = Describe("Archiver", func() { }}).Return(mfs, nil) ds.On("MediaFile", mock.Anything).Return(mfRepo) - ms.On("DoStream", mock.Anything, mock.Anything, "mp3", 128, 0, 0, 0, 0).Return(io.NopCloser(strings.NewReader("test")), nil).Times(3) + ms.On("DoStream", mock.Anything, mock.Anything, core.StreamRequest{Format: "mp3", BitRate: 128}).Return(io.NopCloser(strings.NewReader("test")), nil).Times(3) out := new(bytes.Buffer) err := arch.ZipAlbum(context.Background(), "1", "mp3", 128, out) @@ -73,7 +73,7 @@ var _ = Describe("Archiver", func() { }}).Return(mfs, nil) ds.On("MediaFile", mock.Anything).Return(mfRepo) - ms.On("DoStream", mock.Anything, mock.Anything, "mp3", 128, 0, 0, 0, 0).Return(io.NopCloser(strings.NewReader("test")), nil).Times(2) + ms.On("DoStream", mock.Anything, mock.Anything, core.StreamRequest{Format: "mp3", BitRate: 128}).Return(io.NopCloser(strings.NewReader("test")), nil).Times(2) out := new(bytes.Buffer) err := arch.ZipArtist(context.Background(), "1", "mp3", 128, out) @@ -104,7 +104,7 @@ var _ = Describe("Archiver", func() { } sh.On("Load", mock.Anything, "1").Return(share, nil) - ms.On("DoStream", mock.Anything, mock.Anything, "mp3", 128, 0, 0, 0, 0).Return(io.NopCloser(strings.NewReader("test")), nil).Times(2) + ms.On("DoStream", mock.Anything, mock.Anything, core.StreamRequest{Format: "mp3", BitRate: 128}).Return(io.NopCloser(strings.NewReader("test")), nil).Times(2) out := new(bytes.Buffer) err := arch.ZipShare(context.Background(), "1", out) @@ -136,7 +136,7 @@ var _ = Describe("Archiver", func() { plRepo := &mockPlaylistRepository{} plRepo.On("GetWithTracks", "1", true, false).Return(pls, nil) ds.On("Playlist", mock.Anything).Return(plRepo) - ms.On("DoStream", mock.Anything, mock.Anything, "mp3", 128, 0, 0, 0, 0).Return(io.NopCloser(strings.NewReader("test")), nil).Times(2) + ms.On("DoStream", mock.Anything, mock.Anything, core.StreamRequest{Format: "mp3", BitRate: 128}).Return(io.NopCloser(strings.NewReader("test")), nil).Times(2) out := new(bytes.Buffer) err := arch.ZipPlaylist(context.Background(), "1", "mp3", 128, out) @@ -218,7 +218,7 @@ type mockMediaStreamer struct { } func (m *mockMediaStreamer) DoStream(ctx context.Context, mf *model.MediaFile, req core.StreamRequest) (*core.Stream, error) { - args := m.Called(ctx, mf, req.Format, req.BitRate, req.SampleRate, req.BitDepth, req.Channels, req.Offset) + args := m.Called(ctx, mf, req) if args.Error(1) != nil { return nil, args.Error(1) } diff --git a/core/transcode/transcode.go b/core/transcode/transcode.go index d9bc41018..fe246d89d 100644 --- a/core/transcode/transcode.go +++ b/core/transcode/transcode.go @@ -15,6 +15,16 @@ 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 { @@ -299,21 +309,21 @@ 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{ - "mid": decision.MediaID, - "dp": decision.CanDirectPlay, - "ua": decision.SourceUpdatedAt.Truncate(time.Second).Unix(), + claimMediaID: decision.MediaID, + claimDirectPlay: decision.CanDirectPlay, + claimUpdatedAt: decision.SourceUpdatedAt.Truncate(time.Second).Unix(), } if decision.CanTranscode && decision.TargetFormat != "" { - claims["fmt"] = decision.TargetFormat - claims["br"] = decision.TargetBitrate + claims[claimFormat] = decision.TargetFormat + claims[claimBitrate] = decision.TargetBitrate if decision.TargetChannels > 0 { - claims["ch"] = decision.TargetChannels + claims[claimChannels] = decision.TargetChannels } if decision.TargetSampleRate > 0 { - claims["sr"] = decision.TargetSampleRate + claims[claimSampleRate] = decision.TargetSampleRate } if decision.TargetBitDepth > 0 { - claims["bd"] = decision.TargetBitDepth + claims[claimBitDepth] = decision.TargetBitDepth } } return auth.CreateExpiringPublicToken(exp, claims) @@ -328,38 +338,38 @@ func (s *deciderService) ParseTranscodeParams(token string) (*Params, error) { params := &Params{} // Required claims - mid, ok := claims["mid"].(string) + mid, ok := claims[claimMediaID].(string) if !ok || mid == "" { - return nil, fmt.Errorf("invalid transcode token: missing media ID") + return nil, fmt.Errorf("%w: invalid transcode token: missing media ID", ErrTokenInvalid) } params.MediaID = mid - dp, ok := claims["dp"].(bool) + dp, ok := claims[claimDirectPlay].(bool) if !ok { - return nil, fmt.Errorf("invalid transcode token: missing direct play flag") + 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["fmt"].(string); ok { + if f, ok := claims[claimFormat].(string); ok { params.TargetFormat = f } - if br, ok := claims["br"].(float64); ok { + if br, ok := claims[claimBitrate].(float64); ok { params.TargetBitrate = int(br) } - if ch, ok := claims["ch"].(float64); ok { + if ch, ok := claims[claimChannels].(float64); ok { params.TargetChannels = int(ch) } - if sr, ok := claims["sr"].(float64); ok { + if sr, ok := claims[claimSampleRate].(float64); ok { params.TargetSampleRate = int(sr) } - if bd, ok := claims["bd"].(float64); ok { + if bd, ok := claims[claimBitDepth].(float64); ok { params.TargetBitDepth = int(bd) } - ua, ok := claims["ua"].(float64) + ua, ok := claims[claimUpdatedAt].(float64) if !ok { - return nil, fmt.Errorf("invalid transcode token: missing source timestamp") + return nil, fmt.Errorf("%w: invalid transcode token: missing source timestamp", ErrTokenInvalid) } params.SourceUpdatedAt = time.Unix(int64(ua), 0) diff --git a/model/mediafile.go b/model/mediafile.go index e92109a06..908540a0f 100644 --- a/model/mediafile.go +++ b/model/mediafile.go @@ -239,7 +239,9 @@ func (mf MediaFile) IsLossless() bool { if slices.Contains(confmime.LosslessFormats, mf.Suffix) { return true } - // Fallback heuristic: if BitDepth is set, it's likely lossless + // Fallback heuristic: if BitDepth is set, it's likely lossless. + // This may produce false positives for lossy formats that report bit depth, + // but it becomes irrelevant once the Codec column is populated after a full rescan. return mf.BitDepth > 0 } diff --git a/tests/mock_transcoding_repo.go b/tests/mock_transcoding_repo.go index ff0aba86b..796e84111 100644 --- a/tests/mock_transcoding_repo.go +++ b/tests/mock_transcoding_repo.go @@ -21,7 +21,7 @@ func (m *MockTranscodingRepo) FindByFormat(format string) (*model.Transcoding, e case "flac": return &model.Transcoding{ID: "flac1", TargetFormat: "flac", DefaultBitRate: 0, Command: "ffmpeg -i %s -ss %t -map 0:a:0 -v 0 -c:a flac -f flac -"}, nil case "aac": - return &model.Transcoding{ID: "aac1", TargetFormat: "aac", DefaultBitRate: 256, Command: "ffmpeg -i %s -ss %t -map 0:a:0 -b:a %bk -v 0 -c:a aac -f adts -"}, nil + return &model.Transcoding{ID: "aac1", TargetFormat: "aac", DefaultBitRate: 256, Command: "ffmpeg -i %s -ss %t -map 0:a:0 -b:a %bk -v 0 -c:a aac -f ipod -movflags frag_keyframe+empty_moov -"}, nil default: return nil, model.ErrNotFound }