refactor: rename core/transcode to core/stream, simplify MediaStreamer (#5166)
* refactor: rename core/transcode directory to core/stream * refactor: update all imports from core/transcode to core/stream * refactor: rename exported symbols to fit core/stream package name * refactor: simplify MediaStreamer interface to single NewStream method Remove the two-method interface (NewStream + DoStream) in favor of a single NewStream(ctx, mf, req) method. Callers are now responsible for fetching the MediaFile before calling NewStream. This removes the implicit DB lookup from the streamer, making it a pure streaming concern. * refactor: update all callers from DoStream to NewStream * chore: update wire_gen.go and stale comment for core/stream rename * refactor: update wire command to handle GO_BUILD_TAGS correctly Signed-off-by: Deluan <deluan@navidrome.org> * fix: distinguish not-found from internal errors in public stream handler * refactor: remove unused ID field from stream.Request * refactor: simplify ResolveRequestFromToken to receive *model.MediaFile Move MediaFile fetching responsibility to callers, making the method focused on token validation and request resolution. Remove ErrMediaNotFound (no longer produced). Update GetTranscodeStream handler to fetch the media file before calling ResolveRequestFromToken. * refactor: extend tokenTTL from 12 to 48 hours Signed-off-by: Deluan <deluan@navidrome.org> --------- Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
@@ -28,7 +28,7 @@ import (
|
||||
"github.com/navidrome/navidrome/core/playlists"
|
||||
"github.com/navidrome/navidrome/core/scrobbler"
|
||||
"github.com/navidrome/navidrome/core/storage/storagetest"
|
||||
"github.com/navidrome/navidrome/core/transcode"
|
||||
"github.com/navidrome/navidrome/core/stream"
|
||||
"github.com/navidrome/navidrome/db"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
@@ -284,25 +284,21 @@ func (n noopArtwork) GetOrPlaceholder(_ context.Context, _ string, _ int, _ bool
|
||||
return io.NopCloser(io.LimitReader(nil, 0)), time.Time{}, nil
|
||||
}
|
||||
|
||||
// spyStreamer captures the StreamRequest passed to DoStream for test assertions,
|
||||
// spyStreamer captures the Request passed to NewStream for test assertions,
|
||||
// then returns a minimal fake Stream so the handler completes without error.
|
||||
type spyStreamer struct {
|
||||
LastRequest transcode.StreamRequest
|
||||
LastRequest stream.Request
|
||||
LastMediaFile *model.MediaFile
|
||||
}
|
||||
|
||||
func (s *spyStreamer) NewStream(ctx context.Context, req transcode.StreamRequest) (*transcode.Stream, error) {
|
||||
return nil, model.ErrNotFound
|
||||
}
|
||||
|
||||
func (s *spyStreamer) DoStream(_ context.Context, mf *model.MediaFile, req transcode.StreamRequest) (*transcode.Stream, error) {
|
||||
func (s *spyStreamer) NewStream(_ context.Context, mf *model.MediaFile, req stream.Request) (*stream.Stream, error) {
|
||||
s.LastRequest = req
|
||||
s.LastMediaFile = mf
|
||||
format := req.Format
|
||||
if format == "" || format == "raw" {
|
||||
format = mf.Suffix
|
||||
}
|
||||
return transcode.NewTestStream(mf, format, req.BitRate), nil
|
||||
return stream.NewTestStream(mf, format, req.BitRate), nil
|
||||
}
|
||||
|
||||
// noopFFmpeg implements ffmpeg.FFmpeg with no-op methods.
|
||||
@@ -391,12 +387,12 @@ func (n noopPlayTracker) Submit(context.Context, []scrobbler.Submission) error {
|
||||
|
||||
// Compile-time interface checks
|
||||
var (
|
||||
_ artwork.Artwork = noopArtwork{}
|
||||
_ transcode.MediaStreamer = &spyStreamer{}
|
||||
_ core.Archiver = noopArchiver{}
|
||||
_ external.Provider = noopProvider{}
|
||||
_ scrobbler.PlayTracker = noopPlayTracker{}
|
||||
_ ffmpeg.FFmpeg = noopFFmpeg{}
|
||||
_ artwork.Artwork = noopArtwork{}
|
||||
_ stream.MediaStreamer = &spyStreamer{}
|
||||
_ core.Archiver = noopArchiver{}
|
||||
_ external.Provider = noopProvider{}
|
||||
_ scrobbler.PlayTracker = noopPlayTracker{}
|
||||
_ ffmpeg.FFmpeg = noopFFmpeg{}
|
||||
)
|
||||
|
||||
var _ = BeforeSuite(func() {
|
||||
@@ -477,7 +473,7 @@ func setupTestDB() {
|
||||
|
||||
// Create the Subsonic Router with real DS, spy streamer, and real Decider
|
||||
spy = &spyStreamer{}
|
||||
decider := transcode.NewDecider(ds, noopFFmpeg{})
|
||||
decider := stream.NewTranscodeDecider(ds, noopFFmpeg{})
|
||||
s := scanner.New(ctx, ds, artwork.NoopCacheWarmer(), events.NoopBroker(),
|
||||
playlists.NewPlaylists(ds), metrics.NewNoopInstance())
|
||||
router = subsonic.New(
|
||||
|
||||
@@ -7,8 +7,9 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/navidrome/navidrome/core/auth"
|
||||
"github.com/navidrome/navidrome/core/transcode"
|
||||
"github.com/navidrome/navidrome/core/stream"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/utils/req"
|
||||
)
|
||||
|
||||
@@ -23,8 +24,19 @@ func (pub *Router) handleStream(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
stream, err := pub.streamer.NewStream(ctx, transcode.StreamRequest{
|
||||
ID: info.id, Format: info.format, BitRate: info.bitrate,
|
||||
mf, err := pub.ds.MediaFile(ctx).Get(info.id)
|
||||
if err != nil {
|
||||
if errors.Is(err, model.ErrNotFound) {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
} else {
|
||||
log.Error(ctx, "Error retrieving media file for shared stream", "id", info.id, err)
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
stream, err := pub.streamer.NewStream(ctx, mf, stream.Request{
|
||||
Format: info.format, BitRate: info.bitrate,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error(ctx, "Error starting shared stream", err)
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"github.com/navidrome/navidrome/core"
|
||||
"github.com/navidrome/navidrome/core/artwork"
|
||||
"github.com/navidrome/navidrome/core/publicurl"
|
||||
"github.com/navidrome/navidrome/core/transcode"
|
||||
"github.com/navidrome/navidrome/core/stream"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/server"
|
||||
@@ -21,14 +21,14 @@ import (
|
||||
type Router struct {
|
||||
http.Handler
|
||||
artwork artwork.Artwork
|
||||
streamer transcode.MediaStreamer
|
||||
streamer stream.MediaStreamer
|
||||
archiver core.Archiver
|
||||
share core.Share
|
||||
assetsHandler http.Handler
|
||||
ds model.DataStore
|
||||
}
|
||||
|
||||
func New(ds model.DataStore, artwork artwork.Artwork, streamer transcode.MediaStreamer, share core.Share, archiver core.Archiver) *Router {
|
||||
func New(ds model.DataStore, artwork artwork.Artwork, streamer stream.MediaStreamer, share core.Share, archiver core.Archiver) *Router {
|
||||
p := &Router{ds: ds, artwork: artwork, streamer: streamer, share: share, archiver: archiver}
|
||||
shareRoot := path.Join(conf.Server.BasePath, consts.URLPathPublic)
|
||||
p.assetsHandler = http.StripPrefix(shareRoot, http.FileServer(http.FS(ui.BuildAssets())))
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
"github.com/navidrome/navidrome/core/playback"
|
||||
playlistsvc "github.com/navidrome/navidrome/core/playlists"
|
||||
"github.com/navidrome/navidrome/core/scrobbler"
|
||||
"github.com/navidrome/navidrome/core/transcode"
|
||||
"github.com/navidrome/navidrome/core/stream"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/server"
|
||||
@@ -39,7 +39,7 @@ type Router struct {
|
||||
http.Handler
|
||||
ds model.DataStore
|
||||
artwork artwork.Artwork
|
||||
streamer transcode.MediaStreamer
|
||||
streamer stream.MediaStreamer
|
||||
archiver core.Archiver
|
||||
players core.Players
|
||||
provider external.Provider
|
||||
@@ -51,13 +51,13 @@ type Router struct {
|
||||
playback playback.PlaybackServer
|
||||
metrics metrics.Metrics
|
||||
lyrics lyricssvc.Lyrics
|
||||
transcodeDecision transcode.Decider
|
||||
transcodeDecision stream.TranscodeDecider
|
||||
}
|
||||
|
||||
func New(ds model.DataStore, artwork artwork.Artwork, streamer transcode.MediaStreamer, archiver core.Archiver,
|
||||
func New(ds model.DataStore, artwork artwork.Artwork, streamer stream.MediaStreamer, archiver core.Archiver,
|
||||
players core.Players, provider external.Provider, scanner model.Scanner, broker events.Broker,
|
||||
playlists playlistsvc.Playlists, scrobbler scrobbler.PlayTracker, share core.Share, playback playback.PlaybackServer,
|
||||
metrics metrics.Metrics, lyrics lyricssvc.Lyrics, transcodeDecision transcode.Decider,
|
||||
metrics metrics.Metrics, lyrics lyricssvc.Lyrics, transcodeDecision stream.TranscodeDecider,
|
||||
) *Router {
|
||||
r := &Router{
|
||||
ds: ds,
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/core/transcode"
|
||||
"github.com/navidrome/navidrome/core/stream"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/model/request"
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
"github.com/navidrome/navidrome/utils/req"
|
||||
)
|
||||
|
||||
func (api *Router) serveStream(ctx context.Context, w http.ResponseWriter, r *http.Request, stream *transcode.Stream, id string) {
|
||||
func (api *Router) serveStream(ctx context.Context, w http.ResponseWriter, r *http.Request, stream *stream.Stream, id string) {
|
||||
if stream.Seekable() {
|
||||
http.ServeContent(w, r, stream.Name(), stream.ModTime(), stream)
|
||||
} else {
|
||||
@@ -66,7 +66,7 @@ func (api *Router) Stream(w http.ResponseWriter, r *http.Request) (*responses.Su
|
||||
}
|
||||
|
||||
streamReq := api.transcodeDecision.ResolveRequest(ctx, mf, format, maxBitRate, timeOffset)
|
||||
stream, err := api.streamer.DoStream(ctx, mf, streamReq)
|
||||
stream, err := api.streamer.NewStream(ctx, mf, streamReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -136,7 +136,7 @@ func (api *Router) Download(w http.ResponseWriter, r *http.Request) (*responses.
|
||||
switch v := entity.(type) {
|
||||
case *model.MediaFile:
|
||||
streamReq := api.transcodeDecision.ResolveRequest(ctx, v, format, maxBitRate, 0)
|
||||
stream, err := api.streamer.DoStream(ctx, v, streamReq)
|
||||
stream, err := api.streamer.NewStream(ctx, v, streamReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"slices"
|
||||
"strconv"
|
||||
|
||||
"github.com/navidrome/navidrome/core/transcode"
|
||||
"github.com/navidrome/navidrome/core/stream"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/server/subsonic/responses"
|
||||
@@ -59,10 +59,10 @@ type limitationRequest struct {
|
||||
Required bool `json:"required,omitempty"`
|
||||
}
|
||||
|
||||
// toCoreClientInfo converts the API request struct to the transcode.ClientInfo struct.
|
||||
// toCoreClientInfo converts the API request struct to the stream.ClientInfo struct.
|
||||
// The OpenSubsonic spec uses bps for bitrate values; core uses kbps.
|
||||
func (r *clientInfoRequest) toCoreClientInfo() *transcode.ClientInfo {
|
||||
ci := &transcode.ClientInfo{
|
||||
func (r *clientInfoRequest) toCoreClientInfo() *stream.ClientInfo {
|
||||
ci := &stream.ClientInfo{
|
||||
Name: r.Name,
|
||||
Platform: r.Platform,
|
||||
MaxAudioBitrate: bpsToKbps(r.MaxAudioBitrate),
|
||||
@@ -70,7 +70,7 @@ func (r *clientInfoRequest) toCoreClientInfo() *transcode.ClientInfo {
|
||||
}
|
||||
|
||||
for _, dp := range r.DirectPlayProfiles {
|
||||
ci.DirectPlayProfiles = append(ci.DirectPlayProfiles, transcode.DirectPlayProfile{
|
||||
ci.DirectPlayProfiles = append(ci.DirectPlayProfiles, stream.DirectPlayProfile{
|
||||
Containers: dp.Containers,
|
||||
AudioCodecs: dp.AudioCodecs,
|
||||
Protocols: dp.Protocols,
|
||||
@@ -79,7 +79,7 @@ func (r *clientInfoRequest) toCoreClientInfo() *transcode.ClientInfo {
|
||||
}
|
||||
|
||||
for _, tp := range r.TranscodingProfiles {
|
||||
ci.TranscodingProfiles = append(ci.TranscodingProfiles, transcode.Profile{
|
||||
ci.TranscodingProfiles = append(ci.TranscodingProfiles, stream.Profile{
|
||||
Container: tp.Container,
|
||||
AudioCodec: tp.AudioCodec,
|
||||
Protocol: tp.Protocol,
|
||||
@@ -88,19 +88,19 @@ func (r *clientInfoRequest) toCoreClientInfo() *transcode.ClientInfo {
|
||||
}
|
||||
|
||||
for _, cp := range r.CodecProfiles {
|
||||
coreCP := transcode.CodecProfile{
|
||||
coreCP := stream.CodecProfile{
|
||||
Type: cp.Type,
|
||||
Name: cp.Name,
|
||||
}
|
||||
for _, lim := range cp.Limitations {
|
||||
coreLim := transcode.Limitation{
|
||||
coreLim := stream.Limitation{
|
||||
Name: lim.Name,
|
||||
Comparison: lim.Comparison,
|
||||
Values: lim.Values,
|
||||
Required: lim.Required,
|
||||
}
|
||||
// Convert audioBitrate limitation values from bps to kbps
|
||||
if lim.Name == transcode.LimitationAudioBitrate {
|
||||
if lim.Name == stream.LimitationAudioBitrate {
|
||||
coreLim.Values = convertBitrateValues(lim.Values)
|
||||
}
|
||||
coreCP.Limitations = append(coreCP.Limitations, coreLim)
|
||||
@@ -178,8 +178,8 @@ func isValidMediaType(mediaType string) bool {
|
||||
}
|
||||
|
||||
var validProtocols = []string{
|
||||
transcode.ProtocolHTTP,
|
||||
transcode.ProtocolHLS,
|
||||
stream.ProtocolHTTP,
|
||||
stream.ProtocolHLS,
|
||||
}
|
||||
|
||||
func isValidProtocol(p string) bool {
|
||||
@@ -187,7 +187,7 @@ func isValidProtocol(p string) bool {
|
||||
}
|
||||
|
||||
var validCodecProfileTypes = []string{
|
||||
transcode.CodecProfileTypeAudio,
|
||||
stream.CodecProfileTypeAudio,
|
||||
}
|
||||
|
||||
func isValidCodecProfileType(t string) bool {
|
||||
@@ -195,11 +195,11 @@ func isValidCodecProfileType(t string) bool {
|
||||
}
|
||||
|
||||
var validLimitationNames = []string{
|
||||
transcode.LimitationAudioChannels,
|
||||
transcode.LimitationAudioBitrate,
|
||||
transcode.LimitationAudioProfile,
|
||||
transcode.LimitationAudioSamplerate,
|
||||
transcode.LimitationAudioBitdepth,
|
||||
stream.LimitationAudioChannels,
|
||||
stream.LimitationAudioBitrate,
|
||||
stream.LimitationAudioProfile,
|
||||
stream.LimitationAudioSamplerate,
|
||||
stream.LimitationAudioBitdepth,
|
||||
}
|
||||
|
||||
func isValidLimitationName(n string) bool {
|
||||
@@ -207,10 +207,10 @@ func isValidLimitationName(n string) bool {
|
||||
}
|
||||
|
||||
var validComparisons = []string{
|
||||
transcode.ComparisonEquals,
|
||||
transcode.ComparisonNotEquals,
|
||||
transcode.ComparisonLessThanEqual,
|
||||
transcode.ComparisonGreaterThanEqual,
|
||||
stream.ComparisonEquals,
|
||||
stream.ComparisonNotEquals,
|
||||
stream.ComparisonLessThanEqual,
|
||||
stream.ComparisonGreaterThanEqual,
|
||||
}
|
||||
|
||||
func isValidComparison(c string) bool {
|
||||
@@ -218,9 +218,9 @@ func isValidComparison(c string) bool {
|
||||
}
|
||||
|
||||
// toResponseStreamDetails converts a core StreamDetails to the API response type.
|
||||
func toResponseStreamDetails(sd *transcode.StreamDetails) *responses.StreamDetails {
|
||||
func toResponseStreamDetails(sd *stream.Details) *responses.StreamDetails {
|
||||
return &responses.StreamDetails{
|
||||
Protocol: transcode.ProtocolHTTP, // TODO: derive from decision when HLS support is added
|
||||
Protocol: stream.ProtocolHTTP, // TODO: derive from decision when HLS support is added
|
||||
Container: sd.Container,
|
||||
Codec: sd.Codec,
|
||||
AudioBitrate: int32(kbpsToBps(sd.Bitrate)),
|
||||
@@ -232,7 +232,7 @@ func toResponseStreamDetails(sd *transcode.StreamDetails) *responses.StreamDetai
|
||||
}
|
||||
|
||||
// GetTranscodeDecision handles the OpenSubsonic getTranscodeDecision endpoint.
|
||||
// It receives client capabilities and returns a decision on whether to direct play or transcode.
|
||||
// It receives client capabilities and returns a decision on whether to direct play or stream.
|
||||
func (api *Router) GetTranscodeDecision(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) {
|
||||
if r.Method != http.MethodPost {
|
||||
w.Header().Set("Allow", "POST")
|
||||
@@ -279,7 +279,7 @@ func (api *Router) GetTranscodeDecision(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
// Make the decision
|
||||
decision, err := api.transcodeDecision.MakeDecision(ctx, mf, clientInfo, transcode.DecisionOptions{})
|
||||
decision, err := api.transcodeDecision.MakeDecision(ctx, mf, clientInfo, stream.TranscodeOptions{})
|
||||
if err != nil {
|
||||
log.Error(ctx, "Failed to make transcode decision", "mediaID", mediaID, err)
|
||||
return nil, newError(responses.ErrorGeneric, "failed to make transcode decision")
|
||||
@@ -343,13 +343,23 @@ func (api *Router) GetTranscodeStream(w http.ResponseWriter, r *http.Request) (*
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Fetch the media file
|
||||
mf, err := api.ds.MediaFile(ctx).Get(mediaID)
|
||||
if err != nil {
|
||||
if errors.Is(err, model.ErrNotFound) {
|
||||
http.Error(w, "Not Found", http.StatusNotFound)
|
||||
} else {
|
||||
log.Error(ctx, "Error retrieving media file", "mediaID", mediaID, err)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Validate the token and resolve streaming parameters
|
||||
streamReq, mf, err := api.transcodeDecision.ResolveRequestFromToken(ctx, transcodeParamsToken, mediaID, p.IntOr("offset", 0))
|
||||
streamReq, err := api.transcodeDecision.ResolveRequestFromToken(ctx, transcodeParamsToken, mf, p.IntOr("offset", 0))
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, transcode.ErrMediaNotFound):
|
||||
http.Error(w, "Not Found", http.StatusNotFound)
|
||||
case errors.Is(err, transcode.ErrTokenInvalid), errors.Is(err, transcode.ErrTokenStale):
|
||||
case errors.Is(err, stream.ErrTokenInvalid), errors.Is(err, stream.ErrTokenStale):
|
||||
http.Error(w, "Gone", http.StatusGone)
|
||||
default:
|
||||
log.Error(ctx, "Error validating transcode params", err)
|
||||
@@ -358,8 +368,8 @@ func (api *Router) GetTranscodeStream(w http.ResponseWriter, r *http.Request) (*
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Create stream (use DoStream to avoid duplicate DB fetch)
|
||||
stream, err := api.streamer.DoStream(ctx, mf, streamReq)
|
||||
// Create stream
|
||||
stream, err := api.streamer.NewStream(ctx, mf, streamReq)
|
||||
if err != nil {
|
||||
log.Error(ctx, "Error creating stream", "mediaID", mediaID, err)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
||||
"github.com/navidrome/navidrome/core/transcode"
|
||||
"github.com/navidrome/navidrome/core/stream"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/tests"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
@@ -156,10 +156,10 @@ var _ = Describe("Transcode endpoints", func() {
|
||||
mockMFRepo.SetData(model.MediaFiles{
|
||||
{ID: "song-1", Suffix: "mp3", Codec: "MP3", BitRate: 320, Channels: 2, SampleRate: 44100},
|
||||
})
|
||||
mockTD.decision = &transcode.Decision{
|
||||
mockTD.decision = &stream.TranscodeDecision{
|
||||
MediaID: "song-1",
|
||||
CanDirectPlay: true,
|
||||
SourceStream: transcode.StreamDetails{
|
||||
SourceStream: stream.Details{
|
||||
Container: "mp3", Codec: "mp3", Bitrate: 320,
|
||||
SampleRate: 44100, Channels: 2,
|
||||
},
|
||||
@@ -184,18 +184,18 @@ var _ = Describe("Transcode endpoints", func() {
|
||||
mockMFRepo.SetData(model.MediaFiles{
|
||||
{ID: "song-2", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 96000, BitDepth: 24},
|
||||
})
|
||||
mockTD.decision = &transcode.Decision{
|
||||
mockTD.decision = &stream.TranscodeDecision{
|
||||
MediaID: "song-2",
|
||||
CanDirectPlay: false,
|
||||
CanTranscode: true,
|
||||
TargetFormat: "mp3",
|
||||
TargetBitrate: 256,
|
||||
TranscodeReasons: []string{"container not supported"},
|
||||
SourceStream: transcode.StreamDetails{
|
||||
SourceStream: stream.Details{
|
||||
Container: "flac", Codec: "flac", Bitrate: 1000,
|
||||
SampleRate: 96000, BitDepth: 24, Channels: 2,
|
||||
},
|
||||
TranscodeStream: &transcode.StreamDetails{
|
||||
TranscodeStream: &stream.Details{
|
||||
Container: "mp3", Codec: "mp3", Bitrate: 256,
|
||||
SampleRate: 96000, Channels: 2,
|
||||
},
|
||||
@@ -231,7 +231,8 @@ var _ = Describe("Transcode endpoints", func() {
|
||||
})
|
||||
|
||||
It("returns 410 for invalid or mismatched token", func() {
|
||||
mockTD.resolveErr = transcode.ErrTokenInvalid
|
||||
mockMFRepo.SetData(model.MediaFiles{{ID: "123"}})
|
||||
mockTD.resolveErr = stream.ErrTokenInvalid
|
||||
r := newGetRequest("mediaId=123", "mediaType=song", "transcodeParams=bad-token")
|
||||
resp, err := router.GetTranscodeStream(w, r)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
@@ -240,7 +241,7 @@ var _ = Describe("Transcode endpoints", func() {
|
||||
})
|
||||
|
||||
It("returns 404 when media file not found", func() {
|
||||
mockTD.resolveErr = transcode.ErrMediaNotFound
|
||||
// mockMFRepo has no data, so Get() returns ErrNotFound
|
||||
r := newGetRequest("mediaId=gone-id", "mediaType=song", "transcodeParams=valid-token")
|
||||
resp, err := router.GetTranscodeStream(w, r)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
@@ -249,7 +250,8 @@ var _ = Describe("Transcode endpoints", func() {
|
||||
})
|
||||
|
||||
It("returns 410 when media file has changed (stale token)", func() {
|
||||
mockTD.resolveErr = transcode.ErrTokenStale
|
||||
mockMFRepo.SetData(model.MediaFiles{{ID: "song-1"}})
|
||||
mockTD.resolveErr = stream.ErrTokenStale
|
||||
r := newGetRequest("mediaId=song-1", "mediaType=song", "transcodeParams=stale-token")
|
||||
resp, err := router.GetTranscodeStream(w, r)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
@@ -260,14 +262,13 @@ 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.resolvedReq = transcode.StreamRequest{ID: "song-1"}
|
||||
mockTD.resolvedMF = &model.MediaFile{ID: "song-1"}
|
||||
mockMFRepo.SetData(model.MediaFiles{{ID: "song-1"}})
|
||||
mockTD.resolvedReq = stream.Request{}
|
||||
|
||||
r := newGetRequest("mediaId=song-1", "mediaType=song", "transcodeParams=valid-token")
|
||||
_, _ = router.GetTranscodeStream(w, r)
|
||||
|
||||
Expect(fakeStreamer.captured).ToNot(BeNil())
|
||||
Expect(fakeStreamer.captured.ID).To(Equal("song-1"))
|
||||
Expect(fakeStreamer.captured.Format).To(BeEmpty())
|
||||
Expect(fakeStreamer.captured.BitRate).To(BeZero())
|
||||
Expect(fakeStreamer.captured.SampleRate).To(BeZero())
|
||||
@@ -278,21 +279,19 @@ 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.resolvedReq = transcode.StreamRequest{
|
||||
ID: "song-2",
|
||||
mockMFRepo.SetData(model.MediaFiles{{ID: "song-2"}})
|
||||
mockTD.resolvedReq = stream.Request{
|
||||
Format: "mp3",
|
||||
BitRate: 256,
|
||||
SampleRate: 44100,
|
||||
BitDepth: 16,
|
||||
Channels: 2,
|
||||
}
|
||||
mockTD.resolvedMF = &model.MediaFile{ID: "song-2"}
|
||||
|
||||
r := newGetRequest("mediaId=song-2", "mediaType=song", "transcodeParams=valid-token", "offset=10")
|
||||
_, _ = router.GetTranscodeStream(w, r)
|
||||
|
||||
Expect(fakeStreamer.captured).ToNot(BeNil())
|
||||
Expect(fakeStreamer.captured.ID).To(Equal("song-2"))
|
||||
Expect(fakeStreamer.captured.Format).To(Equal("mp3"))
|
||||
Expect(fakeStreamer.captured.BitRate).To(Equal(256))
|
||||
Expect(fakeStreamer.captured.SampleRate).To(Equal(44100))
|
||||
@@ -353,38 +352,37 @@ func newJSONPostRequest(queryParams string, jsonBody string) *http.Request {
|
||||
return r
|
||||
}
|
||||
|
||||
// mockTranscodeDecision is a test double for transcode.Decider
|
||||
// mockTranscodeDecision is a test double for stream.TranscodeDecider
|
||||
type mockTranscodeDecision struct {
|
||||
decision *transcode.Decision
|
||||
decision *stream.TranscodeDecision
|
||||
token string
|
||||
tokenErr error
|
||||
resolvedReq transcode.StreamRequest
|
||||
resolvedMF *model.MediaFile
|
||||
resolvedReq stream.Request
|
||||
resolveErr error
|
||||
}
|
||||
|
||||
func (m *mockTranscodeDecision) MakeDecision(_ context.Context, _ *model.MediaFile, _ *transcode.ClientInfo, _ transcode.DecisionOptions) (*transcode.Decision, error) {
|
||||
func (m *mockTranscodeDecision) MakeDecision(_ context.Context, _ *model.MediaFile, _ *stream.ClientInfo, _ stream.TranscodeOptions) (*stream.TranscodeDecision, error) {
|
||||
if m.decision != nil {
|
||||
return m.decision, nil
|
||||
}
|
||||
return &transcode.Decision{}, nil
|
||||
return &stream.TranscodeDecision{}, nil
|
||||
}
|
||||
|
||||
func (m *mockTranscodeDecision) ResolveRequest(_ context.Context, _ *model.MediaFile, _ string, _ int, _ int) transcode.StreamRequest {
|
||||
return transcode.StreamRequest{Format: "raw"}
|
||||
func (m *mockTranscodeDecision) ResolveRequest(_ context.Context, _ *model.MediaFile, _ string, _ int, _ int) stream.Request {
|
||||
return stream.Request{Format: "raw"}
|
||||
}
|
||||
|
||||
func (m *mockTranscodeDecision) CreateTranscodeParams(_ *transcode.Decision) (string, error) {
|
||||
func (m *mockTranscodeDecision) CreateTranscodeParams(_ *stream.TranscodeDecision) (string, error) {
|
||||
return m.token, m.tokenErr
|
||||
}
|
||||
|
||||
func (m *mockTranscodeDecision) ResolveRequestFromToken(_ context.Context, _ string, _ string, offset int) (transcode.StreamRequest, *model.MediaFile, error) {
|
||||
func (m *mockTranscodeDecision) ResolveRequestFromToken(_ context.Context, _ string, _ *model.MediaFile, offset int) (stream.Request, error) {
|
||||
if m.resolveErr != nil {
|
||||
return transcode.StreamRequest{}, nil, m.resolveErr
|
||||
return stream.Request{}, m.resolveErr
|
||||
}
|
||||
req := m.resolvedReq
|
||||
req.Offset = offset
|
||||
return req, m.resolvedMF, nil
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// fakeMediaStreamer captures the StreamRequest and returns a sentinel error,
|
||||
@@ -392,15 +390,10 @@ func (m *mockTranscodeDecision) ResolveRequestFromToken(_ context.Context, _ str
|
||||
var errStreamCaptured = errors.New("stream request captured")
|
||||
|
||||
type fakeMediaStreamer struct {
|
||||
captured *transcode.StreamRequest
|
||||
captured *stream.Request
|
||||
}
|
||||
|
||||
func (f *fakeMediaStreamer) NewStream(_ context.Context, req transcode.StreamRequest) (*transcode.Stream, error) {
|
||||
f.captured = &req
|
||||
return nil, errStreamCaptured
|
||||
}
|
||||
|
||||
func (f *fakeMediaStreamer) DoStream(_ context.Context, _ *model.MediaFile, req transcode.StreamRequest) (*transcode.Stream, error) {
|
||||
func (f *fakeMediaStreamer) NewStream(_ context.Context, _ *model.MediaFile, req stream.Request) (*stream.Stream, error) {
|
||||
f.captured = &req
|
||||
return nil, errStreamCaptured
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user