diff --git a/osxphotos/__main__.py b/osxphotos/__main__.py index 93ec48f0..e3ec498f 100644 --- a/osxphotos/__main__.py +++ b/osxphotos/__main__.py @@ -520,10 +520,14 @@ def debug_dump(ctx, cli_obj, db, photos_library, dump, uuid): print("_dbkeywords_uuid:") pprint.pprint(photosdb._dbkeywords_uuid) elif attr == "persons": - print("_dbfaces_person:") - pprint.pprint(photosdb._dbfaces_person) print("_dbfaces_uuid:") pprint.pprint(photosdb._dbfaces_uuid) + print("_dbfaces_pk:") + pprint.pprint(photosdb._dbfaces_pk) + print("_dbpersons_pk:") + pprint.pprint(photosdb._dbpersons_pk) + print("_dbpersons_fullname:") + pprint.pprint(photosdb._dbpersons_fullname) elif attr == "photos": if uuid: for uuid_ in uuid: diff --git a/osxphotos/_version.py b/osxphotos/_version.py index 19b5e419..08182e61 100644 --- a/osxphotos/_version.py +++ b/osxphotos/_version.py @@ -1,3 +1,3 @@ """ version info """ -__version__ = "0.30.7" +__version__ = "0.30.8" diff --git a/osxphotos/photoinfo/photoinfo.py b/osxphotos/photoinfo/photoinfo.py index 7d5fbd37..79aa4a78 100644 --- a/osxphotos/photoinfo/photoinfo.py +++ b/osxphotos/photoinfo/photoinfo.py @@ -339,7 +339,7 @@ class PhotoInfo: @property def persons(self): """ list of persons in picture """ - return self._info["persons"] + return [self._db._dbpersons_pk[k]["fullname"] for k in self._info["persons"]] @property def albums(self): diff --git a/osxphotos/photosdb/photosdb.py b/osxphotos/photosdb/photosdb.py index 754d50a2..cc1bfa46 100644 --- a/osxphotos/photosdb/photosdb.py +++ b/osxphotos/photosdb/photosdb.py @@ -122,17 +122,28 @@ class PhotosDB: # currently used to get information on RAW images self._dbphotos_master = {} + # Dict with information about all persons by person PK + # key is person PK, value is dict with info about each person + # e.g. {3: {"pk": 3, "fullname": "Maria Smith"...}} + self._dbpersons_pk = {} + + # Dict with information about all persons by person fullname + # key is person PK, value is list of person PKs with fullname + # there may be more than one person PK with the same fullname + # e.g. {"Maria Smith": [1, 2]} + self._dbpersons_fullname = {} + # Dict with information about all persons/photos by uuid - # key is photo UUID, value is list of face names in that photo + # key is photo UUID, value is list of person primary keys of persons in the photo # Note: Photos 5 identifies faces even if not given a name # and those are labeled by process_database as _UNKNOWN_ - # e.g. {'1EB2B765-0765-43BA-A90C-0D0580E6172C': ['Katie', '_UNKNOWN_', 'Suzy']} + # e.g. {'1EB2B765-0765-43BA-A90C-0D0580E6172C': [1, 3, 5]} self._dbfaces_uuid = {} - # Dict with information about all persons/photos by person - # key is person name, value is list of photo UUIDs - # e.g. {'Maria': ['E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51']} - self._dbfaces_person = {} + # Dict with information about detected faces by person primary key + # key is person pk, value is list of photo UUIDs + # e.g. {3: ['E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51']} + self._dbfaces_pk = {} # Dict with information about all keywords/photos by uuid # key is photo uuid and value is list of keywords @@ -303,7 +314,13 @@ class PhotosDB: @property def persons_as_dict(self): """ return persons as dict of person, count in reverse sorted order (descending) """ - persons = {k: len(self._dbfaces_person[k]) for k in self._dbfaces_person.keys()} + persons = {} + for pk in self._dbfaces_pk: + fullname = self._dbpersons_pk[pk]["fullname"] + try: + persons[fullname] += len(self._dbfaces_pk[pk]) + except KeyError: + persons[fullname] = len(self._dbfaces_pk[pk]) persons = dict(sorted(persons.items(), key=lambda kv: kv[1], reverse=True)) return persons @@ -352,7 +369,7 @@ class PhotosDB: @property def persons(self): """ return list of persons found in photos database """ - persons = self._dbfaces_person.keys() + persons = {self._dbpersons_pk[k]["fullname"] for k in self._dbfaces_pk} return list(persons) @property @@ -536,22 +553,77 @@ class PhotosDB: (conn, c) = _open_sql_file(self._tmp_db) - # Look for all combinations of persons and pictures + # get info to associate persons with photos + # then get detected faces in each photo and link to persons c.execute( - """ select RKPerson.name, RKVersion.uuid from RKFace, RKPerson, RKVersion, RKMaster - where RKFace.personID = RKperson.modelID and RKVersion.modelId = RKFace.ImageModelId - and RKVersion.masterUuid = RKMaster.uuid + """ SELECT + RKPerson.modelID, + RKPerson.uuid, + RKPerson.name, + RKPerson.faceCount, + RKPerson.displayName + FROM RKPerson """ ) + + # 0 RKPerson.modelID, + # 1 RKPerson.uuid, + # 2 RKPerson.name, + # 3 RKPerson.faceCount, + # 4 RKPerson.displayName + for person in c: - if person[0] is None: - continue - if not person[1] in self._dbfaces_uuid: - self._dbfaces_uuid[person[1]] = [] - if not person[0] in self._dbfaces_person: - self._dbfaces_person[person[0]] = [] - self._dbfaces_uuid[person[1]].append(person[0]) - self._dbfaces_person[person[0]].append(person[1]) + pk = person[0] + fullname = person[2] if person[2] is not None else _UNKNOWN_PERSON + self._dbpersons_pk[pk] = { + "pk": pk, + "uuid": person[1], + "fullname": fullname, + "facecount": person[3], + "keyface": None, + "displayname": person[4], + } + try: + self._dbpersons_fullname[fullname].append(pk) + except KeyError: + self._dbpersons_fullname[fullname] = [pk] + + # get information on detected faces + c.execute( + """ SELECT + RKPerson.modelID, + RKVersion.uuid + FROM + RKFace, RKPerson, RKVersion, RKMaster + WHERE + RKFace.personID = RKperson.modelID AND + RKVersion.modelId = RKFace.ImageModelId AND + RKVersion.masterUuid = RKMaster.uuid + """ + ) + + # 0 RKPerson.modelID + # 1 RKVersion.uuid + + for face in c: + pk = face[0] + uuid = face[1] + try: + self._dbfaces_uuid[uuid].append(pk) + except KeyError: + self._dbfaces_uuid[uuid] = [pk] + + try: + self._dbfaces_pk[pk].append(uuid) + except KeyError: + self._dbfaces_pk[pk] = [uuid] + + if _debug(): + logging.debug(f"Finished walking through persons") + logging.debug(pformat(self._dbpersons_pk)) + logging.debug(pformat(self._dbpersons_fullname)) + logging.debug(pformat(self._dbfaces_pk)) + logging.debug(pformat(self._dbfaces_uuid)) # Get info on albums c.execute( @@ -1233,8 +1305,8 @@ class PhotosDB: logging.debug("Faces (_dbfaces_uuid):") logging.debug(pformat(self._dbfaces_uuid)) - logging.debug("Faces by person (_dbfaces_person):") - logging.debug(pformat(self._dbfaces_person)) + logging.debug("Persons (_dbpersons_pk):") + logging.debug(pformat(self._dbpersons_pk)) logging.debug("Keywords by uuid (_dbkeywords_uuid):") logging.debug(pformat(self._dbkeywords_uuid)) @@ -1295,8 +1367,12 @@ class PhotosDB: return folders def _process_database5(self): - """ process the Photos database to extract info """ - """ works on Photos version >= 5.0 """ + """ process the Photos database to extract info + works on Photos version >= 5.0 + + This is a big hairy 700 line function that should probably be refactored + but it works so don't touch it. + """ if _debug(): logging.debug(f"_process_database5") @@ -1310,26 +1386,76 @@ class PhotosDB: if _debug(): logging.debug(f"Getting information about persons") + # get info to associate persons with photos + # then get detected faces in each photo and link to persons c.execute( - "SELECT ZPERSON.ZFULLNAME, ZGENERICASSET.ZUUID " - "FROM ZPERSON, ZDETECTEDFACE, ZGENERICASSET " - "WHERE ZDETECTEDFACE.ZPERSON = ZPERSON.Z_PK AND ZDETECTEDFACE.ZASSET = ZGENERICASSET.Z_PK " + """ SELECT + ZPERSON.Z_PK, + ZPERSON.ZPERSONUUID, + ZPERSON.ZFULLNAME, + ZPERSON.ZFACECOUNT, + ZPERSON.ZKEYFACE, + ZPERSON.ZDISPLAYNAME + FROM ZPERSON + """ ) + + # 0 ZPERSON.Z_PK, + # 1 ZPERSON.ZPERSONUUID, + # 2 ZPERSON.ZFULLNAME, + # 3 ZPERSON.ZFACECOUNT, + # 4 ZPERSON.ZKEYFACE, + # 5 ZPERSON.ZDISPLAYNAME, + for person in c: - if person[0] is None: - continue - person_name = person[0] if person[0] != "" else _UNKNOWN_PERSON - if not person[1] in self._dbfaces_uuid: - self._dbfaces_uuid[person[1]] = [] - if not person_name in self._dbfaces_person: - self._dbfaces_person[person_name] = [] - self._dbfaces_uuid[person[1]].append(person_name) - self._dbfaces_person[person_name].append(person[1]) + pk = person[0] + fullname = person[2] if person[2] != "" else _UNKNOWN_PERSON + self._dbpersons_pk[pk] = { + "pk": pk, + "uuid": person[1], + "fullname": fullname, + "facecount": person[3], + "keyface": person[4], + "displayname": person[5], + } + try: + self._dbpersons_fullname[fullname].append(pk) + except KeyError: + self._dbpersons_fullname[fullname] = [pk] + + # get information on detected faces + c.execute( + """ SELECT + ZPERSON.Z_PK, + ZGENERICASSET.ZUUID + FROM ZPERSON, ZDETECTEDFACE, ZGENERICASSET + WHERE ZDETECTEDFACE.ZPERSON = ZPERSON.Z_PK AND + ZDETECTEDFACE.ZASSET = ZGENERICASSET.Z_PK; + """ + ) + + # 0 ZPERSON.Z_PK, + # 1 ZGENERICASSET.ZUUID, + + for face in c: + pk = face[0] + uuid = face[1] + try: + self._dbfaces_uuid[uuid].append(pk) + except KeyError: + self._dbfaces_uuid[uuid] = [pk] + + try: + self._dbfaces_pk[pk].append(uuid) + except KeyError: + self._dbfaces_pk[pk] = [uuid] if _debug(): logging.debug(f"Finished walking through persons") - logging.debug(pformat(self._dbfaces_person)) - logging.debug(self._dbfaces_uuid) + logging.debug(pformat(self._dbpersons_pk)) + logging.debug(pformat(self._dbpersons_fullname)) + logging.debug(pformat(self._dbfaces_pk)) + logging.debug(pformat(self._dbfaces_uuid)) # get details about albums c.execute( @@ -1921,8 +2047,8 @@ class PhotosDB: logging.debug("Faces (_dbfaces_uuid):") logging.debug(pformat(self._dbfaces_uuid)) - logging.debug("Faces by person (_dbfaces_person):") - logging.debug(pformat(self._dbfaces_person)) + logging.debug("Persons (_dbpersons_pk):") + logging.debug(pformat(self._dbpersons_pk)) logging.debug("Keywords by uuid (_dbkeywords_uuid):") logging.debug(pformat(self._dbkeywords_uuid)) @@ -2283,8 +2409,9 @@ class PhotosDB: if persons: person_set = set() for person in persons: - if person in self._dbfaces_person: - person_set.update(self._dbfaces_person[person]) + if person in self._dbpersons_fullname: + for pk in self._dbpersons_fullname[person]: + person_set.update(self._dbfaces_pk[pk]) else: logging.debug(f"Could not find person '{person}' in database") photos_sets.append(person_set) diff --git a/tests/test_10_12_6.py b/tests/test_10_12_6.py index f0dda984..ffba6898 100644 --- a/tests/test_10_12_6.py +++ b/tests/test_10_12_6.py @@ -1,7 +1,6 @@ import pytest -# TODO: put some of this code into a pre-function -# TODO: All the hardocded uuids, etc in test functions should be in some sort of config +from osxphotos._constants import _UNKNOWN_PERSON PHOTOS_DB = "./tests/Test-10.12.6.photoslibrary/database/photos.db" KEYWORDS = [ @@ -15,7 +14,7 @@ KEYWORDS = [ "UK", "United Kingdom", ] -PERSONS = ["Katie", "Suzy", "Maria"] +PERSONS = ["Katie", "Suzy", "Maria", _UNKNOWN_PERSON] ALBUMS = ["Pumpkin Farm", "AlbumInFolder"] KEYWORDS_DICT = { "Kids": 4, @@ -28,7 +27,7 @@ KEYWORDS_DICT = { "UK": 1, "United Kingdom": 1, } -PERSONS_DICT = {"Katie": 3, "Suzy": 2, "Maria": 1} +PERSONS_DICT = {"Katie": 3, "Suzy": 2, "Maria": 1, _UNKNOWN_PERSON: 1} ALBUM_DICT = {"Pumpkin Farm": 3, "AlbumInFolder": 1} diff --git a/tests/test_catalina_10_15_5.py b/tests/test_catalina_10_15_5.py index 971aed9b..04f54f1d 100644 --- a/tests/test_catalina_10_15_5.py +++ b/tests/test_catalina_10_15_5.py @@ -560,7 +560,14 @@ def test_album_folder_name(): photos = photosdb.photos(albums=["Pumpkin Farm"]) assert sorted(p.uuid for p in photos) == sorted(UUID_PUMPKIN_FARM) +def test_multi_person(): + import osxphotos + photosdb = osxphotos.PhotosDB(PHOTOS_DB) + photos = photosdb.photos(persons=["Katie", "Suzy"]) + + assert len(photos) == 3 + def test_get_db_path(): import osxphotos diff --git a/tests/test_highsierra.py b/tests/test_highsierra.py index 05064694..789ef3ba 100644 --- a/tests/test_highsierra.py +++ b/tests/test_highsierra.py @@ -1,6 +1,6 @@ import pytest -# TODO: put some of this code into a pre-function +from osxphotos._constants import _UNKNOWN_PERSON PHOTOS_DB = "./tests/Test-10.13.6.photoslibrary/database/photos.db" KEYWORDS = [ @@ -14,7 +14,7 @@ KEYWORDS = [ "UK", "United Kingdom", ] -PERSONS = ["Katie", "Suzy", "Maria"] +PERSONS = ["Katie", "Suzy", "Maria", _UNKNOWN_PERSON] ALBUMS = ["Pumpkin Farm", "AlbumInFolder", "TestAlbum"] KEYWORDS_DICT = { "Kids": 4, @@ -27,7 +27,7 @@ KEYWORDS_DICT = { "UK": 1, "United Kingdom": 1, } -PERSONS_DICT = {"Katie": 3, "Suzy": 2, "Maria": 1} +PERSONS_DICT = {"Katie": 3, "Suzy": 2, "Maria": 1, _UNKNOWN_PERSON: 1} ALBUM_DICT = {"Pumpkin Farm": 3, "TestAlbum": 1, "AlbumInFolder": 1} diff --git a/tests/test_mojave_10_14_5.py b/tests/test_mojave_10_14_5.py index a2d6de85..31a24aec 100644 --- a/tests/test_mojave_10_14_5.py +++ b/tests/test_mojave_10_14_5.py @@ -1,6 +1,7 @@ import pytest -# TODO: put some of this code into a pre-function + +from osxphotos._constants import _UNKNOWN_PERSON PHOTOS_DB = "./tests/Test-10.14.5.photoslibrary/database/photos.db" KEYWORDS = [ @@ -14,7 +15,7 @@ KEYWORDS = [ "UK", "United Kingdom", ] -PERSONS = ["Katie", "Suzy", "Maria"] +PERSONS = ["Katie", "Suzy", "Maria", _UNKNOWN_PERSON] ALBUMS = ["Pumpkin Farm"] KEYWORDS_DICT = { "Kids": 4, @@ -27,7 +28,7 @@ KEYWORDS_DICT = { "UK": 1, "United Kingdom": 1, } -PERSONS_DICT = {"Katie": 3, "Suzy": 2, "Maria": 1} +PERSONS_DICT = {"Katie": 3, "Suzy": 2, "Maria": 1, _UNKNOWN_PERSON: 1} ALBUM_DICT = {"Pumpkin Farm": 3} diff --git a/tests/test_mojave_10_14_6.py b/tests/test_mojave_10_14_6.py index ea397937..5a25cf7a 100644 --- a/tests/test_mojave_10_14_6.py +++ b/tests/test_mojave_10_14_6.py @@ -1,6 +1,6 @@ import pytest -# TODO: put some of this code into a pre-function +from osxphotos._constants import _UNKNOWN_PERSON PHOTOS_DB = "./tests/Test-10.14.6.photoslibrary/database/photos.db" PHOTOS_DB_PATH = "/Test-10.14.6.photoslibrary/database/photos.db" @@ -17,7 +17,7 @@ KEYWORDS = [ "UK", "United Kingdom", ] -PERSONS = ["Katie", "Suzy", "Maria"] +PERSONS = ["Katie", "Suzy", "Maria", _UNKNOWN_PERSON] ALBUMS = ["Pumpkin Farm", "AlbumInFolder", "Test Album", "Test Album (1)"] KEYWORDS_DICT = { "Kids": 4, @@ -30,7 +30,7 @@ KEYWORDS_DICT = { "UK": 1, "United Kingdom": 1, } -PERSONS_DICT = {"Katie": 3, "Suzy": 2, "Maria": 1} +PERSONS_DICT = {"Katie": 3, "Suzy": 2, "Maria": 1, _UNKNOWN_PERSON: 1} ALBUM_DICT = { "Pumpkin Farm": 3, "AlbumInFolder": 1,