Skip to content

Instantly share code, notes, and snippets.

@RhetTbull
Last active January 25, 2021 02:28
Show Gist options
  • Save RhetTbull/3db332106631cef1d582b0f9eb8847a6 to your computer and use it in GitHub Desktop.
Save RhetTbull/3db332106631cef1d582b0f9eb8847a6 to your computer and use it in GitHub Desktop.
Proof of concept to merge Apple Photos libraries -- still very unfinished
"""Proof of concept showing how to merge Apple Photos libraries including most of the metadata """
# Currently working:
# * places photos into correct albums and folder structure
# * sets title, description, keywords, location
# Limitations:
# * doesn't currently handle Live Photos or RAW+JPEG pairs
# * only merges the most recent version of the photo (edit history is lost)
# * very limited error handling
# * doesn't merge Person In Image
import pathlib
import tempfile
import click
import photoscript
import osxphotos
@click.command()
@click.argument("src_library", metavar="SOURCE", nargs=1, type=click.Path(exists=True))
@click.argument(
"dest_library", metavar="DESTINATION", nargs=1, type=click.Path(exists=True)
)
@click.pass_context
def cli(ctx, src_library, dest_library):
click.echo(f"Opening destination library {dest_library}")
dest = photoscript.PhotosLibrary()
dest.open(dest_library)
click.echo(f"Opening source library {src_library}")
src = osxphotos.PhotosDB(dbfile=src_library)
src_photos = src.photos()
click.echo(f"Merging {len(src_photos)} photos from {src_library} to {dest_library}")
with click.progressbar(src_photos) as bar:
for src_photo in bar:
path = src_photo.path_edited if src_photo.hasadjustments else src_photo.path
if not path:
click.secho(
f"Skipping missing photo {src_photo.original_filename} ({src_photo.uuid})",
fg="red",
)
continue
# export photo to temp file and rename to original_filename
# handling of RAW+JPEG pairs, Live photos, etc. left as exercise for the reader
# RAW+JPEG pairs will be correctly handled if imported like this:
# dest.import_photos(["/Users/rhet/Desktop/export/IMG_1994.JPG", "/Users/rhet/Desktop/export/IMG_1994.cr2"])
# Live Photos will be correctly handled if imported like this:
# dest.import_photos(["/Users/rhet/Desktop/export/IMG_3259.HEIC","/Users/rhet/Desktop/export/IMG_3259.mov"])
with tempfile.TemporaryDirectory() as tmpdir:
# get right suffix for original or edited file
ext = pathlib.Path(path).suffix
dest_file = pathlib.Path(src_photo.original_filename).stem + ext
exported = src_photo.export(
tmpdir, dest_file, edited=src_photo.hasadjustments
)
if exported:
exported = [
str(pathlib.Path(tmpdir) / filename) for filename in exported
]
dest_photos = dest.import_photos(
exported, skip_duplicate_check=True
)
else:
click.secho(
f"Error exporting photo {src_photo.original_filename} ({src_photo.uuid})",
fg="red",
)
continue
if not dest_photos:
click.secho(
f"Error importing photo {src_photo.original_filename} ({src_photo.uuid})",
fg="red",
)
continue
for dest_photo in dest_photos:
dest_photo.description = src_photo.description
dest_photo.title = src_photo.title
if src_photo.persons:
# add keywords for each person
dest_photo.keywords = src_photo.keywords + [
f"People/{p}" for p in src_photo.persons
]
else:
dest_photo.keywords = src_photo.keywords
if src_photo.location[0]:
dest_photo.location = src_photo.location
for album in src_photo.album_info:
if album.folder_names:
# make_folders silently ignores existing folders (like os.makedirs)
folder = dest.make_folders(album.folder_names)
dest_album = folder.album(album.title)
if not dest_album:
# album doesn't exist
dest_album = folder.create_album(album.title)
else:
dest_album = dest.album(album.title)
if not dest_album:
dest_album = dest.create_album(album.title)
dest_album.add([dest_photo])
if __name__ == "__main__":
cli()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment