use serde_json::Value;
use crate::model::{Clip, ClipRoot, HistoryEntry, MediaUrl, cdn_audio_url};
pub(crate) fn parse_clip(body: &[u8]) -> Option<Clip> {
let data: Value = serde_json::from_slice(body).ok()?;
let raw = unwrap_clip(&data);
let has_id = raw
.get("id")
.and_then(Value::as_str)
.is_some_and(|id| !id.is_empty());
has_id.then(|| map_clip(raw))
}
pub(crate) fn parse_songs_batch(body: &[u8]) -> Option<Vec<Clip>> {
let data: Value = serde_json::from_slice(body).ok()?;
let clips = data.get("clips")?.as_array()?;
Some(
clips
.iter()
.map(map_clip)
.filter(|clip| !clip.id.is_empty())
.collect(),
)
}
pub(super) fn unwrap_clip(value: &Value) -> &Value {
value
.get("clip")
.filter(|clip| clip.is_object())
.unwrap_or(value)
}
static NULL_METADATA: Value = Value::Null;
pub(crate) fn map_clip(raw: &Value) -> Clip {
let metadata: &Value = raw.get("metadata").unwrap_or(&NULL_METADATA);
let id = string(raw, "id");
let audio_url = cdn_audio_url(&string(raw, "audio_url"), &id);
let title = match raw.get("title") {
Some(Value::String(title)) => title.clone(),
_ => "Untitled".to_string(),
};
Clip {
id,
title,
audio_url,
media_urls: parse_media_urls(raw),
image_url: cdn(raw, "image_url"),
image_large_url: cdn(raw, "image_large_url"),
video_url: cdn(raw, "video_url"),
video_cover_url: cdn(raw, "video_cover_url"),
tags: string(metadata, "tags"),
duration: metadata
.get("duration")
.and_then(Value::as_f64)
.unwrap_or(0.0),
play_count: raw.get("play_count").and_then(Value::as_u64).unwrap_or(0),
status: raw
.get("status")
.and_then(Value::as_str)
.unwrap_or("unknown")
.to_string(),
created_at: string(raw, "created_at"),
display_name: string_or(raw, "display_name", "user_display_name"),
handle: string_or(raw, "handle", "user_handle"),
user_id: string(raw, "user_id"),
batch_index: raw.get("batch_index").and_then(Value::as_i64),
avatar_image_url: string_or(raw, "avatar_image_url", "user_avatar_image_url"),
is_liked: bool_field(raw, "is_liked"),
is_trashed: bool_field(raw, "is_trashed"),
has_vocal: bool_field(metadata, "has_vocal"),
has_stem: bool_field(metadata, "has_stem"),
stem_from_id: string(metadata, "stem_from_id"),
stem_task: string(metadata, "stem_task"),
stem_type_id: int_tolerant(metadata, "stem_type_id"),
stem_type_group_name: string(metadata, "stem_type_group_name"),
clip_type: string(metadata, "type"),
prompt: string(metadata, "prompt"),
gpt_description_prompt: string(metadata, "gpt_description_prompt"),
lyrics: string(raw, "lyrics"),
model_name: string(raw, "model_name"),
major_model_version: string(raw, "major_model_version"),
edited_clip_id: string(metadata, "edited_clip_id"),
task: string(metadata, "task"),
is_remix: bool_field(metadata, "is_remix"),
cover_clip_id: string(metadata, "cover_clip_id"),
upsample_clip_id: string(metadata, "upsample_clip_id"),
remaster_clip_id: string(metadata, "remaster_clip_id"),
speed_clip_id: string(metadata, "speed_clip_id"),
override_history_clip_id: string(metadata, "override_history_clip_id"),
override_future_clip_id: string(metadata, "override_future_clip_id"),
history: history_entries(metadata, "history"),
concat_history: history_entries(metadata, "concat_history"),
clip_roots: parse_clip_roots(raw),
clip_attribution_type: raw
.get("clip_roots")
.map(|roots| string(roots, "clip_attribution_type"))
.unwrap_or_default(),
}
}
fn string(value: &Value, key: &str) -> String {
value
.get(key)
.and_then(Value::as_str)
.unwrap_or("")
.to_string()
}
fn string_or(value: &Value, primary: &str, fallback: &str) -> String {
let first = string(value, primary);
if first.is_empty() {
string(value, fallback)
} else {
first
}
}
fn bool_field(value: &Value, key: &str) -> bool {
value.get(key).and_then(Value::as_bool).unwrap_or(false)
}
fn int_tolerant(value: &Value, key: &str) -> Option<i64> {
let field = value.get(key)?;
field.as_i64().or_else(|| {
field
.as_f64()
.filter(|number| number.fract() == 0.0)
.map(|number| number as i64)
})
}
fn cdn(value: &Value, key: &str) -> String {
let url = string(value, key);
if url.contains("cdn2.suno.ai") {
url.replace("cdn2.suno.ai", "cdn1.suno.ai")
} else {
url
}
}
fn parse_clip_roots(raw: &Value) -> Vec<ClipRoot> {
let Some(Value::Array(items)) = raw.get("clip_roots").and_then(|roots| roots.get("clips"))
else {
return Vec::new();
};
items
.iter()
.map(|item| ClipRoot {
id: string(item, "id"),
title: string(item, "title"),
image_url: cdn(item, "image_url"),
is_public: bool_field(item, "is_public"),
display_name: string(item, "user_display_name"),
handle: string(item, "user_handle"),
avatar_image_url: string(item, "user_avatar_image_url"),
})
.collect()
}
fn parse_media_urls(raw: &Value) -> Vec<MediaUrl> {
let Some(Value::Array(items)) = raw.get("media_urls") else {
return Vec::new();
};
items
.iter()
.map(|item| MediaUrl {
url: string(item, "url"),
content_type: string(item, "content_type"),
delivery: string(item, "delivery"),
encoding: string(item, "encoding"),
})
.collect()
}
fn history_entries(value: &Value, key: &str) -> Vec<HistoryEntry> {
let Some(Value::Array(items)) = value.get(key) else {
return Vec::new();
};
items
.iter()
.map(|item| match item {
Value::String(id) => HistoryEntry {
id: id.clone(),
..HistoryEntry::default()
},
_ => HistoryEntry {
id: string(item, "id"),
infill: bool_field(item, "infill"),
continue_at: item.get("continue_at").and_then(Value::as_f64),
infill_start_s: item.get("infill_start_s").and_then(Value::as_f64),
infill_end_s: item.get("infill_end_s").and_then(Value::as_f64),
infill_lyrics: string(item, "infill_lyrics"),
},
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn audiopipe_url_is_rewritten_to_cdn() {
let raw =
serde_json::json!({"id": "x", "audio_url": "https://audiopipe.suno.ai/?item_id=x"});
assert_eq!(map_clip(&raw).audio_url, "https://cdn1.suno.ai/x.mp3");
}
#[test]
fn parse_clip_accepts_bare_and_wrapped_shapes() {
let bare = serde_json::json!({"id": "z", "title": "Zed"}).to_string();
assert_eq!(parse_clip(bare.as_bytes()).unwrap().id, "z");
let wrapped = serde_json::json!({"clip": {"id": "w", "title": "Wai"}}).to_string();
assert_eq!(parse_clip(wrapped.as_bytes()).unwrap().id, "w");
let missing = serde_json::json!({"detail": "not found"}).to_string();
assert!(parse_clip(missing.as_bytes()).is_none());
}
#[test]
fn from_json_reads_media_urls_user_id_and_batch_index() {
let raw = serde_json::json!({
"id": "clip-1",
"user_id": "owner-9",
"batch_index": 23,
"media_urls": [
{
"url": "https://media/clip-1.m4a",
"content_type": "m4a-opus",
"delivery": "progressive",
"encoding": "1.0.0"
},
{
"url": "https://cdn1.suno.ai/clip-1.mp3",
"content_type": "mp3",
"delivery": "progressive"
}
]
});
let clip = map_clip(&raw);
assert_eq!(clip.user_id, "owner-9");
assert_eq!(clip.batch_index, Some(23));
assert_eq!(clip.media_urls.len(), 2);
assert_eq!(clip.media_urls[0].content_type, "m4a-opus");
assert_eq!(clip.media_urls[0].encoding, "1.0.0");
assert_eq!(clip.media_urls[1].content_type, "mp3");
assert_eq!(clip.media_urls[1].encoding, "");
assert_eq!(clip.mp3_url(), "https://cdn1.suno.ai/clip-1.mp3");
}
#[test]
fn from_json_defaults_media_urls_user_id_and_batch_index_when_absent() {
let clip = map_clip(&serde_json::json!({"id": "clip-1"}));
assert!(clip.media_urls.is_empty());
assert_eq!(clip.user_id, "");
assert_eq!(clip.batch_index, None);
let odd = map_clip(&serde_json::json!({"id": "x", "media_urls": "nope"}));
assert!(odd.media_urls.is_empty());
}
#[test]
fn from_json_parses_nested_clip_roots_and_owner_identity() {
let raw = serde_json::json!({
"id": "00000000-0000-4000-8000-000000000017",
"title": "Track 1",
"user_id": "00000000-0000-4000-8000-000000000019",
"display_name": "Example Artist 4",
"handle": "example-artist-1",
"avatar_image_url": "https://cdn1.suno.ai/avatar.jpg",
"batch_index": 1,
"clip_roots": {
"clips": [
{
"id": "00000000-0000-4000-8000-000000000020",
"title": "Track 2",
"image_url": "https://cdn2.suno.ai/image_00000000-0000-4000-8000-000000000020.jpeg",
"is_public": false,
"user_display_name": "Example Artist 4",
"user_handle": "example-artist-1",
"user_avatar_image_url": "https://cdn1.suno.ai/avatar.jpg"
}
],
"clip_attribution_type": "remix"
}
});
let clip = map_clip(&raw);
assert_eq!(clip.display_name, "Example Artist 4");
assert_eq!(clip.handle, "example-artist-1");
assert_eq!(clip.avatar_image_url, "https://cdn1.suno.ai/avatar.jpg");
assert_eq!(clip.clip_attribution_type, "remix");
assert_eq!(clip.clip_roots.len(), 1);
let root = &clip.clip_roots[0];
assert_eq!(root.id, "00000000-0000-4000-8000-000000000020");
assert_eq!(root.title, "Track 2");
assert!(!root.is_public);
assert_eq!(root.display_name, "Example Artist 4");
assert_eq!(root.handle, "example-artist-1");
assert_eq!(root.avatar_image_url, "https://cdn1.suno.ai/avatar.jpg");
assert_eq!(
root.image_url,
"https://cdn1.suno.ai/image_00000000-0000-4000-8000-000000000020.jpeg"
);
}
#[test]
fn from_json_reads_user_prefixed_identity_on_a_parent_shape() {
let raw = serde_json::json!({
"id": "00000000-0000-4000-8000-000000000020",
"title": "Track 2",
"is_public": false,
"user_display_name": "Example Artist 4",
"user_handle": "example-artist-1",
"user_avatar_image_url": "https://cdn1.suno.ai/avatar.jpg"
});
let clip = map_clip(&raw);
assert_eq!(clip.display_name, "Example Artist 4");
assert_eq!(clip.handle, "example-artist-1");
assert_eq!(clip.avatar_image_url, "https://cdn1.suno.ai/avatar.jpg");
}
#[test]
fn from_json_prefers_bare_identity_over_user_prefixed() {
let raw = serde_json::json!({
"id": "x",
"display_name": "Bare Name",
"user_display_name": "Prefixed Name",
"handle": "bare-handle",
"user_handle": "prefixed-handle"
});
let clip = map_clip(&raw);
assert_eq!(clip.display_name, "Bare Name");
assert_eq!(clip.handle, "bare-handle");
}
#[test]
fn from_json_defaults_clip_roots_when_absent_or_malformed() {
let none = map_clip(&serde_json::json!({"id": "x"}));
assert!(none.clip_roots.is_empty());
assert_eq!(none.clip_attribution_type, "");
let no_clips = map_clip(&serde_json::json!({
"id": "x",
"clip_roots": {"clip_attribution_type": "remix"}
}));
assert!(no_clips.clip_roots.is_empty());
assert_eq!(no_clips.clip_attribution_type, "remix");
let odd = map_clip(&serde_json::json!({
"id": "x",
"clip_roots": {"clips": "nope"}
}));
assert!(odd.clip_roots.is_empty());
let array_shape = map_clip(&serde_json::json!({
"id": "x",
"clip_roots": [{"id": "r"}]
}));
assert!(array_shape.clip_roots.is_empty());
assert_eq!(array_shape.clip_attribution_type, "");
}
#[test]
fn from_json_reads_multiple_clip_roots_in_order() {
let raw = serde_json::json!({
"id": "x",
"clip_roots": {
"clips": [
{"id": "root-a", "title": "A"},
{"id": "root-b", "title": "B"}
],
"clip_attribution_type": "remix"
}
});
let clip = map_clip(&raw);
assert_eq!(clip.clip_roots.len(), 2);
assert_eq!(clip.clip_roots[0].id, "root-a");
assert_eq!(clip.clip_roots[1].id, "root-b");
}
#[test]
fn from_json_parses_all_lineage_metadata_fields() {
let raw = serde_json::json!({
"id": "self",
"title": "Lineage",
"is_trashed": true,
"metadata": {
"task": "extend",
"is_remix": true,
"cover_clip_id": "cover-1",
"upsample_clip_id": "upsample-2",
"remaster_clip_id": "remaster-3",
"speed_clip_id": "speed-4",
"override_history_clip_id": "ovh-5",
"override_future_clip_id": "ovf-6",
"history": [
{
"infill": false,
"id": "0a3c311a-hist",
"source": "ios",
"type": "gen",
"continue_at": 115.35
},
{
"infill": true,
"id": "infill-hist",
"source": "web",
"type": "gen",
"infill_start_s": 12.0,
"infill_end_s": 28.5,
"infill_lyrics": "new words here"
}
],
"concat_history": [
{"infill": false, "id": "122d0d15-base", "continue_at": 131.5},
{"id": "cf7cb30f-part"}
]
}
});
let clip = map_clip(&raw);
assert_eq!(clip.task, "extend");
assert!(clip.is_remix);
assert!(clip.is_trashed);
assert_eq!(clip.cover_clip_id, "cover-1");
assert_eq!(clip.upsample_clip_id, "upsample-2");
assert_eq!(clip.remaster_clip_id, "remaster-3");
assert_eq!(clip.speed_clip_id, "speed-4");
assert_eq!(clip.override_history_clip_id, "ovh-5");
assert_eq!(clip.override_future_clip_id, "ovf-6");
assert_eq!(
clip.history,
vec![
HistoryEntry {
id: "0a3c311a-hist".to_owned(),
infill: false,
continue_at: Some(115.35),
..Default::default()
},
HistoryEntry {
id: "infill-hist".to_owned(),
infill: true,
infill_start_s: Some(12.0),
infill_end_s: Some(28.5),
infill_lyrics: "new words here".to_owned(),
..Default::default()
},
]
);
assert_eq!(
clip.concat_history,
vec![
HistoryEntry {
id: "122d0d15-base".to_owned(),
continue_at: Some(131.5),
..Default::default()
},
HistoryEntry {
id: "cf7cb30f-part".to_owned(),
..Default::default()
},
]
);
}
#[test]
fn bare_string_history_element_parses_to_id_only_entry() {
let raw = serde_json::json!({
"id": "self",
"metadata": {"history": ["m_bare-id-verbatim"]}
});
let clip = map_clip(&raw);
assert_eq!(
clip.history,
vec![HistoryEntry {
id: "m_bare-id-verbatim".to_owned(),
..Default::default()
}]
);
}
#[test]
fn play_count_parses_top_level_and_defaults_to_zero() {
let with_count = serde_json::json!({"id": "x", "play_count": 4242});
assert_eq!(map_clip(&with_count).play_count, 4242);
assert_eq!(map_clip(&serde_json::json!({"id": "x"})).play_count, 0);
assert_eq!(
map_clip(&serde_json::json!({"id": "x", "play_count": null})).play_count,
0
);
}
#[test]
fn has_stem_parses_from_metadata_and_defaults_to_false() {
let with_stem = serde_json::json!({"id": "x", "metadata": {"has_stem": true}});
assert!(map_clip(&with_stem).has_stem);
assert!(!map_clip(&serde_json::json!({"id": "x"})).has_stem);
assert!(
!map_clip(&serde_json::json!({"id": "x", "metadata": {"has_stem": null}})).has_stem
);
assert!(
!map_clip(&serde_json::json!({"id": "x", "metadata": {"has_stem": false}})).has_stem
);
}
#[test]
fn stem_lineage_quartet_parses_from_metadata_float_tolerant() {
let raw = serde_json::json!({
"id": "stem-child",
"metadata": {
"has_stem": false,
"stem_from_id": "source-074",
"stem_task": "twelve",
"stem_type_id": 91.0,
"stem_type_group_name": "Backing_Vocals"
}
});
let clip = map_clip(&raw);
assert_eq!(clip.stem_from_id, "source-074");
assert_eq!(clip.stem_task, "twelve");
assert_eq!(clip.stem_type_id, Some(91));
assert_eq!(clip.stem_type_group_name, "Backing_Vocals");
assert!(!clip.has_stem);
let as_int = serde_json::json!({"id": "x", "metadata": {"stem_type_id": 91}});
assert_eq!(map_clip(&as_int).stem_type_id, Some(91));
let bare = map_clip(&serde_json::json!({"id": "x"}));
assert_eq!(bare.stem_type_id, None);
assert_eq!(bare.stem_from_id, "");
assert_eq!(bare.stem_task, "");
assert_eq!(bare.stem_type_group_name, "");
for odd in [
serde_json::json!({"id": "x", "metadata": {"stem_type_id": null}}),
serde_json::json!({"id": "x", "metadata": {"stem_type_id": 91.5}}),
serde_json::json!({"id": "x", "metadata": {"stem_type_id": "91"}}),
] {
assert_eq!(map_clip(&odd).stem_type_id, None);
}
}
#[test]
fn absent_or_null_lineage_metadata_defaults_to_empty() {
let raw = serde_json::json!({
"id": "self",
"metadata": {
"cover_clip_id": null,
"is_remix": null,
"history": null
}
});
let clip = map_clip(&raw);
assert_eq!(clip.task, "");
assert!(!clip.is_remix);
assert!(!clip.is_trashed);
assert_eq!(clip.cover_clip_id, "");
assert_eq!(clip.upsample_clip_id, "");
assert_eq!(clip.remaster_clip_id, "");
assert_eq!(clip.speed_clip_id, "");
assert_eq!(clip.override_history_clip_id, "");
assert_eq!(clip.override_future_clip_id, "");
assert!(clip.history.is_empty());
assert!(clip.concat_history.is_empty());
}
}