Add --only-photo/--only-video filters, skip NAS thumbnail dirs
Verified against real files pulled from are-nas: confirmed the tool correctly leaves already-correct EXIF-dated photos untouched and only flags the ones actually missing real metadata (validates the earlier manual fix was precisely targeted). Recursive --dir walk now prunes @eaDir/#recycle/#snapshot/.SynologyWorkingDirectory instead of wasting time on generated thumbnails. Fixed a misleading "no files given" error when --dir was given but --only-photo/--only-video filtered everything out. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -39,6 +39,8 @@ Usage:
|
||||
./set_mtime_from_metadata.py --dry-run *.mov
|
||||
./set_mtime_from_metadata.py --dir /path/to/library # recursive, prompts before applying
|
||||
./set_mtime_from_metadata.py --dir /path/to/library --yes # recursive, unattended
|
||||
./set_mtime_from_metadata.py --dir /path/to/library --only-photo --dry-run
|
||||
./set_mtime_from_metadata.py --dir /path/to/library --only-video --yes
|
||||
|
||||
Requirements:
|
||||
- ffprobe on PATH (for .mov/.mp4/.m4v)
|
||||
@@ -57,8 +59,14 @@ QUICKTIME_TAG_PRIORITY = ["com.apple.quicktime.creationdate", "creation_time"]
|
||||
|
||||
QUICKTIME_EXTS = {".mov", ".mp4", ".m4v"}
|
||||
AVI_EXTS = {".avi"}
|
||||
VIDEO_EXTS = QUICKTIME_EXTS | AVI_EXTS
|
||||
EXIF_PHOTO_EXTS = {".jpg", ".jpeg", ".png", ".tif", ".tiff", ".heic", ".heif"}
|
||||
ALL_SUPPORTED_EXTS = QUICKTIME_EXTS | AVI_EXTS | EXIF_PHOTO_EXTS
|
||||
ALL_SUPPORTED_EXTS = VIDEO_EXTS | EXIF_PHOTO_EXTS
|
||||
|
||||
# Synology (and other NAS/sync tool) internal housekeeping directories that
|
||||
# only ever contain generated thumbnails/cache, never real source media --
|
||||
# pruned from --dir's recursive walk so they don't show up as noisy skips.
|
||||
SKIP_DIR_NAMES = {"@eaDir", "#recycle", "#snapshot", ".SynologyWorkingDirectory"}
|
||||
|
||||
# Reject anything dated before this as a clock-never-set placeholder rather
|
||||
# than a real timestamp (observed in the wild: "0000:00:00 00:00:00").
|
||||
@@ -155,10 +163,11 @@ def fetch_creation_time(path):
|
||||
return None
|
||||
|
||||
|
||||
def iter_batch_files(root):
|
||||
for dirpath, _dirnames, filenames in os.walk(root):
|
||||
def iter_batch_files(root, allowed_exts):
|
||||
for dirpath, dirnames, filenames in os.walk(root):
|
||||
dirnames[:] = [d for d in dirnames if d not in SKIP_DIR_NAMES]
|
||||
for name in filenames:
|
||||
if Path(name).suffix.lower() in ALL_SUPPORTED_EXTS:
|
||||
if Path(name).suffix.lower() in allowed_exts:
|
||||
yield Path(dirpath) / name
|
||||
|
||||
|
||||
@@ -180,22 +189,39 @@ def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
parser.add_argument("files", nargs="*", type=Path, help="Media files to fix")
|
||||
parser.add_argument("--dir", type=Path, help="Recursively scan this directory for supported files instead of (or in addition to) explicit FILES")
|
||||
type_group = parser.add_mutually_exclusive_group()
|
||||
type_group.add_argument("--only-photo", action="store_true", help="Only consider photo formats (jpg/jpeg/png/tif/tiff/heic/heif)")
|
||||
type_group.add_argument("--only-video", action="store_true", help="Only consider video formats (mov/mp4/m4v/avi)")
|
||||
parser.add_argument("--dry-run", action="store_true", help="Show what would change without modifying anything or prompting")
|
||||
parser.add_argument("--yes", "-y", "--unattended", dest="yes", action="store_true", help="Apply changes without an interactive confirmation prompt")
|
||||
args = parser.parse_args()
|
||||
|
||||
targets = list(args.files)
|
||||
if not args.files and not args.dir:
|
||||
parser.error("no files given -- pass FILES and/or --dir")
|
||||
|
||||
allowed_exts = EXIF_PHOTO_EXTS if args.only_photo else VIDEO_EXTS if args.only_video else ALL_SUPPORTED_EXTS
|
||||
|
||||
targets = []
|
||||
excluded_by_type = []
|
||||
for f in args.files:
|
||||
if f.suffix.lower() not in allowed_exts:
|
||||
excluded_by_type.append((f, "excluded by --only-photo/--only-video"))
|
||||
continue
|
||||
targets.append(f)
|
||||
if args.dir:
|
||||
if not args.dir.is_dir():
|
||||
print(f"error: --dir {args.dir}: not a directory", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
targets.extend(sorted(iter_batch_files(args.dir)))
|
||||
targets.extend(sorted(iter_batch_files(args.dir, allowed_exts)))
|
||||
if not targets:
|
||||
parser.error("no files given -- pass FILES and/or --dir")
|
||||
for f, err in excluded_by_type:
|
||||
print(f"skip: {f}: {err}", file=sys.stderr)
|
||||
print("nothing to do: no files matched (check --only-photo/--only-video and the extensions under --dir)", file=sys.stderr)
|
||||
sys.exit(1 if excluded_by_type else 0)
|
||||
|
||||
plans = [plan_for(f) for f in targets]
|
||||
ok_plans = [(p, dt) for p, dt, err in plans if err is None]
|
||||
skipped = [(p, err) for p, dt, err in plans if err is not None]
|
||||
skipped = excluded_by_type + [(p, err) for p, dt, err in plans if err is not None]
|
||||
|
||||
for p, err in skipped:
|
||||
print(f"skip: {p}: {err}", file=sys.stderr)
|
||||
|
||||
Reference in New Issue
Block a user