diff --git a/config/scripts/camera_name_date b/config/scripts/camera_name_date new file mode 100755 index 0000000..eed88e3 --- /dev/null +++ b/config/scripts/camera_name_date @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 + +""" +Same as picture_name_date +except it's tailored for OpenCamera. +It uses filenames, that way: +- Easier to get metadata +- JPG/DNG, MP4/SRT keep the same filename +""" + +import argparse +import logging +import os +import re + +import coloredlogs + +log = logging.getLogger(__name__) +coloredlogs.install(level="DEBUG", fmt="%(levelname)s %(message)s", logger=log) + +PATTERNS = [ + re.compile( # OpenCamera + r"^(?PIMG|VID)_" + # No TRIM. We don't want those as they're a short copy of an existing VID. + r"(?P\d\d\d\d)(?P\d\d)(?P\d\d)_" + r"(?P\d\d)(?P\d\d)(?P\d\d)" + r"(?P_(\d+))?" + r"(?P_(PANO|HDR))?" + r"\.(?Pjpg|dng|mp4|srt)$" + ), + re.compile( # Samsung camera app (?) + r"(?P\d\d\d\d)(?P\d\d)(?P\d\d)_" + r"(?P\d\d)(?P\d\d)(?P\d\d)" + r"\.(?Pjpg|mp4)$" + ), + re.compile( # Telegram Media Downloader photo + r"photo_" + r"(?P\d\d\d\d)-(?P\d\d)-(?P\d\d)_" + r"(?P\d\d)-(?P\d\d)-(?P\d\d)" + r"(?P_\d{19})" + r"\.(?Pjpg)$" + ), # Time of publication, not time of photo! + re.compile( # Telegram Media Downloader video + r"video_" + r"(?P\d\d\d\d)-(?P\d\d)-(?P\d\d)_" + r"(?P\d\d)-(?P\d\d)-(?P\d\d)" + r"(?P_\d{19})" + r"\.(?Pmp4)$" + ), # Time of publication, not time of video! +] + + +def main(args: argparse.Namespace) -> None: + for root, _, files in os.walk(args.dir): + for filename in files: + full_path = os.path.join(root, filename) + for pattern in PATTERNS: + match = re.match(pattern, filename) + if match: + break + else: + log.warning(f"{full_path} doesn't any pattern") + continue + + # Build new filename + m = match.groupdict() + new_filename = ( + f"{m['Y']}-{m['M']}-{m['D']}_" + f"{m['h']}-{m['m']}-{m['s']}" + f"{m.get('dup', '')}" + f"{m.get('spec', '')}" + f"{args.suffix}" + f".{m['ext']}" + ) + new_path = os.path.join(args.dir, new_filename) + # TODO Allow keeping image in same folder + + # Rename file + if full_path == new_path: + log.debug(f"{full_path} already at required filename") + continue + log.info(f"{full_path} →\t{new_path}") + if os.path.exists(new_path): + raise FileExistsError(f"{new_path} already exists!") + if not args.dry: + os.rename(full_path, new_path) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Rename OpenCamera files based on their dates" + ) + parser.add_argument( + "dir", + metavar="DIRECTORY", + type=str, + default=".", + nargs="?", + help="Directory containing the pictures", + ) + parser.add_argument( + "-d", + "--dry", + action="store_true", + help="Do not actually rename, just show old and new path", + ) + parser.add_argument( + "-s", + "--suffix", + default="", + help="Text to add before the extension", + ) + args = parser.parse_args() + main(args) diff --git a/config/scripts/picture_name_date b/config/scripts/picture_name_date index 55cf027..4cf0df8 100755 --- a/config/scripts/picture_name_date +++ b/config/scripts/picture_name_date @@ -1,20 +1,22 @@ #!/usr/bin/env python3 +import argparse +import datetime +import logging import os import re import typing -import datetime -import PIL.ExifTags -import PIL.Image +import coloredlogs +import exifread import progressbar -EXTENSION_PATTERN = re.compile(r"\.JPE?G", re.I) +log = logging.getLogger(__name__) +coloredlogs.install(level="DEBUG", fmt="%(levelname)s %(message)s", logger=log) + +EXTENSION_PATTERN = re.compile(r"\.(JPE?G|DNG)", re.I) COMMON_PATTERN = re.compile(r"(IMG|DSC[NF]?|100|P10|f|t)_?\d+", re.I) -EXIF_TAG_NAME = "DateTimeOriginal" -EXIF_TAG_ID = list(PIL.ExifTags.TAGS.keys())[ - list(PIL.ExifTags.TAGS.values()).index(EXIF_TAG_NAME) -] +EXIF_TAG_ID = 0x9003 # DateTimeOriginal EXIF_DATE_FORMAT = "%Y:%m:%d %H:%M:%S" @@ -23,7 +25,6 @@ def get_pictures(directory: str = ".", skip_renamed: bool = True) -> typing.Gene for filename in files: filename_trunk, extension = os.path.splitext(filename) - # if extension.upper() not in ('.JPEG', '.JPG'): if not re.match(EXTENSION_PATTERN, extension): continue if skip_renamed: @@ -33,23 +34,85 @@ def get_pictures(directory: str = ".", skip_renamed: bool = True) -> typing.Gene yield full_path -def main() -> None: - print("Counting files...") - nb_imgs = len(list(get_pictures())) - print("Processing files...") - iterator = progressbar.progressbar(get_pictures(), max_value=nb_imgs) +def main(args: argparse.Namespace) -> None: + log.warning("Counting files...") + kwargs = {"directory": args.dir, "skip_renamed": args.skip_renamed} + log.warning("Processing files...") + if args.hide_bar: + iterator = get_pictures(**kwargs) + else: + nb_imgs = len(list(get_pictures(**kwargs))) + iterator = progressbar.progressbar(get_pictures(**kwargs), max_value=nb_imgs) for full_path in iterator: - img = PIL.Image.open(full_path) - exif_data = img._getexif() - if exif_data and EXIF_TAG_ID in exif_data: - date_raw = exif_data[EXIF_TAG_ID] - date = datetime.datetime.strptime(date_raw, EXIF_DATE_FORMAT) - new_name = date.isoformat().replace(":", "-") + ".jpg" # For NTFS - print(full_path, new_name) - os.rename(full_path, new_name) # TODO FOLDER - img.close() + # Find date + with open(full_path, 'rb') as fd: + exif_data = exifread.process_file(fd) + if not exif_data: + log.warning(f"{full_path} does not have EXIF data") + for ifd_tag in exif_data.values(): + if ifd_tag.tag == EXIF_TAG_ID: + date_raw = ifd_tag.values + break + else: + log.warning(f"{full_path} does not have required EXIF tag") + continue + date = datetime.datetime.strptime(date_raw, EXIF_DATE_FORMAT) + + # Determine new filename + ext = os.path.splitext(full_path)[1].lower() + if ext == '.jpeg': + ext = '.jpg' + new_name = date.isoformat().replace(":", "-").replace("T", "_") + # First substitution is to allow images being sent to a NTFS filesystem + # Second substitution is for esthetics + new_path = os.path.join(args.dir, f"{new_name}{ext}") + # TODO Allow keeping image in same folder + i = 0 + while os.path.exists(new_path): + if full_path == new_path: + break + log.debug(f"{full_path} already exists, incrementing") + i += 1 + new_path = os.path.join(args.dir, f"{new_name}_{i}{ext}") + + # Rename file + if full_path == new_path: + log.debug(f"{full_path} already at required filename") + continue + log.info(f"{full_path} →\t{new_path}") + if os.path.exists(new_path): + raise FileExistsError(f"Won't overwrite {new_path}") + if not args.dry: + os.rename(full_path, new_path) if __name__ == "__main__": - # TODO Arguments parsing - main() + parser = argparse.ArgumentParser(description="Rename images based on their dates") + parser.add_argument( + "dir", + metavar="DIRECTORY", + type=str, + default=".", + nargs="?", + help="Directory containing the pictures", + ) + parser.add_argument( + "-d", + "--dry", + action="store_true", + help="Do not actually rename, just show old and new path", + ) + parser.add_argument( + "-s", + "--skip-renamed", + action="store_true", + help="Skip images whose filename doesn't match usual camera output filenames.", + ) + parser.add_argument( + "-b", + "--hide-bar", + action="store_true", + help="Do not show a progress bar. Also skip counting images", + ) + args = parser.parse_args() + main(args) diff --git a/config/scripts/raw_move_precomp b/config/scripts/raw_move_precomp new file mode 100755 index 0000000..9b62408 --- /dev/null +++ b/config/scripts/raw_move_precomp @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 + +""" +Same as picture_name_date +except it's tailored for OpenCamera. +It uses filenames, that way: +- Easier to get metadata +- JPG/DNG, MP4/SRT keep the same filename +""" + +import argparse +import logging +import os + +import coloredlogs + +log = logging.getLogger(__name__) +coloredlogs.install(level="DEBUG", fmt="%(levelname)s %(message)s", logger=log) + +RAW_EXTENSIONS = [".dng"] +PRECOMP_EXTENSIONS = [".jpg", ".jpeg"] +PRECOMP_DIRECTORY = ".precomp" + + +def main(args: argparse.Namespace) -> None: + for root, _, files in os.walk(args.dir): + for filename in files: + raw_path = os.path.join(root, filename) + basename, raw_ext = os.path.splitext(filename) + if raw_ext.lower() not in RAW_EXTENSIONS: + log.debug(f"{raw_path} isn't a RAW file") + continue + + # TODO Search for upper case extension + for precomp_ext in PRECOMP_EXTENSIONS: + precomp_filename = basename + precomp_ext + precomp_path = os.path.join(root, precomp_filename) + if not os.path.exists(precomp_path): + continue + precomp_dir = os.path.join(root, PRECOMP_DIRECTORY) + precomp_dest = os.path.join(precomp_dir, precomp_filename) + log.info(f"{precomp_path} -> {precomp_dest} because of {raw_path}") + if not args.dry: + os.makedirs(precomp_dir, exist_ok=True) + os.rename(precomp_path, precomp_dest) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Move pre-composed JPEG to a directory " + "when a matching raw picture is found" + ) + parser.add_argument( + "dir", + metavar="DIRECTORY", + type=str, + default=".", + nargs="?", + help="Directory containing the pictures", + ) + parser.add_argument( + "-d", + "--dry", + action="store_true", + help="Do not actually rename, just show old and new path", + ) + args = parser.parse_args() + main(args) diff --git a/config/scripts/smtpdummy b/config/scripts/smtpdummy index b191200..2b167a4 100755 --- a/config/scripts/smtpdummy +++ b/config/scripts/smtpdummy @@ -50,6 +50,7 @@ if __name__ == "__main__": parser.add_argument("-f", "--from", env_var="FROM") parser.add_argument("-t", "--to", env_var="TO") + parser.add_argument("-T", "--reply-to", env_var="REPLYTO") parser.add_argument( "-j", "--subject", @@ -77,6 +78,8 @@ if __name__ == "__main__": setattr(args, "from", args.sender) if args.to is None: args.to = args.receiver[0] + if args.reply_to is None: + args.reply_to = args.to if args.password: password = args.password args.password = "********" @@ -98,6 +101,7 @@ XJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X""" From: {args.me} <{getattr(args, 'from')}> Subject: {args.subject} To: {args.to} +Reply-To: {args.reply_to} Message-ID: {mid} Hello there,