work for new and old files 09-18-2024 and before

This commit is contained in:
Josh Patra
2024-12-25 14:25:36 -05:00
parent e88165b96b
commit 0b106ed32f
3 changed files with 257 additions and 183 deletions

8
.gitignore vendored
View File

@@ -6,6 +6,14 @@ __pycache__/
# C extensions # C extensions
*.so *.so
# json files
*.json
# Input/Output Files
Photos/
out/
correctout/
# Distribution / packaging # Distribution / packaging
.Python .Python
build/ build/

View File

@@ -1,10 +1,10 @@
import argparse
import json import json
import os import os
from exiftool import ExifToolHelper as et
from shutil import copy2 as cp
from datetime import datetime as dt from datetime import datetime as dt
import argparse from shutil import copy2 as cp
from exiftool import ExifToolHelper as et
def init_parser() -> argparse.Namespace: def init_parser() -> argparse.Namespace:
@@ -12,16 +12,57 @@ def init_parser() -> argparse.Namespace:
Initializes the argparse module. Initializes the argparse module.
""" """
parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter) parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter)
parser.add_argument('-v', '--verbose', default=False, action='store_true', help="Explain what is being done") parser.add_argument(
parser.add_argument('--exiftool-path', dest='exiftool_path', type=str, help="Set the path to the ExifTool executable (needed if it isn't on the $PATH)") "-v",
parser.add_argument('-t', '--timespan', type=str, help="Exports the given timespan\n" "--verbose",
default=False,
action="store_true",
help="Explain what is being done",
)
parser.add_argument(
"--exiftool-path",
dest="exiftool_path",
type=str,
help="Set the path to the ExifTool executable (needed if it isn't on the $PATH)",
)
parser.add_argument(
"-t",
"--timespan",
type=str,
help="Exports the given timespan\n"
"Valid format: 'DD.MM.YYYY-DD.MM.YYYY'\n" "Valid format: 'DD.MM.YYYY-DD.MM.YYYY'\n"
"Wildcards can be used: 'DD.MM.YYYY-*'") "Wildcards can be used: 'DD.MM.YYYY-*'",
parser.add_argument('-y', '--year', type=int, help="Exports the given year") )
parser.add_argument('-p', '--out-path', dest='out_path', type=str, default="./out", help="Set a custom output path (default ./out)") parser.add_argument("-y", "--year", type=int, help="Exports the given year")
parser.add_argument('--bereal-path', dest='bereal_path', type=str, default=".", help="Set a custom BeReal path (default ./)") parser.add_argument(
parser.add_argument('--no-memories', dest='memories', default=True, action='store_false', help="Don't export the memories") "-p",
parser.add_argument('--no-realmojis', dest='realmojis', default=True, action='store_false', help="Don't export the realmojis") "--out-path",
dest="out_path",
type=str,
default="./out",
help="Set a custom output path (default ./out)",
)
parser.add_argument(
"--bereal-path",
dest="bereal_path",
type=str,
default=".",
help="Set a custom BeReal path (default ./)",
)
parser.add_argument(
"--no-memories",
dest="memories",
default=True,
action="store_false",
help="Don't export the memories",
)
parser.add_argument(
"--no-realmojis",
dest="realmojis",
default=True,
action="store_false",
help="Don't export the realmojis",
)
args = parser.parse_args() args = parser.parse_args()
if args.year and args.timespan: if args.year and args.timespan:
print("Timespan argument will be prioritized") print("Timespan argument will be prioritized")
@@ -32,27 +73,34 @@ class BeRealExporter:
def __init__(self, args: argparse.Namespace): def __init__(self, args: argparse.Namespace):
self.time_span = self.init_time_span(args) self.time_span = self.init_time_span(args)
self.exiftool_path = args.exiftool_path self.exiftool_path = args.exiftool_path
self.out_path = args.out_path.strip().removesuffix('/') self.out_path = args.out_path.rstrip("/")
self.bereal_path = args.bereal_path.strip().removesuffix('/') self.bereal_path = args.bereal_path.rstrip("/")
self.verbose = args.verbose self.verbose = args.verbose
@staticmethod @staticmethod
def init_time_span(args: argparse.Namespace) -> tuple: def init_time_span(args: argparse.Namespace) -> tuple:
""" """
Initializes time span based on the arguments. Initializes time span based on the arguments.
""" """
if args.timespan: if args.timespan:
try:
start_str, end_str = args.timespan.strip().split("-") start_str, end_str = args.timespan.strip().split("-")
start = dt.fromtimestamp(0) if start_str == '*' else dt.strptime(start_str, '%d.%m.%Y') start = (
end = dt.now() if end_str == '*' else dt.strptime(end_str, '%d.%m.%Y') 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 return start, end
except ValueError:
raise ValueError(
"Invalid timespan format. Use 'DD.MM.YYYY-DD.MM.YYYY'."
)
elif args.year: elif args.year:
return dt(args.year, 1, 1), dt(args.year, 12, 31) return dt(args.year, 1, 1), dt(args.year, 12, 31)
else: else:
return dt.fromtimestamp(0), dt.now() return dt.fromtimestamp(0), dt.now()
def verbose_msg(self, msg: str): def verbose_msg(self, msg: str):
""" """
Prints an explanation of what is being done to the terminal. Prints an explanation of what is being done to the terminal.
@@ -60,137 +108,155 @@ class BeRealExporter:
if self.verbose: if self.verbose:
print(msg) 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.
Not my creation: https://stackoverflow.com/questions/3173320/text-progress-bar-in-terminal-with-block-characters
"""
percent = ("{0:." + str(decimals) + "f}").format(100 * (iteration / float(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()
@staticmethod @staticmethod
def get_img_filename(image: dict) -> str: def get_img_filename(image: dict) -> str:
""" """
Returns the image filename from an image object (frontImage, backImage, primary, secondary). Returns the image filename from an image object (frontImage, backImage, primary, secondary).
""" """
return image['path'].split("/")[-1] return os.path.basename(image["path"])
@staticmethod @staticmethod
def get_datetime_from_str(time: str) -> dt: def get_datetime_from_str(time: str) -> dt:
""" """
Returns a datetime object from a time key. Returns a datetime object from a time key.
""" """
try:
format_string = "%Y-%m-%dT%H:%M:%S.%fZ" format_string = "%Y-%m-%dT%H:%M:%S.%fZ"
return dt.strptime(time, format_string) return dt.strptime(time, format_string)
except ValueError:
raise ValueError(f"Invalid datetime format: {time}")
def export_img(
def export_img(self, old_img_name: str, img_name: str, img_dt: dt, img_location=None): 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. 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}") self.verbose_msg(f"Exporting {old_img_name} to {img_name}")
if os.path.isfile(old_img_name): # Adjust path if not found in the given location
if not os.path.isfile(old_img_name):
# Older format fallback
fallback_img_name = os.path.join(
self.bereal_path, "Photos/bereal", os.path.basename(old_img_name)
)
if os.path.isfile(fallback_img_name):
old_img_name = fallback_img_name
else:
# Newer format fallback
newer_img_name = os.path.join(
self.bereal_path, "Photos/post", os.path.basename(old_img_name)
)
if os.path.isfile(newer_img_name):
old_img_name = newer_img_name
else:
print(f"File not found in expected locations: {old_img_name}")
return
# Create output directory and copy file
os.makedirs(os.path.dirname(img_name), exist_ok=True)
cp(old_img_name, img_name) cp(old_img_name, img_name)
# Add EXIF metadata
tags = {"DateTimeOriginal": img_dt.strftime("%Y:%m:%d %H:%M:%S")} tags = {"DateTimeOriginal": img_dt.strftime("%Y:%m:%d %H:%M:%S")}
if img_location: 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(
tags.update({ {
"GPSLatitude*": img_location['latitude'], "GPSLatitude": img_location["latitude"],
"GPSLongitude*": img_location['longitude'] "GPSLongitude": img_location["longitude"],
}) }
else: )
self.verbose_msg(f"Add metadata to image:\n - DateTimeOriginal={img_dt}")
if self.exiftool_path:
et(executable=self.exiftool_path).set_tags(img_name, tags=tags, params=["-P", "-overwrite_original"])
else:
et().set_tags(img_name, tags=tags, params=["-P", "-overwrite_original"])
else:
self.verbose_msg(f"File {old_img_name} not found. Skipping this image.")
try:
with (
et(executable=self.exiftool_path) if self.exiftool_path else et()
) as exif_tool:
exif_tool.set_tags(
img_name, tags=tags, params=["-P", "-overwrite_original"]
)
self.verbose_msg(f"Metadata added to {img_name}")
except Exception as e:
print(f"Error adding metadata to {img_name}: {e}")
def export_memories(self, memories: list): def export_memories(self, memories: list):
""" """
Exports all memories from the Photos/post directory to the corresponding output folder. Exports all memories from the Photos directory to the corresponding output folder.
""" """
out_path_memories = os.path.join(self.out_path, "memories") out_path_memories = os.path.join(self.out_path, "memories")
memory_count = len(memories) os.makedirs(out_path_memories, exist_ok=True)
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 i, memory in enumerate(memories): for i, memory in enumerate(memories, start=1):
memory_dt = self.get_datetime_from_str(memory['takenTime']) memory_dt = self.get_datetime_from_str(memory["takenTime"])
types = [('frontImage', 'webp'), ('backImage', 'webp')] if not (self.time_span[0] <= memory_dt <= self.time_span[1]):
if 'btsMedia' in memory: continue
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]: # Loop through the front and back images
for img_name, type in zip(img_names, types): for img_type, extension in [("frontImage", "webp"), ("backImage", "webp")]:
old_img_name = os.path.join(self.bereal_path, f"Photos/post/{self.get_img_filename(memory[type[0]])}") # Handle both older and newer formats
self.verbose_msg(f"Export Memory nr {i} {type[0]}:") img_path = memory[img_type]["path"]
if 'location' in memory: if img_path.startswith("/"):
self.export_img(old_img_name, img_name, memory_dt, memory['location']) img_path = img_path[1:] # Remove leading slash if present
else:
self.export_img(old_img_name, img_name, memory_dt)
self.print_progress_bar(i + 1, memory_count, prefix="Exporting Memories", suffix=f"- {memory_dt.strftime('%Y-%m-%d')}") # Construct output filename
self.verbose_msg(f"\n\n{'#'*100}\n") img_name = f"{out_path_memories}/{memory_dt.strftime('%Y-%m-%d_%H-%M-%S')}_{img_type.replace('Image', '').lower()}.{extension}"
# Construct the old image path
old_img_name = os.path.join(self.bereal_path, img_path)
# Export image
img_location = memory.get("location", None)
self.export_img(old_img_name, img_name, memory_dt, img_location)
self.verbose_msg(f"Exported memory {i}/{len(memories)}")
def export_realmojis(self, realmojis: list): def export_realmojis(self, realmojis: list):
""" """
Exports all realmojis from the Photos/realmoji directory to the corresponding output folder. Exports all realmojis from the Photos directory to the corresponding output folder.
""" """
realmoji_count = len(realmojis)
out_path_realmojis = os.path.join(self.out_path, "realmojis") out_path_realmojis = os.path.join(self.out_path, "realmojis")
if not os.path.exists(out_path_realmojis): os.makedirs(out_path_realmojis, exist_ok=True)
self.verbose_msg(f"Create {out_path_realmojis} folder for memories output")
os.makedirs(out_path_realmojis)
for i, realmoji in enumerate(realmojis): for i, realmoji in enumerate(realmojis, start=1):
realmoji_dt = self.get_datetime_from_str(realmoji['postedAt']) 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 not (self.time_span[0] <= realmoji_dt <= self.time_span[1]):
if self.time_span[0] <= realmoji_dt <= self.time_span[1] and realmoji['isInstant']: continue
self.verbose_msg(f"Export Realmoji nr {i}:")
self.export_img(os.path.join(self.bereal_path, f"Photos/realmoji/{self.get_img_filename(realmoji['media'])}"), img_name, realmoji_dt) img_name = (
self.print_progress_bar(i + 1, realmoji_count, prefix="Exporting Realmojis", suffix=f"- {realmoji_dt.strftime('%Y-%m-%d')}") f"{out_path_realmojis}/{realmoji_dt.strftime('%Y-%m-%d_%H-%M-%S')}.webp"
self.verbose_msg(f"\n\n{'#'*100}\n") )
old_img_name = os.path.join(
self.bereal_path,
realmoji["media"]["path"],
)
self.export_img(old_img_name, img_name, realmoji_dt)
self.verbose_msg(f"Exported realmoji {i}/{len(realmojis)}")
if __name__ == '__main__': if __name__ == "__main__":
args = init_parser() args = init_parser()
exporter = BeRealExporter(args) exporter = BeRealExporter(args)
if args.memories: if args.memories:
exporter.verbose_msg("Open memories.json file")
try: try:
with open(os.path.join(exporter.bereal_path, 'memories.json'), encoding='utf-8') as memories_file: with open(
exporter.verbose_msg("Start exporting memories") os.path.join(args.bereal_path, "memories.json"), encoding="utf-8"
exporter.export_memories(json.load(memories_file)) ) as f:
memories = json.load(f)
exporter.export_memories(memories)
except FileNotFoundError: except FileNotFoundError:
print("memories.json file not found.") print("Error: memories.json file not found.")
except json.JSONDecodeError: except json.JSONDecodeError:
print("Error decoding memories.json file.") print("Error decoding memories.json file.")
if args.realmojis: if args.realmojis:
exporter.verbose_msg("Open realmojis.json file")
try: try:
with open(os.path.join(exporter.bereal_path, 'realmojis.json'), encoding='utf-8') as realmojis_file: with open(
exporter.verbose_msg("Start exporting realmojis") os.path.join(args.bereal_path, "realmojis.json"), encoding="utf-8"
exporter.export_realmojis(json.load(realmojis_file)) ) as f:
realmojis = json.load(f)
exporter.export_realmojis(realmojis)
except FileNotFoundError: except FileNotFoundError:
print("realmojis.json file not found.") print("Error: realmojis.json file not found.")
except json.JSONDecodeError: except json.JSONDecodeError:
print("Error decoding realmojis.json file.") print("Error decoding realmojis.json file.")

View File

@@ -1 +1 @@
pyexiftool=0.5.6 pyexiftool==0.5.6