diff --git a/src/emulsion/main.py b/src/emulsion/main.py index 7c62484..63c5704 100644 --- a/src/emulsion/main.py +++ b/src/emulsion/main.py @@ -94,10 +94,16 @@ def prompt_if_missing(args): # Same prompts as config, plus base_date prompt_for_config(args) try: - if not args.base_date: + while not args.base_date: dflt = datetime.datetime.now().strftime("%Y-%m-%d") resp = input(f"Base date/time for first image [{dflt}]: ").strip() args.base_date = resp if resp else dflt + # Validate immediately so we don't crash later + try: + parse_user_date(args.base_date) + except ValueError: + print("Invalid format. Please use 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'.") + args.base_date = None except KeyboardInterrupt: print("\nInterrupted by user. Exiting.") sys.exit(1) @@ -111,10 +117,10 @@ def parse_user_date(dt_str): return datetime.datetime.strptime(dt_str, "%Y-%m-%d") -def build_exiftool_cmd(file_path, author, lab, make, model, film, timestamp): +def build_exiftool_cmd(file_path, author, lab, make, model, film, timestamp, sidecar_source=None): """ - Builds the command list. Does NOT handle dry_run formatting. - Always returns a list for subprocess safety. + Builds the command list. + sidecar_source: If set, we are creating/updating a sidecar FROM this source image. """ current_year = datetime.datetime.now().year cmd = [ @@ -142,7 +148,15 @@ def build_exiftool_cmd(file_path, author, lab, make, model, film, timestamp): cmd.append(f"-UserComment={film}") cmd.append(f"-XMP:Description={film}") - cmd.append(file_path) + if sidecar_source: + # Advanced ExifTool usage: read source, write to specific sidecar file + # This ensures it works even if the sidecar doesn't exist yet + cmd.append(f"-srcfile") + cmd.append(file_path) + cmd.append(sidecar_source) + else: + cmd.append(file_path) + return cmd @@ -166,6 +180,8 @@ def run_exiftool(cmd, dry_run=False): return True, "Updated" except subprocess.CalledProcessError as e: return False, f"Error: {e}" + except FileNotFoundError: + return False, "Error: 'exiftool' not found. Please install it." def create_config_file(args): @@ -201,9 +217,6 @@ def main(): if args.workers is None: args.workers = os.cpu_count() or 1 - # LOGIC SIMPLIFICATION 1: - # Instead of writing a separate sequential loop for dry-runs, - # we just force workers=1 here. The executor handles the rest. if args.dry_run: print("Dry run detected: Forcing sequential processing.") args.workers = 1 @@ -243,8 +256,12 @@ def main(): ts_dt = base_dt + datetime.timedelta(seconds=i * time_increment) timestamp_str = ts_dt.strftime("%Y:%m:%d %H:%M:%S") + sidecar_source = None if args.sidecar: target_file_path = f"{f}.xmp" + # If sidecar doesn't exist, we must tell ExifTool to create it from the source image + if not os.path.exists(target_file_path): + sidecar_source = f else: target_file_path = f @@ -255,14 +272,14 @@ def main(): make=args.make, model=args.model, film=args.film, - timestamp=timestamp_str + timestamp=timestamp_str, + sidecar_source=sidecar_source ) tasks.append((cmd, f, timestamp_str)) with ThreadPoolExecutor(max_workers=args.workers) as executor: # Submit all tasks - # Note: We pass args.dry_run into the function here futures = { executor.submit(run_exiftool, cmd, args.dry_run): (f, ts) for cmd, f, ts in tasks @@ -274,8 +291,6 @@ def main(): success, msg = future.result() if args.dry_run: - # For dry run, we PRINT the command so the user can copy it - # bar.text() is transient, print() persists in terminal print(msg) elif not success: bar.text(f"Failed {original_file}: {msg}")