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
This commit is contained in:
vicrodh committed 2026-02-02 13:09:35 -06:00
1 parent 26c5a1b0a3
commit bec6907dbe
2 files changed
+192 -13

No files matched your search

+78 -5
View File
@@ -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<String, String> {
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<String, String> {
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<u32, String> {
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<u64>,
state: State<'_, AppState>,
blacklist_state: State<'_, BlacklistState>,
) -> Result<String, String> {
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());
}
+114 -8
View File
@@ -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<u32>,
search_type: Option<String>,
state: State<'_, AppState>,
blacklist_state: State<'_, BlacklistState>,
) -> Result<SearchResultsPage<Track>, 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<u32>,
search_type: Option<String>,
state: State<'_, AppState>,
blacklist_state: State<'_, BlacklistState>,
) -> Result<SearchResultsPage<Artist>, 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<SearchAllResults, String> {
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<Track> = response
let mut tracks: SearchResultsPage<Track> = 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<Artist> = response
let mut artists: SearchResultsPage<Artist> = 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<u32>,
offset: Option<u32>,
state: State<'_, AppState>,
blacklist_state: State<'_, BlacklistState>,
) -> Result<SearchResultsPage<Artist>, 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