1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
use super::SunoClient;
use super::types::{Clip, GenerateRequest, GenerateResponse};
use crate::core::CliError;
impl SunoClient {
/// Submit a music generation request (custom mode or inspiration mode).
/// Posts only to the current `/api/generate/v2-web/` route. The older
/// `/api/generate/v2/` route returned `Token validation failed` after Suno
/// migrated creates to `v2-web` server-side in the April 2026 capture.
/// Wrapped in `with_auth_retry` so a single stale-JWT failure recovers
/// transparently via Clerk refresh.
pub async fn generate(&self, req: &GenerateRequest) -> Result<Vec<Clip>, CliError> {
self.with_auth_retry(|| async {
let resp = self.post("/api/generate/v2-web/").json(req).send().await?;
let resp = self.check_response(resp).await?;
let result: GenerateResponse = resp.json().await?;
Ok(result.clips)
})
.await
}
/// Fetch clips by IDs. Batches in pairs because Suno's feed endpoint can
/// drop results when queried with larger mixed batches.
/// Each chunk is wrapped in `with_auth_retry` so explicit wait/status
/// flows survive Suno's JWT staleness window mid-generation.
pub async fn get_clips(&self, ids: &[String]) -> Result<Vec<Clip>, CliError> {
let mut all_clips = Vec::new();
for chunk in ids.chunks(2) {
let ids_param = chunk.join(",");
let path = format!("/api/feed/?ids={ids_param}");
let clips: Vec<Clip> = self
.with_auth_retry(|| async {
let resp = self.get(&path).send().await?;
let resp = self.check_response(resp).await?;
let clips: Vec<Clip> = resp.json().await?;
Ok(clips)
})
.await?;
all_clips.extend(clips);
}
Ok(all_clips)
}
}