use std::{collections::HashMap, path::PathBuf, str::FromStr, sync::{mpsc::{self, Receiver, Sender}, Arc, Mutex, MutexGuard}, time::Duration}; use anyhow::anyhow; use camino::{Utf8Path, Utf8PathBuf}; use downloader::song::SongStatus; use xmpd_manifest::song::Song; pub mod downloader; type Result = anyhow::Result; lazy_static::lazy_static!( static ref CACHE: Arc> = Arc::new(Mutex::new(Cache::default())); ); #[derive(Debug, Default)] pub struct Cache { cache_dir: camino::Utf8PathBuf, song_cache: HashMap, icon_cache: HashMap, song_queue: Vec<(uuid::Uuid, Song)>, icon_queue: Vec<(uuid::Uuid, Song)>, //meta_queue: Vec<(uuid::Uuid, Song)> // TODO: Add Icon, metadata cache } #[derive(Debug, Clone)] pub enum DlStatus { Done(Option), Downloading, Error(&'static str, usize, String), } #[derive(Debug, Clone)] pub enum Message { DownloadDone(uuid::Uuid), Error(&'static str, usize, String) } impl Cache { pub fn get() -> crate::Result> { match CACHE.lock() { Ok(l) => Ok(l), Err(e) => Err(anyhow::anyhow!(format!("{e:?}"))), } } fn check_if_tool_exists(&self, tool_path: &Utf8Path) -> crate::Result<()> { if std::fs::metadata(tool_path).is_ok() { return Ok(()); } if let Ok(path) = std::env::var("PATH") { for p in path.split(":") { let p_str = Utf8PathBuf::from(p).join(tool_path); if std::fs::metadata(p_str).is_ok() { return Ok(()); } } } anyhow::bail!("Tool {} was not found", tool_path) } pub fn init(&mut self) -> Result> { // Check for missing tooling let tooling = xmpd_settings::Settings::get()?.tooling.clone(); self.check_if_tool_exists(&tooling.ytdlp_path)?; self.check_if_tool_exists(&tooling.spotdl_path)?; self.check_if_tool_exists(&tooling.ffmpeg_path)?; let (internal_tx, cache_rx) = mpsc::channel::(); // let (internal_rx, cache_tx) = mpsc::channel::(); start_cache_mv_thread(internal_tx); self.cache_dir = xmpd_settings::Settings::get()?.cache_settings.cache_path.clone(); std::fs::create_dir_all(&self.cache_dir)?; { // Get cached songs let mut song_cache_dir = self.cache_dir.clone(); song_cache_dir.push("songs"); std::fs::create_dir_all(&song_cache_dir)?; for file in song_cache_dir.read_dir_utf8().map_err(|e| anyhow!("failed to read cache dir: {e}"))? { if let Ok(file) = file { if !file.file_type()?.is_file() { log::warn!("Non song file in: {}", file.path()); continue; } let file_path = file.path(); let file2 = file_path.with_extension(""); if let Some(file_name) = file2.file_name() { let id = uuid::Uuid::from_str(file_name)?; log::debug!("Found song {id}"); // TODO: Check if id is in manifest self.song_cache.insert(id, DlStatus::Done(Some(file_path.into()))); } } } } { // Get cached icons } { // Get Cached meta } Ok(cache_rx) } pub fn download_song_to_cache(&mut self, sid: uuid::Uuid, song: Song) { let song_format = xmpd_settings::Settings::get().unwrap().tooling.song_format.clone(); let mut p = self.cache_dir.clone(); p.push("songs"); p.push(format!("{sid}.{song_format}")); if !p.exists() { log::info!("p: {p:?}"); self.song_queue.push((sid, song)); self.song_cache.insert(sid, DlStatus::Downloading); } } pub fn download_icon_to_cache(&mut self, sid: uuid::Uuid, song: Song) { self.icon_queue.push((sid, song)); self.icon_cache.insert(sid, DlStatus::Downloading); } pub fn get_cached_song_status(&mut self, sid: &uuid::Uuid) -> Option { let original = self.song_cache.get(sid)?.clone(); Some(original) } pub fn get_cached_icon_status(&mut self, sid: &uuid::Uuid) -> Option { let original = self.icon_cache.get(sid)?.clone(); Some(original) } } macro_rules! he { ($tx:expr_2021, $val:expr_2021) => { match $val { Ok(v) => v, Err(e) => { let _ = $tx.send(Message::Error(std::file!(), std::line!() as usize, format!("{e:?}"))); continue; } } }; } fn start_cache_mv_thread(tx: Sender) { std::thread::spawn(move || { loop { { std::thread::sleep(Duration::from_millis(500)); let song_format = he!(tx, xmpd_settings::Settings::get()).tooling.song_format.clone(); let mut done_jobs = Vec::new(); let mut dlc = he!(tx, downloader::song::SongCacheDl::get()); for (sid, status) in &dlc.jobs { if *status == SongStatus::Done { let mut cache = he!(tx, CACHE.lock()); let mut song_p = he!(tx, xmpd_settings::Settings::get()).cache_settings.cache_path.clone(); song_p.push("songs"); song_p.push(sid.clone().to_string()); let song_p = song_p.with_extension(&song_format); if song_p.exists() { let _ = tx.send(Message::DownloadDone(sid.clone())); cache.song_cache.insert(sid.clone(), DlStatus::Done(Some(song_p.into()))); done_jobs.push(sid.clone()); } } else if let SongStatus::Failed(e) = status { let mut cache = he!(tx, CACHE.lock()); let _ = tx.send(Message::Error(std::file!(), std::line!() as usize, format!("Failed to download song {sid}: {e}"))); cache.song_cache.insert(sid.clone(), DlStatus::Error(std::file!(), std::line!() as usize, format!("Failed to download song {sid}: {e}"))); done_jobs.push(sid.clone()); } } for sid in done_jobs { dlc.jobs.remove(&sid); } { let mut done_jobs = Vec::new(); let mut dlc = he!(tx, downloader::icon::IconCacheDl::get()); for (sid, status) in &dlc.jobs { if let DlStatus::Done(path) = status { let mut cache = he!(tx, CACHE.lock()); cache.icon_cache.insert(sid.clone(), DlStatus::Done(path.clone())); done_jobs.push(sid.clone()); } } for sid in done_jobs { dlc.jobs.remove(&sid); } } } { let mut cache = he!(tx, Cache::get()); { let mut dlc = he!(tx, downloader::song::SongCacheDl::get()); if !dlc.is_job_list_full() { if let Some((sid, song)) = cache.song_queue.pop() { he!(tx, dlc.download(sid, song)); } } } { let mut icnc = he!(tx, downloader::icon::IconCacheDl::get()); if !icnc.is_job_list_full() { if let Some((sid, song)) = cache.icon_queue.pop() { log::debug!("Downloading {sid:?}"); he!(tx, icnc.download(sid, song)); } } } } } }); }