From 15d7ad538df9349749a14365d9962a45a1b33607 Mon Sep 17 00:00:00 2001 From: Rhet Turnbull Date: Sun, 8 Mar 2020 12:52:44 -0700 Subject: [PATCH] Added media type specials, closes #60 --- README.md | 19 +++++ osxphotos/_constants.py | 3 + osxphotos/_version.py | 2 +- osxphotos/photoinfo.py | 35 +++++++++ osxphotos/photosdb.py | 57 +++++++++------ tests/test_specials_catalina_10_15_1.py | 94 ++++++++++++++++++++++++ tests/test_specials_mojave_10_14_6.py | 96 +++++++++++++++++++++++++ tests/test_specials_sierra_10_12.py | 87 ++++++++++++++++++++++ 8 files changed, 372 insertions(+), 21 deletions(-) create mode 100644 tests/test_specials_catalina_10_15_1.py create mode 100644 tests/test_specials_mojave_10_14_6.py create mode 100644 tests/test_specials_sierra_10_12.py diff --git a/README.md b/README.md index ee0cccc2..c3f3a1b5 100644 --- a/README.md +++ b/README.md @@ -657,6 +657,25 @@ Returns the path to the live video component of a [live photo](#live_photo). If **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. +#### `portrait` +Returns True if photo was taken in iPhone portrait mode, otherwise False. + +#### `hdr` +Returns True if photo was taken in High Dynamic Range (HDR) mode, otherwise False. + +#### `selfie` +Returns True if photo is a selfie (taken with front-facing camera), otherwise False. + +**Note**: Only implemented for Photos version 3.0+. On Photos version < 3.0, returns None. + +#### `time_lapse` +Returns True if photo is a time lapse video, otherwise False. + +#### `panorama` +Returns True if photo is a panorama, otherwise False. + +**Note**: The result of `PhotoInfo.panorama` will differ from the "Panoramas" Media Types smart album in that it will also identify panorama photos from older phones that Photos does not recognize as panoramas. + #### `json()` Returns a JSON representation of all photo info diff --git a/osxphotos/_constants.py b/osxphotos/_constants.py index 98d3b42c..09855526 100644 --- a/osxphotos/_constants.py +++ b/osxphotos/_constants.py @@ -13,6 +13,9 @@ import os.path # TODO: Should this also use compatibleBackToVersion from LiGlobals? _TESTED_DB_VERSIONS = ["6000", "4025", "4016", "3301", "2622"] +# only version 3 - 4 have RKVersion.selfPortrait +_PHOTOS_3_VERSION = "3301" + # versions later than this have a different database structure _PHOTOS_5_VERSION = "6000" diff --git a/osxphotos/_version.py b/osxphotos/_version.py index 17e24109..9181d129 100644 --- a/osxphotos/_version.py +++ b/osxphotos/_version.py @@ -1,3 +1,3 @@ """ version info """ -__version__ = "0.22.12" +__version__ = "0.22.13" diff --git a/osxphotos/photoinfo.py b/osxphotos/photoinfo.py index 77afb5cd..c2a9d61d 100644 --- a/osxphotos/photoinfo.py +++ b/osxphotos/photoinfo.py @@ -454,6 +454,41 @@ class PhotoInfo: return photopath + @property + def panorama(self): + """ Returns True if photo is a panorama, otherwise False """ + return self._info["panorama"] + + @property + def slow_mo(self): + """ Returns True if photo is a slow motion video, otherwise False """ + return self._info["slow_mo"] + + @property + def time_lapse(self): + """ Returns True if photo is a time lapse video, otherwise False """ + return self._info["time_lapse"] + + @property + def hdr(self): + """ Returns True if photo is an HDR photo, otherwise False """ + return self._info["hdr"] + + @property + def screenshot(self): + """ Returns True if photo is an HDR photo, otherwise False """ + return self._info["screenshot"] + + @property + def portrait(self): + """ Returns True if photo is a portrait, otherwise False """ + return self._info["portrait"] + + @property + def selfie(self): + """ Returns True if photo is a selfie (front facing camera), otherwise False """ + return self._info["selfie"] + def export( self, dest, diff --git a/osxphotos/photosdb.py b/osxphotos/photosdb.py index 368734a9..690e2cc4 100644 --- a/osxphotos/photosdb.py +++ b/osxphotos/photosdb.py @@ -18,6 +18,7 @@ from shutil import copyfile from ._constants import ( _MOVIE_TYPE, _PHOTO_TYPE, + _PHOTOS_3_VERSION, _PHOTOS_5_VERSION, _TESTED_DB_VERSIONS, _TESTED_OS_VERSIONS, @@ -522,21 +523,36 @@ class PhotosDB: self._dbvolumes[vol[0]] = vol[1] # Get photo details - c.execute( - """ SELECT RKVersion.uuid, RKVersion.modelId, RKVersion.masterUuid, RKVersion.filename, - RKVersion.lastmodifieddate, RKVersion.imageDate, RKVersion.mainRating, - RKVersion.hasAdjustments, RKVersion.hasKeywords, RKVersion.imageTimeZoneOffsetSeconds, - RKMaster.volumeId, RKMaster.imagePath, RKVersion.extendedDescription, RKVersion.name, - RKMaster.isMissing, RKMaster.originalFileName, RKVersion.isFavorite, RKVersion.isHidden, - RKVersion.latitude, RKVersion.longitude, - RKVersion.adjustmentUuid, RKVersion.type, RKMaster.UTI, - 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' """ - ) - - # TODO: RKVersion.selfPortrait -- only in Photos 3 and up + if self._db_version < _PHOTOS_3_VERSION: + # Photos < 3.0 doesn't have RKVersion.selfPortrait (selfie) + c.execute( + """ SELECT RKVersion.uuid, RKVersion.modelId, RKVersion.masterUuid, RKVersion.filename, + RKVersion.lastmodifieddate, RKVersion.imageDate, RKVersion.mainRating, + RKVersion.hasAdjustments, RKVersion.hasKeywords, RKVersion.imageTimeZoneOffsetSeconds, + RKMaster.volumeId, RKMaster.imagePath, RKVersion.extendedDescription, RKVersion.name, + RKMaster.isMissing, RKMaster.originalFileName, RKVersion.isFavorite, RKVersion.isHidden, + RKVersion.latitude, RKVersion.longitude, + RKVersion.adjustmentUuid, RKVersion.type, RKMaster.UTI, + 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' """ + ) + else: + c.execute( + """ SELECT RKVersion.uuid, RKVersion.modelId, RKVersion.masterUuid, RKVersion.filename, + RKVersion.lastmodifieddate, RKVersion.imageDate, RKVersion.mainRating, + RKVersion.hasAdjustments, RKVersion.hasKeywords, RKVersion.imageTimeZoneOffsetSeconds, + RKMaster.volumeId, RKMaster.imagePath, RKVersion.extendedDescription, RKVersion.name, + RKMaster.isMissing, RKMaster.originalFileName, RKVersion.isFavorite, RKVersion.isHidden, + RKVersion.latitude, RKVersion.longitude, + RKVersion.adjustmentUuid, RKVersion.type, RKMaster.UTI, + RKVersion.burstUuid, RKVersion.burstPickType, + RKVersion.specialType, RKMaster.modelID, + RKVersion.selfPortrait + FROM RKVersion, RKMaster WHERE RKVersion.isInTrash = 0 AND + RKVersion.masterUuid = RKMaster.uuid AND RKVersion.filename NOT LIKE '%.pdf' """ + ) # order of results # 0 RKVersion.uuid @@ -566,8 +582,7 @@ class PhotosDB: # 24 RKVersion.burstPickType # 25 RKVersion.specialType # 26 RKMaster.modelID - - # 27 RKVersion.selfPortrait -- 1 if selfie (not yet implemented) + # 27 RKVersion.selfPortrait -- 1 if selfie, Photos >= 3, not present for Photos < 3 for row in c: uuid = row[0] @@ -671,9 +686,11 @@ class PhotosDB: self._dbphotos[uuid]["screenshot"] = True if row[25] == 6 else False self._dbphotos[uuid]["portrait"] = True if row[25] == 9 else False - # 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 + # selfies (front facing camera, RKVersion.selfPortrait == 1) + if self._db_version >= _PHOTOS_3_VERSION: + self._dbphotos[uuid]["selfie"] = True if row[27] == 1 else False + else: + self._dbphotos[uuid]["selfie"] = None # Init cloud details that will be filled in later if cloud asset self._dbphotos[uuid]["cloudAssetGUID"] = None # Photos 5 diff --git a/tests/test_specials_catalina_10_15_1.py b/tests/test_specials_catalina_10_15_1.py new file mode 100644 index 00000000..7e85e7e7 --- /dev/null +++ b/tests/test_specials_catalina_10_15_1.py @@ -0,0 +1,94 @@ +# Test cloud photos + +import pytest + +PHOTOS_DB_CLOUD = "./tests/Test-Cloud-10.15.1.photoslibrary/database/photos.db" + +UUID_DICT = { + "portrait": "7CDA5F84-AA16-4D28-9AA6-A49E1DF8A332", + "hdr": "D11D25FF-5F31-47D2-ABA9-58418878DC15", + "selfie": "080525C4-1F05-48E5-A3F4-0C53127BB39C", + "time_lapse": "4614086E-C797-4876-B3B9-3057E8D757C9", + "panorama": "1C1C8F1F-826B-4A24-B1CB-56628946A834", + "no_specials": "C2BBC7A4-5333-46EE-BAF0-093E72111B39", +} + + +def test_portrait(): + import osxphotos + + photosdb = osxphotos.PhotosDB(PHOTOS_DB_CLOUD) + photos = photosdb.photos(uuid=[UUID_DICT["portrait"]]) + + assert photos[0].portrait + assert not photos[0].hdr + assert not photos[0].selfie + assert not photos[0].time_lapse + assert not photos[0].panorama + + photos = photosdb.photos(uuid=[UUID_DICT["no_specials"]]) + assert not photos[0].portrait + + +def test_hdr(): + import osxphotos + + photosdb = osxphotos.PhotosDB(PHOTOS_DB_CLOUD) + photos = photosdb.photos(uuid=[UUID_DICT["hdr"]]) + + assert photos[0].hdr + assert not photos[0].portrait + assert not photos[0].selfie + assert not photos[0].time_lapse + assert not photos[0].panorama + + photos = photosdb.photos(uuid=[UUID_DICT["no_specials"]]) + assert not photos[0].hdr + + +def test_selfie(): + import osxphotos + + photosdb = osxphotos.PhotosDB(PHOTOS_DB_CLOUD) + photos = photosdb.photos(uuid=[UUID_DICT["selfie"]]) + + assert photos[0].selfie + assert not photos[0].portrait + assert not photos[0].hdr + assert not photos[0].time_lapse + assert not photos[0].panorama + + photos = photosdb.photos(uuid=[UUID_DICT["no_specials"]]) + assert not photos[0].selfie + + +def test_time_lapse(): + import osxphotos + + photosdb = osxphotos.PhotosDB(PHOTOS_DB_CLOUD) + photos = photosdb.photos(uuid=[UUID_DICT["time_lapse"]], movies=True) + + assert photos[0].time_lapse + assert not photos[0].portrait + assert not photos[0].hdr + assert not photos[0].selfie + assert not photos[0].panorama + + photos = photosdb.photos(uuid=[UUID_DICT["no_specials"]]) + assert not photos[0].time_lapse + + +def test_panorama(): + import osxphotos + + photosdb = osxphotos.PhotosDB(PHOTOS_DB_CLOUD) + photos = photosdb.photos(uuid=[UUID_DICT["panorama"]]) + + assert photos[0].panorama + assert not photos[0].portrait + assert not photos[0].selfie + assert not photos[0].time_lapse + assert not photos[0].hdr + + photos = photosdb.photos(uuid=[UUID_DICT["no_specials"]]) + assert not photos[0].panorama diff --git a/tests/test_specials_mojave_10_14_6.py b/tests/test_specials_mojave_10_14_6.py new file mode 100644 index 00000000..19041690 --- /dev/null +++ b/tests/test_specials_mojave_10_14_6.py @@ -0,0 +1,96 @@ +# Test cloud photos + +import pytest + +PHOTOS_DB_CLOUD = "./tests/Test-Cloud-10.14.6.photoslibrary/database/photos.db" + +UUID_DICT = { + # "portrait": "7CDA5F84-AA16-4D28-9AA6-A49E1DF8A332", + "hdr": "UIgouj2cQqyKJnB2bCHrSg", + "selfie": "NsO5Yg8qSPGBGiVxsCd5Kw", + "time_lapse": "pKAWFwtlQYuR962KEaonPA", + # "panorama": "1C1C8F1F-826B-4A24-B1CB-56628946A834", + "no_specials": "%PgMNP%xRTWTJF+oOyZbXQ", +} + + +@pytest.mark.skip(reason="don't have portrait photo in the 10.14.6yy database") +def test_portrait(): + import osxphotos + + photosdb = osxphotos.PhotosDB(PHOTOS_DB_CLOUD) + photos = photosdb.photos(uuid=[UUID_DICT["portrait"]]) + + assert photos[0].portrait + assert not photos[0].hdr + assert not photos[0].selfie + assert not photos[0].time_lapse + assert not photos[0].panorama + + photos = photosdb.photos(uuid=[UUID_DICT["no_specials"]]) + assert not photos[0].portrait + + +def test_hdr(): + import osxphotos + + photosdb = osxphotos.PhotosDB(PHOTOS_DB_CLOUD) + photos = photosdb.photos(uuid=[UUID_DICT["hdr"]]) + + assert photos[0].hdr + assert not photos[0].portrait + assert not photos[0].selfie + assert not photos[0].time_lapse + assert not photos[0].panorama + + photos = photosdb.photos(uuid=[UUID_DICT["no_specials"]]) + assert not photos[0].hdr + + +def test_selfie(): + import osxphotos + + photosdb = osxphotos.PhotosDB(PHOTOS_DB_CLOUD) + photos = photosdb.photos(uuid=[UUID_DICT["selfie"]]) + + assert photos[0].selfie + assert not photos[0].portrait + assert not photos[0].hdr + assert not photos[0].time_lapse + assert not photos[0].panorama + + photos = photosdb.photos(uuid=[UUID_DICT["no_specials"]]) + assert not photos[0].selfie + + +def test_time_lapse(): + import osxphotos + + photosdb = osxphotos.PhotosDB(PHOTOS_DB_CLOUD) + photos = photosdb.photos(uuid=[UUID_DICT["time_lapse"]], movies=True) + + assert photos[0].time_lapse + assert not photos[0].portrait + assert not photos[0].hdr + assert not photos[0].selfie + assert not photos[0].panorama + + photos = photosdb.photos(uuid=[UUID_DICT["no_specials"]]) + assert not photos[0].time_lapse + + +@pytest.mark.skip(reason="no panorama in 10.14.6 database") +def test_panorama(): + import osxphotos + + photosdb = osxphotos.PhotosDB(PHOTOS_DB_CLOUD) + photos = photosdb.photos(uuid=[UUID_DICT["panorama"]]) + + assert photos[0].panorama + assert not photos[0].portrait + assert not photos[0].selfie + assert not photos[0].time_lapse + assert not photos[0].hdr + + photos = photosdb.photos(uuid=[UUID_DICT["no_specials"]]) + assert not photos[0].panorama diff --git a/tests/test_specials_sierra_10_12.py b/tests/test_specials_sierra_10_12.py new file mode 100644 index 00000000..bccf380c --- /dev/null +++ b/tests/test_specials_sierra_10_12.py @@ -0,0 +1,87 @@ +# Test cloud photos + +import pytest + +PHOTOS_DB = "./tests/Test-10.12.6.photoslibrary/database/photos.db" + +UUID_DICT = {"no_specials": "Pj99JmYjQkeezdY2OFuSaw"} + + +def test_portrait(): + import osxphotos + + photosdb = osxphotos.PhotosDB(PHOTOS_DB) + # photos = photosdb.photos(uuid=[UUID_DICT["portrait"]]) + + # assert photos[0].portrait + # assert not photos[0].hdr + # assert not photos[0].selfie + # assert not photos[0].time_lapse + # assert not photos[0].panorama + + photos = photosdb.photos(uuid=[UUID_DICT["no_specials"]]) + assert not photos[0].portrait + + +def test_hdr(): + import osxphotos + + photosdb = osxphotos.PhotosDB(PHOTOS_DB) + # photos = photosdb.photos(uuid=[UUID_DICT["hdr"]]) + + # assert photos[0].hdr + # assert not photos[0].portrait + # assert not photos[0].selfie + # assert not photos[0].time_lapse + # assert not photos[0].panorama + + photos = photosdb.photos(uuid=[UUID_DICT["no_specials"]]) + assert not photos[0].hdr + + +def test_selfie(): + import osxphotos + + photosdb = osxphotos.PhotosDB(PHOTOS_DB) + # photos = photosdb.photos(uuid=[UUID_DICT["selfie"]]) + + # assert photos[0].selfie + # assert not photos[0].portrait + # assert not photos[0].hdr + # assert not photos[0].time_lapse + # assert not photos[0].panorama + + photos = photosdb.photos(uuid=[UUID_DICT["no_specials"]]) + assert photos[0].selfie is None + + +def test_time_lapse(): + import osxphotos + + photosdb = osxphotos.PhotosDB(PHOTOS_DB) + # photos = photosdb.photos(uuid=[UUID_DICT["time_lapse"]], movies=True) + + # assert photos[0].time_lapse + # assert not photos[0].portrait + # assert not photos[0].hdr + # assert not photos[0].selfie + # assert not photos[0].panorama + + photos = photosdb.photos(uuid=[UUID_DICT["no_specials"]]) + assert not photos[0].time_lapse + + +def test_panorama(): + import osxphotos + + photosdb = osxphotos.PhotosDB(PHOTOS_DB) + # photos = photosdb.photos(uuid=[UUID_DICT["panorama"]]) + + # assert photos[0].panorama + # assert not photos[0].portrait + # assert not photos[0].selfie + # assert not photos[0].time_lapse + # assert not photos[0].hdr + + photos = photosdb.photos(uuid=[UUID_DICT["no_specials"]]) + assert not photos[0].panorama