1use crate::ExifTool;
6use crate::error::{Error, Result};
7use crate::types::Metadata;
8use std::io::{self, BufRead, Write};
9use std::path::{Path, PathBuf};
10
11#[derive(Debug, Clone)]
13pub struct BatchScript {
14 commands: Vec<ScriptCommand>,
16 show_progress: bool,
18 continue_on_error: bool,
20}
21
22#[derive(Debug, Clone)]
24enum ScriptCommand {
25 Read {
27 path: PathBuf,
28 tags: Option<Vec<String>>,
29 },
30 Write {
32 path: PathBuf,
33 tag: String,
34 value: String,
35 },
36 Delete { path: PathBuf, tag: String },
38 BatchRead { paths: Vec<PathBuf> },
40 #[allow(dead_code)]
42 CopyTags {
43 source: PathBuf,
44 target: PathBuf,
45 #[allow(dead_code)]
46 tags: Vec<String>,
47 },
48 Print(String),
50 #[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 pub fn new() -> Self {
69 Self {
70 commands: Vec::new(),
71 show_progress: true,
72 continue_on_error: false,
73 }
74 }
75
76 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 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 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 pub fn show_progress(mut self, yes: bool) -> Self {
178 self.show_progress = yes;
179 self
180 }
181
182 pub fn continue_on_error(mut self, yes: bool) -> Self {
184 self.continue_on_error = yes;
185 self
186 }
187
188 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 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 }
273 }
274 Ok(())
275 }
276}
277
278#[derive(Debug, Clone)]
280pub struct BatchResult {
281 pub total: usize,
283 pub success: usize,
285 pub failed: usize,
287 pub errors: Vec<String>,
289}
290
291impl BatchResult {
292 fn new() -> Self {
294 Self {
295 total: 0,
296 success: 0,
297 failed: 0,
298 errors: Vec::new(),
299 }
300 }
301
302 pub fn is_success(&self) -> bool {
304 self.failed == 0
305 }
306
307 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
317pub struct PipeProcessor {
319 exiftool: ExifTool,
320 delimiter: String,
321}
322
323impl PipeProcessor {
324 pub fn new(exiftool: ExifTool) -> Self {
326 Self {
327 exiftool,
328 delimiter: "\n".to_string(),
329 }
330 }
331
332 pub fn delimiter(mut self, delim: impl Into<String>) -> Self {
334 self.delimiter = delim.into();
335 self
336 }
337
338 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 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
394pub 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}