Added media type specials, closes #60

This commit is contained in:
Rhet Turnbull 2020-03-08 12:52:44 -07:00
parent 1f8fd6e929
commit 15d7ad538d
8 changed files with 372 additions and 21 deletions

View File

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

View File

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

View File

@ -1,3 +1,3 @@
""" version info """
__version__ = "0.22.12"
__version__ = "0.22.13"

View File

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

View File

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

View File

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

View File

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

View File

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