diff --git a/main.py b/main.py index d45c4e5..f020e05 100644 --- a/main.py +++ b/main.py @@ -2,203 +2,158 @@ import json import os from exiftool import ExifToolHelper as et from shutil import copy2 as cp -import datetime from datetime import datetime as dt import argparse -from PIL import Image, ImageDraw, ImageFont -def init_parser(): +def init_parser() -> argparse.Namespace: """ - Initializes the argparse module + Initializes the argparse module. """ parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter) - parser.add_argument('-t', '--timespan', type=str, help="Exports the given timespan\n"\ - "Valid format: 'DD.MM.YYYY-DD.MM.YYYY'\n"\ + parser.add_argument('-t', '--timespan', type=str, help="Exports the given timespan\n" + "Valid format: 'DD.MM.YYYY-DD.MM.YYYY'\n" "Wildcards can be used: 'DD.MM.YYYY-*'") parser.add_argument('-y', '--year', type=int, help="Exports the given year") parser.add_argument('-p', '--path', type=str, help="Set a custom output path (default ./out)") parser.add_argument('-v', '--verbose', action='store_true', default=False, help="Explain what is being done") parser.add_argument('--no-memories', action='store_false', default=True, dest='memories', help="Don't export the memories") parser.add_argument('--no-realmojis', action='store_false', default=True, dest='realmojis', help="Don't export the realmojis") - args = parser.parse_args() if args.year and args.timespan: - print("Timespan argument will be prioritized") - + print("Timespan argument will be prioritized") return args -def init_global_var(args: argparse.Namespace): - """ - Initializes global variables - """ - global time_span - global out_path - global verbose - - # Initialize time_span - if args.timespan: - temp_times = args.timespan.strip().split("-") - time_span = (dt.fromtimestamp(0) if temp_times[0] == '*' else dt.strptime(temp_times[0], '%d.%m.%Y'), - dt.now() if temp_times[1] == '*' else dt.strptime(temp_times[1], '%d.%m.%Y')) - elif args.year: - time_span = (dt(args.year, 1, 1), dt(args.year, 12, 31)) - else: - time_span = (dt.fromtimestamp(0), dt.now()) - - # Initialize out_path - if args.path: - out_path = args.path.strip().removesuffix('/') - else: - out_path = "./out" - - verbose = args.verbose - - -def verbose_msg(msg: str): - """ - Prints an explanation what is being done to the terminal - """ - if verbose: print(msg) - - -def printProgressBar (iteration: int, total: int, prefix: str='', suffix: str='', decimals: str=1, length: int=60, fill: str='█', printEnd: str="\r"): +class BeRealExporter: + def __init__(self, args: argparse.Namespace): + self.time_span = self.init_time_span(args) + self.out_path = args.path.strip().removesuffix('/') if args.path else "./out" + self.verbose = args.verbose + @staticmethod + def init_time_span(args: argparse.Namespace) -> tuple: """ - Call in a loop to create terminal progress bar - Not my creation: https://stackoverflow.com/questions/3173320/text-progress-bar-in-terminal-with-block-characters - @params: - iteration - Required : current iteration (Int) - total - Required : total iterations (Int) - prefix - Optional : prefix string (Str) - suffix - Optional : suffix string (Str) - decimals - Optional : positive number of decimals in percent complete (Int) - length - Optional : character length of bar (Int) - fill - Optional : bar fill character (Str) - printEnd - Optional : end character (e.g. "\r", "\r\n") (Str) + Initializes time span based on the arguments. + """ + if args.timespan: + start_str, end_str = args.timespan.strip().split("-") + start = dt.fromtimestamp(0) if start_str == '*' else dt.strptime(start_str, '%d.%m.%Y') + end = dt.now() if end_str == '*' else dt.strptime(end_str, '%d.%m.%Y') + return start, end + elif args.year: + return dt(args.year, 1, 1), dt(args.year, 12, 31) + else: + return dt.fromtimestamp(0), dt.now() + def verbose_msg(self, msg: str): + """ + Prints an explanation of what is being done to the terminal. + """ + if self.verbose: + print(msg) + @staticmethod + def print_progress_bar(iteration: int, total: int, prefix: str = '', suffix: str = '', decimals: int = 1, length: int = 60, fill: str = '█', print_end: str = "\r"): + """ + Call in a loop to create terminal progress bar. """ percent = ("{0:." + str(decimals) + "f}").format(100 * (iteration / float(total))) - filledLength = int(length * iteration // total) - bar = fill * filledLength + '-' * (length - filledLength) - print(f'\r{prefix} |{bar}| {percent}% {suffix}', end = printEnd) - # Print New Line on Complete - if iteration == total: + filled_length = int(length * iteration // total) + bar = fill * filled_length + '-' * (length - filled_length) + print(f'\r{prefix} |{bar}| {percent}% {suffix}', end=print_end) + if iteration == total: print() - - -def get_img_filename(image: json): - """ - Returns the image filename from a image object (frontImage, backImage, primary, secondary) - """ - return image['path'].split("/")[-1] - - -def get_datetime_from_str(time: str): - """ - Returns a datetime object form a time key - """ - format_string = "%Y-%m-%dT%H:%M:%S.%fZ" - return dt.strptime(time, format_string) - - -def export_img(old_img_name: str, img_name: str, img_dt: datetime, img_location=None): - """ - Makes a copy of the image and adds exif tags to the image - """ - - # Makes a copy of the images - verbose_msg("Export %s image to %s" % (old_img_name, img_name)) - cp(old_img_name, img_name) - - # Add metadata to the images with or without location - if img_location: - verbose_msg("Add metadata to image:\n - DateTimeOriginal=%s\n - GPS=(%s, %s)" % (img_dt, img_location['latitude'], img_location['longitude'])) - et().set_tags(img_name, - tags={"DateTimeOriginal": img_dt.strftime("%Y:%m:%d %H:%M:%S"), - "GPSLatitude*": img_location['latitude'], - "GPSLongitude*": img_location['longitude']}, - params=["-P", "-overwrite_original"]) - else: - verbose_msg("Add metadata to image:\n - DateTimeOriginal=%s" % img_dt) - et().set_tags(img_name, - tags={"DateTimeOriginal": img_dt.strftime("%Y:%m:%d %H:%M:%S")}, - params=["-P", "-overwrite_original"]) - - -def export_memories(memories: json): - """ - Exports all memories from the Photos/post directory to the corresponding output folder - """ - out_path_memories = out_path + "/memories" - memory_count = len(memories) - - if not os.path.exists(out_path_memories): - verbose_msg("Create %s folder for memories output" % out_path_memories) - os.makedirs(out_path_memories) - - for i, n in zip(memories, range(memory_count)): - memory_dt = get_datetime_from_str(i['takenTime']) - types = [('frontImage', 'webp'), ('backImage', 'webp')] - if 'btsMedia' in i: - types.append(('btsMedia', 'mp4')) - img_names = ["%s/%s_%s.%s" % (out_path_memories, memory_dt.strftime('%Y-%m-%d_%H-%M-%S'), type[0].removesuffix('Image').removesuffix('Media'), type[1]) - for type in types] - - # Checks if the memory is in the time span - if time_span[0] <= memory_dt <= time_span[1]: - for img_name, type in zip(img_names, types): - old_img_name = "./Photos/post/" + get_img_filename(i[type[0]]) - - verbose_msg("\nExport Memory nr %s %s:" % (n, type[0])) - if 'location' in i: - export_img(old_img_name, img_name, memory_dt, i['location']) - else: - export_img(old_img_name, img_name, memory_dt) - - if verbose: - printProgressBar(n+1, memory_count, prefix="Exporting Memories", suffix=("- " + memory_dt.strftime("%Y-%m-%d")), printEnd='\n') + @staticmethod + def get_img_filename(image: dict) -> str: + """ + Returns the image filename from an image object (frontImage, backImage, primary, secondary). + """ + return image['path'].split("/")[-1] + @staticmethod + def get_datetime_from_str(time: str) -> dt: + """ + Returns a datetime object from a time key. + """ + format_string = "%Y-%m-%dT%H:%M:%S.%fZ" + return dt.strptime(time, format_string) + def export_img(self, old_img_name: str, img_name: str, img_dt: dt, img_location=None): + """ + Makes a copy of the image and adds EXIF tags to the image. + """ + self.verbose_msg(f"Export {old_img_name} image to {img_name}") + cp(old_img_name, img_name) + tags = {"DateTimeOriginal": img_dt.strftime("%Y:%m:%d %H:%M:%S")} + if img_location: + self.verbose_msg(f"Add metadata to image:\n - DateTimeOriginal={img_dt}\n - GPS=({img_location['latitude']}, {img_location['longitude']})") + tags.update({ + "GPSLatitude*": img_location['latitude'], + "GPSLongitude*": img_location['longitude'] + }) else: - printProgressBar(n+1, memory_count, prefix="Exporting Memories", suffix=("- " + memory_dt.strftime("%Y-%m-%d"))) - - -def export_realmojis(realmojis: json): - """ - Exports all realmojis from the Photos/realmoji directory to the corresponding output folder - """ - realmoji_count = len(realmojis) - out_path_realmojis = out_path + "/realmojis" - - if not os.path.exists(out_path_realmojis): - verbose_msg("Create %s folder for memories output" % out_path_realmojis) - os.makedirs(out_path_realmojis) - - for i, n in zip(realmojis, range(realmoji_count)): - realmoji_dt = get_datetime_from_str(i['postedAt']) - img_name = "%s/%s.webp" % (out_path_realmojis, realmoji_dt.strftime('%Y-%m-%d_%H-%M-%S')) - - # Checks if the realmojis is in the time span - if time_span[0] <= realmoji_dt <= time_span[1] and i['isInstant']: - verbose_msg("\nExport Memory nr %s:" % n) - export_img("./Photos/realmoji/" + get_img_filename(i['media']), img_name, realmoji_dt) - - if verbose: - printProgressBar(n+1, realmoji_count, prefix="Exporting Realmojis", suffix=("- Current Date: %s" % realmoji_dt.strftime("%Y-%m-%d")), printEnd='\n') - else: - printProgressBar(n+1, realmoji_count, prefix="Exporting Realmojis", suffix=("- Current Date: %s" % realmoji_dt.strftime("%Y-%m-%d"))) + self.verbose_msg(f"Add metadata to image:\n - DateTimeOriginal={img_dt}") + et().set_tags(img_name, tags=tags, params=["-P", "-overwrite_original"]) + def export_memories(self, memories: list): + """ + Exports all memories from the Photos/post directory to the corresponding output folder. + """ + out_path_memories = os.path.join(self.out_path, "memories") + memory_count = len(memories) + if not os.path.exists(out_path_memories): + self.verbose_msg(f"Create {out_path_memories} folder for memories output") + os.makedirs(out_path_memories) + for n, memory in enumerate(memories): + memory_dt = self.get_datetime_from_str(memory['takenTime']) + types = [('frontImage', 'webp'), ('backImage', 'webp')] + if 'btsMedia' in memory: + types.append(('btsMedia', 'mp4')) + img_names = [f"{out_path_memories}/{memory_dt.strftime('%Y-%m-%d_%H-%M-%S')}_{t[0].removesuffix('Image').removesuffix('Media')}.{t[1]}" for t in types] + if self.time_span[0] <= memory_dt <= self.time_span[1]: + for img_name, type in zip(img_names, types): + old_img_name = f"./Photos/post/{self.get_img_filename(memory[type[0]])}" + self.verbose_msg(f"\nExport Memory nr {n} {type[0]}:") + if 'location' in memory: + self.export_img(old_img_name, img_name, memory_dt, memory['location']) + else: + self.export_img(old_img_name, img_name, memory_dt) + self.print_progress_bar(n + 1, memory_count, prefix="Exporting Memories", suffix=f"- {memory_dt.strftime('%Y-%m-%d')}") + def export_realmojis(self, realmojis: list): + """ + Exports all realmojis from the Photos/realmoji directory to the corresponding output folder. + """ + realmoji_count = len(realmojis) + out_path_realmojis = os.path.join(self.out_path, "realmojis") + if not os.path.exists(out_path_realmojis): + self.verbose_msg(f"Create {out_path_realmojis} folder for memories output") + os.makedirs(out_path_realmojis) + for n, realmoji in enumerate(realmojis): + realmoji_dt = self.get_datetime_from_str(realmoji['postedAt']) + img_name = f"{out_path_realmojis}/{realmoji_dt.strftime('%Y-%m-%d_%H-%M-%S')}.webp" + if self.time_span[0] <= realmoji_dt <= self.time_span[1] and realmoji['isInstant']: + self.verbose_msg(f"\nExport Realmoji nr {n}:") + self.export_img(f"./Photos/realmoji/{self.get_img_filename(realmoji['media'])}", img_name, realmoji_dt) + self.print_progress_bar(n + 1, realmoji_count, prefix="Exporting Realmojis", suffix=f"- Current Date: {realmoji_dt.strftime('%Y-%m-%d')}") if __name__ == '__main__': args = init_parser() - init_global_var(args) + exporter = BeRealExporter(args) if args.memories: - verbose_msg("Open memories.json file") - with open('memories.json', encoding='utf-8') as memories: - verbose_msg("Start exporting memories") - export_memories(json.load(memories)) + exporter.verbose_msg("Open memories.json file") + try: + with open('memories.json', encoding='utf-8') as memories_file: + exporter.verbose_msg("Start exporting memories") + exporter.export_memories(json.load(memories_file)) + except FileNotFoundError: + print("memories.json file not found.") + except json.JSONDecodeError: + print("Error decoding memories.json file.") if args.realmojis: - verbose_msg("Open realmojis.json file") - with open('realmojis.json', encoding='utf-8') as realmojis: - verbose_msg("Start exporting realmojis") - export_realmojis(json.load(realmojis)) \ No newline at end of file + exporter.verbose_msg("Open realmojis.json file") + try: + with open('realmojis.json', encoding='utf-8') as realmojis_file: + exporter.verbose_msg("Start exporting realmojis") + exporter.export_realmojis(json.load(realmojis_file)) + except FileNotFoundError: + print("realmojis.json file not found.") + except json.JSONDecodeError: + print("Error decoding realmojis.json file.") \ No newline at end of file