forked from Keelrleek/SoulStream
The previous .git directory was found gutted (only info/ remained), so history is unrecoverable locally. This commit restores version control with the tree exactly as it stands: the working app, the preview-tracks launcher on the artist hero, the scrollable-track-previews design bundle (docs + approved mockups), and the Phase 0 validation artifacts.
163 lines
5.3 KiB
Rust
163 lines
5.3 KiB
Rust
//! 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<String> = args.collect();
|
|
let bing_only = requested.iter().any(|value| value == "--bing-only");
|
|
requested.retain(|value| value != "--bing-only");
|
|
let artists: Vec<String> = 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::<String>()
|
|
.split('-')
|
|
.filter(|part| !part.is_empty())
|
|
.collect::<Vec<_>>()
|
|
.join("-")
|
|
}
|