Playback: play the current track when toggling/restarting from idle

After a restart the queue is restored with the right current track, but
playback does not autostart -- the audio engine has nothing loaded and
the play state is Stopped. Space (TogglePlay) called unpause() and r
(RestartTrack) called restart(), both of which error out with "not
playing" because no source is loaded; only switching to the queue and
pressing Enter (SetCurrent) actually started anything.

Make the resume-style controls load the current queue track when the
player is idle: TogglePlay now plays the current track on any non-
playing/paused state, and RestartTrack starts the current track when
nothing is loaded (and still restarts the loaded one otherwise). Both
route through the same play() path SetCurrent uses, and are a no-op when
the queue is empty. Paused/Playing behaviour is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Test User 2026-07-23 22:52:32 +02:00
parent 28a4c155d0
commit 4e69261e02
1 changed files with 48 additions and 7 deletions

View File

@ -331,14 +331,27 @@ impl Playback {
*state *state
}; };
debug!(?state, "toggling play"); debug!(?state, "toggling play");
if state == PlayState::Playing { match state {
PlayState::Playing => {
if let Err(err) = self.player.pause().await { if let Err(err) = self.player.pause().await {
warn!("pause failed: {err:?}"); warn!("pause failed: {err:?}");
} }
} else if let Err(err) = self.player.unpause().await { }
PlayState::Paused => {
if let Err(err) = self.player.unpause().await {
warn!("unpause failed: {err:?}"); warn!("unpause failed: {err:?}");
} }
} }
// Stopped/idle: nothing is loaded in the player (e.g. right
// after a restart restored the queue without autoplay), so
// there is nothing to unpause. Load and play the current
// queue track, exactly as `SetCurrent` does — a no-op when
// the queue is empty.
_ => {
self.play(self.current_track()).await;
}
}
}
PlaybackCommand::Stop => { PlaybackCommand::Stop => {
debug!("stopping playback"); debug!("stopping playback");
@ -410,9 +423,24 @@ impl Playback {
PlaybackCommand::RestartTrack => { PlaybackCommand::RestartTrack => {
debug!("restarting current track"); debug!("restarting current track");
let state = {
let Ok(state) = self.state.lock() else {
error!("play state lock poisoned");
return;
};
*state
};
// With a track loaded (playing or paused) restart it from the
// top. When nothing is loaded (queue restored without autoplay)
// there is no source to restart, so start the current queue
// track instead.
if matches!(state, PlayState::Playing | PlayState::Paused) {
if let Err(err) = self.player.restart().await { if let Err(err) = self.player.restart().await {
warn!("restart failed: {err:?}"); warn!("restart failed: {err:?}");
} }
} else {
self.play(self.current_track()).await;
}
} }
PlaybackCommand::VolumeChanged { volume } => { PlaybackCommand::VolumeChanged { volume } => {
@ -701,6 +729,19 @@ impl Playback {
} }
} }
/// The queue's current track, or `None` when the queue is empty (or its
/// lock is poisoned). The resume-style controls (`TogglePlay`,
/// `RestartTrack`) use it to load what a restored/idle queue points at.
fn current_track(&self) -> Option<Track> {
match self.queue.lock() {
Ok(queue) => queue.current_track(),
Err(_) => {
error!("queue lock poisoned");
None
}
}
}
/// Plays the given track if there is one, otherwise stops the player. /// Plays the given track if there is one, otherwise stops the player.
#[instrument(skip(self, track), fields(track = track.as_ref().map(|t| t.path.as_str())))] #[instrument(skip(self, track), fields(track = track.as_ref().map(|t| t.path.as_str())))]
async fn play_or_stop(&self, track: Option<Track>) { async fn play_or_stop(&self, track: Option<Track>) {