Refactored PhotoInfo.export2

This commit is contained in:
Rhet Turnbull 2021-06-06 21:02:22 -07:00
parent bb96c35672
commit d7a9ad1d0a
7 changed files with 222 additions and 190 deletions

View File

@ -58,13 +58,13 @@ MAX_PHOTOSCRIPT_RETRIES = 3
class ExportError(Exception):
""" error during export """
"""error during export"""
pass
class ExportResults:
""" holds export results for export2 """
"""holds export results for export2"""
def __init__(
self,
@ -119,7 +119,7 @@ class ExportResults:
self.missing_album = missing_album or []
def all_files(self):
""" return all filenames contained in results """
"""return all filenames contained in results"""
files = (
self.exported
+ self.new
@ -200,7 +200,7 @@ class ExportResults:
# hexdigest is not a class method, don't import this into PhotoInfo
def hexdigest(strval):
""" hexdigest of a string, using blake2b """
"""hexdigest of a string, using blake2b"""
h = hashlib.blake2b(digest_size=20)
h.update(bytes(strval, "utf-8"))
return h.hexdigest()
@ -343,13 +343,13 @@ def _check_export_suffix(src, dest, edited):
# not a class method, don't import into PhotoInfo
def rename_jpeg_files(files, jpeg_ext, fileutil):
""" rename any jpeg files in files so that extension matches jpeg_ext
"""rename any jpeg files in files so that extension matches jpeg_ext
Args:
files: list of file paths
jpeg_ext: extension to use for jpeg files found in files, e.g. "jpg"
fileutil: a FileUtil object
Returns:
list of files with updated names
@ -415,7 +415,7 @@ def export(
sidecar_exiftool: if set will write a json sidecar with data in format readable by exiftool
sidecar filename will be dest/filename.json; does not include exiftool tag group names (e.g. `exiftool -j`)
sidecar_xmp: if set will write an XMP sidecar with IPTC data
sidecar filename will be dest/filename.xmp
sidecar filename will be dest/filename.xmp
use_photos_export: (boolean, default=False); if True will attempt to export photo via applescript interaction with Photos
timeout: (int, default=120) timeout in seconds used with use_photos_export
exiftool: (boolean, default = False); if True, will use exiftool to write metadata to export file
@ -426,7 +426,7 @@ def export(
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
Returns: list of photos exported
"""
@ -555,8 +555,8 @@ def export2(
location: if True, include location in exported metadata
replace_keywords: if True, keyword_template replaces any keywords, otherwise it's additive
Returns: ExportResults class
ExportResults has attributes:
Returns: ExportResults class
ExportResults has attributes:
"exported",
"new",
"updated",
@ -576,7 +576,7 @@ def export2(
"exiftool_warning",
"exiftool_error",
Note: to use dry run mode, you must set dry_run=True and also pass in memory version of export_db,
and no-op fileutil (e.g. ExportDBInMemory and FileUtilNoOp)
"""
@ -817,139 +817,21 @@ def export2(
)
all_results += results
else:
# TODO: move this big if/else block to separate functions
# e.g. _export_with_photos_export or such
# use_photo_export
# export live_photo .mov file?
live_photo = True if live_photo and self.live_photo else False
if edited or self.shared:
# exported edited version and not original
# shared photos (in shared albums) show up as not having adjustments (not edited)
# but Photos is unable to export the "original" as only a jpeg copy is shared in iCloud
# so tell Photos to export the current version in this case
if filename:
# use filename stem provided
filestem = dest.stem
else:
# didn't get passed a filename, add _edited
filestem = f"{dest.stem}{edited_identifier}"
uti = self.uti_edited if edited and self.uti_edited else self.uti
ext = get_preferred_uti_extension(uti)
dest = dest.parent / f"{filestem}{ext}"
if use_photokit:
photolib = PhotoLibrary()
photo = None
try:
photo = photolib.fetch_uuid(self.uuid)
except PhotoKitFetchFailed as e:
# if failed to find UUID, might be a burst photo
if self.burst and self._info["burstUUID"]:
bursts = photolib.fetch_burst_uuid(
self._info["burstUUID"], all=True
)
# PhotoKit UUIDs may contain "/L0/001" so only look at beginning
photo = [p for p in bursts if p.uuid.startswith(self.uuid)]
photo = photo[0] if photo else None
if not photo:
all_results.error.append(
(
str(dest),
f"PhotoKitFetchFailed exception exporting photo {self.uuid}: {e} ({lineno(__file__)})",
)
)
if photo:
if not dry_run:
try:
exported = photo.export(
dest.parent, dest.name, version=PHOTOS_VERSION_CURRENT
)
all_results.exported.extend(exported)
except Exception as e:
all_results.error.append(
(str(dest), f"{e} ({lineno(__file__)})")
)
else:
# dry_run, don't actually export
all_results.exported.append(str(dest))
else:
try:
exported = _export_photo_uuid_applescript(
self.uuid,
dest.parent,
filestem=filestem,
original=False,
edited=True,
live_photo=live_photo,
timeout=timeout,
burst=self.burst,
dry_run=dry_run,
)
all_results.exported.extend(exported)
except ExportError as e:
all_results.error.append((str(dest), f"{e} ({lineno(__file__)})"))
else:
# export original version and not edited
filestem = dest.stem
if use_photokit:
photolib = PhotoLibrary()
photo = None
try:
photo = photolib.fetch_uuid(self.uuid)
except PhotoKitFetchFailed:
# if failed to find UUID, might be a burst photo
if self.burst and self._info["burstUUID"]:
bursts = photolib.fetch_burst_uuid(
self._info["burstUUID"], all=True
)
# PhotoKit UUIDs may contain "/L0/001" so only look at beginning
photo = [p for p in bursts if p.uuid.startswith(self.uuid)]
photo = photo[0] if photo else None
if photo:
if not dry_run:
try:
exported = photo.export(
dest.parent, dest.name, version=PHOTOS_VERSION_ORIGINAL
)
all_results.exported.extend(exported)
except Exception as e:
all_results.error.append(
(str(dest), f"{e} ({lineno(__file__)})")
)
else:
# dry_run, don't actually export
all_results.exported.append(str(dest))
else:
try:
exported = _export_photo_uuid_applescript(
self.uuid,
dest.parent,
filestem=filestem,
original=True,
edited=False,
live_photo=live_photo,
timeout=timeout,
burst=self.burst,
dry_run=dry_run,
)
all_results.exported.extend(exported)
except ExportError as e:
all_results.error.append((str(dest), f"{e} ({lineno(__file__)})"))
if all_results.exported:
if jpeg_ext:
# use_photos_export (both PhotoKit and AppleScript) don't use the
# file extension provided (instead they use extension for UTI)
# so if jpeg_ext is set, rename any non-conforming jpegs
all_results.exported = rename_jpeg_files(
all_results.exported, jpeg_ext, fileutil
)
if touch_file:
for exported_file in all_results.exported:
all_results.touched.append(exported_file)
ts = int(self.date.timestamp())
fileutil.utime(exported_file, (ts, ts))
if update:
all_results.new.extend(all_results.exported)
self._export_photo_with_photos_export(
dest,
filename,
all_results,
fileutil,
use_photokit=use_photokit,
dry_run=dry_run,
timeout=timeout,
jpeg_ext=jpeg_ext,
touch_file=touch_file,
update=update,
live_photo=live_photo,
edited=edited,
edited_identifier=edited_identifier,
)
# export metadata
sidecars = []
@ -1207,6 +1089,156 @@ def export2(
return all_results
def _export_photo_with_photos_export(
self,
dest,
filename,
all_results,
fileutil,
use_photokit=None,
dry_run=None,
timeout=None,
jpeg_ext=None,
touch_file=None,
update=None,
live_photo=None,
edited=None,
edited_identifier=None,
):
# e.g. _export_with_photos_export or such
# use_photo_export
# export live_photo .mov file?
live_photo = True if live_photo and self.live_photo else False
if edited or self.shared:
# exported edited version and not original
# shared photos (in shared albums) show up as not having adjustments (not edited)
# but Photos is unable to export the "original" as only a jpeg copy is shared in iCloud
# so tell Photos to export the current version in this case
if filename:
# use filename stem provided
filestem = dest.stem
else:
# didn't get passed a filename, add _edited
filestem = f"{dest.stem}{edited_identifier}"
uti = self.uti_edited if edited and self.uti_edited else self.uti
ext = get_preferred_uti_extension(uti)
dest = dest.parent / f"{filestem}{ext}"
if use_photokit:
photolib = PhotoLibrary()
photo = None
try:
photo = photolib.fetch_uuid(self.uuid)
except PhotoKitFetchFailed as e:
# if failed to find UUID, might be a burst photo
if self.burst and self._info["burstUUID"]:
bursts = photolib.fetch_burst_uuid(
self._info["burstUUID"], all=True
)
# PhotoKit UUIDs may contain "/L0/001" so only look at beginning
photo = [p for p in bursts if p.uuid.startswith(self.uuid)]
photo = photo[0] if photo else None
if not photo:
all_results.error.append(
(
str(dest),
f"PhotoKitFetchFailed exception exporting photo {self.uuid}: {e} ({lineno(__file__)})",
)
)
if photo:
if not dry_run:
try:
exported = photo.export(
dest.parent, dest.name, version=PHOTOS_VERSION_CURRENT
)
all_results.exported.extend(exported)
except Exception as e:
all_results.error.append(
(str(dest), f"{e} ({lineno(__file__)})")
)
else:
# dry_run, don't actually export
all_results.exported.append(str(dest))
else:
try:
exported = _export_photo_uuid_applescript(
self.uuid,
dest.parent,
filestem=filestem,
original=False,
edited=True,
live_photo=live_photo,
timeout=timeout,
burst=self.burst,
dry_run=dry_run,
)
all_results.exported.extend(exported)
except ExportError as e:
all_results.error.append((str(dest), f"{e} ({lineno(__file__)})"))
else:
# export original version and not edited
filestem = dest.stem
if use_photokit:
photolib = PhotoLibrary()
photo = None
try:
photo = photolib.fetch_uuid(self.uuid)
except PhotoKitFetchFailed:
# if failed to find UUID, might be a burst photo
if self.burst and self._info["burstUUID"]:
bursts = photolib.fetch_burst_uuid(
self._info["burstUUID"], all=True
)
# PhotoKit UUIDs may contain "/L0/001" so only look at beginning
photo = [p for p in bursts if p.uuid.startswith(self.uuid)]
photo = photo[0] if photo else None
if photo:
if not dry_run:
try:
exported = photo.export(
dest.parent, dest.name, version=PHOTOS_VERSION_ORIGINAL
)
all_results.exported.extend(exported)
except Exception as e:
all_results.error.append(
(str(dest), f"{e} ({lineno(__file__)})")
)
else:
# dry_run, don't actually export
all_results.exported.append(str(dest))
else:
try:
exported = _export_photo_uuid_applescript(
self.uuid,
dest.parent,
filestem=filestem,
original=True,
edited=False,
live_photo=live_photo,
timeout=timeout,
burst=self.burst,
dry_run=dry_run,
)
all_results.exported.extend(exported)
except ExportError as e:
all_results.error.append((str(dest), f"{e} ({lineno(__file__)})"))
if all_results.exported:
if jpeg_ext:
# use_photos_export (both PhotoKit and AppleScript) don't use the
# file extension provided (instead they use extension for UTI)
# so if jpeg_ext is set, rename any non-conforming jpegs
all_results.exported = rename_jpeg_files(
all_results.exported, jpeg_ext, fileutil
)
if touch_file:
for exported_file in all_results.exported:
all_results.touched.append(exported_file)
ts = int(self.date.timestamp())
fileutil.utime(exported_file, (ts, ts))
if update:
all_results.new.extend(all_results.exported)
def _export_photo(
self,
src,
@ -1501,7 +1533,7 @@ def _exiftool_dict(
QuickTime:GPSCoordinates
UserData:GPSCoordinates
Reference:
Reference:
https://iptc.org/std/photometadata/specification/IPTC-PhotoMetadata-201610_1.pdf
"""
@ -1682,7 +1714,7 @@ def _exiftool_dict(
def _get_exif_keywords(self):
""" returns list of keywords found in the file's exif metadata """
"""returns list of keywords found in the file's exif metadata"""
keywords = []
exif = self.exiftool
if exif:
@ -1700,7 +1732,7 @@ def _get_exif_keywords(self):
def _get_exif_persons(self):
""" returns list of persons found in the file's exif metadata """
"""returns list of persons found in the file's exif metadata"""
persons = []
exif = self.exiftool
if exif:

View File

@ -61,6 +61,7 @@ class PhotoInfo:
_export_photo,
_exiftool_dict,
_exiftool_json_sidecar,
_export_photo_with_photos_export,
_get_exif_keywords,
_get_exif_persons,
_write_exif_data,

View File

@ -10,9 +10,9 @@ import json
import osxphotos
UUID = [
"C8EAF50A-D891-4E0C-8086-C417E1284153",
"71DFB4C3-E868-4BE4-906E-D96BD8692D7E",
"2C151013-5BBA-4D00-B70F-1C9420418B86",
"DC09F4D8-6173-452D-AC15-725C8D7C185E",
"AFECD4AB-937C-46AF-A79B-9C9A38AA42B1",
"A1C36260-92CD-47E2-927A-35DAF16D7882",
]
data = {

File diff suppressed because one or more lines are too long

View File

@ -115,14 +115,14 @@ UUID_DICT = {
}
UUID_DICT_LOCAL = {
"not_visible": "ABF00253-78E7-4FD6-953B-709307CD489D",
"burst": "44AF1FCA-AC2D-4FA5-B288-E67DC18F9CA8",
"burst_key": "9F90DC00-AAAF-4A05-9A65-61FEEE0D67F2",
"burst_not_key": "38F8F30C-FF6D-49DA-8092-18497F1D6628",
"burst_selected": "38F8F30C-FF6D-49DA-8092-18497F1D6628",
"burst_not_selected": "A385FA13-DF8E-482F-A8C5-970EDDF54C2F",
"burst_default": "964F457D-5FFC-47B9-BEAD-56B0A83FEF63",
"burst_not_default": "A385FA13-DF8E-482F-A8C5-970EDDF54C2F",
"not_visible": "4A836160-51B2-4E32-907D-ECDDB2CEC657", # IMG_9815.JPG
"burst": "9A5B4CE6-6A9F-4917-95D4-1C98D14FCE4F", # IMG_9812.JPG
"burst_key": "9A5B4CE6-6A9F-4917-95D4-1C98D14FCE4F", # IMG_9812.JPG
"burst_not_key": "4A836160-51B2-4E32-907D-ECDDB2CEC657", # IMG_9815.JPG
"burst_selected": "75154738-83AA-4DCD-A913-632D5D1C0FEE", # IMG_9814.JPG
"burst_not_selected": "89E235DD-B9AC-4E8D-BDA2-986981CA7582", # IMG_9813.JPG
"burst_default": "F5E6BD24-B493-44E9-BDA2-7AD9D2CC8C9D", # IMG_9816.JPG
"burst_not_default": "75154738-83AA-4DCD-A913-632D5D1C0FEE", # IMG_9814.JPG
}
UUID_PUMPKIN_FARM = [

View File

@ -20,7 +20,7 @@ NAMES_DICT = {
"heic": "IMG_3092.jpeg"
}
UUID_LIVE_HEIC = "612CE30B-3D8F-417A-9B14-EC42CBA10ACC"
UUID_LIVE_HEIC = "8EC216A2-0032-4934-BD3F-04C6259B3304"
NAMES_LIVE_HEIC = [
"IMG_3259.jpeg",
"IMG_3259.mov"

View File

@ -24,35 +24,35 @@ pytestmark = pytest.mark.skipif(
UUID_DICT = {
"plain_photo": {
"uuid": "A8D646C3-89A9-4D74-8001-4EB46BA55B94",
"uuid": "C6C712C5-9316-408D-A3C3-125661422DA9",
"filename": "IMG_8844.JPG",
},
"hdr": {"uuid": "DA87C6FF-60E8-4DCB-A21D-9C57595667F1", "filename": "IMG_6162.JPG"},
"hdr": {"uuid": "DD641004-4E37-4233-AF31-CAA0896490B2", "filename": "IMG_6162.JPG"},
"selfie": {
"uuid": "316AEBE0-971D-4A33-833C-6BDBFF83469B",
"uuid": "C925CFDC-FF2B-4E71-AC9D-C669B6453A8B",
"filename": "IMG_1929.JPG",
},
"video": {
"uuid": "5814D9DE-FAB6-473A-9C9A-5A73C6DD1AF5",
"uuid": "F4430659-7B17-487E-8029-8C1ABEBE23DF",
"filename": "IMG_9411.TRIM.MOV",
},
"hasadjustments": {
"uuid": "2B2D5434-6D31-49E2-BF47-B973D34A317B",
"uuid": "2F252D2C-C9DE-4BE1-8610-9F968C634D3D",
"filename": "IMG_2860.JPG",
"adjusted_size": 3012634,
"unadjusted_size": 2580058,
},
"slow_mo": {
"uuid": "DAABC6D9-1FBA-4485-AA39-0A2B100300B1",
"uuid": "160447F8-4EB0-4FAE-A26A-3D32EA698F75",
"filename": "IMG_4055.MOV",
},
"live_photo": {
"uuid": "612CE30B-3D8F-417A-9B14-EC42CBA10ACC",
"uuid": "8EC216A2-0032-4934-BD3F-04C6259B3304",
"filename": "IMG_3259.HEIC",
"filename_video": "IMG_3259.mov",
},
"burst": {
"uuid": "CD97EC84-71F0-40C6-BAC1-2BABEE305CAC",
"uuid": "CDE4E5D9-1428-41E6-8569-EC0C45FD8E5A",
"filename": "IMG_8196.JPG",
"burst_selected": 4,
"burst_all": 5,
@ -61,7 +61,7 @@ UUID_DICT = {
def test_fetch_uuid():
""" test fetch_uuid """
"""test fetch_uuid"""
uuid = UUID_DICT["plain_photo"]["uuid"]
filename = UUID_DICT["plain_photo"]["filename"]
@ -71,7 +71,7 @@ def test_fetch_uuid():
def test_plain_photo():
""" test plain_photo """
"""test plain_photo"""
uuid = UUID_DICT["plain_photo"]["uuid"]
filename = UUID_DICT["plain_photo"]["filename"]
@ -83,7 +83,7 @@ def test_plain_photo():
def test_hdr():
""" test hdr """
"""test hdr"""
uuid = UUID_DICT["hdr"]["uuid"]
filename = UUID_DICT["hdr"]["filename"]
@ -94,7 +94,7 @@ def test_hdr():
def test_burst():
""" test burst and burstid """
"""test burst and burstid"""
test_dict = UUID_DICT["burst"]
uuid = test_dict["uuid"]
filename = test_dict["filename"]
@ -106,7 +106,6 @@ def test_burst():
assert photo.burstid
# def test_selfie():
# """ test selfie """
# uuid = UUID_DICT["selfie"]["uuid"]
@ -119,7 +118,7 @@ def test_burst():
def test_video():
""" test ismovie """
"""test ismovie"""
uuid = UUID_DICT["video"]["uuid"]
filename = UUID_DICT["video"]["filename"]
@ -132,7 +131,7 @@ def test_video():
def test_slow_mo():
""" test slow_mo """
"""test slow_mo"""
test_dict = UUID_DICT["slow_mo"]
uuid = test_dict["uuid"]
filename = test_dict["filename"]
@ -150,7 +149,7 @@ def test_slow_mo():
def test_export_photo_original():
""" test PhotoAsset.export """
"""test PhotoAsset.export"""
test_dict = UUID_DICT["hasadjustments"]
uuid = test_dict["uuid"]
lib = PhotoLibrary()
@ -166,7 +165,7 @@ def test_export_photo_original():
def test_export_photo_unadjusted():
""" test PhotoAsset.export """
"""test PhotoAsset.export"""
test_dict = UUID_DICT["hasadjustments"]
uuid = test_dict["uuid"]
lib = PhotoLibrary()
@ -182,7 +181,7 @@ def test_export_photo_unadjusted():
def test_export_photo_current():
""" test PhotoAsset.export """
"""test PhotoAsset.export"""
test_dict = UUID_DICT["hasadjustments"]
uuid = test_dict["uuid"]
lib = PhotoLibrary()
@ -201,7 +200,7 @@ def test_export_photo_current():
def test_export_video_original():
""" test VideoAsset.export """
"""test VideoAsset.export"""
test_dict = UUID_DICT["video"]
uuid = test_dict["uuid"]
lib = PhotoLibrary()
@ -216,7 +215,7 @@ def test_export_video_original():
def test_export_video_unadjusted():
""" test VideoAsset.export """
"""test VideoAsset.export"""
test_dict = UUID_DICT["video"]
uuid = test_dict["uuid"]
lib = PhotoLibrary()
@ -231,7 +230,7 @@ def test_export_video_unadjusted():
def test_export_video_current():
""" test VideoAsset.export """
"""test VideoAsset.export"""
test_dict = UUID_DICT["video"]
uuid = test_dict["uuid"]
lib = PhotoLibrary()
@ -249,7 +248,7 @@ def test_export_video_current():
def test_export_slow_mo_original():
""" test VideoAsset.export for slow mo video"""
"""test VideoAsset.export for slow mo video"""
test_dict = UUID_DICT["slow_mo"]
uuid = test_dict["uuid"]
lib = PhotoLibrary()
@ -264,7 +263,7 @@ def test_export_slow_mo_original():
def test_export_slow_mo_unadjusted():
""" test VideoAsset.export for slow mo video"""
"""test VideoAsset.export for slow mo video"""
test_dict = UUID_DICT["slow_mo"]
uuid = test_dict["uuid"]
lib = PhotoLibrary()
@ -279,7 +278,7 @@ def test_export_slow_mo_unadjusted():
def test_export_slow_mo_current():
""" test VideoAsset.export for slow mo video"""
"""test VideoAsset.export for slow mo video"""
test_dict = UUID_DICT["slow_mo"]
uuid = test_dict["uuid"]
lib = PhotoLibrary()
@ -297,7 +296,7 @@ def test_export_slow_mo_current():
def test_export_live_original():
""" test LivePhotoAsset.export """
"""test LivePhotoAsset.export"""
test_dict = UUID_DICT["live_photo"]
uuid = test_dict["uuid"]
lib = PhotoLibrary()
@ -313,7 +312,7 @@ def test_export_live_original():
def test_export_live_unadjusted():
""" test LivePhotoAsset.export """
"""test LivePhotoAsset.export"""
test_dict = UUID_DICT["live_photo"]
uuid = test_dict["uuid"]
lib = PhotoLibrary()
@ -329,7 +328,7 @@ def test_export_live_unadjusted():
def test_export_live_current():
""" test LivePhotAsset.export """
"""test LivePhotAsset.export"""
test_dict = UUID_DICT["live_photo"]
uuid = test_dict["uuid"]
lib = PhotoLibrary()
@ -345,7 +344,7 @@ def test_export_live_current():
def test_export_live_current_just_photo():
""" test LivePhotAsset.export """
"""test LivePhotAsset.export"""
test_dict = UUID_DICT["live_photo"]
uuid = test_dict["uuid"]
lib = PhotoLibrary()
@ -358,7 +357,7 @@ def test_export_live_current_just_photo():
def test_export_live_current_just_video():
""" test LivePhotAsset.export """
"""test LivePhotAsset.export"""
test_dict = UUID_DICT["live_photo"]
uuid = test_dict["uuid"]
lib = PhotoLibrary()
@ -371,7 +370,7 @@ def test_export_live_current_just_video():
def test_fetch_burst_uuid():
""" test fetch_burst_uuid """
"""test fetch_burst_uuid"""
test_dict = UUID_DICT["burst"]
uuid = test_dict["uuid"]
filename = test_dict["filename"]