diff --git a/backend/config.go b/backend/config.go index 44c4891..69563d8 100644 --- a/backend/config.go +++ b/backend/config.go @@ -142,7 +142,10 @@ type ThemeConfig struct { } type TranscodingConfig struct { - ForceRawFile bool + ForceRawFile bool + RequestTranscode bool + Codec string + MaxBitRateKBPS int } type PeakMeterConfig struct { @@ -260,7 +263,10 @@ func DefaultConfig(appVersionTag string) *Config { PreventClipping: true, }, Transcoding: TranscodingConfig{ - ForceRawFile: false, + ForceRawFile: false, + RequestTranscode: false, + Codec: "opus", + MaxBitRateKBPS: 160, }, Theme: ThemeConfig{ Appearance: "Dark", diff --git a/backend/mediaprovider/jellyfin/jellyfinmediaprovider.go b/backend/mediaprovider/jellyfin/jellyfinmediaprovider.go index 2315c42..19faba7 100644 --- a/backend/mediaprovider/jellyfin/jellyfinmediaprovider.go +++ b/backend/mediaprovider/jellyfin/jellyfinmediaprovider.go @@ -332,12 +332,19 @@ func (j *jellyfinMediaProvider) SetFavorite(params mediaprovider.RatingFavoriteP return err } -func (j *jellyfinMediaProvider) GetStreamURL(trackID string, forceRaw bool) (string, error) { - return j.client.GetStreamURL(trackID) +func (j *jellyfinMediaProvider) GetStreamURL(trackID string, transcode *mediaprovider.TranscodeSettings, forceRaw bool) (string, error) { + var jfTranscode *jellyfin.TranscodeOptions + if transcode != nil { + jfTranscode = &jellyfin.TranscodeOptions{ + AudioCodec: transcode.Codec, + AudioBitRate: uint32(transcode.BitRateKBPS * 1000), + } + } + return j.client.GetStreamURL(trackID, jfTranscode) } func (j *jellyfinMediaProvider) DownloadTrack(trackID string) (io.Reader, error) { - url, err := j.client.GetStreamURL(trackID) + url, err := j.client.GetStreamURL(trackID, nil) if err != nil { return nil, err } diff --git a/backend/mediaprovider/mediaprovider.go b/backend/mediaprovider/mediaprovider.go index a40db18..4f5fc79 100644 --- a/backend/mediaprovider/mediaprovider.go +++ b/backend/mediaprovider/mediaprovider.go @@ -184,6 +184,11 @@ type LoginResponse struct { IsAuthError bool } +type TranscodeSettings struct { + Codec string + BitRateKBPS int +} + type Server interface { Login(username, password string) LoginResponse MediaProvider() MediaProvider @@ -234,7 +239,7 @@ type MediaProvider interface { GetFavorites() (Favorites, error) - GetStreamURL(trackID string, forceRaw bool) (string, error) + GetStreamURL(trackID string, transcodeSettings *TranscodeSettings, forceRaw bool) (string, error) GetTopTracks(artist Artist, count int) ([]*Track, error) diff --git a/backend/mediaprovider/subsonic/subsonicmediaprovider.go b/backend/mediaprovider/subsonic/subsonicmediaprovider.go index 5b0b4cc..3d5d22a 100644 --- a/backend/mediaprovider/subsonic/subsonicmediaprovider.go +++ b/backend/mediaprovider/subsonic/subsonicmediaprovider.go @@ -234,9 +234,12 @@ func (s *subsonicMediaProvider) GetSimilarTracks(artistID string, count int) ([] return sharedutil.MapSlice(tr, toTrack), nil } -func (s *subsonicMediaProvider) GetStreamURL(trackID string, forceRaw bool) (string, error) { +func (s *subsonicMediaProvider) GetStreamURL(trackID string, transcode *mediaprovider.TranscodeSettings, forceRaw bool) (string, error) { m := make(map[string]string) - if forceRaw { + if transcode != nil { + m["format"] = transcode.Codec + m["maxBitRate"] = strconv.Itoa(transcode.BitRateKBPS) + } else if forceRaw { m["format"] = "raw" } u, err := s.client.GetStreamURL(trackID, m) diff --git a/backend/playbackengine.go b/backend/playbackengine.go index 09e7192..fa54680 100644 --- a/backend/playbackengine.go +++ b/backend/playbackengine.go @@ -652,7 +652,14 @@ func (p *playbackEngine) setTrack(idx int, next bool, startTime float64) error { item := p.playQueue[idx] meta = item.Metadata() if tr, ok := item.(*mediaprovider.Track); ok { - url, err = p.sm.Server.GetStreamURL(tr.ID, p.transcodeCfg.ForceRawFile) + var ts *mediaprovider.TranscodeSettings + if p.transcodeCfg.RequestTranscode { + ts = &mediaprovider.TranscodeSettings{ + Codec: p.transcodeCfg.Codec, + BitRateKBPS: p.transcodeCfg.MaxBitRateKBPS, + } + } + url, err = p.sm.Server.GetStreamURL(tr.ID, ts, p.transcodeCfg.ForceRawFile) } else { url = item.(*mediaprovider.RadioStation).StreamURL } diff --git a/go.mod b/go.mod index dcbf747..0df7973 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/dweymouth/fyne-advanced-list v0.0.0-20250211191927-58ea85eec72c github.com/dweymouth/fyne-lyrics v0.0.0-20240528234907-15eee7ce5e64 github.com/dweymouth/fyne-tooltip v0.3.0 - github.com/dweymouth/go-jellyfin v0.0.0-20240517151952-5ceca61cb645 + github.com/dweymouth/go-jellyfin v0.0.0-20250531151636-29591764f0a0 github.com/godbus/dbus/v5 v5.1.0 github.com/google/uuid v1.3.0 github.com/hashicorp/go-retryablehttp v0.7.7 diff --git a/go.sum b/go.sum index c26a6e2..a76ed23 100644 --- a/go.sum +++ b/go.sum @@ -25,8 +25,8 @@ github.com/dweymouth/fyne-tooltip v0.3.0 h1:NKCyTkh9NtvnTsiHtTOtaJzRDOFYP8AckQ2t github.com/dweymouth/fyne-tooltip v0.3.0/go.mod h1:m04ShLW/Tp6LXrNieTumApvNgo7YSB+wi+jZTN+kDBU= github.com/dweymouth/fyne/v2 v2.3.0-rc1.0.20250406173843-f5cb002423a5 h1:/gxlRSmndtF8QVxV3oFjlozm2clkMlLBlfiKFtehH+M= github.com/dweymouth/fyne/v2 v2.3.0-rc1.0.20250406173843-f5cb002423a5/go.mod h1:YZt7SksjvrSNJCwbWFV32WON3mE1Sr7L41D29qMZ/lU= -github.com/dweymouth/go-jellyfin v0.0.0-20240517151952-5ceca61cb645 h1:KzqSaQwG3HsTZQlEtkp0BeUy9vmYZ0rq0B15qIPSiBs= -github.com/dweymouth/go-jellyfin v0.0.0-20240517151952-5ceca61cb645/go.mod h1:fcUagHBaQnt06GmBAllNE0J4O/7064zXRWdqnTTtVjI= +github.com/dweymouth/go-jellyfin v0.0.0-20250531151636-29591764f0a0 h1:9t1CR83uzn5Va4Nycwncmuw/NEAN64O4lf82VBPzdfE= +github.com/dweymouth/go-jellyfin v0.0.0-20250531151636-29591764f0a0/go.mod h1:fcUagHBaQnt06GmBAllNE0J4O/7064zXRWdqnTTtVjI= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g= diff --git a/res/translations/en.json b/res/translations/en.json index 2c8ae9d..4b182a5 100644 --- a/res/translations/en.json +++ b/res/translations/en.json @@ -229,6 +229,7 @@ "Track number": "Track number", "Track peak": "Track peak", "tracks": "tracks", + "Transcode to": "Transcode to", "UI Scaling": "UI Scaling", "Unable to play artist radio": "Unable to play artist radio", "Unable to play random tracks": "Unable to play random tracks", diff --git a/ui/dialogs/settingsdialog.go b/ui/dialogs/settingsdialog.go index 94926e2..8b26732 100644 --- a/ui/dialogs/settingsdialog.go +++ b/ui/dialogs/settingsdialog.go @@ -302,7 +302,35 @@ func (s *SettingsDialog) createGeneralTab(canSaveQueueToServer bool) *container. } func (s *SettingsDialog) createPlaybackTab(isLocalPlayer, isReplayGainPlayer bool) *container.TabItem { - disableTranscode := widget.NewCheckWithData(lang.L("Disable server transcoding"), binding.BindBool(&s.config.Transcoding.ForceRawFile)) + transcodeCodec := widget.NewSelectWithData([]string{"opus", "mp3"}, binding.BindString(&s.config.Transcoding.Codec)) + transcodeBitRate := widget.NewSelectWithData([]string{"96", "128", "160", "192", "256", "320"}, + binding.IntToString(binding.BindInt(&s.config.Transcoding.MaxBitRateKBPS))) + if !s.config.Transcoding.RequestTranscode { + transcodeCodec.Disable() + transcodeBitRate.Disable() + } + + var transcode *widget.Check + disableTranscode := widget.NewCheck(lang.L("Disable server transcoding"), func(b bool) { + s.config.Transcoding.ForceRawFile = b + if b { + transcode.SetChecked(false) + } + }) + disableTranscode.Checked = s.config.Transcoding.ForceRawFile + transcode = widget.NewCheck(lang.L("Transcode to"), func(b bool) { + s.config.Transcoding.RequestTranscode = b + if b { + disableTranscode.SetChecked(false) + transcodeCodec.Enable() + transcodeBitRate.Enable() + } else { + transcodeCodec.Disable() + transcodeBitRate.Disable() + } + }) + transcode.Checked = s.config.Transcoding.RequestTranscode + deviceList := make([]string, len(s.audioDevices)) var selIndex int for i, dev := range s.audioDevices { @@ -392,14 +420,16 @@ func (s *SettingsDialog) createPlaybackTab(isLocalPlayer, isReplayGainPlayer boo } return container.NewTabItem(lang.L("Playback"), container.NewVBox( - disableTranscode, + container.New(&layout.CustomPaddedLayout{TopPadding: 5}, container.New(layout.NewFormLayout(), widget.NewLabel(lang.L("Audio device")), container.NewBorder(nil, nil, nil, util.NewHSpace(70), deviceSelect), layout.NewSpacer(), audioExclusive, )), s.newSectionSeparator(), - + disableTranscode, + container.NewHBox(transcode, transcodeCodec, transcodeBitRate), + s.newSectionSeparator(), widget.NewRichText(&widget.TextSegment{Text: "ReplayGain", Style: util.BoldRichTextStyle}), container.New(layout.NewFormLayout(), widget.NewLabel(lang.L("ReplayGain mode")), container.NewGridWithColumns(2, replayGainSelect),