Merge pull request #526 from dweymouth/feature/autoplay
Add autoplay mode to queue similar tracks when nearing end of play queue
This commit is contained in:
@@ -361,6 +361,7 @@ func (a *App) Shutdown() {
|
||||
repeatMode = "All"
|
||||
}
|
||||
a.Config.Playback.RepeatMode = repeatMode
|
||||
a.Config.Playback.Autoplay = a.PlaybackManager.IsAutoplay()
|
||||
a.Config.LocalPlayback.Volume = a.LocalPlayer.GetVolume()
|
||||
a.SavePlayQueueIfEnabled()
|
||||
a.SaveConfigFile()
|
||||
|
||||
@@ -99,6 +99,7 @@ type NowPlayingPageConfig struct {
|
||||
}
|
||||
|
||||
type PlaybackConfig struct {
|
||||
Autoplay bool
|
||||
RepeatMode string
|
||||
}
|
||||
|
||||
@@ -215,6 +216,7 @@ func DefaultConfig(appVersionTag string) *Config {
|
||||
TracklistColumns: []string{"Album", "Time", "Plays"},
|
||||
},
|
||||
Playback: PlaybackConfig{
|
||||
Autoplay: false,
|
||||
RepeatMode: "None",
|
||||
},
|
||||
LocalPlayback: LocalPlaybackConfig{
|
||||
|
||||
+107
-2
@@ -5,11 +5,14 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"runtime"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/dweymouth/supersonic/backend/mediaprovider"
|
||||
"github.com/dweymouth/supersonic/backend/player"
|
||||
"github.com/dweymouth/supersonic/backend/player/mpv"
|
||||
"github.com/dweymouth/supersonic/sharedutil"
|
||||
)
|
||||
|
||||
// A high-level MediaProvider-aware playback engine, serves as an
|
||||
@@ -19,6 +22,8 @@ type PlaybackManager struct {
|
||||
cmdQueue *playbackCommandQueue
|
||||
cfg *AppConfig
|
||||
|
||||
autoplay bool
|
||||
|
||||
lastPlayTime float64
|
||||
}
|
||||
|
||||
@@ -37,13 +42,14 @@ func NewPlaybackManager(
|
||||
engine: e,
|
||||
cmdQueue: q,
|
||||
cfg: appCfg,
|
||||
autoplay: playbackCfg.Autoplay,
|
||||
}
|
||||
pm.workaroundWindowsPlaybackIssue()
|
||||
pm.addOnTrackChangeHook()
|
||||
go pm.runCmdQueue(ctx)
|
||||
return pm
|
||||
}
|
||||
|
||||
func (p *PlaybackManager) workaroundWindowsPlaybackIssue() {
|
||||
func (p *PlaybackManager) addOnTrackChangeHook() {
|
||||
// See https://github.com/dweymouth/supersonic/issues/483
|
||||
// On Windows, MPV sometimes fails to start playback when switching to a track
|
||||
// with a different sample rate than the previous. If this is detected,
|
||||
@@ -51,7 +57,17 @@ func (p *PlaybackManager) workaroundWindowsPlaybackIssue() {
|
||||
p.OnPlayTimeUpdate(func(curTime, _ float64, _ bool) {
|
||||
p.lastPlayTime = curTime
|
||||
})
|
||||
|
||||
p.OnSongChange(func(mediaprovider.MediaItem, *mediaprovider.Track) {
|
||||
// Autoplay if enabled and we are on the last track
|
||||
if p.autoplay && p.NowPlayingIndex() == len(p.engine.playQueue)-1 {
|
||||
p.enqueueAutoplayTracks()
|
||||
}
|
||||
|
||||
if runtime.GOOS != "windows" {
|
||||
return
|
||||
}
|
||||
// workaround for https://github.com/dweymouth/supersonic/issues/483 (see above comment)
|
||||
if p.NowPlayingIndex() != len(p.engine.playQueue) && p.PlayerStatus().State == player.Playing {
|
||||
p.lastPlayTime = 0
|
||||
go func() {
|
||||
@@ -348,6 +364,10 @@ func (p *PlaybackManager) GetLoopMode() LoopMode {
|
||||
return p.engine.loopMode
|
||||
}
|
||||
|
||||
func (p *PlaybackManager) IsAutoplay() bool {
|
||||
return p.autoplay
|
||||
}
|
||||
|
||||
func (p *PlaybackManager) PlayerStatus() player.Status {
|
||||
return p.engine.PlayerStatus()
|
||||
}
|
||||
@@ -356,6 +376,13 @@ func (p *PlaybackManager) SetVolume(vol int) {
|
||||
p.cmdQueue.SetVolume(vol)
|
||||
}
|
||||
|
||||
func (p *PlaybackManager) SetAutoplay(autoplay bool) {
|
||||
p.autoplay = autoplay
|
||||
if autoplay && p.NowPlayingIndex() == len(p.engine.playQueue)-1 {
|
||||
p.enqueueAutoplayTracks()
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PlaybackManager) Volume() int {
|
||||
return p.engine.CurrentPlayer().GetVolume()
|
||||
}
|
||||
@@ -419,6 +446,84 @@ func (p *PlaybackManager) PlayPause() {
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PlaybackManager) enqueueAutoplayTracks() {
|
||||
nowPlaying := p.NowPlaying()
|
||||
if nowPlaying == nil {
|
||||
return
|
||||
}
|
||||
|
||||
s := p.engine.sm.Server
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// last 500 played items
|
||||
queue := p.GetPlayQueue()
|
||||
if l := len(queue); l > 500 {
|
||||
queue = queue[l-500:]
|
||||
}
|
||||
|
||||
// tracks we will enqueue
|
||||
var tracks []*mediaprovider.Track
|
||||
|
||||
filterRecentlyPlayed := func(tracks []*mediaprovider.Track) []*mediaprovider.Track {
|
||||
return sharedutil.FilterSlice(tracks, func(t *mediaprovider.Track) bool {
|
||||
return !slices.ContainsFunc(queue, func(i mediaprovider.MediaItem) bool {
|
||||
return i.Metadata().Type == mediaprovider.MediaItemTypeTrack && i.Metadata().ID == t.ID
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// since this func is invoked in a callback from the playback engine,
|
||||
// need to do the rest async as it may take time and block other callbacks
|
||||
go func() {
|
||||
// first 2 strategies - similar by artist, and similar by genres - only work for tracks
|
||||
if nowPlaying.Metadata().Type == mediaprovider.MediaItemTypeTrack {
|
||||
tr := nowPlaying.(*mediaprovider.Track)
|
||||
|
||||
// similar tracks by artist
|
||||
if len(tr.ArtistIDs) > 0 {
|
||||
similar, err := s.GetSimilarTracks(tr.ArtistIDs[0], p.cfg.EnqueueBatchSize)
|
||||
if err != nil {
|
||||
log.Println("autoplay error: failed to get similar tracks: %v", err)
|
||||
}
|
||||
tracks = filterRecentlyPlayed(similar)
|
||||
}
|
||||
|
||||
// fallback to random tracks from genre
|
||||
if len(tracks) == 0 {
|
||||
for _, g := range tr.Genres {
|
||||
if g == "" {
|
||||
continue
|
||||
}
|
||||
byGenre, err := s.GetRandomTracks(g, p.cfg.EnqueueBatchSize)
|
||||
if err != nil {
|
||||
log.Println("autoplay error: failed to get tracks by genre: %v", err)
|
||||
}
|
||||
tracks = filterRecentlyPlayed(byGenre)
|
||||
if len(tracks) > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
log.Println("autoplay error: failed to get random tracks: %v", err)
|
||||
}
|
||||
tracks = filterRecentlyPlayed(random)
|
||||
}
|
||||
|
||||
if len(tracks) > 0 {
|
||||
p.LoadTracks(tracks, Append, false /*no need to shuffle, already random*/)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (p *PlaybackManager) runCmdQueue(ctx context.Context) {
|
||||
logIfErr := func(action string, err error) {
|
||||
if err != nil {
|
||||
|
||||
@@ -40,6 +40,11 @@ var ResHeartOutlineSvg = &fyne.StaticResource{
|
||||
StaticContent: []byte(
|
||||
"<?xml version=\"1.0\" encoding=\"utf-8\"?><svg xmlns=\"http://www.w3.org/2000/svg\" height=\"16\" width=\"16\" viewBox=\"0 0 512 512\"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.--><path d=\"M225.8 468.2l-2.5-2.3L48.1 303.2C17.4 274.7 0 234.7 0 192.8v-3.3c0-70.4 50-130.8 119.2-144C158.6 37.9 198.9 47 231 69.6c9 6.4 17.4 13.8 25 22.3c4.2-4.8 8.7-9.2 13.5-13.3c3.7-3.2 7.5-6.2 11.5-9c0 0 0 0 0 0C313.1 47 353.4 37.9 392.8 45.4C462 58.6 512 119.1 512 189.5v3.3c0 41.9-17.4 81.9-48.1 110.4L288.7 465.9l-2.5 2.3c-8.2 7.6-19 11.9-30.2 11.9s-22-4.2-30.2-11.9zM239.1 145c-.4-.3-.7-.7-1-1.1l-17.8-20c0 0-.1-.1-.1-.1c0 0 0 0 0 0c-23.1-25.9-58-37.7-92-31.2C81.6 101.5 48 142.1 48 189.5v3.3c0 28.5 11.9 55.8 32.8 75.2L256 430.7 431.2 268c20.9-19.4 32.8-46.7 32.8-75.2v-3.3c0-47.3-33.6-88-80.1-96.9c-34-6.5-69 5.4-92 31.2c0 0 0 0-.1 .1s0 0-.1 .1l-17.8 20c-.3 .4-.7 .7-1 1.1c-4.5 4.5-10.6 7-16.9 7s-12.4-2.5-16.9-7z\"/></svg>"),
|
||||
}
|
||||
var ResInfinitySvg = &fyne.StaticResource{
|
||||
StaticName: "infinity.svg",
|
||||
StaticContent: []byte(
|
||||
"<?xml version=\"1.0\" encoding=\"utf-8\"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->\n<svg fill=\"#000000\" width=\"800px\" height=\"800px\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\">\n <path fill-rule=\"evenodd\" d=\"M13.8953301,9.49438552 C13.5245249,8.8053229 13.1631795,8.23232447 12.787113,7.76330109 C12.9146953,7.63245274 13.0230614,7.52301032 13.0881226,7.46104725 C14.1672616,6.43329581 15.2308059,6 17,6 C20.2160271,6 23,8.59837471 23,12 C23,15.4016253 20.2160271,18 17,18 C14.1474839,18 12.9356928,16.5161742 11.0880785,12.4103647 C9.56069278,9.0161742 8.73081718,8 7,8 C4.74909152,8 3,9.86806378 3,12 C3,14.1411158 4.73495862,16 7,16 C8.04331411,16 9.29028192,15.5849395 10.3793264,14.9973112 C10.7662435,15.6627411 11.149328,16.202911 11.5567767,16.631918 C10.1542402,17.4308849 8.51106941,18 7,18 C3.59837471,18 1,15.2160271 1,12 C1,8.79183891 3.61403375,6 7,6 C9.85251615,6 11.0643072,7.4838258 12.9119215,11.5896353 C14.4393072,14.9838258 15.2691828,16 17,16 C19.1411158,16 21,14.2650414 21,12 C21,9.73495862 19.1411158,8 17,8 C15.7489921,8 15.177938,8.23265169 14.467433,8.90932312 C14.3735045,8.99877883 14.1397256,9.24036553 13.8953301,9.49438552 Z\"/>\n</svg>"),
|
||||
}
|
||||
var ResMusicnotesSvg = &fyne.StaticResource{
|
||||
StaticName: "musicnotes.svg",
|
||||
StaticContent: []byte(
|
||||
|
||||
@@ -7,6 +7,7 @@ fyne bundle -append -prefix Res icons/publicdomain/disc.svg >> bundled.go
|
||||
fyne bundle -append -prefix Res icons/publicdomain/headphones.svg >> bundled.go
|
||||
fyne bundle -append -prefix Res icons/publicdomain/heart-filled.svg >> bundled.go
|
||||
fyne bundle -append -prefix Res icons/publicdomain/heart-outline.svg >> bundled.go
|
||||
fyne bundle -append -prefix Res icons/publicdomain/infinity.svg >> bundled.go
|
||||
fyne bundle -append -prefix Res icons/publicdomain/musicnotes.svg >> bundled.go
|
||||
fyne bundle -append -prefix Res icons/publicdomain/people.svg >> bundled.go
|
||||
fyne bundle -append -prefix Res icons/publicdomain/playlist.svg >> bundled.go
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" d="M13.8953301,9.49438552 C13.5245249,8.8053229 13.1631795,8.23232447 12.787113,7.76330109 C12.9146953,7.63245274 13.0230614,7.52301032 13.0881226,7.46104725 C14.1672616,6.43329581 15.2308059,6 17,6 C20.2160271,6 23,8.59837471 23,12 C23,15.4016253 20.2160271,18 17,18 C14.1474839,18 12.9356928,16.5161742 11.0880785,12.4103647 C9.56069278,9.0161742 8.73081718,8 7,8 C4.74909152,8 3,9.86806378 3,12 C3,14.1411158 4.73495862,16 7,16 C8.04331411,16 9.29028192,15.5849395 10.3793264,14.9973112 C10.7662435,15.6627411 11.149328,16.202911 11.5567767,16.631918 C10.1542402,17.4308849 8.51106941,18 7,18 C3.59837471,18 1,15.2160271 1,12 C1,8.79183891 3.61403375,6 7,6 C9.85251615,6 11.0643072,7.4838258 12.9119215,11.5896353 C14.4393072,14.9838258 15.2691828,16 17,16 C19.1411158,16 21,14.2650414 21,12 C21,9.73495862 19.1411158,8 17,8 C15.7489921,8 15.177938,8.23265169 14.467433,8.90932312 C14.3735045,8.99877883 14.1397256,9.24036553 13.8953301,9.49438552 Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -28,6 +28,7 @@
|
||||
"Audiobook": "Audiobook",
|
||||
"Authentication failed": "Authentication failed",
|
||||
"Auto": "Auto",
|
||||
"Autoplay": "Autoplay",
|
||||
"Autoselect device": "Autoselect device",
|
||||
"Back": "Back",
|
||||
"Bit rate": "Bit rate",
|
||||
|
||||
+4
-1
@@ -102,7 +102,7 @@ func NewBottomPanel(pm *backend.PlaybackManager, im *backend.ImageManager, contr
|
||||
pm.SeekFraction(f)
|
||||
})
|
||||
|
||||
bp.AuxControls = widgets.NewAuxControls(pm.Volume(), pm.GetLoopMode())
|
||||
bp.AuxControls = widgets.NewAuxControls(pm.Volume(), pm.GetLoopMode(), pm.IsAutoplay())
|
||||
pm.OnLoopModeChange(bp.AuxControls.SetLoopMode)
|
||||
pm.OnVolumeChange(bp.AuxControls.VolumeControl.SetVolume)
|
||||
bp.AuxControls.VolumeControl.OnSetVolume = func(v int) {
|
||||
@@ -111,6 +111,9 @@ func NewBottomPanel(pm *backend.PlaybackManager, im *backend.ImageManager, contr
|
||||
bp.AuxControls.OnChangeLoopMode(func() {
|
||||
pm.SetNextLoopMode()
|
||||
})
|
||||
bp.AuxControls.OnChangeAutoplay = func(autoplay bool) {
|
||||
pm.SetAutoplay(autoplay)
|
||||
}
|
||||
bp.AuxControls.OnShowPlayQueue(contr.ShowPopUpPlayQueue)
|
||||
|
||||
bp.imageLoader = util.NewThumbnailLoader(im, bp.NowPlaying.SetImage)
|
||||
|
||||
@@ -160,6 +160,7 @@ func NewMainWindow(fyneApp fyne.App, appName, displayAppName, appVersion string,
|
||||
repeatMode = "All"
|
||||
}
|
||||
app.Config.Playback.RepeatMode = repeatMode
|
||||
app.Config.Playback.Autoplay = app.PlaybackManager.IsAutoplay()
|
||||
app.SavePlayQueueIfEnabled()
|
||||
app.SaveConfigFile()
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ var (
|
||||
|
||||
AlbumIcon fyne.Resource = theme.NewThemedResource(res.ResDiscSvg)
|
||||
ArtistIcon fyne.Resource = theme.NewThemedResource(res.ResPeopleSvg)
|
||||
AutoplayIcon fyne.Resource = theme.NewThemedResource(res.ResInfinitySvg)
|
||||
RadioIcon fyne.Resource = theme.NewThemedResource(res.ResBroadcastSvg)
|
||||
FavoriteIcon fyne.Resource = theme.NewThemedResource(res.ResHeartFilledSvg)
|
||||
NotFavoriteIcon fyne.Resource = theme.NewThemedResource(res.ResHeartOutlineSvg)
|
||||
|
||||
@@ -18,21 +18,34 @@ import (
|
||||
type AuxControls struct {
|
||||
widget.BaseWidget
|
||||
|
||||
OnChangeAutoplay func(autoplay bool)
|
||||
|
||||
VolumeControl *VolumeControl
|
||||
autoplay *IconButton
|
||||
loop *IconButton
|
||||
showQueue *IconButton
|
||||
|
||||
container *fyne.Container
|
||||
}
|
||||
|
||||
func NewAuxControls(initialVolume int, initialLoopMode backend.LoopMode) *AuxControls {
|
||||
func NewAuxControls(initialVolume int, initialLoopMode backend.LoopMode, initialAutoplay bool) *AuxControls {
|
||||
a := &AuxControls{
|
||||
VolumeControl: NewVolumeControl(initialVolume),
|
||||
autoplay: NewIconButton(myTheme.AutoplayIcon, nil),
|
||||
loop: NewIconButton(myTheme.RepeatIcon, nil),
|
||||
showQueue: NewIconButton(myTheme.PlayQueueIcon, nil),
|
||||
}
|
||||
a.loop.IconSize = IconButtonSizeSmaller
|
||||
a.loop.SetToolTip(lang.L("Repeat"))
|
||||
a.autoplay.Highlighted = initialAutoplay
|
||||
//a.autoplay.IconSize = IconButtonSizeSmaller
|
||||
a.autoplay.SetToolTip(lang.L("Autoplay"))
|
||||
a.autoplay.OnTapped = func() {
|
||||
a.SetAutoplay(!a.autoplay.Highlighted)
|
||||
if a.OnChangeAutoplay != nil {
|
||||
a.OnChangeAutoplay(a.autoplay.Highlighted)
|
||||
}
|
||||
}
|
||||
a.SetLoopMode(initialLoopMode)
|
||||
a.showQueue.IconSize = IconButtonSizeSmaller
|
||||
a.showQueue.SetToolTip(lang.L("Show play queue"))
|
||||
@@ -43,7 +56,7 @@ func NewAuxControls(initialVolume int, initialLoopMode backend.LoopMode) *AuxCon
|
||||
a.VolumeControl,
|
||||
container.New(
|
||||
layout.NewCustomPaddedHBoxLayout(theme.Padding()*1.5),
|
||||
layout.NewSpacer(), a.loop, a.showQueue, util.NewHSpace(5)),
|
||||
layout.NewSpacer(), a.autoplay, a.loop, a.showQueue, util.NewHSpace(5)),
|
||||
layout.NewSpacer(),
|
||||
),
|
||||
)
|
||||
@@ -73,6 +86,14 @@ func (a *AuxControls) SetLoopMode(mode backend.LoopMode) {
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AuxControls) SetAutoplay(autoplay bool) {
|
||||
if autoplay == a.autoplay.Highlighted {
|
||||
return
|
||||
}
|
||||
a.autoplay.Highlighted = autoplay
|
||||
a.autoplay.Refresh()
|
||||
}
|
||||
|
||||
func (a *AuxControls) OnShowPlayQueue(f func()) {
|
||||
a.showQueue.OnTapped = f
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user