From 24b43b5e4dbfec9da6f9e808d74eded44a220138 Mon Sep 17 00:00:00 2001 From: Rhet Turnbull Date: Sat, 11 Jan 2020 08:10:28 -0800 Subject: [PATCH] Added incloud and iscloudasset to PhotoInfo (Photos 5) --- README.md | 10 ++++++- osxphotos/photoinfo.py | 15 +++++++++++ osxphotos/photosdb.py | 37 +++++++++++++++++++++----- tests/test_incloud.py | 60 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 114 insertions(+), 8 deletions(-) create mode 100644 tests/test_incloud.py diff --git a/README.md b/README.md index 4db00ece..a4a260eb 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,8 @@ - [`shared`](#shared) - [`isphoto`](#isphoto) - [`ismovie`](#ismovie) + - [`iscloudasset`](#iscloudasset) + - [`incloud`](#incloud) - [`uti`](#uti) - [`burst`](#burst) - [`burst_photos`](#burst_photos) @@ -66,7 +68,7 @@ * [Implementation Notes](#implementation-notes) * [Dependencies](#dependencies) * [Acknowledgements](#acknowledgements) - + ## What is osxphotos? OSXPhotos provides the ability to interact with and query Apple's Photos.app library database on MacOS. Using this module you can query the Photos database for information about the photos stored in a Photos library on your Mac--for example, file name, file path, and metadata such as keywords/tags, persons/faces, albums, etc. You can also easily export both the original and edited photos. @@ -597,6 +599,12 @@ Returns True if type is photo/still image, otherwise False #### `ismovie` Returns True if type is movie/video, otherwise False +#### `iscloudasset` +Returns True if photo is a cloud asset, that is, it is in a library synched to iCloud. See also [incloud](#incloud) + +#### `incloud` +Returns True if photo is a [cloud asset](#iscloudasset) and is synched to iCloud otherwise False if photo is a cloud asset and not yet synched to iCloud. Returns None if photo is not a cloud asset. + #### `uti` Returns Uniform Type Identifier (UTI) for the image, for example: 'public.jpeg' or 'com.apple.quicktime-movie' diff --git a/osxphotos/photoinfo.py b/osxphotos/photoinfo.py index 231001b0..bd4676c5 100644 --- a/osxphotos/photoinfo.py +++ b/osxphotos/photoinfo.py @@ -312,6 +312,21 @@ class PhotoInfo: """ return True if self._info["type"] == _PHOTO_TYPE else False + @property + def incloud(self): + """ Returns True if photo is cloud asset and is synched to cloud + False if photo is cloud asset and not yet synched to cloud + None if photo is not cloud asset + """ + return self._info["incloud"] + + @property + def iscloudasset(self): + """ Returns True if photo is a cloud asset (in an iCloud library), + otherwise False + """ + return True if self._info["cloudAssetGUID"] is not None else False + @property def burst(self): """ Returns True if photo is part of a Burst photo set, otherwise False """ diff --git a/osxphotos/photosdb.py b/osxphotos/photosdb.py index 9662f184..10ef5e1a 100644 --- a/osxphotos/photosdb.py +++ b/osxphotos/photosdb.py @@ -628,6 +628,12 @@ class PhotosDB: # TODO: Handle selfies (front facing camera, RKVersion.selfPortrait == 1) # self._dbphotos[uuid]["selfie"] = True if row[27] == 1 else False + self._dbphotos[uuid]["selfie"] = None + + # Init cloud details that will be filled in later + self._dbphotos[uuid]["cloudAssetGUID"] = None + self._dbphotos[uuid]["cloudLocalState"] = None # will be initialized later if is cloud asset + self._dbphotos[uuid]["incloud"] = None # will be initialized later if is cloud asset # get details needed to find path of the edited photos c.execute( @@ -929,7 +935,8 @@ class PhotosDB: ZGENERICASSET.ZAVALANCHEPICKTYPE, ZGENERICASSET.ZKINDSUBTYPE, ZGENERICASSET.ZCUSTOMRENDEREDVALUE, - ZADDITIONALASSETATTRIBUTES.ZCAMERACAPTUREDEVICE + ZADDITIONALASSETATTRIBUTES.ZCAMERACAPTUREDEVICE, + ZGENERICASSET.ZCLOUDASSETGUID FROM ZGENERICASSET JOIN ZADDITIONALASSETATTRIBUTES ON ZADDITIONALASSETATTRIBUTES.ZASSET = ZGENERICASSET.Z_PK WHERE ZGENERICASSET.ZTRASHEDSTATE = 0 @@ -960,6 +967,9 @@ class PhotosDB: # 21 ZGENERICASSET.ZKINDSUBTYPE -- determine if live photos, etc # 22 ZGENERICASSET.ZCUSTOMRENDEREDVALUE -- determine if HDR photo # 23 ZADDITIONALASSETATTRIBUTES.ZCAMERACAPTUREDEVICE -- 1 if selfie (front facing camera) + # 25 ZGENERICASSET.ZCLOUDASSETGUID -- not null if asset is cloud asset + # (e.g. user has "iCloud Photos" checked in Photos preferences) + for row in c: @@ -1070,6 +1080,11 @@ class PhotosDB: # Handle selfies (front facing camera, ZCAMERACAPTUREDEVICE=1) info["selfie"] = True if row[23] == 1 else False + # Determine if photo is part of cloud library (ZGENERICASSET.ZCLOUDASSETGUID not NULL) + info["cloudAssetGUID"] = row[24] + info["cloudLocalState"] = None # will be initialized later if is cloud asset + info["incloud"] = None # will be initialized later if is cloud asset + self._dbphotos[uuid] = info # # if row[19] is not None and ((row[20] == 2) or (row[20] == 4)): @@ -1131,9 +1146,6 @@ class PhotosDB: # Get info on remote/local availability for photos in shared albums # Shared photos have a null fingerprint (and some other photos do too) # TODO: There may be a bug here, perhaps ZDATASTORESUBTYPE should be 1 --> it's the longest ZDATALENGTH (is this the original) - # Also, doesn't seem to be entirely accurate for PNGs (screenshots mostly) - # for PNGs, JPEG render seems to be used unless edited or exported - # see for example ./resources/renders/F/F2FF9B89-FB6F-4853-942B-9F8BEE8DFFA1_1_201_a.jpeg c.execute( """ SELECT ZGENERICASSET.ZUUID, @@ -1195,8 +1207,19 @@ class PhotosDB: # f"{uuid} isMissing changed: {old} {self._dbphotos[uuid]['isMissing']}" # ) - if _debug(): - logging.debug(pformat(self._dbphotos)) + # get information about cloud sync state + c.execute( + """ SELECT + ZGENERICASSET.ZUUID, + ZCLOUDMASTER.ZCLOUDLOCALSTATE + FROM ZCLOUDMASTER, ZGENERICASSET + WHERE ZGENERICASSET.ZMASTER = ZCLOUDMASTER.Z_PK """ + ) + for row in c: + uuid = row[0] + if uuid in self._dbphotos: + self._dbphotos[uuid]["cloudLocalState"] = row[1] + self._dbphotos[uuid]["incloud"] = True if row[1] == 3 else False # add faces and keywords to photo data for uuid in self._dbphotos: @@ -1226,6 +1249,7 @@ class PhotosDB: conn.close() self._cleanup_tmp_files() + # done processing, dump debug data if requested if _debug(): logging.debug("Faces:") logging.debug(pformat(self._dbfaces_uuid)) @@ -1254,7 +1278,6 @@ class PhotosDB: logging.debug("Burst Photos:") logging.debug(pformat(self._dbphotos_burst)) - # TODO: fix default values to None instead of [] def photos( self, keywords=None, diff --git a/tests/test_incloud.py b/tests/test_incloud.py new file mode 100644 index 00000000..69d5a48e --- /dev/null +++ b/tests/test_incloud.py @@ -0,0 +1,60 @@ +# Test cloud photos + +import pytest + +PHOTOS_DB_CLOUD = "./tests/Test-Cloud-10.15.1.photoslibrary/database/photos.db" +PHOTOS_DB_NOT_CLOUD = "./tests/Test-10.15.1.photoslibrary/database/photos.db" + +UUID_DICT = { + "incloud": "37210110-E940-4227-92D3-45C40F68EB0A", + "not_incloud": "E5BC411D-30EE-44D3-84C0-54760A10579D", + "cloudasset": "D11D25FF-5F31-47D2-ABA9-58418878DC15", + "not_cloudasset": "6191423D-8DB8-4D4C-92BE-9BBBA308AAC4", +} + + +def test_incloud(): + import osxphotos + + photosdb = osxphotos.PhotosDB(PHOTOS_DB_CLOUD) + photos = photosdb.photos(uuid=[UUID_DICT["incloud"]]) + + assert photos[0].incloud + + +def test_not_incloud(): + import osxphotos + + photosdb = osxphotos.PhotosDB(PHOTOS_DB_CLOUD) + photos = photosdb.photos(uuid=[UUID_DICT["not_incloud"]]) + + assert not photos[0].incloud + + +def test_cloudasset_1(): + import osxphotos + + photosdb = osxphotos.PhotosDB(PHOTOS_DB_CLOUD) + photos = photosdb.photos(uuid=[UUID_DICT["cloudasset"]]) + + assert photos[0].iscloudasset + + +def test_cloudasset_2(): + import osxphotos + + photosdb = osxphotos.PhotosDB(PHOTOS_DB_CLOUD) + photos = photosdb.photos(uuid=[UUID_DICT["not_incloud"]]) + + # not_incloud is still a cloud asset + assert photos[0].iscloudasset + + +def test_cloudasset_3(): + import osxphotos + + photosdb = osxphotos.PhotosDB(PHOTOS_DB_NOT_CLOUD) + photos = photosdb.photos(uuid=[UUID_DICT["not_cloudasset"]]) + + assert not photos[0].iscloudasset +