Files
media-batch-tools/photo_metadata.py
T

137 lines
3.8 KiB
Python

#!/usr/bin/env python3
"""
Photo collection metadata cleanup.
Drag files or directories onto this script, or run:
python3 photo_metadata.py path/to/file/or/directory [...]
The script reads metadata with exiftool, builds a full preview, then edits files in
place only after confirmation.
"""
from __future__ import annotations
import json
import subprocess
import sys
from tools.console import (
ProgressView,
clear_screen,
pause_if_interactive,
prompt_yes_no as ask_yes_no,
)
from tools.exiftool import require_exiftool, run_exiftool_json
from tools.photo_planning import (
plan_pto_updates,
prepare_plan,
read_pto_references,
)
from tools.photo_execution import apply_changes, apply_pto_changes, print_preview, print_removed_dirs
from tools.photo_records import SUPPORTED_EXTS, build_records
from tools.photo_settings import prompt_choices
from tools.filesystem import (
cleanup_roots_from_args,
collect_files,
determine_working_dir,
remove_empty_dirs,
)
def run(argv: list[str]) -> int:
if len(argv) < 2:
print("Usage: python3 photo_metadata.py path/to/file/or/directory [...]")
return 2
try:
working_dir = determine_working_dir(argv[1:])
except RuntimeError as exc:
print(exc)
return 1
cleanup_roots = cleanup_roots_from_args(argv[1:])
files = collect_files(argv[1:], allowed_exts=SUPPORTED_EXTS, print_missing=True)
pto_files = collect_files(argv[1:], allowed_exts={".pto"})
if not files:
print("No supported media files found.")
return 1
try:
exiftool = require_exiftool()
except RuntimeError as exc:
print(exc)
return 1
print(f"Found {len(files)} supported media file(s).")
progress = ProgressView(len(files), "Reading metadata")
read_count = 0
def update_read_progress() -> None:
nonlocal read_count
read_count += 1
progress.update(read_count)
try:
metadata = run_exiftool_json(exiftool, files, progress=update_read_progress)
except (subprocess.CalledProcessError, json.JSONDecodeError) as exc:
progress.finish()
print(f"Failed to read metadata with exiftool: {exc}")
return 1
progress.finish(keep=True)
settings_state = None
while True:
records = build_records(files, metadata)
try:
choices, settings_state = prompt_choices(
exiftool,
records,
working_dir,
has_pto_files=bool(pto_files),
initial_state=settings_state,
)
except RuntimeError as exc:
print(exc)
return 1
if choices is None:
print("Nothing changed.")
return 0
pto_references = read_pto_references(pto_files, records) if choices.process_pto_files else []
prepare_plan(records, choices, pto_references)
pto_plans = plan_pto_updates(pto_references, records) if choices.process_pto_files else []
clear_screen(scrollback=True)
print_preview(records, choices, pto_plans)
try:
if ask_yes_no("Proceed with these changes?", default=True):
break
except RuntimeError as exc:
print(exc)
return 1
failures = apply_changes(exiftool, records, choices)
failures.extend(apply_pto_changes(pto_plans))
removed_dirs = remove_empty_dirs(cleanup_roots, keep_dirs={working_dir.resolve()})
print_removed_dirs(removed_dirs)
if failures:
print("\nSome files failed:")
for failure in failures:
print(f" - {failure}")
return 1
print("\nDone.")
return 0
def main(argv: list[str]) -> int:
try:
return run(argv)
finally:
pause_if_interactive()
if __name__ == "__main__":
raise SystemExit(main(sys.argv))