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,221 @@
|
||||
//! Go/no-go experiment for merging the identity pass's two release-group
|
||||
//! queries into one (theory 5 of the 2026-07-22 load audit).
|
||||
//!
|
||||
//! Current scheme (2 queries):
|
||||
//! STUDIO: arid:X AND primarytype:album AND -secondarytype:* AND status:official (1 page)
|
||||
//! HINTS: arid:X AND primarytype:album AND secondarytype:* (≤5 pages)
|
||||
//!
|
||||
//! Candidate merged scheme (1 query):
|
||||
//! ALL-OFFICIAL: arid:X AND primarytype:album AND status:official (≤5 pages)
|
||||
//! → client-side split: no secondary types ⇒ studio, some ⇒ hint.
|
||||
//!
|
||||
//! The merge is adoptable ONLY if, for every probed artist, the split
|
||||
//! reproduces BOTH current sets exactly. The known hazard: the hints query
|
||||
//! has no status filter today, so status:official could drop bootleg-only
|
||||
//! compilation groups from the hint set — a classification change.
|
||||
//!
|
||||
//! Usage: cargo run -p metadata --example mb_query_merge_probe -- "Artist" ...
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::time::Duration;
|
||||
|
||||
const UA: &str = "SoulSeekPlus/0.1 (dev build)";
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let artists: Vec<String> = std::env::args().skip(1).collect();
|
||||
if artists.is_empty() {
|
||||
eprintln!("usage: mb_query_merge_probe -- \"Artist\" ...");
|
||||
return;
|
||||
}
|
||||
let http = reqwest::Client::builder()
|
||||
.user_agent(UA)
|
||||
.timeout(Duration::from_secs(15))
|
||||
.build()
|
||||
.expect("client");
|
||||
let meta = metadata::MetadataClient::new();
|
||||
|
||||
let mut all_ok = true;
|
||||
for artist in &artists {
|
||||
let search = meta.catalog_search(artist).await;
|
||||
let Some(deezer_id) = search
|
||||
.artists
|
||||
.iter()
|
||||
.find(|a| norm(&a.name) == norm(artist))
|
||||
.map(|a| a.id)
|
||||
else {
|
||||
println!("== {artist}: no exact Deezer artist, skipped ==");
|
||||
continue;
|
||||
};
|
||||
let Some(mbid) = linked_mbid(&http, deezer_id).await else {
|
||||
println!("== {artist}: no linked MBID, skipped ==");
|
||||
continue;
|
||||
};
|
||||
|
||||
let studio_q =
|
||||
format!("arid:{mbid} AND primarytype:album AND -secondarytype:* AND status:official");
|
||||
let hints_q = format!("arid:{mbid} AND primarytype:album AND secondarytype:*");
|
||||
let merged_q = format!("arid:{mbid} AND primarytype:album AND status:official");
|
||||
|
||||
// Current studio behavior is a SINGLE page.
|
||||
let studio_now: BTreeSet<String> = fetch_groups(&http, &studio_q, 1)
|
||||
.await
|
||||
.into_iter()
|
||||
.filter(|g| g.secondary.is_empty())
|
||||
.map(|g| g.title)
|
||||
.collect();
|
||||
let hints_now: BTreeSet<String> = fetch_groups(&http, &hints_q, 5)
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|g| format!("{} :: {:?}", g.title, g.secondary))
|
||||
.collect();
|
||||
|
||||
let merged = fetch_groups(&http, &merged_q, 5).await;
|
||||
let studio_merged: BTreeSet<String> = merged
|
||||
.iter()
|
||||
.filter(|g| g.secondary.is_empty())
|
||||
.map(|g| g.title.clone())
|
||||
.collect();
|
||||
let hints_merged: BTreeSet<String> = merged
|
||||
.iter()
|
||||
.filter(|g| !g.secondary.is_empty())
|
||||
.map(|g| format!("{} :: {:?}", g.title, g.secondary))
|
||||
.collect();
|
||||
|
||||
let studio_ok = studio_now == studio_merged;
|
||||
let hints_ok = hints_now == hints_merged;
|
||||
all_ok &= studio_ok && hints_ok;
|
||||
println!(
|
||||
"== {artist} == studio {}/{} {} hints {}/{} {}",
|
||||
studio_now.len(),
|
||||
studio_merged.len(),
|
||||
if studio_ok { "IDENTICAL" } else { "DIFFERS" },
|
||||
hints_now.len(),
|
||||
hints_merged.len(),
|
||||
if hints_ok { "IDENTICAL" } else { "DIFFERS" },
|
||||
);
|
||||
if !studio_ok {
|
||||
for extra in studio_merged.difference(&studio_now) {
|
||||
println!(" studio only-in-merged: {extra}");
|
||||
}
|
||||
for missing in studio_now.difference(&studio_merged) {
|
||||
println!(" studio missing-from-merged: {missing}");
|
||||
}
|
||||
}
|
||||
if !hints_ok {
|
||||
for extra in hints_merged.difference(&hints_now) {
|
||||
println!(" hint only-in-merged: {extra}");
|
||||
}
|
||||
for missing in hints_now.difference(&hints_merged) {
|
||||
println!(" hint missing-from-merged: {missing}");
|
||||
}
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"\nVERDICT: {}",
|
||||
if all_ok {
|
||||
"merged query reproduced both sets exactly for every probed artist"
|
||||
} else {
|
||||
"NOT identical — do not merge"
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
struct Group {
|
||||
title: String,
|
||||
secondary: Vec<String>,
|
||||
}
|
||||
|
||||
async fn fetch_groups(http: &reqwest::Client, query: &str, max_pages: usize) -> Vec<Group> {
|
||||
let mut out = Vec::new();
|
||||
for page in 0..max_pages {
|
||||
throttle().await;
|
||||
let url = format!(
|
||||
"https://musicbrainz.org/ws/2/release-group/?query={}&limit=100&offset={}&fmt=json",
|
||||
urlencoding(query),
|
||||
page * 100
|
||||
);
|
||||
let Ok(resp) = http.get(&url).send().await else {
|
||||
break;
|
||||
};
|
||||
let Ok(body) = resp.json::<serde_json::Value>().await else {
|
||||
break;
|
||||
};
|
||||
let Some(groups) = body.get("release-groups").and_then(|v| v.as_array()) else {
|
||||
break;
|
||||
};
|
||||
if groups.is_empty() {
|
||||
break;
|
||||
}
|
||||
let page_len = groups.len();
|
||||
for g in groups {
|
||||
let Some(title) = g.get("title").and_then(|v| v.as_str()) else {
|
||||
continue;
|
||||
};
|
||||
let secondary = g
|
||||
.get("secondary-types")
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|a| {
|
||||
let mut s: Vec<String> = a
|
||||
.iter()
|
||||
.filter_map(|v| v.as_str().map(String::from))
|
||||
.collect();
|
||||
s.sort();
|
||||
s
|
||||
})
|
||||
.unwrap_or_default();
|
||||
out.push(Group {
|
||||
title: title.to_string(),
|
||||
secondary,
|
||||
});
|
||||
}
|
||||
if page_len < 100 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
async fn linked_mbid(http: &reqwest::Client, deezer_id: u64) -> Option<String> {
|
||||
throttle().await;
|
||||
let resource = urlencoding(&format!("https://www.deezer.com/artist/{deezer_id}"));
|
||||
let url =
|
||||
format!("https://musicbrainz.org/ws/2/url?resource={resource}&inc=artist-rels&fmt=json");
|
||||
let body = http
|
||||
.get(&url)
|
||||
.send()
|
||||
.await
|
||||
.ok()?
|
||||
.json::<serde_json::Value>()
|
||||
.await
|
||||
.ok()?;
|
||||
body.get("relations")?.as_array()?.iter().find_map(|r| {
|
||||
r.pointer("/artist/id")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(String::from)
|
||||
})
|
||||
}
|
||||
|
||||
async fn throttle() {
|
||||
tokio::time::sleep(Duration::from_millis(1100)).await;
|
||||
}
|
||||
|
||||
fn urlencoding(s: &str) -> String {
|
||||
let mut out = String::new();
|
||||
for b in s.bytes() {
|
||||
match b {
|
||||
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
|
||||
out.push(b as char)
|
||||
}
|
||||
_ => out.push_str(&format!("%{b:02X}")),
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn norm(s: &str) -> String {
|
||||
s.to_lowercase()
|
||||
.chars()
|
||||
.filter(|c| c.is_alphanumeric())
|
||||
.collect()
|
||||
}
|
||||
Reference in new issue
Block a user