Compare commits
6 Commits
v0.59.3
...
RhetTbull-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1a71bba1e | ||
|
|
0c85298c03 | ||
|
|
b4b58d3b00 | ||
|
|
ed543aa2d0 | ||
|
|
1b0c91db97 | ||
|
|
dd3914328b |
@@ -530,7 +530,8 @@
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/2597142?v=4",
|
||||
"profile": "https://github.com/pekingduck",
|
||||
"contributions": [
|
||||
"ideas"
|
||||
"ideas",
|
||||
"bug"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
strategy:
|
||||
max-parallel: 4
|
||||
matrix:
|
||||
os: [macos-latest]
|
||||
os: [macos-latest-xl]
|
||||
python-version: ['3.9', '3.10', '3.11']
|
||||
|
||||
steps:
|
||||
|
||||
@@ -23,6 +23,7 @@ In addition to a command line interface, OSXPhotos provides a access to a Python
|
||||
* [CommentInfo](#commentinfo)
|
||||
* [LikeInfo](#likeinfo)
|
||||
* [AdjustmentsInfo](#adjustmentsinfo)
|
||||
* [PhotoTables](#phototables)
|
||||
* [Raw Photos](#raw-photos)
|
||||
* [Template System](#template-system)
|
||||
* [ExifTool](#exiftoolExifTool)
|
||||
@@ -1395,6 +1396,12 @@ Returns a unique fingerprint for the original photo file. This is a hash of the
|
||||
|
||||
Returns a unique digest of the photo's properties and metadata; useful for detecting changes in any property/metadata of the photo.
|
||||
|
||||
#### `tables()`
|
||||
|
||||
Returns a PhotoTables object which provides access to the underlying SQLite database tables for the photo.
|
||||
See [PhotoTables](#phototables) for more details. This is useful for debugging or developing new features but
|
||||
is not intended for general use.
|
||||
|
||||
#### `json()`
|
||||
|
||||
Returns a JSON representation of all photo info.
|
||||
@@ -2157,6 +2164,37 @@ Returns a JSON representation of the FaceInfo instance.
|
||||
* `adj_version_info`: version info for the application which made the adjustments to the photo decoded from the adjustments data.
|
||||
* `asdict()`: dict representation of the AdjustmentsInfo object; contains all properties with exception of `plist`.
|
||||
|
||||
### PhotoTables
|
||||
|
||||
[PhotoInfo.tables](#tables) returns a PhotoTables object that contains information about the tables in the Photos database that contain information about the photo.
|
||||
The following properties are available:
|
||||
|
||||
* `ZASSET`
|
||||
* `ZADDITIONALASSETATTRIBUTES`
|
||||
* `ZDETECTEDFACE`
|
||||
* `ZPERSON`
|
||||
|
||||
Each of these properties returns a `Table` object that provides access to the row(s) in the table that correspond to the photo.
|
||||
|
||||
The Table object has dynamically created properties that correspond to the associated column in the table and return a tuple of values for that column.
|
||||
|
||||
```pycon
|
||||
>>> photo.tables().ZADDITIONALASSETATTRIBUTES.ZTITLE
|
||||
("St. James's Park",)
|
||||
```
|
||||
|
||||
The Table object also provides a `rows()` method which returns a list a of tuples for the matching rows in the table
|
||||
and a `rows_dict()` method which returns a list of dicts for the matching rows in the table.
|
||||
|
||||
```pycon
|
||||
|
||||
>>> photo.tables().ZASSET.rows()
|
||||
[(6, 3, 35, 0, 0, 0, 0, 0, 0, None, None, None, None, None, 0, 0, 1, 0, 0, 0, 0, -100, 0, 1, 0, 1356, 0, 0, 0, 0, 0, 0, 0, 1, 6192599813128215, 1, 2814835671629878, 1, 0, 3, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 2047, 7, None, 8, None, None, None, None, None, None, None, None, 3, 6, 6, 6, None, 6, 4, None, None, 8, 4, None, 2, None, 3, None, 3, None, None, 585926209.859624, 596906868.198932, 689981763.374756, None, None, None, 0.5, 561129492.501, 0.0, 596906868.198932, None, 0.03816793893129771, None, 51.50357167, -0.1318055, 689982854.802854, 0.6494140625, 0.0, 561129492.501, None, None, None, None, None, None, None, 'D', 'DC99FBDD-7A52-4100-A5BB-344131646C30.jpeg', None, 'sRGB IEC61966-2.1', 'public.jpeg', 'DC99FBDD-7A52-4100-A5BB-344131646C30', b'Ki\t@\x01\x00\x00\x00\td\tH\x01\x00\x00\x00\x93\\\tL\x01\x00\x00\x00\x1aK\x0c\x03\x0c\xa8q\x92\x00\x12C\x0c\x03\x0c"\r\x90\x00\x00<\x0c\x03\x08"\x19\x80\x00', b'\xca\xebV\tu\xc0I@/j\xf7\xab\x00\xdf\xc0\xbf\xcd\xcc\xcc\xcc\xcc\xcc\x04@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')]
|
||||
|
||||
>>> photo.tables().ZASSET.rows_dict()
|
||||
[{'Z_PK': 6, 'Z_ENT': 3, 'Z_OPT': 35, 'ZACTIVELIBRARYSCOPEPARTICIPATIONSTATE': 0, 'ZAVALANCHEPICKTYPE': 0, 'ZBUNDLESCOPE': 0, 'ZCAMERAPROCESSINGADJUSTMENTSTATE': 0, 'ZCLOUDDELETESTATE': 0, 'ZCLOUDDOWNLOADREQUESTS': 0, 'ZCLOUDHASCOMMENTSBYME': None, 'ZCLOUDHASCOMMENTSCONVERSATION': None, 'ZCLOUDHASUNSEENCOMMENTS': None, 'ZCLOUDISDELETABLE': None, 'ZCLOUDISMYASSET': None, 'ZCLOUDLOCALSTATE': 0, 'ZCLOUDPLACEHOLDERKIND': 0, 'ZCOMPLETE': 1, 'ZDEFERREDPROCESSINGNEEDED': 0, 'ZDEPTHTYPE': 0, 'ZDERIVEDCAMERACAPTUREDEVICE': 0, 'ZDUPLICATEASSETVISIBILITYSTATE': 0, 'ZFACEAREAPOINTS': -100, 'ZFAVORITE': 0, 'ZHASADJUSTMENTS': 1, 'ZHDRTYPE': 0, 'ZHEIGHT': 1356, 'ZHIDDEN': 0, 'ZHIGHFRAMERATESTATE': 0, 'ZISMAGICCARPET': 0, 'ZKIND': 0, 'ZKINDSUBTYPE': 0, 'ZLIBRARYSCOPESHARESTATE': 0, 'ZMONOSKITYPE': 0, 'ZORIENTATION': 1, 'ZPACKEDACCEPTABLECROPRECT': 6192599813128215, 'ZPACKEDBADGEATTRIBUTES': 1, 'ZPACKEDPREFERREDCROPRECT': 2814835671629878, 'ZPLAYBACKSTYLE': 1, 'ZPLAYBACKVARIATION': 0, 'ZSAVEDASSETTYPE': 3, 'ZSEARCHINDEXREBUILDSTATE': 0, 'ZSYNDICATIONSTATE': 0, 'ZTHUMBNAILINDEX': 5, 'ZTRASHEDSTATE': 0, 'ZVIDEOCPDURATIONVALUE': 0, 'ZVIDEOCPVISIBILITYSTATE': 0, 'ZVIDEODEFERREDPROCESSINGNEEDED': 0, 'ZVIDEOKEYFRAMETIMESCALE': 0, 'ZVIDEOKEYFRAMEVALUE': 0, 'ZVISIBILITYSTATE': 0, 'ZWIDTH': 2047, 'ZADDITIONALATTRIBUTES': 7, 'ZCLOUDFEEDASSETSENTRY': None, 'ZCOMPUTEDATTRIBUTES': 8, 'ZCONVERSATION': None, 'ZDAYGROUPHIGHLIGHTBEINGASSETS': None, 'ZDAYGROUPHIGHLIGHTBEINGEXTENDEDASSETS': None, 'ZDAYGROUPHIGHLIGHTBEINGKEYASSETPRIVATE': None, 'ZDAYGROUPHIGHLIGHTBEINGKEYASSETSHARED': None, 'ZDAYGROUPHIGHLIGHTBEINGSUMMARYASSETS': None, 'ZDUPLICATEMETADATAMATCHINGALBUM': None, 'ZDUPLICATEPERCEPTUALMATCHINGALBUM': None, 'ZEXTENDEDATTRIBUTES': 3, 'ZHIGHLIGHTBEINGASSETS': 6, 'ZHIGHLIGHTBEINGEXTENDEDASSETS': 6, 'ZHIGHLIGHTBEINGKEYASSETPRIVATE': 6, 'ZHIGHLIGHTBEINGKEYASSETSHARED': None, 'ZHIGHLIGHTBEINGSUMMARYASSETS': 6, 'ZIMPORTSESSION': 4, 'ZLIBRARYSCOPE': None, 'ZMASTER': None, 'ZMEDIAANALYSISATTRIBUTES': 8, 'ZMOMENT': 4, 'ZMOMENTSHARE': None, 'ZMONTHHIGHLIGHTBEINGKEYASSETPRIVATE': 2, 'ZMONTHHIGHLIGHTBEINGKEYASSETSHARED': None, 'ZPHOTOANALYSISATTRIBUTES': 3, 'ZTRASHEDBYPARTICIPANT': None, 'ZYEARHIGHLIGHTBEINGKEYASSETPRIVATE': 3, 'ZYEARHIGHLIGHTBEINGKEYASSETSHARED': None, 'Z_FOK_CLOUDFEEDASSETSENTRY': None, 'ZADDEDDATE': 585926209.859624, 'ZADJUSTMENTTIMESTAMP': 596906868.198932, 'ZANALYSISSTATEMODIFICATIONDATE': 689981763.374756, 'ZCLOUDBATCHPUBLISHDATE': None, 'ZCLOUDLASTVIEWEDCOMMENTDATE': None, 'ZCLOUDSERVERPUBLISHDATE': None, 'ZCURATIONSCORE': 0.5, 'ZDATECREATED': 561129492.501, 'ZDURATION': 0.0, 'ZFACEADJUSTMENTVERSION': 596906868.198932, 'ZHDRGAIN': None, 'ZHIGHLIGHTVISIBILITYSCORE': 0.03816793893129771, 'ZLASTSHAREDDATE': None, 'ZLATITUDE': 51.50357167, 'ZLONGITUDE': -0.1318055, 'ZMODIFICATIONDATE': 689982854.802854, 'ZOVERALLAESTHETICSCORE': 0.6494140625, 'ZPROMOTIONSCORE': 0.0, 'ZSORTTOKEN': 561129492.501, 'ZTRASHEDDATE': None, 'ZAVALANCHEUUID': None, 'ZCLOUDASSETGUID': None, 'ZCLOUDBATCHID': None, 'ZCLOUDCOLLECTIONGUID': None, 'ZCLOUDOWNERHASHEDPERSONID': None, 'ZDELETEREASON': None, 'ZDIRECTORY': 'D', 'ZFILENAME': 'DC99FBDD-7A52-4100-A5BB-344131646C30.jpeg', 'ZMEDIAGROUPUUID': None, 'ZORIGINALCOLORSPACE': 'sRGB IEC61966-2.1', 'ZUNIFORMTYPEIDENTIFIER': 'public.jpeg', 'ZUUID': 'DC99FBDD-7A52-4100-A5BB-344131646C30', 'ZIMAGEREQUESTHINTS': b'Ki\t@\x01\x00\x00\x00\td\tH\x01\x00\x00\x00\x93\\\tL\x01\x00\x00\x00\x1aK\x0c\x03\x0c\xa8q\x92\x00\x12C\x0c\x03\x0c"\r\x90\x00\x00<\x0c\x03\x08"\x19\x80\x00', 'ZLOCATIONDATA': b'\xca\xebV\tu\xc0I@/j\xf7\xab\x00\xdf\xc0\xbf\xcd\xcc\xcc\xcc\xcc\xcc\x04@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'}]
|
||||
```
|
||||
|
||||
### Raw Photos
|
||||
|
||||
Handling raw photos in `osxphotos` requires a bit of extra work. Raw photos in Photos can be imported in two different ways: 1) a single raw photo with no associated JPEG image is imported 2) a raw+JPEG pair is imported -- two separate images with same file stem (e.g. `IMG_0001.CR2` and `IMG_001.JPG`) are imported.
|
||||
|
||||
25
CHANGELOG.md
25
CHANGELOG.md
@@ -3,6 +3,31 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
|
||||
## [v0.59.3](https://github.com/RhetTbull/osxphotos/compare/v0.59.2...v0.59.3)
|
||||
|
||||
Bug fixes for memory leak, crash during export
|
||||
|
||||
### 10 April 2023
|
||||
|
||||
#### Fixed
|
||||
|
||||
- Fixed memory leak in export (#1047)
|
||||
- Fixed crash during export (#1046)
|
||||
- Fixed large crash log size (#1048)
|
||||
|
||||
#### Changed
|
||||
|
||||
- Added better help for no selection with --selected (#1036)
|
||||
- Changed PhotoInfo.asdict() and PhotoInfo.json() to allow deep or shallow option (#1038)
|
||||
- Updated development docs (#1043)
|
||||
|
||||
#### Contributors
|
||||
|
||||
- @RhetTbull for code changes
|
||||
- @wernerzj for finding bug with memory leak
|
||||
- @rajscode for finding export crash
|
||||
- @oPromessa for development docs fix
|
||||
|
||||
## [v0.59.2](https://github.com/RhetTbull/osxphotos/compare/v0.59.1...v0.59.2)
|
||||
|
||||
Bug Fix for Export
|
||||
|
||||
@@ -2701,7 +2701,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/aa599"><img src="https://avatars.githubusercontent.com/u/37746269?v=4?s=75" width="75px;" alt="aa599"/><br /><sub><b>aa599</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Aaa599" title="Bug reports">🐛</a> <a href="#ideas-aa599" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/swduncan"><img src="https://avatars.githubusercontent.com/u/2053195?v=4?s=75" width="75px;" alt="Steve Duncan"/><br /><sub><b>Steve Duncan</b></sub></a><br /><a href="#ideas-swduncan" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.projany.com"><img src="https://avatars.githubusercontent.com/u/15144745?v=4?s=75" width="75px;" alt="Ian Moir"/><br /><sub><b>Ian Moir</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Aianmmoir" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/pekingduck"><img src="https://avatars.githubusercontent.com/u/2597142?v=4?s=75" width="75px;" alt="Peking Duck"/><br /><sub><b>Peking Duck</b></sub></a><br /><a href="#ideas-pekingduck" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/pekingduck"><img src="https://avatars.githubusercontent.com/u/2597142?v=4?s=75" width="75px;" alt="Peking Duck"/><br /><sub><b>Peking Duck</b></sub></a><br /><a href="#ideas-pekingduck" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Apekingduck" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.patreon.com/cclauss"><img src="https://avatars.githubusercontent.com/u/3709715?v=4?s=75" width="75px;" alt="Christian Clauss"/><br /><sub><b>Christian Clauss</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=cclauss" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
||||
@@ -19,6 +19,7 @@ from .photoinfo import PhotoInfo
|
||||
from .photosalbum import PhotosAlbum, PhotosAlbumPhotoScript
|
||||
from .photosdb import PhotosDB
|
||||
from .photosdb._photosdb_process_comments import CommentInfo, LikeInfo
|
||||
from .phototables import PhotoTables
|
||||
from .phototemplate import PhotoTemplate
|
||||
from .placeinfo import PlaceInfo
|
||||
from .queryoptions import QueryOptions
|
||||
@@ -53,6 +54,7 @@ __all__ = [
|
||||
"PersonInfo",
|
||||
"PhotoExporter",
|
||||
"PhotoInfo",
|
||||
"PhotoTables",
|
||||
"PhotoTemplate",
|
||||
"PhotosAlbum",
|
||||
"PhotosAlbumPhotoScript",
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
"""repl command for osxphotos CLI"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import os.path
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
from typing import List
|
||||
|
||||
import click
|
||||
import photoscript
|
||||
from applescript import ScriptError
|
||||
from rich import pretty, print
|
||||
|
||||
import osxphotos
|
||||
@@ -191,10 +194,16 @@ def _get_selected(photosdb):
|
||||
"""get list of PhotoInfo objects for photos selected in Photos"""
|
||||
|
||||
def get_selected():
|
||||
selected = photoscript.PhotosLibrary().selection
|
||||
if not selected:
|
||||
return []
|
||||
return photosdb.photos(uuid=[p.uuid for p in selected])
|
||||
try:
|
||||
selected = photoscript.PhotosLibrary().selection
|
||||
except ScriptError as e:
|
||||
# some photos (e.g. shared items) can't be selected and raise ScriptError:
|
||||
# applescript.ScriptError: Photos got an error: Can’t get media item id "34C26DFA-0CEA-4DB7-8FDA-B87789B3209D/L0/001". (-1728) app='Photos' range=16820-16873
|
||||
# In this case, we can parse the UUID from the error (though this only works for a single selected item)
|
||||
if match := re.match(r".*Can’t get media item id \"(.*)\".*", str(e)):
|
||||
uuid = match[1].split("/")[0]
|
||||
return photosdb.photos(uuid=[uuid])
|
||||
return photosdb.photos(uuid=[p.uuid for p in selected]) if selected else []
|
||||
|
||||
return get_selected
|
||||
|
||||
|
||||
@@ -59,6 +59,7 @@ from .exiftool import ExifToolCaching, get_exiftool_path
|
||||
from .momentinfo import MomentInfo
|
||||
from .personinfo import FaceInfo, PersonInfo
|
||||
from .photoexporter import ExportOptions, PhotoExporter
|
||||
from .phototables import PhotoTables
|
||||
from .phototemplate import PhotoTemplate, RenderOptions
|
||||
from .placeinfo import PlaceInfo4, PlaceInfo5
|
||||
from .query_builder import get_query
|
||||
@@ -1893,6 +1894,10 @@ class PhotoInfo:
|
||||
dict_data[k] = sorted(v, key=lambda v: v if v is not None else "")
|
||||
return json.dumps(dict_data, sort_keys=True, default=default, indent=indent)
|
||||
|
||||
def tables(self) -> PhotoTables:
|
||||
"""Return PhotoTables object to provide access database tables associated with this photo (Photos 5+)"""
|
||||
return None if self._db._photos_ver < 5 else PhotoTables(self)
|
||||
|
||||
def _json_hexdigest(self):
|
||||
"""JSON for use by hexdigest()"""
|
||||
|
||||
|
||||
230
osxphotos/phototables.py
Normal file
230
osxphotos/phototables.py
Normal file
@@ -0,0 +1,230 @@
|
||||
"""Provide direct access to the database tables associated with a photo."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlite3
|
||||
from typing import Any
|
||||
|
||||
import osxphotos
|
||||
|
||||
from ._constants import _DB_TABLE_NAMES
|
||||
|
||||
|
||||
def get_table_columns(conn: sqlite3.Connection, table_name: str) -> list[str]:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(f"PRAGMA table_info({table_name})")
|
||||
return [col[1] for col in cursor.fetchall()]
|
||||
|
||||
|
||||
class PhotoTables:
|
||||
def __init__(self, photo: osxphotos.PhotoInfo):
|
||||
"""Create a PhotoTables object.
|
||||
|
||||
Args:
|
||||
db: PhotosDB object
|
||||
uuid: The UUID of the photo.
|
||||
"""
|
||||
self.db = photo._db
|
||||
self.photo = photo
|
||||
self.uuid = photo.uuid
|
||||
self.version = self.db._photos_ver
|
||||
|
||||
@property
|
||||
def ZASSET(self) -> Table:
|
||||
"""Return the ZASSET table."""
|
||||
return AssetTable(self.db, self.version, self.uuid)
|
||||
|
||||
@property
|
||||
def ZADDITIONALASSETATTRIBUTES(self) -> Table:
|
||||
"""Return the ZADDITIONALASSETATTRIBUTES table."""
|
||||
return AdditionalAttributesTable(self.db, self.version, self.uuid)
|
||||
|
||||
@property
|
||||
def ZDETECTEDFACE(self) -> Table:
|
||||
"""Return the ZDETECTEDFACE table."""
|
||||
return DetectedFaceTable(self.db, self.version, self.uuid)
|
||||
|
||||
@property
|
||||
def ZPERSON(self) -> Table:
|
||||
"""Return the ZPERSON table."""
|
||||
return PersonTable(self.db, self.version, self.uuid)
|
||||
|
||||
|
||||
class Table:
|
||||
def __init__(self, db: osxphotos.PhotosDB, version: int, uuid: str):
|
||||
"""Create a Table object.
|
||||
|
||||
Args:
|
||||
db: PhotosDB object
|
||||
table_name: The name of the table.
|
||||
"""
|
||||
self.db = db
|
||||
self.conn, _ = self.db.get_db_connection()
|
||||
self.version = version
|
||||
self.uuid = uuid
|
||||
self.asset_table = _DB_TABLE_NAMES[self.version]["ASSET"]
|
||||
self.columns = [] # must be set in subclass
|
||||
self.table_name = "" # must be set in subclass
|
||||
|
||||
def rows(self) -> list[tuple[Any]]:
|
||||
"""Return rows for this photo from the table."""
|
||||
# this should be implemented in the subclass
|
||||
raise NotImplementedError
|
||||
|
||||
def rows_dict(self) -> list[dict[str, Any]]:
|
||||
"""Return rows for this photo from the table as a list of dicts."""
|
||||
rows = self.rows()
|
||||
return [dict(zip(self.columns, row)) for row in rows] if rows else []
|
||||
|
||||
def _get_column(self, column: str):
|
||||
"""Get column value for this photo from the table."""
|
||||
# this should be implemented in the subclass
|
||||
raise NotImplementedError
|
||||
|
||||
def __getattr__(self, name):
|
||||
"""Get column value for this photo from the table."""
|
||||
if name not in self.__dict__ and name in self.columns:
|
||||
return self._get_column(name)
|
||||
else:
|
||||
raise AttributeError(f"Table {self.table_name} has no column {name}")
|
||||
|
||||
|
||||
class AssetTable(Table):
|
||||
"""ZASSET table."""
|
||||
|
||||
def __init__(self, db: osxphotos.PhotosDB, version: int, uuid: str):
|
||||
"""Create a Table object."""
|
||||
super().__init__(db, version, uuid)
|
||||
self.columns = get_table_columns(self.conn, self.asset_table)
|
||||
self.table_name = self.asset_table
|
||||
|
||||
def rows(self) -> list[Any]:
|
||||
"""Return row2 for this photo from the ZASSET table."""
|
||||
conn, cursor = self.db.get_db_connection()
|
||||
cursor.execute(
|
||||
f"SELECT * FROM {self.asset_table} WHERE ZUUID = ?", (self.uuid,)
|
||||
)
|
||||
return result if (result := cursor.fetchall()) else []
|
||||
|
||||
def _get_column(self, column: str) -> tuple[Any]:
|
||||
"""Get column value for this photo from the ZASSET table."""
|
||||
conn, cursor = self.db.get_db_connection()
|
||||
cursor.execute(
|
||||
f"SELECT {column} FROM {self.asset_table} WHERE ZUUID = ?",
|
||||
(self.uuid,),
|
||||
)
|
||||
return (
|
||||
tuple(result[0] for result in results)
|
||||
if (results := cursor.fetchall())
|
||||
else ()
|
||||
)
|
||||
|
||||
|
||||
class AdditionalAttributesTable(Table):
|
||||
"""ZADDITIONALASSETATTRIBUTES table."""
|
||||
|
||||
def __init__(self, db: osxphotos.PhotosDB, version: int, uuid: str):
|
||||
"""Create a Table object."""
|
||||
super().__init__(db, version, uuid)
|
||||
self.columns = get_table_columns(self.conn, "ZADDITIONALASSETATTRIBUTES")
|
||||
self.table_name = "ZADDITIONALASSETATTRIBUTES"
|
||||
|
||||
def rows(self) -> list[tuple[Any]]:
|
||||
"""Return rows for this photo from the ZADDITIONALASSETATTRIBUTES table."""
|
||||
conn, cursor = self.db.get_db_connection()
|
||||
sql = f""" SELECT ZADDITIONALASSETATTRIBUTES.*
|
||||
FROM ZADDITIONALASSETATTRIBUTES
|
||||
JOIN {self.asset_table} ON {self.asset_table}.Z_PK = ZADDITIONALASSETATTRIBUTES.ZASSET
|
||||
WHERE {self.asset_table}.ZUUID = ?;
|
||||
"""
|
||||
cursor.execute(sql, (self.uuid,))
|
||||
return result if (result := cursor.fetchall()) else []
|
||||
|
||||
def _get_column(self, column: str) -> tuple[Any]:
|
||||
"""Get column value for this photo from the ZADDITIONALASSETATTRIBUTES table."""
|
||||
conn, cursor = self.db.get_db_connection()
|
||||
sql = f""" SELECT ZADDITIONALASSETATTRIBUTES.{column}
|
||||
FROM ZADDITIONALASSETATTRIBUTES
|
||||
JOIN {self.asset_table} ON {self.asset_table}.Z_PK = ZADDITIONALASSETATTRIBUTES.ZASSET
|
||||
WHERE {self.asset_table}.ZUUID = ?;
|
||||
"""
|
||||
cursor.execute(sql, (self.uuid,))
|
||||
return (
|
||||
tuple(result[0] for result in results)
|
||||
if (results := cursor.fetchall())
|
||||
else ()
|
||||
)
|
||||
|
||||
|
||||
class DetectedFaceTable(Table):
|
||||
"""ZDETECTEDFACE table."""
|
||||
|
||||
def __init__(self, db: osxphotos.PhotosDB, version: int, uuid: str):
|
||||
"""Create a Table object."""
|
||||
super().__init__(db, version, uuid)
|
||||
self.columns = get_table_columns(self.conn, "ZDETECTEDFACE")
|
||||
self.table_name = "ZDETECTEDFACE"
|
||||
|
||||
def rows(self) -> list[tuple[Any]]:
|
||||
"""Return rows for this photo from the ZDETECTEDFACE table."""
|
||||
conn, cursor = self.db.get_db_connection()
|
||||
sql = f""" SELECT ZDETECTEDFACE.*
|
||||
FROM ZDETECTEDFACE
|
||||
JOIN {self.asset_table} ON {self.asset_table}.Z_PK = ZDETECTEDFACE.ZASSET
|
||||
WHERE {self.asset_table}.ZUUID = ?;
|
||||
"""
|
||||
cursor.execute(sql, (self.uuid,))
|
||||
return result if (result := cursor.fetchall()) else []
|
||||
|
||||
def _get_column(self, column: str) -> tuple[Any]:
|
||||
"""Get column value for this photo from the ZDETECTEDFACE table."""
|
||||
conn, cursor = self.db.get_db_connection()
|
||||
sql = f""" SELECT ZDETECTEDFACE.{column}
|
||||
FROM ZDETECTEDFACE
|
||||
JOIN {self.asset_table} ON {self.asset_table}.Z_PK = ZDETECTEDFACE.ZASSET
|
||||
WHERE {self.asset_table}.ZUUID = ?;
|
||||
"""
|
||||
cursor.execute(sql, (self.uuid,))
|
||||
return (
|
||||
tuple(result[0] for result in results)
|
||||
if (results := cursor.fetchall())
|
||||
else ()
|
||||
)
|
||||
|
||||
|
||||
class PersonTable(Table):
|
||||
"""ZPERSON table."""
|
||||
|
||||
def __init__(self, db: osxphotos.PhotosDB, version: int, uuid: str):
|
||||
"""Create a Table object."""
|
||||
super().__init__(db, version, uuid)
|
||||
self.columns = get_table_columns(self.conn, "ZPERSON")
|
||||
self.table_name = "ZPERSON"
|
||||
|
||||
def rows(self) -> list[tuple[Any]]:
|
||||
"""Return rows for this photo from the ZPERSON table."""
|
||||
conn, cursor = self.db.get_db_connection()
|
||||
sql = f""" SELECT ZPERSON.*
|
||||
FROM ZPERSON
|
||||
JOIN ZDETECTEDFACE ON ZDETECTEDFACE.ZPERSON = ZPERSON.Z_PK
|
||||
JOIN ZASSET ON ZASSET.Z_PK = ZDETECTEDFACE.ZASSET
|
||||
WHERE {self.asset_table}.ZUUID = ?;
|
||||
"""
|
||||
cursor.execute(sql, (self.uuid,))
|
||||
return result if (result := cursor.fetchall()) else []
|
||||
|
||||
def _get_column(self, column: str) -> tuple[Any]:
|
||||
"""Get column value for this photo from the ZPERSON table."""
|
||||
conn, cursor = self.db.get_db_connection()
|
||||
sql = f""" SELECT ZPERSON.{column}
|
||||
FROM ZPERSON
|
||||
JOIN ZDETECTEDFACE ON ZDETECTEDFACE.ZPERSON = ZPERSON.Z_PK
|
||||
JOIN ZASSET ON ZASSET.Z_PK = ZDETECTEDFACE.ZASSET
|
||||
WHERE {self.asset_table}.ZUUID = ?;
|
||||
"""
|
||||
cursor.execute(sql, (self.uuid,))
|
||||
return (
|
||||
tuple(result[0] for result in results)
|
||||
if (results := cursor.fetchall())
|
||||
else ()
|
||||
)
|
||||
@@ -5,6 +5,7 @@ from collections import namedtuple
|
||||
|
||||
import pytest
|
||||
|
||||
import osxphotos
|
||||
from osxphotos._constants import _UNKNOWN_PERSON
|
||||
|
||||
PHOTOS_DB = "./tests/Test-10.14.6.photoslibrary/database/photos.db"
|
||||
@@ -531,9 +532,10 @@ def test_photosdb_repr():
|
||||
|
||||
|
||||
def test_photosinfo_repr():
|
||||
import osxphotos
|
||||
import datetime # needed for eval to work
|
||||
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["favorite"]])
|
||||
photo = photos[0]
|
||||
@@ -689,3 +691,10 @@ def test_fingerprint(photosdb):
|
||||
for uuid, fingerprint in UUID_FINGERPRINT.items():
|
||||
photo = photosdb.get_photo(uuid)
|
||||
assert photo.fingerprint == fingerprint
|
||||
|
||||
|
||||
def test_tables(photosdb: osxphotos.PhotosDB):
|
||||
"""Test PhotoInfo.tables"""
|
||||
photo = photosdb.get_photo(UUID_DICT["favorite"])
|
||||
tables = photo.tables()
|
||||
assert tables is None
|
||||
|
||||
@@ -1304,3 +1304,14 @@ def test_photosdb_photos_by_uuid(photosdb: osxphotos.PhotosDB):
|
||||
assert len(photos) == len(UUID_DICT)
|
||||
for photo in photos:
|
||||
assert photo.uuid in UUID_DICT.values()
|
||||
|
||||
|
||||
def test_tables(photosdb: osxphotos.PhotosDB):
|
||||
"""Test PhotoInfo.tables"""
|
||||
photo = photosdb.get_photo(UUID_DICT["in_album"])
|
||||
tables = photo.tables()
|
||||
assert isinstance(tables, osxphotos.PhotoTables)
|
||||
assert tables.ZASSET.ZUUID[0] == photo.uuid
|
||||
assert tables.ZADDITIONALASSETATTRIBUTES.ZTITLE[0] == photo.title
|
||||
assert len(tables.ZASSET.rows()) == 1
|
||||
assert tables.ZASSET.rows_dict()[0]["ZUUID"] == photo.uuid
|
||||
|
||||
Reference in New Issue
Block a user