diff --git a/src/api/cover.rs b/src/api/cover.rs index 7ba598f..70877ff 100644 --- a/src/api/cover.rs +++ b/src/api/cover.rs @@ -14,10 +14,15 @@ impl SunoClient { clip_id: &str, model_key: &str, tags: Option<&str>, + title: Option<&str>, ) -> Result, CliError> { let mut req = GenerateRequest::new(model_key, "cover"); req.tags = tags.map(String::from); req.cover_clip_id = Some(clip_id.to_string()); + // v2-web rejects a null `params.title` with HTTP 422, so it must always + // be a string. Use the caller's title, else inherit the source clip's + // title (matching the web app), else a generic fallback. + req.title = Some(self.resolve_title(clip_id, title, "Cover").await); self.generate(&req).await } } diff --git a/src/api/generate.rs b/src/api/generate.rs index 2852caf..84f79be 100644 --- a/src/api/generate.rs +++ b/src/api/generate.rs @@ -19,6 +19,33 @@ impl SunoClient { .await } + /// Resolve the title for a clip-derived generation (cover/remaster/extend). + /// Prefers an explicit override, then the source clip's own title, then a + /// generic fallback. `/api/generate/v2-web/` requires `params.title` to be a + /// non-null string, so this never returns an empty value. + pub async fn resolve_title( + &self, + clip_id: &str, + override_title: Option<&str>, + fallback: &str, + ) -> String { + if let Some(t) = override_title { + let t = t.trim(); + if !t.is_empty() { + return t.to_string(); + } + } + if let Ok(clips) = self.get_clips(&[clip_id.to_string()]).await + && let Some(c) = clips.into_iter().next() + { + let t = c.title.trim(); + if !t.is_empty() { + return t.to_string(); + } + } + fallback.to_string() + } + /// Poll clip status by IDs until all are complete or errored. /// "streaming" means still generating — we wait for "complete". pub async fn poll_clips( diff --git a/src/api/remaster.rs b/src/api/remaster.rs index 5674bd9..2562b99 100644 --- a/src/api/remaster.rs +++ b/src/api/remaster.rs @@ -11,9 +11,13 @@ impl SunoClient { &self, clip_id: &str, remaster_model_key: &str, + title: Option<&str>, ) -> Result, CliError> { let mut req = GenerateRequest::new(remaster_model_key, "remaster"); req.cover_clip_id = Some(clip_id.to_string()); + // v2-web rejects a null `params.title` with HTTP 422; always send a + // string (caller override, else the source clip's title, else fallback). + req.title = Some(self.resolve_title(clip_id, title, "Remaster").await); self.generate(&req).await } } diff --git a/src/cli.rs b/src/cli.rs index c3b20f8..4e0dd58 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -243,6 +243,10 @@ pub struct ExtendArgs { #[arg(long)] pub tags: Option, + /// Title for the new clip (defaults to the source clip's title) + #[arg(long)] + pub title: Option, + /// Wait for completion #[arg(short, long)] pub wait: bool, @@ -267,6 +271,10 @@ pub struct CoverArgs { #[arg(long)] pub tags: Option, + /// Title for the cover (defaults to the source clip's title) + #[arg(long)] + pub title: Option, + /// Model version for the cover #[arg(short, long, default_value = "v5.5")] pub model: ModelVersion, @@ -289,6 +297,10 @@ pub struct RemasterArgs { #[arg(long, default_value = "v5.5")] pub model: RemasterModel, + /// Title for the remaster (defaults to the source clip's title) + #[arg(long)] + pub title: Option, + /// Wait for completion #[arg(short, long)] pub wait: bool, diff --git a/src/main.rs b/src/main.rs index 947e981..6fed647 100644 --- a/src/main.rs +++ b/src/main.rs @@ -375,13 +375,18 @@ async fn run() -> Result<(), CliError> { } Commands::Extend(args) => { + let c = client().await?; let mut req = GenerateRequest::new("chirp-fenix", "custom"); req.prompt = args.lyrics.unwrap_or_default(); req.tags = args.tags; + // v2-web requires a non-null `params.title`. + req.title = Some( + c.resolve_title(&args.clip_id, args.title.as_deref(), "Extend") + .await, + ); req.continue_clip_id = Some(args.clip_id); req.continue_at = Some(args.at); - let c = client().await?; let clips = c.generate(&req).await?; handle_generation(&c, clips, args.wait, None, &fmt, cli.quiet).await?; } @@ -400,7 +405,12 @@ async fn run() -> Result<(), CliError> { } let c = client().await?; let clips = c - .cover(&args.clip_id, args.model.to_api_key(), args.tags.as_deref()) + .cover( + &args.clip_id, + args.model.to_api_key(), + args.tags.as_deref(), + args.title.as_deref(), + ) .await?; handle_generation( &c, @@ -418,7 +428,9 @@ async fn run() -> Result<(), CliError> { eprintln!("Remastering with {}...", args.model.to_api_key()); } let c = client().await?; - let clips = c.remaster(&args.clip_id, args.model.to_api_key()).await?; + let clips = c + .remaster(&args.clip_id, args.model.to_api_key(), args.title.as_deref()) + .await?; handle_generation( &c, clips,