From 2592651c9a26739a8290d778646ccbfde84113c4 Mon Sep 17 00:00:00 2001 From: Connor Johnstone Date: Wed, 18 Mar 2026 15:36:16 -0400 Subject: [PATCH] Formatting --- src/backend.rs | 9 ++++- src/error.rs | 4 +- src/lib.rs | 9 ++++- src/main.rs | 24 ++++++------ src/queue.rs | 92 ++++++++++++++++++++------------------------ src/rate_limit.rs | 3 +- src/ytdlp.rs | 36 ++++++++--------- tests/integration.rs | 32 +++++++++++---- 8 files changed, 111 insertions(+), 98 deletions(-) diff --git a/src/backend.rs b/src/backend.rs index e391e8a..c8d7376 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -80,7 +80,9 @@ impl std::str::FromStr for AudioFormat { "mp3" => Ok(AudioFormat::Mp3), "flac" => Ok(AudioFormat::Flac), "best" => Ok(AudioFormat::Best), - _ => Err(format!("unsupported format: {s} (expected opus, mp3, flac, or best)")), + _ => Err(format!( + "unsupported format: {s} (expected opus, mp3, flac, or best)" + )), } } } @@ -98,7 +100,10 @@ pub trait DownloadBackend: Send + Sync { fn check_available(&self) -> impl std::future::Future> + Send; /// Search for tracks matching a query. - fn search(&self, query: &str) -> impl std::future::Future>> + Send; + fn search( + &self, + query: &str, + ) -> impl std::future::Future>> + Send; /// Download a target to the configured output directory. fn download( diff --git a/src/error.rs b/src/error.rs index 2dab744..0a99403 100644 --- a/src/error.rs +++ b/src/error.rs @@ -35,9 +35,7 @@ impl DlError { pub fn is_transient(&self) -> bool { matches!( self, - DlError::RateLimited(_) - | DlError::Io(_) - | DlError::BackendError(_) + DlError::RateLimited(_) | DlError::Io(_) | DlError::BackendError(_) ) } } diff --git a/src/lib.rs b/src/lib.rs index feeeb92..0224a2f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,7 +9,12 @@ pub mod queue; pub mod rate_limit; pub mod ytdlp; -pub use backend::{AudioFormat, BackendConfig, DownloadBackend, DownloadResult, DownloadTarget, SearchResult}; +pub use backend::{ + AudioFormat, BackendConfig, DownloadBackend, DownloadResult, DownloadTarget, SearchResult, +}; pub use error::{DlError, DlResult}; -pub use queue::{DlStats, ProgressFn, SyncStats, download_single, run_queue, run_queue_with_progress, sync_wanted_to_queue}; +pub use queue::{ + DlStats, ProgressFn, SyncStats, download_single, run_queue, run_queue_with_progress, + sync_wanted_to_queue, +}; pub use ytdlp::{SearchSource, YtDlpBackend}; diff --git a/src/main.rs b/src/main.rs index 862bf64..379cca4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -156,9 +156,7 @@ fn make_backend_config( output: &Option, cookies: &Option, ) -> anyhow::Result { - let fmt: AudioFormat = format - .parse() - .map_err(|e: String| anyhow::anyhow!(e))?; + let fmt: AudioFormat = format.parse().map_err(|e: String| anyhow::anyhow!(e))?; Ok(BackendConfig { output_dir: output.clone().unwrap_or_else(default_output_dir), format: fmt, @@ -199,13 +197,12 @@ async fn main() -> anyhow::Result<()> { let config = make_backend_config(&format, &output, &cookies)?; // Determine if it's a URL or a search query - let target = if query_or_url.starts_with("http://") - || query_or_url.starts_with("https://") - { - DownloadTarget::Url(query_or_url) - } else { - DownloadTarget::Query(query_or_url) - }; + let target = + if query_or_url.starts_with("http://") || query_or_url.starts_with("https://") { + DownloadTarget::Url(query_or_url) + } else { + DownloadTarget::Query(query_or_url) + }; download_single(&backend, target, &config, dry_run).await?; } @@ -235,7 +232,8 @@ async fn main() -> anyhow::Result<()> { println!("\nQueue processing complete: {stats}"); } QueueAction::Add { query } => { - let item = queries::downloads::enqueue(db.conn(), &query, None, "ytdlp").await?; + let item = + queries::downloads::enqueue(db.conn(), &query, None, "ytdlp").await?; println!("Added to queue: id={}, query=\"{}\"", item.id, item.query); } QueueAction::List { status } => { @@ -254,8 +252,8 @@ async fn main() -> anyhow::Result<()> { println!("Queue is empty."); } else { println!( - "{:<5} {:<12} {:<6} {:<40} {}", - "ID", "STATUS", "RETRY", "QUERY", "ERROR" + "{:<5} {:<12} {:<6} {:<40} ERROR", + "ID", "STATUS", "RETRY", "QUERY" ); for item in &items { println!( diff --git a/src/queue.rs b/src/queue.rs index 82acadf..dabc72f 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -81,7 +81,11 @@ pub async fn run_queue_with_progress( stats.downloads_attempted += 1; if let Some(ref cb) = on_progress { - cb(stats.downloads_attempted, total, &format!("Downloading: {}", item.query)); + cb( + stats.downloads_attempted, + total, + &format!("Downloading: {}", item.query), + ); } tracing::info!( @@ -106,8 +110,7 @@ pub async fn run_queue_with_progress( } // Mark as downloading - queries::downloads::update_status(conn, item.id, DownloadStatus::Downloading, None) - .await?; + queries::downloads::update_status(conn, item.id, DownloadStatus::Downloading, None).await?; // Determine download target let target = if let Some(ref url) = item.source_url { @@ -126,53 +129,45 @@ pub async fn run_queue_with_progress( "download completed" ); - queries::downloads::update_status( - conn, - item.id, - DownloadStatus::Completed, - None, - ) - .await?; + queries::downloads::update_status(conn, item.id, DownloadStatus::Completed, None) + .await?; // Update wanted item status and create track record with MBID - if let Some(wanted_id) = item.wanted_item_id { - if let Ok(wanted) = queries::wanted::get_by_id(conn, wanted_id).await { - // Create a track record with the MBID so the tagger - // can skip searching and go straight to the right recording - let now = chrono::Utc::now().naive_utc(); - let file_path = result.file_path.to_string_lossy().to_string(); - let file_size = std::fs::metadata(&result.file_path) - .map(|m| m.len() as i64) - .unwrap_or(0); + if let Some(wanted_id) = item.wanted_item_id + && let Ok(wanted) = queries::wanted::get_by_id(conn, wanted_id).await + { + // Create a track record with the MBID so the tagger + // can skip searching and go straight to the right recording + let now = chrono::Utc::now().naive_utc(); + let file_path = result.file_path.to_string_lossy().to_string(); + let file_size = std::fs::metadata(&result.file_path) + .map(|m| m.len() as i64) + .unwrap_or(0); - let track_active = shanty_db::entities::track::ActiveModel { - file_path: Set(file_path), - title: Set(Some(result.title.clone())), - artist: Set(result.artist.clone()), - file_size: Set(file_size), - musicbrainz_id: Set(wanted.musicbrainz_id.clone()), - artist_id: Set(wanted.artist_id), - added_at: Set(now), - updated_at: Set(now), - ..Default::default() - }; - if let Err(e) = queries::tracks::upsert(conn, track_active).await { - tracing::warn!(error = %e, "failed to create track record after download"); - } + let track_active = shanty_db::entities::track::ActiveModel { + file_path: Set(file_path), + title: Set(Some(result.title.clone())), + artist: Set(result.artist.clone()), + file_size: Set(file_size), + musicbrainz_id: Set(wanted.musicbrainz_id.clone()), + artist_id: Set(wanted.artist_id), + added_at: Set(now), + updated_at: Set(now), + ..Default::default() + }; + if let Err(e) = queries::tracks::upsert(conn, track_active).await { + tracing::warn!(error = %e, "failed to create track record after download"); + } - if let Err(e) = queries::wanted::update_status( - conn, - wanted_id, - WantedStatus::Downloaded, - ) - .await - { - tracing::warn!( - wanted_id = wanted_id, - error = %e, - "failed to update wanted item status" - ); - } + if let Err(e) = + queries::wanted::update_status(conn, wanted_id, WantedStatus::Downloaded) + .await + { + tracing::warn!( + wanted_id = wanted_id, + error = %e, + "failed to update wanted item status" + ); } } @@ -244,10 +239,7 @@ impl fmt::Display for SyncStats { /// Sync wanted items to the download queue. /// Finds all Track-type Wanted items and enqueues them for download, /// skipping any that already have a queue entry. -pub async fn sync_wanted_to_queue( - conn: &DatabaseConnection, - dry_run: bool, -) -> DlResult { +pub async fn sync_wanted_to_queue(conn: &DatabaseConnection, dry_run: bool) -> DlResult { let wanted = queries::wanted::list(conn, Some(WantedStatus::Wanted)).await?; let mut stats = SyncStats::default(); diff --git a/src/rate_limit.rs b/src/rate_limit.rs index a57487a..6881da3 100644 --- a/src/rate_limit.rs +++ b/src/rate_limit.rs @@ -42,8 +42,7 @@ impl RateLimiter { state.remaining -= 1; // Warn when approaching the limit - let pct_remaining = - (state.remaining as f64 / self.max_per_hour as f64) * 100.0; + let pct_remaining = (state.remaining as f64 / self.max_per_hour as f64) * 100.0; if pct_remaining < 10.0 && pct_remaining > 0.0 { tracing::warn!( remaining = state.remaining, diff --git a/src/ytdlp.rs b/src/ytdlp.rs index 901bdce..6de909d 100644 --- a/src/ytdlp.rs +++ b/src/ytdlp.rs @@ -4,7 +4,9 @@ use std::process::Stdio; use serde::Deserialize; use tokio::process::Command; -use crate::backend::{BackendConfig, DownloadBackend, DownloadResult, DownloadTarget, SearchResult}; +use crate::backend::{ + BackendConfig, DownloadBackend, DownloadResult, DownloadTarget, SearchResult, +}; use crate::error::{DlError, DlResult}; use crate::rate_limit::RateLimiter; @@ -22,7 +24,9 @@ impl std::str::FromStr for SearchSource { match s.to_lowercase().as_str() { "ytmusic" | "youtube_music" | "youtubemusic" => Ok(SearchSource::YouTubeMusic), "youtube" | "yt" => Ok(SearchSource::YouTube), - _ => Err(format!("unknown search source: {s} (expected ytmusic or youtube)")), + _ => Err(format!( + "unknown search source: {s} (expected ytmusic or youtube)" + )), } } } @@ -63,9 +67,7 @@ impl YtDlpBackend { self.rate_limiter.acquire().await; let mut cmd = Command::new("yt-dlp"); - cmd.args(args) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()); + cmd.args(args).stdout(Stdio::piped()).stderr(Stdio::piped()); // Add cookies if configured if let Some(ref cookies) = self.cookies_path { @@ -120,7 +122,9 @@ impl YtDlpBackend { if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr).to_string(); tracing::warn!(stderr = %stderr, "ytmusic search failed"); - return Err(DlError::BackendError(format!("ytmusic search failed: {stderr}"))); + return Err(DlError::BackendError(format!( + "ytmusic search failed: {stderr}" + ))); } let stdout = String::from_utf8_lossy(&output.stdout); @@ -180,17 +184,17 @@ impl YtDlpBackend { fn find_ytmusic_script(&self) -> DlResult { // Check next to the current executable if let Ok(exe) = std::env::current_exe() { - let beside_exe = exe.parent().unwrap_or(std::path::Path::new(".")).join("ytmusic_search.py"); + let beside_exe = exe + .parent() + .unwrap_or(std::path::Path::new(".")) + .join("ytmusic_search.py"); if beside_exe.exists() { return Ok(beside_exe); } } // Check common install locations - for dir in &[ - "/usr/share/shanty", - "/usr/local/share/shanty", - ] { + for dir in &["/usr/share/shanty", "/usr/local/share/shanty"] { let path = PathBuf::from(dir).join("ytmusic_search.py"); if path.exists() { return Ok(path); @@ -303,10 +307,7 @@ impl DownloadBackend for YtDlpBackend { // Add cookies from backend config or backend's own cookies let cookies_str; - let cookies_path = config - .cookies_path - .as_ref() - .or(self.cookies_path.as_ref()); + let cookies_path = config.cookies_path.as_ref().or(self.cookies_path.as_ref()); if let Some(c) = cookies_path { cookies_str = c.to_string_lossy().to_string(); args.push("--cookies"); @@ -318,9 +319,8 @@ impl DownloadBackend for YtDlpBackend { let output = self.run_ytdlp(&args).await?; // Parse the JSON output to get the actual file path - let info: YtDlpDownloadInfo = serde_json::from_str(output.trim()).map_err(|e| { - DlError::BackendError(format!("failed to parse yt-dlp output: {e}")) - })?; + let info: YtDlpDownloadInfo = serde_json::from_str(output.trim()) + .map_err(|e| DlError::BackendError(format!("failed to parse yt-dlp output: {e}")))?; // --print-json reports the pre-extraction filename (e.g. .webm), // but --extract-audio produces a file with the target format extension. diff --git a/tests/integration.rs b/tests/integration.rs index cfc4114..399bb05 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -1,8 +1,10 @@ use shanty_db::entities::download_queue::DownloadStatus; use shanty_db::{Database, queries}; -use shanty_dl::backend::{AudioFormat, BackendConfig, DownloadBackend, DownloadResult, DownloadTarget, SearchResult}; +use shanty_dl::backend::{ + AudioFormat, BackendConfig, DownloadBackend, DownloadResult, DownloadTarget, SearchResult, +}; use shanty_dl::error::DlResult; -use shanty_dl::queue::{run_queue, download_single}; +use shanty_dl::queue::{download_single, run_queue}; use tempfile::TempDir; /// Mock backend for testing without yt-dlp. @@ -84,7 +86,9 @@ async fn test_queue_process_success() { .unwrap(); // Process queue - let stats = run_queue(db.conn(), &backend, &config, false).await.unwrap(); + let stats = run_queue(db.conn(), &backend, &config, false) + .await + .unwrap(); assert_eq!(stats.downloads_attempted, 1); assert_eq!(stats.downloads_completed, 1); assert_eq!(stats.downloads_failed, 0); @@ -112,7 +116,9 @@ async fn test_queue_process_failure() { .await .unwrap(); - let stats = run_queue(db.conn(), &backend, &config, false).await.unwrap(); + let stats = run_queue(db.conn(), &backend, &config, false) + .await + .unwrap(); assert_eq!(stats.downloads_attempted, 1); assert_eq!(stats.downloads_failed, 1); @@ -218,9 +224,17 @@ async fn test_wanted_item_status_updated_on_download() { }; // Create a wanted item - let wanted = queries::wanted::add(db.conn(), ItemType::Track, "Wanted Song", None, None, None, None) - .await - .unwrap(); + let wanted = queries::wanted::add( + db.conn(), + ItemType::Track, + "Wanted Song", + None, + None, + None, + None, + ) + .await + .unwrap(); assert_eq!(wanted.status, WantedStatus::Wanted); // Enqueue download linked to the wanted item @@ -229,7 +243,9 @@ async fn test_wanted_item_status_updated_on_download() { .unwrap(); // Process queue - run_queue(db.conn(), &backend, &config, false).await.unwrap(); + run_queue(db.conn(), &backend, &config, false) + .await + .unwrap(); // Wanted item should now be Downloaded let updated = queries::wanted::get_by_id(db.conn(), wanted.id)