diff --git a/.gitignore b/.gitignore index 5d381cc..dab5811 100644 --- a/.gitignore +++ b/.gitignore @@ -160,3 +160,5 @@ cython_debug/ # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ +mapping.json +.vscode \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c32235e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +Pillow +pytimeparse \ No newline at end of file diff --git a/sorter.py b/sorter.py new file mode 100644 index 0000000..6012172 --- /dev/null +++ b/sorter.py @@ -0,0 +1,81 @@ +from datetime import datetime, timedelta +from os import listdir +from os.path import exists, isfile, join, splitext +from PIL import Image, ExifTags +from pytimeparse.timeparse import timeparse +from shutil import copy +import json + + +class ImageSorter: + def __init__(self, args): + self.args = args + self.mapping = self.read_mapping_config() if args.config_mapping_file else {} + self.verbose = self.args.verbose + + def sort_files(self): + self.print_log(f"Reading files from {self.args.input_dir}") + + files = [f for f in listdir(self.args.input_dir) if isfile(join(self.args.input_dir, f))] + for file in files: + filepath = join(self.args.input_dir, file) + self.print_log(f"Start handling file {filepath}") + if splitext(file)[1].lower() != ".jpg": + continue + + date_taken = self.get_date_taken(filepath) + if date_taken: + self.write_file(filepath, date_taken) + + def get_date_taken(self, file): + try: + with Image.open(file) as img: + exif = {ExifTags.TAGS[k]: v for k, v in img._getexif().items() if k in ExifTags.TAGS} + camera_model = exif.get("Model") + self.print_log(f"Camera model is {camera_model}") + + date_str = exif.get("DateTime") + if not date_str: + self.print_log("No DateTime EXIF data found") + return None + + date_taken = datetime.strptime(date_str, "%Y:%m:%d %H:%M:%S") + self.print_log(f"Original date taken is {date_taken.strftime('%Y-%m-%d %H:%M:%S')}") + return date_taken + self.get_camera_model_time_delta(camera_model) + except Exception as e: + self.print_log(f"Error reading EXIF data from {file}: {e}") + return None + + def read_mapping_config(self): + try: + with open(self.args.config_mapping_file, "r") as f: + return json.load(f) + except Exception as e: + self.print_log(f"Error reading config mapping file: {e}") + return {} + + def get_camera_model_time_delta(self, camera_model): + time_str = self.mapping.get(camera_model, "0s") + seconds = timeparse(time_str) + self.print_log(f"Found timedelta {seconds} seconds for camera model {camera_model}") + return timedelta(seconds=seconds) + + def write_file(self, filepath, date_taken): + base_name = f"Picture_{date_taken.strftime('%Y%m%d_%H%M%S')}" + new_file_name = f"{base_name}_{self.args.suffix}" if self.args.suffix else base_name + + count = 1 + final_file_name = new_file_name + while exists(join(self.args.output_dir, f"{final_file_name}.jpg")): + final_file_name = f"{new_file_name} ({count})" + count += 1 + self.print_log(f"File already exists, trying {final_file_name}.jpg instead") + + output_file = join(self.args.output_dir, f"{final_file_name}.jpg") + copy(filepath, output_file) + self.print_log(f"Copied {filepath} to {output_file}") + self.print_log() + + def print_log(self, msg=""): + if self.verbose: + print(msg) diff --git a/start.py b/start.py new file mode 100644 index 0000000..cd88dd5 --- /dev/null +++ b/start.py @@ -0,0 +1,14 @@ +from argparse import ArgumentParser +from sorter import ImageSorter + +if __name__ == '__main__': + parser = ArgumentParser(prog="image-sorter", description="A tool for renaming images to the date from the exif data") + parser.add_argument("-i", "--input_dir", required=True, type=str) + parser.add_argument("-o", "--output_dir", required=True, type=str) + parser.add_argument("-c", "--config_mapping_file", required=True, type=str) + parser.add_argument("-s", "--suffix", type=str) + parser.add_argument("-v", "--verbose", action="store_true") + + args = parser.parse_args() + sorter = ImageSorter(args) + sorter.sort_files() \ No newline at end of file