use super::NowPlaying;
use std::path::PathBuf;
use std::time::Duration;
const NONE_SENTINEL: &str = "—";
const STALE_AFTER: Duration = Duration::from_secs(10);
fn quick_path() -> Option<PathBuf> {
let home = std::env::var_os("HOME")?;
Some(PathBuf::from(home).join(".mixr").join("quick.txt"))
}
fn fresh_within(path: &std::path::Path, within: Duration) -> bool {
std::fs::metadata(path)
.and_then(|m| m.modified())
.map(|t| t.elapsed().map(|age| age <= within).unwrap_or(true))
.unwrap_or(false)
}
pub fn poll() -> Option<NowPlaying> {
let path = quick_path()?;
if !fresh_within(&path, STALE_AFTER) {
return None;
}
let text = std::fs::read_to_string(&path).ok()?;
Some(project(&text))
}
pub fn project(text: &str) -> NowPlaying {
let (mut track, mut bpm) = (String::new(), String::new());
let mut active: Option<bool> = None;
for line in text.lines() {
let Some((key, val)) = line.split_once('=') else {
continue;
};
let val = val.trim();
let val = if val == NONE_SENTINEL { "" } else { val };
match key.trim() {
"playing" => track = val.to_string(),
"playing_bpm" => bpm = val.to_string(),
"playing_active" => active = Some(val.eq_ignore_ascii_case("true")),
_ => {}
}
}
NowPlaying {
source: "mixr".to_string(),
playing: active.unwrap_or(!track.is_empty()),
track,
detail: bpm,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn idle_quick_txt_is_not_playing() {
let txt = "view=browse\nstate=Idle\nplaying=—\nplaying_bpm=—\n\
playing_time=0/0\nincoming=—\n";
let n = project(txt);
assert_eq!(n.source, "mixr");
assert!(!n.playing);
assert_eq!(n.track, "");
}
#[test]
fn a_playing_track_projects() {
let txt = "state=Playing\nplaying=Daft Punk - Aerodynamic\n\
playing_bpm=123\nplaying_time=1:04/3:38\n";
let n = project(txt);
assert!(n.playing);
assert_eq!(n.track, "Daft Punk - Aerodynamic");
assert_eq!(n.detail, "123"); }
#[test]
fn playing_active_false_means_not_playing() {
let txt = "state=Playing\nplaying=Prospa - Baby\nplaying_active=false\n\
playing_bpm=130\n";
let n = project(txt);
assert!(!n.playing);
assert_eq!(n.track, "Prospa - Baby"); }
#[test]
fn playing_active_true_means_playing() {
let txt = "state=Playing\nplaying=Prospa - Baby\nplaying_active=true\n";
let n = project(txt);
assert!(n.playing);
assert_eq!(n.track, "Prospa - Baby");
}
#[test]
fn projection_is_tolerant_of_junk() {
let n = project("\nunknown=whatever\ngarbage line\nplaying=A=B=C\n");
assert_eq!(n.track, "A=B=C");
assert!(n.playing);
}
#[test]
fn empty_input_is_idle() {
let n = project("");
assert!(!n.playing);
assert_eq!(n.track, "");
assert_eq!(n.source, "mixr");
}
#[test]
fn fresh_within_tracks_recency_and_absence() {
let p = std::env::temp_dir().join(format!("mnml-mixr-fresh-{}.txt", std::process::id()));
std::fs::write(&p, "x").unwrap();
assert!(fresh_within(&p, Duration::from_secs(60)));
assert!(!fresh_within(&p, Duration::from_secs(0)));
std::fs::remove_file(&p).ok();
assert!(!fresh_within(&p, Duration::from_secs(60)));
}
}