use std::fs; use directories::ProjectDirs; use expanduser::expanduser; use serde::{Deserialize, Serialize}; use crate::{albums, config::Config, intuition::Intuition, trajectory::Trajectory}; fn default_state_path() -> String { let name = "mood.json.zstd"; ProjectDirs::from("qualifier", "organisation", "mood") .and_then(|pd| { pd.config_dir() .join(name) .to_str() .map(std::string::ToString::to_string) }) .unwrap_or(format!("~/.config/mood/{name}")) } #[derive(Serialize, Deserialize, Default)] pub struct State { trajectory: Trajectory, intuition: Intuition, } impl State { pub fn next(&mut self, config: &Config) -> Option<(String, f32)> { let albums = albums::load(config); if albums.is_empty() { println!("Found no albums in {}", config.music_root); return None; } let l = albums.len(); println!("Found {} album{}", l, if l > 1 { "s" } else { "" }); let learning = self.trajectory.step(config.skip_window_secs); if let Some(ref learning) = learning { self.intuition.learn(learning); } if let Some((new_album, prob)) = self.intuition.sample( self.trajectory.slice(), &learning.as_ref().into(), &albums, config, ) { self.trajectory.log(&new_album); Some((new_album, prob)) } else { None } } } impl State { pub fn try_load() -> Result { expanduser(default_state_path()) .map_err(|e| e.to_string()) .and_then(|p| fs::read(p).map_err(|e| e.to_string())) .and_then(|contents| zstd::decode_all(&contents[..]).map_err(|e| e.to_string())) .and_then(|decompressed| { std::str::from_utf8(&decompressed) .map(std::string::ToString::to_string) .map_err(|e| e.to_string()) }) .and_then(|utf8| serde_json::from_str(&utf8).map_err(|e| e.to_string())) } pub fn try_save(&self) -> Result<(), String> { let str_path = default_state_path(); let path = expanduser(&str_path).map_err(|e| e.to_string())?; _ = path.parent().map(std::fs::create_dir_all); let serialised = serde_json::to_string_pretty(self) .map_err(|e| format!("Failed to serialize state: {e}"))?; let compressed = zstd::encode_all(serialised.into_bytes().as_slice(), 3) .map_err(|e| format!("Failed to compress state: {e}"))?; fs::write(&path, compressed) .map_err(|e| format!("Failed to write state file at {str_path}: {e}"))?; Ok(()) } } impl State { pub fn dump_intuition(&self) -> Result { serde_json::to_string_pretty(&self.intuition).map_err(|e| e.to_string()) } pub fn dump_trajectory(&self) -> Result { serde_json::to_string_pretty(&self.trajectory).map_err(|e| e.to_string()) } }