@@ -1578,9 +1578,15 @@ Returns `True` if the picture has been marked as a favorite, otherwise `False`
|
||||
#### `hidden`
|
||||
Returns `True` if the picture has been marked as hidden, otherwise `False`
|
||||
|
||||
#### `visible`
|
||||
Returns `True` if the picture is visible in library, otherwise `False`. e.g. non-selected burst photos are not hidden but also not visible
|
||||
|
||||
#### `intrash`
|
||||
Returns `True` if the picture is in the trash ('Recently Deleted' folder), otherwise `False`
|
||||
|
||||
#### `date_trashed`
|
||||
Returns the date the photo was placed in the trash as a datetime.datetime object or None if photo is not in the trash
|
||||
|
||||
#### `location`
|
||||
Returns latitude and longitude as a tuple of floats (latitude, longitude). If location is not set, latitude and longitude are returned as `None`
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
""" version info """
|
||||
|
||||
__version__ = "0.39.11"
|
||||
__version__ = "0.39.12"
|
||||
|
||||
|
||||
|
||||
@@ -113,15 +113,15 @@ class PhotoInfo:
|
||||
# lastmodifieddate anytime photo database record is updated (e.g. adding tags)
|
||||
# only report lastmodified date for Photos <=4 if photo is edited;
|
||||
# even in this case, the date could be incorrect
|
||||
if self.hasadjustments or self._db._db_version > _PHOTOS_4_VERSION:
|
||||
imagedate = self._info["lastmodifieddate"]
|
||||
if imagedate:
|
||||
seconds = self._info["imageTimeZoneOffsetSeconds"] or 0
|
||||
delta = timedelta(seconds=seconds)
|
||||
tz = timezone(delta)
|
||||
return imagedate.astimezone(tz=tz)
|
||||
else:
|
||||
return None
|
||||
if not self.hasadjustments and self._db._db_version <= _PHOTOS_4_VERSION:
|
||||
return None
|
||||
|
||||
imagedate = self._info["lastmodifieddate"]
|
||||
if imagedate:
|
||||
seconds = self._info["imageTimeZoneOffsetSeconds"] or 0
|
||||
delta = timedelta(seconds=seconds)
|
||||
tz = timezone(delta)
|
||||
return imagedate.astimezone(tz=tz)
|
||||
else:
|
||||
return None
|
||||
|
||||
@@ -501,37 +501,52 @@ class PhotoInfo:
|
||||
downloaded from cloud to local storate their status in the database might still show
|
||||
isMissing = 1
|
||||
"""
|
||||
return True if self._info["isMissing"] == 1 else False
|
||||
return self._info["isMissing"] == 1
|
||||
|
||||
@property
|
||||
def hasadjustments(self):
|
||||
""" True if picture has adjustments / edits """
|
||||
return True if self._info["hasAdjustments"] == 1 else False
|
||||
return self._info["hasAdjustments"] == 1
|
||||
|
||||
@property
|
||||
def external_edit(self):
|
||||
""" Returns True if picture was edited outside of Photos using external editor """
|
||||
return (
|
||||
True
|
||||
if self._info["adjustmentFormatID"] == "com.apple.Photos.externalEdit"
|
||||
else False
|
||||
)
|
||||
return self._info["adjustmentFormatID"] == "com.apple.Photos.externalEdit"
|
||||
|
||||
@property
|
||||
def favorite(self):
|
||||
""" True if picture is marked as favorite """
|
||||
return True if self._info["favorite"] == 1 else False
|
||||
return self._info["favorite"] == 1
|
||||
|
||||
@property
|
||||
def hidden(self):
|
||||
""" True if picture is hidden """
|
||||
return True if self._info["hidden"] == 1 else False
|
||||
return self._info["hidden"] == 1
|
||||
|
||||
@property
|
||||
def visible(self):
|
||||
""" True if picture is visble """
|
||||
return self._info["visible"]
|
||||
|
||||
@property
|
||||
def intrash(self):
|
||||
""" True if picture is in trash ('Recently Deleted' folder)"""
|
||||
return self._info["intrash"]
|
||||
|
||||
@property
|
||||
def date_trashed(self):
|
||||
""" Date asset was placed in the trash or None """
|
||||
# TODO: add add_timezone(dt, offset_seconds) to datetime_utils
|
||||
# also update date_modified
|
||||
trasheddate = self._info["trasheddate"]
|
||||
if trasheddate:
|
||||
seconds = self._info["imageTimeZoneOffsetSeconds"] or 0
|
||||
delta = timedelta(seconds=seconds)
|
||||
tz = timezone(delta)
|
||||
return trasheddate.astimezone(tz=tz)
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def location(self):
|
||||
""" returns (latitude, longitude) as float in degrees or None """
|
||||
@@ -551,14 +566,15 @@ class PhotoInfo:
|
||||
"""Returns Uniform Type Identifier (UTI) for the image
|
||||
for example: public.jpeg or com.apple.quicktime-movie
|
||||
"""
|
||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||
if self.hasadjustments:
|
||||
return self._info["UTI_edited"]
|
||||
elif self.has_raw and self.raw_original:
|
||||
# return UTI of the non-raw image to match Photos 5+ behavior
|
||||
return self._info["raw_pair_info"]["UTI"]
|
||||
else:
|
||||
return self._info["UTI"]
|
||||
if self._db._db_version <= _PHOTOS_4_VERSION and self.hasadjustments:
|
||||
return self._info["UTI_edited"]
|
||||
elif (
|
||||
self._db._db_version <= _PHOTOS_4_VERSION
|
||||
and self.has_raw
|
||||
and self.raw_original
|
||||
):
|
||||
# return UTI of the non-raw image to match Photos 5+ behavior
|
||||
return self._info["raw_pair_info"]["UTI"]
|
||||
else:
|
||||
return self._info["UTI"]
|
||||
|
||||
@@ -597,12 +613,12 @@ class PhotoInfo:
|
||||
@property
|
||||
def ismovie(self):
|
||||
"""Returns True if file is a movie, otherwise False"""
|
||||
return True if self._info["type"] == _MOVIE_TYPE else False
|
||||
return self._info["type"] == _MOVIE_TYPE
|
||||
|
||||
@property
|
||||
def isphoto(self):
|
||||
"""Returns True if file is an image, otherwise False"""
|
||||
return True if self._info["type"] == _PHOTO_TYPE else False
|
||||
return self._info["type"] == _PHOTO_TYPE
|
||||
|
||||
@property
|
||||
def incloud(self):
|
||||
|
||||
@@ -887,7 +887,9 @@ class PhotosDB:
|
||||
RKMaster.width,
|
||||
RKMaster.orientation,
|
||||
RKMaster.fileSize,
|
||||
RKVersion.subType
|
||||
RKVersion.subType,
|
||||
RKVersion.inTrashDate,
|
||||
RKVersion.showInLibrary
|
||||
FROM RKVersion, RKMaster
|
||||
WHERE RKVersion.masterUuid = RKMaster.uuid"""
|
||||
)
|
||||
@@ -915,7 +917,9 @@ class PhotosDB:
|
||||
RKMaster.width,
|
||||
RKMaster.orientation,
|
||||
RKMaster.originalFileSize,
|
||||
RKVersion.subType
|
||||
RKVersion.subType,
|
||||
RKVersion.inTrashDate,
|
||||
RKVersion.showInLibrary
|
||||
FROM RKVersion, RKMaster
|
||||
WHERE RKVersion.masterUuid = RKMaster.uuid"""
|
||||
)
|
||||
@@ -962,6 +966,8 @@ class PhotosDB:
|
||||
# 38 RKMaster.orientation,
|
||||
# 39 RKMaster.originalFileSize
|
||||
# 40 RKVersion.subType
|
||||
# 41 RKVersion.inTrashDate
|
||||
# 42 RKVersion.showInLibrary -- is item visible in library (e.g. non-selected burst images are not visible)
|
||||
|
||||
for row in c:
|
||||
uuid = row[0]
|
||||
@@ -1136,7 +1142,14 @@ class PhotosDB:
|
||||
)
|
||||
|
||||
# recently deleted items
|
||||
self._dbphotos[uuid]["intrash"] = True if row[32] == 1 else False
|
||||
self._dbphotos[uuid]["intrash"] = row[32] == 1
|
||||
self._dbphotos[uuid]["trasheddate_timestamp"] = row[41]
|
||||
try:
|
||||
self._dbphotos[uuid]["trasheddate"] = datetime.fromtimestamp(
|
||||
row[41] + TIME_DELTA
|
||||
)
|
||||
except (ValueError, TypeError):
|
||||
self._dbphotos[uuid]["trasheddate"] = None
|
||||
|
||||
# height/width/orientation
|
||||
self._dbphotos[uuid]["height"] = row[33]
|
||||
@@ -1147,6 +1160,10 @@ class PhotosDB:
|
||||
self._dbphotos[uuid]["original_orientation"] = row[38]
|
||||
self._dbphotos[uuid]["original_filesize"] = row[39]
|
||||
|
||||
# visibility state
|
||||
self._dbphotos[uuid]["visibility_state"] = row[42]
|
||||
self._dbphotos[uuid]["visible"] = row[42] == 1
|
||||
|
||||
# import session not yet handled for Photos 4
|
||||
self._dbphotos[uuid]["import_session"] = None
|
||||
self._dbphotos[uuid]["import_uuid"] = None
|
||||
@@ -1840,7 +1857,9 @@ class PhotosDB:
|
||||
ZADDITIONALASSETATTRIBUTES.ZORIGINALORIENTATION,
|
||||
ZADDITIONALASSETATTRIBUTES.ZORIGINALFILESIZE,
|
||||
{depth_state},
|
||||
{asset_table}.ZADJUSTMENTTIMESTAMP
|
||||
{asset_table}.ZADJUSTMENTTIMESTAMP,
|
||||
{asset_table}.ZVISIBILITYSTATE,
|
||||
{asset_table}.ZTRASHEDDATE
|
||||
FROM {asset_table}
|
||||
JOIN ZADDITIONALASSETATTRIBUTES ON ZADDITIONALASSETATTRIBUTES.ZASSET = {asset_table}.Z_PK
|
||||
ORDER BY {asset_table}.ZUUID """
|
||||
@@ -1885,6 +1904,8 @@ class PhotosDB:
|
||||
# 35 ZADDITIONALASSETATTRIBUTES.ZORIGINALFILESIZE
|
||||
# 36 ZGENERICASSET.ZDEPTHSTATES / ZASSET.ZDEPTHTYPE
|
||||
# 37 ZGENERICASSET.ZADJUSTMENTTIMESTAMP -- when was photo edited?
|
||||
# 38 ZGENERICASSET.ZVISIBILITYSTATE -- 0 if visible, 2 if not (e.g. a burst image)
|
||||
# 39 ZGENERICASSET.ZTRASHEDDATE -- date item placed in the trash or null if not in trash
|
||||
|
||||
for row in c:
|
||||
uuid = row[0]
|
||||
@@ -1901,9 +1922,7 @@ class PhotosDB:
|
||||
info["lastmodifieddate_timestamp"] = row[37]
|
||||
try:
|
||||
info["lastmodifieddate"] = datetime.fromtimestamp(row[37] + TIME_DELTA)
|
||||
except ValueError:
|
||||
info["lastmodifieddate"] = None
|
||||
except TypeError:
|
||||
except (ValueError, TypeError):
|
||||
info["lastmodifieddate"] = None
|
||||
|
||||
info["imageTimeZoneOffsetSeconds"] = row[6]
|
||||
@@ -2046,6 +2065,11 @@ class PhotosDB:
|
||||
|
||||
# recently deleted items
|
||||
info["intrash"] = True if row[28] == 1 else False
|
||||
info["trasheddate_timestamp"] = row[39]
|
||||
try:
|
||||
info["trasheddate"] = datetime.fromtimestamp(row[39] + TIME_DELTA)
|
||||
except (ValueError, TypeError):
|
||||
info["trasheddate"] = None
|
||||
|
||||
# height/width/orientation
|
||||
info["height"] = row[29]
|
||||
@@ -2056,6 +2080,11 @@ class PhotosDB:
|
||||
info["original_orientation"] = row[34]
|
||||
info["original_filesize"] = row[35]
|
||||
|
||||
# visibility state, visible (True) if 0, otherwise not visible (False)
|
||||
# only values I've seen are 0 for visible, 2 for not-visible
|
||||
info["visibility_state"] = row[38]
|
||||
info["visible"] = row[38] == 0
|
||||
|
||||
# initialize import session info which will be filled in later
|
||||
# not every photo has an import session so initialize all records now
|
||||
info["import_session"] = None
|
||||
|
||||
@@ -63,8 +63,8 @@ def noop(*args, **kwargs):
|
||||
|
||||
|
||||
def _get_os_version():
|
||||
# returns tuple containing OS version
|
||||
# e.g. 10.13.6 = (10, 13, 6)
|
||||
# returns tuple of str containing OS version
|
||||
# e.g. 10.13.6 = ("10", "13", "6")
|
||||
version = platform.mac_ver()[0].split(".")
|
||||
if len(version) == 2:
|
||||
(ver, major) = version
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -13,6 +13,11 @@ import pytest
|
||||
|
||||
import osxphotos
|
||||
from osxphotos._constants import _UNKNOWN_PERSON
|
||||
from osxphotos.utils import _get_os_version
|
||||
|
||||
OS_VERSION = _get_os_version()
|
||||
SKIP_TEST = "OSXPHOTOS_TEST_EXPORT" not in os.environ or OS_VERSION[1] != "15"
|
||||
PHOTOS_DB_LOCAL = os.path.expanduser("~/Pictures/Photos Library.photoslibrary")
|
||||
|
||||
PHOTOS_DB = "tests/Test-10.15.7.photoslibrary/database/photos.db"
|
||||
PHOTOS_DB_PATH = "/Test-10.15.7.photoslibrary/database/photos.db"
|
||||
@@ -98,6 +103,11 @@ UUID_DICT = {
|
||||
"movie": "D1359D09-1373-4F3B-B0E3-1A4DE573E4A3",
|
||||
}
|
||||
|
||||
UUID_DICT_LOCAL = {
|
||||
"not_visible": "ABF00253-78E7-4FD6-953B-709307CD489D",
|
||||
"burst": "44AF1FCA-AC2D-4FA5-B288-E67DC18F9CA8",
|
||||
}
|
||||
|
||||
UUID_PUMPKIN_FARM = [
|
||||
"F12384F6-CD17-4151-ACBA-AE0E3688539E",
|
||||
"D79B8D77-BFFC-460B-9312-034F2877D35B",
|
||||
@@ -194,6 +204,11 @@ def photosdb():
|
||||
return osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def photosdb_local():
|
||||
return osxphotos.PhotosDB(dbfile=PHOTOS_DB_LOCAL)
|
||||
|
||||
|
||||
def test_init1():
|
||||
# test named argument
|
||||
|
||||
@@ -413,6 +428,22 @@ def test_not_hidden(photosdb):
|
||||
assert p.hidden == False
|
||||
|
||||
|
||||
def test_visible(photosdb):
|
||||
""" test visible """
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["not_hidden"]])
|
||||
assert len(photos) == 1
|
||||
p = photos[0]
|
||||
assert p.visible
|
||||
|
||||
|
||||
def test_not_burst(photosdb):
|
||||
""" test not burst """
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["not_hidden"]])
|
||||
assert len(photos) == 1
|
||||
p = photos[0]
|
||||
assert not p.burst
|
||||
|
||||
|
||||
def test_location_1(photosdb):
|
||||
# test photo with lat/lon info
|
||||
|
||||
@@ -546,6 +577,7 @@ def test_photoinfo_intrash_1(photosdb):
|
||||
|
||||
p = photosdb.photos(uuid=[UUID_DICT["intrash"]], intrash=True)[0]
|
||||
assert p.intrash
|
||||
assert p.date_trashed.isoformat() == "2120-06-10T11:24:47.685857-05:00"
|
||||
|
||||
|
||||
def test_photoinfo_intrash_2(photosdb):
|
||||
@@ -587,6 +619,7 @@ def test_photoinfo_not_intrash(photosdb):
|
||||
|
||||
p = photosdb.photos(uuid=[UUID_DICT["not_intrash"]])[0]
|
||||
assert not p.intrash
|
||||
assert p.date_trashed is None
|
||||
|
||||
|
||||
def test_keyword_2(photosdb):
|
||||
@@ -973,7 +1006,7 @@ def test_from_to_date(photosdb):
|
||||
time.tzset()
|
||||
|
||||
photos = photosdb.photos(from_date=datetime.datetime(2018, 10, 28))
|
||||
assert len(photos) == 10
|
||||
assert len(photos) == 10
|
||||
|
||||
photos = photosdb.photos(to_date=datetime.datetime(2018, 10, 28))
|
||||
assert len(photos) == 7
|
||||
@@ -1133,3 +1166,22 @@ def test_original_filename(photosdb):
|
||||
assert photo.original_filename == ORIGINAL_FILENAME_DICT["filename"]
|
||||
photo._info["originalFilename"] = original_filename
|
||||
|
||||
|
||||
# The following tests only run on the author's personal library
|
||||
# They test things difficult to test in the test libraries
|
||||
@pytest.mark.skipif(SKIP_TEST, reason="Skip if not running on author's local machine.")
|
||||
def test_not_visible_burst(photosdb_local):
|
||||
""" test not visible and burst (needs image from local library) """
|
||||
photo = photosdb_local.get_photo(UUID_DICT_LOCAL["not_visible"])
|
||||
assert not photo.visible
|
||||
assert photo.burst
|
||||
|
||||
|
||||
@pytest.mark.skipif(SKIP_TEST, reason="Skip if not running on author's local machine.")
|
||||
def test_visible_burst(photosdb_local):
|
||||
""" test not visible and burst (needs image from local library) """
|
||||
photo = photosdb_local.get_photo(UUID_DICT_LOCAL["burst"])
|
||||
assert photo.visible
|
||||
assert photo.burst
|
||||
assert len(photo.burst_photos) == 4
|
||||
|
||||
|
||||
@@ -2,14 +2,15 @@ import os
|
||||
import pytest
|
||||
|
||||
from osxphotos._constants import _UNKNOWN_PERSON
|
||||
from osxphotos.utils import _get_os_version
|
||||
|
||||
skip_test = False if "OSXPHOTOS_TEST_EXPORT" in os.environ else True
|
||||
OS_VERSION = _get_os_version()
|
||||
SKIP_TEST = "OSXPHOTOS_TEST_EXPORT" not in os.environ or OS_VERSION[1] != "15"
|
||||
PHOTOS_DB = os.path.expanduser("~/Pictures/Photos Library.photoslibrary")
|
||||
pytestmark = pytest.mark.skipif(
|
||||
skip_test, reason="These tests only run against system photos library"
|
||||
SKIP_TEST, reason="These tests only run against system photos library"
|
||||
)
|
||||
|
||||
PHOTOS_DB = "/Users/rhet/Pictures/Photos Library.photoslibrary"
|
||||
|
||||
UUID_DICT = {
|
||||
"has_adjustments": "2B2D5434-6D31-49E2-BF47-B973D34A317B",
|
||||
"no_adjustments": "A8D646C3-89A9-4D74-8001-4EB46BA55B94",
|
||||
@@ -21,8 +22,7 @@ UUID_DICT = {
|
||||
def photosdb():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
return photosdb
|
||||
return osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
|
||||
|
||||
def test_export_default_name(photosdb):
|
||||
@@ -326,6 +326,22 @@ def test_not_hidden(photosdb):
|
||||
assert p.hidden == False
|
||||
|
||||
|
||||
def test_visible(photosdb):
|
||||
""" test visible """
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["not_hidden"]])
|
||||
assert len(photos) == 1
|
||||
p = photos[0]
|
||||
assert p.visible
|
||||
|
||||
|
||||
def test_not_burst(photosdb):
|
||||
""" test not burst """
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["not_hidden"]])
|
||||
assert len(photos) == 1
|
||||
p = photos[0]
|
||||
assert not p.burst
|
||||
|
||||
|
||||
def test_location_1(photosdb):
|
||||
# test photo with lat/lon info
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["location"]])
|
||||
@@ -417,6 +433,7 @@ def test_photos_intrash_2(photosdb):
|
||||
photos = photosdb.photos(intrash=True)
|
||||
for p in photos:
|
||||
assert p.intrash
|
||||
assert p.date_trashed.isoformat() == "2305-12-17T13:19:08.978144-07:00"
|
||||
|
||||
|
||||
def test_photos_not_intrash(photosdb):
|
||||
@@ -424,6 +441,7 @@ def test_photos_not_intrash(photosdb):
|
||||
photos = photosdb.photos(intrash=False)
|
||||
for p in photos:
|
||||
assert not p.intrash
|
||||
assert p.date_trashed is None
|
||||
|
||||
|
||||
def test_photoinfo_intrash_1(photosdb):
|
||||
|
||||
Reference in New Issue
Block a user