diff --git a/osxphotos/__main__.py b/osxphotos/__main__.py index e05a3771..62c04f21 100644 --- a/osxphotos/__main__.py +++ b/osxphotos/__main__.py @@ -855,8 +855,14 @@ def query( @cli.command(cls=ExportCommand) @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.") +@query_options @click.option( "--overwrite", is_flag=True, diff --git a/osxphotos/_version.py b/osxphotos/_version.py index fc0d502f..ad8d2558 100644 --- a/osxphotos/_version.py +++ b/osxphotos/_version.py @@ -1,3 +1,3 @@ """ version info """ -__version__ = "0.27.6" +__version__ = "0.27.7" diff --git a/osxphotos/photoinfo.py b/osxphotos/photoinfo.py index 10101a11..30b02005 100644 --- a/osxphotos/photoinfo.py +++ b/osxphotos/photoinfo.py @@ -253,10 +253,6 @@ class PhotoInfo: # 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 - 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: return None # path would be meaningless until downloaded @@ -273,26 +269,45 @@ class PhotoInfo: # ) # return photopath - 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 self._db._db_version < _PHOTOS_5_VERSION: + vol = self._info["raw_info"]["volume"] + if vol is not None: + 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"] + ) 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 + 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 @property @@ -588,10 +603,6 @@ class PhotoInfo: @property def has_raw(self): """ 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"] @property diff --git a/osxphotos/photosdb.py b/osxphotos/photosdb.py index 9694b57d..cf87d6d8 100644 --- a/osxphotos/photosdb.py +++ b/osxphotos/photosdb.py @@ -99,6 +99,11 @@ class PhotosDB: # e.g. {'BD94B7C0-2EB8-43DB-98B4-3B8E9653C255': {'8B386814-CA8A-42AA-BCA8-97C1AA746D8A', '52B95550-DE4A-44DD-9E67-89E979F2E97F'}} 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 # key is photo UUID, value is list of face names in that photo # Note: Photos 5 identifies faces even if not given a name @@ -610,7 +615,10 @@ class PhotosDB: RKVersion.latitude, RKVersion.longitude, RKVersion.adjustmentUuid, RKVersion.type, RKMaster.UTI, 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 RKVersion.masterUuid = RKMaster.uuid AND RKVersion.filename NOT LIKE '%.pdf' """ ) @@ -625,8 +633,11 @@ class PhotosDB: RKVersion.adjustmentUuid, RKVersion.type, RKMaster.UTI, RKVersion.burstUuid, RKVersion.burstPickType, RKVersion.specialType, RKMaster.modelID, - RKVersion.selfPortrait, - RKVersion.momentUuid + RKVersion.selfPortrait, + RKVersion.momentUuid, + RKVersion.rawMasterUuid, + RKVersion.nonRawMasterUuid, + RKMaster.alternateMasterUuid FROM RKVersion, RKMaster WHERE RKVersion.isInTrash = 0 AND RKVersion.masterUuid = RKMaster.uuid AND RKVersion.filename NOT LIKE '%.pdf' """ ) @@ -661,6 +672,9 @@ class PhotosDB: # 26 RKMaster.modelID # 27 RKVersion.selfPortrait -- 1 if selfie, Photos >= 3, not present 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: uuid = row[0] @@ -746,6 +760,7 @@ class PhotosDB: # 4 == HDR # 5 == live photo # 6 == screenshot + # 7 == JPEG/RAW pair # 8 == HDR live photo # 9 = portrait @@ -765,12 +780,12 @@ class PhotosDB: self._dbphotos[uuid]["portrait"] = True if row[25] == 9 else False # 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]["momentID"] = row[28] else: 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 self._dbphotos[uuid]["cloudAssetGUID"] = None # Photos 5 @@ -785,12 +800,59 @@ class PhotosDB: self._dbphotos[uuid]["original_resource_choice"] = None # associated RAW image info - # will be filled in later - self._dbphotos[uuid]["has_raw"] = None + self._dbphotos[uuid]["has_raw"] = True if row[25] == 7 else False self._dbphotos[uuid]["UTI_raw"] = None self._dbphotos[uuid]["raw_data_length"] = None - self._dbphotos[uuid]["resource_type"] = None - self._dbphotos[uuid]["datastore_subtype"] = None + self._dbphotos[uuid]["raw_info"] = 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 c.execute( @@ -979,6 +1041,20 @@ class PhotosDB: else: 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 conn.close() @@ -1423,6 +1499,10 @@ class PhotosDB: info["UTI_raw"] = None info["datastore_subtype"] = 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