Merge pull request #681 from dweymouth/feature/stop-after-current

Add stop after current track option
This commit is contained in:
Drew Weymouth
2025-08-08 08:45:39 -07:00
committed by GitHub
10 changed files with 75 additions and 24 deletions
+4
View File
@@ -548,6 +548,10 @@ func (a *App) checkFlagsAndSendIPCMsg(cli *ipc.Client) error {
return cli.SeekBackOrPrevious() return cli.SeekBackOrPrevious()
case *FlagNext: case *FlagNext:
return cli.SeekNext() return cli.SeekNext()
case *FlagStop:
return cli.Stop()
case *FlagStopAfterCurrent:
return cli.StopAfterCurrent()
case *FlagShow: case *FlagShow:
return cli.Show() return cli.Show()
case VolumeCLIArg >= 0: case VolumeCLIArg >= 0:
+2
View File
@@ -17,6 +17,8 @@ var (
FlagPlayPause = flag.Bool("play-pause", false, "toggle play/pause state") FlagPlayPause = flag.Bool("play-pause", false, "toggle play/pause state")
FlagPrevious = flag.Bool("previous", false, "seek to previous track or beginning of current") FlagPrevious = flag.Bool("previous", false, "seek to previous track or beginning of current")
FlagNext = flag.Bool("next", false, "seek to next track") FlagNext = flag.Bool("next", false, "seek to next track")
FlagStop = flag.Bool("stop", false, "stop playback")
FlagStopAfterCurrent = flag.Bool("stop-after-current", false, "stop playback after current track")
FlagStartMinimized = flag.Bool("start-minimized", false, "start app minimized") FlagStartMinimized = flag.Bool("start-minimized", false, "start app minimized")
FlagShow = flag.Bool("show", false, "show minimized app") FlagShow = flag.Bool("show", false, "show minimized app")
FlagVersion = flag.Bool("version", false, "print app version and exit") FlagVersion = flag.Bool("version", false, "print app version and exit")
+1
View File
@@ -8,6 +8,7 @@ const (
PlayPausePath = "/transport/playpause" PlayPausePath = "/transport/playpause"
PausePath = "/transport/pause" PausePath = "/transport/pause"
StopPath = "/transport/stop" StopPath = "/transport/stop"
StopAfterCurrentPath = "/transport/stop-after-current"
PreviousPath = "/transport/previous" PreviousPath = "/transport/previous"
NextPath = "/transport/next" NextPath = "/transport/next"
TimePosPath = "/transport/timepos" // ?s=<seconds> TimePosPath = "/transport/timepos" // ?s=<seconds>
+8
View File
@@ -48,6 +48,14 @@ func (c *Client) PlayPause() error {
return c.sendRequest(PlayPausePath) return c.sendRequest(PlayPausePath)
} }
func (c *Client) Stop() error {
return c.sendRequest(StopPath)
}
func (c *Client) StopAfterCurrent() error {
return c.sendRequest(StopAfterCurrentPath)
}
func (c *Client) SeekNext() error { func (c *Client) SeekNext() error {
return c.sendRequest(NextPath) return c.sendRequest(NextPath)
} }
+4
View File
@@ -15,6 +15,7 @@ type PlaybackHandler interface {
Continue() Continue()
SeekBackOrPrevious() SeekBackOrPrevious()
SeekNext() SeekNext()
SetStopAfterCurrent(bool)
SeekSeconds(float64) SeekSeconds(float64)
SeekBySeconds(float64) SeekBySeconds(float64)
Volume() int Volume() int
@@ -68,6 +69,9 @@ func (s *serverImpl) createHandler() http.Handler {
m.HandleFunc(PausePath, s.makeSimpleEndpointHandler(s.pbHandler.Pause)) m.HandleFunc(PausePath, s.makeSimpleEndpointHandler(s.pbHandler.Pause))
m.HandleFunc(PlayPausePath, s.makeSimpleEndpointHandler(s.pbHandler.PlayPause)) m.HandleFunc(PlayPausePath, s.makeSimpleEndpointHandler(s.pbHandler.PlayPause))
m.HandleFunc(StopPath, s.makeSimpleEndpointHandler(s.pbHandler.Stop)) m.HandleFunc(StopPath, s.makeSimpleEndpointHandler(s.pbHandler.Stop))
m.HandleFunc(StopAfterCurrentPath, s.makeSimpleEndpointHandler(func() {
s.pbHandler.SetStopAfterCurrent(true)
}))
m.HandleFunc(PreviousPath, s.makeSimpleEndpointHandler(s.pbHandler.SeekBackOrPrevious)) m.HandleFunc(PreviousPath, s.makeSimpleEndpointHandler(s.pbHandler.SeekBackOrPrevious))
m.HandleFunc(NextPath, s.makeSimpleEndpointHandler(s.pbHandler.SeekNext)) m.HandleFunc(NextPath, s.makeSimpleEndpointHandler(s.pbHandler.SeekNext))
m.HandleFunc(TimePosPath, s.makeFloatEndpointHandler("s", s.pbHandler.SeekSeconds)) m.HandleFunc(TimePosPath, s.makeFloatEndpointHandler("s", s.pbHandler.SeekSeconds))
+13 -1
View File
@@ -65,6 +65,8 @@ type playbackEngine struct {
isRadio bool isRadio bool
loopMode LoopMode loopMode LoopMode
stopAfterCurrent bool // flag to stop playback after current track ends
// flags for handleOnTrackChange / handleOnStopped callbacks - reset to false in the callbacks // flags for handleOnTrackChange / handleOnStopped callbacks - reset to false in the callbacks
wasStopped bool // true iff player was stopped before handleOnTrackChange invocation wasStopped bool // true iff player was stopped before handleOnTrackChange invocation
alreadyScrobbled bool // true iff the previously-playing track was already scrobbled alreadyScrobbled bool // true iff the previously-playing track was already scrobbled
@@ -334,6 +336,15 @@ func (p *playbackEngine) Stop() error {
return p.player.Stop(false) return p.player.Stop(false)
} }
func (p *playbackEngine) SetStopAfterCurrent(stopAfterCurrent bool) {
p.stopAfterCurrent = stopAfterCurrent
if p.stopAfterCurrent {
p.setNextTrack(-1) // clear next playing track from internal player, if any
} else if p.loopMode != LoopNone || p.nowPlayingIdx < len(p.playQueue)-1 {
p.needToSetNextTrack = true // need to restore next track to internal player queue
}
}
func (p *playbackEngine) Pause() error { func (p *playbackEngine) Pause() error {
return p.player.Pause() return p.player.Pause()
} }
@@ -634,6 +645,7 @@ func (p *playbackEngine) handleOnStopped() {
p.alreadyScrobbled = false p.alreadyScrobbled = false
p.wasStopped = true p.wasStopped = true
p.nowPlayingIdx = -1 p.nowPlayingIdx = -1
p.stopAfterCurrent = false
} }
// to be invoked as soon as the next item in the queue that should play changes // to be invoked as soon as the next item in the queue that should play changes
@@ -855,7 +867,7 @@ func (p *playbackEngine) handleTimePosUpdate(seeked bool) {
meta = np.Metadata() meta = np.Metadata()
} }
isNearEnd := meta.Type != mediaprovider.MediaItemTypeRadioStation && s.TimePos > meta.Duration.Seconds()-10 isNearEnd := meta.Type != mediaprovider.MediaItemTypeRadioStation && s.TimePos > meta.Duration.Seconds()-10
if p.needToSetNextTrack && isNearEnd { if p.needToSetNextTrack && !p.stopAfterCurrent && isNearEnd {
p.needToSetNextTrack = false p.needToSetNextTrack = false
p.setNextTrack(p.nextPlayingIndex()) p.setNextTrack(p.nextPlayingIndex())
} }
+8
View File
@@ -669,6 +669,14 @@ func (p *PlaybackManager) PlayPause() {
} }
} }
func (p *PlaybackManager) SetStopAfterCurrent(stopAfterCurrent bool) {
p.engine.SetStopAfterCurrent(stopAfterCurrent)
}
func (p *PlaybackManager) IsStopAfterCurrent() bool {
return p.engine.stopAfterCurrent
}
func (p *PlaybackManager) enqueueAutoplayTracks() { func (p *PlaybackManager) enqueueAutoplayTracks() {
nowPlaying := p.NowPlaying() nowPlaying := p.NowPlaying()
if nowPlaying == nil { if nowPlaying == nil {
+3
View File
@@ -229,6 +229,9 @@ func (d *DLNAPlayer) SetNextFile(url string, meta mediaprovider.MediaItemMetadat
Title: meta.Name, Title: meta.Name,
Seekable: true, Seekable: true,
} }
} else {
// empty media item to signify erasing next track in device queue
media = &avtransport.MediaItem{}
} }
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
+1
View File
@@ -216,6 +216,7 @@
"Soundtrack": "Soundtrack", "Soundtrack": "Soundtrack",
"Spoken Word": "Spoken Word", "Spoken Word": "Spoken Word",
"Startup page": "Startup page", "Startup page": "Startup page",
"Stop after current track": "Stop after current track",
"Stopped": "Stopped", "Stopped": "Stopped",
"Success": "Success", "Success": "Success",
"Support the project": "Support the project", "Support the project": "Support the project",
+9 -1
View File
@@ -27,6 +27,7 @@ import (
"fyne.io/fyne/v2/container" "fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/lang" "fyne.io/fyne/v2/lang"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget" "fyne.io/fyne/v2/widget"
) )
@@ -57,6 +58,7 @@ type Controller struct {
popUpQueue *widget.PopUp popUpQueue *widget.PopUp
popUpQueueList *widgets.PlayQueueList popUpQueueList *widgets.PlayQueueList
stopAfterCurrent *widget.Check
popUpQueueLastUsed int64 popUpQueueLastUsed int64
escapablePopUp fyne.CanvasObject escapablePopUp fyne.CanvasObject
haveModal bool haveModal bool
@@ -195,7 +197,11 @@ func (m *Controller) ShowPopUpPlayQueue() {
title := widget.NewRichTextWithText(lang.L("Play Queue")) title := widget.NewRichTextWithText(lang.L("Play Queue"))
title.Segments[0].(*widget.TextSegment).Style.Alignment = fyne.TextAlignCenter title.Segments[0].(*widget.TextSegment).Style.Alignment = fyne.TextAlignCenter
title.Segments[0].(*widget.TextSegment).Style.TextStyle.Bold = true title.Segments[0].(*widget.TextSegment).Style.TextStyle.Bold = true
ctr := container.NewBorder(title, nil, nil, nil, m.stopAfterCurrent = widget.NewCheck(lang.L("Stop after current track"), func(b bool) {
m.App.PlaybackManager.SetStopAfterCurrent(b)
})
bottomRow := container.NewHBox(layout.NewSpacer(), m.stopAfterCurrent)
ctr := container.NewBorder(title, bottomRow, nil, nil,
container.NewPadded(m.popUpQueueList), container.NewPadded(m.popUpQueueList),
) )
m.popUpQueue = widget.NewPopUp(ctr, m.MainWindow.Canvas()) m.popUpQueue = widget.NewPopUp(ctr, m.MainWindow.Canvas())
@@ -224,6 +230,7 @@ func (m *Controller) ShowPopUpPlayQueue() {
fynetooltip.DestroyPopUpToolTipLayer(m.popUpQueue) fynetooltip.DestroyPopUpToolTipLayer(m.popUpQueue)
m.popUpQueue = nil m.popUpQueue = nil
m.popUpQueueList = nil m.popUpQueueList = nil
m.stopAfterCurrent = nil
m.popUpQueueLastUsed = 0 m.popUpQueueLastUsed = 0
t.Stop() t.Stop()
return return
@@ -252,6 +259,7 @@ func (m *Controller) ShowPopUpPlayQueue() {
)) ))
pop.Resize(size) pop.Resize(size)
popUpQueueList.ScrollToNowPlaying() // must come after resize popUpQueueList.ScrollToNowPlaying() // must come after resize
m.stopAfterCurrent.SetChecked(m.App.PlaybackManager.IsStopAfterCurrent())
pop.ShowAtPosition(fyne.NewPos( pop.ShowAtPosition(fyne.NewPos(
canvasSize.Width-size.Width-10, canvasSize.Width-size.Width-10,
canvasSize.Height-size.Height-100, canvasSize.Height-size.Height-100,