Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf4dca10c0 | ||
|
|
27040d1604 | ||
|
|
b91a9828fa | ||
|
|
8c10b61e90 | ||
|
|
b7f4b739de | ||
|
|
f8e62d8f5e | ||
|
|
da551036f9 | ||
|
|
d52b387a29 | ||
|
|
927e25911e | ||
|
|
6688d1ff64 | ||
|
|
3526881ec8 | ||
|
|
3f19276c5c | ||
|
|
091e7b8f2e | ||
|
|
1ef518cc3e | ||
|
|
a934b692ab | ||
|
|
9d820a0557 | ||
|
|
fcff8ec5f8 | ||
|
|
dfcbfa725a | ||
|
|
df75a05645 | ||
|
|
80f5989e2c | ||
|
|
8c3af0a4e4 | ||
|
|
4523224276 | ||
|
|
541c390b7b | ||
|
|
6ab0ad7e86 | ||
|
|
e5755c6144 | ||
|
|
7806e05673 | ||
|
|
bb4bc8fd96 | ||
|
|
59507077ba | ||
|
|
ff0328785f |
60
CHANGELOG.md
60
CHANGELOG.md
@@ -4,6 +4,66 @@ All notable changes to this project will be documented in this file. Dates are d
|
||||
|
||||
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||
|
||||
#### [v0.30.12](https://github.com/RhetTbull/osxphotos/compare/v0.30.10...v0.30.12)
|
||||
|
||||
> 18 July 2020
|
||||
|
||||
- Implemented PersonInfo, closes #181 [`#181`](https://github.com/RhetTbull/osxphotos/issues/181)
|
||||
- Updated dependencies, now supports py36, py37, py38 [`6688d1f`](https://github.com/RhetTbull/osxphotos/commit/6688d1ff6491f2e7e155946b265ef8b5d8929441)
|
||||
- Update README.md [`3526881`](https://github.com/RhetTbull/osxphotos/commit/3526881ec872cc009b0d8936f366afcfff166d42)
|
||||
|
||||
#### [v0.30.10](https://github.com/RhetTbull/osxphotos/compare/v0.30.9...v0.30.10)
|
||||
|
||||
> 6 July 2020
|
||||
|
||||
- Bug fix for empty albums [`1ef518c`](https://github.com/RhetTbull/osxphotos/commit/1ef518cc3e9efbe9d4c16aa3d36c6dc6db86798e)
|
||||
|
||||
#### [v0.30.9](https://github.com/RhetTbull/osxphotos/compare/v0.30.7...v0.30.9)
|
||||
|
||||
> 6 July 2020
|
||||
|
||||
- Refactored person processing to enable implementation of #181 [`fcff8ec`](https://github.com/RhetTbull/osxphotos/commit/fcff8ec5f8286b28e7d8559b40b5808a7b59cc15)
|
||||
- AlbumInfo.photos now returns photos in album sort order [`9d820a0`](https://github.com/RhetTbull/osxphotos/commit/9d820a0557944340d0c664a6c3497d138c6100d5)
|
||||
|
||||
#### [v0.30.7](https://github.com/RhetTbull/osxphotos/compare/v0.30.6...v0.30.7)
|
||||
|
||||
> 4 July 2020
|
||||
|
||||
- Bug fix for keywords, persons in deleted photos [`df75a05`](https://github.com/RhetTbull/osxphotos/commit/df75a05645a88b31daa411f960d99ade71efc908)
|
||||
|
||||
#### [v0.30.6](https://github.com/RhetTbull/osxphotos/compare/v0.30.5...v0.30.6)
|
||||
|
||||
> 3 July 2020
|
||||
|
||||
- Added height, width, orientation, filesize to json, str) [`8c3af0a`](https://github.com/RhetTbull/osxphotos/commit/8c3af0a4e4e49d9bbb33e809973d958334e44dca)
|
||||
|
||||
#### [v0.30.5](https://github.com/RhetTbull/osxphotos/compare/v0.30.4...v0.30.5)
|
||||
|
||||
> 3 July 2020
|
||||
|
||||
- Added height, width, orientation, filesize, closes #163 [`#163`](https://github.com/RhetTbull/osxphotos/issues/163)
|
||||
|
||||
#### [v0.30.4](https://github.com/RhetTbull/osxphotos/compare/v0.30.3...v0.30.4)
|
||||
|
||||
> 3 July 2020
|
||||
|
||||
- Added GPS location to XMP sidecar, closes #175 [`#175`](https://github.com/RhetTbull/osxphotos/issues/175)
|
||||
- Updated README.md [`7806e05`](https://github.com/RhetTbull/osxphotos/commit/7806e05673775ded231e65f53f3a1d5095a4b4e1)
|
||||
|
||||
#### [v0.30.3](https://github.com/RhetTbull/osxphotos/compare/v0.30.2...v0.30.3)
|
||||
|
||||
> 29 June 2020
|
||||
|
||||
- Added --description-template to CLI, closes #166 [`#166`](https://github.com/RhetTbull/osxphotos/issues/166)
|
||||
- Added expand_inplace to PhotoTemplate.render [`ff03287`](https://github.com/RhetTbull/osxphotos/commit/ff0328785f3ea14b1c8ae2b7d1a9b07e8aef0777)
|
||||
- Updated README.md [`5950707`](https://github.com/RhetTbull/osxphotos/commit/59507077bafe39a17bc23babe6d6c52e1f502a53)
|
||||
|
||||
#### [v0.30.2](https://github.com/RhetTbull/osxphotos/compare/v0.30.1...v0.30.2)
|
||||
|
||||
> 28 June 2020
|
||||
|
||||
- Added --deleted, --deleted-only to CLI, closes #179 [`#179`](https://github.com/RhetTbull/osxphotos/issues/179)
|
||||
|
||||
#### [v0.30.1](https://github.com/RhetTbull/osxphotos/compare/v0.30.0...v0.30.1)
|
||||
|
||||
> 27 June 2020
|
||||
|
||||
99
README.md
99
README.md
@@ -18,6 +18,7 @@
|
||||
+ [FolderInfo](#folderinfo)
|
||||
+ [PlaceInfo](#placeinfo)
|
||||
+ [ScoreInfo](#scoreinfo)
|
||||
+ [PersonInfo](#personinfo)
|
||||
+ [Template Substitutions](#template-substitutions)
|
||||
+ [Utility Functions](#utility-functions)
|
||||
* [Examples](#examples)
|
||||
@@ -35,9 +36,9 @@ OSXPhotos provides the ability to interact with and query Apple's Photos.app lib
|
||||
|
||||
## Supported operating systems
|
||||
|
||||
Only works on MacOS (aka Mac OS X). Tested on MacOS 10.12.6 / Photos 2.0, 10.13.6 / Photos 3.0, MacOS 10.14.5, 10.14.6 / Photos 4.0, MacOS 10.15.1 - 10.15.5 / Photos 5.0.
|
||||
Only works on MacOS (aka Mac OS X). Tested on MacOS 10.12.6 / Photos 2.0, 10.13.6 / Photos 3.0, MacOS 10.14.5, 10.14.6 / Photos 4.0, MacOS 10.15.1 - 10.15.6 / Photos 5.0.
|
||||
|
||||
Requires python >= 3.8. You can probably get this to run with Python 3.6 or 3.7 (see notes [below](#Installation-instructions)) but only 3.8+ is officially supported.
|
||||
Requires python >= 3.6.
|
||||
|
||||
This package will read Photos databases for any supported version on any supported OS version. E.g. you can read a database created with Photos 4.0 on MacOS 10.14 on a machine running MacOS 10.12.
|
||||
|
||||
@@ -48,11 +49,7 @@ OSXPhotos uses setuptools, thus simply run:
|
||||
|
||||
python3 setup.py install
|
||||
|
||||
If you're using python 3.6 or 3.7, you'll need to do this first to get around an issue with bpylist2:
|
||||
|
||||
pip install -r requirements.txt
|
||||
|
||||
You can also install directly from [pypi](https://pypi.org/) but you must use python >= 3.8 to avoid an error with bpylist2. The package currently works fine with python 3.6 or 3.7 but I know of no way to get `pip` to install the right dependencies.
|
||||
You can also install directly from [pypi](https://pypi.org/):
|
||||
|
||||
pip install osxphotos
|
||||
|
||||
@@ -253,6 +250,16 @@ Options:
|
||||
--keyword-template "{folder_album}"
|
||||
--keyword-template "{created.year}" See
|
||||
Templating System below.
|
||||
--description-template TEMPLATE
|
||||
For use with --exiftool, --sidecar; specify
|
||||
a template string to use as description in
|
||||
the form '{name,DEFAULT}' This is the same
|
||||
format as --directory. For example, if you
|
||||
wanted to append 'exported with osxphotos on
|
||||
[today's date]' to the description, you
|
||||
could specify --description-template
|
||||
"{descr} exported with osxphotos on
|
||||
{today.date}" See Templating System below.
|
||||
--current-name Use photo's current filename instead of
|
||||
original filename for export. Note:
|
||||
Starting with Photos 5, all photos are
|
||||
@@ -780,7 +787,15 @@ Returns a list names of top level folder names in the database.
|
||||
persons = photosdb.persons
|
||||
```
|
||||
|
||||
Returns a list of the persons (faces) found in the Photos library
|
||||
Returns a list of the person names (faces) found in the Photos library. **Note**: It is of course possible to have more than one person with the same name, e.g. "Maria Smith", in the database. `persons` assumes these are the same person and will list only one person named "Maria Smith". If you need more information about persons in the database, see [person_info](#dbpersoninfo).
|
||||
|
||||
#### <a name="dbpersoninfo">`person_info`</a>
|
||||
```python
|
||||
# assumes photosdb is a PhotosDB object (see above)
|
||||
person_info = photosdb.person_info
|
||||
```
|
||||
|
||||
Returns a list of [PersonInfo](#personinfo) objects representing persons who appear in photos in the database.
|
||||
|
||||
#### `keywords_as_dict`
|
||||
```python
|
||||
@@ -796,7 +811,8 @@ Returns a dictionary of keywords found in the Photos library where key is the ke
|
||||
persons_dict = photosdb.persons_as_dict
|
||||
```
|
||||
|
||||
Returns a dictionary of persons (faces) found in the Photos library where key is the person name and value is the count of how many times that person appears in the library (ie. how many photos are tagged with the person). Resulting dictionary is in reverse sorted order (e.g. person who appears in the most photos is listed first).
|
||||
Returns a dictionary of persons (faces) found in the Photos library where key is the person name and value is the count of how many times that person appears in the library (ie. how many photos are tagged with the person). Resulting dictionary is in reverse sorted order (e.g. person who appears in the most photos is listed first). **Note**: It is of course possible to have more than one person with the same name, e.g. "Maria Smith", in the database. `persons_as_dict` assumes these are the same person and will list only one person named "Maria Smith". If you need more information about persons in the database, see [person_info](#dbpersoninfo).
|
||||
|
||||
|
||||
#### `albums_as_dict`
|
||||
```python
|
||||
@@ -882,8 +898,7 @@ for row in results:
|
||||
|
||||
conn.close()
|
||||
```
|
||||
|
||||
#### ` photos(keywords=None, uuid=None, persons=None, albums=None, images=True, movies=True, from_date=None, to_date=None, intrash=False)`
|
||||
#### <A name="photos">`photos(keywords=None, uuid=None, persons=None, albums=None, images=True, movies=True, from_date=None, to_date=None, intrash=False)`</a>
|
||||
|
||||
```python
|
||||
# assumes photosdb is a PhotosDB object (see above)
|
||||
@@ -919,6 +934,8 @@ photos = photosdb.photos(
|
||||
- ```to_date```: datetime.datetime; if provided, finds photos where creation date <= to_date; default is None
|
||||
- ```intrash```: if True, finds only photos in the "Recently Deleted" or trash folder, if False does not find any photos in the trash; default is False
|
||||
|
||||
See also [get_photo()](#getphoto) which is much faster for retrieving a single photo.
|
||||
|
||||
If more than one of (keywords, uuid, persons, albums,from_date, to_date) is provided, they are treated as "and" criteria. E.g.
|
||||
|
||||
Finds all photos with (keyword = "wedding" or "birthday") and (persons = "Juan Rodriguez")
|
||||
@@ -993,6 +1010,9 @@ For example, in my library, Photos says I have 19,386 photos and 474 movies. Ho
|
||||
>>>
|
||||
```
|
||||
|
||||
#### <a name="getphoto">`get_photo(uuid)`</A>
|
||||
Returns a single PhotoInfo instance for photo with UUID matching `uuid` or None if no photo is found matching `uuid`. If you know the UUID of a photo, `get_photo()` is much faster than `photos`. See also [photos()](#photos).
|
||||
|
||||
|
||||
### PhotoInfo
|
||||
PhotosDB.photos() returns a list of PhotoInfo objects. Each PhotoInfo object represents a single photo in the Photos library.
|
||||
@@ -1030,6 +1050,9 @@ Returns a list of [AlbumInfo](#AlbumInfo) objects representing the albums the ph
|
||||
#### `persons`
|
||||
Returns a list of the names of the persons in the photo
|
||||
|
||||
#### <a name="photopersoninfo">`person_info`</a>
|
||||
Returns a list of [PersonInfo](#personinfo) objects representing persons in the photo.
|
||||
|
||||
#### `path`
|
||||
Returns the absolute path to the photo on disk as a string. **Note**: this returns the path to the *original* unedited file (see [hasadjustments](#hasadjustments)). If the file is missing on disk, path=`None` (see [ismissing](#ismissing)).
|
||||
|
||||
@@ -1038,6 +1061,27 @@ Returns the absolute path to the edited photo on disk as a string. If the photo
|
||||
|
||||
**Note**: will also return None if the edited photo is missing on disk.
|
||||
|
||||
#### `height`
|
||||
Returns height of the photo in pixels. If image has been edited, returns height of the edited image, otherwise returns height of the original image. See also [original_height](#original_height).
|
||||
|
||||
#### `width`
|
||||
Returns width of the photo in pixels. If image has been edited, returns width of the edited image, otherwise returns width of the original image. See also [original_width](#original_width).
|
||||
|
||||
#### `orientation`
|
||||
Returns EXIF orientation value of the photo as integer. If image has been edited, returns orientation of the edited image, otherwise returns orientation of the original image. See also [original_orientation](#original_orientation).
|
||||
|
||||
#### `original_height`
|
||||
Returns height of the original photo in pixels. See also [height](#height).
|
||||
|
||||
#### `original_width`
|
||||
Returns width of the original photo in pixels. See also [width](#width).
|
||||
|
||||
#### `original_orientation`
|
||||
Returns EXIF orientation value of the original photo as integer. See also [orientation](#orientation).
|
||||
|
||||
#### `original_filesize`
|
||||
Returns size of the original photo in bytes as integer.
|
||||
|
||||
#### `ismissing`
|
||||
Returns `True` if the original image file is missing on disk, otherwise `False`. This can occur if the file has been uploaded to iCloud but not yet downloaded to the local library or if the file was deleted or imported from a disk that has been unmounted and user hasn't enabled "Copy items to the Photos library" in Photos preferences. **Note**: this status is computed based on data in the Photos library and `ismissing` does not verify if the photo is actually missing. See also [path](#path).
|
||||
|
||||
@@ -1256,11 +1300,13 @@ If overwrite=False and increment=False, export will fail if destination file alr
|
||||
|
||||
#### <a name="rendertemplate">`render_template()`</a>
|
||||
|
||||
`render_template(template_str, none_str = "_", path_sep = None)`
|
||||
`render_template(template_str, none_str = "_", path_sep = None, expand_inplace = False, inplace_sep = None)`
|
||||
Render template string for photo. none_str is used if template substitution results in None value and no default specified.
|
||||
- `template_str`: str in form "{name,DEFAULT}" where name is one of the values in table below. The "," and default value that follows are optional. If specified, "DEFAULT" will be used if "name" is None. This is useful for values which are not always present, for example reverse geolocation data.
|
||||
- `none_str`: optional str to use as substitution when template value is None and no default specified in the template string. default is "_".
|
||||
- `path_sep`: optional character to use as path separator, default is os.path.sep
|
||||
- `expand_inplace`: expand multi-valued substitutions in-place as a single string instead of returning individual strings
|
||||
- `inplace_sep`: optional string to use as separator between multi-valued keywords with expand_inplace; default is ','
|
||||
|
||||
Returns a tuple of (rendered, unmatched) where rendered is a list of rendered strings with all substitutions made and unmatched is a list of any strings that resembled a template substitution but did not match a known substitution. E.g. if template contained "{foo}", unmatched would be ["foo"].
|
||||
|
||||
@@ -1320,8 +1366,8 @@ Returns the universally unique identifier (uuid) of the album. This is how Phot
|
||||
#### `title`
|
||||
Returns the title or name of the album.
|
||||
|
||||
#### `photos`
|
||||
Returns a list of [PhotoInfo](#PhotoInfo) objects representing each photo contained in the album.
|
||||
#### <a name="albumphotos">`photos`</a>
|
||||
Returns a list of [PhotoInfo](#PhotoInfo) objects representing each photo contained in the album sorted in the same order as in Photos. (e.g. if photos were manually sorted in the Photos albums, photos returned by `photos` will be in same order as they appear in the Photos album)
|
||||
|
||||
#### `folder_list`
|
||||
Returns a hierarchical list of [FolderInfo](#FolderInfo) objects representing the folders the album is contained in. For example, if album "AlbumInFolder" is in SubFolder2 of Folder1 as illustrated below, would return a list of `FolderInfo` objects representing ["Folder1", "SubFolder2"]
|
||||
@@ -1495,6 +1541,31 @@ Example: find your "best" photo of food
|
||||
>>> best_food_photo = sorted([p for p in photos if "food" in p.labels_normalized], key=lambda p: p.score.overall, reverse=True)[0]
|
||||
```
|
||||
|
||||
### PersonInfo
|
||||
[PhotosDB.person_info](#dbpersoninfo) and [PhotoInfo.person_info](#photopersoninfo) return a list of PersonInfo objects represents persons in the database and in a photo, respectively. The PersonInfo class has the following properties and methods.
|
||||
|
||||
#### `name`
|
||||
Returns the full name of the person represented in the photo. For example, "Maria Smith".
|
||||
|
||||
#### `display_name`
|
||||
Returns the display name of the person represented in the photo. For example, "Maria".
|
||||
|
||||
#### `uuid`
|
||||
Returns the UUID of the person as stored in the Photos library database.
|
||||
|
||||
#### `keyphoto`
|
||||
Returns a PhotoInfo instance for the photo designated as the key photo for the person. This is the Photos uses to display the person's face thumbnail in Photos' "People" view.
|
||||
|
||||
#### `facecount`
|
||||
Returns a count of how many times this person appears in images in the database.
|
||||
|
||||
#### <a name="personphotos">`photos`</a>
|
||||
Returns a list of PhotoInfo objects representing all photos the person appears in.
|
||||
|
||||
#### `json()`
|
||||
Returns a json string representation of the PersonInfo instance.
|
||||
|
||||
|
||||
### Template Substitutions
|
||||
|
||||
The following substitutions are availabe for use with `PhotoInfo.render_template()`
|
||||
|
||||
@@ -58,5 +58,6 @@ if __name__ == "__main__":
|
||||
print("getting photos")
|
||||
tic = time.perf_counter()
|
||||
photos = photosdb.photos(images=True, movies=True)
|
||||
photos.extend(photosdb.photos(images=True, movies=True, intrash=True))
|
||||
toc = time.perf_counter()
|
||||
print(f"found {len(photos)} photos in {toc-tic} seconds")
|
||||
|
||||
@@ -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:
|
||||
@@ -1146,6 +1150,18 @@ def query(
|
||||
'--keyword-template "{created.year}" '
|
||||
"See Templating System below.",
|
||||
)
|
||||
@click.option(
|
||||
"--description-template",
|
||||
metavar="TEMPLATE",
|
||||
multiple=False,
|
||||
default=None,
|
||||
help="For use with --exiftool, --sidecar; specify a template string to use as "
|
||||
"description in the form '{name,DEFAULT}' "
|
||||
"This is the same format as --directory. For example, if you wanted to append "
|
||||
"'exported with osxphotos on [today's date]' to the description, you could specify "
|
||||
'--description-template "{descr} exported with osxphotos on {today.date}" '
|
||||
"See Templating System below.",
|
||||
)
|
||||
@click.option(
|
||||
"--current-name",
|
||||
is_flag=True,
|
||||
@@ -1260,6 +1276,7 @@ def export(
|
||||
person_keyword,
|
||||
album_keyword,
|
||||
keyword_template,
|
||||
description_template,
|
||||
current_name,
|
||||
sidecar,
|
||||
only_photos,
|
||||
@@ -1505,6 +1522,7 @@ def export(
|
||||
album_keyword=album_keyword,
|
||||
person_keyword=person_keyword,
|
||||
keyword_template=keyword_template,
|
||||
description_template=description_template,
|
||||
export_db=export_db,
|
||||
fileutil=fileutil,
|
||||
dry_run=dry_run,
|
||||
@@ -1541,6 +1559,7 @@ def export(
|
||||
album_keyword=album_keyword,
|
||||
person_keyword=person_keyword,
|
||||
keyword_template=keyword_template,
|
||||
description_template=description_template,
|
||||
export_db=export_db,
|
||||
fileutil=fileutil,
|
||||
dry_run=dry_run,
|
||||
@@ -2012,6 +2031,7 @@ def export_photo(
|
||||
album_keyword=None,
|
||||
person_keyword=None,
|
||||
keyword_template=None,
|
||||
description_template=None,
|
||||
export_db=None,
|
||||
fileutil=FileUtil,
|
||||
dry_run=None,
|
||||
@@ -2039,6 +2059,7 @@ def export_photo(
|
||||
album_keyword: boolean; if True, exports album names as keywords in metadata
|
||||
person_keyword: boolean; if True, exports person names as keywords in metadata
|
||||
keyword_template: list of strings; if provided use rendered template strings as keywords
|
||||
description_template: string; optional template string that will be rendered for use as photo description
|
||||
export_db: export database instance compatible with ExportDB_ABC
|
||||
fileutil: file util class compatible with FileUtilABC
|
||||
dry_run: boolean; if True, doesn't actually export or update any files
|
||||
@@ -2113,6 +2134,7 @@ def export_photo(
|
||||
use_albums_as_keywords=album_keyword,
|
||||
use_persons_as_keywords=person_keyword,
|
||||
keyword_template=keyword_template,
|
||||
description_template=description_template,
|
||||
update=update,
|
||||
export_db=export_db,
|
||||
fileutil=fileutil,
|
||||
@@ -2168,6 +2190,7 @@ def export_photo(
|
||||
use_albums_as_keywords=album_keyword,
|
||||
use_persons_as_keywords=person_keyword,
|
||||
keyword_template=keyword_template,
|
||||
description_template=description_template,
|
||||
update=update,
|
||||
export_db=export_db,
|
||||
fileutil=fileutil,
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
""" version info """
|
||||
|
||||
__version__ = "0.30.2"
|
||||
__version__ = "0.30.13"
|
||||
|
||||
@@ -48,8 +48,18 @@ class AlbumInfo:
|
||||
try:
|
||||
return self._photos
|
||||
except AttributeError:
|
||||
uuid = self._db._dbalbums_album[self._uuid]
|
||||
self._photos = self._db.photos(uuid=uuid)
|
||||
if self.uuid in self._db._dbalbums_album:
|
||||
uuid, sort_order = zip(*self._db._dbalbums_album[self.uuid])
|
||||
self._photos = self._db.photos(uuid=uuid)
|
||||
# PhotosDB.photos does not preserve order when passing in list of uuids
|
||||
# so need to build photo list one a time
|
||||
# sort uuids by sort order
|
||||
sorted_uuid = sorted(zip(sort_order, uuid))
|
||||
self._photos = [
|
||||
self._db.photos(uuid=[uuid])[0] for _, uuid in sorted_uuid
|
||||
]
|
||||
else:
|
||||
self._photos = []
|
||||
return self._photos
|
||||
|
||||
@property
|
||||
|
||||
77
osxphotos/personinfo.py
Normal file
77
osxphotos/personinfo.py
Normal file
@@ -0,0 +1,77 @@
|
||||
""" PhotoInfo methods to expose info about person in the Photos library """
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
|
||||
class PersonInfo:
|
||||
""" Info about a person in the Photos library
|
||||
"""
|
||||
|
||||
def __init__(self, db=None, pk=None):
|
||||
""" Creates a new PersonInfo instance
|
||||
|
||||
Arguments:
|
||||
db: instance of PhotosDB object
|
||||
pk: primary key value of person to initialize PersonInfo with
|
||||
|
||||
Returns:
|
||||
PersonInfo instance
|
||||
"""
|
||||
self._db = db
|
||||
self._pk = pk
|
||||
|
||||
person = self._db._dbpersons_pk[pk]
|
||||
self.uuid = person["uuid"]
|
||||
self.name = person["fullname"]
|
||||
self.display_name = person["displayname"]
|
||||
self.keyface = person["keyface"]
|
||||
self.facecount = person["facecount"]
|
||||
|
||||
@property
|
||||
def keyphoto(self):
|
||||
try:
|
||||
return self._keyphoto
|
||||
except AttributeError:
|
||||
person = self._db._dbpersons_pk[self._pk]
|
||||
if person["photo_uuid"]:
|
||||
try:
|
||||
key_photo = self._db.get_photo(person["photo_uuid"])
|
||||
except IndexError:
|
||||
key_photo = None
|
||||
else:
|
||||
key_photo = None
|
||||
self._keyphoto = key_photo
|
||||
return self._keyphoto
|
||||
|
||||
@property
|
||||
def photos(self):
|
||||
""" Returns list of PhotoInfo objects associated with this person """
|
||||
return self._db.photos_by_uuid(self._db._dbfaces_pk[self._pk])
|
||||
|
||||
def json(self):
|
||||
""" Returns JSON representation of class instance """
|
||||
keyphoto = self.keyphoto.uuid if self.keyphoto is not None else None
|
||||
person = {
|
||||
"uuid": self.uuid,
|
||||
"name": self.name,
|
||||
"displayname": self.display_name,
|
||||
"keyface": self.keyface,
|
||||
"facecount": self.facecount,
|
||||
"keyphoto": keyphoto,
|
||||
}
|
||||
return json.dumps(person)
|
||||
|
||||
def __str__(self):
|
||||
return f"PersonInfo(name={self.name}, display_name={self.display_name}, uuid={self.uuid}, facecount={self.facecount})"
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, type(self)):
|
||||
return False
|
||||
|
||||
return all(
|
||||
getattr(self, field) == getattr(other, field) for field in ["_db", "_pk"]
|
||||
)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
@@ -5,6 +5,7 @@ import os
|
||||
|
||||
from ..exiftool import ExifTool, get_exiftool_path
|
||||
|
||||
|
||||
@property
|
||||
def exiftool(self):
|
||||
""" Returns an ExifTool object for the photo
|
||||
@@ -26,8 +27,9 @@ def exiftool(self):
|
||||
except FileNotFoundError:
|
||||
# get_exiftool_path raises FileNotFoundError if exiftool not found
|
||||
exiftool = None
|
||||
logging.warning(f"exiftool not in path; download and install from https://exiftool.org/")
|
||||
logging.warning(
|
||||
f"exiftool not in path; download and install from https://exiftool.org/"
|
||||
)
|
||||
|
||||
self._exiftool = exiftool
|
||||
return self._exiftool
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ from .._constants import (
|
||||
from .._export_db import ExportDBNoOp
|
||||
from ..exiftool import ExifTool
|
||||
from ..fileutil import FileUtil
|
||||
from ..utils import dd_to_dms_str
|
||||
from ..utils import dd_to_dms_str, findfiles
|
||||
|
||||
ExportResults = namedtuple(
|
||||
"ExportResults", ["exported", "new", "updated", "skipped", "exif_updated"]
|
||||
@@ -215,6 +215,7 @@ def export(
|
||||
use_albums_as_keywords=False,
|
||||
use_persons_as_keywords=False,
|
||||
keyword_template=None,
|
||||
description_template=None,
|
||||
):
|
||||
""" export photo
|
||||
dest: must be valid destination path (or exception raised)
|
||||
@@ -250,6 +251,7 @@ def export(
|
||||
use_persons_as_keywords: (boolean, default = False); if True, will include person names in keywords
|
||||
when exporting metadata with exiftool or sidecar
|
||||
keyword_template: (list of strings); list of template strings that will be rendered as used as keywords
|
||||
description_template: string; optional template string that will be rendered for use as photo description
|
||||
returns: list of photos exported
|
||||
"""
|
||||
|
||||
@@ -273,6 +275,7 @@ def export(
|
||||
use_albums_as_keywords=use_albums_as_keywords,
|
||||
use_persons_as_keywords=use_persons_as_keywords,
|
||||
keyword_template=keyword_template,
|
||||
description_template=description_template,
|
||||
)
|
||||
|
||||
return results.exported
|
||||
@@ -297,6 +300,7 @@ def export2(
|
||||
use_albums_as_keywords=False,
|
||||
use_persons_as_keywords=False,
|
||||
keyword_template=None,
|
||||
description_template=None,
|
||||
update=False,
|
||||
export_db=None,
|
||||
fileutil=FileUtil,
|
||||
@@ -336,6 +340,7 @@ def export2(
|
||||
use_persons_as_keywords: (boolean, default = False); if True, will include person names in keywords
|
||||
when exporting metadata with exiftool or sidecar
|
||||
keyword_template: (list of strings); list of template strings that will be rendered as used as keywords
|
||||
description_template: string; optional template string that will be rendered for use as photo description
|
||||
update: (boolean, default=False); if True export will run in update mode, that is, it will
|
||||
not export the photo if the current version already exists in the destination
|
||||
export_db: (ExportDB_ABC); instance of a class that conforms to ExportDB_ABC with methods
|
||||
@@ -423,11 +428,10 @@ def export2(
|
||||
# dest will be file1 (1).jpeg even though file1.jpeg doesn't exist to prevent sidecar collision
|
||||
if not update and increment and not overwrite:
|
||||
count = 1
|
||||
glob_str = str(dest.parent / f"{dest.stem}*")
|
||||
dest_files = glob.glob(glob_str)
|
||||
dest_files = [pathlib.Path(f).stem for f in dest_files]
|
||||
dest_files = findfiles(f"{dest.stem}*", str(dest.parent))
|
||||
dest_files = [pathlib.Path(f).stem.lower() for f in dest_files]
|
||||
dest_new = dest.stem
|
||||
while dest_new in dest_files:
|
||||
while dest_new.lower() in dest_files:
|
||||
dest_new = f"{dest.stem} ({count})"
|
||||
count += 1
|
||||
dest = dest.parent / f"{dest_new}{dest.suffix}"
|
||||
@@ -670,6 +674,7 @@ def export2(
|
||||
use_albums_as_keywords=use_albums_as_keywords,
|
||||
use_persons_as_keywords=use_persons_as_keywords,
|
||||
keyword_template=keyword_template,
|
||||
description_template=description_template,
|
||||
)
|
||||
if not dry_run:
|
||||
try:
|
||||
@@ -685,6 +690,7 @@ def export2(
|
||||
use_albums_as_keywords=use_albums_as_keywords,
|
||||
use_persons_as_keywords=use_persons_as_keywords,
|
||||
keyword_template=keyword_template,
|
||||
description_template=description_template,
|
||||
)
|
||||
if not dry_run:
|
||||
try:
|
||||
@@ -712,6 +718,7 @@ def export2(
|
||||
use_albums_as_keywords=use_albums_as_keywords,
|
||||
use_persons_as_keywords=use_persons_as_keywords,
|
||||
keyword_template=keyword_template,
|
||||
description_template=description_template,
|
||||
)
|
||||
)[0]
|
||||
if old_data != current_data:
|
||||
@@ -727,6 +734,7 @@ def export2(
|
||||
use_albums_as_keywords=use_albums_as_keywords,
|
||||
use_persons_as_keywords=use_persons_as_keywords,
|
||||
keyword_template=keyword_template,
|
||||
description_template=description_template,
|
||||
)
|
||||
export_db.set_exifdata_for_file(
|
||||
exported_file,
|
||||
@@ -734,6 +742,7 @@ def export2(
|
||||
use_albums_as_keywords=use_albums_as_keywords,
|
||||
use_persons_as_keywords=use_persons_as_keywords,
|
||||
keyword_template=keyword_template,
|
||||
description_template=description_template,
|
||||
),
|
||||
)
|
||||
export_db.set_stat_exif_for_file(
|
||||
@@ -749,6 +758,7 @@ def export2(
|
||||
use_albums_as_keywords=use_albums_as_keywords,
|
||||
use_persons_as_keywords=use_persons_as_keywords,
|
||||
keyword_template=keyword_template,
|
||||
description_template=description_template,
|
||||
)
|
||||
export_db.set_exifdata_for_file(
|
||||
exported_file,
|
||||
@@ -756,6 +766,7 @@ def export2(
|
||||
use_albums_as_keywords=use_albums_as_keywords,
|
||||
use_persons_as_keywords=use_persons_as_keywords,
|
||||
keyword_template=keyword_template,
|
||||
description_template=description_template,
|
||||
),
|
||||
)
|
||||
export_db.set_stat_exif_for_file(
|
||||
@@ -955,6 +966,7 @@ def _write_exif_data(
|
||||
use_albums_as_keywords=False,
|
||||
use_persons_as_keywords=False,
|
||||
keyword_template=None,
|
||||
description_template=None,
|
||||
):
|
||||
""" write exif data to image file at filepath
|
||||
filepath: full path to the image file """
|
||||
@@ -966,6 +978,7 @@ def _write_exif_data(
|
||||
use_albums_as_keywords=use_albums_as_keywords,
|
||||
use_persons_as_keywords=use_persons_as_keywords,
|
||||
keyword_template=keyword_template,
|
||||
description_template=description_template,
|
||||
)
|
||||
)[0]
|
||||
for exiftag, val in exif_info.items():
|
||||
@@ -984,6 +997,7 @@ def _exiftool_json_sidecar(
|
||||
use_albums_as_keywords=False,
|
||||
use_persons_as_keywords=False,
|
||||
keyword_template=None,
|
||||
description_template=None,
|
||||
):
|
||||
""" return json string of EXIF details in exiftool sidecar format
|
||||
Does not include all the EXIF fields as those are likely already in the image
|
||||
@@ -1009,7 +1023,13 @@ def _exiftool_json_sidecar(
|
||||
exif = {}
|
||||
exif["_CreatedBy"] = "osxphotos, https://github.com/RhetTbull/osxphotos"
|
||||
|
||||
if self.description:
|
||||
if description_template is not None:
|
||||
description = self.render_template(
|
||||
description_template, expand_inplace=True, inplace_sep=", "
|
||||
)[0]
|
||||
exif["EXIF:ImageDescription"] = description
|
||||
exif["XMP:Description"] = description
|
||||
elif self.description:
|
||||
exif["EXIF:ImageDescription"] = self.description
|
||||
exif["XMP:Description"] = self.description
|
||||
|
||||
@@ -1082,7 +1102,6 @@ def _exiftool_json_sidecar(
|
||||
lat_str, lon_str = dd_to_dms_str(lat, lon)
|
||||
exif["EXIF:GPSLatitude"] = lat_str
|
||||
exif["EXIF:GPSLongitude"] = lon_str
|
||||
exif["Composite:GPSPosition"] = f"{lat_str}, {lon_str}"
|
||||
lat_ref = "North" if lat >= 0 else "South"
|
||||
lon_ref = "East" if lon >= 0 else "West"
|
||||
exif["EXIF:GPSLatitudeRef"] = lat_ref
|
||||
@@ -1112,16 +1131,25 @@ def _xmp_sidecar(
|
||||
use_albums_as_keywords=False,
|
||||
use_persons_as_keywords=False,
|
||||
keyword_template=None,
|
||||
description_template=None,
|
||||
):
|
||||
""" returns string for XMP sidecar
|
||||
use_albums_as_keywords: treat album names as keywords
|
||||
use_persons_as_keywords: treat person names as keywords
|
||||
keyword_template: (list of strings); list of template strings to render as keywords """
|
||||
keyword_template: (list of strings); list of template strings to render as keywords
|
||||
description_template: string; optional template string that will be rendered for use as photo description """
|
||||
|
||||
# TODO: add additional fields to XMP file?
|
||||
|
||||
xmp_template = Template(filename=os.path.join(_TEMPLATE_DIR, _XMP_TEMPLATE_NAME))
|
||||
|
||||
if description_template is not None:
|
||||
description = self.render_template(
|
||||
description_template, expand_inplace=True, inplace_sep=", "
|
||||
)[0]
|
||||
else:
|
||||
description = self.description if self.description is not None else ""
|
||||
|
||||
keyword_list = []
|
||||
if self.keywords:
|
||||
keyword_list.extend(self.keywords)
|
||||
@@ -1178,7 +1206,11 @@ def _xmp_sidecar(
|
||||
subject_list = list(self.keywords) + person_list
|
||||
|
||||
xmp_str = xmp_template.render(
|
||||
photo=self, keywords=keyword_list, persons=person_list, subjects=subject_list
|
||||
photo=self,
|
||||
description=description,
|
||||
keywords=keyword_list,
|
||||
persons=person_list,
|
||||
subjects=subject_list,
|
||||
)
|
||||
|
||||
# remove extra lines that mako inserts from template
|
||||
|
||||
@@ -29,6 +29,7 @@ from .._constants import (
|
||||
_PHOTOS_5_SHARED_PHOTO_PATH,
|
||||
)
|
||||
from ..albuminfo import AlbumInfo
|
||||
from ..personinfo import PersonInfo
|
||||
from ..phototemplate import PhotoTemplate
|
||||
from ..placeinfo import PlaceInfo4, PlaceInfo5
|
||||
from ..utils import _debug, _get_resource_loc, findfiles, get_preferred_uti_extension
|
||||
@@ -339,7 +340,18 @@ class PhotoInfo:
|
||||
@property
|
||||
def persons(self):
|
||||
""" list of persons in picture """
|
||||
return self._info["persons"]
|
||||
return [self._db._dbpersons_pk[pk]["fullname"] for pk in self._info["persons"]]
|
||||
|
||||
@property
|
||||
def person_info(self):
|
||||
""" list of PersonInfo objects for person in picture """
|
||||
try:
|
||||
return self._personinfo
|
||||
except AttributeError:
|
||||
self._personinfo = [
|
||||
PersonInfo(db=self._db, pk=pk) for pk in self._info["persons"]
|
||||
]
|
||||
return self._personinfo
|
||||
|
||||
@property
|
||||
def albums(self):
|
||||
@@ -642,7 +654,49 @@ class PhotoInfo:
|
||||
otherwise returns False """
|
||||
return self._info["raw_is_original"]
|
||||
|
||||
def render_template(self, template_str, none_str="_", path_sep=None):
|
||||
@property
|
||||
def height(self):
|
||||
""" returns height of the current photo version in pixels """
|
||||
return self._info["height"]
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
""" returns width of the current photo version in pixels """
|
||||
return self._info["width"]
|
||||
|
||||
@property
|
||||
def orientation(self):
|
||||
""" returns EXIF orientation of the current photo version as int """
|
||||
return self._info["orientation"]
|
||||
|
||||
@property
|
||||
def original_height(self):
|
||||
""" returns height of the original photo version in pixels """
|
||||
return self._info["original_height"]
|
||||
|
||||
@property
|
||||
def original_width(self):
|
||||
""" returns width of the original photo version in pixels """
|
||||
return self._info["original_width"]
|
||||
|
||||
@property
|
||||
def original_orientation(self):
|
||||
""" returns EXIF orientation of the original photo version as int """
|
||||
return self._info["original_orientation"]
|
||||
|
||||
@property
|
||||
def original_filesize(self):
|
||||
""" returns filesize of original photo in bytes as int """
|
||||
return self._info["original_filesize"]
|
||||
|
||||
def render_template(
|
||||
self,
|
||||
template_str,
|
||||
none_str="_",
|
||||
path_sep=None,
|
||||
expand_inplace=False,
|
||||
inplace_sep=None,
|
||||
):
|
||||
"""Renders a template string for PhotoInfo instance using PhotoTemplate
|
||||
|
||||
Args:
|
||||
@@ -650,12 +704,22 @@ class PhotoInfo:
|
||||
none_str: a str to use if template field renders to None, default is "_".
|
||||
path_sep: a single character str to use as path separator when joining
|
||||
fields like folder_album; if not provided, defaults to os.path.sep
|
||||
expand_inplace: expand multi-valued substitutions in-place as a single string
|
||||
instead of returning individual strings
|
||||
inplace_sep: optional string to use as separator between multi-valued keywords
|
||||
with expand_inplace; default is ','
|
||||
|
||||
Returns:
|
||||
([rendered_strings], [unmatched]): tuple of list of rendered strings and list of unmatched template values
|
||||
"""
|
||||
template = PhotoTemplate(self)
|
||||
return template.render(template_str, none_str, path_sep)
|
||||
return template.render(
|
||||
template_str,
|
||||
none_str=none_str,
|
||||
path_sep=path_sep,
|
||||
expand_inplace=expand_inplace,
|
||||
inplace_sep=inplace_sep,
|
||||
)
|
||||
|
||||
@property
|
||||
def _longitude(self):
|
||||
@@ -754,6 +818,13 @@ class PhotoInfo:
|
||||
"exif": exif,
|
||||
"score": score,
|
||||
"intrash": self.intrash,
|
||||
"height": self.height,
|
||||
"width": self.width,
|
||||
"orientation": self.orientation,
|
||||
"original_height": self.original_height,
|
||||
"original_width": self.original_width,
|
||||
"original_orientation": self.original_orientation,
|
||||
"original_filesize": self.original_filesize,
|
||||
}
|
||||
return yaml.dump(info, sort_keys=False)
|
||||
|
||||
@@ -814,6 +885,13 @@ class PhotoInfo:
|
||||
"exif": exif,
|
||||
"score": score,
|
||||
"intrash": self.intrash,
|
||||
"height": self.height,
|
||||
"width": self.width,
|
||||
"orientation": self.orientation,
|
||||
"original_height": self.original_height,
|
||||
"original_width": self.original_width,
|
||||
"original_orientation": self.original_orientation,
|
||||
"original_filesize": self.original_filesize,
|
||||
}
|
||||
return json.dumps(pic)
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ def _process_exifinfo_5(photosdb):
|
||||
photosdb: PhotosDB instance """
|
||||
|
||||
db = photosdb._tmp_db
|
||||
|
||||
|
||||
(conn, cursor) = _open_sql_file(db)
|
||||
|
||||
result = conn.execute(
|
||||
|
||||
@@ -34,6 +34,7 @@ from .._constants import (
|
||||
)
|
||||
from .._version import __version__
|
||||
from ..albuminfo import AlbumInfo, FolderInfo
|
||||
from ..personinfo import PersonInfo
|
||||
from ..photoinfo import PhotoInfo
|
||||
from ..utils import (
|
||||
_check_file_exists,
|
||||
@@ -44,7 +45,6 @@ from ..utils import (
|
||||
get_last_library_path,
|
||||
)
|
||||
|
||||
|
||||
# TODO: Add test for imageTimeZoneOffsetSeconds = None
|
||||
# TODO: Add test for __str__
|
||||
# TODO: Add special albums and magic albums
|
||||
@@ -87,6 +87,7 @@ class PhotosDB:
|
||||
|
||||
# set up the data structures used to store all the Photo database info
|
||||
|
||||
# TODO: I don't think these keywords flags are actually used
|
||||
# if True, will treat persons as keywords when exporting metadata
|
||||
self.use_persons_as_keywords = False
|
||||
|
||||
@@ -122,17 +123,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
|
||||
@@ -156,8 +168,8 @@ class PhotosDB:
|
||||
self._dbalbums_pk = {}
|
||||
|
||||
# Dict with information about all albums/photos by album
|
||||
# key is album UUID, value is list of photo UUIDs contained in that album
|
||||
# e.g. {'0C514A98-7B77-4E4F-801B-364B7B65EAFA': ['1EB2B765-0765-43BA-A90C-0D0580E6172C']}
|
||||
# key is album UUID, value is list of tuples of (photo UUID, sort order) contained in that album
|
||||
# e.g. {'0C514A98-7B77-4E4F-801B-364B7B65EAFA': [('1EB2B765-0765-43BA-A90C-0D0580E6172C', 1024)]}
|
||||
self._dbalbums_album = {}
|
||||
|
||||
# Dict with information about album details
|
||||
@@ -303,7 +315,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,9 +370,20 @@ 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
|
||||
def person_info(self):
|
||||
""" return list of PersonInfo objects for each person in the photos database """
|
||||
try:
|
||||
return self._person_info
|
||||
except AttributeError:
|
||||
self._person_info = [
|
||||
PersonInfo(db=self, pk=pk) for pk in self._dbpersons_pk
|
||||
]
|
||||
return self._person_info
|
||||
|
||||
@property
|
||||
def folder_info(self):
|
||||
""" return list FolderInfo objects representing top-level folders in the photos database """
|
||||
@@ -536,41 +565,139 @@ 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
|
||||
and RKVersion.isInTrash = 0 """
|
||||
""" SELECT
|
||||
RKPerson.modelID,
|
||||
RKPerson.uuid,
|
||||
RKPerson.name,
|
||||
RKPerson.faceCount,
|
||||
RKPerson.displayName,
|
||||
RKPerson.representativeFaceId
|
||||
FROM RKPerson
|
||||
"""
|
||||
)
|
||||
|
||||
# 0 RKPerson.modelID,
|
||||
# 1 RKPerson.uuid,
|
||||
# 2 RKPerson.name,
|
||||
# 3 RKPerson.faceCount,
|
||||
# 4 RKPerson.displayName
|
||||
# 5 RKPerson.representativeFaceId
|
||||
|
||||
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": person[5],
|
||||
"displayname": person[4],
|
||||
"photo_uuid": None,
|
||||
"keyface_uuid": None,
|
||||
}
|
||||
try:
|
||||
self._dbpersons_fullname[fullname].append(pk)
|
||||
except KeyError:
|
||||
self._dbpersons_fullname[fullname] = [pk]
|
||||
|
||||
# get info on key face
|
||||
c.execute(
|
||||
""" SELECT
|
||||
RKPerson.modelID,
|
||||
RKPerson.representativeFaceId,
|
||||
RKVersion.uuid,
|
||||
RKFace.uuid
|
||||
FROM RKPerson, RKFace, RKVersion
|
||||
WHERE
|
||||
RKFace.modelId = RKPerson.representativeFaceId AND
|
||||
RKVersion.modelId = RKFace.ImageModelId
|
||||
"""
|
||||
)
|
||||
|
||||
# 0 RKPerson.modelID,
|
||||
# 1 RKPerson.representativeFaceId
|
||||
# 2 RKVersion.uuid,
|
||||
# 3 RKFace.uuid
|
||||
|
||||
for person in c:
|
||||
pk = person[0]
|
||||
try:
|
||||
self._dbpersons_pk[pk]["photo_uuid"] = person[2]
|
||||
self._dbpersons_pk[pk]["keyface_uuid"] = person[3]
|
||||
except KeyError:
|
||||
logging.debug(f"Unexpected KeyError _dbpersons_pk[{pk}]")
|
||||
|
||||
# get information on detected faces
|
||||
c.execute(
|
||||
""" SELECT
|
||||
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(
|
||||
""" select
|
||||
""" SELECT
|
||||
RKAlbum.uuid,
|
||||
RKVersion.uuid
|
||||
from RKAlbum, RKVersion, RKAlbumVersion
|
||||
where RKAlbum.modelID = RKAlbumVersion.albumId and
|
||||
RKAlbumVersion.versionID = RKVersion.modelId
|
||||
and RKVersion.isInTrash = 0 """
|
||||
RKVersion.uuid,
|
||||
RKCustomSortOrder.orderNumber
|
||||
FROM RKVersion
|
||||
JOIN RKCustomSortOrder on RKCustomSortOrder.objectUuid = RKVersion.uuid
|
||||
JOIN RKAlbum on RKAlbum.uuid = RKCustomSortOrder.containerUuid
|
||||
"""
|
||||
)
|
||||
|
||||
# 0 RKAlbum.uuid,
|
||||
# 1 RKVersion.uuid,
|
||||
# 2 RKCustomSortOrder.orderNumber
|
||||
|
||||
for album in c:
|
||||
# store by uuid in _dbalbums_uuid and by album in _dbalbums_album
|
||||
if not album[1] in self._dbalbums_uuid:
|
||||
self._dbalbums_uuid[album[1]] = []
|
||||
if not album[0] in self._dbalbums_album:
|
||||
self._dbalbums_album[album[0]] = []
|
||||
self._dbalbums_uuid[album[1]].append(album[0])
|
||||
self._dbalbums_album[album[0]].append(album[1])
|
||||
album_uuid = album[0]
|
||||
photo_uuid = album[1]
|
||||
sort_order = album[2]
|
||||
try:
|
||||
self._dbalbums_uuid[photo_uuid].append(album_uuid)
|
||||
except KeyError:
|
||||
self._dbalbums_uuid[photo_uuid] = [album_uuid]
|
||||
|
||||
try:
|
||||
self._dbalbums_album[album_uuid].append((photo_uuid, sort_order))
|
||||
except KeyError:
|
||||
self._dbalbums_album[album_uuid] = [(photo_uuid, sort_order)]
|
||||
|
||||
# now get additional details about albums
|
||||
c.execute(
|
||||
@@ -680,12 +807,17 @@ class PhotosDB:
|
||||
|
||||
# Get info on keywords
|
||||
c.execute(
|
||||
""" select RKKeyword.name, RKVersion.uuid, RKMaster.uuid from
|
||||
""" SELECT
|
||||
RKKeyword.name,
|
||||
RKVersion.uuid,
|
||||
RKMaster.uuid
|
||||
FROM
|
||||
RKKeyword, RKKeywordForVersion, RKVersion, RKMaster
|
||||
where RKKeyword.modelId = RKKeyWordForVersion.keywordID and
|
||||
RKVersion.modelID = RKKeywordForVersion.versionID and
|
||||
RKMaster.uuid = RKVersion.masterUuid and
|
||||
RKVersion.isInTrash = 0 """
|
||||
WHERE
|
||||
RKKeyword.modelId = RKKeyWordForVersion.keywordID AND
|
||||
RKVersion.modelID = RKKeywordForVersion.versionID AND
|
||||
RKMaster.uuid = RKVersion.masterUuid
|
||||
"""
|
||||
)
|
||||
for keyword in c:
|
||||
if not keyword[1] in self._dbkeywords_uuid:
|
||||
@@ -716,7 +848,14 @@ class PhotosDB:
|
||||
RKVersion.rawMasterUuid,
|
||||
RKVersion.nonRawMasterUuid,
|
||||
RKMaster.alternateMasterUuid,
|
||||
RKVersion.isInTrash
|
||||
RKVersion.isInTrash,
|
||||
RKVersion.processedHeight,
|
||||
RKVersion.processedWidth,
|
||||
RKVersion.orientation,
|
||||
RKMaster.height,
|
||||
RKMaster.width,
|
||||
RKMaster.orientation,
|
||||
RKMaster.fileSize
|
||||
FROM RKVersion, RKMaster
|
||||
WHERE RKVersion.masterUuid = RKMaster.uuid"""
|
||||
)
|
||||
@@ -736,7 +875,14 @@ class PhotosDB:
|
||||
RKVersion.rawMasterUuid,
|
||||
RKVersion.nonRawMasterUuid,
|
||||
RKMaster.alternateMasterUuid,
|
||||
RKVersion.isInTrash
|
||||
RKVersion.isInTrash,
|
||||
RKVersion.processedHeight,
|
||||
RKVersion.processedWidth,
|
||||
RKVersion.orientation,
|
||||
RKMaster.height,
|
||||
RKMaster.width,
|
||||
RKMaster.orientation,
|
||||
RKMaster.originalFileSize
|
||||
FROM RKVersion, RKMaster
|
||||
WHERE RKVersion.masterUuid = RKMaster.uuid"""
|
||||
)
|
||||
@@ -775,6 +921,13 @@ class PhotosDB:
|
||||
# 30 RKVersion.nonRawMasterUuid, -- UUID of non-RAW master
|
||||
# 31 RKMaster.alternateMasterUuid -- UUID of alternate master (will be RAW master for JPEG and JPEG master for RAW)
|
||||
# 32 RKVersion.isInTrash
|
||||
# 33 RKVersion.processedHeight,
|
||||
# 34 RKVersion.processedWidth,
|
||||
# 35 RKVersion.orientation,
|
||||
# 36 RKMaster.height,
|
||||
# 37 RKMaster.width,
|
||||
# 38 RKMaster.orientation,
|
||||
# 39 RKMaster.originalFileSize
|
||||
|
||||
for row in c:
|
||||
uuid = row[0]
|
||||
@@ -920,6 +1073,15 @@ class PhotosDB:
|
||||
# recently deleted items
|
||||
self._dbphotos[uuid]["intrash"] = True if row[32] == 1 else False
|
||||
|
||||
# height/width/orientation
|
||||
self._dbphotos[uuid]["height"] = row[33]
|
||||
self._dbphotos[uuid]["width"] = row[34]
|
||||
self._dbphotos[uuid]["orientation"] = row[35]
|
||||
self._dbphotos[uuid]["original_height"] = row[36]
|
||||
self._dbphotos[uuid]["original_width"] = row[37]
|
||||
self._dbphotos[uuid]["original_orientation"] = row[38]
|
||||
self._dbphotos[uuid]["original_filesize"] = row[39]
|
||||
|
||||
# get additional details from RKMaster, needed for RAW processing
|
||||
c.execute(
|
||||
""" SELECT
|
||||
@@ -1198,8 +1360,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))
|
||||
@@ -1260,8 +1422,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")
|
||||
@@ -1275,45 +1441,136 @@ 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],
|
||||
"photo_uuid": None,
|
||||
"keyface_uuid": None,
|
||||
}
|
||||
try:
|
||||
self._dbpersons_fullname[fullname].append(pk)
|
||||
except KeyError:
|
||||
self._dbpersons_fullname[fullname] = [pk]
|
||||
|
||||
# get info on keyface -- some photos have null keyface so can't do a single query
|
||||
# (at least not with my SQL skills)
|
||||
c.execute(
|
||||
""" SELECT
|
||||
ZPERSON.Z_PK,
|
||||
ZPERSON.ZKEYFACE,
|
||||
ZGENERICASSET.ZUUID,
|
||||
ZDETECTEDFACE.ZUUID
|
||||
FROM ZPERSON, ZDETECTEDFACE, ZGENERICASSET
|
||||
WHERE ZDETECTEDFACE.Z_PK = ZPERSON.ZKEYFACE AND
|
||||
ZDETECTEDFACE.ZASSET = ZGENERICASSET.Z_PK
|
||||
"""
|
||||
)
|
||||
|
||||
# 0 ZPERSON.Z_PK,
|
||||
# 1 ZPERSON.ZKEYFACE,
|
||||
# 2 ZGENERICASSET.ZUUID,
|
||||
# 3 ZDETECTEDFACE.ZUUID
|
||||
|
||||
for person in c:
|
||||
pk = person[0]
|
||||
try:
|
||||
self._dbpersons_pk[pk]["photo_uuid"] = person[2]
|
||||
self._dbpersons_pk[pk]["keyface_uuid"] = person[3]
|
||||
except KeyError:
|
||||
logging.debug(f"Unexpected KeyError _dbpersons_pk[{pk}]")
|
||||
|
||||
# get information on detected faces
|
||||
c.execute(
|
||||
""" SELECT
|
||||
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(
|
||||
"SELECT ZGENERICALBUM.ZUUID, ZGENERICASSET.ZUUID "
|
||||
"FROM ZGENERICASSET "
|
||||
"JOIN Z_26ASSETS ON Z_26ASSETS.Z_34ASSETS = ZGENERICASSET.Z_PK "
|
||||
"JOIN ZGENERICALBUM ON ZGENERICALBUM.Z_PK = Z_26ASSETS.Z_26ALBUMS "
|
||||
""" SELECT
|
||||
ZGENERICALBUM.ZUUID,
|
||||
ZGENERICASSET.ZUUID,
|
||||
Z_26ASSETS.Z_FOK_34ASSETS
|
||||
FROM ZGENERICASSET
|
||||
JOIN Z_26ASSETS ON Z_26ASSETS.Z_34ASSETS = ZGENERICASSET.Z_PK
|
||||
JOIN ZGENERICALBUM ON ZGENERICALBUM.Z_PK = Z_26ASSETS.Z_26ALBUMS
|
||||
"""
|
||||
)
|
||||
|
||||
# 0 ZGENERICALBUM.ZUUID,
|
||||
# 1 ZGENERICASSET.ZUUID,
|
||||
# 2 Z_26ASSETS.Z_FOK_34ASSETS
|
||||
|
||||
for album in c:
|
||||
# store by uuid in _dbalbums_uuid and by album in _dbalbums_album
|
||||
album_uuid = album[0]
|
||||
photo_uuid = album[1]
|
||||
sort_order = album[2]
|
||||
try:
|
||||
self._dbalbums_uuid[album[1]].append(album[0])
|
||||
self._dbalbums_uuid[photo_uuid].append(album_uuid)
|
||||
except KeyError:
|
||||
self._dbalbums_uuid[album[1]] = [album[0]]
|
||||
self._dbalbums_uuid[photo_uuid] = [album_uuid]
|
||||
|
||||
try:
|
||||
self._dbalbums_album[album[0]].append(album[1])
|
||||
self._dbalbums_album[album_uuid].append((photo_uuid, sort_order))
|
||||
except KeyError:
|
||||
self._dbalbums_album[album[0]] = [album[1]]
|
||||
self._dbalbums_album[album_uuid] = [(photo_uuid, sort_order)]
|
||||
|
||||
# now get additional details about albums
|
||||
c.execute(
|
||||
@@ -1448,16 +1705,23 @@ class PhotosDB:
|
||||
ZGENERICASSET.ZCLOUDBATCHPUBLISHDATE,
|
||||
ZGENERICASSET.ZKIND,
|
||||
ZGENERICASSET.ZUNIFORMTYPEIDENTIFIER,
|
||||
ZGENERICASSET.ZAVALANCHEUUID,
|
||||
ZGENERICASSET.ZAVALANCHEPICKTYPE,
|
||||
ZGENERICASSET.ZAVALANCHEUUID,
|
||||
ZGENERICASSET.ZAVALANCHEPICKTYPE,
|
||||
ZGENERICASSET.ZKINDSUBTYPE,
|
||||
ZGENERICASSET.ZCUSTOMRENDEREDVALUE,
|
||||
ZADDITIONALASSETATTRIBUTES.ZCAMERACAPTUREDEVICE,
|
||||
ZGENERICASSET.ZCLOUDASSETGUID,
|
||||
ZADDITIONALASSETATTRIBUTES.ZREVERSELOCATIONDATA,
|
||||
ZGENERICASSET.ZMOMENT,
|
||||
ZADDITIONALASSETATTRIBUTES.ZORIGINALRESOURCECHOICE,
|
||||
ZGENERICASSET.ZTRASHEDSTATE
|
||||
ZADDITIONALASSETATTRIBUTES.ZORIGINALRESOURCECHOICE,
|
||||
ZGENERICASSET.ZTRASHEDSTATE,
|
||||
ZGENERICASSET.ZHEIGHT,
|
||||
ZGENERICASSET.ZWIDTH,
|
||||
ZGENERICASSET.ZORIENTATION,
|
||||
ZADDITIONALASSETATTRIBUTES.ZORIGINALHEIGHT,
|
||||
ZADDITIONALASSETATTRIBUTES.ZORIGINALWIDTH,
|
||||
ZADDITIONALASSETATTRIBUTES.ZORIGINALORIENTATION,
|
||||
ZADDITIONALASSETATTRIBUTES.ZORIGINALFILESIZE
|
||||
FROM ZGENERICASSET
|
||||
JOIN ZADDITIONALASSETATTRIBUTES ON ZADDITIONALASSETATTRIBUTES.ZASSET = ZGENERICASSET.Z_PK
|
||||
ORDER BY ZGENERICASSET.ZUUID """
|
||||
@@ -1493,6 +1757,13 @@ class PhotosDB:
|
||||
# 26 ZGENERICASSET.ZMOMENT -- FK for ZMOMENT.Z_PK
|
||||
# 27 ZADDITIONALASSETATTRIBUTES.ZORIGINALRESOURCECHOICE -- 1 if associated RAW image is original else 0
|
||||
# 28 ZGENERICASSET.ZTRASHEDSTATE -- 0 if not in trash, 1 if in trash
|
||||
# 29 ZGENERICASSET.ZHEIGHT,
|
||||
# 30 ZGENERICASSET.ZWIDTH,
|
||||
# 31 ZGENERICASSET.ZORIENTATION,
|
||||
# 32 ZADDITIONALASSETATTRIBUTES.ZORIGINALHEIGHT,
|
||||
# 33 ZADDITIONALASSETATTRIBUTES.ZORIGINALWIDTH,
|
||||
# 34 ZADDITIONALASSETATTRIBUTES.ZORIGINALORIENTATION,
|
||||
# 35 ZADDITIONALASSETATTRIBUTES.ZORIGINALFILESIZE
|
||||
|
||||
for row in c:
|
||||
uuid = row[0]
|
||||
@@ -1644,6 +1915,15 @@ class PhotosDB:
|
||||
# recently deleted items
|
||||
info["intrash"] = True if row[28] == 1 else False
|
||||
|
||||
# height/width/orientation
|
||||
info["height"] = row[29]
|
||||
info["width"] = row[30]
|
||||
info["orientation"] = row[31]
|
||||
info["original_height"] = row[32]
|
||||
info["original_width"] = row[33]
|
||||
info["original_orientation"] = row[34]
|
||||
info["original_filesize"] = row[35]
|
||||
|
||||
# associated RAW image info
|
||||
# will be filled in later
|
||||
info["has_raw"] = False
|
||||
@@ -1697,7 +1977,6 @@ class PhotosDB:
|
||||
"FROM ZGENERICASSET, ZUNMANAGEDADJUSTMENT "
|
||||
"JOIN ZADDITIONALASSETATTRIBUTES ON ZADDITIONALASSETATTRIBUTES.ZASSET = ZGENERICASSET.Z_PK "
|
||||
"WHERE ZADDITIONALASSETATTRIBUTES.ZUNMANAGEDADJUSTMENT = ZUNMANAGEDADJUSTMENT.Z_PK "
|
||||
"AND ZGENERICASSET.ZTRASHEDSTATE = 0 "
|
||||
)
|
||||
for row in c:
|
||||
uuid = row[0]
|
||||
@@ -1864,8 +2143,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))
|
||||
@@ -2196,7 +2475,9 @@ class PhotosDB:
|
||||
title_set = set()
|
||||
for album_id in self._dbalbum_titles[album]:
|
||||
try:
|
||||
title_set.update(self._dbalbums_album[album_id])
|
||||
# _dbalbums_album value is list of tuples: [(uuid, sort order)]
|
||||
uuid_in_album, _ = zip(*self._dbalbums_album[album_id])
|
||||
title_set.update(uuid_in_album)
|
||||
except KeyError:
|
||||
# an empty album will be in _dbalbum_titles but not _dbalbums_album
|
||||
pass
|
||||
@@ -2226,14 +2507,18 @@ 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]:
|
||||
try:
|
||||
person_set.update(self._dbfaces_pk[pk])
|
||||
except KeyError:
|
||||
# some persons have zero photos so they won't be in _dbfaces_pk
|
||||
pass
|
||||
else:
|
||||
logging.debug(f"Could not find person '{person}' in database")
|
||||
photos_sets.append(person_set)
|
||||
|
||||
# sourcery off
|
||||
if from_date or to_date:
|
||||
if from_date or to_date: # sourcery off
|
||||
dsel = self._dbphotos
|
||||
if from_date:
|
||||
dsel = {
|
||||
@@ -2267,6 +2552,42 @@ class PhotosDB:
|
||||
|
||||
return photoinfo
|
||||
|
||||
def get_photo(self, uuid):
|
||||
""" Returns a single photo matching uuid
|
||||
|
||||
Arguments:
|
||||
uuid: the UUID of photo to get
|
||||
|
||||
Returns:
|
||||
PhotoInfo instance for photo with UUID matching uuid or None if no match
|
||||
"""
|
||||
try:
|
||||
return PhotoInfo(db=self, uuid=uuid, info=self._dbphotos[uuid])
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
# TODO: add to docs and test
|
||||
def photos_by_uuid(self, uuids):
|
||||
""" Returns a list of photos with UUID in uuids.
|
||||
Does not generate error if invalid or missing UUID passed.
|
||||
This is faster than using PhotosDB.photos if you have list of UUIDs.
|
||||
Returns photos regardless of intrash state.
|
||||
|
||||
Arguments:
|
||||
uuid: list of UUIDs of photos to get
|
||||
|
||||
Returns:
|
||||
list of PhotoInfo instance for photo with UUID matching uuid or [] if no match
|
||||
"""
|
||||
photos = []
|
||||
for uuid in uuids:
|
||||
try:
|
||||
photos.append(PhotoInfo(db=self, uuid=uuid, info=self._dbphotos[uuid]))
|
||||
except KeyError:
|
||||
# ignore missing/invlaid UUID
|
||||
pass
|
||||
return photos
|
||||
|
||||
def __repr__(self):
|
||||
return f"osxphotos.{self.__class__.__name__}(dbfile='{self.db_path}')"
|
||||
|
||||
|
||||
@@ -124,13 +124,24 @@ class PhotoTemplate:
|
||||
# gets initialized in get_template_value
|
||||
self.today = None
|
||||
|
||||
def render(self, template, none_str="_", path_sep=None):
|
||||
def render(
|
||||
self,
|
||||
template,
|
||||
none_str="_",
|
||||
path_sep=None,
|
||||
expand_inplace=False,
|
||||
inplace_sep=None,
|
||||
):
|
||||
""" Render a filename or directory template
|
||||
|
||||
Args:
|
||||
template: str template
|
||||
none_str: str to use default for None values, default is '_'
|
||||
path_sep: optional character to use as path separator, default is os.path.sep
|
||||
expand_inplace: expand multi-valued substitutions in-place as a single string
|
||||
instead of returning individual strings
|
||||
inplace_sep: optional string to use as separator between multi-valued keywords
|
||||
with expand_inplace; default is ','
|
||||
|
||||
Returns:
|
||||
([rendered_strings], [unmatched]): tuple of list of rendered strings and list of unmatched template values
|
||||
@@ -141,6 +152,9 @@ class PhotoTemplate:
|
||||
elif path_sep is not None and len(path_sep) != 1:
|
||||
raise ValueError(f"path_sep must be single character: {path_sep}")
|
||||
|
||||
if inplace_sep is None:
|
||||
inplace_sep = ","
|
||||
|
||||
# the rendering happens in two phases:
|
||||
# phase 1: handle all the single-value template substitutions
|
||||
# results in a single string with all the template fields replaced
|
||||
@@ -226,13 +240,19 @@ class PhotoTemplate:
|
||||
for str_template in rendered_strings:
|
||||
if regex_multi.search(str_template):
|
||||
values = self.get_template_value_multi(field, path_sep)
|
||||
for val in values:
|
||||
if expand_inplace:
|
||||
# instead of returning multiple strings, join values into a single string
|
||||
val = (
|
||||
inplace_sep.join(sorted(values))
|
||||
if values and values[0]
|
||||
else None
|
||||
)
|
||||
|
||||
def lookup_template_value_multi(lookup_value, default):
|
||||
""" Closure passed to make_subst_function get_func
|
||||
Capture val and field in the closure
|
||||
Allows make_subst_function to be re-used w/o modification
|
||||
default is not used but required so signature matches get_template_value """
|
||||
Capture val and field in the closure
|
||||
Allows make_subst_function to be re-used w/o modification
|
||||
default is not used but required so signature matches get_template_value """
|
||||
if lookup_value == field:
|
||||
return val
|
||||
else:
|
||||
@@ -242,10 +262,33 @@ class PhotoTemplate:
|
||||
self, none_str, get_func=lookup_template_value_multi
|
||||
)
|
||||
new_string = regex_multi.sub(subst, str_template)
|
||||
new_strings.add(new_string)
|
||||
|
||||
# update rendered_strings for the next field to process
|
||||
rendered_strings = new_strings
|
||||
# update rendered_strings for the next field to process
|
||||
rendered_strings = {new_string}
|
||||
else:
|
||||
# create a new template string for each value
|
||||
for val in values:
|
||||
|
||||
def lookup_template_value_multi(lookup_value, default):
|
||||
""" Closure passed to make_subst_function get_func
|
||||
Capture val and field in the closure
|
||||
Allows make_subst_function to be re-used w/o modification
|
||||
default is not used but required so signature matches get_template_value """
|
||||
if lookup_value == field:
|
||||
return val
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unexpected value: {lookup_value}"
|
||||
)
|
||||
|
||||
subst = make_subst_function(
|
||||
self, none_str, get_func=lookup_template_value_multi
|
||||
)
|
||||
new_string = regex_multi.sub(subst, str_template)
|
||||
new_strings.add(new_string)
|
||||
|
||||
# update rendered_strings for the next field to process
|
||||
rendered_strings = new_strings
|
||||
|
||||
# find any {fields} that weren't replaced
|
||||
unmatched = []
|
||||
|
||||
@@ -71,29 +71,42 @@
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="gps_info(latitude, longitude)">
|
||||
% if latitude is not None and longitude is not None:
|
||||
<exif:GPSLongitudeRef>${"E" if longitude >= 0 else "W"}</exif:GPSLongitudeRef>
|
||||
<exif:GPSLongitude>${abs(longitude)}</exif:GPSLongitude>
|
||||
<exif:GPSLatitude>${abs(latitude)}</exif:GPSLatitude>
|
||||
<exif:GPSLatitudeRef>${"N" if latitude >= 0 else "S"}</exif:GPSLatitudeRef>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="XMP Core 5.4.0">
|
||||
<!-- mirrors Photos 5 "Export IPTC as XMP" option -->
|
||||
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:photoshop="http://ns.adobe.com/photoshop/1.0/">
|
||||
${dc_description(photo.description)}
|
||||
${dc_description(description)}
|
||||
${dc_title(photo.title)}
|
||||
${dc_subject(subjects)}
|
||||
${dc_datecreated(photo.date)}
|
||||
</rdf:Description>
|
||||
<rdf:Description rdf:about=''
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:Iptc4xmpExt='http://iptc.org/std/Iptc4xmpExt/2008-02-29/'>
|
||||
${iptc_personinimage(persons)}
|
||||
</rdf:Description>
|
||||
<rdf:Description rdf:about=''
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:digiKam='http://www.digikam.org/ns/1.0/'>
|
||||
${dk_tagslist(keywords)}
|
||||
</rdf:Description>
|
||||
<rdf:Description rdf:about=''
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:xmp='http://ns.adobe.com/xap/1.0/'>
|
||||
${adobe_createdate(photo.date)}
|
||||
${adobe_modifydate(photo.date)}
|
||||
</rdf:Description>
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:exif='http://ns.adobe.com/exif/1.0/'>
|
||||
${gps_info(*photo.location)}
|
||||
</rdf:Description>
|
||||
</rdf:RDF>
|
||||
</x:xmpmeta>
|
||||
@@ -262,7 +262,10 @@ def get_preferred_uti_extension(uti):
|
||||
|
||||
def findfiles(pattern, path_):
|
||||
"""Returns list of filenames from path_ matched by pattern
|
||||
shell pattern. Matching is case-insensitive."""
|
||||
shell pattern. Matching is case-insensitive.
|
||||
If 'path_' is invalid/doesn't exist, returns []."""
|
||||
if not os.path.isdir(path_):
|
||||
return []
|
||||
# See: https://gist.github.com/techtonik/5694830
|
||||
|
||||
rule = re.compile(fnmatch.translate(pattern), re.IGNORECASE)
|
||||
|
||||
@@ -5,9 +5,7 @@ astroid==2.2.5
|
||||
atomicwrites==1.3.0
|
||||
attrs==19.1.0
|
||||
better-exceptions-fork==0.2.1.post6
|
||||
# bpylist2==2.0.3;python_version<"3.8"
|
||||
https://github.com/RhetTbull/bpylist/releases/download/v2.0.3/bpylist2-2.0.3.tar.gz#egg=bpylist2;python_version<"3.8"
|
||||
bpylist2==3.0.0;python_version>="3.8"
|
||||
bpylist2==3.0.2
|
||||
certifi==2019.3.9
|
||||
Click==7.0
|
||||
colorama==0.4.1
|
||||
|
||||
16
setup.py
16
setup.py
@@ -48,17 +48,6 @@ with open(
|
||||
with open(os.path.join(this_directory, "README.md"), encoding="utf-8") as f:
|
||||
about["long_description"] = f.read()
|
||||
|
||||
# ugly hack to install custom version of bpylist2 needed for Python < 3.8
|
||||
# the stock version of bylist2==2.0.3 causes an error related to
|
||||
# "pkg_resources.ContextualVersionConflict: (pycodestyle 2.3.1..."
|
||||
# PEP 508 no help here as URL-based lookups not allowed in PyPI packages
|
||||
# if you know a better way, PRs welcome!
|
||||
# once I go to 3.8+ required, this won't be necessary as bpylist2 3.0+ solves this issue
|
||||
if py_ver < 3.8:
|
||||
os.system(
|
||||
"python3 -m pip install git+git://github.com/RhetTbull/bpylist2.git#egg=bpylist2"
|
||||
)
|
||||
|
||||
setup(
|
||||
name="osxphotos",
|
||||
version=about["__version__"],
|
||||
@@ -78,7 +67,7 @@ setup(
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: MacOS :: MacOS X",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||
],
|
||||
install_requires=[
|
||||
@@ -86,8 +75,7 @@ setup(
|
||||
"Click>=7",
|
||||
"PyYAML>=5.1.2",
|
||||
"Mako>=1.1.1",
|
||||
"bpylist2==2.0.3;python_version<'3.8'",
|
||||
"bpylist2==3.0.0;python_version>='3.8'",
|
||||
"bpylist2==3.0.2",
|
||||
"pathvalidate==2.2.1",
|
||||
"dataclasses==0.7;python_version<'3.7'",
|
||||
],
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -3,8 +3,8 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PhotoAnalysisGraphLastBackgroundGraphRebuildJobDate</key>
|
||||
<date>2020-04-25T23:54:43Z</date>
|
||||
<date>2020-07-16T04:41:20Z</date>
|
||||
<key>PhotoAnalysisGraphLastBackgroundMemoryGenerationJobDate</key>
|
||||
<date>2020-06-27T16:03:48Z</date>
|
||||
<date>2020-07-16T04:41:20Z</date>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -3,7 +3,7 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IncrementalPersonProcessingStage</key>
|
||||
<integer>0</integer>
|
||||
<integer>4</integer>
|
||||
<key>PersonBuilderLastMinimumFaceGroupSizeForCreatingMergeCandidates</key>
|
||||
<integer>15</integer>
|
||||
<key>PersonBuilderMergeCandidatesEnabled</key>
|
||||
|
||||
@@ -11,6 +11,6 @@
|
||||
<key>PLLastRevGeoForcedProviderOutOfDateCheckVersionKey</key>
|
||||
<integer>1</integer>
|
||||
<key>PLLastRevGeoVerFileFetchDateKey</key>
|
||||
<date>2020-06-27T16:03:43Z</date>
|
||||
<date>2020-07-16T04:41:16Z</date>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>LastHistoryRowId</key>
|
||||
<integer>651</integer>
|
||||
<integer>707</integer>
|
||||
<key>LibraryBuildTag</key>
|
||||
<string>D8C4AAA1-3AB6-4A65-BEBD-99CC3E5D433E</string>
|
||||
<key>LibrarySchemaVersion</key>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<key>HistoricalMarker</key>
|
||||
<dict>
|
||||
<key>LastHistoryRowId</key>
|
||||
<integer>606</integer>
|
||||
<integer>707</integer>
|
||||
<key>LibraryBuildTag</key>
|
||||
<string>D8C4AAA1-3AB6-4A65-BEBD-99CC3E5D433E</string>
|
||||
<key>LibrarySchemaVersion</key>
|
||||
@@ -24,7 +24,7 @@
|
||||
<key>SnapshotCompletedDate</key>
|
||||
<date>2019-07-27T13:16:43Z</date>
|
||||
<key>SnapshotLastValidated</key>
|
||||
<date>2020-06-27T16:03:33Z</date>
|
||||
<date>2020-07-16T04:41:16Z</date>
|
||||
<key>SnapshotTables</key>
|
||||
<dict/>
|
||||
</dict>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -7,7 +7,7 @@
|
||||
<key>hostuuid</key>
|
||||
<string>9575E48B-8D5F-5654-ABAC-4431B1167324</string>
|
||||
<key>pid</key>
|
||||
<integer>763</integer>
|
||||
<integer>3125</integer>
|
||||
<key>processname</key>
|
||||
<string>photolibraryd</string>
|
||||
<key>uid</key>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 524 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 196 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 60 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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}
|
||||
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ ALBUMS = [
|
||||
"Pumpkin Farm",
|
||||
"Test Album",
|
||||
"AlbumInFolder",
|
||||
"Raw"
|
||||
"Raw",
|
||||
] # Note: there are 2 albums named "Test Album" for testing duplicate album names
|
||||
KEYWORDS_DICT = {
|
||||
"Kids": 4,
|
||||
@@ -110,14 +110,14 @@ def test_init4():
|
||||
def test_init5(mocker):
|
||||
# test failed get_last_library_path
|
||||
import osxphotos
|
||||
|
||||
|
||||
def bad_library():
|
||||
return None
|
||||
|
||||
# get_last_library actually in utils but need to patch it in photosdb because it's imported into photosdb
|
||||
# because of the layout of photosdb/ need to patch it this way...don't really understand why, but it works
|
||||
mocker.patch("osxphotos.photosdb.photosdb.get_last_library_path", new=bad_library)
|
||||
|
||||
|
||||
with pytest.raises(Exception):
|
||||
assert osxphotos.PhotosDB()
|
||||
|
||||
@@ -127,7 +127,7 @@ def test_db_len():
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
# assert photosdb.db_version in osxphotos._TESTED_DB_VERSIONS
|
||||
assert len(photosdb) == 12
|
||||
assert len(photosdb) == 12
|
||||
|
||||
|
||||
def test_db_version():
|
||||
@@ -379,7 +379,7 @@ def test_count():
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos()
|
||||
assert len(photos) == 12
|
||||
assert len(photos) == 12
|
||||
|
||||
|
||||
def test_keyword_2():
|
||||
@@ -782,7 +782,7 @@ def test_from_to_date():
|
||||
photosdb = osxphotos.PhotosDB(PHOTOS_DB)
|
||||
|
||||
photos = photosdb.photos(from_date=dt.datetime(2018, 10, 28))
|
||||
assert len(photos) ==6
|
||||
assert len(photos) == 6
|
||||
|
||||
photos = photosdb.photos(to_date=dt.datetime(2018, 10, 28))
|
||||
assert len(photos) == 6
|
||||
|
||||
@@ -7,9 +7,9 @@ PHOTOS_DB = "tests/Test-10.15.5.photoslibrary/database/photos.db"
|
||||
PHOTOS_DB_PATH = "/Test-10.15.5.photoslibrary/database/photos.db"
|
||||
PHOTOS_LIBRARY_PATH = "/Test-10.15.5.photoslibrary"
|
||||
|
||||
PHOTOS_DB_LEN = 14
|
||||
PHOTOS_DB_LEN = 15
|
||||
PHOTOS_NOT_IN_TRASH_LEN = 13
|
||||
PHOTOS_IN_TRASH_LEN = 1
|
||||
PHOTOS_IN_TRASH_LEN = 2
|
||||
|
||||
KEYWORDS = [
|
||||
"Kids",
|
||||
@@ -34,7 +34,7 @@ ALBUMS = [
|
||||
]
|
||||
KEYWORDS_DICT = {
|
||||
"Kids": 4,
|
||||
"wedding": 2,
|
||||
"wedding": 3,
|
||||
"flowers": 1,
|
||||
"England": 1,
|
||||
"London": 1,
|
||||
@@ -43,7 +43,7 @@ KEYWORDS_DICT = {
|
||||
"UK": 1,
|
||||
"United Kingdom": 1,
|
||||
}
|
||||
PERSONS_DICT = {"Katie": 3, "Suzy": 2, "Maria": 1, _UNKNOWN_PERSON: 1}
|
||||
PERSONS_DICT = {"Katie": 3, "Suzy": 2, "Maria": 2, _UNKNOWN_PERSON: 1}
|
||||
ALBUM_DICT = {
|
||||
"Pumpkin Farm": 3,
|
||||
"Test Album": 2,
|
||||
@@ -71,6 +71,7 @@ UUID_DICT = {
|
||||
"date_invalid": "8846E3E6-8AC8-4857-8448-E3D025784410",
|
||||
"intrash": "71E3E212-00EB-430D-8A63-5E294B268554",
|
||||
"not_intrash": "DC99FBDD-7A52-4100-A5BB-344131646C30",
|
||||
"intrash_person_keywords": "6FD38366-3BF2-407D-81FE-7153EB6125B6",
|
||||
}
|
||||
|
||||
UUID_PUMPKIN_FARM = [
|
||||
@@ -79,6 +80,13 @@ UUID_PUMPKIN_FARM = [
|
||||
"1EB2B765-0765-43BA-A90C-0D0580E6172C",
|
||||
]
|
||||
|
||||
ALBUM_SORT_ORDER = [
|
||||
"1EB2B765-0765-43BA-A90C-0D0580E6172C",
|
||||
"F12384F6-CD17-4151-ACBA-AE0E3688539E",
|
||||
"D79B8D77-BFFC-460B-9312-034F2877D35B",
|
||||
]
|
||||
ALBUM_KEY_PHOTO = "D79B8D77-BFFC-460B-9312-034F2877D35B"
|
||||
|
||||
|
||||
def test_init1():
|
||||
# test named argument
|
||||
@@ -195,7 +203,7 @@ def test_keywords_dict():
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
keywords = photosdb.keywords_as_dict
|
||||
assert keywords["wedding"] == 2
|
||||
assert keywords["wedding"] == 3
|
||||
assert keywords == KEYWORDS_DICT
|
||||
|
||||
|
||||
@@ -204,7 +212,7 @@ def test_persons_as_dict():
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
persons = photosdb.persons_as_dict
|
||||
assert persons["Maria"] == 1
|
||||
assert persons["Maria"] == 2
|
||||
assert persons == PERSONS_DICT
|
||||
|
||||
|
||||
@@ -217,6 +225,26 @@ def test_albums_as_dict():
|
||||
assert albums == ALBUM_DICT
|
||||
|
||||
|
||||
def test_album_sort_order():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
album = [a for a in photosdb.album_info if a.title == "Pumpkin Farm"][0]
|
||||
photos = album.photos
|
||||
|
||||
uuids = [p.uuid for p in photos]
|
||||
assert uuids == ALBUM_SORT_ORDER
|
||||
|
||||
|
||||
def test_album_empty_album():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
album = [a for a in photosdb.album_info if a.title == "EmptyAlbum"][0]
|
||||
photos = album.photos
|
||||
assert photos == []
|
||||
|
||||
|
||||
def test_attributes():
|
||||
import datetime
|
||||
import osxphotos
|
||||
@@ -241,6 +269,46 @@ def test_attributes():
|
||||
assert p.ismissing == False
|
||||
|
||||
|
||||
def test_attributes_2():
|
||||
""" Test attributes including height, width, etc """
|
||||
import datetime
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["has_adjustments"]])
|
||||
assert len(photos) == 1
|
||||
p = photos[0]
|
||||
assert p.keywords == ["wedding"]
|
||||
assert p.original_filename == "wedding.jpg"
|
||||
assert p.filename == "E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51.jpeg"
|
||||
assert p.date == datetime.datetime(
|
||||
2019,
|
||||
4,
|
||||
15,
|
||||
14,
|
||||
40,
|
||||
24,
|
||||
86000,
|
||||
datetime.timezone(datetime.timedelta(seconds=-14400)),
|
||||
)
|
||||
assert p.description == "Bride Wedding day"
|
||||
assert p.title is None
|
||||
assert sorted(p.albums) == ["AlbumInFolder", "I have a deleted twin"]
|
||||
assert p.persons == ["Maria"]
|
||||
assert p.path.endswith(
|
||||
"tests/Test-10.15.5.photoslibrary/originals/E/E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51.jpeg"
|
||||
)
|
||||
assert not p.ismissing
|
||||
assert p.hasadjustments
|
||||
assert p.height == 1325
|
||||
assert p.width == 1526
|
||||
assert p.original_height == 1367
|
||||
assert p.original_width == 2048
|
||||
assert p.orientation == 1
|
||||
assert p.original_orientation == 1
|
||||
assert p.original_filesize == 460483
|
||||
|
||||
|
||||
def test_missing():
|
||||
import osxphotos
|
||||
|
||||
@@ -419,7 +487,7 @@ def test_photos_intrash_2():
|
||||
assert p.intrash
|
||||
|
||||
|
||||
def test_photos_intrash_2():
|
||||
def test_photos_intrash_3():
|
||||
""" test PhotosDB.photos(intrash=False) """
|
||||
import osxphotos
|
||||
|
||||
@@ -447,6 +515,39 @@ def test_photoinfo_intrash_2():
|
||||
assert not p
|
||||
|
||||
|
||||
def test_photoinfo_intrash_3():
|
||||
""" Test PhotoInfo.intrash and photo has keyword and person """
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
p = photosdb.photos(uuid=[UUID_DICT["intrash_person_keywords"]], intrash=True)[0]
|
||||
assert p.intrash
|
||||
assert "Maria" in p.persons
|
||||
assert "wedding" in p.keywords
|
||||
|
||||
|
||||
def test_photoinfo_intrash_4():
|
||||
""" Test PhotoInfo.intrash and photo has keyword and person """
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
p = photosdb.photos(persons=["Maria"], intrash=True)[0]
|
||||
assert p.intrash
|
||||
assert "Maria" in p.persons
|
||||
assert "wedding" in p.keywords
|
||||
|
||||
|
||||
def test_photoinfo_intrash_5():
|
||||
""" Test PhotoInfo.intrash and photo has keyword and person """
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
p = photosdb.photos(keywords=["wedding"], intrash=True)[0]
|
||||
assert p.intrash
|
||||
assert "Maria" in p.persons
|
||||
assert "wedding" in p.keywords
|
||||
|
||||
|
||||
def test_photoinfo_not_intrash():
|
||||
""" Test PhotoInfo.intrash """
|
||||
import osxphotos
|
||||
@@ -461,7 +562,7 @@ def test_keyword_2():
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(keywords=["wedding"])
|
||||
assert len(photos) == 2
|
||||
assert len(photos) == 2 # won't show the one in the trash
|
||||
|
||||
|
||||
def test_keyword_not_in_album():
|
||||
@@ -487,6 +588,15 @@ def test_album_folder_name():
|
||||
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
|
||||
|
||||
@@ -964,4 +1074,3 @@ def test_date_modified_invalid():
|
||||
assert len(photos) == 1
|
||||
p = photos[0]
|
||||
assert p.date_modified is None
|
||||
|
||||
|
||||
@@ -197,7 +197,13 @@ CLI_EXPORT_RAW_EDITED = [
|
||||
]
|
||||
CLI_EXPORT_RAW_EDITED_ORIGINAL = ["IMG_0476_2.CR2", "IMG_0476_2_edited.jpeg"]
|
||||
|
||||
CLI_UUID_DICT_15_5 = {"intrash": "71E3E212-00EB-430D-8A63-5E294B268554"}
|
||||
CLI_UUID_DICT_15_5 = {
|
||||
"intrash": "71E3E212-00EB-430D-8A63-5E294B268554",
|
||||
"template": "F12384F6-CD17-4151-ACBA-AE0E3688539E",
|
||||
}
|
||||
|
||||
CLI_TEMPLATE_SIDECAR_FILENAME = "Pumkins1.json"
|
||||
|
||||
CLI_UUID_DICT_14_6 = {"intrash": "3tljdX43R8+k6peNHVrJNQ"}
|
||||
|
||||
PHOTOS_NOT_IN_TRASH_LEN_14_6 = 7
|
||||
@@ -205,7 +211,7 @@ PHOTOS_IN_TRASH_LEN_14_6 = 1
|
||||
PHOTOS_MISSING_14_6 = 1
|
||||
|
||||
PHOTOS_NOT_IN_TRASH_LEN_15_5 = 13
|
||||
PHOTOS_IN_TRASH_LEN_15_5 = 1
|
||||
PHOTOS_IN_TRASH_LEN_15_5 = 2
|
||||
PHOTOS_MISSING_15_5 = 2
|
||||
|
||||
CLI_PLACES_JSON = """{"places": {"_UNKNOWN_": 1, "Maui, Wailea, Hawai'i, United States": 1, "Washington, District of Columbia, United States": 1}}"""
|
||||
@@ -225,12 +231,18 @@ CLI_EXIFTOOL = {
|
||||
|
||||
LABELS_JSON = {
|
||||
"labels": {
|
||||
"Plant": 5,
|
||||
"Plant": 7,
|
||||
"Outdoor": 4,
|
||||
"Sky": 3,
|
||||
"Tree": 2,
|
||||
"Sky": 2,
|
||||
"Outdoor": 2,
|
||||
"Art": 2,
|
||||
"Foliage": 2,
|
||||
"People": 2,
|
||||
"Agriculture": 2,
|
||||
"Farm": 2,
|
||||
"Food": 2,
|
||||
"Vegetable": 2,
|
||||
"Pumpkin": 2,
|
||||
"Waterways": 1,
|
||||
"River": 1,
|
||||
"Cloudy": 1,
|
||||
@@ -248,13 +260,17 @@ LABELS_JSON = {
|
||||
"Vase": 1,
|
||||
"Container": 1,
|
||||
"Camera": 1,
|
||||
"Child": 1,
|
||||
"Clothing": 1,
|
||||
"Jeans": 1,
|
||||
"Straw Hay": 1,
|
||||
}
|
||||
}
|
||||
|
||||
KEYWORDS_JSON = {
|
||||
"keywords": {
|
||||
"Kids": 4,
|
||||
"wedding": 2,
|
||||
"wedding": 3,
|
||||
"London 2018": 1,
|
||||
"St. James's Park": 1,
|
||||
"England": 1,
|
||||
@@ -277,7 +293,7 @@ ALBUMS_JSON = {
|
||||
"shared albums": {},
|
||||
}
|
||||
|
||||
PERSONS_JSON = {"persons": {"Katie": 3, "Suzy": 2, "_UNKNOWN_": 1, "Maria": 1}}
|
||||
PERSONS_JSON = {"persons": {"Katie": 3, "Suzy": 2, "_UNKNOWN_": 1, "Maria": 2}}
|
||||
|
||||
# determine if exiftool installed so exiftool tests can be skipped
|
||||
try:
|
||||
@@ -965,7 +981,7 @@ def test_query_label_4():
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
json_got = json.loads(result.output)
|
||||
assert len(json_got) == 6
|
||||
assert len(json_got) == 8
|
||||
|
||||
|
||||
def test_query_deleted_deleted_only():
|
||||
@@ -1095,6 +1111,48 @@ def test_export_sidecar():
|
||||
assert sorted(files) == sorted(CLI_EXPORT_SIDECAR_FILENAMES)
|
||||
|
||||
|
||||
def test_export_sidecar_templates():
|
||||
import json
|
||||
import os
|
||||
import os.path
|
||||
import osxphotos
|
||||
|
||||
from osxphotos.__main__ import cli
|
||||
|
||||
runner = CliRunner()
|
||||
cwd = os.getcwd()
|
||||
# pylint: disable=not-context-manager
|
||||
with runner.isolated_filesystem():
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[
|
||||
"export",
|
||||
"--db",
|
||||
os.path.join(cwd, PHOTOS_DB_15_5),
|
||||
".",
|
||||
"--sidecar=json",
|
||||
f"--uuid={CLI_UUID_DICT_15_5['template']}",
|
||||
"-V",
|
||||
"--keyword-template",
|
||||
"{person}",
|
||||
"--description-template",
|
||||
"{descr} {person} {keyword} {album}",
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert os.path.isfile(CLI_TEMPLATE_SIDECAR_FILENAME)
|
||||
with open(CLI_TEMPLATE_SIDECAR_FILENAME, "r") as jsonfile:
|
||||
exifdata = json.load(jsonfile)
|
||||
assert (
|
||||
exifdata[0]["XMP:Description"][0]
|
||||
== "Girls with pumpkins Katie, Suzy Kids Pumpkin Farm, Test Album"
|
||||
)
|
||||
assert (
|
||||
exifdata[0]["EXIF:ImageDescription"][0]
|
||||
== "Girls with pumpkins Katie, Suzy Kids Pumpkin Farm, Test Album"
|
||||
)
|
||||
|
||||
|
||||
def test_export_live():
|
||||
import glob
|
||||
import os
|
||||
|
||||
@@ -455,7 +455,6 @@ def test_exiftool_json_sidecar():
|
||||
"XMP:Subject": ["London 2018", "St. James\'s Park", "England", "United Kingdom", "UK", "London"],
|
||||
"EXIF:GPSLatitude": "51 deg 30\' 12.86\\" N",
|
||||
"EXIF:GPSLongitude": "0 deg 7\' 54.50\\" W",
|
||||
"Composite:GPSPosition": "51 deg 30\' 12.86\\" N, 0 deg 7\' 54.50\\" W",
|
||||
"EXIF:GPSLatitudeRef": "North", "EXIF:GPSLongitudeRef": "West",
|
||||
"EXIF:DateTimeOriginal": "2018:10:13 09:18:12",
|
||||
"EXIF:OffsetTimeOriginal": "-04:00",
|
||||
@@ -586,7 +585,7 @@ def test_xmp_sidecar():
|
||||
</dc:subject>
|
||||
<photoshop:DateCreated>2018-09-28T15:35:49.063000-04:00</photoshop:DateCreated>
|
||||
</rdf:Description>
|
||||
<rdf:Description rdf:about=''
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:Iptc4xmpExt='http://iptc.org/std/Iptc4xmpExt/2008-02-29/'>
|
||||
<Iptc4xmpExt:PersonInImage>
|
||||
<rdf:Bag>
|
||||
@@ -595,7 +594,7 @@ def test_xmp_sidecar():
|
||||
</rdf:Bag>
|
||||
</Iptc4xmpExt:PersonInImage>
|
||||
</rdf:Description>
|
||||
<rdf:Description rdf:about=''
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:digiKam='http://www.digikam.org/ns/1.0/'>
|
||||
<digiKam:TagsList>
|
||||
<rdf:Seq>
|
||||
@@ -603,10 +602,13 @@ def test_xmp_sidecar():
|
||||
</rdf:Seq>
|
||||
</digiKam:TagsList>
|
||||
</rdf:Description>
|
||||
<rdf:Description rdf:about=''
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:xmp='http://ns.adobe.com/xap/1.0/'>
|
||||
<xmp:CreateDate>2018-09-28T15:35:49</xmp:CreateDate>
|
||||
<xmp:ModifyDate>2018-09-28T15:35:49</xmp:ModifyDate>
|
||||
</rdf:Description>
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:exif='http://ns.adobe.com/exif/1.0/'>
|
||||
</rdf:Description>
|
||||
</rdf:RDF>
|
||||
</x:xmpmeta>"""
|
||||
@@ -647,7 +649,7 @@ def test_xmp_sidecar_use_persons_keyword():
|
||||
</dc:subject>
|
||||
<photoshop:DateCreated>2018-09-28T15:35:49.063000-04:00</photoshop:DateCreated>
|
||||
</rdf:Description>
|
||||
<rdf:Description rdf:about=''
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:Iptc4xmpExt='http://iptc.org/std/Iptc4xmpExt/2008-02-29/'>
|
||||
<Iptc4xmpExt:PersonInImage>
|
||||
<rdf:Bag>
|
||||
@@ -656,7 +658,7 @@ def test_xmp_sidecar_use_persons_keyword():
|
||||
</rdf:Bag>
|
||||
</Iptc4xmpExt:PersonInImage>
|
||||
</rdf:Description>
|
||||
<rdf:Description rdf:about=''
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:digiKam='http://www.digikam.org/ns/1.0/'>
|
||||
<digiKam:TagsList>
|
||||
<rdf:Seq>
|
||||
@@ -666,11 +668,14 @@ def test_xmp_sidecar_use_persons_keyword():
|
||||
</rdf:Seq>
|
||||
</digiKam:TagsList>
|
||||
</rdf:Description>
|
||||
<rdf:Description rdf:about=''
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:xmp='http://ns.adobe.com/xap/1.0/'>
|
||||
<xmp:CreateDate>2018-09-28T15:35:49</xmp:CreateDate>
|
||||
<xmp:ModifyDate>2018-09-28T15:35:49</xmp:ModifyDate>
|
||||
</rdf:Description>
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:exif='http://ns.adobe.com/exif/1.0/'>
|
||||
</rdf:Description>
|
||||
</rdf:RDF>
|
||||
</x:xmpmeta>"""
|
||||
|
||||
@@ -710,7 +715,7 @@ def test_xmp_sidecar_use_albums_keyword():
|
||||
</dc:subject>
|
||||
<photoshop:DateCreated>2018-09-28T15:35:49.063000-04:00</photoshop:DateCreated>
|
||||
</rdf:Description>
|
||||
<rdf:Description rdf:about=''
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:Iptc4xmpExt='http://iptc.org/std/Iptc4xmpExt/2008-02-29/'>
|
||||
<Iptc4xmpExt:PersonInImage>
|
||||
<rdf:Bag>
|
||||
@@ -719,7 +724,7 @@ def test_xmp_sidecar_use_albums_keyword():
|
||||
</rdf:Bag>
|
||||
</Iptc4xmpExt:PersonInImage>
|
||||
</rdf:Description>
|
||||
<rdf:Description rdf:about=''
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:digiKam='http://www.digikam.org/ns/1.0/'>
|
||||
<digiKam:TagsList>
|
||||
<rdf:Seq>
|
||||
@@ -729,11 +734,14 @@ def test_xmp_sidecar_use_albums_keyword():
|
||||
</rdf:Seq>
|
||||
</digiKam:TagsList>
|
||||
</rdf:Description>
|
||||
<rdf:Description rdf:about=''
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:xmp='http://ns.adobe.com/xap/1.0/'>
|
||||
<xmp:CreateDate>2018-09-28T15:35:49</xmp:CreateDate>
|
||||
<xmp:ModifyDate>2018-09-28T15:35:49</xmp:ModifyDate>
|
||||
</rdf:Description>
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:exif='http://ns.adobe.com/exif/1.0/'>
|
||||
</rdf:Description>
|
||||
</rdf:RDF>
|
||||
</x:xmpmeta>"""
|
||||
|
||||
@@ -746,3 +754,74 @@ def test_xmp_sidecar_use_albums_keyword():
|
||||
sorted(xmp_expected_lines), sorted(xmp_got_lines)
|
||||
):
|
||||
assert line_expected == line_got
|
||||
|
||||
|
||||
def test_xmp_sidecar_gps():
|
||||
""" Test export XMP sidecar with GPS info """
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["location"]])
|
||||
|
||||
xmp_expected = """<!-- Created with osxphotos https://github.com/RhetTbull/osxphotos -->
|
||||
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="XMP Core 5.4.0">
|
||||
<!-- mirrors Photos 5 "Export IPTC as XMP" option -->
|
||||
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:photoshop="http://ns.adobe.com/photoshop/1.0/">
|
||||
<dc:description></dc:description>
|
||||
<dc:title>St. James's Park</dc:title>
|
||||
<!-- keywords and persons listed in <dc:subject> as Photos does -->
|
||||
<dc:subject>
|
||||
<rdf:Seq>
|
||||
<rdf:li>UK</rdf:li>
|
||||
<rdf:li>England</rdf:li>
|
||||
<rdf:li>London</rdf:li>
|
||||
<rdf:li>United Kingdom</rdf:li>
|
||||
<rdf:li>London 2018</rdf:li>
|
||||
<rdf:li>St. James's Park</rdf:li>
|
||||
</rdf:Seq>
|
||||
</dc:subject>
|
||||
<photoshop:DateCreated>2018-10-13T09:18:12.501000-04:00</photoshop:DateCreated>
|
||||
</rdf:Description>
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:Iptc4xmpExt='http://iptc.org/std/Iptc4xmpExt/2008-02-29/'>
|
||||
</rdf:Description>
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:digiKam='http://www.digikam.org/ns/1.0/'>
|
||||
<digiKam:TagsList>
|
||||
<rdf:Seq>
|
||||
<rdf:li>UK</rdf:li>
|
||||
<rdf:li>England</rdf:li>
|
||||
<rdf:li>London</rdf:li>
|
||||
<rdf:li>United Kingdom</rdf:li>
|
||||
<rdf:li>London 2018</rdf:li>
|
||||
<rdf:li>St. James's Park</rdf:li>
|
||||
</rdf:Seq>
|
||||
</digiKam:TagsList>
|
||||
</rdf:Description>
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:xmp='http://ns.adobe.com/xap/1.0/'>
|
||||
<xmp:CreateDate>2018-10-13T09:18:12</xmp:CreateDate>
|
||||
<xmp:ModifyDate>2018-10-13T09:18:12</xmp:ModifyDate>
|
||||
</rdf:Description>
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:exif='http://ns.adobe.com/exif/1.0/'>
|
||||
<exif:GPSLongitudeRef>W</exif:GPSLongitudeRef>
|
||||
<exif:GPSLongitude>0.1318055</exif:GPSLongitude>
|
||||
<exif:GPSLatitude>51.50357167</exif:GPSLatitude>
|
||||
<exif:GPSLatitudeRef>N</exif:GPSLatitudeRef>
|
||||
</rdf:Description>
|
||||
</rdf:RDF>
|
||||
</x:xmpmeta>"""
|
||||
|
||||
xmp_expected_lines = [line.strip() for line in xmp_expected.split("\n")]
|
||||
|
||||
xmp_got = photos[0]._xmp_sidecar()
|
||||
xmp_got_lines = [line.strip() for line in xmp_got.split("\n")]
|
||||
|
||||
for line_expected, line_got in zip(
|
||||
sorted(xmp_expected_lines), sorted(xmp_got_lines)
|
||||
):
|
||||
assert line_expected == line_got
|
||||
|
||||
@@ -178,7 +178,7 @@ def test_xmp_sidecar_keyword_template():
|
||||
</dc:subject>
|
||||
<photoshop:DateCreated>2018-09-28T15:35:49.063000-04:00</photoshop:DateCreated>
|
||||
</rdf:Description>
|
||||
<rdf:Description rdf:about=''
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:Iptc4xmpExt='http://iptc.org/std/Iptc4xmpExt/2008-02-29/'>
|
||||
<Iptc4xmpExt:PersonInImage>
|
||||
<rdf:Bag>
|
||||
@@ -187,7 +187,7 @@ def test_xmp_sidecar_keyword_template():
|
||||
</rdf:Bag>
|
||||
</Iptc4xmpExt:PersonInImage>
|
||||
</rdf:Description>
|
||||
<rdf:Description rdf:about=''
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:digiKam='http://www.digikam.org/ns/1.0/'>
|
||||
<digiKam:TagsList>
|
||||
<rdf:Seq>
|
||||
@@ -198,11 +198,14 @@ def test_xmp_sidecar_keyword_template():
|
||||
</rdf:Seq>
|
||||
</digiKam:TagsList>
|
||||
</rdf:Description>
|
||||
<rdf:Description rdf:about=''
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:xmp='http://ns.adobe.com/xap/1.0/'>
|
||||
<xmp:CreateDate>2018-09-28T15:35:49</xmp:CreateDate>
|
||||
<xmp:ModifyDate>2018-09-28T15:35:49</xmp:ModifyDate>
|
||||
</rdf:Description>
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:exif='http://ns.adobe.com/exif/1.0/'>
|
||||
</rdf:Description>
|
||||
</rdf:RDF>
|
||||
</x:xmpmeta>"""
|
||||
|
||||
|
||||
@@ -380,7 +380,6 @@ def test_exiftool_json_sidecar():
|
||||
"XMP:Subject": ["London 2018", "St. James\'s Park", "England", "United Kingdom", "UK", "London"],
|
||||
"EXIF:GPSLatitude": "51 deg 30\' 12.86\\" N",
|
||||
"EXIF:GPSLongitude": "0 deg 7\' 54.50\\" W",
|
||||
"Composite:GPSPosition": "51 deg 30\' 12.86\\" N, 0 deg 7\' 54.50\\" W",
|
||||
"EXIF:GPSLatitudeRef": "North", "EXIF:GPSLongitudeRef": "West",
|
||||
"EXIF:DateTimeOriginal": "2018:10:13 09:18:12",
|
||||
"EXIF:OffsetTimeOriginal": "-04:00",
|
||||
@@ -431,7 +430,7 @@ def test_xmp_sidecar():
|
||||
</dc:subject>
|
||||
<photoshop:DateCreated>2018-09-28T15:35:49.063000-04:00</photoshop:DateCreated>
|
||||
</rdf:Description>
|
||||
<rdf:Description rdf:about=''
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:Iptc4xmpExt='http://iptc.org/std/Iptc4xmpExt/2008-02-29/'>
|
||||
<Iptc4xmpExt:PersonInImage>
|
||||
<rdf:Bag>
|
||||
@@ -440,7 +439,7 @@ def test_xmp_sidecar():
|
||||
</rdf:Bag>
|
||||
</Iptc4xmpExt:PersonInImage>
|
||||
</rdf:Description>
|
||||
<rdf:Description rdf:about=''
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:digiKam='http://www.digikam.org/ns/1.0/'>
|
||||
<digiKam:TagsList>
|
||||
<rdf:Seq>
|
||||
@@ -448,11 +447,14 @@ def test_xmp_sidecar():
|
||||
</rdf:Seq>
|
||||
</digiKam:TagsList>
|
||||
</rdf:Description>
|
||||
<rdf:Description rdf:about=''
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:xmp='http://ns.adobe.com/xap/1.0/'>
|
||||
<xmp:CreateDate>2018-09-28T15:35:49</xmp:CreateDate>
|
||||
<xmp:ModifyDate>2018-09-28T15:35:49</xmp:ModifyDate>
|
||||
</rdf:Description>
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:exif='http://ns.adobe.com/exif/1.0/'>
|
||||
</rdf:Description>
|
||||
</rdf:RDF>
|
||||
</x:xmpmeta>"""
|
||||
|
||||
@@ -490,7 +492,7 @@ def test_xmp_sidecar_keyword_template():
|
||||
</dc:subject>
|
||||
<photoshop:DateCreated>2018-09-28T15:35:49.063000-04:00</photoshop:DateCreated>
|
||||
</rdf:Description>
|
||||
<rdf:Description rdf:about=''
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:Iptc4xmpExt='http://iptc.org/std/Iptc4xmpExt/2008-02-29/'>
|
||||
<Iptc4xmpExt:PersonInImage>
|
||||
<rdf:Bag>
|
||||
@@ -499,7 +501,7 @@ def test_xmp_sidecar_keyword_template():
|
||||
</rdf:Bag>
|
||||
</Iptc4xmpExt:PersonInImage>
|
||||
</rdf:Description>
|
||||
<rdf:Description rdf:about=''
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:digiKam='http://www.digikam.org/ns/1.0/'>
|
||||
<digiKam:TagsList>
|
||||
<rdf:Seq>
|
||||
@@ -510,11 +512,14 @@ def test_xmp_sidecar_keyword_template():
|
||||
</rdf:Seq>
|
||||
</digiKam:TagsList>
|
||||
</rdf:Description>
|
||||
<rdf:Description rdf:about=''
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:xmp='http://ns.adobe.com/xap/1.0/'>
|
||||
<xmp:CreateDate>2018-09-28T15:35:49</xmp:CreateDate>
|
||||
<xmp:ModifyDate>2018-09-28T15:35:49</xmp:ModifyDate>
|
||||
</rdf:Description>
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:exif='http://ns.adobe.com/exif/1.0/'>
|
||||
</rdf:Description>
|
||||
</rdf:RDF>
|
||||
</x:xmpmeta>"""
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
|
||||
@@ -57,4 +57,3 @@ def test_cloudasset_3():
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["not_cloudasset"]])
|
||||
|
||||
assert not photos[0].iscloudasset
|
||||
|
||||
|
||||
@@ -57,4 +57,3 @@ def test_cloudasset_3():
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["not_cloudasset"]])
|
||||
|
||||
assert not photos[0].iscloudasset
|
||||
|
||||
|
||||
@@ -26,4 +26,3 @@ def test_not_modified():
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["not_modified"]])
|
||||
assert photos[0].date_modified is None
|
||||
|
||||
|
||||
@@ -27,4 +27,3 @@ def test_modified():
|
||||
# photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
# photos = photosdb.photos(uuid=[UUID_DICT["not_modified"]])
|
||||
# assert photos[0].date_modified is None
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
@@ -44,8 +44,16 @@ UUID_DICT = {
|
||||
"date_invalid": "YZFCPY24TUySvpu7owiqxA",
|
||||
"intrash": "3tljdX43R8+k6peNHVrJNQ",
|
||||
"not_intrash": "6bxcNnzRQKGnK4uPrCJ9UQ",
|
||||
"has_adjustments": "6bxcNnzRQKGnK4uPrCJ9UQ",
|
||||
}
|
||||
|
||||
ALBUM_SORT_ORDER = [
|
||||
"HrK3ZQdlQ7qpDA0FgOYXLA",
|
||||
"8SOE9s0XQVGsuq4ONohTng",
|
||||
"15uNd7%8RguTEgNPKHfTWw",
|
||||
]
|
||||
ALBUM_KEY_PHOTO = "15uNd7%8RguTEgNPKHfTWw"
|
||||
|
||||
PHOTOS_DB_LEN = 8
|
||||
PHOTOS_NOT_IN_TRASH_LEN = 7
|
||||
PHOTOS_IN_TRASH_LEN = 1
|
||||
@@ -135,6 +143,17 @@ def test_albums_as_dict():
|
||||
assert albums == ALBUM_DICT
|
||||
|
||||
|
||||
def test_album_sort_order():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
album = [a for a in photosdb.album_info if a.title == "Pumpkin Farm"][0]
|
||||
photos = album.photos
|
||||
|
||||
uuids = [p.uuid for p in photos]
|
||||
assert uuids == ALBUM_SORT_ORDER
|
||||
|
||||
|
||||
def test_attributes():
|
||||
import datetime
|
||||
import osxphotos
|
||||
@@ -161,6 +180,44 @@ def test_attributes():
|
||||
assert p.ismissing == False
|
||||
|
||||
|
||||
def test_attributes_2():
|
||||
""" Test attributes including height, width, etc """
|
||||
import datetime
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["has_adjustments"]])
|
||||
assert len(photos) == 1
|
||||
p = photos[0]
|
||||
assert p.keywords == ["wedding"]
|
||||
assert p.original_filename == "wedding.jpg"
|
||||
assert p.filename == "wedding.jpg"
|
||||
assert p.date == datetime.datetime(
|
||||
2019,
|
||||
4,
|
||||
15,
|
||||
14,
|
||||
40,
|
||||
24,
|
||||
86000,
|
||||
datetime.timezone(datetime.timedelta(seconds=-14400)),
|
||||
)
|
||||
assert p.description == "Bride Wedding day"
|
||||
assert p.title is None
|
||||
assert sorted(p.albums) == []
|
||||
assert p.persons == ["Maria"]
|
||||
assert p.path.endswith("Masters/2019/07/27/20190727-131650/wedding.jpg")
|
||||
assert not p.ismissing
|
||||
assert p.hasadjustments
|
||||
assert p.height == 1325
|
||||
assert p.width == 1526
|
||||
assert p.original_height == 1367
|
||||
assert p.original_width == 2048
|
||||
assert p.orientation == 1
|
||||
assert p.original_orientation == 1
|
||||
assert p.original_filesize == 460483
|
||||
|
||||
|
||||
def test_missing():
|
||||
import osxphotos
|
||||
|
||||
@@ -168,8 +225,8 @@ def test_missing():
|
||||
photos = photosdb.photos(uuid=["od0fmC7NQx+ayVr+%i06XA"])
|
||||
assert len(photos) == 1
|
||||
p = photos[0]
|
||||
assert p.path == None
|
||||
assert p.ismissing == True
|
||||
assert p.path is None
|
||||
assert p.ismissing
|
||||
|
||||
|
||||
def test_favorite():
|
||||
@@ -497,4 +554,3 @@ def test_date_modified_invalid():
|
||||
assert len(photos) == 1
|
||||
p = photos[0]
|
||||
assert p.date_modified is None
|
||||
|
||||
|
||||
150
tests/test_personinfo.py
Normal file
150
tests/test_personinfo.py
Normal file
@@ -0,0 +1,150 @@
|
||||
""" Test PersonInfo class """
|
||||
|
||||
import pytest
|
||||
|
||||
PHOTOS_DB_5 = "tests/Test-10.15.5.photoslibrary"
|
||||
PHOTOS_DB_4 = "tests/Test-10.14.6.photoslibrary"
|
||||
|
||||
UUID_DICT = {
|
||||
"katie_5": "0FFCE0A2-BE93-4661-A783-957BE54072E4",
|
||||
"katie_4": "D%zgor6TRmGng5V75UBy5A",
|
||||
}
|
||||
PHOTO_DICT = {
|
||||
"katie_5": [
|
||||
"1EB2B765-0765-43BA-A90C-0D0580E6172C",
|
||||
"F12384F6-CD17-4151-ACBA-AE0E3688539E",
|
||||
"D79B8D77-BFFC-460B-9312-034F2877D35B",
|
||||
],
|
||||
"katie_4": [
|
||||
"8SOE9s0XQVGsuq4ONohTng",
|
||||
"HrK3ZQdlQ7qpDA0FgOYXLA",
|
||||
"15uNd7%8RguTEgNPKHfTWw",
|
||||
],
|
||||
}
|
||||
|
||||
KEY_DICT = {
|
||||
"katie_5": "F12384F6-CD17-4151-ACBA-AE0E3688539E",
|
||||
"katie_4": "8SOE9s0XQVGsuq4ONohTng",
|
||||
}
|
||||
|
||||
STR_DICT = {
|
||||
"katie_5": "PersonInfo(name=Katie, display_name=Katie, uuid=0FFCE0A2-BE93-4661-A783-957BE54072E4, facecount=3)",
|
||||
"katie_4": "PersonInfo(name=Katie, display_name=Katie, uuid=D%zgor6TRmGng5V75UBy5A, facecount=3)",
|
||||
}
|
||||
|
||||
JSON_DICT = {
|
||||
"katie_5": {
|
||||
"uuid": "0FFCE0A2-BE93-4661-A783-957BE54072E4",
|
||||
"name": "Katie",
|
||||
"displayname": "Katie",
|
||||
"keyface": 2,
|
||||
"facecount": 3,
|
||||
"keyphoto": "F12384F6-CD17-4151-ACBA-AE0E3688539E",
|
||||
},
|
||||
"katie_4": {
|
||||
"uuid": "D%zgor6TRmGng5V75UBy5A",
|
||||
"name": "Katie",
|
||||
"displayname": "Katie",
|
||||
"keyface": 7,
|
||||
"facecount": 3,
|
||||
"keyphoto": "8SOE9s0XQVGsuq4ONohTng",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def photosdb5():
|
||||
import osxphotos
|
||||
|
||||
return osxphotos.PhotosDB(dbfile=PHOTOS_DB_5)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def photosdb4():
|
||||
import osxphotos
|
||||
|
||||
return osxphotos.PhotosDB(dbfile=PHOTOS_DB_4)
|
||||
|
||||
|
||||
def test_person_info_photosdb_v5(photosdb5):
|
||||
""" Test PersonInfo object """
|
||||
import json
|
||||
|
||||
test_key = "katie_5"
|
||||
katie = [p for p in photosdb5.person_info if p.uuid == UUID_DICT[test_key]][0]
|
||||
|
||||
assert katie.facecount == 3
|
||||
assert katie.name == "Katie"
|
||||
assert katie.display_name == "Katie"
|
||||
photos = katie.photos
|
||||
assert len(photos) == 3
|
||||
uuid = [p.uuid for p in photos]
|
||||
assert sorted(uuid) == sorted(PHOTO_DICT[test_key])
|
||||
assert str(katie) == STR_DICT[test_key]
|
||||
assert json.loads(katie.json()) == JSON_DICT[test_key]
|
||||
|
||||
|
||||
def test_person_info_photosinfo_v5(photosdb5):
|
||||
""" Test PersonInfo object """
|
||||
import json
|
||||
|
||||
test_key = "katie_5"
|
||||
photo = photosdb5.photos(uuid=[KEY_DICT[test_key]])[0]
|
||||
assert "Katie" in photo.persons
|
||||
|
||||
person_info = photo.person_info
|
||||
assert len(person_info) == 2
|
||||
|
||||
katie = [p for p in person_info if p.name == "Katie"][0]
|
||||
|
||||
assert katie.facecount == 3
|
||||
assert katie.name == "Katie"
|
||||
assert katie.display_name == "Katie"
|
||||
photos = katie.photos
|
||||
assert len(photos) == 3
|
||||
uuid = [p.uuid for p in photos]
|
||||
assert sorted(uuid) == sorted(PHOTO_DICT[test_key])
|
||||
assert katie.keyphoto.uuid == KEY_DICT[test_key]
|
||||
assert str(katie) == STR_DICT[test_key]
|
||||
assert json.loads(katie.json()) == JSON_DICT[test_key]
|
||||
|
||||
|
||||
def test_person_info_photosdb_v4(photosdb4):
|
||||
""" Test PersonInfo object """
|
||||
import json
|
||||
|
||||
test_key = "katie_4"
|
||||
katie = [p for p in photosdb4.person_info if p.uuid == UUID_DICT[test_key]][0]
|
||||
|
||||
assert katie.facecount == 3
|
||||
assert katie.name == "Katie"
|
||||
assert katie.display_name == "Katie"
|
||||
photos = katie.photos
|
||||
assert len(photos) == 3
|
||||
uuid = [p.uuid for p in photos]
|
||||
assert sorted(uuid) == sorted(PHOTO_DICT[test_key])
|
||||
assert katie.keyphoto.uuid == KEY_DICT[test_key]
|
||||
assert json.loads(katie.json()) == JSON_DICT[test_key]
|
||||
|
||||
|
||||
def test_person_info_photosinfo_v4(photosdb4):
|
||||
""" Test PersonInfo object """
|
||||
import json
|
||||
|
||||
test_key = "katie_4"
|
||||
photo = photosdb4.photos(uuid=[KEY_DICT[test_key]])[0]
|
||||
assert "Katie" in photo.persons
|
||||
|
||||
person_info = photo.person_info
|
||||
assert len(person_info) == 2
|
||||
|
||||
katie = [p for p in person_info if p.name == "Katie"][0]
|
||||
assert katie.facecount == 3
|
||||
assert katie.name == "Katie"
|
||||
assert katie.display_name == "Katie"
|
||||
photos = katie.photos
|
||||
assert len(photos) == 3
|
||||
uuid = [p.uuid for p in photos]
|
||||
assert sorted(uuid) == sorted(PHOTO_DICT[test_key])
|
||||
assert katie.keyphoto.uuid == KEY_DICT[test_key]
|
||||
assert json.loads(katie.json()) == JSON_DICT[test_key]
|
||||
@@ -87,6 +87,7 @@ def test_place_str():
|
||||
"field16=[], street_address=[], body_of_water=[])', country_code='GB')"
|
||||
)
|
||||
|
||||
|
||||
def test_place_as_dict():
|
||||
# test PlaceInfo.as_dict()
|
||||
import osxphotos
|
||||
|
||||
@@ -20,7 +20,17 @@ LABELS_DICT = {
|
||||
"Tree",
|
||||
],
|
||||
# F12384F6-CD17-4151-ACBA-AE0E3688539E Pumkins1.jpg Can we carry this? Girls with pumpkins [] False
|
||||
"F12384F6-CD17-4151-ACBA-AE0E3688539E": [],
|
||||
"F12384F6-CD17-4151-ACBA-AE0E3688539E": [
|
||||
"Vegetable",
|
||||
"Pumpkin",
|
||||
"Farm",
|
||||
"Food",
|
||||
"Outdoor",
|
||||
"Agriculture",
|
||||
"People",
|
||||
"Plant",
|
||||
"Straw Hay",
|
||||
],
|
||||
# D79B8D77-BFFC-460B-9312-034F2877D35B Pumkins2.jpg I found one! Girl holding pumpkin [] False
|
||||
"D79B8D77-BFFC-460B-9312-034F2877D35B": [],
|
||||
# D05A5FE3-15FB-49A1-A15D-AB3DA6F8B068 DSC03584.dng None RAW only [] False
|
||||
@@ -49,7 +59,20 @@ LABELS_DICT = {
|
||||
"Plant",
|
||||
],
|
||||
# 1EB2B765-0765-43BA-A90C-0D0580E6172C Pumpkins3.jpg None Kids in pumpkin field [] False
|
||||
"1EB2B765-0765-43BA-A90C-0D0580E6172C": [],
|
||||
"1EB2B765-0765-43BA-A90C-0D0580E6172C": [
|
||||
"Child",
|
||||
"Sky",
|
||||
"Plant",
|
||||
"People",
|
||||
"Clothing",
|
||||
"Jeans",
|
||||
"Outdoor",
|
||||
"Agriculture",
|
||||
"Farm",
|
||||
"Food",
|
||||
"Vegetable",
|
||||
"Pumpkin",
|
||||
],
|
||||
# DC99FBDD-7A52-4100-A5BB-344131646C30 St James Park.jpg St. James's Park None ['Tree', 'Plant', 'Waterways', 'River', 'Sky', 'Cloudy', 'Land', 'Water Body', 'Water', 'Outdoor'] False
|
||||
"DC99FBDD-7A52-4100-A5BB-344131646C30": [
|
||||
"Tree",
|
||||
@@ -117,8 +140,33 @@ LABELS_NORMALIZED_DICT = {
|
||||
],
|
||||
# D79B8D77-BFFC-460B-9312-034F2877D35B Pumkins2.jpg I found one! Girl holding pumpkin [] False
|
||||
"D79B8D77-BFFC-460B-9312-034F2877D35B": [],
|
||||
# 1EB2B765-0765-43BA-A90C-0D0580E6172C Pumpkins3.jpg None Kids in pumpkin field [] False
|
||||
"1EB2B765-0765-43BA-A90C-0D0580E6172C": [
|
||||
"child",
|
||||
"sky",
|
||||
"plant",
|
||||
"people",
|
||||
"clothing",
|
||||
"jeans",
|
||||
"outdoor",
|
||||
"agriculture",
|
||||
"farm",
|
||||
"food",
|
||||
"vegetable",
|
||||
"pumpkin",
|
||||
],
|
||||
# F12384F6-CD17-4151-ACBA-AE0E3688539E Pumkins1.jpg Can we carry this? Girls with pumpkins [] False
|
||||
"F12384F6-CD17-4151-ACBA-AE0E3688539E": [],
|
||||
"F12384F6-CD17-4151-ACBA-AE0E3688539E": [
|
||||
"vegetable",
|
||||
"pumpkin",
|
||||
"farm",
|
||||
"food",
|
||||
"outdoor",
|
||||
"agriculture",
|
||||
"people",
|
||||
"plant",
|
||||
"straw hay",
|
||||
],
|
||||
# A1DD1F98-2ECD-431F-9AC9-5AFEFE2D3A5C Pumpkins4.jpg Pumpkin heads None [] True
|
||||
"A1DD1F98-2ECD-431F-9AC9-5AFEFE2D3A5C": [],
|
||||
}
|
||||
@@ -155,6 +203,16 @@ LABELS = [
|
||||
"Vase",
|
||||
"Container",
|
||||
"Camera",
|
||||
"Child",
|
||||
"People",
|
||||
"Clothing",
|
||||
"Jeans",
|
||||
"Agriculture",
|
||||
"Farm",
|
||||
"Food",
|
||||
"Vegetable",
|
||||
"Pumpkin",
|
||||
"Straw Hay",
|
||||
]
|
||||
|
||||
LABELS_NORMALIZED = [
|
||||
@@ -181,15 +239,31 @@ LABELS_NORMALIZED = [
|
||||
"vase",
|
||||
"container",
|
||||
"camera",
|
||||
"child",
|
||||
"people",
|
||||
"clothing",
|
||||
"jeans",
|
||||
"agriculture",
|
||||
"farm",
|
||||
"food",
|
||||
"vegetable",
|
||||
"pumpkin",
|
||||
"straw hay",
|
||||
]
|
||||
|
||||
LABELS_AS_DICT = {
|
||||
"Plant": 5,
|
||||
"Plant": 7,
|
||||
"Outdoor": 4,
|
||||
"Sky": 3,
|
||||
"Tree": 2,
|
||||
"Sky": 2,
|
||||
"Outdoor": 2,
|
||||
"Art": 2,
|
||||
"Foliage": 2,
|
||||
"People": 2,
|
||||
"Agriculture": 2,
|
||||
"Farm": 2,
|
||||
"Food": 2,
|
||||
"Vegetable": 2,
|
||||
"Pumpkin": 2,
|
||||
"Waterways": 1,
|
||||
"River": 1,
|
||||
"Cloudy": 1,
|
||||
@@ -207,15 +281,25 @@ LABELS_AS_DICT = {
|
||||
"Vase": 1,
|
||||
"Container": 1,
|
||||
"Camera": 1,
|
||||
"Child": 1,
|
||||
"Clothing": 1,
|
||||
"Jeans": 1,
|
||||
"Straw Hay": 1,
|
||||
}
|
||||
|
||||
LABELS_NORMALIZED_AS_DICT = {
|
||||
"plant": 5,
|
||||
"plant": 7,
|
||||
"outdoor": 4,
|
||||
"sky": 3,
|
||||
"tree": 2,
|
||||
"sky": 2,
|
||||
"outdoor": 2,
|
||||
"art": 2,
|
||||
"foliage": 2,
|
||||
"people": 2,
|
||||
"agriculture": 2,
|
||||
"farm": 2,
|
||||
"food": 2,
|
||||
"vegetable": 2,
|
||||
"pumpkin": 2,
|
||||
"waterways": 1,
|
||||
"river": 1,
|
||||
"cloudy": 1,
|
||||
@@ -233,6 +317,10 @@ LABELS_NORMALIZED_AS_DICT = {
|
||||
"vase": 1,
|
||||
"container": 1,
|
||||
"camera": 1,
|
||||
"child": 1,
|
||||
"clothing": 1,
|
||||
"jeans": 1,
|
||||
"straw hay": 1,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -458,3 +458,47 @@ def test_subst_strftime():
|
||||
|
||||
rendered, unmatched = photo.render_template("{created.strftime}")
|
||||
assert rendered[0] == "_"
|
||||
|
||||
|
||||
def test_subst_expand_inplace_1():
|
||||
""" Test that substitutions are correct when expand_inplace=True """
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_1)
|
||||
# one album, one keyword, two persons
|
||||
photo = photosdb.photos(uuid=[UUID_DICT["1_1_2"]])[0]
|
||||
|
||||
template = "{person}"
|
||||
expected = ["Katie,Suzy"]
|
||||
rendered, unknown = photo.render_template(template, expand_inplace=True)
|
||||
assert sorted(rendered) == sorted(expected)
|
||||
|
||||
|
||||
def test_subst_expand_inplace_2():
|
||||
""" Test that substitutions are correct when expand_inplace=True """
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_1)
|
||||
# one album, one keyword, two persons
|
||||
photo = photosdb.photos(uuid=[UUID_DICT["1_1_2"]])[0]
|
||||
|
||||
template = "{person}-{keyword}"
|
||||
expected = ["Katie,Suzy-Kids"]
|
||||
rendered, unknown = photo.render_template(template, expand_inplace=True)
|
||||
assert sorted(rendered) == sorted(expected)
|
||||
|
||||
|
||||
def test_subst_expand_inplace_3():
|
||||
""" Test that substitutions are correct when expand_inplace=True and inplace_sep specified"""
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_1)
|
||||
# one album, one keyword, two persons
|
||||
photo = photosdb.photos(uuid=[UUID_DICT["1_1_2"]])[0]
|
||||
|
||||
template = "{person}-{keyword}"
|
||||
expected = ["Katie; Suzy-Kids"]
|
||||
rendered, unknown = photo.render_template(
|
||||
template, expand_inplace=True, inplace_sep="; "
|
||||
)
|
||||
assert sorted(rendered) == sorted(expected)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user