Refactor photo metadata workflow
This commit is contained in:
+71
-21
@@ -2,8 +2,11 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import tempfile
|
||||
from collections.abc import Callable, Iterator
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -18,15 +21,12 @@ def _use_windows_argument_file() -> bool:
|
||||
return os.name == "nt"
|
||||
|
||||
|
||||
def run_exiftool_command(
|
||||
executable: str,
|
||||
args: list[str],
|
||||
**run_kwargs: Any,
|
||||
) -> subprocess.CompletedProcess:
|
||||
"""Run ExifTool with Unicode-safe filename arguments on Windows."""
|
||||
@contextmanager
|
||||
def _prepared_command(executable: str, args: list[str]) -> Iterator[tuple[list[str], str | None]]:
|
||||
command = [executable, "-charset", "filename=UTF8", *args]
|
||||
if not _use_windows_argument_file():
|
||||
return subprocess.run(command, **run_kwargs)
|
||||
yield command, None
|
||||
return
|
||||
|
||||
# ExifTool can't reliably receive arbitrary Unicode paths on the Windows
|
||||
# command line. The outer path is ASCII and the UTF-8 argument file holds
|
||||
@@ -34,11 +34,20 @@ def run_exiftool_command(
|
||||
with tempfile.TemporaryDirectory(prefix="mbt-exiftool-") as temp_dir:
|
||||
argument_file = Path(temp_dir) / "arguments.txt"
|
||||
argument_file.write_text("\n".join(args) + "\n", encoding="utf-8")
|
||||
return subprocess.run(
|
||||
[executable, "-charset", "filename=UTF8", "-@", argument_file.name],
|
||||
cwd=temp_dir,
|
||||
**run_kwargs,
|
||||
)
|
||||
yield [executable, "-charset", "filename=UTF8", "-@", argument_file.name], temp_dir
|
||||
|
||||
|
||||
def run_exiftool_command(
|
||||
executable: str,
|
||||
args: list[str],
|
||||
**run_kwargs: Any,
|
||||
) -> subprocess.CompletedProcess:
|
||||
"""Run ExifTool with Unicode-safe filename arguments on Windows."""
|
||||
with _prepared_command(executable, args) as (command, cwd):
|
||||
return subprocess.run(command, cwd=cwd, **run_kwargs)
|
||||
|
||||
|
||||
_PROGRESS_RE = re.compile(r"^======== .* \[\d+/(\d+)\]$")
|
||||
|
||||
|
||||
def run_exiftool_json(
|
||||
@@ -46,6 +55,7 @@ def run_exiftool_json(
|
||||
paths: list[Path],
|
||||
*,
|
||||
quicktime_utc: bool = True,
|
||||
progress: Callable[[], None] | None = None,
|
||||
) -> list[dict]:
|
||||
results: list[dict] = []
|
||||
for start in range(0, len(paths), 80):
|
||||
@@ -57,14 +67,45 @@ def run_exiftool_json(
|
||||
if quicktime_utc:
|
||||
args.extend(["-api", "QuickTimeUTC=1"])
|
||||
args.extend(str(path) for path in chunk)
|
||||
completed = run_exiftool_command(
|
||||
executable,
|
||||
args,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
results.extend(json.loads(completed.stdout or "[]"))
|
||||
if progress is None:
|
||||
completed = run_exiftool_command(
|
||||
executable,
|
||||
args,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
results.extend(json.loads(completed.stdout or "[]"))
|
||||
continue
|
||||
|
||||
with _prepared_command(executable, ["-progress", *args]) as (command, cwd):
|
||||
with tempfile.TemporaryFile(mode="w+t", encoding="utf-8") as output:
|
||||
process = subprocess.Popen(
|
||||
command,
|
||||
cwd=cwd,
|
||||
stdout=output,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
assert process.stderr is not None
|
||||
errors = []
|
||||
started_file = False
|
||||
for line in process.stderr:
|
||||
if _PROGRESS_RE.match(line.rstrip("\r\n")):
|
||||
# ExifTool emits this marker before reading the named
|
||||
# file, so it confirms that the previous one finished.
|
||||
if started_file:
|
||||
progress()
|
||||
started_file = True
|
||||
else:
|
||||
errors.append(line)
|
||||
return_code = process.wait()
|
||||
if return_code:
|
||||
raise subprocess.CalledProcessError(return_code, command, stderr="".join(errors))
|
||||
if started_file:
|
||||
progress()
|
||||
output.seek(0)
|
||||
results.extend(json.load(output))
|
||||
return results
|
||||
|
||||
|
||||
@@ -76,4 +117,13 @@ def run_exiftool_write(executable: str, path: Path, args: list[str]) -> None:
|
||||
*args,
|
||||
str(path),
|
||||
]
|
||||
run_exiftool_command(executable, command, check=True)
|
||||
completed = run_exiftool_command(
|
||||
executable,
|
||||
command,
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if completed.returncode:
|
||||
detail = (completed.stderr or completed.stdout or "").strip()
|
||||
raise RuntimeError(f"ExifTool failed: {detail or f'exit code {completed.returncode}'}")
|
||||
|
||||
Reference in New Issue
Block a user