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",
|
"avatar_url": "https://avatars.githubusercontent.com/u/2597142?v=4",
|
||||||
"profile": "https://github.com/pekingduck",
|
"profile": "https://github.com/pekingduck",
|
||||||
"contributions": [
|
"contributions": [
|
||||||
"ideas"
|
"ideas",
|
||||||
|
"bug"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
max-parallel: 4
|
max-parallel: 4
|
||||||
matrix:
|
matrix:
|
||||||
os: [macos-latest]
|
os: [macos-latest-xl]
|
||||||
python-version: ['3.9', '3.10', '3.11']
|
python-version: ['3.9', '3.10', '3.11']
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ In addition to a command line interface, OSXPhotos provides a access to a Python
|
|||||||
* [CommentInfo](#commentinfo)
|
* [CommentInfo](#commentinfo)
|
||||||
* [LikeInfo](#likeinfo)
|
* [LikeInfo](#likeinfo)
|
||||||
* [AdjustmentsInfo](#adjustmentsinfo)
|
* [AdjustmentsInfo](#adjustmentsinfo)
|
||||||
|
* [PhotoTables](#phototables)
|
||||||
* [Raw Photos](#raw-photos)
|
* [Raw Photos](#raw-photos)
|
||||||
* [Template System](#template-system)
|
* [Template System](#template-system)
|
||||||
* [ExifTool](#exiftoolExifTool)
|
* [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.
|
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()`
|
#### `json()`
|
||||||
|
|
||||||
Returns a JSON representation of all photo info.
|
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.
|
* `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`.
|
* `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
|
### 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.
|
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.
|
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)
|
## [v0.59.2](https://github.com/RhetTbull/osxphotos/compare/v0.59.1...v0.59.2)
|
||||||
|
|
||||||
Bug Fix for Export
|
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/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="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="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>
|
<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>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ from .photoinfo import PhotoInfo
|
|||||||
from .photosalbum import PhotosAlbum, PhotosAlbumPhotoScript
|
from .photosalbum import PhotosAlbum, PhotosAlbumPhotoScript
|
||||||
from .photosdb import PhotosDB
|
from .photosdb import PhotosDB
|
||||||
from .photosdb._photosdb_process_comments import CommentInfo, LikeInfo
|
from .photosdb._photosdb_process_comments import CommentInfo, LikeInfo
|
||||||
|
from .phototables import PhotoTables
|
||||||
from .phototemplate import PhotoTemplate
|
from .phototemplate import PhotoTemplate
|
||||||
from .placeinfo import PlaceInfo
|
from .placeinfo import PlaceInfo
|
||||||
from .queryoptions import QueryOptions
|
from .queryoptions import QueryOptions
|
||||||
@@ -53,6 +54,7 @@ __all__ = [
|
|||||||
"PersonInfo",
|
"PersonInfo",
|
||||||
"PhotoExporter",
|
"PhotoExporter",
|
||||||
"PhotoInfo",
|
"PhotoInfo",
|
||||||
|
"PhotoTables",
|
||||||
"PhotoTemplate",
|
"PhotoTemplate",
|
||||||
"PhotosAlbum",
|
"PhotosAlbum",
|
||||||
"PhotosAlbumPhotoScript",
|
"PhotosAlbumPhotoScript",
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
"""repl command for osxphotos CLI"""
|
"""repl command for osxphotos CLI"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import os.path
|
import os.path
|
||||||
import pathlib
|
import pathlib
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
import click
|
import click
|
||||||
import photoscript
|
import photoscript
|
||||||
|
from applescript import ScriptError
|
||||||
from rich import pretty, print
|
from rich import pretty, print
|
||||||
|
|
||||||
import osxphotos
|
import osxphotos
|
||||||
@@ -191,10 +194,16 @@ def _get_selected(photosdb):
|
|||||||
"""get list of PhotoInfo objects for photos selected in Photos"""
|
"""get list of PhotoInfo objects for photos selected in Photos"""
|
||||||
|
|
||||||
def get_selected():
|
def get_selected():
|
||||||
selected = photoscript.PhotosLibrary().selection
|
try:
|
||||||
if not selected:
|
selected = photoscript.PhotosLibrary().selection
|
||||||
return []
|
except ScriptError as e:
|
||||||
return photosdb.photos(uuid=[p.uuid for p in selected])
|
# 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
|
return get_selected
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ from .exiftool import ExifToolCaching, get_exiftool_path
|
|||||||
from .momentinfo import MomentInfo
|
from .momentinfo import MomentInfo
|
||||||
from .personinfo import FaceInfo, PersonInfo
|
from .personinfo import FaceInfo, PersonInfo
|
||||||
from .photoexporter import ExportOptions, PhotoExporter
|
from .photoexporter import ExportOptions, PhotoExporter
|
||||||
|
from .phototables import PhotoTables
|
||||||
from .phototemplate import PhotoTemplate, RenderOptions
|
from .phototemplate import PhotoTemplate, RenderOptions
|
||||||
from .placeinfo import PlaceInfo4, PlaceInfo5
|
from .placeinfo import PlaceInfo4, PlaceInfo5
|
||||||
from .query_builder import get_query
|
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 "")
|
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)
|
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):
|
def _json_hexdigest(self):
|
||||||
"""JSON for use by hexdigest()"""
|
"""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 pytest
|
||||||
|
|
||||||
|
import osxphotos
|
||||||
from osxphotos._constants import _UNKNOWN_PERSON
|
from osxphotos._constants import _UNKNOWN_PERSON
|
||||||
|
|
||||||
PHOTOS_DB = "./tests/Test-10.14.6.photoslibrary/database/photos.db"
|
PHOTOS_DB = "./tests/Test-10.14.6.photoslibrary/database/photos.db"
|
||||||
@@ -531,9 +532,10 @@ def test_photosdb_repr():
|
|||||||
|
|
||||||
|
|
||||||
def test_photosinfo_repr():
|
def test_photosinfo_repr():
|
||||||
import osxphotos
|
|
||||||
import datetime # needed for eval to work
|
import datetime # needed for eval to work
|
||||||
|
|
||||||
|
import osxphotos
|
||||||
|
|
||||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||||
photos = photosdb.photos(uuid=[UUID_DICT["favorite"]])
|
photos = photosdb.photos(uuid=[UUID_DICT["favorite"]])
|
||||||
photo = photos[0]
|
photo = photos[0]
|
||||||
@@ -689,3 +691,10 @@ def test_fingerprint(photosdb):
|
|||||||
for uuid, fingerprint in UUID_FINGERPRINT.items():
|
for uuid, fingerprint in UUID_FINGERPRINT.items():
|
||||||
photo = photosdb.get_photo(uuid)
|
photo = photosdb.get_photo(uuid)
|
||||||
assert photo.fingerprint == fingerprint
|
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)
|
assert len(photos) == len(UUID_DICT)
|
||||||
for photo in photos:
|
for photo in photos:
|
||||||
assert photo.uuid in UUID_DICT.values()
|
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