Implemented PersonInfo, closes #181
This commit is contained in:
@@ -1,3 +1,3 @@
|
||||
""" version info """
|
||||
|
||||
__version__ = "0.30.10"
|
||||
__version__ = "0.30.11"
|
||||
|
||||
@@ -55,7 +55,9 @@ class AlbumInfo:
|
||||
# so need to build photo list one a time
|
||||
# sort uuids by sort order
|
||||
sorted_uuid = sorted(zip(sort_order, uuid))
|
||||
self._photos = [self._db.photos(uuid=[uuid])[0] for _, uuid in sorted_uuid]
|
||||
self._photos = [
|
||||
self._db.photos(uuid=[uuid])[0] for _, uuid in sorted_uuid
|
||||
]
|
||||
else:
|
||||
self._photos = []
|
||||
return self._photos
|
||||
|
||||
77
osxphotos/personinfo.py
Normal file
77
osxphotos/personinfo.py
Normal file
@@ -0,0 +1,77 @@
|
||||
""" PhotoInfo methods to expose info about person in the Photos library """
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
|
||||
class PersonInfo:
|
||||
""" Info about a person in the Photos library
|
||||
"""
|
||||
|
||||
def __init__(self, db=None, pk=None):
|
||||
""" Creates a new PersonInfo instance
|
||||
|
||||
Arguments:
|
||||
db: instance of PhotosDB object
|
||||
pk: primary key value of person to initialize PersonInfo with
|
||||
|
||||
Returns:
|
||||
PersonInfo instance
|
||||
"""
|
||||
self._db = db
|
||||
self._pk = pk
|
||||
|
||||
person = self._db._dbpersons_pk[pk]
|
||||
self.uuid = person["uuid"]
|
||||
self.name = person["fullname"]
|
||||
self.display_name = person["displayname"]
|
||||
self.keyface = person["keyface"]
|
||||
self.facecount = person["facecount"]
|
||||
|
||||
@property
|
||||
def keyphoto(self):
|
||||
try:
|
||||
return self._keyphoto
|
||||
except AttributeError:
|
||||
person = self._db._dbpersons_pk[self._pk]
|
||||
if person["photo_uuid"]:
|
||||
try:
|
||||
key_photo = self._db.get_photo(person["photo_uuid"])
|
||||
except IndexError:
|
||||
key_photo = None
|
||||
else:
|
||||
key_photo = None
|
||||
self._keyphoto = key_photo
|
||||
return self._keyphoto
|
||||
|
||||
@property
|
||||
def photos(self):
|
||||
""" Returns list of PhotoInfo objects associated with this person """
|
||||
return self._db.photos_by_uuid(self._db._dbfaces_pk[self._pk])
|
||||
|
||||
def json(self):
|
||||
""" Returns JSON representation of class instance """
|
||||
keyphoto = self.keyphoto.uuid if self.keyphoto is not None else None
|
||||
person = {
|
||||
"uuid": self.uuid,
|
||||
"name": self.name,
|
||||
"displayname": self.display_name,
|
||||
"keyface": self.keyface,
|
||||
"facecount": self.facecount,
|
||||
"keyphoto": keyphoto,
|
||||
}
|
||||
return json.dumps(person)
|
||||
|
||||
def __str__(self):
|
||||
return f"PersonInfo(name={self.name}, display_name={self.display_name}, uuid={self.uuid}, facecount={self.facecount})"
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, type(self)):
|
||||
return False
|
||||
|
||||
return all(
|
||||
getattr(self, field) == getattr(other, field) for field in ["_db", "_pk"]
|
||||
)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
@@ -5,6 +5,7 @@ import os
|
||||
|
||||
from ..exiftool import ExifTool, get_exiftool_path
|
||||
|
||||
|
||||
@property
|
||||
def exiftool(self):
|
||||
""" Returns an ExifTool object for the photo
|
||||
@@ -26,8 +27,9 @@ def exiftool(self):
|
||||
except FileNotFoundError:
|
||||
# get_exiftool_path raises FileNotFoundError if exiftool not found
|
||||
exiftool = None
|
||||
logging.warning(f"exiftool not in path; download and install from https://exiftool.org/")
|
||||
logging.warning(
|
||||
f"exiftool not in path; download and install from https://exiftool.org/"
|
||||
)
|
||||
|
||||
self._exiftool = exiftool
|
||||
return self._exiftool
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ from .._constants import (
|
||||
_PHOTOS_5_SHARED_PHOTO_PATH,
|
||||
)
|
||||
from ..albuminfo import AlbumInfo
|
||||
from ..personinfo import PersonInfo
|
||||
from ..phototemplate import PhotoTemplate
|
||||
from ..placeinfo import PlaceInfo4, PlaceInfo5
|
||||
from ..utils import _debug, _get_resource_loc, findfiles, get_preferred_uti_extension
|
||||
@@ -339,7 +340,18 @@ class PhotoInfo:
|
||||
@property
|
||||
def persons(self):
|
||||
""" list of persons in picture """
|
||||
return [self._db._dbpersons_pk[k]["fullname"] for k in self._info["persons"]]
|
||||
return [self._db._dbpersons_pk[pk]["fullname"] for pk in self._info["persons"]]
|
||||
|
||||
@property
|
||||
def person_info(self):
|
||||
""" list of PersonInfo objects for person in picture """
|
||||
try:
|
||||
return self._personinfo
|
||||
except AttributeError:
|
||||
self._personinfo = [
|
||||
PersonInfo(db=self._db, pk=pk) for pk in self._info["persons"]
|
||||
]
|
||||
return self._personinfo
|
||||
|
||||
@property
|
||||
def albums(self):
|
||||
|
||||
@@ -34,7 +34,7 @@ def _process_exifinfo_5(photosdb):
|
||||
photosdb: PhotosDB instance """
|
||||
|
||||
db = photosdb._tmp_db
|
||||
|
||||
|
||||
(conn, cursor) = _open_sql_file(db)
|
||||
|
||||
result = conn.execute(
|
||||
|
||||
@@ -34,6 +34,7 @@ from .._constants import (
|
||||
)
|
||||
from .._version import __version__
|
||||
from ..albuminfo import AlbumInfo, FolderInfo
|
||||
from ..personinfo import PersonInfo
|
||||
from ..photoinfo import PhotoInfo
|
||||
from ..utils import (
|
||||
_check_file_exists,
|
||||
@@ -44,7 +45,6 @@ from ..utils import (
|
||||
get_last_library_path,
|
||||
)
|
||||
|
||||
|
||||
# TODO: Add test for imageTimeZoneOffsetSeconds = None
|
||||
# TODO: Add test for __str__
|
||||
# TODO: Add special albums and magic albums
|
||||
@@ -87,6 +87,7 @@ class PhotosDB:
|
||||
|
||||
# set up the data structures used to store all the Photo database info
|
||||
|
||||
# TODO: I don't think these keywords flags are actually used
|
||||
# if True, will treat persons as keywords when exporting metadata
|
||||
self.use_persons_as_keywords = False
|
||||
|
||||
@@ -372,6 +373,17 @@ class PhotosDB:
|
||||
persons = {self._dbpersons_pk[k]["fullname"] for k in self._dbfaces_pk}
|
||||
return list(persons)
|
||||
|
||||
@property
|
||||
def person_info(self):
|
||||
""" return list of PersonInfo objects for each person in the photos database """
|
||||
try:
|
||||
return self._person_info
|
||||
except AttributeError:
|
||||
self._person_info = [
|
||||
PersonInfo(db=self, pk=pk) for pk in self._dbpersons_pk
|
||||
]
|
||||
return self._person_info
|
||||
|
||||
@property
|
||||
def folder_info(self):
|
||||
""" return list FolderInfo objects representing top-level folders in the photos database """
|
||||
@@ -561,7 +573,8 @@ class PhotosDB:
|
||||
RKPerson.uuid,
|
||||
RKPerson.name,
|
||||
RKPerson.faceCount,
|
||||
RKPerson.displayName
|
||||
RKPerson.displayName,
|
||||
RKPerson.representativeFaceId
|
||||
FROM RKPerson
|
||||
"""
|
||||
)
|
||||
@@ -571,6 +584,7 @@ class PhotosDB:
|
||||
# 2 RKPerson.name,
|
||||
# 3 RKPerson.faceCount,
|
||||
# 4 RKPerson.displayName
|
||||
# 5 RKPerson.representativeFaceId
|
||||
|
||||
for person in c:
|
||||
pk = person[0]
|
||||
@@ -580,14 +594,43 @@ class PhotosDB:
|
||||
"uuid": person[1],
|
||||
"fullname": fullname,
|
||||
"facecount": person[3],
|
||||
"keyface": None,
|
||||
"keyface": person[5],
|
||||
"displayname": person[4],
|
||||
"photo_uuid": None,
|
||||
"keyface_uuid": None,
|
||||
}
|
||||
try:
|
||||
self._dbpersons_fullname[fullname].append(pk)
|
||||
except KeyError:
|
||||
self._dbpersons_fullname[fullname] = [pk]
|
||||
|
||||
# get info on key face
|
||||
c.execute(
|
||||
""" SELECT
|
||||
RKPerson.modelID,
|
||||
RKPerson.representativeFaceId,
|
||||
RKVersion.uuid,
|
||||
RKFace.uuid
|
||||
FROM RKPerson, RKFace, RKVersion
|
||||
WHERE
|
||||
RKFace.modelId = RKPerson.representativeFaceId AND
|
||||
RKVersion.modelId = RKFace.ImageModelId
|
||||
"""
|
||||
)
|
||||
|
||||
# 0 RKPerson.modelID,
|
||||
# 1 RKPerson.representativeFaceId
|
||||
# 2 RKVersion.uuid,
|
||||
# 3 RKFace.uuid
|
||||
|
||||
for person in c:
|
||||
pk = person[0]
|
||||
try:
|
||||
self._dbpersons_pk[pk]["photo_uuid"] = person[2]
|
||||
self._dbpersons_pk[pk]["keyface_uuid"] = person[3]
|
||||
except KeyError:
|
||||
logging.debug(f"Unexpected KeyError _dbpersons_pk[{pk}]")
|
||||
|
||||
# get information on detected faces
|
||||
c.execute(
|
||||
""" SELECT
|
||||
@@ -632,8 +675,8 @@ class PhotosDB:
|
||||
RKVersion.uuid,
|
||||
RKCustomSortOrder.orderNumber
|
||||
FROM RKVersion
|
||||
JOIN RKCustomSortOrder on RKCustomSortOrder.objectUuid = RKVersion.uuid
|
||||
JOIN RKAlbum on RKAlbum.uuid = RKCustomSortOrder.containerUuid
|
||||
JOIN RKCustomSortOrder on RKCustomSortOrder.objectUuid = RKVersion.uuid
|
||||
JOIN RKAlbum on RKAlbum.uuid = RKCustomSortOrder.containerUuid
|
||||
"""
|
||||
)
|
||||
|
||||
@@ -1417,7 +1460,7 @@ class PhotosDB:
|
||||
# 2 ZPERSON.ZFULLNAME,
|
||||
# 3 ZPERSON.ZFACECOUNT,
|
||||
# 4 ZPERSON.ZKEYFACE,
|
||||
# 5 ZPERSON.ZDISPLAYNAME,
|
||||
# 5 ZPERSON.ZDISPLAYNAME
|
||||
|
||||
for person in c:
|
||||
pk = person[0]
|
||||
@@ -1429,12 +1472,41 @@ class PhotosDB:
|
||||
"facecount": person[3],
|
||||
"keyface": person[4],
|
||||
"displayname": person[5],
|
||||
"photo_uuid": None,
|
||||
"keyface_uuid": None,
|
||||
}
|
||||
try:
|
||||
self._dbpersons_fullname[fullname].append(pk)
|
||||
except KeyError:
|
||||
self._dbpersons_fullname[fullname] = [pk]
|
||||
|
||||
# get info on keyface -- some photos have null keyface so can't do a single query
|
||||
# (at least not with my SQL skills)
|
||||
c.execute(
|
||||
""" SELECT
|
||||
ZPERSON.Z_PK,
|
||||
ZPERSON.ZKEYFACE,
|
||||
ZGENERICASSET.ZUUID,
|
||||
ZDETECTEDFACE.ZUUID
|
||||
FROM ZPERSON, ZDETECTEDFACE, ZGENERICASSET
|
||||
WHERE ZDETECTEDFACE.Z_PK = ZPERSON.ZKEYFACE AND
|
||||
ZDETECTEDFACE.ZASSET = ZGENERICASSET.Z_PK
|
||||
"""
|
||||
)
|
||||
|
||||
# 0 ZPERSON.Z_PK,
|
||||
# 1 ZPERSON.ZKEYFACE,
|
||||
# 2 ZGENERICASSET.ZUUID,
|
||||
# 3 ZDETECTEDFACE.ZUUID
|
||||
|
||||
for person in c:
|
||||
pk = person[0]
|
||||
try:
|
||||
self._dbpersons_pk[pk]["photo_uuid"] = person[2]
|
||||
self._dbpersons_pk[pk]["keyface_uuid"] = person[3]
|
||||
except KeyError:
|
||||
logging.debug(f"Unexpected KeyError _dbpersons_pk[{pk}]")
|
||||
|
||||
# get information on detected faces
|
||||
c.execute(
|
||||
""" SELECT
|
||||
@@ -1442,7 +1514,7 @@ class PhotosDB:
|
||||
ZGENERICASSET.ZUUID
|
||||
FROM ZPERSON, ZDETECTEDFACE, ZGENERICASSET
|
||||
WHERE ZDETECTEDFACE.ZPERSON = ZPERSON.Z_PK AND
|
||||
ZDETECTEDFACE.ZASSET = ZGENERICASSET.Z_PK;
|
||||
ZDETECTEDFACE.ZASSET = ZGENERICASSET.Z_PK
|
||||
"""
|
||||
)
|
||||
|
||||
@@ -1474,7 +1546,7 @@ class PhotosDB:
|
||||
""" SELECT
|
||||
ZGENERICALBUM.ZUUID,
|
||||
ZGENERICASSET.ZUUID,
|
||||
Z_26ASSETS.Z_FOK_34ASSETS
|
||||
Z_26ASSETS.Z_FOK_34ASSETS
|
||||
FROM ZGENERICASSET
|
||||
JOIN Z_26ASSETS ON Z_26ASSETS.Z_34ASSETS = ZGENERICASSET.Z_PK
|
||||
JOIN ZGENERICALBUM ON ZGENERICALBUM.Z_PK = Z_26ASSETS.Z_26ALBUMS
|
||||
@@ -1483,7 +1555,7 @@ class PhotosDB:
|
||||
|
||||
# 0 ZGENERICALBUM.ZUUID,
|
||||
# 1 ZGENERICASSET.ZUUID,
|
||||
# 2 Z_26ASSETS.Z_FOK_34ASSETS
|
||||
# 2 Z_26ASSETS.Z_FOK_34ASSETS
|
||||
|
||||
for album in c:
|
||||
# store by uuid in _dbalbums_uuid and by album in _dbalbums_album
|
||||
@@ -1633,15 +1705,15 @@ class PhotosDB:
|
||||
ZGENERICASSET.ZCLOUDBATCHPUBLISHDATE,
|
||||
ZGENERICASSET.ZKIND,
|
||||
ZGENERICASSET.ZUNIFORMTYPEIDENTIFIER,
|
||||
ZGENERICASSET.ZAVALANCHEUUID,
|
||||
ZGENERICASSET.ZAVALANCHEPICKTYPE,
|
||||
ZGENERICASSET.ZAVALANCHEUUID,
|
||||
ZGENERICASSET.ZAVALANCHEPICKTYPE,
|
||||
ZGENERICASSET.ZKINDSUBTYPE,
|
||||
ZGENERICASSET.ZCUSTOMRENDEREDVALUE,
|
||||
ZADDITIONALASSETATTRIBUTES.ZCAMERACAPTUREDEVICE,
|
||||
ZGENERICASSET.ZCLOUDASSETGUID,
|
||||
ZADDITIONALASSETATTRIBUTES.ZREVERSELOCATIONDATA,
|
||||
ZGENERICASSET.ZMOMENT,
|
||||
ZADDITIONALASSETATTRIBUTES.ZORIGINALRESOURCECHOICE,
|
||||
ZADDITIONALASSETATTRIBUTES.ZORIGINALRESOURCECHOICE,
|
||||
ZGENERICASSET.ZTRASHEDSTATE,
|
||||
ZGENERICASSET.ZHEIGHT,
|
||||
ZGENERICASSET.ZWIDTH,
|
||||
@@ -2437,7 +2509,11 @@ class PhotosDB:
|
||||
for person in persons:
|
||||
if person in self._dbpersons_fullname:
|
||||
for pk in self._dbpersons_fullname[person]:
|
||||
person_set.update(self._dbfaces_pk[pk])
|
||||
try:
|
||||
person_set.update(self._dbfaces_pk[pk])
|
||||
except KeyError:
|
||||
# some persons have zero photos so they won't be in _dbfaces_pk
|
||||
pass
|
||||
else:
|
||||
logging.debug(f"Could not find person '{person}' in database")
|
||||
photos_sets.append(person_set)
|
||||
@@ -2476,6 +2552,42 @@ class PhotosDB:
|
||||
|
||||
return photoinfo
|
||||
|
||||
def get_photo(self, uuid):
|
||||
""" Returns a single photo matching uuid
|
||||
|
||||
Arguments:
|
||||
uuid: the UUID of photo to get
|
||||
|
||||
Returns:
|
||||
PhotoInfo instance for photo with UUID matching uuid or None if no match
|
||||
"""
|
||||
try:
|
||||
return PhotoInfo(db=self, uuid=uuid, info=self._dbphotos[uuid])
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
# TODO: add to docs and test
|
||||
def photos_by_uuid(self, uuids):
|
||||
""" Returns a list of photos with UUID in uuids.
|
||||
Does not generate error if invalid or missing UUID passed.
|
||||
This is faster than using PhotosDB.photos if you have list of UUIDs.
|
||||
Returns photos regardless of intrash state.
|
||||
|
||||
Arguments:
|
||||
uuid: list of UUIDs of photos to get
|
||||
|
||||
Returns:
|
||||
list of PhotoInfo instance for photo with UUID matching uuid or [] if no match
|
||||
"""
|
||||
photos = []
|
||||
for uuid in uuids:
|
||||
try:
|
||||
photos.append(PhotoInfo(db=self, uuid=uuid, info=self._dbphotos[uuid]))
|
||||
except KeyError:
|
||||
# ignore missing/invlaid UUID
|
||||
pass
|
||||
return photos
|
||||
|
||||
def __repr__(self):
|
||||
return f"osxphotos.{self.__class__.__name__}(dbfile='{self.db_path}')"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user