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
*.so
# json files
*.json
# Input/Output Files
Photos/
out/
correctout/
# Distribution / packaging
.Python
build/

View File

@@ -1,196 +1,262 @@
import argparse
import json
import os
from exiftool import ExifToolHelper as et
from shutil import copy2 as cp
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:
"""
Initializes the argparse module.
"""
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('--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"
"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('--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()
if args.year and args.timespan:
print("Timespan argument will be prioritized")
return args
"""
Initializes the argparse module.
"""
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(
"--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"
"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(
"--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()
if args.year and args.timespan:
print("Timespan argument will be prioritized")
return args
class BeRealExporter:
def __init__(self, args: argparse.Namespace):
self.time_span = self.init_time_span(args)
self.exiftool_path = args.exiftool_path
self.out_path = args.out_path.strip().removesuffix('/')
self.bereal_path = args.bereal_path.strip().removesuffix('/')
self.verbose = args.verbose
def __init__(self, args: argparse.Namespace):
self.time_span = self.init_time_span(args)
self.exiftool_path = args.exiftool_path
self.out_path = args.out_path.rstrip("/")
self.bereal_path = args.bereal_path.rstrip("/")
self.verbose = args.verbose
@staticmethod
def init_time_span(args: argparse.Namespace) -> tuple:
"""
Initializes time span based on the arguments.
"""
if args.timespan:
try:
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
except ValueError:
raise ValueError(
"Invalid timespan format. Use 'DD.MM.YYYY-DD.MM.YYYY'."
)
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 get_img_filename(image: dict) -> str:
"""
Returns the image filename from an image object (frontImage, backImage, primary, secondary).
"""
return os.path.basename(image["path"])
@staticmethod
def get_datetime_from_str(time: str) -> dt:
"""
Returns a datetime object from a time key.
"""
try:
format_string = "%Y-%m-%dT%H:%M:%S.%fZ"
return dt.strptime(time, format_string)
except ValueError:
raise ValueError(f"Invalid datetime format: {time}")
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"Exporting {old_img_name} to {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)
# Add EXIF metadata
tags = {"DateTimeOriginal": img_dt.strftime("%Y:%m:%d %H:%M:%S")}
if img_location:
tags.update(
{
"GPSLatitude": img_location["latitude"],
"GPSLongitude": img_location["longitude"],
}
)
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):
"""
Exports all memories from the Photos directory to the corresponding output folder.
"""
out_path_memories = os.path.join(self.out_path, "memories")
os.makedirs(out_path_memories, exist_ok=True)
for i, memory in enumerate(memories, start=1):
memory_dt = self.get_datetime_from_str(memory["takenTime"])
if not (self.time_span[0] <= memory_dt <= self.time_span[1]):
continue
# Loop through the front and back images
for img_type, extension in [("frontImage", "webp"), ("backImage", "webp")]:
# Handle both older and newer formats
img_path = memory[img_type]["path"]
if img_path.startswith("/"):
img_path = img_path[1:] # Remove leading slash if present
# Construct output filename
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):
"""
Exports all realmojis from the Photos directory to the corresponding output folder.
"""
out_path_realmojis = os.path.join(self.out_path, "realmojis")
os.makedirs(out_path_realmojis, exist_ok=True)
for i, realmoji in enumerate(realmojis, start=1):
realmoji_dt = self.get_datetime_from_str(realmoji["postedAt"])
if not (self.time_span[0] <= realmoji_dt <= self.time_span[1]):
continue
img_name = (
f"{out_path_realmojis}/{realmoji_dt.strftime('%Y-%m-%d_%H-%M-%S')}.webp"
)
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)}")
@staticmethod
def init_time_span(args: argparse.Namespace) -> tuple:
"""
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()
if __name__ == "__main__":
args = init_parser()
exporter = BeRealExporter(args)
if args.memories:
try:
with open(
os.path.join(args.bereal_path, "memories.json"), encoding="utf-8"
) as f:
memories = json.load(f)
exporter.export_memories(memories)
except FileNotFoundError:
print("Error: memories.json file not found.")
except json.JSONDecodeError:
print("Error decoding memories.json file.")
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.
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
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}")
if os.path.isfile(old_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:
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.")
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 i, 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 = os.path.join(self.bereal_path, f"Photos/post/{self.get_img_filename(memory[type[0]])}")
self.verbose_msg(f"Export Memory nr {i} {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(i + 1, memory_count, prefix="Exporting Memories", suffix=f"- {memory_dt.strftime('%Y-%m-%d')}")
self.verbose_msg(f"\n\n{'#'*100}\n")
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 i, 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"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)
self.print_progress_bar(i + 1, realmoji_count, prefix="Exporting Realmojis", suffix=f"- {realmoji_dt.strftime('%Y-%m-%d')}")
self.verbose_msg(f"\n\n{'#'*100}\n")
if __name__ == '__main__':
args = init_parser()
exporter = BeRealExporter(args)
if args.memories:
exporter.verbose_msg("Open memories.json file")
try:
with open(os.path.join(exporter.bereal_path, '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:
exporter.verbose_msg("Open realmojis.json file")
try:
with open(os.path.join(exporter.bereal_path, '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.")
if args.realmojis:
try:
with open(
os.path.join(args.bereal_path, "realmojis.json"), encoding="utf-8"
) as f:
realmojis = json.load(f)
exporter.export_realmojis(realmojis)
except FileNotFoundError:
print("Error: realmojis.json file not found.")
except json.JSONDecodeError:
print("Error decoding realmojis.json file.")

View File

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