213 lines
8.1 KiB
Rust
213 lines
8.1 KiB
Rust
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<T> = anyhow::Result<T>;
|
|
|
|
lazy_static::lazy_static!(
|
|
static ref CACHE: Arc<Mutex<Cache>> = Arc::new(Mutex::new(Cache::default()));
|
|
);
|
|
|
|
#[derive(Debug, Default)]
|
|
pub struct Cache {
|
|
cache_dir: camino::Utf8PathBuf,
|
|
song_cache: HashMap<uuid::Uuid, DlStatus>,
|
|
icon_cache: HashMap<uuid::Uuid, DlStatus>,
|
|
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<PathBuf>),
|
|
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<MutexGuard<'static, Self>> {
|
|
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<Receiver<Message>> {
|
|
// 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::<Message>();
|
|
// let (internal_rx, cache_tx) = mpsc::channel::<Message>();
|
|
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<DlStatus> {
|
|
let original = self.song_cache.get(sid)?.clone();
|
|
Some(original)
|
|
}
|
|
pub fn get_cached_icon_status(&mut self, sid: &uuid::Uuid) -> Option<DlStatus> {
|
|
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<Message>) {
|
|
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));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
|