//! Background task that periodically refreshes YouTube cookies via headless Firefox. use std::path::{Path, PathBuf}; use std::process::Stdio; use std::sync::Arc; use std::time::Duration; use tokio::process::Command; use tokio::sync::RwLock; use crate::config::AppConfig; /// Spawn the cookie refresh background loop. /// /// This task runs forever, sleeping for `cookie_refresh_hours` between refreshes. /// It reads the current config on each iteration so changes take effect without restart. pub fn spawn(config: Arc>) { tokio::spawn(async move { loop { let (enabled, hours) = { let cfg = config.read().await; ( cfg.download.cookie_refresh_enabled, cfg.download.cookie_refresh_hours.max(1), ) }; // Sleep for the configured interval tokio::time::sleep(Duration::from_secs(u64::from(hours) * 3600)).await; if !enabled { continue; } let profile_dir = shanty_config::data_dir().join("firefox-profile"); let cookies_path = shanty_config::data_dir().join("cookies.txt"); if !profile_dir.exists() { tracing::warn!( "cookie refresh skipped: no Firefox profile at {}", profile_dir.display() ); continue; } tracing::info!("starting cookie refresh"); match run_refresh(&profile_dir, &cookies_path).await { Ok(msg) => tracing::info!("cookie refresh complete: {msg}"), Err(e) => tracing::error!("cookie refresh failed: {e}"), } } }); } async fn run_refresh(profile_dir: &Path, cookies_path: &Path) -> Result { let script = find_script()?; let output = Command::new("python3") .arg(&script) .args([ "refresh", &profile_dir.to_string_lossy(), &cookies_path.to_string_lossy(), ]) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .output() .await .map_err(|e| format!("failed to run cookie_manager.py: {e}"))?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(format!("cookie_manager.py failed: {stderr}")); } let stdout = String::from_utf8_lossy(&output.stdout).to_string(); // Check for error in JSON response if let Ok(v) = serde_json::from_str::(&stdout) && v.get("status").and_then(|s| s.as_str()) == Some("error") { let err = v.get("error").and_then(|e| e.as_str()).unwrap_or("unknown"); return Err(err.to_string()); } Ok(stdout) } fn find_script() -> Result { let candidates = [ std::env::current_exe() .ok() .and_then(|p| p.parent().map(|d| d.join("cookie_manager.py"))), Some(PathBuf::from("/usr/share/shanty/cookie_manager.py")), Some(PathBuf::from("/usr/local/share/shanty/cookie_manager.py")), Some(PathBuf::from("shanty-dl/scripts/cookie_manager.py")), ]; for candidate in candidates.into_iter().flatten() { if candidate.exists() { return Ok(candidate); } } Err("cookie_manager.py not found".into()) }