use serde_json::Value;
use crate::model::{Stem, stem_label};
use super::map_clip;
pub(crate) fn parse_stems_page(body: &[u8]) -> Vec<Stem> {
let Ok(data) = serde_json::from_slice::<Value>(body) else {
return Vec::new();
};
let items = if let Some(array) = data.as_array() {
array.as_slice()
} else {
data.get("stems")
.and_then(Value::as_array)
.map(Vec::as_slice)
.unwrap_or(&[])
};
items
.iter()
.map(parse_stem)
.filter(|stem| !stem.id.is_empty() && !stem.url.is_empty())
.collect()
}
fn parse_stem(raw: &Value) -> Stem {
let clip = map_clip(raw);
Stem {
id: clip.id.clone(),
label: stem_label(&clip),
url: clip.mp3_url(),
}
}
pub(crate) fn parse_stem_page_count(body: &[u8]) -> u32 {
serde_json::from_slice::<Value>(body)
.ok()
.and_then(|data| data.get("pages").and_then(Value::as_u64))
.and_then(|pages| u32::try_from(pages).ok())
.unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::*;
fn stem_page(stems: &[(&str, &str, &str)]) -> String {
let entries: Vec<Value> = stems
.iter()
.map(|(id, label, url)| {
serde_json::json!({
"id": id,
"title": format!("My Song ({label})"),
"status": "complete",
"audio_url": url,
})
})
.collect();
serde_json::json!({ "stems": entries }).to_string()
}
#[test]
fn parse_stems_page_maps_full_clips_and_skips_idless() {
let page = stem_page(&[("x", "Backing Vocals", "https://cdn1.suno.ai/x.mp3")]);
let stems = parse_stems_page(page.as_bytes());
assert_eq!(stems.len(), 1);
assert_eq!(stems[0].id, "x");
assert_eq!(stems[0].label, "Backing Vocals");
assert_eq!(stems[0].url, "https://cdn1.suno.ai/x.mp3");
let no_id = br#"{"stems": [{"title": "Ghost (Vocals)", "audio_url": "https://cdn1.suno.ai/g.mp3"}]}"#;
assert!(parse_stems_page(no_id).is_empty());
let no_url = br#"{"stems": [{"id": "y", "title": "Song (Bass)"}]}"#;
let recovered = parse_stems_page(no_url);
assert_eq!(recovered.len(), 1);
assert_eq!(recovered[0].url, "https://cdn1.suno.ai/y.mp3");
assert!(parse_stems_page(b"not json").is_empty());
}
#[test]
fn parse_stem_page_count_reads_pages_field() {
assert_eq!(parse_stem_page_count(br#"{"pages": 12}"#), 12);
assert_eq!(parse_stem_page_count(br#"{"pages": 0}"#), 0);
assert_eq!(parse_stem_page_count(br#"{}"#), 0);
assert_eq!(parse_stem_page_count(br#"{"pages": -1}"#), 0);
assert_eq!(parse_stem_page_count(b"not json"), 0);
}
}