diff --git a/src/main.rs b/src/main.rs index a252260..38fe6db 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,5 @@ use actix_cors::Cors; -use actix_web::{web, App, HttpServer}; +use actix_web::{App, HttpServer, web}; use clap::Parser; use tracing_actix_web::TracingLogger; use tracing_subscriber::EnvFilter; @@ -64,7 +64,7 @@ async fn main() -> anyhow::Result<()> { mb_client, search, config: std::sync::Arc::new(tokio::sync::RwLock::new(config)), - config_path: config_path, + config_path, tasks: TaskManager::new(), }); diff --git a/src/routes/albums.rs b/src/routes/albums.rs index 0e3cb5f..333e172 100644 --- a/src/routes/albums.rs +++ b/src/routes/albums.rs @@ -1,4 +1,4 @@ -use actix_web::{web, HttpResponse}; +use actix_web::{HttpResponse, web}; use serde::{Deserialize, Serialize}; use shanty_db::entities::wanted_item::WantedStatus; @@ -15,7 +15,9 @@ pub struct PaginationParams { #[serde(default)] offset: u64, } -fn default_limit() -> u64 { 50 } +fn default_limit() -> u64 { + 50 +} #[derive(Deserialize)] pub struct AddAlbumRequest { @@ -40,10 +42,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) { .route(web::get().to(list_albums)) .route(web::post().to(add_album)), ) - .service( - web::resource("/albums/{mbid}") - .route(web::get().to(get_album)), - ); + .service(web::resource("/albums/{mbid}").route(web::get().to(get_album))); } async fn list_albums( @@ -69,7 +68,8 @@ async fn get_album( Err(_) => { // Probably a release-group MBID. Browse releases for this group. let release_mbid = resolve_release_from_group(&state, &mbid).await?; - state.mb_client + state + .mb_client .get_release_tracks(&release_mbid) .await .map_err(|e| ApiError::Internal(format!("MusicBrainz error: {e}")))? @@ -112,7 +112,7 @@ async fn get_album( /// Given a release-group MBID, find the first release MBID via the MB API. async fn resolve_release_from_group( - state: &web::Data, + _state: &web::Data, release_group_mbid: &str, ) -> Result { // Use the MB client's get_json (it's private, so we go through search) @@ -152,7 +152,11 @@ async fn resolve_release_from_group( .and_then(|r| r.get("id")) .and_then(|id| id.as_str()) .map(String::from) - .ok_or_else(|| ApiError::NotFound(format!("no releases found for release group {release_group_mbid}"))) + .ok_or_else(|| { + ApiError::NotFound(format!( + "no releases found for release group {release_group_mbid}" + )) + }) } async fn add_album( diff --git a/src/routes/artists.rs b/src/routes/artists.rs index 655c160..c155581 100644 --- a/src/routes/artists.rs +++ b/src/routes/artists.rs @@ -1,4 +1,4 @@ -use actix_web::{web, HttpResponse}; +use actix_web::{HttpResponse, web}; use serde::{Deserialize, Serialize}; use shanty_db::entities::wanted_item::WantedStatus; @@ -16,7 +16,9 @@ pub struct PaginationParams { #[serde(default)] offset: u64, } -fn default_limit() -> u64 { 50 } +fn default_limit() -> u64 { + 50 +} #[derive(Deserialize)] pub struct AddArtistRequest { @@ -67,10 +69,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) { .route(web::get().to(list_artists)) .route(web::post().to(add_artist)), ) - .service( - web::resource("/artists/{id}/full") - .route(web::get().to(get_artist_full)), - ) + .service(web::resource("/artists/{id}/full").route(web::get().to(get_artist_full))) .service( web::resource("/artists/{id}") .route(web::get().to(get_artist)) @@ -94,22 +93,25 @@ async fn list_artists( // Check if we have cached artist-level totals from a prior detail page load let cache_key = format!("artist_totals:{}", a.id); - let cached_totals: Option<(u32, u32, u32)> = if let Ok(Some(json)) = - queries::cache::get(state.db.conn(), &cache_key).await - { - serde_json::from_str(&json).ok() - } else { - None - }; + let cached_totals: Option<(u32, u32, u32)> = + if let Ok(Some(json)) = queries::cache::get(state.db.conn(), &cache_key).await { + serde_json::from_str(&json).ok() + } else { + None + }; - let (total_watched, total_owned, total_items) = if let Some((avail, watched, owned)) = cached_totals { - (watched as usize, owned as usize, avail as usize) - } else { - // Fall back to wanted item counts - let total_items = artist_wanted.len(); - let total_owned = artist_wanted.iter().filter(|w| w.status == WantedStatus::Owned).count(); - (total_items, total_owned, total_items) - }; + let (total_watched, total_owned, total_items) = + if let Some((avail, watched, owned)) = cached_totals { + (watched as usize, owned as usize, avail as usize) + } else { + // Fall back to wanted item counts + let total_items = artist_wanted.len(); + let total_owned = artist_wanted + .iter() + .filter(|w| w.status == WantedStatus::Owned) + .count(); + (total_items, total_owned, total_items) + }; items.push(ArtistListItem { id: a.id, @@ -137,7 +139,9 @@ async fn get_artist( "albums": albums, }))) } else { - Err(ApiError::BadRequest("use /artists/{id}/full for MBID lookups".into())) + Err(ApiError::BadRequest( + "use /artists/{id}/full for MBID lookups".into(), + )) } } @@ -153,16 +157,23 @@ async fn get_cached_album_tracks( let cache_key = format!("artist_rg_tracks:{rg_id}"); // Check cache first - if let Some(json) = queries::cache::get(state.db.conn(), &cache_key).await + if let Some(json) = queries::cache::get(state.db.conn(), &cache_key) + .await .map_err(|e| ApiError::Internal(e.to_string()))? + && let Ok(cached) = serde_json::from_str::(&json) { - if let Ok(cached) = serde_json::from_str::(&json) { - // Extend TTL if artist is now watched (upgrades 7-day browse cache to permanent) - if extend_ttl { - let _ = queries::cache::set(state.db.conn(), &cache_key, "musicbrainz", &json, ttl_seconds).await; - } - return Ok(cached); + // Extend TTL if artist is now watched (upgrades 7-day browse cache to permanent) + if extend_ttl { + let _ = queries::cache::set( + state.db.conn(), + &cache_key, + "musicbrainz", + &json, + ttl_seconds, + ) + .await; } + return Ok(cached); } // Not cached — resolve release MBID and fetch tracks @@ -173,7 +184,8 @@ async fn get_cached_album_tracks( resolve_release_from_group(rg_id).await? }; - let mb_tracks = state.mb_client + let mb_tracks = state + .mb_client .get_release_tracks(&release_mbid) .await .map_err(|e| ApiError::Internal(format!("MB error for release {release_mbid}: {e}")))?; @@ -190,9 +202,15 @@ async fn get_cached_album_tracks( }; // Cache with caller-specified TTL - let json = serde_json::to_string(&cached) - .map_err(|e| ApiError::Internal(e.to_string()))?; - let _ = queries::cache::set(state.db.conn(), &cache_key, "musicbrainz", &json, ttl_seconds).await; + let json = serde_json::to_string(&cached).map_err(|e| ApiError::Internal(e.to_string()))?; + let _ = queries::cache::set( + state.db.conn(), + &cache_key, + "musicbrainz", + &json, + ttl_seconds, + ) + .await; Ok(cached) } @@ -258,10 +276,14 @@ pub async fn enrich_artist( let mbid = match &artist.musicbrainz_id { Some(m) => m.clone(), None => { - let results = state.search.search_artist(&artist.name, 1).await + let results = state + .search + .search_artist(&artist.name, 1) + .await .map_err(|e| ApiError::Internal(e.to_string()))?; - results.into_iter().next().map(|a| a.id) - .ok_or_else(|| ApiError::NotFound(format!("no MBID for artist '{}'", artist.name)))? + results.into_iter().next().map(|a| a.id).ok_or_else(|| { + ApiError::NotFound(format!("no MBID for artist '{}'", artist.name)) + })? } }; (artist, Some(local_id), mbid) @@ -272,7 +294,8 @@ pub async fn enrich_artist( let local = { // Check if any local artist has this MBID let all = queries::artists::list(state.db.conn(), 1000, 0).await?; - all.into_iter().find(|a| a.musicbrainz_id.as_deref() == Some(&mbid)) + all.into_iter() + .find(|a| a.musicbrainz_id.as_deref() == Some(&mbid)) }; if let Some(a) = local { @@ -280,7 +303,8 @@ pub async fn enrich_artist( (a, Some(local_id), mbid) } else { // Look up artist name from MusicBrainz by MBID — don't create a local record - let (name, _disambiguation) = state.mb_client + let (name, _disambiguation) = state + .mb_client .get_artist_by_mbid(&mbid) .await .map_err(|e| ApiError::NotFound(format!("artist MBID {mbid} not found: {e}")))?; @@ -299,7 +323,10 @@ pub async fn enrich_artist( }; // Fetch release groups and filter by allowed secondary types - let all_release_groups = state.search.get_release_groups(&mbid).await + let all_release_groups = state + .search + .get_release_groups(&mbid) + .await .map_err(|e| ApiError::Internal(e.to_string()))?; let allowed = state.config.read().await.allowed_secondary_types.clone(); let release_groups: Vec<_> = all_release_groups @@ -369,15 +396,21 @@ pub async fn enrich_artist( // If artist has any watched items, cache permanently (10 years); // otherwise cache for 7 days (just browsing) let is_watched = !artist_wanted.is_empty(); - let cache_ttl = if is_watched { 10 * 365 * 86400 } else { 7 * 86400 }; + let cache_ttl = if is_watched { + 10 * 365 * 86400 + } else { + 7 * 86400 + }; let cached = match get_cached_album_tracks( - &state, + state, &rg.id, rg.first_release_id.as_deref(), cache_ttl, is_watched, - ).await { + ) + .await + { Ok(c) => c, Err(e) => { tracing::warn!(rg_id = %rg.id, title = %rg.title, error = %e, "failed to fetch tracks"); @@ -435,7 +468,10 @@ pub async fn enrich_artist( .find(|a| a.name.to_lowercase() == rg.title.to_lowercase()); let local_album_id = local.map(|a| a.id); let local_tracks = if let Some(aid) = local_album_id { - queries::tracks::get_by_album(state.db.conn(), aid).await.unwrap_or_default().len() as u32 + queries::tracks::get_by_album(state.db.conn(), aid) + .await + .unwrap_or_default() + .len() as u32 } else { 0 }; @@ -468,9 +504,14 @@ pub async fn enrich_artist( // Sort: owned first, then partial, then wanted, then unwatched; within each by date albums.sort_by(|a, b| { let order = |s: &str| match s { - "owned" => 0, "partial" => 1, "wanted" => 2, _ => 3, + "owned" => 0, + "partial" => 1, + "wanted" => 2, + _ => 3, }; - order(&a.status).cmp(&order(&b.status)).then_with(|| a.date.cmp(&b.date)) + order(&a.status) + .cmp(&order(&b.status)) + .then_with(|| a.date.cmp(&b.date)) }); // Deduplicated artist-level totals @@ -478,7 +519,10 @@ pub async fn enrich_artist( let total_artist_watched = seen_watched.len() as u32; let total_artist_owned = seen_owned.len() as u32; - let artist_status = if total_artist_owned > 0 && total_artist_owned >= total_available_tracks && total_available_tracks > 0 { + let artist_status = if total_artist_owned > 0 + && total_artist_owned >= total_available_tracks + && total_available_tracks > 0 + { "owned" } else if total_artist_watched > 0 { "partial" @@ -487,16 +531,25 @@ pub async fn enrich_artist( }; // Cache artist-level totals for the library listing page - if !skip_track_fetch { - if let Some(local_id) = id { - let cache_key = format!("artist_totals:{local_id}"); - let totals = serde_json::json!([total_available_tracks, total_artist_watched, total_artist_owned]); - let _ = queries::cache::set( - state.db.conn(), &cache_key, "computed", - &totals.to_string(), - if artist_wanted.is_empty() { 7 * 86400 } else { 10 * 365 * 86400 }, - ).await; - } + if !skip_track_fetch && let Some(local_id) = id { + let cache_key = format!("artist_totals:{local_id}"); + let totals = serde_json::json!([ + total_available_tracks, + total_artist_watched, + total_artist_owned + ]); + let _ = queries::cache::set( + state.db.conn(), + &cache_key, + "computed", + &totals.to_string(), + if artist_wanted.is_empty() { + 7 * 86400 + } else { + 10 * 365 * 86400 + }, + ) + .await; } Ok(serde_json::json!({ @@ -515,10 +568,7 @@ pub async fn enrich_all_watched_artists(state: &AppState) -> Result = all_wanted - .iter() - .filter_map(|w| w.artist_id) - .collect(); + let mut artist_ids: Vec = all_wanted.iter().filter_map(|w| w.artist_id).collect(); artist_ids.sort(); artist_ids.dedup(); diff --git a/src/routes/downloads.rs b/src/routes/downloads.rs index ae76f64..98fd44c 100644 --- a/src/routes/downloads.rs +++ b/src/routes/downloads.rs @@ -1,4 +1,4 @@ -use actix_web::{web, HttpResponse}; +use actix_web::{HttpResponse, web}; use serde::Deserialize; use shanty_db::entities::download_queue::DownloadStatus; @@ -19,30 +19,12 @@ pub struct EnqueueRequest { } pub fn configure(cfg: &mut web::ServiceConfig) { - cfg.service( - web::resource("/downloads/queue") - .route(web::get().to(list_queue)), - ) - .service( - web::resource("/downloads") - .route(web::post().to(enqueue_download)), - ) - .service( - web::resource("/downloads/sync") - .route(web::post().to(sync_downloads)), - ) - .service( - web::resource("/downloads/process") - .route(web::post().to(trigger_process)), - ) - .service( - web::resource("/downloads/retry/{id}") - .route(web::post().to(retry_download)), - ) - .service( - web::resource("/downloads/{id}") - .route(web::delete().to(cancel_download)), - ); + cfg.service(web::resource("/downloads/queue").route(web::get().to(list_queue))) + .service(web::resource("/downloads").route(web::post().to(enqueue_download))) + .service(web::resource("/downloads/sync").route(web::post().to(sync_downloads))) + .service(web::resource("/downloads/process").route(web::post().to(trigger_process))) + .service(web::resource("/downloads/retry/{id}").route(web::post().to(retry_download))) + .service(web::resource("/downloads/{id}").route(web::delete().to(cancel_download))); } async fn list_queue( @@ -69,9 +51,7 @@ async fn enqueue_download( Ok(HttpResponse::Ok().json(item)) } -async fn sync_downloads( - state: web::Data, -) -> Result { +async fn sync_downloads(state: web::Data) -> Result { let stats = shanty_dl::sync_wanted_to_queue(state.db.conn(), false).await?; Ok(HttpResponse::Ok().json(serde_json::json!({ "found": stats.found, @@ -80,9 +60,7 @@ async fn sync_downloads( }))) } -async fn trigger_process( - state: web::Data, -) -> Result { +async fn trigger_process(state: web::Data) -> Result { let task_id = state.tasks.register("download"); let state = state.clone(); let tid = task_id.clone(); @@ -90,9 +68,21 @@ async fn trigger_process( tokio::spawn(async move { let cfg = state.config.read().await.clone(); let cookies = cfg.download.cookies_path.clone(); - let format: shanty_dl::AudioFormat = cfg.download.format.parse().unwrap_or(shanty_dl::AudioFormat::Opus); - let source: shanty_dl::SearchSource = cfg.download.search_source.parse().unwrap_or(shanty_dl::SearchSource::YouTubeMusic); - let rate = if cookies.is_some() { cfg.download.rate_limit_auth } else { cfg.download.rate_limit }; + let format: shanty_dl::AudioFormat = cfg + .download + .format + .parse() + .unwrap_or(shanty_dl::AudioFormat::Opus); + let source: shanty_dl::SearchSource = cfg + .download + .search_source + .parse() + .unwrap_or(shanty_dl::SearchSource::YouTubeMusic); + let rate = if cookies.is_some() { + cfg.download.rate_limit_auth + } else { + cfg.download.rate_limit + }; let backend = shanty_dl::YtDlpBackend::new(rate, source, cookies.clone()); let backend_config = shanty_dl::BackendConfig { output_dir: cfg.download_path.clone(), @@ -103,10 +93,20 @@ async fn trigger_process( let task_state = state.clone(); let progress_tid = tid.clone(); let on_progress: shanty_dl::ProgressFn = Box::new(move |current, total, msg| { - task_state.tasks.update_progress(&progress_tid, current, total, msg); + task_state + .tasks + .update_progress(&progress_tid, current, total, msg); }); - match shanty_dl::run_queue_with_progress(state.db.conn(), &backend, &backend_config, false, Some(on_progress)).await { + match shanty_dl::run_queue_with_progress( + state.db.conn(), + &backend, + &backend_config, + false, + Some(on_progress), + ) + .await + { Ok(stats) => { state.tasks.complete(&tid, format!("{stats}")); // Refresh artist data in background diff --git a/src/routes/search.rs b/src/routes/search.rs index 0127da0..c599ab2 100644 --- a/src/routes/search.rs +++ b/src/routes/search.rs @@ -1,4 +1,4 @@ -use actix_web::{web, HttpResponse}; +use actix_web::{HttpResponse, web}; use serde::Deserialize; use shanty_search::SearchProvider; @@ -21,7 +21,9 @@ pub struct AlbumTrackSearchParams { limit: u32, } -fn default_limit() -> u32 { 25 } +fn default_limit() -> u32 { + 25 +} pub fn configure(cfg: &mut web::ServiceConfig) { cfg.service(web::resource("/search/artist").route(web::get().to(search_artist))) diff --git a/src/routes/system.rs b/src/routes/system.rs index a381a66..43ecc7c 100644 --- a/src/routes/system.rs +++ b/src/routes/system.rs @@ -1,4 +1,4 @@ -use actix_web::{web, HttpResponse}; +use actix_web::{HttpResponse, web}; use serde::Deserialize; use shanty_db::entities::download_queue::DownloadStatus; @@ -25,13 +25,14 @@ pub fn configure(cfg: &mut web::ServiceConfig) { ); } -async fn get_status( - state: web::Data, -) -> Result { +async fn get_status(state: web::Data) -> Result { let summary = shanty_watch::library_summary(state.db.conn()).await?; - let pending_items = queries::downloads::list(state.db.conn(), Some(DownloadStatus::Pending)).await?; - let downloading_items = queries::downloads::list(state.db.conn(), Some(DownloadStatus::Downloading)).await?; - let failed_items = queries::downloads::list(state.db.conn(), Some(DownloadStatus::Failed)).await?; + let pending_items = + queries::downloads::list(state.db.conn(), Some(DownloadStatus::Pending)).await?; + let downloading_items = + queries::downloads::list(state.db.conn(), Some(DownloadStatus::Downloading)).await?; + let failed_items = + queries::downloads::list(state.db.conn(), Some(DownloadStatus::Failed)).await?; let tasks = state.tasks.list(); let mut queue_items = Vec::new(); @@ -57,16 +58,16 @@ async fn get_status( }))) } -async fn trigger_index( - state: web::Data, -) -> Result { +async fn trigger_index(state: web::Data) -> Result { let task_id = state.tasks.register("index"); let state = state.clone(); let tid = task_id.clone(); tokio::spawn(async move { let cfg = state.config.read().await.clone(); - state.tasks.update_progress(&tid, 0, 0, "Scanning library..."); + state + .tasks + .update_progress(&tid, 0, 0, "Scanning library..."); let scan_config = shanty_index::ScanConfig { root: cfg.library_path.clone(), dry_run: false, @@ -81,16 +82,16 @@ async fn trigger_index( Ok(HttpResponse::Accepted().json(serde_json::json!({ "task_id": task_id }))) } -async fn trigger_tag( - state: web::Data, -) -> Result { +async fn trigger_tag(state: web::Data) -> Result { let task_id = state.tasks.register("tag"); let state = state.clone(); let tid = task_id.clone(); tokio::spawn(async move { let cfg = state.config.read().await.clone(); - state.tasks.update_progress(&tid, 0, 0, "Preparing tagger..."); + state + .tasks + .update_progress(&tid, 0, 0, "Preparing tagger..."); let mb = match shanty_tag::MusicBrainzClient::new() { Ok(c) => c, Err(e) => { @@ -113,16 +114,16 @@ async fn trigger_tag( Ok(HttpResponse::Accepted().json(serde_json::json!({ "task_id": task_id }))) } -async fn trigger_organize( - state: web::Data, -) -> Result { +async fn trigger_organize(state: web::Data) -> Result { let task_id = state.tasks.register("organize"); let state = state.clone(); let tid = task_id.clone(); tokio::spawn(async move { let cfg = state.config.read().await.clone(); - state.tasks.update_progress(&tid, 0, 0, "Organizing files..."); + state + .tasks + .update_progress(&tid, 0, 0, "Organizing files..."); let org_config = shanty_org::OrgConfig { target_dir: cfg.library_path.clone(), format: cfg.organization_format.clone(), @@ -149,9 +150,7 @@ async fn trigger_organize( Ok(HttpResponse::Accepted().json(serde_json::json!({ "task_id": task_id }))) } -async fn trigger_pipeline( - state: web::Data, -) -> Result { +async fn trigger_pipeline(state: web::Data) -> Result { let sync_id = state.tasks.register_pending("sync"); let download_id = state.tasks.register_pending("download"); let index_id = state.tasks.register_pending("index"); @@ -175,7 +174,9 @@ async fn trigger_pipeline( // Step 1: Sync state.tasks.start(&sync_id); - state.tasks.update_progress(&sync_id, 0, 0, "Syncing watchlist to download queue..."); + state + .tasks + .update_progress(&sync_id, 0, 0, "Syncing watchlist to download queue..."); match shanty_dl::sync_wanted_to_queue(state.db.conn(), false).await { Ok(stats) => state.tasks.complete(&sync_id, format!("{stats}")), Err(e) => state.tasks.fail(&sync_id, e.to_string()), @@ -184,9 +185,21 @@ async fn trigger_pipeline( // Step 2: Download state.tasks.start(&download_id); let cookies = cfg.download.cookies_path.clone(); - let format: shanty_dl::AudioFormat = cfg.download.format.parse().unwrap_or(shanty_dl::AudioFormat::Opus); - let source: shanty_dl::SearchSource = cfg.download.search_source.parse().unwrap_or(shanty_dl::SearchSource::YouTubeMusic); - let rate = if cookies.is_some() { cfg.download.rate_limit_auth } else { cfg.download.rate_limit }; + let format: shanty_dl::AudioFormat = cfg + .download + .format + .parse() + .unwrap_or(shanty_dl::AudioFormat::Opus); + let source: shanty_dl::SearchSource = cfg + .download + .search_source + .parse() + .unwrap_or(shanty_dl::SearchSource::YouTubeMusic); + let rate = if cookies.is_some() { + cfg.download.rate_limit_auth + } else { + cfg.download.rate_limit + }; let backend = shanty_dl::YtDlpBackend::new(rate, source, cookies.clone()); let backend_config = shanty_dl::BackendConfig { output_dir: cfg.download_path.clone(), @@ -196,9 +209,19 @@ async fn trigger_pipeline( let task_state = state.clone(); let progress_tid = download_id.clone(); let on_progress: shanty_dl::ProgressFn = Box::new(move |current, total, msg| { - task_state.tasks.update_progress(&progress_tid, current, total, msg); + task_state + .tasks + .update_progress(&progress_tid, current, total, msg); }); - match shanty_dl::run_queue_with_progress(state.db.conn(), &backend, &backend_config, false, Some(on_progress)).await { + match shanty_dl::run_queue_with_progress( + state.db.conn(), + &backend, + &backend_config, + false, + Some(on_progress), + ) + .await + { Ok(stats) => { let _ = queries::cache::purge_prefix(state.db.conn(), "artist_totals:").await; state.tasks.complete(&download_id, format!("{stats}")); @@ -208,7 +231,9 @@ async fn trigger_pipeline( // Step 3: Index state.tasks.start(&index_id); - state.tasks.update_progress(&index_id, 0, 0, "Scanning library..."); + state + .tasks + .update_progress(&index_id, 0, 0, "Scanning library..."); let scan_config = shanty_index::ScanConfig { root: cfg.library_path.clone(), dry_run: false, @@ -221,7 +246,9 @@ async fn trigger_pipeline( // Step 4: Tag state.tasks.start(&tag_id); - state.tasks.update_progress(&tag_id, 0, 0, "Tagging tracks..."); + state + .tasks + .update_progress(&tag_id, 0, 0, "Tagging tracks..."); match shanty_tag::MusicBrainzClient::new() { Ok(mb) => { let tag_config = shanty_tag::TagConfig { @@ -239,7 +266,9 @@ async fn trigger_pipeline( // Step 5: Organize state.tasks.start(&organize_id); - state.tasks.update_progress(&organize_id, 0, 0, "Organizing files..."); + state + .tasks + .update_progress(&organize_id, 0, 0, "Organizing files..."); let org_config = shanty_org::OrgConfig { target_dir: cfg.library_path.clone(), format: cfg.organization_format.clone(), @@ -249,7 +278,8 @@ async fn trigger_pipeline( match shanty_org::organize_from_db(state.db.conn(), &org_config).await { Ok(stats) => { let promoted = queries::wanted::promote_downloaded_to_owned(state.db.conn()) - .await.unwrap_or(0); + .await + .unwrap_or(0); let msg = if promoted > 0 { format!("{stats} — {promoted} items marked as owned") } else { @@ -262,9 +292,13 @@ async fn trigger_pipeline( // Step 6: Enrich state.tasks.start(&enrich_id); - state.tasks.update_progress(&enrich_id, 0, 0, "Refreshing artist data..."); + state + .tasks + .update_progress(&enrich_id, 0, 0, "Refreshing artist data..."); match enrich_all_watched_artists(&state).await { - Ok(count) => state.tasks.complete(&enrich_id, format!("{count} artists refreshed")), + Ok(count) => state + .tasks + .complete(&enrich_id, format!("{count} artists refreshed")), Err(e) => state.tasks.fail(&enrich_id, e.to_string()), } }); @@ -283,9 +317,7 @@ async fn get_task( } } -async fn list_watchlist( - state: web::Data, -) -> Result { +async fn list_watchlist(state: web::Data) -> Result { let items = shanty_watch::list_items(state.db.conn(), None, None).await?; Ok(HttpResponse::Ok().json(items)) } @@ -299,9 +331,7 @@ async fn remove_watchlist( Ok(HttpResponse::NoContent().finish()) } -async fn get_config( - state: web::Data, -) -> Result { +async fn get_config(state: web::Data) -> Result { let config = state.config.read().await; Ok(HttpResponse::Ok().json(&*config)) } @@ -319,8 +349,9 @@ async fn save_config( let new_config = body.into_inner().config; // Persist to YAML - new_config.save(state.config_path.as_deref()) - .map_err(|e| ApiError::Internal(e))?; + new_config + .save(state.config_path.as_deref()) + .map_err(ApiError::Internal)?; // Update in-memory config let mut config = state.config.write().await; diff --git a/src/routes/tracks.rs b/src/routes/tracks.rs index 9995f61..2d970b0 100644 --- a/src/routes/tracks.rs +++ b/src/routes/tracks.rs @@ -1,4 +1,4 @@ -use actix_web::{web, HttpResponse}; +use actix_web::{HttpResponse, web}; use serde::Deserialize; use shanty_db::queries; @@ -6,7 +6,9 @@ use shanty_db::queries; use crate::error::ApiError; use crate::state::AppState; -fn default_limit() -> u64 { 50 } +fn default_limit() -> u64 { + 50 +} #[derive(Deserialize)] pub struct SearchParams { @@ -18,14 +20,8 @@ pub struct SearchParams { } pub fn configure(cfg: &mut web::ServiceConfig) { - cfg.service( - web::resource("/tracks") - .route(web::get().to(list_tracks)), - ) - .service( - web::resource("/tracks/{id}") - .route(web::get().to(get_track)), - ); + cfg.service(web::resource("/tracks").route(web::get().to(list_tracks))) + .service(web::resource("/tracks/{id}").route(web::get().to(get_track))); } async fn list_tracks( diff --git a/src/tasks.rs b/src/tasks.rs index ea970d7..b84f682 100644 --- a/src/tasks.rs +++ b/src/tasks.rs @@ -34,6 +34,12 @@ pub struct TaskManager { tasks: Mutex>, } +impl Default for TaskManager { + fn default() -> Self { + Self::new() + } +} + impl TaskManager { pub fn new() -> Self { Self {