Added live photo support for both Photos 4 & 5

This commit is contained in:
Rhet Turnbull
2020-01-04 09:07:23 -08:00
parent 9fb05e4dd1
commit d5eaff02f2
3 changed files with 174 additions and 41 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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