use crate::*;
pub(crate) struct NowPlaying {
pub(crate) uri: String,
pub(crate) title: String,
pub(crate) artist: String,
pub(crate) album: String,
pub(crate) duration_ms: u32,
pub(crate) position_ms: u32,
pub(crate) position_at: Instant,
pub(crate) is_playing: bool,
pub(crate) cover: Option<Cover>,
}
pub(crate) struct TrackMeta {
pub(crate) uri: String,
pub(crate) title: String,
pub(crate) artist: String,
pub(crate) album: String,
pub(crate) duration_ms: u32,
pub(crate) image: TrackImage,
pub(crate) theme: Option<Theme>,
}
pub(crate) struct TrackImage {
pub(crate) url: Option<String>,
pub(crate) image: Option<image::DynamicImage>,
}
#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
pub(crate) enum PlaySource {
#[default]
None,
Context(String), Radio(String), Liked,
}
pub(crate) struct PlaybackState {
pub(crate) now: Option<NowPlaying>,
pub(crate) seek_target: Option<u32>,
pub(crate) seek_last_step: Instant,
pub(crate) seek_last_input: Instant,
}
pub(crate) const SEEK_STEP_MS: i64 = 5_000;
pub(crate) const SEEK_REPEAT: Duration = Duration::from_millis(200);
pub(crate) const SEEK_SETTLE: Duration = Duration::from_millis(250);
pub(crate) fn should_apply_engine_position(from_engine: bool, seek_target: Option<u32>) -> bool {
!(from_engine && seek_target.is_some())
}
pub(crate) fn scrub_target(from_ms: u32, duration_ms: u32, delta_ms: i64) -> u32 {
(from_ms as i64 + delta_ms).clamp(0, duration_ms as i64) as u32
}
impl PlaybackState {
pub(crate) fn position_ms(&self) -> u32 {
match &self.now {
Some(n) if n.is_playing => {
(n.position_ms + n.position_at.elapsed().as_millis() as u32).min(n.duration_ms)
}
Some(n) => n.position_ms.min(n.duration_ms),
None => 0,
}
}
pub(crate) fn set_local_position(&mut self, position_ms: u32, from_engine: bool) {
if !should_apply_engine_position(from_engine, self.seek_target) {
return;
}
if let Some(n) = self.now.as_mut() {
n.position_ms = position_ms.min(n.duration_ms);
n.position_at = Instant::now();
}
}
pub(crate) fn seek_to(&mut self, engine: &Engine, position_ms: u32) {
let Some(dur) = self.now.as_ref().map(|n| n.duration_ms) else {
return;
};
let new = position_ms.min(dur);
let _ = engine.seek(new);
self.set_local_position(new, false);
}
pub(crate) fn seek_step(&mut self, delta_ms: i64) {
let now = Instant::now();
if self.seek_target.is_some() && now.duration_since(self.seek_last_step) < SEEK_REPEAT {
self.seek_last_input = now;
return;
}
let Some(dur) = self.now.as_ref().map(|n| n.duration_ms) else {
return;
};
let from = self.seek_target.unwrap_or_else(|| self.position_ms());
let target = scrub_target(from, dur, delta_ms);
self.seek_target = Some(target);
self.seek_last_step = now;
self.seek_last_input = now;
self.set_local_position(target, false);
}
pub(crate) fn flush_seek(&mut self, engine: &Engine, now: Instant) {
if now.duration_since(self.seek_last_input) < SEEK_SETTLE {
return;
}
if let Some(target) = self.seek_target.take() {
self.seek_to(engine, target);
}
}
}