Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resolve #539 #599

Merged
merged 3 commits into from
Feb 20, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 92 additions & 21 deletions src/app/state/playback_state.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::borrow::Cow;
use std::time::Instant;

use crate::app::models::{SongBatch, SongDescription, SongListModel, SongListModelPending};
use crate::app::state::{AppAction, AppEvent, UpdatableState};
Expand All @@ -8,7 +9,8 @@ use crate::app::{BatchQuery, LazyRandomIndex, SongsSource};
pub struct PlaybackState {
index: LazyRandomIndex,
songs: SongListModel,
position: Option<usize>,
list_position: Option<usize>,
seek_position: PositionMillis,
source: Option<SongsSource>,
repeat: RepeatMode,
is_playing: bool,
Expand All @@ -21,7 +23,7 @@ impl PlaybackState {
}

pub fn is_playing(&self) -> bool {
self.is_playing && self.position.is_some()
self.is_playing && self.list_position.is_some()
}

pub fn is_shuffled(&self) -> bool {
Expand Down Expand Up @@ -58,11 +60,11 @@ impl PlaybackState {
}

pub fn current_song_id(&self) -> Option<String> {
Some(self.index(self.position?)?.id)
Some(self.index(self.list_position?)?.id)
}

pub fn current_song(&self) -> Option<SongDescription> {
self.index(self.position?)
self.index(self.list_position?)
}

fn next_id(&self) -> Option<String> {
Expand All @@ -73,7 +75,7 @@ impl PlaybackState {
fn clear(&mut self, source: Option<SongsSource>) -> SongListModelPending {
self.source = source;
self.index = Default::default();
self.position = None;
self.list_position = None;
self.songs.clear()
}

Expand Down Expand Up @@ -103,14 +105,14 @@ impl PlaybackState {
pub fn dequeue(&mut self, ids: &[String]) {
let current_id = self.current_song_id();
self.songs.remove(ids).commit();
self.position = current_id.and_then(|id| self.songs.find_index(&id));
self.list_position = current_id.and_then(|id| self.songs.find_index(&id));
self.index.shrink(self.songs.len());
}

fn swap_pos(&mut self, index: usize, other_index: usize) {
let len = self.songs.len();
self.position = self
.position
self.list_position = self
.list_position
.map(|position| match position {
i if i == index => other_index,
i if i == other_index => index,
Expand Down Expand Up @@ -154,24 +156,29 @@ impl PlaybackState {
}

fn stop(&mut self) {
self.position = None;
self.list_position = None;
self.is_playing = false;
self.seek_position.set(0, false);
}

fn play_index(&mut self, index: usize) -> Option<String> {
self.is_playing = true;
self.position.replace(index);
self.list_position.replace(index);
self.seek_position.set(0, true);
self.index.next_until(index + 1);
self.current_song_id()
}

fn play_next(&mut self) -> Option<String> {
self.next_index().and_then(move |i| self.play_index(i))
self.next_index().and_then(move |i| {
self.seek_position.set(0, true);
self.play_index(i)
})
}

pub fn next_index(&self) -> Option<usize> {
let len = self.songs.len();
self.position.and_then(|p| match self.repeat {
self.list_position.and_then(|p| match self.repeat {
RepeatMode::Song => Some(p),
RepeatMode::Playlist if len != 0 => Some((p + 1) % len),
RepeatMode::None => Some(p + 1).filter(|&i| i < len),
Expand All @@ -180,12 +187,23 @@ impl PlaybackState {
}

fn play_prev(&mut self) -> Option<String> {
self.prev_index().and_then(move |i| self.play_index(i))
self.prev_index().and_then(move |i| {
// Only jump to the previous track if we aren't more than 2 seconds (2,000 ms) into the current track.
// Otherwise, seek to the start of the current track.
// (This replicates the behavior of official Spotify clients.)
if self.seek_position.current() <= 2000 {
self.seek_position.set(0, true);
self.play_index(i)
} else {
self.seek_position.set(0, true);
None
}
})
}

pub fn prev_index(&self) -> Option<usize> {
let len = self.songs.len();
self.position.and_then(|p| match self.repeat {
self.list_position.and_then(|p| match self.repeat {
RepeatMode::Song => Some(p),
RepeatMode::Playlist if len != 0 => Some((if p == 0 { len } else { p }) - 1),
RepeatMode::None => Some(p).filter(|&i| i > 0).map(|i| i - 1),
Expand All @@ -194,8 +212,14 @@ impl PlaybackState {
}

fn toggle_play(&mut self) -> Option<bool> {
if self.position.is_some() {
if self.list_position.is_some() {
self.is_playing = !self.is_playing;

match self.is_playing {
false => self.seek_position.pause(),
true => self.seek_position.resume(),
};

Some(self.is_playing)
} else {
None
Expand All @@ -204,7 +228,7 @@ impl PlaybackState {

fn toggle_shuffle(&mut self) {
self.is_shuffled = !self.is_shuffled;
let old = self.position.replace(0).unwrap_or(0);
let old = self.list_position.replace(0).unwrap_or(0);
self.index.reset_picking_first(old);
}
}
Expand All @@ -214,7 +238,8 @@ impl Default for PlaybackState {
Self {
index: LazyRandomIndex::default(),
songs: SongListModel::new(50),
position: None,
list_position: None,
seek_position: PositionMillis::new(1.0),
source: None,
repeat: RepeatMode::None,
is_playing: false,
Expand Down Expand Up @@ -352,7 +377,7 @@ impl UpdatableState for PlaybackState {
Some(PlaybackEvent::PlaybackResumed),
])
} else {
vec![]
make_events(vec![Some(PlaybackEvent::TrackSeeked(0))])
}
}
PlaybackAction::Load(id) => {
Expand Down Expand Up @@ -399,14 +424,60 @@ impl UpdatableState for PlaybackState {
self.dequeue(&[id]);
vec![PlaybackEvent::PlaylistChanged]
}
PlaybackAction::Seek(pos) => vec![PlaybackEvent::TrackSeeked(pos)],
PlaybackAction::SyncSeek(pos) => vec![PlaybackEvent::SeekSynced(pos)],
PlaybackAction::Seek(pos) => {
self.seek_position.set(pos as u64 * 1000, true);
vec![PlaybackEvent::TrackSeeked(pos)]
}
PlaybackAction::SyncSeek(pos) => {
self.seek_position.set(pos as u64 * 1000, true);
vec![PlaybackEvent::SeekSynced(pos)]
}
PlaybackAction::SetVolume(volume) => vec![PlaybackEvent::VolumeSet(volume)],
_ => vec![],
}
}
}

#[derive(Debug)]
struct PositionMillis {
last_known_position: u64,
last_resume_instant: Option<Instant>,
rate: f32,
}

impl PositionMillis {
fn new(rate: f32) -> Self {
Self {
last_known_position: 0,
last_resume_instant: None,
rate,
}
}

fn current(&self) -> u64 {
let current_progress = self.last_resume_instant.map(|ri| {
let elapsed = ri.elapsed().as_millis() as f32;
let real_elapsed = self.rate * elapsed;
real_elapsed.ceil() as u64
});
self.last_known_position + current_progress.unwrap_or(0)
}

fn set(&mut self, position: u64, playing: bool) {
self.last_known_position = position;
self.last_resume_instant = if playing { Some(Instant::now()) } else { None }
}

fn pause(&mut self) {
self.last_known_position = self.current();
self.last_resume_instant = None;
}

fn resume(&mut self) {
self.last_resume_instant = Some(Instant::now());
}
}

#[cfg(test)]
mod tests {

Expand All @@ -431,7 +502,7 @@ mod tests {

impl PlaybackState {
fn current_position(&self) -> Option<usize> {
self.position
self.list_position
}

fn prev_id(&self) -> Option<String> {
Expand Down