Added live photo support for both Photos 4 & 5
This commit is contained in:
16
README.md
16
README.md
@@ -50,6 +50,8 @@
|
||||
- [`uti`](#uti)
|
||||
- [`burst`](#burst)
|
||||
- [`burst_photos`](#burst_photos)
|
||||
- [`live_photo`](#live_photo)
|
||||
- [`path_live_photo`](#path_live_photo)
|
||||
- [`json()`](#json)
|
||||
- [`export(dest, *filename, edited=False, overwrite=False, increment=True, sidecar=False)`](#exportdest-filename-editedfalse-overwritefalse-incrementtrue-sidecarfalse)
|
||||
+ [Utility Functions](#utility-functions)
|
||||
@@ -522,13 +524,15 @@ Returns a list of albums the photo is contained in
|
||||
Returns a list of the names of the persons in the photo
|
||||
|
||||
#### `path`
|
||||
Returns the absolute path to the photo on disk as a string. **Note**: this returns the path to the *original* unedited file (see [hasadjustments](#hasadjustments)). If the file is missing on disk, path=`None` (see [ismissing](#ismissing))
|
||||
Returns the absolute path to the photo on disk as a string. **Note**: this returns the path to the *original* unedited file (see [hasadjustments](#hasadjustments)). If the file is missing on disk, path=`None` (see [ismissing](#ismissing)).
|
||||
|
||||
#### `path_edited`
|
||||
Returns the absolute path to the edited photo on disk as a string. If the photo has not been edited, returns `None`. See also [path](#path) and [hasadjustments](#hasadjustments).
|
||||
|
||||
**Note**: will also return None if the edited photo is missing on disk.
|
||||
|
||||
#### `ismissing`
|
||||
Returns `True` if the original image file is missing on disk, otherwise `False`. This can occur if the file has been uploaded to iCloud but not yet downloaded to the local library or if the file was deleted or imported from a disk that has been unmounted. **Note**: this status is set by Photos and osxphotos does not verify that the file path returned by `path` actually exists. It merely reports what Photos has stored in the library database.
|
||||
Returns `True` if the original image file is missing on disk, otherwise `False`. This can occur if the file has been uploaded to iCloud but not yet downloaded to the local library or if the file was deleted or imported from a disk that has been unmounted and user hasn't enabled "Copy items to the Photos library" in Photos preferences. **Note**: this status is computed based on data in the Photos library and `ismissing` does not verify if the photo is actually missing. See also [path](#path).
|
||||
|
||||
#### `hasadjustments`
|
||||
Returns `True` if the picture has been edited, otherwise `False`
|
||||
@@ -586,6 +590,14 @@ IMG_9854.JPG
|
||||
IMG_9855.JPG
|
||||
```
|
||||
|
||||
#### `live_photo`
|
||||
Returns True if photo is an Apple live photo (ie. it has an associated "live" video component), otherwise returns False. See [path_live_photo](#path_live_photo).
|
||||
|
||||
#### `path_live_photo`
|
||||
Returns the path to the live video component of a [live photo](#live_photo). If photo is not a live photo, returns None.
|
||||
|
||||
**Note**: will also return None if the live video component is missing on disk. It's possible that the original photo may be on disk ([ismissing](#ismissing)==False) but the video component is missing, likely because it has not been downloaded from iCloud.
|
||||
|
||||
#### `json()`
|
||||
Returns a JSON representation of all photo info
|
||||
|
||||
|
||||
@@ -115,6 +115,9 @@ class PhotoInfo:
|
||||
""" absolute path on disk of the edited picture """
|
||||
""" None if photo has not been edited """
|
||||
|
||||
# TODO: break this code into a _path_edited_4 and _path_edited_5
|
||||
# version to simplify the big if/then; same for path_live_photo
|
||||
|
||||
photopath = None
|
||||
|
||||
if self._db._db_version < _PHOTOS_5_VERSION:
|
||||
@@ -148,13 +151,14 @@ class PhotoInfo:
|
||||
)
|
||||
if not os.path.isfile(photopath):
|
||||
logging.warning(
|
||||
f"edited file for UUID {self._uuid} should be at {photopath} but does not appear to exist"
|
||||
f"MISSING PATH: edited file for UUID {self._uuid} should be at {photopath} but does not appear to exist"
|
||||
)
|
||||
photopath = None
|
||||
else:
|
||||
logging.warning(
|
||||
f"{self.uuid} hasAdjustments but edit_resource_id is None"
|
||||
)
|
||||
photopath = None
|
||||
else:
|
||||
photopath = None
|
||||
|
||||
@@ -231,7 +235,6 @@ class PhotoInfo:
|
||||
@property
|
||||
def title(self):
|
||||
""" name / title of picture """
|
||||
# TODO: Update documentation and tests to use title
|
||||
return self._info["name"]
|
||||
|
||||
@property
|
||||
@@ -333,11 +336,7 @@ class PhotoInfo:
|
||||
@property
|
||||
def live_photo(self):
|
||||
""" Returns True if photo is a live photo, otherwise False """
|
||||
# TODO: fixme for Photos 4
|
||||
if self._db._db_version >= _PHOTOS_5_VERSION:
|
||||
return self._info["live_photo"]
|
||||
else:
|
||||
return None
|
||||
return self._info["live_photo"]
|
||||
|
||||
@property
|
||||
def path_live_photo(self):
|
||||
@@ -346,20 +345,49 @@ class PhotoInfo:
|
||||
If photo is missing, returns None """
|
||||
|
||||
photopath = None
|
||||
# TODO: fixme for Photos 4
|
||||
if self._db._db_version < _PHOTOS_5_VERSION:
|
||||
photopath = None
|
||||
if self.live_photo and not self.ismissing:
|
||||
live_model_id = self._info["live_model_id"]
|
||||
if live_model_id == None:
|
||||
logging.debug(f"missing live_model_id: {self._uuid}")
|
||||
photopath = None
|
||||
else:
|
||||
folder_id, file_id = _get_resource_loc(live_model_id)
|
||||
library_path = self._db.library_path
|
||||
photopath = os.path.join(
|
||||
library_path,
|
||||
"resources",
|
||||
"media",
|
||||
"master",
|
||||
folder_id,
|
||||
"00",
|
||||
f"jpegvideocomplement_{file_id}.mov",
|
||||
)
|
||||
if not os.path.isfile(photopath):
|
||||
# In testing, I've seen occasional missing movie for live photo
|
||||
# These appear to be valid -- e.g. live component hasn't been downloaded from iCloud
|
||||
# photos 4 has "isOnDisk" column we could check
|
||||
# or could do the actual check with "isfile"
|
||||
# TODO: should this be a warning or debug?
|
||||
logging.debug(
|
||||
f"MISSING PATH: live photo path for UUID {self._uuid} should be at {photopath} but does not appear to exist"
|
||||
)
|
||||
photopath = None
|
||||
else:
|
||||
photopath = None
|
||||
else:
|
||||
# Photos 5
|
||||
if self.live_photo and not self.ismissing:
|
||||
filename = pathlib.Path(self.path)
|
||||
photopath = filename.parent.joinpath(f"{filename.stem}_3.mov")
|
||||
if not os.path.isfile(photopath):
|
||||
photopath = None
|
||||
# In testing, I've seen occasional missing movie for live photo
|
||||
# these appear to be valid -- e.g. video component not yet downloaded from iCloud
|
||||
# TODO: should this be a warning or debug?
|
||||
logging.debug(
|
||||
f"live photo path for UUID {self._uuid} should be at {photopath} but does not appear to exist"
|
||||
f"MISSING PATH: live photo path for UUID {self._uuid} should be at {photopath} but does not appear to exist"
|
||||
)
|
||||
photopath = None
|
||||
else:
|
||||
photopath = None
|
||||
|
||||
|
||||
@@ -484,9 +484,10 @@ class PhotosDB:
|
||||
RKMaster.isMissing, RKMaster.originalFileName, RKVersion.isFavorite, RKVersion.isHidden,
|
||||
RKVersion.latitude, RKVersion.longitude,
|
||||
RKVersion.adjustmentUuid, RKVersion.type, RKMaster.UTI,
|
||||
RKVersion.burstUuid, RKVersion.burstPickType
|
||||
from RKVersion, RKMaster where RKVersion.isInTrash = 0 and
|
||||
RKVersion.masterUuid = RKMaster.uuid and RKVersion.filename not like '%.pdf' """
|
||||
RKVersion.burstUuid, RKVersion.burstPickType,
|
||||
RKVersion.specialType, RKMaster.modelID
|
||||
FROM RKVersion, RKMaster WHERE RKVersion.isInTrash = 0 AND
|
||||
RKVersion.masterUuid = RKMaster.uuid AND RKVersion.filename NOT LIKE '%.pdf' """
|
||||
)
|
||||
|
||||
# order of results
|
||||
@@ -515,6 +516,8 @@ class PhotosDB:
|
||||
# 22 RKMaster.UTI
|
||||
# 23 RKVersion.burstUuid
|
||||
# 24 RKVersion.burstPickType
|
||||
# 25 RKVersion.specialType
|
||||
# 26 RKMaster.modelID
|
||||
|
||||
for row in c:
|
||||
uuid = row[0]
|
||||
@@ -579,26 +582,56 @@ class PhotosDB:
|
||||
self._dbphotos[uuid]["burst"] = True
|
||||
burst_uuid = row[23]
|
||||
if burst_uuid not in self._dbphotos_burst:
|
||||
self._dbphotos_burst[burst_uuid] = set()
|
||||
self._dbphotos_burst[burst_uuid] = set()
|
||||
self._dbphotos_burst[burst_uuid].add(uuid)
|
||||
if row[24] != 2 and row[24] != 4:
|
||||
self._dbphotos[uuid]["burst_key"] = True # it's a key photo (selected from the burst)
|
||||
self._dbphotos[uuid][
|
||||
"burst_key"
|
||||
] = True # it's a key photo (selected from the burst)
|
||||
else:
|
||||
self._dbphotos[uuid]["burst_key"] = False # it's a burst photo but not one that's selected
|
||||
self._dbphotos[uuid][
|
||||
"burst_key"
|
||||
] = False # it's a burst photo but not one that's selected
|
||||
else:
|
||||
# not a burst photo
|
||||
self._dbphotos[uuid]["burst"] = False
|
||||
self._dbphotos[uuid]["burst_key"] = None
|
||||
|
||||
# get details needed to find path of the edited photos and live photos
|
||||
# RKVersion.specialType
|
||||
# 1 == panorama
|
||||
# 2 == slow-mo movie
|
||||
# 3 == time-lapse movie
|
||||
# 4 == HDR
|
||||
# 5 == live photo
|
||||
# 6 == screenshot
|
||||
# 8 == HDR live photo
|
||||
# 9 = portrait
|
||||
|
||||
# get info on special types
|
||||
self._dbphotos[uuid]["specialType"] = row[25]
|
||||
self._dbphotos[uuid]["masterModelID"] = row[26]
|
||||
self._dbphotos[uuid]["panorama"] = True if row[25] == 1 else False
|
||||
self._dbphotos[uuid]["slow_mo"] = True if row[25] == 2 else False
|
||||
self._dbphotos[uuid]["time_lapse"] = True if row[25] == 3 else False
|
||||
self._dbphotos[uuid]["hdr"] = (
|
||||
True if (row[25] == 4 or row[25] == 8) else False
|
||||
)
|
||||
self._dbphotos[uuid]["live_photo"] = (
|
||||
True if (row[25] == 5 or row[25] == 8) else False
|
||||
)
|
||||
self._dbphotos[uuid]["screenshot"] = True if row[25] == 6 else False
|
||||
self._dbphotos[uuid]["portrait"] = True if row[25] == 9 else False
|
||||
|
||||
# get details needed to find path of the edited photos
|
||||
c.execute(
|
||||
"SELECT RKVersion.uuid, RKVersion.adjustmentUuid, RKModelResource.modelId, "
|
||||
"RKModelResource.resourceTag, RKModelResource.UTI, RKVersion.specialType, "
|
||||
"RKModelResource.attachedModelType, RKModelResource.resourceType "
|
||||
"FROM RKVersion "
|
||||
"JOIN RKModelResource on RKModelResource.attachedModelId = RKVersion.modelId "
|
||||
"WHERE RKVersion.isInTrash = 0 "
|
||||
""" SELECT RKVersion.uuid, RKVersion.adjustmentUuid, RKModelResource.modelId,
|
||||
RKModelResource.resourceTag, RKModelResource.UTI, RKVersion.specialType,
|
||||
RKModelResource.attachedModelType, RKModelResource.resourceType
|
||||
FROM RKVersion
|
||||
JOIN RKModelResource on RKModelResource.attachedModelId = RKVersion.modelId
|
||||
WHERE RKVersion.isInTrash = 0 """
|
||||
)
|
||||
# get info on path of live photo movie
|
||||
|
||||
# Order of results:
|
||||
# 0 RKVersion.uuid
|
||||
@@ -610,15 +643,10 @@ class PhotosDB:
|
||||
# 6 RKModelResource.attachedModelType
|
||||
# 7 RKModelResource.resourceType
|
||||
|
||||
# TODO: add live photos
|
||||
# attachedmodeltype is 2, it's a photo, could be more than one
|
||||
# attachedmodeltype == 2 could also be movie?
|
||||
# if 5, it's a facetile
|
||||
# specialtype = 0 == image, 5 or 8 == live photo movie
|
||||
|
||||
for row in c:
|
||||
uuid = row[0]
|
||||
if uuid in self._dbphotos:
|
||||
# get info on adjustments (edits)
|
||||
if self._dbphotos[uuid]["adjustmentUuid"] == row[3]:
|
||||
if (
|
||||
row[1] != "UNADJUSTEDNONRAW"
|
||||
@@ -654,10 +682,50 @@ class PhotosDB:
|
||||
if uuid in self._dbphotos:
|
||||
self._dbphotos[uuid]["adjustmentFormatID"] = row[3]
|
||||
|
||||
# init any uuids that had no edits
|
||||
|
||||
|
||||
# get details to find path of live photos
|
||||
c.execute(
|
||||
""" SELECT
|
||||
RKVersion.uuid,
|
||||
RKModelResource.modelId,
|
||||
RKModelResource.UTI,
|
||||
RKVersion.specialType,
|
||||
RKModelResource.attachedModelType,
|
||||
RKModelResource.resourceType,
|
||||
RKModelResource.isOnDisk
|
||||
FROM RKVersion
|
||||
INNER JOIN RKMaster on RKVersion.masterUuid = RKMaster.uuid
|
||||
INNER JOIN RKModelResource on RKMaster.modelId = RKModelResource.attachedModelId
|
||||
WHERE RKModelResource.UTI = 'com.apple.quicktime-movie'
|
||||
AND RKMaster.isInTrash = 0
|
||||
AND RKVersion.isInTrash = 0
|
||||
"""
|
||||
)
|
||||
|
||||
# Order of results
|
||||
# 0 RKVersion.uuid,
|
||||
# 1 RKModelResource.modelId,
|
||||
# 2 RKModelResource.UTI,
|
||||
# 3 RKVersion.specialType,
|
||||
# 4 RKModelResource.attachedModelType,
|
||||
# 5 RKModelResource.resourceType
|
||||
# 6 RKModelResource.isOnDisk
|
||||
|
||||
# TODO: don't think we need most of these fields, remove from SQL query?
|
||||
for row in c:
|
||||
uuid = row[0]
|
||||
if uuid in self._dbphotos:
|
||||
self._dbphotos[uuid]["live_model_id"] = row[1]
|
||||
self._dbphotos[uuid]["modeResourceIsOnDisk"] = True if row[6] == 1 else False
|
||||
|
||||
# init any uuids that had no edits or live photos
|
||||
for uuid in self._dbphotos:
|
||||
if "edit_resource_id" not in self._dbphotos[uuid]:
|
||||
self._dbphotos[uuid]["edit_resource_id"] = None
|
||||
if "live_model_id" not in self._dbphotos[uuid]:
|
||||
self._dbphotos[uuid]["live_model_id"] = None
|
||||
self._dbphotos[uuid]["modeResourceIsOnDisk"] = None
|
||||
|
||||
conn.close()
|
||||
|
||||
@@ -852,7 +920,8 @@ class PhotosDB:
|
||||
ZGENERICASSET.ZUNIFORMTYPEIDENTIFIER,
|
||||
ZGENERICASSET.ZAVALANCHEUUID,
|
||||
ZGENERICASSET.ZAVALANCHEPICKTYPE,
|
||||
ZGENERICASSET.ZKINDSUBTYPE
|
||||
ZGENERICASSET.ZKINDSUBTYPE,
|
||||
ZGENERICASSET.ZCUSTOMRENDEREDVALUE
|
||||
FROM ZGENERICASSET
|
||||
JOIN ZADDITIONALASSETATTRIBUTES ON ZADDITIONALASSETATTRIBUTES.ZASSET = ZGENERICASSET.Z_PK
|
||||
WHERE ZGENERICASSET.ZTRASHEDSTATE = 0
|
||||
@@ -880,8 +949,8 @@ class PhotosDB:
|
||||
# 18 ZUNIFORMTYPEIDENTIFIER -- UTI
|
||||
# 19 ZGENERICASSET.ZAVALANCHEUUID, -- if not NULL, is burst photo
|
||||
# 20 ZGENERICASSET.ZAVALANCHEPICKTYPE -- if not 2, is a selected burst photo
|
||||
# 21 ZGENERICASSET.ZKINDSUBTYPE
|
||||
|
||||
# 21 ZGENERICASSET.ZKINDSUBTYPE -- determine if live photos, etc
|
||||
# 22 ZGENERICASSET.ZCUSTOMRENDEREDVALUE -- determine if HDR photo
|
||||
|
||||
for row in c:
|
||||
uuid = row[0]
|
||||
@@ -942,27 +1011,51 @@ class PhotosDB:
|
||||
# handle burst photos
|
||||
# if burst photo, determine whether or not it's a selected burst photo
|
||||
# in Photos 5, burstUUID is called avalancheUUID
|
||||
info["burstUUID"] = row[19] # avalancheUUID
|
||||
info["burstPickType"] = row[20] #avalanchePickType
|
||||
info["burstUUID"] = row[19] # avalancheUUID
|
||||
info["burstPickType"] = row[20] # avalanchePickType
|
||||
if row[19] is not None:
|
||||
# it's a burst photo
|
||||
info["burst"] = True
|
||||
burst_uuid = row[19]
|
||||
if burst_uuid not in self._dbphotos_burst:
|
||||
self._dbphotos_burst[burst_uuid] = set()
|
||||
self._dbphotos_burst[burst_uuid] = set()
|
||||
self._dbphotos_burst[burst_uuid].add(uuid)
|
||||
if row[20] != 2 and row[20] != 4:
|
||||
info["burst_key"] = True # it's a key photo (selected from the burst)
|
||||
info[
|
||||
"burst_key"
|
||||
] = True # it's a key photo (selected from the burst)
|
||||
else:
|
||||
info["burst_key"] = False # it's a burst photo but not one that's selected
|
||||
info[
|
||||
"burst_key"
|
||||
] = False # it's a burst photo but not one that's selected
|
||||
else:
|
||||
# not a burst photo
|
||||
info["burst"] = False
|
||||
info["burst_key"] = None
|
||||
|
||||
# Info on sub-type (live photo, panorama, etc)
|
||||
# ZGENERICASSET.ZKINDSUBTYPE
|
||||
# 1 == panorama
|
||||
# 2 == live photo
|
||||
# 10 = screenshot
|
||||
# 100 = shared movie (MP4) ??
|
||||
# 101 = slow-motion video
|
||||
# 102 = Time lapse video
|
||||
info["subtype"] = row[21]
|
||||
info["panorama"] = True if row[21] == 1 else False
|
||||
info["live_photo"] = True if row[21] == 2 else False
|
||||
info["screenshot"] = True if row[21] == 10 else False
|
||||
info["slow_mo"] = True if row[21] == 101 else False
|
||||
info["time_lapse"] = True if row[21] == 102 else False
|
||||
|
||||
# Handle HDR photos and portraits
|
||||
# ZGENERICASSET.ZCUSTOMRENDEREDVALUE
|
||||
# 3 = HDR photo
|
||||
# 4 = non-HDR version of the photo
|
||||
# 8 = portrait
|
||||
info["customRenderedValue"] = row[22]
|
||||
info["hdr"] = True if row[22] == 3 else False
|
||||
info["portrait"] = True if row[22] == 8 else False
|
||||
|
||||
self._dbphotos[uuid] = info
|
||||
|
||||
|
||||
Reference in New Issue
Block a user