use std::{collections::HashMap, path::PathBuf, process::Stdio}; use lazy_static::lazy_static; use log::Level; use tokio::sync::{Mutex, RwLock}; use crate::{config::ConfigWrapper, manifest::{song::{Song, SongType}, Format, Manifest}}; #[allow(dead_code)] #[derive(Debug, Clone)] struct Proc { url: String, path: String, finished: bool } lazy_static!( static ref PROCESSES: Mutex>> = Mutex::new(RwLock::new(HashMap::new())); ); #[derive(Debug, Default)] pub struct Downloader { count: usize, nb_initial_song_count: usize, nb_cache: Vec<(String, String, Song, Format)> } impl Downloader { pub fn new() -> Self { Self { ..Default::default() } } pub fn get_initial_song_count_nb(&self) -> usize { self.nb_initial_song_count } pub fn get_songs_left_nb(&self) -> usize { self.nb_cache.len() + crate::process_manager::proc_count() } pub fn download_song_nb(&mut self, cfg: &ConfigWrapper, pname: &String, sname: &String, song: &Song, format: &Format) -> anyhow::Result<()> { self.nb_cache.push((pname.clone(), sname.clone(), song.clone(), format.clone())); self.nb_initial_song_count += 1; self.download_all_nb_poll(cfg)?; Ok(()) } pub fn download_all_nb(&mut self, manifest: &Manifest, cfg: &ConfigWrapper) -> anyhow::Result> { for (pname, playlist) in manifest.get_playlists() { for (sname, song) in playlist.get_songs() { self.nb_cache.push((pname.clone(), sname.clone(), song.clone(), manifest.get_format().clone())); } } self.nb_initial_song_count = self.nb_cache.len(); self.download_all_nb_poll(cfg) } pub fn download_all_nb_poll(&mut self, cfg: &ConfigWrapper) -> anyhow::Result> { if !crate::process_manager::is_proc_queue_full(10) { if let Some((pname, sname, song, format)) = self.nb_cache.pop() { self.download_song(cfg, &sname, &song, &pname, &format)?; } } if self.get_songs_left_nb() == 0 { self.nb_initial_song_count = 0; } if crate::process_manager::proc_count() == 0 && self.nb_cache.is_empty() { Ok(None) } else { Ok(Some(crate::process_manager::purge_done_procs())) } } pub async fn download_all(&mut self, manifest: &Manifest, cfg: &ConfigWrapper) -> anyhow::Result { let format = manifest.get_format(); for (name, playlist) in manifest.get_playlists() { for (song_name, song) in playlist.get_songs() { self.download_song(cfg, song_name, song, &name, format)?; self.count += crate::process_manager::wait_for_procs_untill(10)?; } } self.count += crate::process_manager::wait_for_procs_untill(0)?; Ok(self.count) } #[allow(dead_code)] pub fn download_playlist(&mut self, cfg: &ConfigWrapper, url: &String, pname: &String, format: &Format) -> anyhow::Result { self.download_playlist_nb(cfg, url, pname, format)?; let mut count = 0; while let Some(c) = self.download_all_nb_poll(cfg)? { count += c; } Ok(count) } pub fn download_playlist_nb(&mut self, cfg: &ConfigWrapper, url: &String, pname: &String, format: &Format) -> anyhow::Result> { log::warn!("This automatically assumes its a youtube link as it is currently the only supported playlist source"); let mut cmd = tokio::process::Command::new(&cfg.cfg.ytdlp.path); cmd.args([ "--flat-playlist", "--simulate", "-O", "%(url)s|%(title)s", url.as_str() ]); cmd .stderr(Stdio::null()) .stdout(Stdio::piped()); let ftr = cmd.output(); let mut ret = HashMap::new(); let out = futures::executor::block_on(ftr)?.stdout; let out = String::from_utf8(out)?; for line in out.lines() { let mut split_text = line.split("|").collect::>(); let url = split_text.swap_remove(0).to_string(); let sname = split_text.join("|"); let song = Song::from_url_str(url)?.set_type(SongType::Youtube).clone(); self.nb_cache.push((pname.clone(), sname.clone(), song.clone(), format.clone())); ret.insert(sname, song.clone()); } self.nb_initial_song_count += out.lines().count(); self.download_all_nb_poll(cfg)?; Ok(ret) } pub fn download_song(&mut self, cfg: &ConfigWrapper, name: &String, song: &Song, playlist: &String, format: &Format) -> anyhow::Result<()> { let dl_dir = format!("{}/{playlist}", cfg.cli.output); let dl_file = format!("{dl_dir}/{}.{}", name, &format); log::debug!("Checking: {dl_file}"); if PathBuf::from(&dl_file).exists() { log::debug!("File {dl_file} exists, skipping"); return Ok(()) } log::debug!("File {dl_file} doesnt exist, downloading"); let mut cmd = match song.get_type() { &SongType::Youtube => { log::debug!("Song {} is from yotube", song.get_url_str()); let mut cmd = tokio::process::Command::new(&cfg.cfg.ytdlp.path); cmd.args([ "-x", "--audio-format", &format.to_string(), "-o", dl_file.as_str(), song.get_url_str().as_str() ]); cmd } SongType::Spotify => { let mut cmd = tokio::process::Command::new(&cfg.cfg.spotdl.path); cmd.args([ "--format", &format.to_string(), "--output", dl_dir.as_str(), song.get_url_str().as_str() ]); cmd } url => { log::error!("Unknown or unsupported hostname '{:?}'", url); return Ok(()); } }; if log::max_level() < Level::Debug { cmd.stdout(Stdio::null()).stderr(Stdio::null()); }; crate::process_manager::add_proc(cmd, format!("Downloaded {dl_file}"))?; Ok(()) } }