From 666b6cac33fb8a2d0fc602609f11e190e11c538f Mon Sep 17 00:00:00 2001
From: Rhet Turnbull
Date: Fri, 23 Jul 2021 06:11:29 -0700
Subject: [PATCH] Updated docs
---
README.md | 4 +-
docs/.buildinfo | 2 +-
docs/_modules/index.html | 16 +-
.../photoinfo/_photoinfo_export.html | 843 ++++---
.../osxphotos/photoinfo/photoinfo.html | 488 ++--
.../_modules/osxphotos/photosdb/photosdb.html | 364 ++-
docs/_static/basic.css | 106 +-
docs/_static/documentation_options.js | 2 +-
docs/_static/searchtools.js | 2 +-
docs/_static/underscore-1.13.1.js | 2042 +++++++++++++++++
docs/_static/underscore.js | 8 +-
docs/cli.html | 1004 ++++----
docs/genindex.html | 369 ++-
docs/index.html | 18 +-
docs/modules.html | 10 +-
docs/objects.inv | Bin 3460 -> 3724 bytes
docs/reference.html | 1018 ++++----
docs/search.html | 18 +-
docs/searchindex.js | 2 +-
requirements_dev.txt | 5 +
20 files changed, 4556 insertions(+), 1765 deletions(-)
create mode 100644 docs/_static/underscore-1.13.1.js
create mode 100644 requirements_dev.txt
diff --git a/README.md b/README.md
index 2fde3555..563ab596 100644
--- a/README.md
+++ b/README.md
@@ -1823,7 +1823,7 @@ Substitution Description
{lf} A line feed: '\n', alias for {newline}
{cr} A carriage return: '\r'
{crlf} a carriage return + line feed: '\r\n'
-{osxphotos_version} The osxphotos version, e.g. '0.42.65'
+{osxphotos_version} The osxphotos version, e.g. '0.42.66'
{osxphotos_cmd_line} The full command line used to run osxphotos
The following substitutions may result in multiple values. Thus if specified for
@@ -3672,7 +3672,7 @@ The following template field substitutions are availabe for use the templating s
|{lf}|A line feed: '\n', alias for {newline}|
|{cr}|A carriage return: '\r'|
|{crlf}|a carriage return + line feed: '\r\n'|
-|{osxphotos_version}|The osxphotos version, e.g. '0.42.65'|
+|{osxphotos_version}|The osxphotos version, e.g. '0.42.66'|
|{osxphotos_cmd_line}|The full command line used to run osxphotos|
|{album}|Album(s) photo is contained in|
|{folder_album}|Folder path + album photo is contained in. e.g. 'Folder/Subfolder/Album' or just 'Album' if no enclosing folder|
diff --git a/docs/.buildinfo b/docs/.buildinfo
index 8454719b..ba1f918a 100644
--- a/docs/.buildinfo
+++ b/docs/.buildinfo
@@ -1,4 +1,4 @@
# Sphinx build info version 1
# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done.
-config: 210ecd9d654dea5d4c21627449ca1d63
+config: 37d92c35f55b2e7d711392f3f43dd1ef
tags: 645f666f9bcd5a90fca523b33c5a78b7
diff --git a/docs/_modules/index.html b/docs/_modules/index.html
index 119b7160..ee61285c 100644
--- a/docs/_modules/index.html
+++ b/docs/_modules/index.html
@@ -5,10 +5,10 @@
- Overview: module code — osxphotos 0.42.20 documentation
-
-
-
+ Overview: module code — osxphotos 0.42.66 documentation
+
+
+
@@ -31,7 +31,11 @@
[docs]classPhotoInfo:
@@ -79,30 +80,31 @@
"""# import additional methods
- from._photoinfo_searchinfoimport(
- search_info,
- search_info_normalized,
- labels,
- labels_normalized,
- SearchInfo,
- )
- from._photoinfo_exifinfoimportexif_info,ExifInfo
+ from._photoinfo_commentsimportcomments,likes
+ from._photoinfo_exifinfoimportExifInfo,exif_infofrom._photoinfo_exiftoolimportexiftoolfrom._photoinfo_exportimport(
- export,
- export2,
- _export_photo,
+ ExportResults,_exiftool_dict,_exiftool_json_sidecar,
+ _export_photo,
+ _export_photo_with_photos_export,_get_exif_keywords,_get_exif_persons,_write_exif_data,_write_sidecar,_xmp_sidecar,
- ExportResults,
+ export,
+ export2,
+ )
+ from._photoinfo_scoreinfoimportScoreInfo,score
+ from._photoinfo_searchinfoimport(
+ SearchInfo,
+ labels,
+ labels_normalized,
+ search_info,
+ search_info_normalized,)
- from._photoinfo_scoreinfoimportscore,ScoreInfo
- from._photoinfo_commentsimportcomments,likesdef__init__(self,db=None,uuid=None,info=None):self._uuid=uuid
@@ -110,9 +112,12 @@
self._db=dbself._verbose=self._db._verbose
+ # TODO: remove this once refactor of PhotoExporter is done
+ self._render_options=RenderOptions()
+
@propertydeffilename(self):
- """ filename of the picture """
+ """filename of the picture"""if(self._db._db_version<=_PHOTOS_4_VERSIONandself.has_raw
@@ -140,7 +145,7 @@
@propertydefdate(self):
- """ image creation date as timezone aware datetime object """
+ """image creation date as timezone aware datetime object"""returnself._info["imageDate"]@property
@@ -166,52 +171,22 @@
@propertydeftzoffset(self):
- """ timezone offset from UTC in seconds """
+ """timezone offset from UTC in seconds"""returnself._info["imageTimeZoneOffsetSeconds"]@propertydefpath(self):
- """ absolute path on disk of the original picture """
+ """absolute path on disk of the original picture"""try:returnself._pathexceptAttributeError:self._path=Nonephotopath=None
- # TODO: should path try to return path even if ismissing?ifself._info["isMissing"]==1:returnphotopath# path would be meaningless until downloadedifself._db._db_version<=_PHOTOS_4_VERSION:
- ifself._info["has_raw"]:
- # return the path to JPEG even if RAW is original
- vol=(
- self._db._dbvolumes[self._info["raw_pair_info"]["volumeId"]]
- ifself._info["raw_pair_info"]["volumeId"]isnotNone
- elseNone
- )
- ifvolisnotNone:
- photopath=os.path.join(
- "/Volumes",vol,self._info["raw_pair_info"]["imagePath"]
- )
- else:
- photopath=os.path.join(
- self._db._masters_path,
- self._info["raw_pair_info"]["imagePath"],
- )
- else:
- vol=self._info["volume"]
- ifvolisnotNone:
- photopath=os.path.join(
- "/Volumes",vol,self._info["imagePath"]
- )
- else:
- photopath=os.path.join(
- self._db._masters_path,self._info["imagePath"]
- )
- ifnotos.path.isfile(photopath):
- photopath=None
- self._path=photopath
- returnphotopath
+ returnself._path_4()ifself._info["shared"]:# shared photo
@@ -241,9 +216,40 @@
self._path=photopathreturnphotopath
+ def_path_4(self):
+ """return path for photo on Photos <= version 4"""
+ ifself._info["has_raw"]:
+ # return the path to JPEG even if RAW is original
+ vol=(
+ self._db._dbvolumes[self._info["raw_pair_info"]["volumeId"]]
+ ifself._info["raw_pair_info"]["volumeId"]isnotNone
+ elseNone
+ )
+ ifvolisnotNone:
+ photopath=os.path.join(
+ "/Volumes",vol,self._info["raw_pair_info"]["imagePath"]
+ )
+ else:
+ photopath=os.path.join(
+ self._db._masters_path,
+ self._info["raw_pair_info"]["imagePath"],
+ )
+ else:
+ vol=self._info["volume"]
+ ifvolisnotNone:
+ photopath=os.path.join("/Volumes",vol,self._info["imagePath"])
+ else:
+ photopath=os.path.join(
+ self._db._masters_path,self._info["imagePath"]
+ )
+ ifnotos.path.isfile(photopath):
+ photopath=None
+ self._path=photopath
+ returnphotopath
+
@propertydefpath_edited(self):
- """ absolute path on disk of the edited picture """
+ """absolute path on disk of the edited picture"""""" None if photo has not been edited """try:
@@ -257,7 +263,7 @@
returnself._path_editeddef_path_edited_5(self):
- """ return path_edited for Photos >= 5 """
+ """return path_edited for Photos >= 5"""# In Photos 5.0 / Catalina / MacOS 10.15:# edited photos appear to always be converted to .jpeg and stored in# library_name/resources/renders/X/UUID_1_201_a.jpeg
@@ -280,14 +286,10 @@
filename=Noneifself._info["type"]==_PHOTO_TYPE:# it's a photo
- ifself._db._photos_ver==5:
- filename=f"{self._uuid}_1_201_a.jpeg"
+ ifself._db._photos_ver!=5andself.uti=="public.heic":
+ filename=f"{self._uuid}_1_201_a.heic"else:
- # could be a heic or a jpeg
- ifself.uti=="public.heic":
- filename=f"{self._uuid}_1_201_a.heic"
- else:
- filename=f"{self._uuid}_1_201_a.jpeg"
+ filename=f"{self._uuid}_1_201_a.jpeg"elifself._info["type"]==_MOVIE_TYPE:# it's a moviefilename=f"{self._uuid}_2_0_a.mov"
@@ -315,7 +317,7 @@
returnphotopathdef_path_edited_4(self):
- """ return path_edited for Photos <= 4 """
+ """return path_edited for Photos <= 4"""ifself._db._db_version>_PHOTOS_4_VERSION:raiseRuntimeError("Wrong database format!")
@@ -373,9 +375,40 @@
returnphotopath
+ @property
+ defpath_edited_live_photo(self):
+ """return path to edited version of live photo movie; only valid for Photos 5+"""
+ ifself._db._db_version<_PHOTOS_5_VERSION:
+ returnNone
+
+ try:
+ returnself._path_edited_live_photo
+ exceptAttributeError:
+ self._path_edited_live_photo=self._path_edited_5_live_photo()
+ returnself._path_edited_live_photo
+
+ def_path_edited_5_live_photo(self):
+ """return path_edited_live_photo for Photos >= 5"""
+ ifself._db._db_version<_PHOTOS_5_VERSION:
+ raiseRuntimeError("Wrong database format!")
+
+ ifself.live_photoandself._info["hasAdjustments"]:
+ library=self._db._library_path
+ directory=self._uuid[0]# first char of uuid
+ filename=f"{self._uuid}_2_100_a.mov"
+ photopath=os.path.join(
+ library,"resources","renders",directory,filename
+ )
+ ifnotos.path.isfile(photopath):
+ photopath=None
+ else:
+ photopath=None
+
+ returnphotopath
+
@propertydefpath_raw(self):
- """ absolute path of associated RAW image or None if there is not one """
+ """absolute path of associated RAW image or None if there is not one"""# In Photos 5, raw is in same folder as original but with _4.ext# Unless "Copy Items to the Photos Library" is not checked
@@ -402,60 +435,72 @@
# return photopathifself._db._db_version<=_PHOTOS_4_VERSION:
- vol=self._info["raw_info"]["volume"]
- ifvolisnotNone:
- photopath=os.path.join(
- "/Volumes",vol,self._info["raw_info"]["imagePath"]
- )
- else:
- photopath=os.path.join(
- self._db._masters_path,self._info["raw_info"]["imagePath"]
- )
- ifnotos.path.isfile(photopath):
- logging.debug(
- f"MISSING PATH: RAW photo for UUID {self._uuid} should be at {photopath} but does not appear to exist"
- )
- photopath=None
- else:
+ returnself._path_raw_4()
+
+ ifnotself.isreference:filestem=pathlib.Path(self._info["filename"]).stem
- raw_ext=get_preferred_uti_extension(self._info["UTI_raw"])
+ # raw_ext = get_preferred_uti_extension(self._info["UTI_raw"])ifself._info["directory"].startswith("/"):filepath=self._info["directory"]else:filepath=os.path.join(self._db._masters_path,self._info["directory"])
- glob_str=f"{filestem}*.{raw_ext}"
+ # raw files have same name as original but with _4.raw_ext appended
+ # I believe the _4 maps to PHAssetResourceTypeAlternatePhoto = 4
+ # see: https://developer.apple.com/documentation/photokit/phassetresourcetype/phassetresourcetypealternatephoto?language=objc
+ glob_str=f"{filestem}_4*"raw_file=findfiles(glob_str,filepath)
- iflen(raw_file)!=1:
- # Note: In Photos Version 5.0 (141.19.150), images not copied to Photos Library
- # that are missing do not always trigger is_missing = True as happens
- # in earlier version so it's possible for this check to fail, if so, return None
- logging.debug(f"Error getting path to RAW file: {filepath}/{glob_str}")
+ ifnotraw_file:photopath=Noneelse:
- photopath=os.path.join(filepath,raw_file[0])
- ifnotos.path.isfile(photopath):
- logging.debug(
- f"MISSING PATH: RAW photo for UUID {self._uuid} should be at {photopath} but does not appear to exist"
- )
- photopath=None
+ photopath=pathlib.Path(filepath)/raw_file[0]
+ photopath=str(photopath)ifphotopath.is_file()elseNone
+ else:
+ # is a reference
+ try:
+ photopath=(
+ pathlib.Path("/Volumes")
+ /self._info["raw_volume"]
+ /self._info["raw_relative_path"]
+ )
+ photopath=str(photopath)ifphotopath.is_file()elseNone
+ exceptKeyError:
+ # don't have the path details
+ photopath=Nonereturnphotopath
+ def_path_raw_4(self):
+ """Return path_raw for Photos <= version 4"""
+ vol=self._info["raw_info"]["volume"]
+ ifvolisnotNone:
+ photopath=os.path.join(
+ "/Volumes",vol,self._info["raw_info"]["imagePath"]
+ )
+ else:
+ photopath=os.path.join(
+ self._db._masters_path,self._info["raw_info"]["imagePath"]
+ )
+ ifnotos.path.isfile(photopath):
+ logging.debug(
+ f"MISSING PATH: RAW photo for UUID {self._uuid} should be at {photopath} but does not appear to exist"
+ )
+ photopath=None
+
@propertydefdescription(self):
- """ long / extended description of picture """
+ """long / extended description of picture"""returnself._info["extendedDescription"]@propertydefpersons(self):
- """ list of persons in picture """
+ """list of persons in picture"""return[self._db._dbpersons_pk[pk]["fullname"]forpkinself._info["persons"]]@propertydefperson_info(self):
- """ list of PersonInfo objects for person in picture """
+ """list of PersonInfo objects for person in picture"""try:returnself._personinfoexceptAttributeError:
@@ -466,7 +511,7 @@
@propertydefface_info(self):
- """ list of FaceInfo objects for faces in picture """
+ """list of FaceInfo objects for faces in picture"""try:returnself._faceinfoexceptAttributeError:
@@ -480,7 +525,7 @@
@propertydefalbums(self):
- """ list of albums picture is contained in """
+ """list of albums picture is contained in"""try:returnself._albumsexceptAttributeError:
@@ -492,7 +537,7 @@
@propertydefburst_albums(self):
- """If photo is burst photo, list of albums it is contained in as well as any albums the key photo is contained in, otherwise returns self.albums """
+ """If photo is burst photo, list of albums it is contained in as well as any albums the key photo is contained in, otherwise returns self.albums"""try:returnself._burst_albumsexceptAttributeError:
@@ -505,7 +550,7 @@
@propertydefalbum_info(self):
- """ list of AlbumInfo objects representing albums the photo is contained in """
+ """list of AlbumInfo objects representing albums the photo is contained in"""try:returnself._album_infoexceptAttributeError:
@@ -517,7 +562,7 @@
@propertydefburst_album_info(self):
- """ If photo is a burst photo, returns list of AlbumInfo objects representing albums the photo is contained in as well as albums the burst key photo is contained in, otherwise returns self.album_info. """
+ """If photo is a burst photo, returns list of AlbumInfo objects representing albums the photo is contained in as well as albums the burst key photo is contained in, otherwise returns self.album_info."""try:returnself._burst_album_infoexceptAttributeError:
@@ -530,7 +575,7 @@
@propertydefimport_info(self):
- """ ImportInfo object representing import session for the photo or None if no import session """
+ """ImportInfo object representing import session for the photo or None if no import session"""try:returnself._import_infoexceptAttributeError:
@@ -543,17 +588,17 @@
@propertydefkeywords(self):
- """ list of keywords for picture """
+ """list of keywords for picture"""returnself._info["keywords"]@propertydeftitle(self):
- """ name / title of picture """
+ """name / title of picture"""returnself._info["name"]@propertydefuuid(self):
- """ UUID of picture """
+ """UUID of picture"""returnself._uuid@property
@@ -571,12 +616,12 @@
@propertydefhasadjustments(self):
- """ True if picture has adjustments / edits """
+ """True if picture has adjustments / edits"""returnself._info["hasAdjustments"]==1@propertydefadjustments(self):
- """ Returns AdjustmentsInfo class for adjustment data or None if no adjustments; Photos 5+ only """
+ """Returns AdjustmentsInfo class for adjustment data or None if no adjustments; Photos 5+ only"""ifself._db._db_version<=_PHOTOS_4_VERSION:returnNone
@@ -600,32 +645,32 @@
@propertydefexternal_edit(self):
- """ Returns True if picture was edited outside of Photos using external editor """
+ """Returns True if picture was edited outside of Photos using external editor"""returnself._info["adjustmentFormatID"]=="com.apple.Photos.externalEdit"@propertydeffavorite(self):
- """ True if picture is marked as favorite """
+ """True if picture is marked as favorite"""returnself._info["favorite"]==1@propertydefhidden(self):
- """ True if picture is hidden """
+ """True if picture is hidden"""returnself._info["hidden"]==1@propertydefvisible(self):
- """ True if picture is visble """
+ """True if picture is visble"""returnself._info["visible"]@propertydefintrash(self):
- """ True if picture is in trash ('Recently Deleted' folder)"""
+ """True if picture is in trash ('Recently Deleted' folder)"""returnself._info["intrash"]@propertydefdate_trashed(self):
- """ Date asset was placed in the trash or None """
+ """Date asset was placed in the trash or None"""# TODO: add add_timezone(dt, offset_seconds) to datetime_utils# also update date_modifiedtrasheddate=self._info["trasheddate"]
@@ -639,7 +684,7 @@
@propertydefdate_added(self):
- """ Date photo was added to the database """
+ """Date photo was added to the database"""try:returnself._date_addedexceptAttributeError:
@@ -656,7 +701,7 @@
@propertydeflocation(self):
- """ returns (latitude, longitude) as float in degrees or None """
+ """returns (latitude, longitude) as float in degrees or None"""return(self._latitude,self._longitude)@property
@@ -690,13 +735,23 @@
"""Returns Uniform Type Identifier (UTI) for the original image for example: public.jpeg or com.apple.quicktime-movie """
- ifself._db._db_version<=_PHOTOS_4_VERSIONandself._info["has_raw"]:
- returnself._info["raw_pair_info"]["UTI"]
- elifself.shared:
- # TODO: need reliable way to get original UTI for shared
- returnself.uti
- else:
- returnself._info["UTI_original"]
+ try:
+ returnself._uti_original
+ exceptAttributeError:
+ ifself._db._db_version<=_PHOTOS_4_VERSIONandself._info["has_raw"]:
+ self._uti_original=self._info["raw_pair_info"]["UTI"]
+ elifself.shared:
+ # TODO: need reliable way to get original UTI for shared
+ self._uti_original=self.uti
+ elifself._db._photos_ver>=7:
+ # Monterey+
+ self._uti_original=get_uti_for_extension(
+ pathlib.Path(self.original_filename).suffix
+ )
+ else:
+ self._uti_original=self._info["UTI_original"]
+
+ returnself._uti_original@propertydefuti_edited(self):
@@ -715,7 +770,14 @@
for example: com.canon.cr2-raw-image Returns None if no associated RAW image """
- returnself._info["UTI_raw"]
+ ifself._db._photos_ver<7:
+ returnself._info["UTI_raw"]
+
+ rawpath=self.path_raw
+ ifrawpath:
+ returnget_uti_for_extension(pathlib.Path(rawpath).suffix)
+ else:
+ returnNone@propertydefismovie(self):
@@ -752,27 +814,27 @@
@propertydefisreference(self):
- """ Returns True if photo is a reference (not copied to the Photos library), otherwise False """
+ """Returns True if photo is a reference (not copied to the Photos library), otherwise False"""returnself._info["isreference"]@propertydefburst(self):
- """ Returns True if photo is part of a Burst photo set, otherwise False """
+ """Returns True if photo is part of a Burst photo set, otherwise False"""returnself._info["burst"]@propertydefburst_selected(self):
- """ Returns True if photo is a burst photo and has been selected from the burst set by the user, otherwise False """
+ """Returns True if photo is a burst photo and has been selected from the burst set by the user, otherwise False"""returnbool(self._info["burstPickType"]&BURST_SELECTED)@propertydefburst_key(self):
- """ Returns True if photo is a burst photo and is the key image for the burst set (the image that Photos shows on top of the burst stack), otherwise False """
+ """Returns True if photo is a burst photo and is the key image for the burst set (the image that Photos shows on top of the burst stack), otherwise False"""returnbool(self._info["burstPickType"]&BURST_KEY)@propertydefburst_default_pick(self):
- """ Returns True if photo is a burst image and is the photo that Photos selected as the default image for the burst set, otherwise False """
+ """Returns True if photo is a burst image and is the photo that Photos selected as the default image for the burst set, otherwise False"""returnbool(self._info["burstPickType"]&BURST_DEFAULT_PICK)@property
@@ -792,7 +854,7 @@
@propertydeflive_photo(self):
- """ Returns True if photo is a live photo, otherwise False """
+ """Returns True if photo is a live photo, otherwise False"""returnself._info["live_photo"]@property
@@ -853,25 +915,40 @@
@propertydefpath_derivatives(self):
- """ Return any derivative (preview) images associated with the photo as a list of paths, sorted by file size (largest first) """
- ifself._db._db_version<=_PHOTOS_4_VERSION:
- returnself._path_derivatives_4()
+ """Return any derivative (preview) images associated with the photo as a list of paths, sorted by file size (largest first)"""
+ try:
+ returnself._path_derivatives
+ exceptAttributeError:
+ ifself._db._db_version<=_PHOTOS_4_VERSION:
+ self._path_derivatives=self._path_derivatives_4()
+ returnself._path_derivatives
- directory=self._uuid[0]# first char of uuid
- derivative_path=(
- pathlib.Path(self._db._library_path)
- /"resources"
- /"derivatives"
- /directory
- )
- files=derivative_path.glob(f"{self.uuid}*.*")
- files=sorted(files,reverse=True,key=lambdaf:f.stat().st_size)
- # return list of filename but skip .THM files (these are actually low-res thumbnails in JPEG format but with .THM extension)
- return[str(filename)forfilenameinfilesiffilename.suffix!=".THM"]
+ directory=self._uuid[0]# first char of uuid
+ derivative_path=(
+ pathlib.Path(self._db._library_path)
+ /"resources"
+ /"derivatives"
+ /directory
+ )
+ files=derivative_path.glob(f"{self.uuid}*.*")
+ files=sorted(files,reverse=True,key=lambdaf:f.stat().st_size)
+ # return list of filename but skip .THM files (these are actually low-res thumbnails in JPEG format but with .THM extension)
+ derivatives=[
+ str(filename)forfilenameinfilesiffilename.suffix!=".THM"
+ ]
+ if(
+ self.isphoto
+ andlen(derivatives)>1
+ andderivatives[0].endswith(".mov")
+ ):
+ derivatives[1],derivatives[0]=derivatives[0],derivatives[1]
+
+ self._path_derivatives=derivatives
+ returnself._path_derivativesdef_path_derivatives_4(self):
- """ Return paths to all derivative (preview) files for Photos <= 4"""
- modelid=self._info["masterModelID"]
+ """Return paths to all derivative (preview) files for Photos <= 4"""
+ modelid=self._info["modelID"]ifmodelidisNone:return[]folder_id,file_id=_get_resource_loc(modelid)
@@ -907,42 +984,42 @@
@propertydefpanorama(self):
- """ Returns True if photo is a panorama, otherwise False """
+ """Returns True if photo is a panorama, otherwise False"""returnself._info["panorama"]@propertydefslow_mo(self):
- """ Returns True if photo is a slow motion video, otherwise False """
+ """Returns True if photo is a slow motion video, otherwise False"""returnself._info["slow_mo"]@propertydeftime_lapse(self):
- """ Returns True if photo is a time lapse video, otherwise False """
+ """Returns True if photo is a time lapse video, otherwise False"""returnself._info["time_lapse"]@propertydefhdr(self):
- """ Returns True if photo is an HDR photo, otherwise False """
+ """Returns True if photo is an HDR photo, otherwise False"""returnself._info["hdr"]@propertydefscreenshot(self):
- """ Returns True if photo is an HDR photo, otherwise False """
+ """Returns True if photo is an HDR photo, otherwise False"""returnself._info["screenshot"]@propertydefportrait(self):
- """ Returns True if photo is a portrait, otherwise False """
+ """Returns True if photo is a portrait, otherwise False"""returnself._info["portrait"]@propertydefselfie(self):
- """ Returns True if photo is a selfie (front facing camera), otherwise False """
+ """Returns True if photo is a selfie (front facing camera), otherwise False"""returnself._info["selfie"]@propertydefplace(self):
- """ Returns PlaceInfo object containing reverse geolocation info """
+ """Returns PlaceInfo object containing reverse geolocation info"""# implementation note: doesn't create the PlaceInfo object until requested# then memoizes the object in self._place to avoid recreating the object
@@ -970,12 +1047,12 @@
@propertydefhas_raw(self):
- """ returns True if photo has an associated raw image (that is, it's a RAW+JPEG pair), otherwise False """
+ """returns True if photo has an associated raw image (that is, it's a RAW+JPEG pair), otherwise False"""returnself._info["has_raw"]@propertydefisraw(self):
- """ returns True if photo is a raw image. For images with an associated RAW+JPEG pair, see has_raw """
+ """returns True if photo is a raw image. For images with an associated RAW+JPEG pair, see has_raw"""return"raw-image"inself.uti_original@property
@@ -987,17 +1064,17 @@
@propertydefheight(self):
- """ returns height of the current photo version in pixels """
+ """returns height of the current photo version in pixels"""returnself._info["height"]@propertydefwidth(self):
- """ returns width of the current photo version in pixels """
+ """returns width of the current photo version in pixels"""returnself._info["width"]@propertydeforientation(self):
- """ returns EXIF orientation of the current photo version as int or 0 if current orientation cannot be determined """
+ """returns EXIF orientation of the current photo version as int or 0 if current orientation cannot be determined"""ifself._db._db_version<=_PHOTOS_4_VERSION:returnself._info["orientation"]
@@ -1013,76 +1090,63 @@
@propertydeforiginal_height(self):
- """ returns height of the original photo version in pixels """
+ """returns height of the original photo version in pixels"""returnself._info["original_height"]@propertydeforiginal_width(self):
- """ returns width of the original photo version in pixels """
+ """returns width of the original photo version in pixels"""returnself._info["original_width"]@propertydeforiginal_orientation(self):
- """ returns EXIF orientation of the original photo version as int """
+ """returns EXIF orientation of the original photo version as int"""returnself._info["original_orientation"]@propertydeforiginal_filesize(self):
- """ returns filesize of original photo in bytes as int """
+ """returns filesize of original photo in bytes as int"""returnself._info["original_filesize"]
+ @property
+ defduplicates(self):
+ """return list of PhotoInfo objects for possible duplicates (matching signature of original size, date, height, width) or empty list if no matching duplicates"""
+ signature=self._db._duplicate_signature(self.uuid)
+ duplicates=[]
+ try:
+ foruuidinself._db._db_signatures[signature]:
+ ifuuid!=self.uuid:
+ # found a possible duplicate
+ duplicates.append(self._db.get_photo(uuid))
+ exceptKeyError:
+ # don't expect this to happen as the signature should be in db
+ logging.warning(f"Did not find signature for {self.uuid} in _db_signatures")
+ returnduplicates
+
[docs]defrender_template(
- self,
- template_str,
- none_str="_",
- path_sep=None,
- expand_inplace=False,
- inplace_sep=None,
- filename=False,
- dirname=False,
- strip=False,
- edited=False,
+ self,template_str:str,options:Optional[RenderOptions]=None):"""Renders a template string for PhotoInfo instance using PhotoTemplate Args: template_str: a template string with fields to render
- none_str: a str to use if template field renders to None, default is "_".
- path_sep: a single character str to use as path separator when joining
- fields like folder_album; if not provided, defaults to os.path.sep
- expand_inplace: expand multi-valued substitutions in-place as a single string
- instead of returning individual strings
- inplace_sep: optional string to use as separator between multi-valued keywords
- with expand_inplace; default is ','
- filename: if True, template output will be sanitized to produce valid file name
- dirname: if True, template output will be sanitized to produce valid directory name
- strip: if True, strips leading/trailing white space from resulting template
- edited: if True, sets {edited_version} field to True, otherwise it gets set to False; set if you want template evaluated for edited version
+ options: a RenderOptions instance Returns: ([rendered_strings], [unmatched]): tuple of list of rendered strings and list of unmatched template values """
+ options=optionsorRenderOptions()template=PhotoTemplate(self,exiftool_path=self._db._exiftool_path)
- returntemplate.render(
- template_str,
- none_str=none_str,
- path_sep=path_sep,
- expand_inplace=expand_inplace,
- inplace_sep=inplace_sep,
- filename=filename,
- dirname=dirname,
- strip=strip,
- edited_version=edited,
- )
+ returntemplate.render(template_str,options)
@propertydef_longitude(self):
- """ Returns longitude, in degrees """
+ """Returns longitude, in degrees"""returnself._info["longitude"]@propertydef_latitude(self):
- """ Returns latitude, in degrees """
+ """Returns latitude, in degrees"""returnself._info["latitude"]def_get_album_uuids(self):
@@ -1120,7 +1184,7 @@
returnf"osxphotos.{self.__class__.__name__}(db={self._db}, uuid='{self._uuid}', info={self._info})"def__str__(self):
- """ string representation of PhotoInfo object """
+ """string representation of PhotoInfo object"""date_iso=self.date.isoformat()date_modified_iso=(
@@ -1183,7 +1247,7 @@
returnyaml.dump(info,sort_keys=False)
[docs]classPhotosDB:
- """ Processes a Photos.app library database to extract information about photos """
+ """Processes a Photos.app library database to extract information about photos"""# import additional methods
+ from._photosdb_process_commentsimport_process_commentsfrom._photosdb_process_exifimport_process_exifinfofrom._photosdb_process_faceinfoimport_process_faceinfo
+ from._photosdb_process_scoreinfoimport_process_scoreinfofrom._photosdb_process_searchinfoimport(_process_searchinfo,labels,
- labels_normalized,labels_as_dict,
+ labels_normalized,labels_normalized_as_dict,)
- from._photosdb_process_scoreinfoimport_process_scoreinfo
- from._photosdb_process_commentsimport_process_commentsdef__init__(self,dbfile=None,verbose=None,exiftool=None):
- """ Create a new PhotosDB object.
+ """Create a new PhotosDB object. Args: dbfile: specify full path to photos library or photos.db; if None, will attempt to locate last library opened by Photos. verbose: optional callable function to use for printing verbose text during processing; if None (default), does not print output. exiftool: optional path to exiftool for methods that require this (e.g. PhotoInfo.exiftool); if not provided, will search PATH
-
+
Raises: FileNotFoundError if dbfile is not a valid Photos library. TypeError if verbose is not None and not callable.
@@ -273,6 +276,13 @@
# Will hold the primary key of root folderself._folder_root_pk=None
+ # Dict to hold signatures for finding possible duplicates
+ # key is tuple of (original_filesize, date) and value is list of uuids that match that signature
+ self._db_signatures={}
+
+ # Dict to hold information on volume names (Photos 5+)
+ self._db_filesystem_volumes={}
+
if_debug():logging.debug(f"dbfile = {dbfile}")
@@ -355,7 +365,7 @@
@propertydefkeywords_as_dict(self):
- """ return keywords as dict of keyword, count in reverse sorted order (descending) """
+ """return keywords as dict of keyword, count in reverse sorted order (descending)"""keywords={k:len(self._dbkeywords_keyword[k])forkinself._dbkeywords_keyword.keys()}
@@ -365,7 +375,7 @@
@propertydefpersons_as_dict(self):
- """ return persons as dict of person, count in reverse sorted order (descending) """
+ """return persons as dict of person, count in reverse sorted order (descending)"""persons={}forpkinself._dbfaces_pk:fullname=self._dbpersons_pk[pk]["fullname"]
@@ -378,7 +388,7 @@
@propertydefalbums_as_dict(self):
- """ return albums as dict of albums, count in reverse sorted order (descending) """
+ """return albums as dict of albums, count in reverse sorted order (descending)"""albums={}album_keys=self._get_album_uuids(shared=False)foralbuminalbum_keys:
@@ -395,8 +405,8 @@
@propertydefalbums_shared_as_dict(self):
- """ returns shared albums as dict of albums, count in reverse sorted order (descending)
- valid only on Photos 5; on Photos <= 4, prints warning and returns empty dict """
+ """returns shared albums as dict of albums, count in reverse sorted order (descending)
+ valid only on Photos 5; on Photos <= 4, prints warning and returns empty dict"""albums={}album_keys=self._get_album_uuids(shared=True)
@@ -414,19 +424,19 @@
@propertydefkeywords(self):
- """ return list of keywords found in photos database """
+ """return list of keywords found in photos database"""keywords=self._dbkeywords_keyword.keys()returnlist(keywords)@propertydefpersons(self):
- """ return list of persons found in photos database """
+ """return list of persons found in photos database"""persons={self._dbpersons_pk[k]["fullname"]forkinself._dbfaces_pk}returnlist(persons)@propertydefperson_info(self):
- """ return list of PersonInfo objects for each person in the photos database """
+ """return list of PersonInfo objects for each person in the photos database"""try:returnself._person_infoexceptAttributeError:
@@ -437,7 +447,7 @@
@propertydeffolder_info(self):
- """ return list FolderInfo objects representing top-level folders in the photos database """
+ """return list FolderInfo objects representing top-level folders in the photos database"""ifself._db_version<=_PHOTOS_4_VERSION:folders=[FolderInfo(db=self,uuid=folder)
@@ -458,7 +468,7 @@
@propertydeffolders(self):
- """ return list of top-level folder names in the photos database """
+ """return list of top-level folder names in the photos database"""ifself._db_version<=_PHOTOS_4_VERSION:folder_names=[folder["name"]
@@ -479,7 +489,7 @@
@propertydefalbum_info(self):
- """ return list of AlbumInfo objects for each album in the photos database """
+ """return list of AlbumInfo objects for each album in the photos database"""try:returnself._album_infoexceptAttributeError:
@@ -491,8 +501,8 @@
@propertydefalbum_info_shared(self):
- """ return list of AlbumInfo objects for each shared album in the photos database
- only valid for Photos 5; on Photos <= 4, prints warning and returns empty list """
+ """return list of AlbumInfo objects for each shared album in the photos database
+ only valid for Photos 5; on Photos <= 4, prints warning and returns empty list"""# if _dbalbum_details[key]["cloudownerhashedpersonid"] is not None, then it's a shared albumtry:returnself._album_info_shared
@@ -505,7 +515,7 @@
@propertydefalbums(self):
- """ return list of albums found in photos database """
+ """return list of albums found in photos database"""# Could be more than one album with same name# Right now, they are treated as same album and photos are combined from albums with same name
@@ -518,8 +528,8 @@
@propertydefalbums_shared(self):
- """ return list of shared albums found in photos database
- only valid for Photos 5; on Photos <= 4, prints warning and returns empty list """
+ """return list of shared albums found in photos database
+ only valid for Photos 5; on Photos <= 4, prints warning and returns empty list"""# Could be more than one album with same name# Right now, they are treated as same album and photos are combined from albums with same name
@@ -534,7 +544,7 @@
@propertydefimport_info(self):
- """ return list of ImportInfo objects for each import session in the database """
+ """return list of ImportInfo objects for each import session in the database"""try:returnself._import_infoexceptAttributeError:
@@ -546,21 +556,21 @@
@propertydefdb_version(self):
- """ return the database version as stored in LiGlobals table """
+ """return the database version as stored in LiGlobals table"""returnself._db_version@propertydefdb_path(self):
- """ returns path to the Photos library database PhotosDB was initialized with """
+ """returns path to the Photos library database PhotosDB was initialized with"""returnos.path.abspath(self._dbfile)@propertydeflibrary_path(self):
- """ returns path to the Photos library PhotosDB was initialized with """
+ """returns path to the Photos library PhotosDB was initialized with"""returnself._library_path
[docs]defget_db_connection(self):
- """ Get connection to the working copy of the Photos database
+ """Get connection to the working copy of the Photos database Returns: tuple of (connection, cursor) to sqlite3 database
@@ -568,7 +578,7 @@
return_open_sql_file(self._tmp_db)
def_copy_db_file(self,fname):
- """ copies the sqlite database file to a temp file """
+ """copies the sqlite database file to a temp file"""""" returns the name of the temp file """""" If sqlite shared memory and write-ahead log files exist, those are copied too """# required because python's sqlite3 implementation can't read a locked file
@@ -620,13 +630,15 @@
# return dest_pathdef_process_database4(self):
- """ process the Photos database to extract info
- works on Photos version <= 4.0 """
+ """process the Photos database to extract info
+ works on Photos version <= 4.0"""verbose=self._verboseverbose("Processing database.")verbose(f"Database version: {self._db_version}.")
+ self._photos_ver=4# only used in Photos 5+
+
(conn,c)=_open_sql_file(self._tmp_db)# get info to associate persons with photos
@@ -811,6 +823,8 @@
"creation_date":album[8],"start_date":None,# Photos 5 only"end_date":None,# Photos 5 only
+ "customsortascending":None,# Photos 5 only
+ "customsortkey":None,# Photos 5 only}# get details about folders
@@ -1123,6 +1137,7 @@
# get info on special typesself._dbphotos[uuid]["specialType"]=row[25]self._dbphotos[uuid]["masterModelID"]=row[26]
+ self._dbphotos[uuid]["pk"]=row[26]# same as masterModelID, to match Photos 5self._dbphotos[uuid]["panorama"]=Trueifrow[25]==1elseFalseself._dbphotos[uuid]["slow_mo"]=Trueifrow[25]==2elseFalseself._dbphotos[uuid]["time_lapse"]=Trueifrow[25]==3elseFalse
@@ -1213,6 +1228,13 @@
self._dbphotos[uuid]["import_uuid"]=row[44]self._dbphotos[uuid]["fok_import_session"]=None
+ # compute signatures for finding possible duplicates
+ signature=self._duplicate_signature(uuid)
+ try:
+ self._db_signatures[signature].append(uuid)
+ exceptKeyError:
+ self._db_signatures[signature]=[uuid]
+
# get additional details from RKMaster, needed for RAW processingverbose("Processing additional photo details.")c.execute(
@@ -1565,15 +1587,15 @@
logging.debug(pformat(self._dbphotos_burst))def_build_album_folder_hierarchy_4(self,uuid,folders=None):
- """ recursively build folder/album hierarchy
- uuid: parent uuid of the album being processed
- (parent uuid is a folder in RKFolders)
- folders: dict holding the folder hierarchy
- NOTE: This implementation is different than _build_album_folder_hierarchy_5
- which takes the uuid of the album being processed. Here uuid is the parent uuid
- of the parent folder album because in Photos <=4, folders are in RKFolders and
- albums in RKAlbums. In Photos 5, folders are just special albums
- with kind = _PHOTOS_5_FOLDER_KIND """
+ """recursively build folder/album hierarchy
+ uuid: parent uuid of the album being processed
+ (parent uuid is a folder in RKFolders)
+ folders: dict holding the folder hierarchy
+ NOTE: This implementation is different than _build_album_folder_hierarchy_5
+ which takes the uuid of the album being processed. Here uuid is the parent uuid
+ of the parent folder album because in Photos <=4, folders are in RKFolders and
+ albums in RKAlbums. In Photos 5, folders are just special albums
+ with kind = _PHOTOS_5_FOLDER_KIND"""parent_uuid=self._dbfolder_details[uuid]["parentFolderUuid"]
@@ -1596,11 +1618,11 @@
returnfoldersdef_process_database5(self):
- """ process the Photos database to extract info
- works on Photos version 5 and version 6
+ """process the Photos database to extract info
+ works on Photos version 5 and version 6
- This is a big hairy 700 line function that should probably be refactored
- but it works so don't touch it.
+ This is a big hairy 700 line function that should probably be refactored
+ but it works so don't touch it. """if_debug():
@@ -1615,10 +1637,14 @@
verbose(f"Database version: {self._db_version}, {photos_ver}.")asset_table=_DB_TABLE_NAMES[photos_ver]["ASSET"]keyword_join=_DB_TABLE_NAMES[photos_ver]["KEYWORD_JOIN"]
+ asset_album_table=_DB_TABLE_NAMES[photos_ver]["ASSET_ALBUM_TABLE"]album_join=_DB_TABLE_NAMES[photos_ver]["ALBUM_JOIN"]album_sort=_DB_TABLE_NAMES[photos_ver]["ALBUM_SORT_ORDER"]
+ asset_album_join=_DB_TABLE_NAMES[photos_ver]["ASSET_ALBUM_JOIN"]import_fok=_DB_TABLE_NAMES[photos_ver]["IMPORT_FOK"]depth_state=_DB_TABLE_NAMES[photos_ver]["DEPTH_STATE"]
+ uti_original_column=_DB_TABLE_NAMES[photos_ver]["UTI_ORIGINAL"]
+ hdr_type_column=_DB_TABLE_NAMES[photos_ver]["HDR_TYPE"]# Look for all combinations of persons and picturesif_debug():
@@ -1648,7 +1674,11 @@
forpersoninc:pk=person[0]
- fullname=person[2]ifperson[2]!=""else_UNKNOWN_PERSON
+ fullname=(
+ person[2]
+ if(person[2]!=""andperson[2]isnotNone)
+ else_UNKNOWN_PERSON
+ )self._dbpersons_pk[pk]={"pk":pk,"uuid":person[1],
@@ -1734,8 +1764,8 @@
{asset_table}.ZUUID,{album_sort} FROM {asset_table}
- JOIN Z_26ASSETS ON {album_join} = {asset_table}.Z_PK
- JOIN ZGENERICALBUM ON ZGENERICALBUM.Z_PK = Z_26ASSETS.Z_26ALBUMS
+ JOIN {asset_album_table} ON {album_join} = {asset_table}.Z_PK
+ JOIN ZGENERICALBUM ON ZGENERICALBUM.Z_PK = {asset_album_join} """)
@@ -1773,7 +1803,9 @@
"ZTRASHEDSTATE, "# 9"ZCREATIONDATE, "# 10"ZSTARTDATE, "# 11
- "ZENDDATE "# 12
+ "ZENDDATE, "# 12
+ "ZCUSTOMSORTASCENDING, "# 13
+ "ZCUSTOMSORTKEY "# 14"FROM ZGENERICALBUM ")foralbuminc:
@@ -1793,6 +1825,8 @@
"creation_date":album[10],"start_date":album[11],"end_date":album[12],
+ "customsortascending":album[13],
+ "customsortkey":album[14],}# add cross-reference by pk to uuid
@@ -1902,7 +1936,7 @@
{asset_table}.ZAVALANCHEUUID,{asset_table}.ZAVALANCHEPICKTYPE,{asset_table}.ZKINDSUBTYPE,
-{asset_table}.ZCUSTOMRENDEREDVALUE,
+{asset_table}.{hdr_type_column}, ZADDITIONALASSETATTRIBUTES.ZCAMERACAPTUREDEVICE,{asset_table}.ZCLOUDASSETGUID, ZADDITIONALASSETATTRIBUTES.ZREVERSELOCATIONDATA,
@@ -1921,7 +1955,8 @@
{asset_table}.ZVISIBILITYSTATE,{asset_table}.ZTRASHEDDATE,{asset_table}.ZSAVEDASSETTYPE,
-{asset_table}.ZADDEDDATE
+{asset_table}.ZADDEDDATE,
+{asset_table}.Z_PK FROM {asset_table} JOIN ZADDITIONALASSETATTRIBUTES ON ZADDITIONALASSETATTRIBUTES.ZASSET = {asset_table}.Z_PK ORDER BY {asset_table}.ZUUID """
@@ -1970,6 +2005,7 @@
# 39 ZGENERICASSET.ZTRASHEDDATE -- date item placed in the trash or null if not in trash# 40 ZGENERICASSET.ZSAVEDASSETTYPE -- how item imported# 41 ZGENERICASSET.ZADDEDDATE -- date item added to the library
+ # 42 ZGENERICASSET.Z_PK -- primary keyforrowinc:uuid=row[0]
@@ -2154,6 +2190,8 @@
except(ValueError,TypeError):info["added_date"]=datetime(1970,1,1)
+ info["pk"]=row[42]
+
# initialize import session info which will be filled in later# not every photo has an import session so initialize all records nowinfo["import_session"]=None
@@ -2174,6 +2212,13 @@
self._dbphotos[uuid]=info
+ # compute signatures for finding possible duplicates
+ signature=self._duplicate_signature(uuid)
+ try:
+ self._db_signatures[signature].append(uuid)
+ exceptKeyError:
+ self._db_signatures[signature]=[uuid]
+
# # if row[19] is not None and ((row[20] == 2) or (row[20] == 4)):# # burst photo# if row[19] is not None:
@@ -2259,20 +2304,33 @@
# Get info on remote/local availability for photos in shared albums# Also get UTI of original image (zdatastoresubtype = 1)
- c.execute(
- f""" SELECT
+ ifself._photos_ver>=7:
+ sql_missing=f""" SELECT {asset_table}.ZUUID, ZINTERNALRESOURCE.ZLOCALAVAILABILITY, ZINTERNALRESOURCE.ZREMOTEAVAILABILITY, ZINTERNALRESOURCE.ZDATASTORESUBTYPE,
- ZINTERNALRESOURCE.ZUNIFORMTYPEIDENTIFIER,
+{uti_original_column},
+ null
+ FROM {asset_table}
+ JOIN ZADDITIONALASSETATTRIBUTES ON ZADDITIONALASSETATTRIBUTES.ZASSET = {asset_table}.Z_PK
+ JOIN ZINTERNALRESOURCE ON ZINTERNALRESOURCE.ZASSET = ZADDITIONALASSETATTRIBUTES.ZASSET
+ WHERE ZDATASTORESUBTYPE = 1 OR ZDATASTORESUBTYPE = 3 """
+ else:
+ sql_missing=f""" SELECT
+{asset_table}.ZUUID,
+ ZINTERNALRESOURCE.ZLOCALAVAILABILITY,
+ ZINTERNALRESOURCE.ZREMOTEAVAILABILITY,
+ ZINTERNALRESOURCE.ZDATASTORESUBTYPE,
+{uti_original_column}, ZUNIFORMTYPEIDENTIFIER.ZIDENTIFIER FROM {asset_table} JOIN ZADDITIONALASSETATTRIBUTES ON ZADDITIONALASSETATTRIBUTES.ZASSET = {asset_table}.Z_PK JOIN ZINTERNALRESOURCE ON ZINTERNALRESOURCE.ZASSET = ZADDITIONALASSETATTRIBUTES.ZASSET JOIN ZUNIFORMTYPEIDENTIFIER ON ZUNIFORMTYPEIDENTIFIER.Z_PK = ZINTERNALRESOURCE.ZUNIFORMTYPEIDENTIFIER WHERE ZDATASTORESUBTYPE = 1 OR ZDATASTORESUBTYPE = 3 """
- )
+
+ c.execute(sql_missing)# Order of results:# 0 {asset_table}.ZUUID,
@@ -2332,20 +2390,36 @@
# get information about associted RAW images# RAW images have ZDATASTORESUBTYPE = 17
- c.execute(
- f""" SELECT
+ ifself._photos_ver>=7:
+ sql_raw=f""" SELECT
+{asset_table}.ZUUID,
+ ZINTERNALRESOURCE.ZDATALENGTH,
+ null,
+ ZINTERNALRESOURCE.ZDATASTORESUBTYPE,
+ ZINTERNALRESOURCE.ZRESOURCETYPE,
+ ZINTERNALRESOURCE.ZFILESYSTEMBOOKMARK
+ FROM {asset_table}
+ JOIN ZINTERNALRESOURCE ON ZINTERNALRESOURCE.ZASSET = ZADDITIONALASSETATTRIBUTES.ZASSET
+ JOIN ZADDITIONALASSETATTRIBUTES ON ZADDITIONALASSETATTRIBUTES.ZASSET = {asset_table}.Z_PK
+ WHERE ZINTERNALRESOURCE.ZDATASTORESUBTYPE = 17
+ """
+ else:
+ sql_raw=f""" SELECT{asset_table}.ZUUID, ZINTERNALRESOURCE.ZDATALENGTH, ZUNIFORMTYPEIDENTIFIER.ZIDENTIFIER, ZINTERNALRESOURCE.ZDATASTORESUBTYPE,
- ZINTERNALRESOURCE.ZRESOURCETYPE
+ ZINTERNALRESOURCE.ZRESOURCETYPE,
+ ZINTERNALRESOURCE.ZFILESYSTEMBOOKMARK FROM {asset_table} JOIN ZINTERNALRESOURCE ON ZINTERNALRESOURCE.ZASSET = ZADDITIONALASSETATTRIBUTES.ZASSET JOIN ZADDITIONALASSETATTRIBUTES ON ZADDITIONALASSETATTRIBUTES.ZASSET = {asset_table}.Z_PK JOIN ZUNIFORMTYPEIDENTIFIER ON ZUNIFORMTYPEIDENTIFIER.Z_PK = ZINTERNALRESOURCE.ZUNIFORMTYPEIDENTIFIER WHERE ZINTERNALRESOURCE.ZDATASTORESUBTYPE = 17
- """
- )
+ """
+
+ c.execute(sql_raw)
+
forrowinc:uuid=row[0]ifuuidinself._dbphotos:
@@ -2354,6 +2428,33 @@
self._dbphotos[uuid]["UTI_raw"]=row[2]self._dbphotos[uuid]["datastore_subtype"]=row[3]self._dbphotos[uuid]["resource_type"]=row[4]
+ self._dbphotos[uuid]["raw_bookmark"]=row[5]
+
+ # get paths for the relative imports for RAW+JPEG images
+ c.execute(
+ f""" SELECT
+{asset_table}.ZUUID,
+ ZFILESYSTEMVOLUME.ZNAME,
+ ZFILESYSTEMBOOKMARK.ZPATHRELATIVETOVOLUME
+ FROM {asset_table}
+ JOIN ZINTERNALRESOURCE ON ZINTERNALRESOURCE.ZASSET = ZADDITIONALASSETATTRIBUTES.ZASSET
+ JOIN ZADDITIONALASSETATTRIBUTES ON ZADDITIONALASSETATTRIBUTES.ZASSET = {asset_table}.Z_PK
+ JOIN ZFILESYSTEMBOOKMARK ON ZFILESYSTEMBOOKMARK.ZRESOURCE = ZINTERNALRESOURCE.Z_PK
+ JOIN ZFILESYSTEMVOLUME ON ZFILESYSTEMVOLUME.Z_PK = ZINTERNALRESOURCE.ZFILESYSTEMVOLUME
+ WHERE ZINTERNALRESOURCE.ZDATASTORESUBTYPE = 17
+ """
+ )
+
+ # path to the raw image will be /Volumes/ZFILESYSTEMVOLUME.ZNAME/ZFILESYSTEMBOOKMARK.ZPATHRELATIVETOVOLUME
+ # 0: {asset_table}.ZUUID, -- UUID
+ # 1: ZFILESYSTEMVOLUME.ZNAME, -- name of the volume
+ # 2: ZFILESYSTEMBOOKMARK.ZPATHRELATIVETOVOLUME -- path to the raw image
+
+ forrowinc:
+ uuid=row[0]
+ ifuuidinself._dbphotos:
+ self._dbphotos[uuid]["raw_volume"]=row[1]
+ self._dbphotos[uuid]["raw_relative_path"]=row[2]# add faces and keywords to photo dataforuuidinself._dbphotos:
@@ -2459,9 +2560,9 @@
logging.debug(pformat(self._dbphotos_burst))def_build_album_folder_hierarchy_5(self,uuid,folders=None):
- """ recursively build folder/album hierarchy
- uuid: uuid of the album/folder being processed
- folders: dict holding the folder hierarchy """
+ """recursively build folder/album hierarchy
+ uuid: uuid of the album/folder being processed
+ folders: dict holding the folder hierarchy"""# get parent uuidparent=self._dbalbum_details[uuid]["parentfolder"]
@@ -2482,17 +2583,17 @@
returnfoldersdef_album_folder_hierarchy_list(self,album_uuid):
- """ return appropriate album_folder_hierarchy_list for the _db_version """
+ """return appropriate album_folder_hierarchy_list for the _db_version"""ifself._db_version<=_PHOTOS_4_VERSION:returnself._album_folder_hierarchy_list_4(album_uuid)else:returnself._album_folder_hierarchy_list_5(album_uuid)def_album_folder_hierarchy_list_4(self,album_uuid):
- """ return hierarchical list of folder names album_uuid is contained in
- the folder list is in form:
- ["Top level folder", "sub folder 1", "sub folder 2"]
- returns empty list of album is not in any folders """
+ """return hierarchical list of folder names album_uuid is contained in
+ the folder list is in form:
+ ["Top level folder", "sub folder 1", "sub folder 2"]
+ returns empty list of album is not in any folders"""try:folders=self._dbalbum_folders[album_uuid]exceptKeyError:
@@ -2500,7 +2601,7 @@
return[]def_recurse_folder_hierarchy(folders,hierarchy=[]):
- """ recursively walk the folders dict to build list of folder hierarchy """
+ """recursively walk the folders dict to build list of folder hierarchy"""ifnotfolders:# empty folder dict (album has no folder hierarchy)return[]
@@ -2526,10 +2627,10 @@
returnhierarchydef_album_folder_hierarchy_list_5(self,album_uuid):
- """ return hierarchical list of folder names album_uuid is contained in
- the folder list is in form:
- ["Top level folder", "sub folder 1", "sub folder 2"]
- returns empty list of album is not in any folders """
+ """return hierarchical list of folder names album_uuid is contained in
+ the folder list is in form:
+ ["Top level folder", "sub folder 1", "sub folder 2"]
+ returns empty list of album is not in any folders"""try:folders=self._dbalbum_folders[album_uuid]exceptKeyError:
@@ -2537,7 +2638,7 @@
return[]def_recurse_folder_hierarchy(folders,hierarchy=[]):
- """ recursively walk the folders dict to build list of folder hierarchy """
+ """recursively walk the folders dict to build list of folder hierarchy"""ifnotfolders:# empty folder dict (album has no folder hierarchy)
@@ -2569,15 +2670,15 @@
returnself._album_folder_hierarchy_folderinfo_5(album_uuid)def_album_folder_hierarchy_folderinfo_4(self,album_uuid):
- """ return hierarchical list of FolderInfo objects album_uuid is contained in
- ["Top level folder", "sub folder 1", "sub folder 2"]
- returns empty list of album is not in any folders """
+ """return hierarchical list of FolderInfo objects album_uuid is contained in
+ ["Top level folder", "sub folder 1", "sub folder 2"]
+ returns empty list of album is not in any folders"""# title = photosdb._dbalbum_details[album_uuid]["title"]folders=self._dbalbum_folders[album_uuid]# logging.warning(f"uuid = {album_uuid}, folder = {folders}")def_recurse_folder_hierarchy(folders,hierarchy=[]):
- """ recursively walk the folders dict to build list of folder hierarchy """
+ """recursively walk the folders dict to build list of folder hierarchy"""# logging.warning(f"folders={folders},hierarchy = {hierarchy}")ifnotfolders:# empty folder dict (album has no folder hierarchy)
@@ -2603,14 +2704,14 @@
returnhierarchydef_album_folder_hierarchy_folderinfo_5(self,album_uuid):
- """ return hierarchical list of FolderInfo objects album_uuid is contained in
- ["Top level folder", "sub folder 1", "sub folder 2"]
- returns empty list of album is not in any folders """
+ """return hierarchical list of FolderInfo objects album_uuid is contained in
+ ["Top level folder", "sub folder 1", "sub folder 2"]
+ returns empty list of album is not in any folders"""# title = photosdb._dbalbum_details[album_uuid]["title"]folders=self._dbalbum_folders[album_uuid]def_recurse_folder_hierarchy(folders,hierarchy=[]):
- """ recursively walk the folders dict to build list of folder hierarchy """
+ """recursively walk the folders dict to build list of folder hierarchy"""ifnotfolders:# empty folder dict (album has no folder hierarchy)
@@ -2635,19 +2736,19 @@
returnhierarchydef_get_album_uuids(self,shared=False,import_session=False):
- """ Return list of album UUIDs found in photos database
-
+ """Return list of album UUIDs found in photos database
+
Filters out albums in the trash and any special album types
-
+
Args: shared: boolean; if True, returns shared albums, else normal albums import_session: boolean, if True, returns import session albums, else normal or shared albums Note: flags (shared, import_session) are mutually exclusive
-
+
Raises: ValueError: raised if mutually exclusive flags passed
- Returns: list of album UUIDs
+ Returns: list of album UUIDs """ifsharedandimport_session:raiseValueError(
@@ -2699,14 +2800,14 @@
returnalbum_listdef_get_albums(self,shared=False):
- """ Return list of album titles found in photos database
+ """Return list of album titles found in photos database Albums may have duplicate titles -- these will be treated as a single album.
-
+
Filters out albums in the trash and any special album types Args: shared: boolean; if True, returns shared albums, else normal albums
-
+
Returns: list of album names """
@@ -2725,7 +2826,7 @@
to_date=None,intrash=False,):
- """ Return a list of PhotoInfo objects
+ """Return a list of PhotoInfo objects If called with no args, returns the entire database of photos If called with args, returns photos matching the args (e.g. keywords, persons, etc.) If more than one arg, returns photos matching all the criteria (e.g. keywords AND persons)
@@ -2740,10 +2841,10 @@
persons: list of persons to search for albums: list of album names to search for images: if True, returns image files, if False, does not return images; default is True
- movies: if True, returns movie files, if False, does not return movies; default is True
+ movies: if True, returns movie files, if False, does not return movies; default is True from_date: return photos with creation date >= from_date (datetime.datetime object, default None) to_date: return photos with creation date <= to_date (datetime.datetime object, default None)
- intrash: if True, returns only images in "Recently deleted items" folder,
+ intrash: if True, returns only images in "Recently deleted items" folder, if False returns only photos that aren't deleted; default is False Returns:
@@ -2850,7 +2951,7 @@
returnphotoinfo
[docs]defget_photo(self,uuid):
- """ Returns a single photo matching uuid
+ """Returns a single photo matching uuid Arguments: uuid: the UUID of photo to get
@@ -2865,7 +2966,7 @@
# TODO: add to docs and test
[docs]defphotos_by_uuid(self,uuids):
- """ Returns a list of photos with UUID in uuids.
+ """Returns a list of photos with UUID in uuids. Does not generate error if invalid or missing UUID passed. This is faster than using PhotosDB.photos if you have list of UUIDs. Returns photos regardless of intrash state.
@@ -3217,11 +3318,12 @@
ifoptions.regex:flags=re.IGNORECASEifoptions.ignore_caseelse0
+ render_options=RenderOptions(none_str="")forregex,templateinoptions.regex:regex=re.compile(regex,flags)photo_list=[]forpinphotos:
- rendered,_=p.render_template(template,none_str="")
+ rendered,_=p.render_template(template,render_options)forvalueinrendered:ifregex.search(value):photo_list.append(p)
@@ -3236,8 +3338,66 @@
exceptExceptionase:raiseValueError(f"Invalid query_eval CRITERIA: {e}")
+ ifoptions.duplicate:
+ no_date=datetime(1970,1,1)
+ tz=timezone(timedelta(0))
+ no_date=no_date.astimezone(tz=tz)
+ photos=sorted(
+ [pforpinphotosifp.duplicates],
+ key=lambdax:x.date_addedorno_date,
+ )
+ # gather all duplicates but ensure each uuid is only represented once
+ photodict=OrderedDict()
+ forpinphotos:
+ ifp.uuidnotinphotodict:
+ photodict[p.uuid]=p
+ fordinsorted(
+ p.duplicates,key=lambdax:x.date_addedorno_date
+ ):
+ ifd.uuidnotinphotodict:
+ photodict[d.uuid]=d
+ photos=list(photodict.values())
+
+ # filter for deleted as photo.duplicates will include photos in the trash
+ ifnot(options.deletedoroptions.deleted_only):
+ photos=[pforpinphotosifnotp.intrash]
+ ifoptions.deleted_only:
+ photos=[pforpinphotosifp.intrash]
+
+ ifoptions.location:
+ photos=[pforpinphotosifp.location!=(None,None)]
+ elifoptions.no_location:
+ photos=[pforpinphotosifp.location==(None,None)]
+
+ ifoptions.selected:
+ # photos selected in Photos app
+ try:
+ # catch AppleScript errors as the scripting interfce to Photos is flaky
+ selected=photoscript.PhotosLibrary().selection
+ selected_uuid=[p.uuidforpinselected]
+ photos=[pforpinphotosifp.uuidinselected_uuid]
+ exceptException:
+ # no photos selected or a selected photo was "open"
+ # selection only works if photos selected in main media browser
+ photos=[]
+
+ ifoptions.function:
+ forfunctioninoptions.function:
+ photos=function[0](photos)
+
returnphotos
+ def_duplicate_signature(self,uuid):
+ """Compute a signature for finding possible duplicates"""
+ return(
+ self._dbphotos[uuid]["original_filesize"],
+ self._dbphotos[uuid]["imageDate"],
+ self._dbphotos[uuid]["height"],
+ self._dbphotos[uuid]["width"],
+ self._dbphotos[uuid]["UTI"],
+ self._dbphotos[uuid]["hasAdjustments"],
+ )
+
def__repr__(self):returnf"osxphotos.{self.__class__.__name__}(dbfile='{self.db_path}')"
@@ -3249,8 +3409,8 @@
returnFalsedef__len__(self):
- """ Returns number of photos in the database
- Includes recently deleted photos and non-selected burst images
+ """Returns number of photos in the database
+ Includes recently deleted photos and non-selected burst images """returnlen(self._dbphotos)
Specify Photos database path. Path to Photos library/database can be specified using either –db or directly as PHOTOS_LIBRARY positional argument. If neither –db or PHOTOS_LIBRARY provided, will attempt to find the library to use in the following order: 1. last opened library, 2. system library, 3. ~/Pictures/Photos Library.photoslibrary
Specify Photos database path. Path to Photos library/database can be specified using either –db or directly as PHOTOS_LIBRARY positional argument. If neither –db or PHOTOS_LIBRARY provided, will attempt to find the library to use in the following order: 1. last opened library, 2. system library, 3. ~/Pictures/Photos Library.photoslibrary
Specify Photos database path. Path to Photos library/database can be specified using either –db or directly as PHOTOS_LIBRARY positional argument. If neither –db or PHOTOS_LIBRARY provided, will attempt to find the library to use in the following order: 1. last opened library, 2. system library, 3. ~/Pictures/Photos Library.photoslibrary
Specify Photos database path. Path to Photos library/database can be specified using either –db or directly as PHOTOS_LIBRARY positional argument. If neither –db or PHOTOS_LIBRARY provided, will attempt to find the library to use in the following order: 1. last opened library, 2. system library, 3. ~/Pictures/Photos Library.photoslibrary
Search for photos in an album in folder FOLDER. If more than one folder, treated as “OR”, e.g. find photos in any FOLDER. Only searches top level folders (e.g. does not look at subfolders)
Search for photos with filename matching FILENAME. If more than one –name options is specified, they are treated as “OR”, e.g. find photos matching any FILENAME.
Search for photos with possible duplicates. osxphotos will compare signatures of photos, evaluating date created, size, height, width, and edited status to find possible duplicates. This does not compare images byte-for-byte nor compare hashes but should find photos imported multiple times or duplicated within Photos.
Search for photos with size >= SIZE bytes. The size evaluated is the photo’s original size (when imported to Photos). Size may be specified as integer bytes or using SI or NIST units. For example, the following are all valid and equivalent sizes: ‘1048576’ ‘1.048576MB’, ‘1 MiB’.
Search for photos with size <= SIZE bytes. The size evaluated is the photo’s original size (when imported to Photos). Size may be specified as integer bytes or using SI or NIST units. For example, the following are all valid and equivalent sizes: ‘1048576’ ‘1.048576MB’, ‘1 MiB’.
Search for photos where TEMPLATE matches regular expression REGEX. For example, to find photos in an album that begins with ‘Beach’: ‘–regex “^Beach” “{album}”’. You may specify more than one regular expression match by repeating ‘–regex’ with different arguments.
Evaluate CRITERIA to filter photos. CRITERIA will be evaluated in context of the following python list comprehension: photos = [photo for photo in photos if CRITERIA] where photo represents a PhotoInfo object. For example: –query-eval photo.favorite returns all photos that have been favorited and is equivalent to –favorite. You may specify more than one CRITERIA by using –query-eval multiple times. CRITERIA must be a valid python expression. See https://rhettbull.github.io/osxphotos/ for additional documentation on the PhotoInfo class.
Run function to filter photos. Use this in format: –query-function filename.py::function where filename.py is a python file you’ve created and function is the name of the function in the python file you want to call. Your function will be passed a list of PhotoInfo objects and is expected to return a filtered list of PhotoInfo objects. You may use more than one function by repeating the –query-function option with a different value. Your query function will be called after all other query options have been evaluated. See https://github.com/RhetTbull/osxphotos/blob/master/examples/query_function.py for example of how to use this option.
When used with ‘–update’, ignores file signature when updating files. This is useful if you have processed or edited exported photos changing the file signature (size & modification date). In this case, ‘–update’ would normally re-export the processed files but with ‘–ignore-signature’, files which exist in the export directory will not be re-exported. If used with ‘–sidecar’, ‘–ignore-signature’ has the following behavior: 1) if the metadata (in Photos) that went into the sidecar did not change, the sidecar will not be updated; 2) if the metadata (in Photos) that went into the sidecar did change, a new sidecar is written but a new image file is not; 3) if a sidecar does not exist for the photo, a sidecar will be written whether or not the photo file was written or updated.
If used with –update, ignores any previously exported files, even if missing from the export folder and only exports new files that haven’t previously been exported.
Hardlink files instead of copying them. Cannot be used with –exiftool which creates copies of the files with embedded EXIF data. Note: on APFS volumes, files are cloned when exporting giving many of the same advantages as hardlinks without having to use –export-as-hardlink.
Overwrite existing files. Default behavior is to add (1), (2), etc to filename if file already exists. Use this with caution as it may create name collisions on export. (e.g. if two files happen to have the same name)
Automatically retry export up to RETRY times if an error occurs during export. This may be useful with network drives that experience intermittent errors.
Do not export associated raw images of a RAW+JPEG pair. Note: this does not skip raw photos if the raw photo does not have an associated jpeg image (e.g. the raw file was imported to Photos without a jpeg preview).
Do not export associated RAW image of a RAW+JPEG pair. Note: this does not skip RAW photos if the RAW photo does not have an associated JPEG image (e.g. the RAW file was imported to Photos without a JPEG preview).
Use photo’s current filename instead of original filename for export. Note: Starting with Photos 5, all photos are renamed upon import. By default, photos are exported with the the original name they had before import.
Convert all non-JPEG images (e.g. RAW, HEIC, PNG, etc) to JPEG upon export. Note: does not convert the RAW component of a RAW+JPEG pair as the associated JPEG image will be exported. You can use –skip-raw to skip exporting the associated RAW image of a RAW+JPEG pair. See also –jpeg-quality and –jpeg-ext. Only works if your Mac has a GPU (thus may not work on virtual machines).
Value in range 0.0 to 1.0 to use with –convert-to-jpeg. A value of 1.0 specifies best quality, a value of 0.0 specifies maximum compression. Defaults to 1.0
Export preview image generated by Photos. This is a lower-resolution image used by Photos to quickly preview the image. See also –preview-suffix and –preview-if-missing.
Export preview image generated by Photos if the actual photo file is missing from the library. This may be helpful if photos were not copied to the Photos library and the original photo is missing. See also –preview-suffix and –preview.
Optional suffix template for naming preview photos. Default name for preview photos is in form ‘photoname_preview.ext’. For example, with ‘–preview-suffix _low_res’, the preview photo would be named ‘photoname_low_res.ext’. The default suffix is ‘_preview’. Multi-value templates (see Templating System) are not permitted with –preview-suffix. See also –preview and –preview-if-missing.
Attempt to download missing photos from iCloud. The current implementation uses Applescript to interact with Photos to export the photo which will force Photos to download from iCloud if the photo does not exist on disk. This will be slow and will require internet connection. This obviously only works if the Photos library is synched to iCloud. Note: –download-missing does not currently export all burst images; only the primary photo will be exported–associated burst images will be skipped.
Create sidecar for each photo exported; valid FORMAT values: xmp, json, exiftool; –sidecar xmp: create XMP sidecar used by Digikam, Adobe Lightroom, etc. The sidecar file is named in format photoname.ext.xmp The XMP sidecar exports the following tags: Description, Title, Keywords/Tags, Subject (set to Keywords + PersonInImage), PersonInImage, CreateDate, ModifyDate, GPSLongitude, Face Regions (Metadata Working Group and Microsoft Photo).
–sidecar json: create JSON sidecar useable by exiftool (https://exiftool.org/) The sidecar file can be used to apply metadata to the file with exiftool, for example: “exiftool -j=photoname.jpg.json photoname.jpg” The sidecar file is named in format photoname.ext.json; format includes tag groups (equivalent to running ‘exiftool -G -j’).
–sidecar exiftool: create JSON sidecar compatible with output of ‘exiftool -j’. Unlike ‘–sidecar json’, ‘–sidecar exiftool’ does not export tag groups. Sidecar filename is in format photoname.ext.json; For a list of tags exported in the JSON and exiftool sidecar, see ‘–exiftool’. See also ‘–ignore-signature’.
Drop the photo’s extension when naming sidecar files. By default, sidecar files are named in format ‘photo_filename.photo_ext.sidecar_ext’, e.g. ‘IMG_1234.JPG.xmp’. Use ‘–sidecar-drop-ext’ to ignore the photo extension. Resulting sidecar files will have name in format ‘IMG_1234.xmp’. Warning: this may result in sidecar filename collisions if there are files of different types but the same name in the output directory, e.g. ‘IMG_1234.JPG’ and ‘IMG_1234.MOV’.
Use exiftool to write metadata directly to exported photos. To use this option, exiftool must be installed and in the path. exiftool may be installed from https://exiftool.org/. Cannot be used with –export-as-hardlink. Writes the following metadata: EXIF:ImageDescription, XMP:Description (see also –description-template); XMP:Title; XMP:TagsList, IPTC:Keywords, XMP:Subject (see also –keyword-template, –person-keyword, –album-keyword); XMP:PersonInImage; EXIF:GPSLatitudeRef; EXIF:GPSLongitudeRef; EXIF:GPSLatitude; EXIF:GPSLongitude; EXIF:GPSPosition; EXIF:DateTimeOriginal; EXIF:OffsetTimeOriginal; EXIF:ModifyDate (see –ignore-date-modified); IPTC:DateCreated; IPTC:TimeCreated; (video files only): QuickTime:CreationDate; QuickTime:CreateDate; QuickTime:ModifyDate (see also –ignore-date-modified); QuickTime:GPSCoordinates; UserData:GPSCoordinates.
Optional flag/option to pass to exiftool when using –exiftool. For example, –exiftool-option ‘-m’ to ignore minor warnings. Specify these as you would on the exiftool command line. See exiftool docs at https://exiftool.org/exiftool_pod.html for full list of options. More than one option may be specified by repeating the option, e.g. –exiftool-option ‘-m’ –exiftool-option ‘-F’.
If used with –exiftool or –sidecar, will ignore the photo modification date and set EXIF:ModifyDate to EXIF:DateTimeOriginal; this is consistent with how Photos handles the EXIF:ModifyDate tag.
For use with –exiftool, –sidecar; specify a template string to use as keyword in the form ‘{name,DEFAULT}’ This is the same format as –directory. For example, if you wanted to add the full path to the folder and album photo is contained in as a keyword when exporting you could specify –keyword-template “{folder_album}” You may specify more than one template, for example –keyword-template “{folder_album}” –keyword-template “{created.year}”. See ‘–replace-keywords’ and Templating System below.
Replace keywords with any values specified with –keyword-template. By default, –keyword-template will add keywords to any keywords already associated with the photo. If –replace-keywords is specified, values from –keyword-template will replace any existing keywords instead of adding additional keywords.
For use with –exiftool, –sidecar; specify a template string to use as description in the form ‘{name,DEFAULT}’ This is the same format as –directory. For example, if you wanted to append ‘exported with osxphotos on [today’s date]’ to the description, you could specify –description-template “{descr} exported with osxphotos on {today.date}” See Templating System below.
Set MacOS Finder tags to TEMPLATE. These tags can be searched in the Finder or Spotlight with ‘tag:tagname’ format. For example, ‘–finder-tag-template “{label}”’ to set Finder tags to photo labels. You may specify multiple TEMPLATE values by using ‘–finder-tag-template’ multiple times. See also ‘–finder-tag-keywords and Extended Attributes below.’.
Set MacOS Finder tags to keywords; any keywords specified via ‘–keyword-template’, ‘–person-keyword’, etc. will also be used as Finder tags. See also ‘–finder-tag-template and Extended Attributes below.’.
Set extended attribute ATTRIBUTE to TEMPLATE value. Valid attributes are: ‘authors’, ‘comment’, ‘copyright’, ‘description’, ‘findercomment’, ‘headline’, ‘keywords’. For example, to set Finder comment to the photo’s title and description: ‘–xattr-template findercomment “{title}; {descr}” See Extended Attributes below for additional details on this option.
Set extended attribute ATTRIBUTE to TEMPLATE value. Valid attributes are: ‘authors’, ‘comment’, ‘copyright’, ‘creator’, ‘description’, ‘findercomment’, ‘headline’, ‘keywords’, ‘participants’, ‘projects’, ‘rating’, ‘subject’, ‘title’, ‘version’. For example, to set Finder comment to the photo’s title and description: ‘–xattr-template findercomment “{title}; {descr}” See Extended Attributes below for additional details on this option.
Optional template for specifying name of output file in the form ‘{name,DEFAULT}’. File extension will be added automatically–do not include an extension in the FILENAME template. See below for additional details on templating system.
Specify file extension for JPEG files. Photos uses .jpeg for edited images but many images are imported with .jpg or .JPG which can result in multiple different extensions used for JPEG files upon export. Use –jpeg-ext to specify a single extension to use for all exported JPEG images. Valid values are jpeg, jpg, JPEG, JPG; e.g. ‘–jpeg-ext jpg’ to use ‘.jpg’ for all JPEGs.
Optionally strip leading and trailing whitespace from any rendered templates. For example, if –filename template is “{title,} {original_name}” and image has no title, resulting file would have a leading space but if used with –strip, this will be removed.
Optional suffix template for naming edited photos. Default name for edited photos is in form ‘photoname_edited.ext’. For example, with ‘–edited-suffix _bearbeiten’, the edited photo would be named ‘photoname_bearbeiten.ext’. The default suffix is ‘_edited’. Multi-value templates (see Templating System) are not permitted with –edited-suffix.
Optional suffix template for naming original photos. Default name for original photos is in form ‘filename.ext’. For example, with ‘–original-suffix _original’, the original photo would be named ‘filename_original.ext’. The default suffix is ‘’ (no suffix). Multi-value templates (see Templating System) are not permitted with –original-suffix.
Use with ‘–download-missing’ or ‘–use-photos-export’ to use direct Photos interface instead of AppleScript to export. Highly experimental alpha feature; does not work with iTerm2 (use with Terminal.app). This is faster and more reliable than the default AppleScript interface.
Cleanup export directory by deleting any files which were not included in this export set. For example, photos which had previously been exported and were subsequently deleted in Photos. WARNING: –cleanup will delete any files in the export directory that were not exported by osxphotos, for example, your own scripts or other files. Be sure this is what you intend before using –cleanup. Use –dry-run with –cleanup first if you’re not certain.
Add all exported photos to album ALBUM in Photos. Album ALBUM will be created if it doesn’t exist. All exported photos will be added to this album. This only works if the Photos library being exported is the last-opened (default) library in Photos. This feature is currently experimental. I don’t know how well it will work on large export sets.
Add all skipped photos to album ALBUM in Photos. Album ALBUM will be created if it doesn’t exist. All skipped photos will be added to this album. This only works if the Photos library being exported is the last-opened (default) library in Photos. This feature is currently experimental. I don’t know how well it will work on large export sets.
Add all missing photos to album ALBUM in Photos. Album ALBUM will be created if it doesn’t exist. All missing photos will be added to this album. This only works if the Photos library being exported is the last-opened (default) library in Photos. This feature is currently experimental. I don’t know how well it will work on large export sets.
Run COMMAND on exported files of category CATEGORY. CATEGORY can be one of: exported, new, updated, skipped, missing, exif_updated, touched, converted_to_jpeg, sidecar_json_written, sidecar_json_skipped, sidecar_exiftool_written, sidecar_exiftool_skipped, sidecar_xmp_written, sidecar_xmp_skipped, error. COMMAND is an osxphotos template string, for example: ‘–post-command exported “echo {filepath|shell_quote} >> {export_dir}/exported.txt”’, which appends the full path of all exported files to the file ‘exported.txt’. You can run more than one command by repeating the ‘–post-command’ option with different arguments. See Post Command below.
Run function on exported files. Use this in format: –post-function filename.py::function where filename.py is a python file you’ve created and function is the name of the function in the python file you want to call. The function will be passed information about the photo that’s been exported and a list of all exported files associated with the photo. You can run more than one function by repeating the ‘–post-function’ option with different arguments. See Post Function below.
Specify alternate name for database file which stores state information for export and –update. If –exportdb is not specified, export database will be saved to ‘.osxphotos_export.db’ in the export directory. Must be specified as filename only, not a path, as export database will be saved in export directory.
Load options from file as written with –save-config. This allows you to save a complex export command to file for later reuse. For example: ‘osxphotos export <lots of options here> –save-config osxphotos.toml’ then ‘osxphotos export /path/to/export –load-config osxphotos.toml’. If any other command line options are used in conjunction with –load-config, they will override the corresponding values in the config file.
Specify Photos database path. Path to Photos library/database can be specified using either –db or directly as PHOTOS_LIBRARY positional argument. If neither –db or PHOTOS_LIBRARY provided, will attempt to find the library to use in the following order: 1. last opened library, 2. system library, 3. ~/Pictures/Photos Library.photoslibrary
Specify Photos database path. Path to Photos library/database can be specified using either –db or directly as PHOTOS_LIBRARY positional argument. If neither –db or PHOTOS_LIBRARY provided, will attempt to find the library to use in the following order: 1. last opened library, 2. system library, 3. ~/Pictures/Photos Library.photoslibrary
Specify Photos database path. Path to Photos library/database can be specified using either –db or directly as PHOTOS_LIBRARY positional argument. If neither –db or PHOTOS_LIBRARY provided, will attempt to find the library to use in the following order: 1. last opened library, 2. system library, 3. ~/Pictures/Photos Library.photoslibrary
Specify Photos database path. Path to Photos library/database can be specified using either –db or directly as PHOTOS_LIBRARY positional argument. If neither –db or PHOTOS_LIBRARY provided, will attempt to find the library to use in the following order: 1. last opened library, 2. system library, 3. ~/Pictures/Photos Library.photoslibrary
Specify Photos database path. Path to Photos library/database can be specified using either –db or directly as PHOTOS_LIBRARY positional argument. If neither –db or PHOTOS_LIBRARY provided, will attempt to find the library to use in the following order: 1. last opened library, 2. system library, 3. ~/Pictures/Photos Library.photoslibrary
Specify Photos database path. Path to Photos library/database can be specified using either –db or directly as PHOTOS_LIBRARY positional argument. If neither –db or PHOTOS_LIBRARY provided, will attempt to find the library to use in the following order: 1. last opened library, 2. system library, 3. ~/Pictures/Photos Library.photoslibrary
Search for photos in an album in folder FOLDER. If more than one folder, treated as “OR”, e.g. find photos in any FOLDER. Only searches top level folders (e.g. does not look at subfolders)
Search for photos with filename matching FILENAME. If more than one –name options is specified, they are treated as “OR”, e.g. find photos matching any FILENAME.
Search for photos with possible duplicates. osxphotos will compare signatures of photos, evaluating date created, size, height, width, and edited status to find possible duplicates. This does not compare images byte-for-byte nor compare hashes but should find photos imported multiple times or duplicated within Photos.
Search for photos with size >= SIZE bytes. The size evaluated is the photo’s original size (when imported to Photos). Size may be specified as integer bytes or using SI or NIST units. For example, the following are all valid and equivalent sizes: ‘1048576’ ‘1.048576MB’, ‘1 MiB’.
Search for photos with size <= SIZE bytes. The size evaluated is the photo’s original size (when imported to Photos). Size may be specified as integer bytes or using SI or NIST units. For example, the following are all valid and equivalent sizes: ‘1048576’ ‘1.048576MB’, ‘1 MiB’.
Search for photos where TEMPLATE matches regular expression REGEX. For example, to find photos in an album that begins with ‘Beach’: ‘–regex “^Beach” “{album}”’. You may specify more than one regular expression match by repeating ‘–regex’ with different arguments.
Evaluate CRITERIA to filter photos. CRITERIA will be evaluated in context of the following python list comprehension: photos = [photo for photo in photos if CRITERIA] where photo represents a PhotoInfo object. For example: –query-eval photo.favorite returns all photos that have been favorited and is equivalent to –favorite. You may specify more than one CRITERIA by using –query-eval multiple times. CRITERIA must be a valid python expression. See https://rhettbull.github.io/osxphotos/ for additional documentation on the PhotoInfo class.
Run function to filter photos. Use this in format: –query-function filename.py::function where filename.py is a python file you’ve created and function is the name of the function in the python file you want to call. Your function will be passed a list of PhotoInfo objects and is expected to return a filtered list of PhotoInfo objects. You may use more than one function by repeating the –query-function option with a different value. Your query function will be called after all other query options have been evaluated. See https://github.com/RhetTbull/osxphotos/blob/master/examples/query_function.py for example of how to use this option.
Add all photos from query to album ALBUM in Photos. Album ALBUM will be created if it doesn’t exist. All photos in the query results will be added to this album. This only works if the Photos library being queried is the last-opened (default) library in Photos. This feature is currently experimental. I don’t know how well it will work on large query sets.
Specify Photos database path. Path to Photos library/database can be specified using either –db or directly as PHOTOS_LIBRARY positional argument. If neither –db or PHOTOS_LIBRARY provided, will attempt to find the library to use in the following order: 1. last opened library, 2. system library, 3. ~/Pictures/Photos Library.photoslibrary
Only works on macOS (aka Mac OS X). Tested on macOS Sierra (10.12.6) until macOS Catalina (10.15.7).
-Beta support for macOS Big Sur (10.16.01/11.01).
+
Only works on macOS (aka Mac OS X). Tested on macOS Sierra (10.12.6) through macOS Big Sur (11.3).
+
If you have access to macOS 12 / Monterey beta and would like to help ensure osxphotos is compatible, please contact me via GitHub.
This package will read Photos databases for any supported version on any supported macOS version.
E.g. you can read a database created with Photos 5.0 on MacOS 10.15 on a machine running macOS 10.12 and vice versa.
Requires python >= 3.7.
@@ -122,6 +122,8 @@ Alternatively, you can also run the command line utility like this: persons Printoutpersons(faces)foundinthePhotoslibrary.placesPrintoutplacesfoundinthePhotoslibrary.queryQuerythePhotosdatabaseusing1ormoresearchoptions;if...
+ replRuninteractiveosxphotosshell
+ tutorialDisplayosxphotostutorial.
To get help on a specific command, use osxphotoshelp<command_name>
@@ -292,6 +294,8 @@ Alternatively, you can also run the command line utility like this: persons
return list of AlbumInfo objects for each shared album in the photos database
only valid for Photos 5; on Photos <= 4, prints warning and returns empty list
returns shared albums as dict of albums, count in reverse sorted order (descending)
valid only on Photos 5; on Photos <= 4, prints warning and returns empty dict
Return a list of PhotoInfo objects
If called with no args, returns the entire database of photos
If called with args, returns photos matching the args (e.g. keywords, persons, etc.)
@@ -236,8 +236,8 @@ if False returns only photos that aren’t deleted; default is False
Does not generate error if invalid or missing UUID passed.
This is faster than using PhotosDB.photos if you have list of UUIDs.
@@ -255,8 +255,8 @@ Returns photos regardless of intrash state.
If photo is a burst photo, returns list of AlbumInfo objects representing albums the photo is contained in as well as albums the burst key photo is contained in, otherwise returns self.album_info.
If photo is a burst photo, returns list of PhotoInfo objects
that are part of the same burst photo set; otherwise returns empty list.
self is not included in the returned list
return list of PhotoInfo objects for possible duplicates (matching signature of original size, date, height, width) or empty list if no matching duplicates
Returns an ExifInfo object with the EXIF data for photo
Note: the returned EXIF data is the data Photos stores in the database on import;
ExifInfo does not provide access to the EXIF info in the actual image file
@@ -767,18 +773,18 @@ Some or all of the fields may be None
Only valid for Photos 5; on earlier database returns None
Returns a ExifToolCaching (read-only instance of ExifTool) object for the photo.
+Requires that exiftool (https://exiftool.org/) be installed
If exiftool not installed, logs warning and returns None
If photo path is missing, returns None
export photo
dest: must be valid destination path (or exception raised)
filename: (optional): name of exported picture; if not provided, will use current filename
@@ -794,13 +800,13 @@ e.g. to get the extension of the edited photo,
reference PhotoInfo.path_edited
-
edited: (boolean, default=False); if True will export the edited version of the photo
(or raise exception if no edited version)
+
edited: (boolean, default=False); if True will export the edited version of the photo, otherwise exports the original version
(or raise exception if no edited version)
-
live_photo: (boolean, default=False); if True, will also export the associted .mov for live photos
-raw_photo: (boolean, default=False); if True, will also export the associted RAW photo
+
live_photo: (boolean, default=False); if True, will also export the associated .mov for live photos
+raw_photo: (boolean, default=False); if True, will also export the associated RAW photo
export_as_hardlink: (boolean, default=False); if True, will hardlink files instead of copying them
-overwrite: (boolean, default=False); if True will overwrite files if they alreay exist
+overwrite: (boolean, default=False); if True will overwrite files if they already exist
increment: (boolean, default=True); if True, will increment file name until a non-existant name is found
if overwrite=False and increment=False, export will fail if destination file already exists
@@ -822,13 +828,14 @@ when exporting metadata with exiftool or sidecar
use_persons_as_keywords: (boolean, default = False); if True, will include person names in keywords
when exporting metadata with exiftool or sidecar
keyword_template: (list of strings); list of template strings that will be rendered as used as keywords
-description_template: string; optional template string that will be rendered for use as photo description
+description_template: string; optional template string that will be rendered for use as photo description
+render_options: an optional osxphotos.phototemplate.RenderOptions instance with options to pass to template renderer
export photo, like export but with update and dry_run options
dest: must be valid destination path or exception raised
filename: (optional): name of exported picture; if not provided, will use current filename
@@ -841,14 +848,12 @@ in which case export will use the extension provided by Photos upon export.
e.g. to get the extension of the edited photo,
reference PhotoInfo.path_edited
-
-
edited: (boolean, default=False); if True will export the edited version of the photo
(or raise exception if no edited version)
-
-
-
live_photo: (boolean, default=False); if True, will also export the associted .mov for live photos
-raw_photo: (boolean, default=False); if True, will also export the associted RAW photo
+
original: (boolean, default=True); if True, will export the original version of the photo
+edited: (boolean, default=False); if True will export the edited version of the photo
+live_photo: (boolean, default=False); if True, will also export the associated .mov for live photos
+raw_photo: (boolean, default=False); if True, will also export the associated RAW photo
export_as_hardlink: (boolean, default=False); if True, will hardlink files instead of copying them
-overwrite: (boolean, default=False); if True will overwrite files if they alreay exist
+overwrite: (boolean, default=False); if True will overwrite files if they already exist
increment: (boolean, default=True); if True, will increment file name until a non-existant name is found
if overwrite=False and increment=False, export will fail if destination file already exists
@@ -896,7 +901,10 @@ merge_exif_persons: boolean; if True, merged persons found in file’s exif data
jpeg_ext: if set, will use this value for extension on jpegs converted to jpeg with convert_to_jpeg; if not set, uses jpeg; do not include the leading “.”
persons: if True, include persons in exported metadata
location: if True, include location in exported metadata
-replace_keywords: if True, keyword_template replaces any keywords, otherwise it’s additive
+replace_keywords: if True, keyword_template replaces any keywords, otherwise it’s additive
+preview: if True, also exports preview image
+preview_suffix: optional string to append to end of filename for preview images
+render_options: optional osxphotos.phototemplate.RenderOptions instance to specify options for rendering templates
Returns: ExportResults class
ExportResults has attributes:
“exported”,
@@ -923,90 +931,90 @@ replace_keywords: if True, keyword_template replaces any keywords, otherwise it
Returns True if photo is cloud asset and is synched to cloud
False if photo is cloud asset and not yet synched to cloud
None if photo is not cloud asset
returns true if photo is missing from disk (which means it’s not been downloaded from iCloud)
NOTE: the photos.db database uses an asynchrounous write-ahead log so changes in Photos