//! Live diagnostic for the production artist-art resolver. //! //! Usage: //! cargo run -p metadata --example hero_source_probe -- [output-dir] [artist ...] //! cargo run -p metadata --example hero_source_probe -- [output-dir] --extended //! cargo run -p metadata --example hero_source_probe -- [output-dir] --bing-only [artist ...] //! //! With no artists, the five core sourcing fixtures are used. `--extended` //! adds ten lesser-known stress fixtures. Passing `-` as the output directory //! prints URLs without saving the image bytes. use metadata::{CatalogArtist, MetadataClient}; use std::path::PathBuf; const DEFAULT_ARTISTS: [&str; 5] = [ "Camping in Alaska", "Partiboi69", "Hermeth", "Billy Cobb", "Fishmans", ]; const EXTENDED_ARTISTS: [&str; 10] = [ "Wednesday", "Equipment", "Hana Vu", "Jane Remover", "DJ Sabrina The Teenage DJ", "Nikki Nair", "MSPAINT", "Horse Jumper of Love", "They Are Gutting a Body of Water", "Sword II", ]; #[tokio::main] async fn main() { let mut args = std::env::args().skip(1); let output = args.next().filter(|value| value != "-").map(PathBuf::from); let mut requested: Vec = args.collect(); let bing_only = requested.iter().any(|value| value == "--bing-only"); requested.retain(|value| value != "--bing-only"); let artists: Vec = if requested.is_empty() { DEFAULT_ARTISTS .iter() .map(|name| name.to_string()) .collect() } else if requested.len() == 1 && requested[0] == "--extended" { DEFAULT_ARTISTS .iter() .chain(EXTENDED_ARTISTS.iter()) .map(|name| name.to_string()) .collect() } else { requested }; if let Some(output) = output.as_ref() { if let Err(error) = std::fs::create_dir_all(output) { eprintln!("could not create {}: {error}", output.display()); std::process::exit(2); } } let client = MetadataClient::new().with_hero_diagnostics(); for requested in artists { // Bing-only diagnostics do not need a Deezer id. Keeping that path // independent means catalog rate limits cannot turn a scraper probe // into an immediate, misleading NOT_FOUND for every requested name. let (artist_id, artist_name, fans) = if bing_only { (None, requested, 0) } else { let search = client.catalog_search(&requested).await; let Some(artist) = best_exact_artist(&search.artists, &requested) else { println!("ARTIST\t{requested}\tNOT_FOUND"); continue; }; ( Some(artist.id), artist.name.clone(), artist.fans.unwrap_or(0), ) }; println!( "ARTIST\t{}\t{}\t{}", artist_id .map(|id| id.to_string()) .unwrap_or_else(|| "BING_ONLY".to_string()), artist_name, fans, ); let banners = if let Some(artist_id) = artist_id { let Some(art) = client.catalog_artist_art(artist_id).await else { println!("ERROR\t{}\tartist art unavailable", artist_name); continue; }; art.banners } else { client.debug_bing_hero_banners(&artist_name).await }; if banners.is_empty() { println!("BANNER\t{}\tNONE", artist_name); continue; } for (index, url) in banners.iter().enumerate() { println!("BANNER\t{}\t{}\t{}", artist_name, index + 1, url); let Some(output) = output.as_ref() else { continue; }; let Some(bytes) = client.fetch_image_bytes(url).await else { println!( "SAVE_ERROR\t{}\t{}\tdownload failed", artist_name, index + 1 ); continue; }; let extension = match image::guess_format(&bytes) { Ok(image::ImageFormat::Png) => "png", Ok(image::ImageFormat::WebP) => "webp", _ => "jpg", }; let path = output.join(format!( "{}-{}.{}", file_stem(&artist_name), index + 1, extension )); match std::fs::write(&path, bytes) { Ok(()) => println!("SAVED\t{}", path.display()), Err(error) => println!("SAVE_ERROR\t{}\t{}", path.display(), error), } } } } fn best_exact_artist<'a>( artists: &'a [CatalogArtist], requested: &str, ) -> Option<&'a CatalogArtist> { artists .iter() .filter(|artist| artist.name.trim().eq_ignore_ascii_case(requested.trim())) .max_by_key(|artist| artist.fans.unwrap_or(0)) } fn file_stem(name: &str) -> String { name.to_lowercase() .chars() .map(|character| { if character.is_ascii_alphanumeric() { character } else { '-' } }) .collect::() .split('-') .filter(|part| !part.is_empty()) .collect::>() .join("-") }