From bec6907dbee082d2bfb3623911563ad983f9818b Mon Sep 17 00:00:00 2001 From: vicrodh <103399283+vicrodh@users.noreply.github.com> Date: Mon, 2 Feb 2026 13:09:35 -0600 Subject: [PATCH] feat(blacklist): integrate filtering into search and radio commands Phase 2 (part 1) - Read-only integration: - Filter blacklisted artists from search_tracks, search_artists, search_all - Filter blacklisted artists from get_similar_artists - Filter most_popular item if blacklisted - Add blacklist filtering to all radio commands (artist/track/infinite/refill) - Use output-layer filtering for radio (engine internals unchanged) cc --- src-tauri/src/commands/radio.rs | 83 +++++++++++++++++++-- src-tauri/src/commands/search.rs | 122 +++++++++++++++++++++++++++++-- 2 files changed, 192 insertions(+), 13 deletions(-) diff --git a/src-tauri/src/commands/radio.rs b/src-tauri/src/commands/radio.rs index fe6840bee..0478e78be 100644 --- a/src-tauri/src/commands/radio.rs +++ b/src-tauri/src/commands/radio.rs @@ -5,7 +5,8 @@ use tauri::State; use tokio::task; -use crate::api::{QobuzClient, Track}; +use crate::api::Track; +use crate::artist_blacklist::BlacklistState; use crate::playback_context::{ContentSource, ContextType, PlaybackContext}; use crate::queue::QueueTrack; use crate::radio_engine::{BuildRadioOptions, RadioEngine, RadioPoolBuilder}; @@ -20,6 +21,7 @@ pub async fn create_artist_radio( artist_id: u64, artist_name: String, state: State<'_, AppState>, + blacklist_state: State<'_, BlacklistState>, ) -> Result { log::info!("[Radio] Creating artist radio for: {} (ID: {})", artist_name, artist_id); @@ -83,11 +85,24 @@ pub async fn create_artist_radio( .await .map_err(|e| format!("Track generation task failed: {}", e))??; - // Fetch full track details from Qobuz + // Fetch full track details from Qobuz, filtering blacklisted artists let mut tracks = Vec::new(); + let mut blacklist_skipped = 0; for track_id in track_ids { match client.get_track(track_id).await { Ok(track) => { + // Check if track's artist is blacklisted + if let Some(ref performer) = track.performer { + if blacklist_state.is_blacklisted(performer.id) { + log::debug!( + "[Radio] Skipping blacklisted artist track: {} - {}", + performer.name, + track.title + ); + blacklist_skipped += 1; + continue; + } + } tracks.push(track); } Err(e) => { @@ -96,6 +111,10 @@ pub async fn create_artist_radio( } } + if blacklist_skipped > 0 { + log::info!("[Radio] Skipped {} tracks from blacklisted artists", blacklist_skipped); + } + if tracks.is_empty() { return Err("Failed to generate any radio tracks".to_string()); } @@ -135,6 +154,7 @@ pub async fn create_track_radio( track_name: String, artist_id: u64, state: State<'_, AppState>, + blacklist_state: State<'_, BlacklistState>, ) -> Result { log::info!( "[Radio] Creating track radio for: {} (Track ID: {}, Artist ID: {})", @@ -204,11 +224,24 @@ pub async fn create_track_radio( .await .map_err(|e| format!("Track generation task failed: {}", e))??; - // Fetch full track details from Qobuz + // Fetch full track details from Qobuz, filtering blacklisted artists let mut tracks = Vec::new(); + let mut blacklist_skipped = 0; for track_id in track_ids { match client.get_track(track_id).await { Ok(track) => { + // Check if track's artist is blacklisted + if let Some(ref performer) = track.performer { + if blacklist_state.is_blacklisted(performer.id) { + log::debug!( + "[Radio] Skipping blacklisted artist track: {} - {}", + performer.name, + track.title + ); + blacklist_skipped += 1; + continue; + } + } tracks.push(track); } Err(e) => { @@ -217,6 +250,10 @@ pub async fn create_track_radio( } } + if blacklist_skipped > 0 { + log::info!("[Radio] Skipped {} tracks from blacklisted artists", blacklist_skipped); + } + if tracks.is_empty() { return Err("Failed to generate any radio tracks".to_string()); } @@ -301,6 +338,7 @@ fn track_to_queue_track(track: &Track) -> QueueTrack { pub async fn refill_radio_queue( session_id: String, state: State<'_, AppState>, + blacklist_state: State<'_, BlacklistState>, ) -> Result { log::info!("[Radio] Refilling queue for session: {}", session_id); @@ -337,12 +375,25 @@ pub async fn refill_radio_queue( return Ok(0); } - // Fetch full track details from Qobuz + // Fetch full track details from Qobuz, filtering blacklisted artists let client = state.client.lock().await; let mut tracks = Vec::new(); + let mut blacklist_skipped = 0; for track_id in track_ids.iter().take(20) { match client.get_track(*track_id).await { Ok(track) => { + // Check if track's artist is blacklisted + if let Some(ref performer) = track.performer { + if blacklist_state.is_blacklisted(performer.id) { + log::debug!( + "[Radio] Skipping blacklisted artist track during refill: {} - {}", + performer.name, + track.title + ); + blacklist_skipped += 1; + continue; + } + } tracks.push(track); } Err(e) => { @@ -351,6 +402,10 @@ pub async fn refill_radio_queue( } } + if blacklist_skipped > 0 { + log::info!("[Radio] Refill skipped {} tracks from blacklisted artists", blacklist_skipped); + } + let added_count = tracks.len() as u32; if tracks.is_empty() { @@ -391,6 +446,7 @@ pub fn get_queue_remaining(state: State<'_, AppState>) -> u32 { pub async fn create_infinite_radio( recent_track_ids: Vec, state: State<'_, AppState>, + blacklist_state: State<'_, BlacklistState>, ) -> Result { if recent_track_ids.is_empty() { return Err("No recent tracks provided for infinite radio".to_string()); @@ -468,11 +524,24 @@ pub async fn create_infinite_radio( .await .map_err(|e| format!("Track generation task failed: {}", e))??; - // Fetch full track details from Qobuz + // Fetch full track details from Qobuz, filtering blacklisted artists let mut tracks = Vec::new(); + let mut blacklist_skipped = 0; for track_id in track_ids { match client.get_track(track_id).await { Ok(track) => { + // Check if track's artist is blacklisted + if let Some(ref performer) = track.performer { + if blacklist_state.is_blacklisted(performer.id) { + log::debug!( + "[Radio] Skipping blacklisted artist track in infinite radio: {} - {}", + performer.name, + track.title + ); + blacklist_skipped += 1; + continue; + } + } tracks.push(track); } Err(e) => { @@ -481,6 +550,10 @@ pub async fn create_infinite_radio( } } + if blacklist_skipped > 0 { + log::info!("[Radio] Infinite radio skipped {} tracks from blacklisted artists", blacklist_skipped); + } + if tracks.is_empty() { return Err("Failed to generate any infinite radio tracks".to_string()); } diff --git a/src-tauri/src/commands/search.rs b/src-tauri/src/commands/search.rs index 013e56782..c5531e34d 100644 --- a/src-tauri/src/commands/search.rs +++ b/src-tauri/src/commands/search.rs @@ -4,6 +4,7 @@ use tauri::State; use crate::api::{endpoints, endpoints::paths, Album, Artist, ArtistAlbums, LabelDetail, Playlist, SearchResultsPage, Track, TracksContainer}; use crate::api_cache::ApiCacheState; +use crate::artist_blacklist::BlacklistState; use crate::AppState; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -48,12 +49,32 @@ pub async fn search_tracks( offset: Option, search_type: Option, state: State<'_, AppState>, + blacklist_state: State<'_, BlacklistState>, ) -> Result, String> { let client = state.client.lock().await; - client + let mut results = client .search_tracks(&query, limit.unwrap_or(20), offset.unwrap_or(0), search_type.as_deref()) .await - .map_err(|e| e.to_string()) + .map_err(|e| e.to_string())?; + + // Filter out tracks from blacklisted artists + let original_count = results.items.len(); + results.items.retain(|track| { + if let Some(ref performer) = track.performer { + !blacklist_state.is_blacklisted(performer.id) + } else { + true // Keep tracks without performer info + } + }); + + let filtered_count = original_count - results.items.len(); + if filtered_count > 0 { + log::debug!("[Blacklist] Filtered {} tracks from search results", filtered_count); + // Adjust total to reflect filtered count (approximate) + results.total = results.total.saturating_sub(filtered_count as u32); + } + + Ok(results) } #[tauri::command] @@ -63,18 +84,32 @@ pub async fn search_artists( offset: Option, search_type: Option, state: State<'_, AppState>, + blacklist_state: State<'_, BlacklistState>, ) -> Result, String> { let client = state.client.lock().await; - client + let mut results = client .search_artists(&query, limit.unwrap_or(20), offset.unwrap_or(0), search_type.as_deref()) .await - .map_err(|e| e.to_string()) + .map_err(|e| e.to_string())?; + + // Filter out blacklisted artists + let original_count = results.items.len(); + results.items.retain(|artist| !blacklist_state.is_blacklisted(artist.id)); + + let filtered_count = original_count - results.items.len(); + if filtered_count > 0 { + log::debug!("[Blacklist] Filtered {} artists from search results", filtered_count); + results.total = results.total.saturating_sub(filtered_count as u32); + } + + Ok(results) } #[tauri::command] pub async fn search_all( query: String, state: State<'_, AppState>, + blacklist_state: State<'_, BlacklistState>, ) -> Result { log::debug!("search_all called with query: {}", query); let client = state.client.lock().await; @@ -106,13 +141,13 @@ pub async fn search_all( .unwrap_or_else(|| SearchResultsPage { items: vec![], total: 0, offset: 0, limit: 30 }); // Parse tracks - let tracks: SearchResultsPage = response + let mut tracks: SearchResultsPage = response .get("tracks") .and_then(|v| serde_json::from_value(v.clone()).ok()) .unwrap_or_else(|| SearchResultsPage { items: vec![], total: 0, offset: 0, limit: 30 }); // Parse artists - let artists: SearchResultsPage = response + let mut artists: SearchResultsPage = response .get("artists") .and_then(|v| serde_json::from_value(v.clone()).ok()) .unwrap_or_else(|| SearchResultsPage { items: vec![], total: 0, offset: 0, limit: 30 }); @@ -152,6 +187,64 @@ pub async fn search_all( } }); + // Apply blacklist filtering + let mut filtered_tracks = 0; + let mut filtered_artists = 0; + + // Filter tracks from blacklisted artists + let original_track_count = tracks.items.len(); + tracks.items.retain(|track| { + if let Some(ref performer) = track.performer { + !blacklist_state.is_blacklisted(performer.id) + } else { + true + } + }); + filtered_tracks = original_track_count - tracks.items.len(); + if filtered_tracks > 0 { + tracks.total = tracks.total.saturating_sub(filtered_tracks as u32); + } + + // Filter blacklisted artists + let original_artist_count = artists.items.len(); + artists.items.retain(|artist| !blacklist_state.is_blacklisted(artist.id)); + filtered_artists = original_artist_count - artists.items.len(); + if filtered_artists > 0 { + artists.total = artists.total.saturating_sub(filtered_artists as u32); + } + + // Filter most_popular if it's a blacklisted item + let most_popular = most_popular.and_then(|mp| { + match &mp { + MostPopularItem::Artists(artist) => { + if blacklist_state.is_blacklisted(artist.id) { + log::debug!("[Blacklist] Filtered most_popular artist: {}", artist.name); + None + } else { + Some(mp) + } + } + MostPopularItem::Tracks(track) => { + if let Some(ref performer) = track.performer { + if blacklist_state.is_blacklisted(performer.id) { + log::debug!("[Blacklist] Filtered most_popular track: {}", track.title); + return None; + } + } + Some(mp) + } + MostPopularItem::Albums(_) => Some(mp), // Albums not filtered by artist blacklist + } + }); + + if filtered_tracks > 0 || filtered_artists > 0 { + log::debug!( + "[Blacklist] search_all filtered: {} tracks, {} artists", + filtered_tracks, + filtered_artists + ); + } + Ok(SearchAllResults { albums, tracks, @@ -365,6 +458,7 @@ pub async fn get_similar_artists( limit: Option, offset: Option, state: State<'_, AppState>, + blacklist_state: State<'_, BlacklistState>, ) -> Result, String> { log::info!( "Command: get_similar_artists {} limit={:?} offset={:?}", @@ -374,10 +468,22 @@ pub async fn get_similar_artists( ); let client = state.client.lock().await; - client + let mut results = client .get_similar_artists(artist_id, limit.unwrap_or(5), offset.unwrap_or(0)) .await - .map_err(|e| e.to_string()) + .map_err(|e| e.to_string())?; + + // Filter out blacklisted artists + let original_count = results.items.len(); + results.items.retain(|artist| !blacklist_state.is_blacklisted(artist.id)); + + let filtered_count = original_count - results.items.len(); + if filtered_count > 0 { + log::debug!("[Blacklist] Filtered {} similar artists", filtered_count); + results.total = results.total.saturating_sub(filtered_count as u32); + } + + Ok(results) } /// Get label detail with albums