refactor(transcoding): rename TranscodeDecision to Decider and update related methods for clarity

Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Deluan 2026-02-06 07:14:06 -05:00
parent d4357c670b
commit 1cc7a2cd66
8 changed files with 101 additions and 81 deletions

View File

@ -21,6 +21,7 @@ import (
"github.com/navidrome/navidrome/core/playback" "github.com/navidrome/navidrome/core/playback"
"github.com/navidrome/navidrome/core/playlists" "github.com/navidrome/navidrome/core/playlists"
"github.com/navidrome/navidrome/core/scrobbler" "github.com/navidrome/navidrome/core/scrobbler"
"github.com/navidrome/navidrome/core/transcode"
"github.com/navidrome/navidrome/db" "github.com/navidrome/navidrome/db"
"github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/persistence" "github.com/navidrome/navidrome/persistence"
@ -105,8 +106,8 @@ func CreateSubsonicAPIRouter(ctx context.Context) *subsonic.Router {
playTracker := scrobbler.GetPlayTracker(dataStore, broker, manager) playTracker := scrobbler.GetPlayTracker(dataStore, broker, manager)
playbackServer := playback.GetInstance(dataStore) playbackServer := playback.GetInstance(dataStore)
lyricsLyrics := lyrics.NewLyrics(manager) lyricsLyrics := lyrics.NewLyrics(manager)
transcodeDecision := core.NewTranscodeDecision(dataStore) decider := transcode.NewDecider(dataStore)
router := subsonic.New(dataStore, artworkArtwork, mediaStreamer, archiver, players, provider, modelScanner, broker, playlistsPlaylists, playTracker, share, playbackServer, metricsMetrics, lyricsLyrics, transcodeDecision) router := subsonic.New(dataStore, artworkArtwork, mediaStreamer, archiver, players, provider, modelScanner, broker, playlistsPlaylists, playTracker, share, playbackServer, metricsMetrics, lyricsLyrics, decider)
return router return router
} }

View File

