Created
September 22, 2022 00:18
-
-
Save arkarkark/0484af118c7416db883410709ee4b030 to your computer and use it in GitHub Desktop.
select photos in Photos.app and then run this to find duplicate heic and jpeg photos.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# Copyright 2020 Alex K (wtwf.com) | |
# PYTHON_ARGCOMPLETE_OK | |
"""Remove duplicate heic/jpg photos from Photos.app | |
Keep the HEIC versions.""" | |
__author__ = "wtwf.com (Alex K)" | |
import argparse | |
import atexit | |
import collections | |
import datetime | |
import logging | |
import os | |
import sys | |
import appscript | |
import argcomplete | |
import humanize | |
import tqdm | |
class LogRuntime: | |
RUNTIMES = collections.defaultdict(list) | |
def __init__(self, name=None, args=None, kwargs=None): | |
self.args = args or [] | |
self.kwargs = kwargs or [] | |
self.name = name | |
def __call__(self, func): | |
def wrapped_f(*args, **kwargs): | |
start = datetime.datetime.now() | |
reply = func(*args, **kwargs) | |
delta = start - datetime.datetime.now() | |
description = self.name or func.__name__ | |
description += "(" | |
description += ", ".join( | |
[args[x] for x in self.args] + [f"{x}={kwargs[x]}" for x in self.kwargs] | |
) | |
description += ")" | |
LogRuntime.RUNTIMES[description].append(delta) | |
return reply | |
return wrapped_f | |
@staticmethod | |
def show_runtimes(): | |
print("\nRuntimes:\n", file=sys.stdout) | |
for desc, deltas in LogRuntime.RUNTIMES.items(): | |
duration = ", ".join([humanize.naturaldelta(delta) for delta in deltas]) | |
print(f"{desc}: {duration}", file=sys.stdout) | |
atexit.register(LogRuntime.show_runtimes) | |
class ShutdownHandler(logging.Handler): | |
def emit(self, record): | |
logging.shutdown() | |
sys.exit(1) | |
class HeicOnly: | |
def __init__(self): | |
self.photos = appscript.app("Photos") | |
self.dupe_album = self.get_or_make_album("Dupes") | |
self.both_dupe_album = self.get_or_make_album("BOTH Dupes") | |
self.remove_dupes() | |
def get_or_make_album(self, name): | |
a = self.photos.albums[appscript.its.name == name]() | |
if a and a[0] and a[0].name() == name: | |
logging.debug("Found album: %r", name) | |
return a[0] | |
logging.debug("Making album: %r", name) | |
a = self.photos.make(new=appscript.k.album) | |
a.name.set(name) | |
return a | |
def remove_dupes(self): | |
previous_item = None | |
for item in tqdm.tqdm(self.photos.selection()): | |
logging.debug("\n%r\n", item.filename()) | |
if self.same_item(item, previous_item): | |
logging.debug("Same item!") | |
self.remove_dupe_item(item, previous_item) | |
else: | |
logging.debug("different item!") | |
previous_item = item | |
logging.debug("") | |
@staticmethod | |
def same_item(a, b): | |
return ( | |
a | |
and b | |
# and a.locaton() == b.location() | |
and os.path.splitext(a.filename())[0] == os.path.splitext(b.filename())[0] | |
) | |
def remove_dupe_item(self, a, b): | |
if os.path.splitext(a.filename())[1].lower() == ".heic": | |
heic = a | |
jpeg = b | |
else: | |
heic = b | |
jpeg = a | |
logging.debug("HEIC: %r\tJPEG: %r", heic.filename(), jpeg.filename()) | |
# copy over if it's a favorite | |
if jpeg.favorite(): | |
logging.debug("It's a favorite!") | |
heic.favorite.set(True) | |
# add heic to albums that jpeg is part of | |
for album in self.albums_with(jpeg): | |
if album.id() != self.dupe_album.id(): | |
logging.debug("Duplicating to %r", album.name()) | |
try: | |
self.photos.add([heic], to=album) | |
except: | |
pass | |
# add this to the dupes album | |
self.photos.add([heic], to=self.both_dupe_album) | |
self.photos.add([jpeg], to=self.both_dupe_album) | |
self.photos.add([jpeg], to=self.dupe_album) | |
def albums_with(self, jpeg): | |
albums = self.photos.albums() | |
album_items = self.photos.albums.media_items[appscript.its.id == jpeg.id()]() | |
matches = [] | |
for index, items in enumerate(album_items): | |
if items: | |
matches.append(albums[index]) | |
logging.debug( | |
"albums_with %r is %r", jpeg.filename(), [x.name() for x in matches] | |
) | |
return matches | |
@LogRuntime() | |
def main(): | |
"""Parse args and do the thing.""" | |
logging.basicConfig() | |
logging.getLogger().addHandler(ShutdownHandler(level=50)) | |
parser = argparse.ArgumentParser(description="Program to do the thing.") | |
parser.add_argument("-p", "--password", help="Password") | |
parser.add_argument("-size", "--size", help="Size", type=int) | |
parser.add_argument("-v", "--verbose", help="Log verbosely", action="store_true") | |
parser.add_argument("-d", "--debug", help="Log debug messages", action="store_true") | |
argcomplete.autocomplete(parser) | |
args = parser.parse_args() | |
if args.verbose: | |
logging.getLogger().setLevel(logging.INFO) | |
if args.debug: | |
logging.getLogger().setLevel(logging.DEBUG) | |
logging.info("Log") | |
HeicOnly() | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment