From 4c1a95e851d94744e5c73c778bc26177f0988d31 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Wed, 24 Sep 2025 18:59:55 -0700 Subject: [PATCH] add settings for skipping one-star or tracks with keyword (#724) --- backend/config.go | 8 +++--- backend/playbackmanager.go | 50 +++++++++++++++++++++++------------- go.mod | 1 + go.sum | 2 ++ res/translations/en.json | 3 +++ ui/dialogs/settingsdialog.go | 9 ++++++- 6 files changed, 51 insertions(+), 22 deletions(-) diff --git a/backend/config.go b/backend/config.go index acf53a6..29566bb 100644 --- a/backend/config.go +++ b/backend/config.go @@ -112,9 +112,11 @@ type NowPlayingPageConfig struct { } type PlaybackConfig struct { - Autoplay bool - RepeatMode string - UseWaveformSeekbar bool + Autoplay bool + RepeatMode string + SkipOneStarWhenShuffling bool + SkipKeywordWhenShuffling string + UseWaveformSeekbar bool } type LocalPlaybackConfig struct { diff --git a/backend/playbackmanager.go b/backend/playbackmanager.go index 38e54f3..7da5eea 100644 --- a/backend/playbackmanager.go +++ b/backend/playbackmanager.go @@ -11,6 +11,7 @@ import ( "sync" "time" + "github.com/charlievieth/strcase" "github.com/dweymouth/supersonic/backend/mediaprovider" "github.com/dweymouth/supersonic/backend/player" "github.com/dweymouth/supersonic/backend/player/dlna" @@ -27,7 +28,8 @@ type PlaybackManager struct { wfmGen *WaveformImageGenerator cache *AudioCache cmdQueue *playbackCommandQueue - cfg *AppConfig + appCfg *AppConfig + cfg *PlaybackConfig localPlayer player.BasePlayer remotePlayersLock sync.Mutex @@ -36,8 +38,6 @@ type PlaybackManager struct { onWaveformImgUpdate []func(*WaveformImage) - autoplay bool - lastPlayTime float64 lastPlayingID string wfmUpdateImageCancel context.CancelFunc @@ -66,8 +66,8 @@ func NewPlaybackManager( pm := &PlaybackManager{ engine: e, cmdQueue: q, - cfg: appCfg, - autoplay: playbackCfg.Autoplay, + appCfg: appCfg, + cfg: playbackCfg, localPlayer: p, cache: c, } @@ -117,7 +117,7 @@ func (p *PlaybackManager) addOnTrackChangeHook() { p.OnSongChange(func(item mediaprovider.MediaItem, _ *mediaprovider.Track) { // Autoplay if enabled and we are on the last track - if p.autoplay && p.NowPlayingIndex() == len(p.engine.playQueue)-1 { + if p.cfg.Autoplay && p.NowPlayingIndex() == len(p.engine.playQueue)-1 { p.enqueueAutoplayTracks() } p.handleWaveformImageSongChange(item) @@ -470,13 +470,23 @@ func (p *PlaybackManager) PlayTrackAt(idx int) { func (p *PlaybackManager) PlayRandomSongs(genreName string) error { return p.fetchAndPlayTracks(func() ([]*mediaprovider.Track, error) { - return p.engine.sm.Server.GetRandomTracks(genreName, p.cfg.EnqueueBatchSize) + tr, err := p.engine.sm.Server.GetRandomTracks(genreName, p.appCfg.EnqueueBatchSize) + if err != nil { + return nil, err + } + return sharedutil.FilterSlice(tr, func(t *mediaprovider.Track) bool { + skipKwd := p.cfg.SkipKeywordWhenShuffling + include := + (skipKwd == "" || !strcase.Contains(t.Title, skipKwd)) && + (!p.cfg.SkipOneStarWhenShuffling || t.Rating != 1) + return include + }), nil }) } func (p *PlaybackManager) PlaySimilarSongs(id string) error { return p.fetchAndPlayTracks(func() ([]*mediaprovider.Track, error) { - return p.engine.sm.Server.GetSimilarTracks(id, p.cfg.EnqueueBatchSize) + return p.engine.sm.Server.GetSimilarTracks(id, p.appCfg.EnqueueBatchSize) }) } @@ -592,7 +602,7 @@ func (p *PlaybackManager) GetLoopMode() LoopMode { } func (p *PlaybackManager) IsAutoplay() bool { - return p.autoplay + return p.cfg.Autoplay } func (p *PlaybackManager) PlaybackStatus() PlaybackStatus { @@ -604,7 +614,7 @@ func (p *PlaybackManager) SetVolume(vol int) { } func (p *PlaybackManager) SetAutoplay(autoplay bool) { - p.autoplay = autoplay + p.cfg.Autoplay = autoplay if autoplay && p.NowPlayingIndex() == len(p.engine.playQueue)-1 { p.enqueueAutoplayTracks() } @@ -705,11 +715,15 @@ func (p *PlaybackManager) enqueueAutoplayTracks() { // tracks we will enqueue var tracks []*mediaprovider.Track - filterRecentlyPlayed := func(tracks []*mediaprovider.Track) []*mediaprovider.Track { + filterAutoplayTracks := func(tracks []*mediaprovider.Track) []*mediaprovider.Track { return sharedutil.FilterSlice(tracks, func(t *mediaprovider.Track) bool { - return !slices.ContainsFunc(queue, func(i mediaprovider.MediaItem) bool { + shouldSkip := + (p.cfg.SkipOneStarWhenShuffling && t.Rating == 1) || + (p.cfg.SkipKeywordWhenShuffling != "" && strcase.Contains(t.Title, p.cfg.SkipKeywordWhenShuffling)) + recentlyPlayed := slices.ContainsFunc(queue, func(i mediaprovider.MediaItem) bool { return i.Metadata().Type == mediaprovider.MediaItemTypeTrack && i.Metadata().ID == t.ID }) + return !shouldSkip && !recentlyPlayed }) } @@ -722,11 +736,11 @@ func (p *PlaybackManager) enqueueAutoplayTracks() { // similar tracks by artist if len(tr.ArtistIDs) > 0 { - similar, err := s.GetSimilarTracks(tr.ArtistIDs[0], p.cfg.EnqueueBatchSize) + similar, err := s.GetSimilarTracks(tr.ArtistIDs[0], p.appCfg.EnqueueBatchSize) if err != nil { log.Printf("autoplay error: failed to get similar tracks: %v", err) } - tracks = filterRecentlyPlayed(similar) + tracks = filterAutoplayTracks(similar) } // fallback to random tracks from genre @@ -735,11 +749,11 @@ func (p *PlaybackManager) enqueueAutoplayTracks() { if g == "" { continue } - byGenre, err := s.GetRandomTracks(g, p.cfg.EnqueueBatchSize) + byGenre, err := s.GetRandomTracks(g, p.appCfg.EnqueueBatchSize) if err != nil { log.Printf("autoplay error: failed to get tracks by genre: %v", err) } - tracks = filterRecentlyPlayed(byGenre) + tracks = filterAutoplayTracks(byGenre) if len(tracks) > 0 { break } @@ -750,11 +764,11 @@ func (p *PlaybackManager) enqueueAutoplayTracks() { // random tracks works regardless of the type of the last playing media if len(tracks) == 0 { // fallback to random tracks - random, err := s.GetRandomTracks("", p.cfg.EnqueueBatchSize) + random, err := s.GetRandomTracks("", p.appCfg.EnqueueBatchSize) if err != nil { log.Printf("autoplay error: failed to get random tracks: %v", err) } - tracks = filterRecentlyPlayed(random) + tracks = filterAutoplayTracks(random) } if len(tracks) > 0 { diff --git a/go.mod b/go.mod index f0d0833..554e29c 100644 --- a/go.mod +++ b/go.mod @@ -33,6 +33,7 @@ require ( al.essio.dev/pkg/shellescape v1.5.1 // indirect fyne.io/systray v1.11.0 // indirect github.com/BurntSushi/toml v1.4.0 // indirect + github.com/charlievieth/strcase v0.0.5 // indirect github.com/danieljoos/wincred v1.2.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fogleman/gg v1.3.0 // indirect diff --git a/go.sum b/go.sum index b85fb41..b8d81f4 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,8 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/cenkalti/dominantcolor v1.0.3 h1:Pt0vfRZ8enkZh1n22RvoboA53SMM/v2aEwNQTZKSqww= github.com/cenkalti/dominantcolor v1.0.3/go.mod h1:mGpFMbWUnyXaGN48Zbf9bU9HJP1eCCD7dnsscb4lyR4= +github.com/charlievieth/strcase v0.0.5 h1:gV4iXVyD6eI5KdfOV+/vIVCKXZwtCWOmDMcu7Uy00Rs= +github.com/charlievieth/strcase v0.0.5/go.mod h1:FIOYY1aDBMSIOFqmVomHBpoK+bteGlESRsgsdWjrhx8= github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0= github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/res/translations/en.json b/res/translations/en.json index 73fd979..02b5408 100644 --- a/res/translations/en.json +++ b/res/translations/en.json @@ -211,8 +211,10 @@ "Singles": "Singles", "Size": "Size", "Skip duplicate tracks": "Skip duplicate tracks", + "Skip one-star tracks": "Skip one-star tracks", "Skip SSL certificate verification": "Skip SSL certificate verification", "Skip this version": "Skip this version", + "Skip tracks with keyword": "Skip tracks with keyword", "Smaller": "Smaller", "Sort": "Sort", "Soundtrack": "Soundtrack", @@ -251,6 +253,7 @@ "version": "version", "Visualizations": "Visualizations", "Volume": "Volume", + "When enqueuing random": "When enqueuing random", "wrong URL": "wrong URL", "wrong username/password": "wrong username/password", "Year": "Year", diff --git a/ui/dialogs/settingsdialog.go b/ui/dialogs/settingsdialog.go index d085ed5..43a2cf5 100644 --- a/ui/dialogs/settingsdialog.go +++ b/ui/dialogs/settingsdialog.go @@ -427,12 +427,19 @@ func (s *SettingsDialog) createPlaybackTab(isLocalPlayer, isReplayGainPlayer boo disableTranscode, container.NewHBox(transcode, transcodeCodec, transcodeBitRate), s.newSectionSeparator(), - widget.NewRichText(&widget.TextSegment{Text: "ReplayGain", Style: util.BoldRichTextStyle}), + widget.NewLabelWithStyle("ReplayGain", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), container.New(layout.NewFormLayout(), widget.NewLabel(lang.L("ReplayGain mode")), container.NewGridWithColumns(2, replayGainSelect), widget.NewLabel(lang.L("ReplayGain preamp")), container.NewHBox(preampGain, widget.NewLabel("dB")), widget.NewLabel(lang.L("Prevent clipping")), preventClipping, ), + s.newSectionSeparator(), + widget.NewLabelWithStyle(lang.L("When enqueuing random"), fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), + widget.NewCheckWithData(lang.L("Skip one-star tracks"), binding.BindBool(&s.config.Playback.SkipOneStarWhenShuffling)), + container.NewBorder(nil, nil, + widget.NewLabel(lang.L("Skip tracks with keyword")), nil, + widget.NewEntryWithData(binding.BindString(&s.config.Playback.SkipKeywordWhenShuffling)), + ), )) }