diff --git a/src/emulsion/main.py b/src/emulsion/main.py index 66735de..dc0c460 100644 --- a/src/emulsion/main.py +++ b/src/emulsion/main.py @@ -5,6 +5,7 @@ import subprocess import datetime import toml from alive_progress import alive_bar +from concurrent.futures import ThreadPoolExecutor, as_completed CONFIG_PATH = os.path.expanduser("~/.config/emulsion/config.toml") @@ -38,6 +39,7 @@ def parse_args(config): parser.add_argument('--base-date', default=None, help='Base date or date/time (e.g. 2023-04-10 or 2023-04-10 12:00:00).') parser.add_argument('--time-increment', type=int, default=None, help='Time increment in seconds between images.') parser.add_argument('--dry-run', action='store_true', help='Show what would be changed without modifying files.') + parser.add_argument('-j', '--workers', type=int, default=os.cpu_count() or 1, help='Number of parallel workers to run exiftool; defaults to number of CPUs.') parser.add_argument('--init-config', action='store_true', help='Create a default config file (if none exists) and exit.') args = parser.parse_args() @@ -236,6 +238,9 @@ def main(): try: config = load_config() args = parse_args(config) + # Default to number of CPUs if workers not specified + if args.workers is None: + args.workers = os.cpu_count() or 1 if args.init_config: prompt_for_config(args) @@ -261,41 +266,84 @@ def main(): print(f"Processing {total_files} file(s)...") with alive_bar(total_files, title="Tagging files") as bar: - for f in files: - ext = os.path.splitext(f.lower())[1] - if ext not in ['.jpg', '.jpeg', '.tif', '.tiff']: - bar.text(f"Skipping unsupported file: {f}") - bar() - continue + if args.workers > 1 and not args.dry_run: + executor = ThreadPoolExecutor(max_workers=args.workers) + futures = {} + supported_idx = 0 + for f in files: + ext = os.path.splitext(f)[1].lower() + if ext not in ['.jpg', '.jpeg', '.tif', '.tiff']: + bar.text(f"Skipping unsupported file: {f}") + bar() + continue - timestamp_str = current_dt.strftime("%Y:%m:%d %H:%M:%S") - cmd = build_exiftool_cmd( - file_path=f, - author=args.author, - lab=args.lab, - make=args.make, - model=args.model, - film=args.film, - timestamp=timestamp_str, - dry_run=args.dry_run - ) + ts_dt = base_dt + datetime.timedelta(seconds=supported_idx * time_increment) + timestamp_str = ts_dt.strftime("%Y:%m:%d %H:%M:%S") + cmd = build_exiftool_cmd( + file_path=f, + author=args.author, + lab=args.lab, + make=args.make, + model=args.model, + film=args.film, + timestamp=timestamp_str, + dry_run=False + ) + future = executor.submit( + subprocess.run, cmd, check=True, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL + ) + futures[future] = (f, timestamp_str) + supported_idx += 1 - if args.dry_run: - bar.text(f"DRY RUN: {cmd}") - else: + for future in as_completed(futures): + f, ts = futures[future] try: - subprocess.run( - cmd, - check=True, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL - ) - bar.text(f"Updated {f} => {timestamp_str}") + future.result() + bar.text(f"Updated {f} => {ts}") except subprocess.CalledProcessError as e: bar.text(f"Failed to update {f}: {e}") + bar() - current_dt += datetime.timedelta(seconds=time_increment) - bar() + executor.shutdown(wait=True) + else: + supported_idx = 0 + current_dt = base_dt + for f in files: + ext = os.path.splitext(f.lower())[1] + if ext not in ['.jpg', '.jpeg', '.tif', '.tiff']: + bar.text(f"Skipping unsupported file: {f}") + bar() + continue + + timestamp_str = current_dt.strftime("%Y:%m:%d %H:%M:%S") + cmd = build_exiftool_cmd( + file_path=f, + author=args.author, + lab=args.lab, + make=args.make, + model=args.model, + film=args.film, + timestamp=timestamp_str, + dry_run=args.dry_run + ) + + if args.dry_run: + bar.text(f"DRY RUN: {cmd}") + else: + try: + subprocess.run( + cmd, + check=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL + ) + bar.text(f"Updated {f} => {timestamp_str}") + except subprocess.CalledProcessError as e: + bar.text(f"Failed to update {f}: {e}") + + current_dt += datetime.timedelta(seconds=time_increment) + bar() except KeyboardInterrupt: print("\nInterrupted by user. Exiting.")