137 lines
3.8 KiB
Python
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))
|