Skip to main content

exiftool_rs_wrapper/
batch.rs

1//! 批处理脚本和管道处理模块
2//!
3//! 支持批处理脚本执行和管道数据流
4
5use crate::ExifTool;
6use crate::error::{Error, Result};
7use crate::types::Metadata;
8use std::io::{self, BufRead, Write};
9use std::path::{Path, PathBuf};
10
11/// 批处理脚本
12#[derive(Debug, Clone)]
13pub struct BatchScript {
14    /// 脚本命令列表
15    commands: Vec<ScriptCommand>,
16    /// 是否显示进度
17    show_progress: bool,
18    /// 遇到错误时是否继续
19    continue_on_error: bool,
20}
21
22/// 脚本命令
23#[derive(Debug, Clone)]
24enum ScriptCommand {
25    /// 读取文件元数据
26    Read {
27        path: PathBuf,
28        tags: Option<Vec<String>>,
29    },
30    /// 写入标签
31    Write {
32        path: PathBuf,
33        tag: String,
34        value: String,
35    },
36    /// 删除标签
37    Delete { path: PathBuf, tag: String },
38    /// 批量读取
39    BatchRead { paths: Vec<PathBuf> },
40    /// 复制标签
41    #[allow(dead_code)]
42    CopyTags {
43        source: PathBuf,
44        target: PathBuf,
45        #[allow(dead_code)]
46        tags: Vec<String>,
47    },
48    /// 打印消息
49    Print(String),
50    /// 设置变量
51    #[allow(dead_code)]
52    SetVar {
53        #[allow(dead_code)]
54        name: String,
55        #[allow(dead_code)]
56        value: String,
57    },
58}
59
60impl Default for BatchScript {
61    fn default() -> Self {
62        Self::new()
63    }
64}
65
66impl BatchScript {
67    /// 创建新的批处理脚本
68    pub fn new() -> Self {
69        Self {
70            commands: Vec::new(),
71            show_progress: true,
72            continue_on_error: false,
73        }
74    }
75
76    /// 从文件加载脚本
77    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
78        let path = path.as_ref();
79        let content = std::fs::read_to_string(path).map_err(Error::Io)?;
80        Self::from_string(content)
81    }
82
83    /// 从字符串解析脚本
84    pub fn from_string(content: String) -> Result<Self> {
85        let mut script = Self::new();
86
87        for line in content.lines() {
88            let line = line.trim();
89
90            // 跳过空行和注释
91            if line.is_empty() || line.starts_with('#') {
92                continue;
93            }
94
95            let parts: Vec<&str> = line.split_whitespace().collect();
96            if parts.is_empty() {
97                continue;
98            }
99
100            let cmd = parts[0].to_lowercase();
101            let args = &parts[1..];
102
103            match cmd.as_str() {
104                "read" => {
105                    if !args.is_empty() {
106                        let path = PathBuf::from(args[0]);
107                        let tags = if args.len() > 1 {
108                            Some(args[1..].iter().map(|s| s.to_string()).collect())
109                        } else {
110                            None
111                        };
112                        script.commands.push(ScriptCommand::Read { path, tags });
113                    }
114                }
115                "write" => {
116                    if args.len() >= 3 {
117                        script.commands.push(ScriptCommand::Write {
118                            path: PathBuf::from(args[0]),
119                            tag: args[1].to_string(),
120                            value: args[2..].join(" "),
121                        });
122                    }
123                }
124                "delete" => {
125                    if args.len() >= 2 {
126                        script.commands.push(ScriptCommand::Delete {
127                            path: PathBuf::from(args[0]),
128                            tag: args[1].to_string(),
129                        });
130                    }
131                }
132                "batch" => {
133                    if !args.is_empty() {
134                        script.commands.push(ScriptCommand::BatchRead {
135                            paths: args.iter().map(PathBuf::from).collect(),
136                        });
137                    }
138                }
139                "copy" => {
140                    if args.len() >= 3 {
141                        script.commands.push(ScriptCommand::CopyTags {
142                            source: PathBuf::from(args[0]),
143                            target: PathBuf::from(args[1]),
144                            tags: args[2..].iter().map(|s| s.to_string()).collect(),
145                        });
146                    }
147                }
148                "print" => {
149                    if !args.is_empty() {
150                        script.commands.push(ScriptCommand::Print(args.join(" ")));
151                    }
152                }
153                "set" => {
154                    if args.len() >= 2 {
155                        script.commands.push(ScriptCommand::SetVar {
156                            name: args[0].to_string(),
157                            value: args[1..].join(" "),
158                        });
159                    }
160                }
161                "progress" => {
162                    script.show_progress = args.first().map(|s| *s != "off").unwrap_or(true);
163                }
164                "continue_on_error" => {
165                    script.continue_on_error = args.first().map(|s| *s == "on").unwrap_or(true);
166                }
167                _ => {
168                    return Err(Error::invalid_arg(format!("未知命令: {}", cmd)));
169                }
170            }
171        }
172
173        Ok(script)
174    }
175
176    /// 显示进度
177    pub fn show_progress(mut self, yes: bool) -> Self {
178        self.show_progress = yes;
179        self
180    }
181
182    /// 遇到错误时继续
183    pub fn continue_on_error(mut self, yes: bool) -> Self {
184        self.continue_on_error = yes;
185        self
186    }
187
188    /// 执行脚本
189    pub fn execute(&self, exiftool: &ExifTool) -> Result<BatchResult> {
190        let mut result = BatchResult::new();
191        let total = self.commands.len();
192
193        for (i, cmd) in self.commands.iter().enumerate() {
194            if self.show_progress {
195                print!(
196                    "\r进度: {}/{} ({:.1}%)",
197                    i + 1,
198                    total,
199                    (i + 1) as f64 / total as f64 * 100.0
200                );
201                io::stdout().flush().unwrap();
202            }
203
204            match self.execute_command(exiftool, cmd) {
205                Ok(_) => result.success += 1,
206                Err(e) => {
207                    result.failed += 1;
208                    result.errors.push(format!("命令 {:?}: {}", cmd, e));
209                    if !self.continue_on_error {
210                        if self.show_progress {
211                            println!();
212                        }
213                        return Err(e);
214                    }
215                }
216            }
217        }
218
219        if self.show_progress {
220            println!();
221        }
222
223        result.total = total;
224        Ok(result)
225    }
226
227    /// 执行单个命令
228    fn execute_command(&self, exiftool: &ExifTool, cmd: &ScriptCommand) -> Result<()> {
229        match cmd {
230            ScriptCommand::Read { path, tags } => {
231                if let Some(tags) = tags {
232                    for tag in tags {
233                        let _: String = exiftool.read_tag(path, tag)?;
234                    }
235                } else {
236                    exiftool.query(path).execute()?;
237                }
238            }
239            ScriptCommand::Write { path, tag, value } => {
240                exiftool
241                    .write(path)
242                    .tag(tag, value)
243                    .overwrite_original(true)
244                    .execute()?;
245            }
246            ScriptCommand::Delete { path, tag } => {
247                exiftool
248                    .write(path)
249                    .delete(tag)
250                    .overwrite_original(true)
251                    .execute()?;
252            }
253            ScriptCommand::BatchRead { paths } => {
254                exiftool.query_batch(paths).execute()?;
255            }
256            ScriptCommand::CopyTags {
257                source,
258                target,
259                tags: _,
260            } => {
261                exiftool
262                    .write(target)
263                    .copy_from(source)
264                    .overwrite_original(true)
265                    .execute()?;
266            }
267            ScriptCommand::Print(msg) => {
268                println!("{}", msg);
269            }
270            ScriptCommand::SetVar { name: _, value: _ } => {
271                // 变量系统待实现
272            }
273        }
274        Ok(())
275    }
276}
277
278/// 批处理结果
279#[derive(Debug, Clone)]
280pub struct BatchResult {
281    /// 总命令数
282    pub total: usize,
283    /// 成功数
284    pub success: usize,
285    /// 失败数
286    pub failed: usize,
287    /// 错误信息
288    pub errors: Vec<String>,
289}
290
291impl BatchResult {
292    /// 创建新的结果
293    fn new() -> Self {
294        Self {
295            total: 0,
296            success: 0,
297            failed: 0,
298            errors: Vec::new(),
299        }
300    }
301
302    /// 检查是否全部成功
303    pub fn is_success(&self) -> bool {
304        self.failed == 0
305    }
306
307    /// 获取成功率
308    pub fn success_rate(&self) -> f64 {
309        if self.total == 0 {
310            0.0
311        } else {
312            self.success as f64 / self.total as f64
313        }
314    }
315}
316
317/// 管道处理器
318pub struct PipeProcessor {
319    exiftool: ExifTool,
320    delimiter: String,
321}
322
323impl PipeProcessor {
324    /// 创建新的管道处理器
325    pub fn new(exiftool: ExifTool) -> Self {
326        Self {
327            exiftool,
328            delimiter: "\n".to_string(),
329        }
330    }
331
332    /// 设置分隔符
333    pub fn delimiter(mut self, delim: impl Into<String>) -> Self {
334        self.delimiter = delim.into();
335        self
336    }
337
338    /// 从 stdin 读取并处理
339    pub fn process_stdin(
340        &self,
341        processor: impl Fn(&ExifTool, &str) -> Result<String>,
342    ) -> Result<()> {
343        let stdin = io::stdin();
344        let mut stdout = io::stdout();
345
346        for line in stdin.lock().lines() {
347            let line = line.map_err(Error::Io)?;
348
349            if line.trim().is_empty() {
350                continue;
351            }
352
353            match processor(&self.exiftool, &line) {
354                Ok(output) => {
355                    writeln!(stdout, "{}", output)?;
356                }
357                Err(e) => {
358                    eprintln!("处理失败 '{}': {}", line, e);
359                }
360            }
361        }
362
363        Ok(())
364    }
365
366    /// 处理文件列表
367    pub fn process_file_list<P: AsRef<Path>>(
368        &self,
369        list_file: P,
370        processor: impl Fn(&ExifTool, &Path) -> Result<Metadata>,
371    ) -> Result<Vec<(PathBuf, Metadata)>> {
372        let content = std::fs::read_to_string(list_file.as_ref()).map_err(Error::Io)?;
373
374        let mut results = Vec::new();
375
376        for line in content.lines() {
377            let path = PathBuf::from(line.trim());
378            if path.exists() {
379                match processor(&self.exiftool, &path) {
380                    Ok(metadata) => {
381                        results.push((path, metadata));
382                    }
383                    Err(e) => {
384                        eprintln!("处理失败 '{}': {}", path.display(), e);
385                    }
386                }
387            }
388        }
389
390        Ok(results)
391    }
392}
393
394/// 批处理脚本示例
395pub fn example_script() -> &'static str {
396    r#"# ExifTool 批处理脚本示例
397# 这是一个注释
398
399# 设置进度显示
400progress on
401
402# 读取文件
403read photo1.jpg
404read photo2.jpg Make Model
405
406# 写入标签
407write photo1.jpg Copyright "© 2026 Photographer"
408write photo2.jpg Artist "My Name"
409
410# 批量读取
411batch photo1.jpg photo2.jpg photo3.jpg
412
413# 打印消息
414print "批处理完成"
415"#
416}