Initial support for RAW photos in Photos 4 to address issue #101
This commit is contained in:
@@ -855,8 +855,14 @@ def query(
|
|||||||
|
|
||||||
@cli.command(cls=ExportCommand)
|
@cli.command(cls=ExportCommand)
|
||||||
@DB_OPTION
|
@DB_OPTION
|
||||||
@query_options
|
# @click.option(
|
||||||
|
# "--all",
|
||||||
|
# is_flag=True,
|
||||||
|
# help="Export all versions of photos including "
|
||||||
|
# "edited photos, live photos, burst photos, and RAW photos.",
|
||||||
|
# )
|
||||||
@click.option("--verbose", "-V", is_flag=True, help="Print verbose output.")
|
@click.option("--verbose", "-V", is_flag=True, help="Print verbose output.")
|
||||||
|
@query_options
|
||||||
@click.option(
|
@click.option(
|
||||||
"--overwrite",
|
"--overwrite",
|
||||||
is_flag=True,
|
is_flag=True,
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
""" version info """
|
""" version info """
|
||||||
|
|
||||||
__version__ = "0.27.6"
|
__version__ = "0.27.7"
|
||||||
|
|||||||
@@ -253,10 +253,6 @@ class PhotoInfo:
|
|||||||
# TODO: I don't like this -- would prefer a more deterministic approach but until I have more
|
# TODO: I don't like this -- would prefer a more deterministic approach but until I have more
|
||||||
# data on how Photos stores and retrieves RAW images, this seems to be working
|
# data on how Photos stores and retrieves RAW images, this seems to be working
|
||||||
|
|
||||||
if self._db._db_version < _PHOTOS_5_VERSION:
|
|
||||||
logging.warning("RAW support not yet implemented for Photos version < 5")
|
|
||||||
return None
|
|
||||||
|
|
||||||
if self._info["isMissing"] == 1:
|
if self._info["isMissing"] == 1:
|
||||||
return None # path would be meaningless until downloaded
|
return None # path would be meaningless until downloaded
|
||||||
|
|
||||||
@@ -273,26 +269,45 @@ class PhotoInfo:
|
|||||||
# )
|
# )
|
||||||
# return photopath
|
# return photopath
|
||||||
|
|
||||||
filestem = pathlib.Path(self._info["filename"]).stem
|
if self._db._db_version < _PHOTOS_5_VERSION:
|
||||||
raw_ext = get_preferred_uti_extension(self._info["UTI_raw"])
|
vol = self._info["raw_info"]["volume"]
|
||||||
|
if vol is not None:
|
||||||
if self._info["directory"].startswith("/"):
|
photopath = os.path.join(
|
||||||
filepath = self._info["directory"]
|
"/Volumes", vol, self._info["raw_info"]["imagePath"]
|
||||||
else:
|
)
|
||||||
filepath = os.path.join(self._db._masters_path, self._info["directory"])
|
else:
|
||||||
|
photopath = os.path.join(
|
||||||
glob_str = f"{filestem}*.{raw_ext}"
|
self._db._masters_path, self._info["raw_info"]["imagePath"]
|
||||||
raw_file = findfiles(glob_str, filepath)
|
)
|
||||||
if len(raw_file) != 1:
|
|
||||||
logging.warning(f"Error getting path to RAW file: {filepath}/{glob_str}")
|
|
||||||
photopath = None
|
|
||||||
else:
|
|
||||||
photopath = os.path.join(filepath, raw_file[0])
|
|
||||||
if not os.path.isfile(photopath):
|
if not os.path.isfile(photopath):
|
||||||
logging.debug(
|
logging.debug(
|
||||||
f"MISSING PATH: RAW photo for UUID {self._uuid} should be at {photopath} but does not appear to exist"
|
f"MISSING PATH: RAW photo for UUID {self._uuid} should be at {photopath} but does not appear to exist"
|
||||||
)
|
)
|
||||||
photopath = None
|
photopath = None
|
||||||
|
else:
|
||||||
|
filestem = pathlib.Path(self._info["filename"]).stem
|
||||||
|
raw_ext = get_preferred_uti_extension(self._info["UTI_raw"])
|
||||||
|
|
||||||
|
if self._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_file = findfiles(glob_str, filepath)
|
||||||
|
if len(raw_file) != 1:
|
||||||
|
logging.warning(
|
||||||
|
f"Error getting path to RAW file: {filepath}/{glob_str}"
|
||||||
|
)
|
||||||
|
photopath = None
|
||||||
|
else:
|
||||||
|
photopath = os.path.join(filepath, raw_file[0])
|
||||||
|
if not os.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
|
||||||
|
|
||||||
return photopath
|
return photopath
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -588,10 +603,6 @@ class PhotoInfo:
|
|||||||
@property
|
@property
|
||||||
def has_raw(self):
|
def has_raw(self):
|
||||||
""" returns True if photo has an associated RAW image, otherwise False """
|
""" returns True if photo has an associated RAW image, otherwise False """
|
||||||
if self._db._db_version < _PHOTOS_5_VERSION:
|
|
||||||
logging.warning("RAW support not yet implemented for Photos version < 5")
|
|
||||||
return None
|
|
||||||
|
|
||||||
return self._info["has_raw"]
|
return self._info["has_raw"]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -99,6 +99,11 @@ class PhotosDB:
|
|||||||
# e.g. {'BD94B7C0-2EB8-43DB-98B4-3B8E9653C255': {'8B386814-CA8A-42AA-BCA8-97C1AA746D8A', '52B95550-DE4A-44DD-9E67-89E979F2E97F'}}
|
# e.g. {'BD94B7C0-2EB8-43DB-98B4-3B8E9653C255': {'8B386814-CA8A-42AA-BCA8-97C1AA746D8A', '52B95550-DE4A-44DD-9E67-89E979F2E97F'}}
|
||||||
self._dbphotos_burst = {}
|
self._dbphotos_burst = {}
|
||||||
|
|
||||||
|
# Dict with additional information from RKMaster
|
||||||
|
# key is UUID from RKMaster, value is dict with info related to each master
|
||||||
|
# currently used to get information on RAW images
|
||||||
|
self._dbphotos_master = {}
|
||||||
|
|
||||||
# Dict with information about all persons/photos by uuid
|
# Dict with information about all persons/photos by uuid
|
||||||
# key is photo UUID, value is list of face names in that photo
|
# key is photo UUID, value is list of face names in that photo
|
||||||
# Note: Photos 5 identifies faces even if not given a name
|
# Note: Photos 5 identifies faces even if not given a name
|
||||||
@@ -610,7 +615,10 @@ class PhotosDB:
|
|||||||
RKVersion.latitude, RKVersion.longitude,
|
RKVersion.latitude, RKVersion.longitude,
|
||||||
RKVersion.adjustmentUuid, RKVersion.type, RKMaster.UTI,
|
RKVersion.adjustmentUuid, RKVersion.type, RKMaster.UTI,
|
||||||
RKVersion.burstUuid, RKVersion.burstPickType,
|
RKVersion.burstUuid, RKVersion.burstPickType,
|
||||||
RKVersion.specialType, RKMaster.modelID, RKVersion.momentUuid
|
RKVersion.specialType, RKMaster.modelID, null, RKVersion.momentUuid,
|
||||||
|
RKVersion.rawMasterUuid,
|
||||||
|
RKVersion.nonRawMasterUuid,
|
||||||
|
RKMaster.alternateMasterUuid
|
||||||
FROM RKVersion, RKMaster WHERE RKVersion.isInTrash = 0 AND
|
FROM RKVersion, RKMaster WHERE RKVersion.isInTrash = 0 AND
|
||||||
RKVersion.masterUuid = RKMaster.uuid AND RKVersion.filename NOT LIKE '%.pdf' """
|
RKVersion.masterUuid = RKMaster.uuid AND RKVersion.filename NOT LIKE '%.pdf' """
|
||||||
)
|
)
|
||||||
@@ -626,7 +634,10 @@ class PhotosDB:
|
|||||||
RKVersion.burstUuid, RKVersion.burstPickType,
|
RKVersion.burstUuid, RKVersion.burstPickType,
|
||||||
RKVersion.specialType, RKMaster.modelID,
|
RKVersion.specialType, RKMaster.modelID,
|
||||||
RKVersion.selfPortrait,
|
RKVersion.selfPortrait,
|
||||||
RKVersion.momentUuid
|
RKVersion.momentUuid,
|
||||||
|
RKVersion.rawMasterUuid,
|
||||||
|
RKVersion.nonRawMasterUuid,
|
||||||
|
RKMaster.alternateMasterUuid
|
||||||
FROM RKVersion, RKMaster WHERE RKVersion.isInTrash = 0 AND
|
FROM RKVersion, RKMaster WHERE RKVersion.isInTrash = 0 AND
|
||||||
RKVersion.masterUuid = RKMaster.uuid AND RKVersion.filename NOT LIKE '%.pdf' """
|
RKVersion.masterUuid = RKMaster.uuid AND RKVersion.filename NOT LIKE '%.pdf' """
|
||||||
)
|
)
|
||||||
@@ -661,6 +672,9 @@ class PhotosDB:
|
|||||||
# 26 RKMaster.modelID
|
# 26 RKMaster.modelID
|
||||||
# 27 RKVersion.selfPortrait -- 1 if selfie, Photos >= 3, not present for Photos < 3
|
# 27 RKVersion.selfPortrait -- 1 if selfie, Photos >= 3, not present for Photos < 3
|
||||||
# 28 RKVersion.momentID (# 27 for Photos < 3)
|
# 28 RKVersion.momentID (# 27 for Photos < 3)
|
||||||
|
# 29 RKVersion.rawMasterUuid, -- UUID of RAW master
|
||||||
|
# 30 RKVersion.nonRawMasterUuid, -- UUID of non-RAW master
|
||||||
|
# 31 RKMaster.alternateMasterUuid -- UUID of alternate master (will be RAW master for JPEG and JPEG master for RAW)
|
||||||
|
|
||||||
for row in c:
|
for row in c:
|
||||||
uuid = row[0]
|
uuid = row[0]
|
||||||
@@ -746,6 +760,7 @@ class PhotosDB:
|
|||||||
# 4 == HDR
|
# 4 == HDR
|
||||||
# 5 == live photo
|
# 5 == live photo
|
||||||
# 6 == screenshot
|
# 6 == screenshot
|
||||||
|
# 7 == JPEG/RAW pair
|
||||||
# 8 == HDR live photo
|
# 8 == HDR live photo
|
||||||
# 9 = portrait
|
# 9 = portrait
|
||||||
|
|
||||||
@@ -765,12 +780,12 @@ class PhotosDB:
|
|||||||
self._dbphotos[uuid]["portrait"] = True if row[25] == 9 else False
|
self._dbphotos[uuid]["portrait"] = True if row[25] == 9 else False
|
||||||
|
|
||||||
# selfies (front facing camera, RKVersion.selfPortrait == 1)
|
# selfies (front facing camera, RKVersion.selfPortrait == 1)
|
||||||
if self._db_version >= _PHOTOS_3_VERSION:
|
if row[27] is not None:
|
||||||
self._dbphotos[uuid]["selfie"] = True if row[27] == 1 else False
|
self._dbphotos[uuid]["selfie"] = True if row[27] == 1 else False
|
||||||
self._dbphotos[uuid]["momentID"] = row[28]
|
|
||||||
else:
|
else:
|
||||||
self._dbphotos[uuid]["selfie"] = None
|
self._dbphotos[uuid]["selfie"] = None
|
||||||
self._dbphotos[uuid]["momentID"] = row[27]
|
|
||||||
|
self._dbphotos[uuid]["momentID"] = row[28]
|
||||||
|
|
||||||
# Init cloud details that will be filled in later if cloud asset
|
# Init cloud details that will be filled in later if cloud asset
|
||||||
self._dbphotos[uuid]["cloudAssetGUID"] = None # Photos 5
|
self._dbphotos[uuid]["cloudAssetGUID"] = None # Photos 5
|
||||||
@@ -785,12 +800,59 @@ class PhotosDB:
|
|||||||
self._dbphotos[uuid]["original_resource_choice"] = None
|
self._dbphotos[uuid]["original_resource_choice"] = None
|
||||||
|
|
||||||
# associated RAW image info
|
# associated RAW image info
|
||||||
# will be filled in later
|
self._dbphotos[uuid]["has_raw"] = True if row[25] == 7 else False
|
||||||
self._dbphotos[uuid]["has_raw"] = None
|
|
||||||
self._dbphotos[uuid]["UTI_raw"] = None
|
self._dbphotos[uuid]["UTI_raw"] = None
|
||||||
self._dbphotos[uuid]["raw_data_length"] = None
|
self._dbphotos[uuid]["raw_data_length"] = None
|
||||||
self._dbphotos[uuid]["resource_type"] = None
|
self._dbphotos[uuid]["raw_info"] = None
|
||||||
self._dbphotos[uuid]["datastore_subtype"] = None
|
self._dbphotos[uuid]["resource_type"] = None # Photos 5
|
||||||
|
self._dbphotos[uuid]["datastore_subtype"] = None # Photos 5
|
||||||
|
self._dbphotos[uuid]["raw_master_uuid"] = row[29]
|
||||||
|
self._dbphotos[uuid]["non_raw_master_uuid"] = row[30]
|
||||||
|
self._dbphotos[uuid]["alt_master_uuid"] = row[31]
|
||||||
|
|
||||||
|
# get additional details from RKMaster, needed for RAW processing
|
||||||
|
c.execute(
|
||||||
|
""" SELECT
|
||||||
|
RKMaster.uuid,
|
||||||
|
RKMaster.volumeId,
|
||||||
|
RKMaster.imagePath,
|
||||||
|
RKMaster.isMissing,
|
||||||
|
RKMaster.originalFileName,
|
||||||
|
RKMaster.UTI,
|
||||||
|
RKMaster.modelID,
|
||||||
|
RKMaster.fileSize,
|
||||||
|
RKMaster.isTrulyRaw,
|
||||||
|
RKMaster.alternateMasterUuid
|
||||||
|
FROM RKMaster
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
# Order of results:
|
||||||
|
# 0 RKMaster.uuid,
|
||||||
|
# 1 RKMaster.volumeId,
|
||||||
|
# 2 RKMaster.imagePath,
|
||||||
|
# 3 RKMaster.isMissing,
|
||||||
|
# 4 RKMaster.originalFileName,
|
||||||
|
# 5 RKMaster.UTI,
|
||||||
|
# 6 RKMaster.modelID,
|
||||||
|
# 7 RKMaster.fileSize,
|
||||||
|
# 8 RKMaster.isTrulyRaw,
|
||||||
|
# 9 RKMaster.alternateMasterUuid
|
||||||
|
|
||||||
|
for row in c:
|
||||||
|
uuid = row[0]
|
||||||
|
info = {}
|
||||||
|
info["_uuid"] = uuid
|
||||||
|
info["volumeId"] = row[1]
|
||||||
|
info["imagePath"] = row[2]
|
||||||
|
info["isMissing"] = row[3]
|
||||||
|
info["originalFilename"] = row[4]
|
||||||
|
info["UTI"] = row[5]
|
||||||
|
info["modelID"] = row[6]
|
||||||
|
info["fileSize"] = row[7]
|
||||||
|
info["isTrulyRAW"] = row[8]
|
||||||
|
info["alternateMasterUuid"] = row[9]
|
||||||
|
self._dbphotos_master[uuid] = info
|
||||||
|
|
||||||
# get details needed to find path of the edited photos
|
# get details needed to find path of the edited photos
|
||||||
c.execute(
|
c.execute(
|
||||||
@@ -979,6 +1041,20 @@ class PhotosDB:
|
|||||||
else:
|
else:
|
||||||
self._dbalbum_titles[title] = [album_id]
|
self._dbalbum_titles[title] = [album_id]
|
||||||
|
|
||||||
|
# add volume name to _dbphotos_master
|
||||||
|
for info in self._dbphotos_master.values():
|
||||||
|
info["volume"] = (
|
||||||
|
self._dbvolumes[info["volumeId"]]
|
||||||
|
if info["volumeId"] is not None
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
# add data on RAW images
|
||||||
|
for info in self._dbphotos.values():
|
||||||
|
if info["has_raw"]:
|
||||||
|
raw_uuid = info["raw_master_uuid"]
|
||||||
|
info["raw_info"] = self._dbphotos_master[raw_uuid]
|
||||||
|
|
||||||
# done with the database connection
|
# done with the database connection
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
@@ -1423,6 +1499,10 @@ class PhotosDB:
|
|||||||
info["UTI_raw"] = None
|
info["UTI_raw"] = None
|
||||||
info["datastore_subtype"] = None
|
info["datastore_subtype"] = None
|
||||||
info["resource_type"] = None
|
info["resource_type"] = None
|
||||||
|
info["raw_master_uuid"] = None # Photos 4
|
||||||
|
info["non_raw_master_uuid"] = None # Photos 4
|
||||||
|
info["alt_master_uuid"] = None # Photos 4
|
||||||
|
info["raw_info"] = None # Photos 4
|
||||||
|
|
||||||
self._dbphotos[uuid] = info
|
self._dbphotos[uuid] = info
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user