@ -1,4 +1,4 @@
package core package transcode
import ( import (
"context" "context"
@ -13,15 +13,15 @@ import (
) )
const ( const (
transcodeTokenTTL = 12 * time.Hour tokenTTL = 12 * time.Hour
defaultTranscodeBitrate = 256 // kbps defaultBitrate = 256 // kbps
) )
// TranscodeDecision is the core service interface for making transcoding decisions // Decider is the core service interface for making transcoding decisions
type TranscodeDecision interface { type Decider interface {
MakeDecision(ctx context.Context, mf *model.MediaFile, clientInfo *ClientInfo) (*Decision, error) MakeDecision(ctx context.Context, mf *model.MediaFile, clientInfo *ClientInfo) (*Decision, error)
CreateTranscodeParams(decision *Decision) (string, error) CreateTranscodeParams(decision *Decision) (string, error)
ParseTranscodeParams(token string) (*TranscodeParams, error) ParseTranscodeParams(token string) (*Params, error)
} }
// ClientInfo represents client playback capabilities. // ClientInfo represents client playback capabilities.
@ -32,7 +32,7 @@ type ClientInfo struct {
MaxAudioBitrate int MaxAudioBitrate int
MaxTranscodingAudioBitrate int MaxTranscodingAudioBitrate int
DirectPlayProfiles []DirectPlayProfile DirectPlayProfiles []DirectPlayProfile
TranscodingProfiles []TranscodingProfile TranscodingProfiles []Profile
CodecProfiles []CodecProfile CodecProfiles []CodecProfile
} }
@ -44,8 +44,8 @@ type DirectPlayProfile struct {
MaxAudioChannels int MaxAudioChannels int
} }
// TranscodingProfile describes a transcoding target the client supports // Profile describes a transcoding target the client supports
type TranscodingProfile struct { type Profile struct {
Container string Container string
AudioCodec string AudioCodec string
Protocol string Protocol string
@ -125,9 +125,9 @@ type StreamDetails struct {
IsLossless bool IsLossless bool
} }
// TranscodeParams contains the parameters extracted from a transcode token. // Params contains the parameters extracted from a transcode token.
// TargetBitrate is in kilobits per second (kbps). // TargetBitrate is in kilobits per second (kbps).
type TranscodeParams struct { type Params struct {
MediaID string MediaID string
DirectPlay bool DirectPlay bool
TargetFormat string TargetFormat string
@ -135,17 +135,17 @@ type TranscodeParams struct {
TargetChannels int TargetChannels int
} }
func NewTranscodeDecision(ds model.DataStore) TranscodeDecision { func NewDecider(ds model.DataStore) Decider {
return &transcodeDecisionService{ return &deciderService{
ds: ds, ds: ds,
} }
} }
type transcodeDecisionService struct { type deciderService struct {
ds model.DataStore ds model.DataStore
} }
func (s *transcodeDecisionService) MakeDecision(ctx context.Context, mf *model.MediaFile, clientInfo *ClientInfo) (*Decision, error) { func (s *deciderService) MakeDecision(ctx context.Context, mf *model.MediaFile, clientInfo *ClientInfo) (*Decision, error) {
decision := &Decision{ decision := &Decision{
MediaID: mf.ID, MediaID: mf.ID,
} }
@ -224,7 +224,7 @@ func (s *transcodeDecisionService) MakeDecision(ctx context.Context, mf *model.M
// checkDirectPlayProfile returns "" if the profile matches (direct play OK), // checkDirectPlayProfile returns "" if the profile matches (direct play OK),
// or a typed reason string if it doesn't match. // or a typed reason string if it doesn't match.
func (s *transcodeDecisionService) checkDirectPlayProfile(mf *model.MediaFile, sourceBitrate int, profile *DirectPlayProfile, clientInfo *ClientInfo) string { func (s *deciderService) checkDirectPlayProfile(mf *model.MediaFile, sourceBitrate int, profile *DirectPlayProfile, clientInfo *ClientInfo) string {
// Check protocol (only http for now) // Check protocol (only http for now)
if len(profile.Protocols) > 0 && !containsIgnoreCase(profile.Protocols, ProtocolHTTP) { if len(profile.Protocols) > 0 && !containsIgnoreCase(profile.Protocols, ProtocolHTTP) {
return "protocol not supported" return "protocol not supported"
@ -303,7 +303,7 @@ const (
// computeTranscodedStream attempts to build a valid transcoded stream for the given profile. // computeTranscodedStream attempts to build a valid transcoded stream for the given profile.
// Returns nil if the profile cannot produce a valid output. // Returns nil if the profile cannot produce a valid output.
func (s *transcodeDecisionService) computeTranscodedStream(ctx context.Context, mf *model.MediaFile, sourceBitrate int, profile *TranscodingProfile, clientInfo *ClientInfo) *StreamDetails { func (s *deciderService) computeTranscodedStream(ctx context.Context, mf *model.MediaFile, sourceBitrate int, profile *Profile, clientInfo *ClientInfo) *StreamDetails {
// Check protocol (only http for now) // Check protocol (only http for now)
if profile.Protocol != "" && !strings.EqualFold(profile.Protocol, ProtocolHTTP) { if profile.Protocol != "" && !strings.EqualFold(profile.Protocol, ProtocolHTTP) {
log.Trace(ctx, "Skipping transcoding profile: unsupported protocol", "protocol", profile.Protocol) log.Trace(ctx, "Skipping transcoding profile: unsupported protocol", "protocol", profile.Protocol)
@ -348,7 +348,7 @@ func (s *transcodeDecisionService) computeTranscodedStream(ctx context.Context,
if clientInfo.MaxTranscodingAudioBitrate > 0 { if clientInfo.MaxTranscodingAudioBitrate > 0 {
ts.Bitrate = clientInfo.MaxTranscodingAudioBitrate ts.Bitrate = clientInfo.MaxTranscodingAudioBitrate
} else { } else {
ts.Bitrate = defaultTranscodeBitrate ts.Bitrate = defaultBitrate
} }
} else { } else {
// Lossless to lossless: check if bitrate is under the global max // Lossless to lossless: check if bitrate is under the global max
@ -490,8 +490,8 @@ func applyIntLimitation(comparison string, values []string, current int, setter
return adjustNone return adjustNone
} }
func (s *transcodeDecisionService) CreateTranscodeParams(decision *Decision) (string, error) { func (s *deciderService) CreateTranscodeParams(decision *Decision) (string, error) {
exp := time.Now().Add(transcodeTokenTTL) exp := time.Now().Add(tokenTTL)
claims := map[string]any{ claims := map[string]any{
"mid": decision.MediaID, "mid": decision.MediaID,
"dp": decision.CanDirectPlay, "dp": decision.CanDirectPlay,
@ -506,13 +506,13 @@ func (s *transcodeDecisionService) CreateTranscodeParams(decision *Decision) (st
return auth.CreateExpiringPublicToken(exp, claims) return auth.CreateExpiringPublicToken(exp, claims)
} }
func (s *transcodeDecisionService) ParseTranscodeParams(token string) (*TranscodeParams, error) { func (s *deciderService) ParseTranscodeParams(token string) (*Params, error) {
claims, err := auth.Validate(token) claims, err := auth.Validate(token)
if err != nil { if err != nil {
return nil, err return nil, err
} }
params := &TranscodeParams{} params := &Params{}
if mid, ok := claims["mid"].(string); ok { if mid, ok := claims["mid"].(string); ok {
params.MediaID = mid params.MediaID = mid
} }

View File

@ -0,0 +1,17 @@
package transcode
import (
"testing"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/tests"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestTranscode(t *testing.T) {
tests.Init(t, false)
log.SetLevel(log.LevelFatal)
RegisterFailHandler(Fail)
RunSpecs(t, "Transcode Suite")
}

View File

@ -1,4 +1,4 @@
package core package transcode
import ( import (
"context" "context"
@ -10,10 +10,10 @@ import (
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
) )
var _ = Describe("TranscodeDecision", func() { var _ = Describe("Decider", func() {
var ( var (
ds *tests.MockDataStore ds *tests.MockDataStore
svc TranscodeDecision svc Decider
ctx context.Context ctx context.Context
) )
@ -24,7 +24,7 @@ var _ = Describe("TranscodeDecision", func() {
MockedTranscoding: &tests.MockTranscodingRepo{}, MockedTranscoding: &tests.MockTranscodingRepo{},
} }
auth.Init(ds) auth.Init(ds)
svc = NewTranscodeDecision(ds) svc = NewDecider(ds)
}) })
Describe("MakeDecision", func() { Describe("MakeDecision", func() {
@ -151,7 +151,7 @@ var _ = Describe("TranscodeDecision", func() {
DirectPlayProfiles: []DirectPlayProfile{ DirectPlayProfiles: []DirectPlayProfile{
{Containers: []string{"flac"}, Protocols: []string{"http"}}, {Containers: []string{"flac"}, Protocols: []string{"http"}},
}, },
TranscodingProfiles: []TranscodingProfile{ TranscodingProfiles: []Profile{
{Container: "mp3", AudioCodec: "mp3", Protocol: "http"}, {Container: "mp3", AudioCodec: "mp3", Protocol: "http"},
}, },
} }
@ -171,7 +171,7 @@ var _ = Describe("TranscodeDecision", func() {
DirectPlayProfiles: []DirectPlayProfile{ DirectPlayProfiles: []DirectPlayProfile{
{Containers: []string{"mp3"}, Protocols: []string{"http"}}, {Containers: []string{"mp3"}, Protocols: []string{"http"}},
}, },
TranscodingProfiles: []TranscodingProfile{ TranscodingProfiles: []Profile{
{Container: "mp3", AudioCodec: "mp3", Protocol: "http", MaxAudioChannels: 2}, {Container: "mp3", AudioCodec: "mp3", Protocol: "http", MaxAudioChannels: 2},
}, },
} }
@ -187,7 +187,7 @@ var _ = Describe("TranscodeDecision", func() {
It("rejects lossy to lossless transcoding", func() { It("rejects lossy to lossless transcoding", func() {
mf := &model.MediaFile{ID: "1", Suffix: "mp3", Codec: "MP3", BitRate: 320, Channels: 2} mf := &model.MediaFile{ID: "1", Suffix: "mp3", Codec: "MP3", BitRate: 320, Channels: 2}
ci := &ClientInfo{ ci := &ClientInfo{
TranscodingProfiles: []TranscodingProfile{ TranscodingProfiles: []Profile{
{Container: "flac", Protocol: "http"}, {Container: "flac", Protocol: "http"},
}, },
} }
@ -199,21 +199,21 @@ var _ = Describe("TranscodeDecision", func() {
It("uses default bitrate when client doesn't specify", func() { It("uses default bitrate when client doesn't specify", func() {
mf := &model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, BitDepth: 16} mf := &model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, BitDepth: 16}
ci := &ClientInfo{ ci := &ClientInfo{
TranscodingProfiles: []TranscodingProfile{ TranscodingProfiles: []Profile{
{Container: "mp3", Protocol: "http"}, {Container: "mp3", Protocol: "http"},
}, },
} }
decision, err := svc.MakeDecision(ctx, mf, ci) decision, err := svc.MakeDecision(ctx, mf, ci)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
Expect(decision.CanTranscode).To(BeTrue()) Expect(decision.CanTranscode).To(BeTrue())
Expect(decision.TargetBitrate).To(Equal(defaultTranscodeBitrate)) // 256 kbps Expect(decision.TargetBitrate).To(Equal(defaultBitrate)) // 256 kbps
}) })
It("preserves lossy bitrate when under max", func() { It("preserves lossy bitrate when under max", func() {
mf := &model.MediaFile{ID: "1", Suffix: "ogg", BitRate: 192, Channels: 2} mf := &model.MediaFile{ID: "1", Suffix: "ogg", BitRate: 192, Channels: 2}
ci := &ClientInfo{ ci := &ClientInfo{
MaxTranscodingAudioBitrate: 256, // kbps MaxTranscodingAudioBitrate: 256, // kbps
TranscodingProfiles: []TranscodingProfile{ TranscodingProfiles: []Profile{
{Container: "mp3", Protocol: "http"}, {Container: "mp3", Protocol: "http"},
}, },
} }
@ -226,7 +226,7 @@ var _ = Describe("TranscodeDecision", func() {
It("rejects unsupported transcoding format", func() { It("rejects unsupported transcoding format", func() {
mf := &model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2} mf := &model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2}
ci := &ClientInfo{ ci := &ClientInfo{
TranscodingProfiles: []TranscodingProfile{ TranscodingProfiles: []Profile{
{Container: "aac", Protocol: "http"}, {Container: "aac", Protocol: "http"},
}, },
} }
@ -239,7 +239,7 @@ var _ = Describe("TranscodeDecision", func() {
mf := &model.MediaFile{ID: "1", Suffix: "mp3", Codec: "MP3", BitRate: 320, Channels: 2} mf := &model.MediaFile{ID: "1", Suffix: "mp3", Codec: "MP3", BitRate: 320, Channels: 2}
ci := &ClientInfo{ ci := &ClientInfo{
MaxAudioBitrate: 96, // kbps MaxAudioBitrate: 96, // kbps
TranscodingProfiles: []TranscodingProfile{ TranscodingProfiles: []Profile{
{Container: "mp3", AudioCodec: "mp3", Protocol: "http"}, {Container: "mp3", AudioCodec: "mp3", Protocol: "http"},
}, },
} }
@ -256,7 +256,7 @@ var _ = Describe("TranscodeDecision", func() {
DirectPlayProfiles: []DirectPlayProfile{ DirectPlayProfiles: []DirectPlayProfile{
{Containers: []string{"mp3"}, Protocols: []string{"http"}}, {Containers: []string{"mp3"}, Protocols: []string{"http"}},
}, },
TranscodingProfiles: []TranscodingProfile{ TranscodingProfiles: []Profile{
{Container: "opus", AudioCodec: "opus", Protocol: "http"}, {Container: "opus", AudioCodec: "opus", Protocol: "http"},
{Container: "mp3", AudioCodec: "mp3", Protocol: "http", MaxAudioChannels: 2}, {Container: "mp3", AudioCodec: "mp3", Protocol: "http", MaxAudioChannels: 2},
}, },
@ -278,7 +278,7 @@ var _ = Describe("TranscodeDecision", func() {
DirectPlayProfiles: []DirectPlayProfile{ DirectPlayProfiles: []DirectPlayProfile{
{Containers: []string{"flac"}, Protocols: []string{"http"}}, {Containers: []string{"flac"}, Protocols: []string{"http"}},
}, },
TranscodingProfiles: []TranscodingProfile{ TranscodingProfiles: []Profile{
{Container: "mp3", AudioCodec: "mp3", Protocol: "http"}, {Container: "mp3", AudioCodec: "mp3", Protocol: "http"},
}, },
} }
@ -292,14 +292,14 @@ var _ = Describe("TranscodeDecision", func() {
// Simulate DSD→FLAC transcoding by using a mock that supports "flac" // Simulate DSD→FLAC transcoding by using a mock that supports "flac"
mockTranscoding := &tests.MockTranscodingRepo{} mockTranscoding := &tests.MockTranscodingRepo{}
ds.MockedTranscoding = mockTranscoding ds.MockedTranscoding = mockTranscoding
svc = NewTranscodeDecision(ds) svc = NewDecider(ds)
// MockTranscodingRepo doesn't support flac, so this will skip lossless profile. // MockTranscodingRepo doesn't support flac, so this will skip lossless profile.
// Use mp3 which is supported as the fallback. // Use mp3 which is supported as the fallback.
mf := &model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 96000, BitDepth: 24} mf := &model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 96000, BitDepth: 24}
ci := &ClientInfo{ ci := &ClientInfo{
MaxTranscodingAudioBitrate: 320, MaxTranscodingAudioBitrate: 320,
TranscodingProfiles: []TranscodingProfile{ TranscodingProfiles: []Profile{
{Container: "mp3", AudioCodec: "mp3", Protocol: "http"}, {Container: "mp3", AudioCodec: "mp3", Protocol: "http"},
}, },
} }
@ -480,7 +480,7 @@ var _ = Describe("TranscodeDecision", func() {
mf := &model.MediaFile{ID: "1", Suffix: "mp3", Codec: "MP3", BitRate: 192, Channels: 2, SampleRate: 44100} mf := &model.MediaFile{ID: "1", Suffix: "mp3", Codec: "MP3", BitRate: 192, Channels: 2, SampleRate: 44100}
ci := &ClientInfo{ ci := &ClientInfo{
MaxAudioBitrate: 96, // force transcode MaxAudioBitrate: 96, // force transcode
TranscodingProfiles: []TranscodingProfile{ TranscodingProfiles: []Profile{
{Container: "mp3", AudioCodec: "mp3", Protocol: "http"}, {Container: "mp3", AudioCodec: "mp3", Protocol: "http"},
}, },
CodecProfiles: []CodecProfile{ CodecProfiles: []CodecProfile{
@ -503,7 +503,7 @@ var _ = Describe("TranscodeDecision", func() {
mf := &model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 6, SampleRate: 48000, BitDepth: 16} mf := &model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 6, SampleRate: 48000, BitDepth: 16}
ci := &ClientInfo{ ci := &ClientInfo{
MaxTranscodingAudioBitrate: 320, MaxTranscodingAudioBitrate: 320,
TranscodingProfiles: []TranscodingProfile{ TranscodingProfiles: []Profile{
{Container: "mp3", AudioCodec: "mp3", Protocol: "http"}, {Container: "mp3", AudioCodec: "mp3", Protocol: "http"},
}, },
CodecProfiles: []CodecProfile{ CodecProfiles: []CodecProfile{
@ -526,7 +526,7 @@ var _ = Describe("TranscodeDecision", func() {
mf := &model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 96000, BitDepth: 24} mf := &model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 96000, BitDepth: 24}
ci := &ClientInfo{ ci := &ClientInfo{
MaxTranscodingAudioBitrate: 320, MaxTranscodingAudioBitrate: 320,
TranscodingProfiles: []TranscodingProfile{ TranscodingProfiles: []Profile{
{Container: "mp3", AudioCodec: "mp3", Protocol: "http"}, {Container: "mp3", AudioCodec: "mp3", Protocol: "http"},
}, },
CodecProfiles: []CodecProfile{ CodecProfiles: []CodecProfile{
@ -549,7 +549,7 @@ var _ = Describe("TranscodeDecision", func() {
mf := &model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: 16} mf := &model.MediaFile{ID: "1", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 44100, BitDepth: 16}
ci := &ClientInfo{ ci := &ClientInfo{
MaxTranscodingAudioBitrate: 320, MaxTranscodingAudioBitrate: 320,
TranscodingProfiles: []TranscodingProfile{ TranscodingProfiles: []Profile{
{Container: "mp3", AudioCodec: "mp3", Protocol: "http"}, {Container: "mp3", AudioCodec: "mp3", Protocol: "http"},
}, },
CodecProfiles: []CodecProfile{ CodecProfiles: []CodecProfile{
@ -577,7 +577,7 @@ var _ = Describe("TranscodeDecision", func() {
{Containers: []string{"mp3"}, AudioCodecs: []string{"mp3"}, Protocols: []string{"http"}}, {Containers: []string{"mp3"}, AudioCodecs: []string{"mp3"}, Protocols: []string{"http"}},
{Containers: []string{"m4a", "mp4"}, AudioCodecs: []string{"aac"}, Protocols: []string{"http"}}, {Containers: []string{"m4a", "mp4"}, AudioCodecs: []string{"aac"}, Protocols: []string{"http"}},
}, },
TranscodingProfiles: []TranscodingProfile{ TranscodingProfiles: []Profile{
{Container: "mp3", AudioCodec: "mp3", Protocol: "http"}, {Container: "mp3", AudioCodec: "mp3", Protocol: "http"},
}, },
} }

View File

@ -10,6 +10,7 @@ import (
"github.com/navidrome/navidrome/core/playback" "github.com/navidrome/navidrome/core/playback"
"github.com/navidrome/navidrome/core/playlists" "github.com/navidrome/navidrome/core/playlists"
"github.com/navidrome/navidrome/core/scrobbler" "github.com/navidrome/navidrome/core/scrobbler"
"github.com/navidrome/navidrome/core/transcode"
) )
var Set = wire.NewSet( var Set = wire.NewSet(
@ -22,7 +23,7 @@ var Set = wire.NewSet(
NewLibrary, NewLibrary,
NewUser, NewUser,
NewMaintenance, NewMaintenance,
NewTranscodeDecision, transcode.NewDecider,
agents.GetAgents, agents.GetAgents,
external.NewProvider, external.NewProvider,
wire.Bind(new(external.Agents), new(*agents.Agents)), wire.Bind(new(external.Agents), new(*agents.Agents)),

View File

@ -19,6 +19,7 @@ import (
"github.com/navidrome/navidrome/core/playback" "github.com/navidrome/navidrome/core/playback"
playlistsvc "github.com/navidrome/navidrome/core/playlists" playlistsvc "github.com/navidrome/navidrome/core/playlists"
"github.com/navidrome/navidrome/core/scrobbler" "github.com/navidrome/navidrome/core/scrobbler"
"github.com/navidrome/navidrome/core/transcode"
"github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/server" "github.com/navidrome/navidrome/server"
@ -50,13 +51,13 @@ type Router struct {
playback playback.PlaybackServer playback playback.PlaybackServer
metrics metrics.Metrics metrics metrics.Metrics
lyrics lyricssvc.Lyrics lyrics lyricssvc.Lyrics
transcodeDecision core.TranscodeDecision transcodeDecision transcode.Decider
} }
func New(ds model.DataStore, artwork artwork.Artwork, streamer core.MediaStreamer, archiver core.Archiver, func New(ds model.DataStore, artwork artwork.Artwork, streamer core.MediaStreamer, archiver core.Archiver,
players core.Players, provider external.Provider, scanner model.Scanner, broker events.Broker, players core.Players, provider external.Provider, scanner model.Scanner, broker events.Broker,
playlists playlistsvc.Playlists, scrobbler scrobbler.PlayTracker, share core.Share, playback playback.PlaybackServer, playlists playlistsvc.Playlists, scrobbler scrobbler.PlayTracker, share core.Share, playback playback.PlaybackServer,
metrics metrics.Metrics, lyrics lyricssvc.Lyrics, transcodeDecision core.TranscodeDecision, metrics metrics.Metrics, lyrics lyricssvc.Lyrics, transcodeDecision transcode.Decider,
) *Router { ) *Router {
r := &Router{ r := &Router{
ds: ds, ds: ds,

View File

@ -6,7 +6,7 @@ import (
"net/http" "net/http"
"strconv" "strconv"
"github.com/navidrome/navidrome/core" "github.com/navidrome/navidrome/core/transcode"
"github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/server/subsonic/responses" "github.com/navidrome/navidrome/server/subsonic/responses"
"github.com/navidrome/navidrome/utils/req" "github.com/navidrome/navidrome/utils/req"
@ -56,10 +56,10 @@ type limitationReq struct {
Required bool `json:"required,omitempty"` Required bool `json:"required,omitempty"`
} }
// toCoreClientInfo converts the API request struct to the core.ClientInfo struct. // toCoreClientInfo converts the API request struct to the transcode.ClientInfo struct.
// The OpenSubsonic spec uses bps for bitrate values; core uses kbps. // The OpenSubsonic spec uses bps for bitrate values; core uses kbps.
func (r *clientInfoRequest) toCoreClientInfo() *core.ClientInfo { func (r *clientInfoRequest) toCoreClientInfo() *transcode.ClientInfo {
ci := &core.ClientInfo{ ci := &transcode.ClientInfo{
Name: r.Name, Name: r.Name,
Platform: r.Platform, Platform: r.Platform,
MaxAudioBitrate: bpsToKbps(r.MaxAudioBitrate), MaxAudioBitrate: bpsToKbps(r.MaxAudioBitrate),
@ -67,7 +67,7 @@ func (r *clientInfoRequest) toCoreClientInfo() *core.ClientInfo {
} }
for _, dp := range r.DirectPlayProfiles { for _, dp := range r.DirectPlayProfiles {
ci.DirectPlayProfiles = append(ci.DirectPlayProfiles, core.DirectPlayProfile{ ci.DirectPlayProfiles = append(ci.DirectPlayProfiles, transcode.DirectPlayProfile{
Containers: dp.Containers, Containers: dp.Containers,
AudioCodecs: dp.AudioCodecs, AudioCodecs: dp.AudioCodecs,
Protocols: dp.Protocols, Protocols: dp.Protocols,
@ -76,7 +76,7 @@ func (r *clientInfoRequest) toCoreClientInfo() *core.ClientInfo {
} }
for _, tp := range r.TranscodingProfiles { for _, tp := range r.TranscodingProfiles {
ci.TranscodingProfiles = append(ci.TranscodingProfiles, core.TranscodingProfile{ ci.TranscodingProfiles = append(ci.TranscodingProfiles, transcode.Profile{
Container: tp.Container, Container: tp.Container,
AudioCodec: tp.AudioCodec, AudioCodec: tp.AudioCodec,
Protocol: tp.Protocol, Protocol: tp.Protocol,
@ -85,19 +85,19 @@ func (r *clientInfoRequest) toCoreClientInfo() *core.ClientInfo {
} }
for _, cp := range r.CodecProfiles { for _, cp := range r.CodecProfiles {
coreCP := core.CodecProfile{ coreCP := transcode.CodecProfile{
Type: cp.Type, Type: cp.Type,
Name: cp.Name, Name: cp.Name,
} }
for _, lim := range cp.Limitations { for _, lim := range cp.Limitations {
coreLim := core.Limitation{ coreLim := transcode.Limitation{
Name: lim.Name, Name: lim.Name,
Comparison: lim.Comparison, Comparison: lim.Comparison,
Values: lim.Values, Values: lim.Values,
Required: lim.Required, Required: lim.Required,
} }
// Convert audioBitrate limitation values from bps to kbps // Convert audioBitrate limitation values from bps to kbps
if lim.Name == core.LimitationAudioBitrate { if lim.Name == transcode.LimitationAudioBitrate {
coreLim.Values = convertBitrateValues(lim.Values) coreLim.Values = convertBitrateValues(lim.Values)
} }
coreCP.Limitations = append(coreCP.Limitations, coreLim) coreCP.Limitations = append(coreCP.Limitations, coreLim)
@ -163,26 +163,26 @@ func (r *clientInfoRequest) validate() error {
} }
func isValidProtocol(p string) bool { func isValidProtocol(p string) bool {
return p == core.ProtocolHTTP || p == core.ProtocolHLS return p == transcode.ProtocolHTTP || p == transcode.ProtocolHLS
} }
func isValidCodecProfileType(t string) bool { func isValidCodecProfileType(t string) bool {
return t == core.CodecProfileTypeAudio return t == transcode.CodecProfileTypeAudio
} }
func isValidLimitationName(n string) bool { func isValidLimitationName(n string) bool {
return n == core.LimitationAudioChannels || return n == transcode.LimitationAudioChannels ||
n == core.LimitationAudioBitrate || n == transcode.LimitationAudioBitrate ||
n == core.LimitationAudioProfile || n == transcode.LimitationAudioProfile ||
n == core.LimitationAudioSamplerate || n == transcode.LimitationAudioSamplerate ||
n == core.LimitationAudioBitdepth n == transcode.LimitationAudioBitdepth
} }
func isValidComparison(c string) bool { func isValidComparison(c string) bool {
return c == core.ComparisonEquals || return c == transcode.ComparisonEquals ||
c == core.ComparisonNotEquals || c == transcode.ComparisonNotEquals ||
c == core.ComparisonLessThanEqual || c == transcode.ComparisonLessThanEqual ||
c == core.ComparisonGreaterThanEqual c == transcode.ComparisonGreaterThanEqual
} }
// GetTranscodeDecision handles the OpenSubsonic getTranscodeDecision endpoint. // GetTranscodeDecision handles the OpenSubsonic getTranscodeDecision endpoint.

View File

@ -6,7 +6,7 @@ import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"github.com/navidrome/navidrome/core" "github.com/navidrome/navidrome/core/transcode"
"github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/tests" "github.com/navidrome/navidrome/tests"
. "github.com/onsi/ginkgo/v2" . "github.com/onsi/ginkgo/v2"
@ -146,10 +146,10 @@ var _ = Describe("Transcode endpoints", func() {
mockMFRepo.SetData(model.MediaFiles{ mockMFRepo.SetData(model.MediaFiles{
{ID: "song-1", Suffix: "mp3", Codec: "MP3", BitRate: 320, Channels: 2, SampleRate: 44100}, {ID: "song-1", Suffix: "mp3", Codec: "MP3", BitRate: 320, Channels: 2, SampleRate: 44100},
}) })
mockTD.decision = &core.Decision{ mockTD.decision = &transcode.Decision{
MediaID: "song-1", MediaID: "song-1",
CanDirectPlay: true, CanDirectPlay: true,
SourceStream: core.StreamDetails{ SourceStream: transcode.StreamDetails{
Container: "mp3", Codec: "mp3", Bitrate: 320, Container: "mp3", Codec: "mp3", Bitrate: 320,
SampleRate: 44100, Channels: 2, SampleRate: 44100, Channels: 2,
}, },
@ -174,18 +174,18 @@ var _ = Describe("Transcode endpoints", func() {
mockMFRepo.SetData(model.MediaFiles{ mockMFRepo.SetData(model.MediaFiles{
{ID: "song-2", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 96000, BitDepth: 24}, {ID: "song-2", Suffix: "flac", Codec: "FLAC", BitRate: 1000, Channels: 2, SampleRate: 96000, BitDepth: 24},
}) })
mockTD.decision = &core.Decision{ mockTD.decision = &transcode.Decision{
MediaID: "song-2", MediaID: "song-2",
CanDirectPlay: false, CanDirectPlay: false,
CanTranscode: true, CanTranscode: true,
TargetFormat: "mp3", TargetFormat: "mp3",
TargetBitrate: 256, TargetBitrate: 256,
TranscodeReasons: []string{"container not supported"}, TranscodeReasons: []string{"container not supported"},
SourceStream: core.StreamDetails{ SourceStream: transcode.StreamDetails{
Container: "flac", Codec: "flac", Bitrate: 1000, Container: "flac", Codec: "flac", Bitrate: 1000,
SampleRate: 96000, BitDepth: 24, Channels: 2, SampleRate: 96000, BitDepth: 24, Channels: 2,
}, },
TranscodeStream: &core.StreamDetails{ TranscodeStream: &transcode.StreamDetails{
Container: "mp3", Codec: "mp3", Bitrate: 256, Container: "mp3", Codec: "mp3", Bitrate: 256,
SampleRate: 96000, Channels: 2, SampleRate: 96000, Channels: 2,
}, },
@ -224,7 +224,7 @@ var _ = Describe("Transcode endpoints", func() {
}) })
It("returns error when mediaId doesn't match token", func() { It("returns error when mediaId doesn't match token", func() {
mockTD.params = &core.TranscodeParams{MediaID: "other-id", DirectPlay: true} mockTD.params = &transcode.Params{MediaID: "other-id", DirectPlay: true}
r := newGetRequest("mediaId=wrong-id", "mediaType=song", "transcodeParams=valid-token") r := newGetRequest("mediaId=wrong-id", "mediaType=song", "transcodeParams=valid-token")
_, err := router.GetTranscodeStream(w, r) _, err := router.GetTranscodeStream(w, r)
Expect(err).To(HaveOccurred()) Expect(err).To(HaveOccurred())
@ -242,25 +242,25 @@ func newJSONPostRequest(queryParams string, jsonBody string) *http.Request {
// mockTranscodeDecision is a test double for core.TranscodeDecision // mockTranscodeDecision is a test double for core.TranscodeDecision
type mockTranscodeDecision struct { type mockTranscodeDecision struct {
decision *core.Decision decision *transcode.Decision
token string token string
tokenErr error tokenErr error
params *core.TranscodeParams params *transcode.Params
parseErr error parseErr error
} }
func (m *mockTranscodeDecision) MakeDecision(_ context.Context, _ *model.MediaFile, _ *core.ClientInfo) (*core.Decision, error) { func (m *mockTranscodeDecision) MakeDecision(_ context.Context, _ *model.MediaFile, _ *transcode.ClientInfo) (*transcode.Decision, error) {
if m.decision != nil { if m.decision != nil {
return m.decision, nil return m.decision, nil
} }
return &core.Decision{}, nil return &transcode.Decision{}, nil
} }
func (m *mockTranscodeDecision) CreateTranscodeParams(_ *core.Decision) (string, error) { func (m *mockTranscodeDecision) CreateTranscodeParams(_ *transcode.Decision) (string, error) {
return m.token, m.tokenErr return m.token, m.tokenErr
} }
func (m *mockTranscodeDecision) ParseTranscodeParams(_ string) (*core.TranscodeParams, error) { func (m *mockTranscodeDecision) ParseTranscodeParams(_ string) (*transcode.Params, error) {
if m.parseErr != nil { if m.parseErr != nil {
return nil, m.parseErr return nil, m.parseErr
} }