Bug memory leak 1047 (#1052)
* Fix for memory leak, #1047 * Refactored photos_by_uuid, AlbumInfo.asdict for speed optimization * Fix for huge crash log, #1048 * Fix for error on export #1046 * add rajscode as a contributor for bug (#1049) * update README.md [skip ci] * update .all-contributorsrc [skip ci] --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> * add wernerzj as a contributor for bug (#1050) * update README.md [skip ci] * update .all-contributorsrc [skip ci] --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> Co-authored-by: Rhet Turnbull <rturnbull@gmail.com> * Fixed all-contributors badge --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
This commit is contained in:
@@ -50,9 +50,8 @@ def sort_list_by_keys(values, sort_keys):
|
|||||||
ValueError: raised if len(values) != len(sort_keys)
|
ValueError: raised if len(values) != len(sort_keys)
|
||||||
"""
|
"""
|
||||||
if len(values) != len(sort_keys):
|
if len(values) != len(sort_keys):
|
||||||
return ValueError("values and sort_keys must have same length")
|
raise ValueError("values and sort_keys must be same length")
|
||||||
|
return [x for _, x in sorted(zip(sort_keys, values))]
|
||||||
return list(zip(*sorted(zip(sort_keys, values))))[1]
|
|
||||||
|
|
||||||
|
|
||||||
class AlbumInfoBaseClass:
|
class AlbumInfoBaseClass:
|
||||||
@@ -166,14 +165,13 @@ class AlbumInfoBaseClass:
|
|||||||
return self._owner
|
return self._owner
|
||||||
|
|
||||||
def asdict(self):
|
def asdict(self):
|
||||||
"""Return album info as a dict"""
|
"""Return album info as a dict; does not include photos"""
|
||||||
return {
|
return {
|
||||||
"uuid": self.uuid,
|
"uuid": self.uuid,
|
||||||
"creation_date": self.creation_date,
|
"creation_date": self.creation_date,
|
||||||
"start_date": self.start_date,
|
"start_date": self.start_date,
|
||||||
"end_date": self.end_date,
|
"end_date": self.end_date,
|
||||||
"owner": self.owner,
|
"owner": self.owner,
|
||||||
"photos": [p.uuid for p in self.photos],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self):
|
||||||
@@ -299,7 +297,7 @@ class AlbumInfo(AlbumInfoBaseClass):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def asdict(self):
|
def asdict(self):
|
||||||
"""Return album info as a dict"""
|
"""Return album info as a dict; does not include photos"""
|
||||||
dict_data = super().asdict()
|
dict_data = super().asdict()
|
||||||
dict_data["title"] = self.title
|
dict_data["title"] = self.title
|
||||||
dict_data["folder_names"] = self.folder_names
|
dict_data["folder_names"] = self.folder_names
|
||||||
@@ -362,14 +360,13 @@ class ImportInfo(AlbumInfoBaseClass):
|
|||||||
return self._photos
|
return self._photos
|
||||||
|
|
||||||
def asdict(self):
|
def asdict(self):
|
||||||
"""Return import info as a dict"""
|
"""Return import info as a dict; does not include photos"""
|
||||||
return {
|
return {
|
||||||
"uuid": self.uuid,
|
"uuid": self.uuid,
|
||||||
"creation_date": self.creation_date,
|
"creation_date": self.creation_date,
|
||||||
"start_date": self.start_date,
|
"start_date": self.start_date,
|
||||||
"end_date": self.end_date,
|
"end_date": self.end_date,
|
||||||
"title": self.title,
|
"title": self.title,
|
||||||
"photos": [p.uuid for p in self.photos],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def __bool__(self):
|
def __bool__(self):
|
||||||
|
|||||||
@@ -911,7 +911,8 @@ def export(
|
|||||||
|
|
||||||
# capture locals for use with ConfigOptions before changing any of them
|
# capture locals for use with ConfigOptions before changing any of them
|
||||||
locals_ = locals()
|
locals_ = locals()
|
||||||
set_crash_data("locals", locals_)
|
crash_data = locals_.copy()
|
||||||
|
set_crash_data("locals", crash_data)
|
||||||
|
|
||||||
# config expects --verbose to be named "verbose" not "verbose_flag"
|
# config expects --verbose to be named "verbose" not "verbose_flag"
|
||||||
locals_["verbose"] = verbose_flag
|
locals_["verbose"] = verbose_flag
|
||||||
@@ -2254,7 +2255,7 @@ def export_photo_to_directory(
|
|||||||
err=True,
|
err=True,
|
||||||
)
|
)
|
||||||
if tries > retry:
|
if tries > retry:
|
||||||
results.error.append((str(pathlib.Path(dest) / filename), e))
|
results.error.append((str(pathlib.Path(dest) / filename), str(e)))
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
rich_echo(
|
rich_echo(
|
||||||
|
|||||||
@@ -1521,10 +1521,10 @@ class PhotoExporter:
|
|||||||
if not options.dry_run:
|
if not options.dry_run:
|
||||||
warning_, error_ = self._write_exif_data(src, options=options)
|
warning_, error_ = self._write_exif_data(src, options=options)
|
||||||
if warning_:
|
if warning_:
|
||||||
exiftool_results.exiftool_warning.append((dest, warning_))
|
exiftool_results.exiftool_warning.append((str(dest), str(warning_)))
|
||||||
if error_:
|
if error_:
|
||||||
exiftool_results.exiftool_error.append((dest, error_))
|
exiftool_results.exiftool_error.append((str(dest), str(error_)))
|
||||||
exiftool_results.error.append((dest, error_))
|
exiftool_results.error.append((str(dest), str(error_)))
|
||||||
|
|
||||||
exiftool_results.exif_updated.append(dest)
|
exiftool_results.exif_updated.append(dest)
|
||||||
exiftool_results.to_touch.append(dest)
|
exiftool_results.to_touch.append(dest)
|
||||||
|
|||||||
@@ -517,39 +517,26 @@ class PhotoInfo:
|
|||||||
@property
|
@property
|
||||||
def person_info(self):
|
def person_info(self):
|
||||||
"""list of PersonInfo objects for person in picture"""
|
"""list of PersonInfo objects for person in picture"""
|
||||||
try:
|
return [PersonInfo(db=self._db, pk=pk) for pk in self._info["persons"]]
|
||||||
return self._personinfo
|
|
||||||
except AttributeError:
|
|
||||||
self._personinfo = [
|
|
||||||
PersonInfo(db=self._db, pk=pk) for pk in self._info["persons"]
|
|
||||||
]
|
|
||||||
return self._personinfo
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def face_info(self):
|
def face_info(self):
|
||||||
"""list of FaceInfo objects for faces in picture"""
|
"""list of FaceInfo objects for faces in picture"""
|
||||||
try:
|
try:
|
||||||
return self._faceinfo
|
faces = self._db._db_faceinfo_uuid[self._uuid]
|
||||||
except AttributeError:
|
self._faceinfo = [FaceInfo(db=self._db, pk=pk) for pk in faces]
|
||||||
try:
|
except KeyError:
|
||||||
faces = self._db._db_faceinfo_uuid[self._uuid]
|
# no faces
|
||||||
self._faceinfo = [FaceInfo(db=self._db, pk=pk) for pk in faces]
|
self._faceinfo = []
|
||||||
except KeyError:
|
return self._faceinfo
|
||||||
# no faces
|
|
||||||
self._faceinfo = []
|
|
||||||
return self._faceinfo
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def moment_info(self):
|
def moment_info(self):
|
||||||
"""Moment photo belongs to"""
|
"""Moment photo belongs to"""
|
||||||
try:
|
try:
|
||||||
return self._moment
|
return MomentInfo(db=self._db, moment_pk=self._info["momentID"])
|
||||||
except AttributeError:
|
except ValueError:
|
||||||
try:
|
return None
|
||||||
self._moment = MomentInfo(db=self._db, moment_pk=self._info["momentID"])
|
|
||||||
except ValueError:
|
|
||||||
self._moment = None
|
|
||||||
return self._moment
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def albums(self):
|
def albums(self):
|
||||||
@@ -566,65 +553,41 @@ class PhotoInfo:
|
|||||||
@property
|
@property
|
||||||
def burst_albums(self):
|
def burst_albums(self):
|
||||||
"""If photo is burst photo, list of albums it is contained in as well as any albums the key photo is contained in, otherwise returns self.albums"""
|
"""If photo is burst photo, list of albums it is contained in as well as any albums the key photo is contained in, otherwise returns self.albums"""
|
||||||
try:
|
burst_albums = list(self.albums)
|
||||||
return self._burst_albums
|
for photo in self.burst_photos:
|
||||||
except AttributeError:
|
if photo.burst_key:
|
||||||
burst_albums = list(self.albums)
|
burst_albums.extend(photo.albums)
|
||||||
for photo in self.burst_photos:
|
return list(set(burst_albums))
|
||||||
if photo.burst_key:
|
|
||||||
burst_albums.extend(photo.albums)
|
|
||||||
self._burst_albums = list(set(burst_albums))
|
|
||||||
return self._burst_albums
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def album_info(self):
|
def album_info(self):
|
||||||
"""list of AlbumInfo objects representing albums the photo is contained in"""
|
"""list of AlbumInfo objects representing albums the photo is contained in"""
|
||||||
try:
|
album_uuids = self._get_album_uuids()
|
||||||
return self._album_info
|
return [AlbumInfo(db=self._db, uuid=album) for album in album_uuids]
|
||||||
except AttributeError:
|
|
||||||
album_uuids = self._get_album_uuids()
|
|
||||||
self._album_info = [
|
|
||||||
AlbumInfo(db=self._db, uuid=album) for album in album_uuids
|
|
||||||
]
|
|
||||||
return self._album_info
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def burst_album_info(self):
|
def burst_album_info(self):
|
||||||
"""If photo is a burst photo, returns list of AlbumInfo objects representing albums the photo is contained in as well as albums the burst key photo is contained in, otherwise returns self.album_info."""
|
"""If photo is a burst photo, returns list of AlbumInfo objects representing albums the photo is contained in as well as albums the burst key photo is contained in, otherwise returns self.album_info."""
|
||||||
try:
|
burst_album_info = list(self.album_info)
|
||||||
return self._burst_album_info
|
for photo in self.burst_photos:
|
||||||
except AttributeError:
|
if photo.burst_key:
|
||||||
burst_album_info = list(self.album_info)
|
burst_album_info.extend(photo.album_info)
|
||||||
for photo in self.burst_photos:
|
return list(set(burst_album_info))
|
||||||
if photo.burst_key:
|
|
||||||
burst_album_info.extend(photo.album_info)
|
|
||||||
self._burst_album_info = list(set(burst_album_info))
|
|
||||||
return self._burst_album_info
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def import_info(self):
|
def import_info(self):
|
||||||
"""ImportInfo object representing import session for the photo or None if no import session"""
|
"""ImportInfo object representing import session for the photo or None if no import session"""
|
||||||
try:
|
return (
|
||||||
return self._import_info
|
ImportInfo(db=self._db, uuid=self._info["import_uuid"])
|
||||||
except AttributeError:
|
if self._info["import_uuid"] is not None
|
||||||
self._import_info = (
|
else None
|
||||||
ImportInfo(db=self._db, uuid=self._info["import_uuid"])
|
)
|
||||||
if self._info["import_uuid"] is not None
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
return self._import_info
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def project_info(self):
|
def project_info(self):
|
||||||
"""list of AlbumInfo objects representing projects for the photo or None if no projects"""
|
"""list of AlbumInfo objects representing projects for the photo or None if no projects"""
|
||||||
try:
|
project_uuids = self._get_album_uuids(project=True)
|
||||||
return self._project_info
|
return [ProjectInfo(db=self._db, uuid=album) for album in project_uuids]
|
||||||
except AttributeError:
|
|
||||||
project_uuids = self._get_album_uuids(project=True)
|
|
||||||
self._project_info = [
|
|
||||||
ProjectInfo(db=self._db, uuid=album) for album in project_uuids
|
|
||||||
]
|
|
||||||
return self._project_info
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def keywords(self):
|
def keywords(self):
|
||||||
@@ -638,8 +601,7 @@ class PhotoInfo:
|
|||||||
# in this case, return None so result is the same as if title had never been set (which returns NULL)
|
# in this case, return None so result is the same as if title had never been set (which returns NULL)
|
||||||
# issue #512
|
# issue #512
|
||||||
title = self._info["name"]
|
title = self._info["name"]
|
||||||
title = None if title == "" else title
|
return None if title == "" else title
|
||||||
return title
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def uuid(self):
|
def uuid(self):
|
||||||
@@ -1777,41 +1739,31 @@ class PhotoInfo:
|
|||||||
}
|
}
|
||||||
return yaml.dump(info, sort_keys=False)
|
return yaml.dump(info, sort_keys=False)
|
||||||
|
|
||||||
def asdict(self):
|
def asdict(self, shallow: bool = True) -> dict[str, Any]:
|
||||||
"""return dict representation"""
|
"""Return dict representation of PhotoInfo object.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
shallow: if True, return shallow representation (does not contain folder_info, person_info, etc.)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict representation of PhotoInfo object
|
||||||
|
|
||||||
|
Note:
|
||||||
|
The shallow representation is used internally by export as it contains only the subset of data needed for export.
|
||||||
|
"""
|
||||||
|
|
||||||
adjustments = self.adjustments.asdict() if self.adjustments else {}
|
|
||||||
album_info = [album.asdict() for album in self.album_info]
|
|
||||||
burst_album_info = [a.asdict() for a in self.burst_album_info]
|
|
||||||
burst_photos = [p.uuid for p in self.burst_photos]
|
|
||||||
comments = [comment.asdict() for comment in self.comments]
|
comments = [comment.asdict() for comment in self.comments]
|
||||||
exif_info = dataclasses.asdict(self.exif_info) if self.exif_info else {}
|
exif_info = dataclasses.asdict(self.exif_info) if self.exif_info else {}
|
||||||
face_info = [face.asdict() for face in self.face_info]
|
face_info = [face.asdict() for face in self.face_info]
|
||||||
folders = {album.title: album.folder_names for album in self.album_info}
|
folders = {album.title: album.folder_names for album in self.album_info}
|
||||||
import_info = self.import_info.asdict() if self.import_info else {}
|
|
||||||
likes = [like.asdict() for like in self.likes]
|
likes = [like.asdict() for like in self.likes]
|
||||||
person_info = [p.asdict() for p in self.person_info]
|
|
||||||
place = self.place.asdict() if self.place else {}
|
place = self.place.asdict() if self.place else {}
|
||||||
project_info = [p.asdict() for p in self.project_info]
|
|
||||||
score = dataclasses.asdict(self.score) if self.score else {}
|
score = dataclasses.asdict(self.score) if self.score else {}
|
||||||
search_info = self.search_info.asdict() if self.search_info else {}
|
|
||||||
search_info_normalized = (
|
|
||||||
self.search_info_normalized.asdict() if self.search_info_normalized else {}
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
dict_data = {
|
||||||
"adjustments": adjustments,
|
|
||||||
"album_info": album_info,
|
|
||||||
"albums": self.albums,
|
"albums": self.albums,
|
||||||
"burst_album_info": burst_album_info,
|
|
||||||
"burst_albums": self.burst_albums,
|
|
||||||
"burst_default_pick": self.burst_default_pick,
|
|
||||||
"burst_key": self.burst_key,
|
|
||||||
"burst_photos": burst_photos,
|
|
||||||
"burst_selected": self.burst_selected,
|
|
||||||
"burst": self.burst,
|
"burst": self.burst,
|
||||||
"cloud_guid": self.cloud_guid,
|
"cloud_guid": self.cloud_guid,
|
||||||
"cloud_metadata": self.cloud_metadata,
|
|
||||||
"cloud_owner_hashed_id": self.cloud_owner_hashed_id,
|
"cloud_owner_hashed_id": self.cloud_owner_hashed_id,
|
||||||
"comments": comments,
|
"comments": comments,
|
||||||
"date_added": self.date_added,
|
"date_added": self.date_added,
|
||||||
@@ -1831,7 +1783,6 @@ class PhotoInfo:
|
|||||||
"hdr": self.hdr,
|
"hdr": self.hdr,
|
||||||
"height": self.height,
|
"height": self.height,
|
||||||
"hidden": self.hidden,
|
"hidden": self.hidden,
|
||||||
"import_info": import_info,
|
|
||||||
"incloud": self.incloud,
|
"incloud": self.incloud,
|
||||||
"intrash": self.intrash,
|
"intrash": self.intrash,
|
||||||
"iscloudasset": self.iscloudasset,
|
"iscloudasset": self.iscloudasset,
|
||||||
@@ -1841,7 +1792,6 @@ class PhotoInfo:
|
|||||||
"israw": self.israw,
|
"israw": self.israw,
|
||||||
"isreference": self.isreference,
|
"isreference": self.isreference,
|
||||||
"keywords": self.keywords,
|
"keywords": self.keywords,
|
||||||
"labels_normalized": self.labels_normalized,
|
|
||||||
"labels": self.labels,
|
"labels": self.labels,
|
||||||
"latitude": self._latitude,
|
"latitude": self._latitude,
|
||||||
"library": self._db._library_path,
|
"library": self._db._library_path,
|
||||||
@@ -1857,22 +1807,17 @@ class PhotoInfo:
|
|||||||
"original_width": self.original_width,
|
"original_width": self.original_width,
|
||||||
"owner": self.owner,
|
"owner": self.owner,
|
||||||
"panorama": self.panorama,
|
"panorama": self.panorama,
|
||||||
"path_derivatives": self.path_derivatives,
|
|
||||||
"path_edited_live_photo": self.path_edited_live_photo,
|
"path_edited_live_photo": self.path_edited_live_photo,
|
||||||
"path_edited": self.path_edited,
|
"path_edited": self.path_edited,
|
||||||
"path_live_photo": self.path_live_photo,
|
"path_live_photo": self.path_live_photo,
|
||||||
"path_raw": self.path_raw,
|
"path_raw": self.path_raw,
|
||||||
"path": self.path,
|
"path": self.path,
|
||||||
"person_info": person_info,
|
|
||||||
"persons": self.persons,
|
"persons": self.persons,
|
||||||
"place": place,
|
"place": place,
|
||||||
"portrait": self.portrait,
|
"portrait": self.portrait,
|
||||||
"project_info": project_info,
|
|
||||||
"raw_original": self.raw_original,
|
"raw_original": self.raw_original,
|
||||||
"score": score,
|
"score": score,
|
||||||
"screenshot": self.screenshot,
|
"screenshot": self.screenshot,
|
||||||
"search_info_normalized": search_info_normalized,
|
|
||||||
"search_info": search_info,
|
|
||||||
"selfie": self.selfie,
|
"selfie": self.selfie,
|
||||||
"shared": self.shared,
|
"shared": self.shared,
|
||||||
"slow_mo": self.slow_mo,
|
"slow_mo": self.slow_mo,
|
||||||
@@ -1888,7 +1833,38 @@ class PhotoInfo:
|
|||||||
"width": self.width,
|
"width": self.width,
|
||||||
}
|
}
|
||||||
|
|
||||||
def json(self, indent: int | None = None, shallow: bool = False) -> str:
|
# non-shallow keys
|
||||||
|
if not shallow:
|
||||||
|
dict_data["album_info"] = [album.asdict() for album in self.album_info]
|
||||||
|
dict_data["path_derivatives"] = self.path_derivatives
|
||||||
|
dict_data["adjustments"] = (
|
||||||
|
self.adjustments.asdict() if self.adjustments else {}
|
||||||
|
)
|
||||||
|
dict_data["burst_album_info"] = [a.asdict() for a in self.burst_album_info]
|
||||||
|
dict_data["burst_albums"] = self.burst_albums
|
||||||
|
dict_data["burst_default_pick"] = self.burst_default_pick
|
||||||
|
dict_data["burst_key"] = self.burst_key
|
||||||
|
dict_data["burst_photos"] = [p.uuid for p in self.burst_photos]
|
||||||
|
dict_data["burst_selected"] = self.burst_selected
|
||||||
|
dict_data["cloud_metadata"] = self.cloud_metadata
|
||||||
|
dict_data["import_info"] = (
|
||||||
|
self.import_info.asdict() if self.import_info else {}
|
||||||
|
)
|
||||||
|
dict_data["labels_normalized"] = self.labels_normalized
|
||||||
|
dict_data["person_info"] = [p.asdict() for p in self.person_info]
|
||||||
|
dict_data["project_info"] = [p.asdict() for p in self.project_info]
|
||||||
|
dict_data["search_info"] = (
|
||||||
|
self.search_info.asdict() if self.search_info else {}
|
||||||
|
)
|
||||||
|
dict_data["search_info_normalized"] = (
|
||||||
|
self.search_info_normalized.asdict()
|
||||||
|
if self.search_info_normalized
|
||||||
|
else {}
|
||||||
|
)
|
||||||
|
|
||||||
|
return dict_data
|
||||||
|
|
||||||
|
def json(self, indent: int | None = None, shallow: bool = True) -> str:
|
||||||
"""Return JSON representation
|
"""Return JSON representation
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -1897,36 +1873,16 @@ class PhotoInfo:
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
JSON string
|
JSON string
|
||||||
|
|
||||||
|
Note:
|
||||||
|
The shallow representation is used internally by export as it contains only the subset of data needed for export.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def default(o):
|
def default(o):
|
||||||
if isinstance(o, (datetime.date, datetime.datetime)):
|
if isinstance(o, (datetime.date, datetime.datetime)):
|
||||||
return o.isoformat()
|
return o.isoformat()
|
||||||
|
|
||||||
dict_data = self.asdict()
|
dict_data = self.asdict(shallow=True) if shallow else self.asdict(shallow=False)
|
||||||
|
|
||||||
if shallow:
|
|
||||||
# delete items that are not needed for shallow JSON
|
|
||||||
# these are removed to match behavior of osxphotos < 0.59.0 (See #999, #1039)
|
|
||||||
for key in [
|
|
||||||
"adjustments",
|
|
||||||
"album_info",
|
|
||||||
"burst_album_info",
|
|
||||||
"burst_albums",
|
|
||||||
"burst_default_pick",
|
|
||||||
"burst_key",
|
|
||||||
"burst_photos",
|
|
||||||
"burst_selected",
|
|
||||||
"cloud_metadata",
|
|
||||||
"import_info",
|
|
||||||
"labels_normalized",
|
|
||||||
"path_derivatives",
|
|
||||||
"person_info",
|
|
||||||
"project_info",
|
|
||||||
"search_info_normalized",
|
|
||||||
"search_info",
|
|
||||||
]:
|
|
||||||
del dict_data[key]
|
|
||||||
|
|
||||||
for k, v in dict_data.items():
|
for k, v in dict_data.items():
|
||||||
# sort lists such as keywords so JSON is consistent
|
# sort lists such as keywords so JSON is consistent
|
||||||
@@ -1949,15 +1905,9 @@ class PhotoInfo:
|
|||||||
if isinstance(o, (datetime.date, datetime.datetime)):
|
if isinstance(o, (datetime.date, datetime.datetime)):
|
||||||
return o.isoformat()
|
return o.isoformat()
|
||||||
|
|
||||||
dict_data = self.asdict()
|
dict_data = self.asdict(shallow=True)
|
||||||
|
|
||||||
for k in [
|
for k in ["face_info", "visible"]:
|
||||||
"album_info",
|
|
||||||
"burst_album_info",
|
|
||||||
"face_info",
|
|
||||||
"person_info",
|
|
||||||
"visible",
|
|
||||||
]:
|
|
||||||
del dict_data[k]
|
del dict_data[k]
|
||||||
|
|
||||||
for k, v in dict_data.items():
|
for k, v in dict_data.items():
|
||||||
|
|||||||
@@ -516,7 +516,8 @@ class PhotosDB:
|
|||||||
@property
|
@property
|
||||||
def album_info_shared(self):
|
def album_info_shared(self):
|
||||||
"""return list of AlbumInfo objects for each shared album in the photos database
|
"""return list of AlbumInfo objects for each shared album in the photos database
|
||||||
only valid for Photos 5; on Photos <= 4, prints warning and returns empty list"""
|
only valid for Photos 5; on Photos <= 4, prints warning and returns empty list
|
||||||
|
"""
|
||||||
# if _dbalbum_details[key]["cloudownerhashedpersonid"] is not None, then it's a shared album
|
# if _dbalbum_details[key]["cloudownerhashedpersonid"] is not None, then it's a shared album
|
||||||
try:
|
try:
|
||||||
return self._album_info_shared
|
return self._album_info_shared
|
||||||
@@ -543,7 +544,8 @@ class PhotosDB:
|
|||||||
@property
|
@property
|
||||||
def albums_shared(self):
|
def albums_shared(self):
|
||||||
"""return list of shared albums found in photos database
|
"""return list of shared albums found in photos database
|
||||||
only valid for Photos 5; on Photos <= 4, prints warning and returns empty list"""
|
only valid for Photos 5; on Photos <= 4, prints warning and returns empty list
|
||||||
|
"""
|
||||||
|
|
||||||
# Could be more than one album with same name
|
# Could be more than one album with same name
|
||||||
# Right now, they are treated as same album and photos are combined from albums with same name
|
# Right now, they are treated as same album and photos are combined from albums with same name
|
||||||
@@ -3053,7 +3055,6 @@ class PhotosDB:
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# TODO: add to docs and test
|
|
||||||
def photos_by_uuid(self, uuids):
|
def photos_by_uuid(self, uuids):
|
||||||
"""Returns a list of photos with UUID in uuids.
|
"""Returns a list of photos with UUID in uuids.
|
||||||
Does not generate error if invalid or missing UUID passed.
|
Does not generate error if invalid or missing UUID passed.
|
||||||
@@ -3066,14 +3067,11 @@ class PhotosDB:
|
|||||||
Returns:
|
Returns:
|
||||||
list of PhotoInfo instance for photo with UUID matching uuid or [] if no match
|
list of PhotoInfo instance for photo with UUID matching uuid or [] if no match
|
||||||
"""
|
"""
|
||||||
photos = []
|
return [
|
||||||
for uuid in uuids:
|
PhotoInfo(db=self, uuid=uuid, info=self._dbphotos[uuid])
|
||||||
try:
|
for uuid in uuids
|
||||||
photos.append(PhotoInfo(db=self, uuid=uuid, info=self._dbphotos[uuid]))
|
if uuid in self._dbphotos
|
||||||
except KeyError:
|
]
|
||||||
# ignore missing/invlaid UUID
|
|
||||||
pass
|
|
||||||
return photos
|
|
||||||
|
|
||||||
def query(self, options: QueryOptions) -> List[PhotoInfo]:
|
def query(self, options: QueryOptions) -> List[PhotoInfo]:
|
||||||
"""Run a query against PhotosDB to extract the photos based on user supplied options
|
"""Run a query against PhotosDB to extract the photos based on user supplied options
|
||||||
|
|||||||
@@ -1497,7 +1497,7 @@ def test_json(photosdb: osxphotos.PhotosDB):
|
|||||||
def test_json_indent(photosdb: osxphotos.PhotosDB):
|
def test_json_indent(photosdb: osxphotos.PhotosDB):
|
||||||
"""Test PhotoInfo.json() with indent"""
|
"""Test PhotoInfo.json() with indent"""
|
||||||
photo = photosdb.get_photo(UUID_DICT["favorite"])
|
photo = photosdb.get_photo(UUID_DICT["favorite"])
|
||||||
photo_dict = json.loads(photo.json(indent=4))
|
photo_dict = json.loads(photo.json(indent=4, shallow=False))
|
||||||
assert photo_dict["favorite"]
|
assert photo_dict["favorite"]
|
||||||
assert "album_info" in photo_dict
|
assert "album_info" in photo_dict
|
||||||
|
|
||||||
|
|||||||
@@ -1285,7 +1285,7 @@ def test_json(photosdb: osxphotos.PhotosDB):
|
|||||||
def test_json_indent(photosdb: osxphotos.PhotosDB):
|
def test_json_indent(photosdb: osxphotos.PhotosDB):
|
||||||
"""Test PhotoInfo.json() with indent"""
|
"""Test PhotoInfo.json() with indent"""
|
||||||
photo = photosdb.get_photo(UUID_DICT["favorite"])
|
photo = photosdb.get_photo(UUID_DICT["favorite"])
|
||||||
photo_dict = json.loads(photo.json(indent=4))
|
photo_dict = json.loads(photo.json(indent=4, shallow=False))
|
||||||
assert photo_dict["favorite"]
|
assert photo_dict["favorite"]
|
||||||
assert "album_info" in photo_dict
|
assert "album_info" in photo_dict
|
||||||
|
|
||||||
@@ -1296,3 +1296,11 @@ def test_json_shallow(photosdb: osxphotos.PhotosDB):
|
|||||||
photo_dict = json.loads(photo.json(shallow=True))
|
photo_dict = json.loads(photo.json(shallow=True))
|
||||||
assert photo_dict["favorite"]
|
assert photo_dict["favorite"]
|
||||||
assert "album_info" not in photo_dict
|
assert "album_info" not in photo_dict
|
||||||
|
|
||||||
|
|
||||||
|
def test_photosdb_photos_by_uuid(photosdb: osxphotos.PhotosDB):
|
||||||
|
"""Test PhotosDB.photos_by_uuid"""
|
||||||
|
photos = photosdb.photos_by_uuid(UUID_DICT.values())
|
||||||
|
assert len(photos) == len(UUID_DICT)
|
||||||
|
for photo in photos:
|
||||||
|
assert photo.uuid in UUID_DICT.values()
|
||||||
|
|||||||
Reference in New Issue
Block a user