80 lines
2.5 KiB
Python
Executable File
80 lines
2.5 KiB
Python
Executable File
#!/usr/bin/env -S uv run
|
|
# /// script
|
|
# dependencies = ["piexif"]
|
|
# ///
|
|
|
|
import argparse
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
import logging
|
|
import re
|
|
import subprocess
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
import piexif
|
|
|
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s: %(message)s")
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(
|
|
description="Set creation time to date in filename and mod time to now for image files.",
|
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
)
|
|
parser.add_argument(
|
|
"paths",
|
|
nargs="+",
|
|
type=Path,
|
|
help="Paths to image files or folders containing images.",
|
|
)
|
|
return parser.parse_args()
|
|
|
|
|
|
def update_image_times(path: Path) -> None:
|
|
match = re.search(r"\D(\d{4}-\d{2}-\d{2})\D?", path.name)
|
|
if not match:
|
|
logging.debug(f"No date found in {path.name}")
|
|
return
|
|
date_str = match.group(1)
|
|
try:
|
|
dt = datetime.fromisoformat(date_str)
|
|
formatted_date = dt.strftime("%m/%d/%Y %H:%M:%S")
|
|
today = datetime.now().strftime("%m/%d/%Y %H:%M:%S")
|
|
subprocess.run(["SetFile", "-d", formatted_date, str(path)], check=True)
|
|
subprocess.run(["SetFile", "-m", today, str(path)], check=True)
|
|
# Update EXIF DateTimeOriginal
|
|
exif_dict = piexif.load(str(path))
|
|
exif_dict["Exif"][piexif.ExifIFD.DateTimeOriginal] = dt.strftime("%Y:%m:%d %H:%M:%S").encode()
|
|
exif_bytes = piexif.dump(exif_dict)
|
|
piexif.insert(exif_bytes, str(path))
|
|
logging.info(f"Updated times and EXIF for {path}")
|
|
except ValueError as e:
|
|
logging.error(f"Invalid date {date_str} in {path.name}: {e}")
|
|
except subprocess.CalledProcessError as e:
|
|
logging.error(f"Failed to update {path}: {e}")
|
|
except Exception as e:
|
|
logging.error(f"Failed to update EXIF for {path}: {e}")
|
|
|
|
|
|
def main() -> None:
|
|
args = parse_args()
|
|
|
|
files = []
|
|
for it in args.paths:
|
|
if it.is_dir():
|
|
files.extend(it.glob("*.jpg"))
|
|
files.extend(it.glob("*.jpeg"))
|
|
elif it.is_file() and it.suffix.lower() in {".jpg", ".jpeg"}:
|
|
files.append(it)
|
|
else:
|
|
logging.warning(f"Path {it} is neither an image nor a directory, skipping.")
|
|
|
|
pool = ThreadPoolExecutor(max_workers=5)
|
|
for path in files:
|
|
pool.submit(update_image_times, path)
|
|
pool.shutdown(wait=True)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|