forked from Keelrleek/SoulStream
Baseline: full tree snapshot after git repository loss
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.
This commit is contained in:
commit
a667b4b28e
161 files changed
+53170
No files matched your search
@@ -0,0 +1,162 @@
|
||||
//! 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("-")
|
||||
}
|
||||
Reference in new issue
Block a user