mirror of
https://github.com/SoPat712/BeReal-Export-Manager.git
synced 2025-08-21 18:28:46 -04:00
263 lines
8.9 KiB
Python
263 lines
8.9 KiB
Python
import argparse
|
|
import json
|
|
import os
|
|
from datetime import datetime as dt
|
|
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
|
|
|
|
|
|
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.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)}")
|
|
|
|
|
|
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.")
|
|
|
|
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.")
|