Added PhotoInfo.visible, PhotoInfo.date_trashed, closes #333, #334

This commit is contained in:
Rhet Turnbull
2021-01-09 10:20:13 -08:00
parent 87701822ae
commit 51b1058785
9 changed files with 167 additions and 46 deletions

View File

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

View File

@@ -1,5 +1,5 @@
""" version info """
__version__ = "0.39.11"
__version__ = "0.39.12"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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