From f8a156d255e9a93fd42f1f27c331efb87f76f945 Mon Sep 17 00:00:00 2001 From: MarekSpodBiedry <73122968+MarekSpodBiedry@users.noreply.github.com> Date: Fri, 9 Oct 2026 20:28:46 +0200 Subject: [PATCH] Warn about missing media when opening a session (#57) --- apps/soundcraft-cli/src/main.rs | 31 ++++++++++++++++++++++++++--- crates/engine/src/cmd/file.rs | 35 +++++++++++++++++++++++++++++++-- docs/mcp.md | 2 +- 3 files changed, 62 insertions(+), 6 deletions(-) diff --git a/apps/soundcraft-cli/src/main.rs b/apps/soundcraft-cli/src/main.rs index 9dc8ec3..1f8539b 100644 --- a/apps/soundcraft-cli/src/main.rs +++ b/apps/soundcraft-cli/src/main.rs @@ -76,8 +76,9 @@ fn info(args: &[String]) -> ExitCode { let Some(path) = args.first() else { return fail("info FILE") }; if path.ends_with(".scraft") { let mut e = Engine::default(); - if let Err(err) = soundcraft_engine::io::open_session(&mut e, path) { - return fail(err); + match soundcraft_engine::io::open_session(&mut e, path) { + Ok(missing) => warn_missing(&missing), + Err(err) => return fail(err), } outln!("{}", soundcraft_engine::inspect::session_text(&e)); return ExitCode::SUCCESS; @@ -135,10 +136,22 @@ fn convert(args: &[String]) -> ExitCode { } } +/// One stderr line per media file a session refers to but could not load (the exit code is unchanged). +fn missing_warnings(missing: &[String]) -> Vec { + missing.iter().map(|m| format!("warning: missing media: {m}")).collect() +} + +fn warn_missing(missing: &[String]) { + for line in missing_warnings(missing) { + eprintln!("{line}"); + } +} + fn load_engine(args: &[String]) -> Result { if let Some(p) = arg_value(args, "--in") { let mut e = Engine::default(); - soundcraft_engine::io::open_session(&mut e, &p).map_err(|e| e.to_string())?; + let missing = soundcraft_engine::io::open_session(&mut e, &p).map_err(|e| e.to_string())?; + warn_missing(&missing); Ok(e) } else if args.iter().any(|a| a == "--demo" || a == "--sample") { Ok(soundcraft_engine::demo::demo_engine()) @@ -336,3 +349,15 @@ fn parity(args: &[String]) -> ExitCode { } ExitCode::SUCCESS } + +#[cfg(test)] +mod tests { + use super::missing_warnings; + + #[test] + fn missing_media_gets_one_warning_each() { + assert!(missing_warnings(&[]).is_empty()); + let w = missing_warnings(&["a.wav".to_string(), "b.wav: decode failed".to_string()]); + assert_eq!(w, vec!["warning: missing media: a.wav", "warning: missing media: b.wav: decode failed"]); + } +} diff --git a/crates/engine/src/cmd/file.rs b/crates/engine/src/cmd/file.rs index 86bb4b5..558c18a 100644 --- a/crates/engine/src/cmd/file.rs +++ b/crates/engine/src/cmd/file.rs @@ -11,8 +11,8 @@ pub fn specs() -> Vec { cmd!(noundo "session.new", "New...", ["File"], Some("Cmd+N"), "{name?: 'Untitled', sample_rate?: 48000, bit_depth?: 24, template?: blank|demo}", always, new_session), cmd!(noundo "session.open", "Open Session...", ["File"], Some("Cmd+O"), "{path}", always, |e, p| { let path = str_param(p, "path").ok_or_else(|| bad("session.open", "`path` required"))?.to_string(); - crate::io::open_session(e, &path)?; - Ok(json!({"name": e.session().name, "tracks": e.session().tracks.len()})) + let missing = crate::io::open_session(e, &path)?; + Ok(json!({"name": e.session().name, "tracks": e.session().tracks.len(), "missing": missing})) }), cmd!(noundo "session.close", "Close Session", ["File"], Some("Cmd+Shift+W"), "{}", always, |e, _| { e.replace_session(Session::default()); e.path = None; Ok(json!({})) }), cmd!(noundo "session.save", "Save Session", ["File"], Some("Cmd+S"), "{path?}", always, |e, p| { @@ -270,3 +270,34 @@ fn write_score(e: &mut Engine, p: &Value, id: &str, svg: bool) -> Result let measures = parts.iter().map(|p| p.measures.len()).max().unwrap_or(0); Ok(json!({"path": path, "parts": parts.len(), "measures": measures})) } + +#[cfg(test)] +mod open_tests { + use super::*; + + fn remove_media(dir: &std::path::Path) { + for entry in std::fs::read_dir(dir).into_iter().flatten().flatten() { + let p = entry.path(); + if p.is_dir() { + remove_media(&p); + } else if p.extension().is_some_and(|x| x != "scraft") { + let _ = std::fs::remove_file(&p); + } + } + } + + #[test] + fn session_open_reports_missing_media() { + let mut e = crate::demo::demo_engine(); + let dir = std::env::temp_dir().join(format!("soundcraft-open-missing-{}", std::process::id())); + let path = dir.join("Gone.scraft").to_string_lossy().into_owned(); + e.execute("session.save_as", &json!({"path": path})).unwrap(); + let mut ok = Engine::default(); + assert!(ok.execute("session.open", &json!({"path": path})).unwrap()["missing"].as_array().is_some_and(Vec::is_empty)); + remove_media(&dir); + let mut r = Engine::default(); + let res = r.execute("session.open", &json!({"path": path})).unwrap(); + assert!(res["missing"].as_array().is_some_and(|m| !m.is_empty()), "{res}"); + let _ = std::fs::remove_dir_all(&dir); + } +} diff --git a/docs/mcp.md b/docs/mcp.md index ed5c8e6..1fd317b 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -24,7 +24,7 @@ claude mcp add soundcraft -- soundcraft-cli mcp --demo | `execute` | Run one command with JSON params. | | `batch` | Run several commands in order (`keep_going` optional). | | `inspect_session` | Verify work: tracks, clips, mixer, plugins, sends, automation, markers, selection. | -| `new_session`, `open_session`, `save_session` | Session files (`.scraft`, audio in `Audio Files/`). | +| `new_session`, `open_session`, `save_session` | Session files (`.scraft`, audio in `Audio Files/`). `open_session` returns `missing`: the media files that could not be loaded (empty when all are found). | | `import_audio` | WAV, AIFF, FLAC, MP3, OGG, AAC/M4A, ALAC, CAF onto a new or existing track. | | `bounce_mix` | Render to WAV/AIFF/FLAC; returns peak dBFS and integrated LUFS. | | `parity` | Feature-parity report. |