1use std::ffi::OsString;
5use std::path::PathBuf;
6
7use clap::Parser;
8use xil_core::journal::{replay_sfx_edits, sfx_edits_path};
9use xil_core::script::hints::py_repr;
10use xil_core::workspace::{derive_paths, resolve_slug};
11use xil_core::{banner, log};
12
13const SCRIPT_NAME: &str = "XILU020_sfx_restore";
14
15#[derive(Parser)]
16#[command(
17 name = "xil-sfx-restore",
18 about = "Reapply journaled timeline sound edits (sfx_<tag>_edits.jsonl) onto the episode's SFX config — recovery for a cleared or regenerated sfx_<tag>.json."
19)]
20struct Args {
21 #[arg(long, required_unless_present = "tag", conflicts_with = "tag")]
23 episode: Option<String>,
24 #[arg(long)]
26 tag: Option<String>,
27 #[arg(long)]
29 show: Option<String>,
30 #[arg(long)]
32 sfx: Option<PathBuf>,
33 #[arg(long)]
35 dry_run: bool,
36}
37
38fn execute(a: &Args, tag: &str) -> anyhow::Result<i32> {
39 let slug = resolve_slug(a.show.as_deref(), "project.json");
40 let sfx_path = a
41 .sfx
42 .clone()
43 .unwrap_or_else(|| derive_paths(&slug, tag)["sfx"].clone());
44
45 if !sfx_path.exists() {
46 log::error(&format!("SFX config not found: {}", sfx_path.display()));
47 log::info(
48 "Run `xil parse --episode TAG` first to generate it (the journal is reapplied automatically).",
49 );
50 return Ok(1);
51 }
52 let journal = sfx_edits_path(&sfx_path);
53 if !journal.exists() {
54 log::error(&format!("No edit journal found: {}", journal.display()));
55 log::info(
56 "The journal is created the first time a sound profile is saved in the GUI timeline editor.",
57 );
58 return Ok(1);
59 }
60
61 let replay = replay_sfx_edits(&sfx_path, a.dry_run, &mut |m| log::warning(&m))?;
62 let action = if a.dry_run {
63 "Would reapply"
64 } else {
65 "Reapplied"
66 };
67 log::info(&format!(
68 " {action} {} edit record(s) from {}",
69 replay.applied,
70 journal.display()
71 ));
72 for key in &replay.orphans {
73 log::warning(&format!(
74 " Orphaned key (not in current effects): {}",
75 py_repr(key)
76 ));
77 }
78 if a.dry_run && replay.applied > 0 {
79 log::info(" Re-run without --dry-run to apply changes.");
80 }
81 Ok(0)
82}
83
84pub fn run(args: &[OsString]) -> anyhow::Result<i32> {
85 log::init("sfx-restore");
86 let a: Args = match super::parse_or_exit("xil-sfx-restore", args) {
87 Ok(a) => a,
88 Err(code) => return Ok(code),
89 };
90 let tag = a
91 .episode
92 .clone()
93 .or_else(|| a.tag.clone())
94 .unwrap_or_default();
95 let _banner = banner::begin(SCRIPT_NAME, &super::argv_line(args));
96 execute(&a, &tag)
97}
98
99pub fn command() -> clap::Command {
101 <Args as clap::CommandFactory>::command()
102}