Compare commits

...

30 Commits

Author SHA1 Message Date
Rhet Turnbull
a57da2346b Bug fix for albums in Photos <= 4 to address issue #116 2020-04-28 18:20:26 -07:00
Rhet Turnbull
3fe03cd127 version bump for pypi 2020-04-28 07:54:22 -07:00
Rhet Turnbull
5cc98c338b Update README.md 2020-04-28 07:48:54 -07:00
Rhet Turnbull
1c9d4f282b Update README.md 2020-04-28 07:44:00 -07:00
Rhet Turnbull
1ceda15134 Fixed implementation of use_albums_as_keywords and use_persons_as_keywords, closes #115 2020-04-28 07:41:37 -07:00
Rhet Turnbull
a80071111f Updated README.md 2020-04-28 07:10:48 -07:00
Rhet Turnbull
072a8d795e Updated CHANGELOG.md 2020-04-27 23:16:31 -07:00
Rhet Turnbull
b35b071634 Added --album-keyword and --person-keyword to CLI, closes #61 2020-04-27 23:08:59 -07:00
Rhet Turnbull
56a000609f Updated tests/README.md 2020-04-26 16:31:24 -07:00
Rhet Turnbull
54d5d4b7ba Updated test libraries 2020-04-26 16:04:03 -07:00
Rhet Turnbull
38137a1351 Updated CHANGELOG.md 2020-04-26 16:03:26 -07:00
Rhet Turnbull
4b29a2e05f Merge branch 'master' of https://github.com/RhetTbull/osxphotos 2020-04-26 15:57:51 -07:00
Rhet Turnbull
9be0f849b7 Updated test to avoid issue with GitHub workflow 2020-04-26 15:57:43 -07:00
Rhet Turnbull
ccb5f252d1 Update pythonpackage.yml to remove older pythons 2020-04-26 15:39:37 -07:00
Rhet Turnbull
d8a64c9573 Fixed locale bug in templates, closes #113 2020-04-26 15:20:28 -07:00
Rhet Turnbull
81d4e392c3 Updated CHANGELOG.md 2020-04-20 22:22:08 -07:00
Rhet Turnbull
85d2baac10 Updated setup.py and README with install instructions 2020-04-20 22:13:42 -07:00
Rhet Turnbull
8a768e62ce Still working on bpylist2 install error 2020-04-20 21:35:12 -07:00
Rhet Turnbull
1c8eb764f5 Merge branch 'master' of https://github.com/RhetTbull/osxphotos 2020-04-20 21:21:54 -07:00
Rhet Turnbull
8e4b88ad1f Updated setup.py to resolve issue with bpylist2 on python < 3.8 2020-04-20 21:21:47 -07:00
Rhet Turnbull
3f80f786a3 Update README.md to clarify install instructions 2020-04-20 08:01:09 -07:00
Rhet Turnbull
a337e79e13 added raw_is_original handling 2020-04-19 19:16:43 -07:00
Rhet Turnbull
ec68feec49 Removed warning from path_raw 2020-04-19 18:39:53 -07:00
Rhet Turnbull
9b9b54e590 Updated tests and test library with RAW images 2020-04-19 18:24:24 -07:00
Rhet Turnbull
22f1e8f2a6 Updated CHANGELOG.md 2020-04-19 00:04:47 -07:00
Rhet Turnbull
1867c1d747 added __len__ to PhotosDB, closes #44 2020-04-18 23:57:34 -07:00
Rhet Turnbull
87eb84fddd Updated use of _PHOTOS_4_VERSION, closes #106 2020-04-18 23:33:02 -07:00
Rhet Turnbull
15a3736b74 Fixed documentation error 2020-04-18 23:10:13 -07:00
Rhet Turnbull
cf28cb6452 Added cli.py for use with pyinstaller 2020-04-18 18:34:09 -07:00
Rhet Turnbull
f20fadcef7 Fixed some stray tabs 2020-04-18 13:38:37 -07:00
96 changed files with 676 additions and 91 deletions

View File

@@ -9,7 +9,7 @@ jobs:
strategy:
max-parallel: 4
matrix:
python-version: [3.6, 3.7, 3.8]
python-version: [3.8]
steps:
- uses: actions/checkout@v1

View File

@@ -4,6 +4,46 @@ 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.28.7](https://github.com/RhetTbull/osxphotos/compare/v0.28.6...v0.28.7)
> 28 April 2020
- Added --album-keyword and --person-keyword to CLI, closes #61 [`#61`](https://github.com/RhetTbull/osxphotos/issues/61)
- Updated test libraries [`54d5d4b`](https://github.com/RhetTbull/osxphotos/commit/54d5d4b7ba99204f58e723231309ab6e306be28c)
- Updated CHANGELOG.md [`38137a1`](https://github.com/RhetTbull/osxphotos/commit/38137a1351cdb7ab72393ea03828933dac0b76b0)
- Updated tests/README.md [`56a0006`](https://github.com/RhetTbull/osxphotos/commit/56a000609f2f08d0f8800fec49cada2980c3bb9d)
#### [v0.28.6](https://github.com/RhetTbull/osxphotos/compare/v0.28.5...v0.28.6)
> 26 April 2020
- Fixed locale bug in templates, closes #113 [`#113`](https://github.com/RhetTbull/osxphotos/issues/113)
- Updated CHANGELOG.md [`81d4e39`](https://github.com/RhetTbull/osxphotos/commit/81d4e392c39f0fe6f967a447c7d0c970bf224032)
- Updated test to avoid issue with GitHub workflow [`9be0f84`](https://github.com/RhetTbull/osxphotos/commit/9be0f849b73061d053d30274ff3295b79c88f0b6)
- Update pythonpackage.yml to remove older pythons [`ccb5f25`](https://github.com/RhetTbull/osxphotos/commit/ccb5f252d14e9335ae04a2e338a6d527b80c9a93)
#### [v0.28.5](https://github.com/RhetTbull/osxphotos/compare/0.28.2...v0.28.5)
> 21 April 2020
- added __len__ to PhotosDB, closes #44 [`#44`](https://github.com/RhetTbull/osxphotos/issues/44)
- Updated use of _PHOTOS_4_VERSION, closes #106 [`#106`](https://github.com/RhetTbull/osxphotos/issues/106)
- Updated tests and test library with RAW images [`9b9b54e`](https://github.com/RhetTbull/osxphotos/commit/9b9b54e590e43ae49fb3ae41d493a1f8faec4181)
- Updated setup.py to resolve issue with bpylist2 on python &lt; 3.8 [`8e4b88a`](https://github.com/RhetTbull/osxphotos/commit/8e4b88ad1fc18438f941e045bfc8aeac878914f9)
- Added cli.py for use with pyinstaller [`cf28cb6`](https://github.com/RhetTbull/osxphotos/commit/cf28cb6452de17f2ef8d80435386e8d5a1aabd34)
- added raw_is_original handling [`a337e79`](https://github.com/RhetTbull/osxphotos/commit/a337e79e13802b4824c2f088ce9db1c027d6f3c5)
- Updated CHANGELOG.md [`22f1e8f`](https://github.com/RhetTbull/osxphotos/commit/22f1e8f2a6478e0576f6bff53e348aad8680ae69)
#### [0.28.2](https://github.com/RhetTbull/osxphotos/compare/v0.28.1...0.28.2)
> 18 April 2020
- Added folder support for Photos &lt;= 4, closes #93 [`#93`](https://github.com/RhetTbull/osxphotos/issues/93)
- cleaned up SQL statements in _process_database4 [`6f28171`](https://github.com/RhetTbull/osxphotos/commit/6f281711e2001a63ffad076d7b9835272d5d09da)
- Updated CHANGELOG.md [`1fa9583`](https://github.com/RhetTbull/osxphotos/commit/1fa9583ea689d54d2613a064f1ade25bcdfbf043)
- Fixed suffix check on export to be case insensitive [`4b30b3b`](https://github.com/RhetTbull/osxphotos/commit/4b30b3b4260e2c7409e18825e5b626efe646db16)
- test library update [`3bac106`](https://github.com/RhetTbull/osxphotos/commit/3bac106eb7a180e9e39643a89087d92bf2a437d0)
#### [v0.28.1](https://github.com/RhetTbull/osxphotos/compare/v0.27.4...v0.28.1)
> 18 April 2020
@@ -273,7 +313,11 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- Moved PhotosDB attributes to properties instead of methods [`d95acdf`](https://github.com/RhetTbull/osxphotos/commit/d95acdf9f8764a1720bcba71a6dad29bf668eaf9)
- changed interface for export, prepped for exiftool_json_sidecar [`1fe8859`](https://github.com/RhetTbull/osxphotos/commit/1fe885962e8a9a420e776bdd3dc640ca143224b2)
#### [v0.15.1](https://github.com/RhetTbull/osxphotos/compare/v0.14.21...v0.15.1)
#### [v0.15.1](https://github.com/RhetTbull/osxphotos/compare/v0.15.0...v0.15.1)
> 28 April 2020
#### [v0.15.0](https://github.com/RhetTbull/osxphotos/compare/v0.14.21...v0.15.0)
> 14 December 2019

View File

@@ -33,9 +33,11 @@ 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 / Photos 5.0. Requires python >= 3.6
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.4 / Photos 5.0.
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
Requires python >= 3.6 though if you use `pip` to install, you must use python >= 3.8. See notes [below](#Installation-instructions). I highly recommend running this with python >= 3.8 as I'll eventually drop support for 3.6 and 3.7.
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.
## Installation instructions
@@ -44,6 +46,14 @@ osxmetadata 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.
pip install osxphotos
## Command Line Usage
This package will install a command line utility called `osxphotos` that allows you to query the Photos database. Alternatively, you can also run the command line utility like this: `python3 -m osxphotos`
@@ -209,6 +219,10 @@ Options:
photos if the RAW photo does not have an
associated jpeg image (e.g. the RAW file was
imported to Photos without a jpeg preview).
--person-keyword Use person in image as keyword/tag when
exporting metadata.
--album-keyword Use album name as keyword/tag when exporting
metadata.
--current-name Use photo's current filename instead of
original filename for export. Note:
Starting with Photos 5, all photos are
@@ -276,7 +290,7 @@ rendered name, use double braces, e.g. '{{' or '}}', thus using
You may specify an optional default value to use if the substitution does not
contain a value (e.g. the value is null) by specifying the default value after
a ',' in the template string: for example, if template is
'{created.year}/{place.address,'NO_ADDRESS'}' but there was no address
'{created.year}/{place.address,NO_ADDRESS}' but there was no address
associated with the photo, the resulting output would be:
'2020/NO_ADDRESS/photoname.jpg'. If specified, the default value may not
contain a brace symbol ('{' or '}').
@@ -806,6 +820,7 @@ For example, in my library, Photos says I have 19,386 photos and 474 movies. Ho
>>>
```
### PhotoInfo
PhotosDB.photos() returns a list of PhotoInfo objects. Each PhotoInfo object represents a single photo in the Photos library.
@@ -950,7 +965,8 @@ Returns True if photo is a panorama, otherwise False.
#### `json()`
Returns a JSON representation of all photo info
#### `export(dest, *filename, edited=False, live_photo=False, overwrite=False, increment=True, sidecar_json=False, sidecar_xmp=False, use_photos_export=False, timeout=120, exiftool=False, no_xattr=False)`
#### `export()`
`export(dest, *filename, edited=False, live_photo=False, overwrite=False, increment=True, sidecar_json=False, sidecar_xmp=False, use_photos_export=False, timeout=120, exiftool=False, no_xattr=False, use_albums_as_keywords=False, use_persons_as_keywords=False)`
Export photo from the Photos library to another destination on disk.
- dest: must be valid destination path as str (or exception raised).
@@ -965,6 +981,8 @@ Export photo from the Photos library to another destination on disk.
- timeout: (int, default=120) timeout in seconds used with use_photos_export
- exiftool: (boolean, default = False) if True, will use [exiftool](https://exiftool.org/) to write metadata directly to the exported photo; exiftool must be installed and in the system path
- no_xattr: (boolean, default = False); if True, exports file without preserving extended attributes
- use_albums_as_keywords: (boolean, default = False); if True, will use album names as keywords when exporting metadata with exiftool or sidecar
- use_persons_as_keywords: (boolean, default = False); if True, will use person names as keywords when exporting metadata with exiftool or sidecar
Returns: list of paths to exported files. More than one file could be exported, for example if live_photo=True, both the original imaage and the associated .mov file will be exported

19
cli.py Normal file
View File

@@ -0,0 +1,19 @@
""" stand alone command line script for use with pyinstaller
To build this into an executable:
- install pyinstaller:
python3 -m pip install pyinstaller
- then use build_cli_exe.sh to run pyinstaller or execute the following command:
pyinstaller --onefile --hidden-import="pkg_resources.py2_warn" --name osxphotos --add-data osxphotos/templates/xmp_sidecar.mako:osxphotos/templates cli.py
Resulting executable will be in "dist/osxphotos"
Note: This is *not* the cli that "python3 -m pip install osxphotos" or "python setup.py install" would install;
it's merely a wrapper around __main__.py to allow pyinstaller to work
"""
from osxphotos.__main__ import cli
if __name__ == "__main__":
cli()

8
make_cli_exe.sh Executable file
View File

@@ -0,0 +1,8 @@
#!/bin/sh
# This will build an stand-alone executable called 'osxphotos' in your ./dist directory
# using pyinstaller
# If you need to install pyinstaller:
# python3 -m pip install --upgrade pyinstaller
pyinstaller --onefile --hidden-import="pkg_resources.py2_warn" --name osxphotos --add-data osxphotos/templates/xmp_sidecar.mako:osxphotos/templates cli.py

View File

@@ -18,7 +18,7 @@ from pathvalidate import (
import osxphotos
from ._constants import _EXIF_TOOL_URL, _PHOTOS_5_VERSION, _UNKNOWN_PLACE
from ._constants import _EXIF_TOOL_URL, _PHOTOS_4_VERSION, _UNKNOWN_PLACE
from ._version import __version__
from .exiftool import get_exiftool_path
from .template import (
@@ -109,7 +109,7 @@ class ExportCommand(click.Command):
"You may specify an optional default value to use if the substitution does not contain a value "
+ "(e.g. the value is null) "
+ "by specifying the default value after a ',' in the template string: "
+ "for example, if template is '{created.year}/{place.address,'NO_ADDRESS'}' "
+ "for example, if template is '{created.year}/{place.address,NO_ADDRESS}' "
+ "but there was no address associated with the photo, the resulting output would be: "
+ "'2020/NO_ADDRESS/photoname.jpg'. "
+ "If specified, the default value may not contain a brace symbol ('{' or '}')."
@@ -428,7 +428,7 @@ def albums(ctx, cli_obj, db, json_, photos_library):
photosdb = osxphotos.PhotosDB(dbfile=db)
albums = {"albums": photosdb.albums_as_dict}
if photosdb.db_version >= _PHOTOS_5_VERSION:
if photosdb.db_version > _PHOTOS_4_VERSION:
albums["shared albums"] = photosdb.albums_shared_as_dict
if json_ or cli_obj.json:
@@ -493,7 +493,7 @@ def info(ctx, cli_obj, db, json_, photos_library):
not_shared_movies = [p for p in movies if not p.shared]
info["movie_count"] = len(not_shared_movies)
if pdb.db_version >= _PHOTOS_5_VERSION:
if pdb.db_version > _PHOTOS_4_VERSION:
shared_photos = [p for p in photos if p.shared]
info["shared_photo_count"] = len(shared_photos)
@@ -508,7 +508,7 @@ def info(ctx, cli_obj, db, json_, photos_library):
info["albums_count"] = len(albums)
info["albums"] = albums
if pdb.db_version >= _PHOTOS_5_VERSION:
if pdb.db_version > _PHOTOS_4_VERSION:
albums_shared = pdb.albums_shared_as_dict
info["shared_albums_count"] = len(albums_shared)
info["shared_albums"] = albums_shared
@@ -893,6 +893,16 @@ def query(
"Note: this does not skip RAW photos if the RAW photo does not have an associated jpeg image "
"(e.g. the RAW file was imported to Photos without a jpeg preview).",
)
@click.option(
"--person-keyword",
is_flag=True,
help="Use person in image as keyword/tag when exporting metadata.",
)
@click.option(
"--album-keyword",
is_flag=True,
help="Use album name as keyword/tag when exporting metadata.",
)
@click.option(
"--current-name",
is_flag=True,
@@ -983,6 +993,8 @@ def export(
skip_bursts,
skip_live,
skip_raw,
person_keyword,
album_keyword,
current_name,
sidecar,
only_photos,
@@ -1174,6 +1186,8 @@ def export(
directory,
no_extended_attributes,
export_raw,
album_keyword,
person_keyword,
)
else:
for p in photos:
@@ -1192,6 +1206,8 @@ def export(
directory,
no_extended_attributes,
export_raw,
album_keyword,
person_keyword,
)
if export_paths:
click.echo(f"Exported {p.filename} to {export_paths}")
@@ -1578,6 +1594,8 @@ def export_photo(
directory,
no_extended_attributes,
export_raw,
album_keyword,
person_keyword,
):
""" Helper function for export that does the actual export
photo: PhotoInfo object
@@ -1594,6 +1612,8 @@ def export_photo(
directory: template used to determine output directory
no_extended_attributes: boolean; if True, exports photo without preserving extended attributes
export_raw: boolean; if True exports RAW image associate with the 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
returns list of path(s) of exported photo or None if photo was missing
"""
@@ -1677,6 +1697,8 @@ def export_photo(
use_photos_export=use_photos_export,
exiftool=exiftool,
no_xattr=no_extended_attributes,
use_albums_as_keywords=album_keyword,
use_persons_as_keywords=person_keyword,
)[0]
photo_paths.append(photo_path)
@@ -1712,6 +1734,8 @@ def export_photo(
use_photos_export=use_photos_export,
exiftool=exiftool,
no_xattr=no_extended_attributes,
use_albums_as_keywords=album_keyword,
use_persons_as_keywords=person_keyword,
)
return photo_paths

View File

@@ -1,3 +1,3 @@
""" version info """
__version__ = "0.28.2"
__version__ = "0.28.10"

View File

@@ -24,6 +24,7 @@ from ._constants import (
_PHOTOS_4_VERSION,
_PHOTOS_5_SHARED_PHOTO_PATH,
_TEMPLATE_DIR,
_UNKNOWN_PERSON,
_XMP_TEMPLATE_NAME,
)
from .exiftool import ExifTool
@@ -53,7 +54,12 @@ class PhotoInfo:
@property
def filename(self):
""" filename of the picture """
return self._info["filename"]
if self.has_raw and self.raw_original:
# return name of the RAW file
# TODO: not yet implemented
return self._info["filename"]
else:
return self._info["filename"]
@property
def original_filename(self):
@@ -296,9 +302,10 @@ class PhotoInfo:
glob_str = f"{filestem}*.{raw_ext}"
raw_file = findfiles(glob_str, filepath)
if len(raw_file) != 1:
logging.warning(
f"Error getting path to RAW file: {filepath}/{glob_str}"
)
# Note: In Photos Version 5.0 (141.19.150), images not copied to Photos Library
# that are missing do not always trigger is_missing = True as happens
# in earlier version so it's possible for this check to fail, if so, return None
logging.debug(f"Error getting path to RAW file: {filepath}/{glob_str}")
photopath = None
else:
photopath = os.path.join(filepath, raw_file[0])
@@ -610,7 +617,7 @@ class PhotoInfo:
""" returns True if associated RAW image and the RAW image is selected in Photos
via "Use RAW as Original "
otherwise returns False """
return True if self._info["original_resource_choice"] == 1 else False
return self._info["raw_is_original"]
def export(
self,
@@ -627,6 +634,8 @@ class PhotoInfo:
timeout=120,
exiftool=False,
no_xattr=False,
use_albums_as_keywords=False,
use_persons_as_keywords=False,
):
""" export photo
dest: must be valid destination path (or exception raised)
@@ -652,7 +661,12 @@ class PhotoInfo:
timeout: (int, default=120) timeout in seconds used with use_photos_export
exiftool: (boolean, default = False); if True, will use exiftool to write metadata to export file
no_xattr: (boolean, default = False); if True, exports file without preserving extended attributes
returns list of full paths to the exported files """
returns list of full paths to the exported files
use_albums_as_keywords: (boolean, default = False); if True, will include album names in keywords
when exporting metadata with exiftool or sidecar
use_persons_as_keywords: (boolean, default = False); if True, will include person names in keywords
when exporting metadata with exiftool or sidecar
"""
# list of all files exported during this call to export
exported_files = []
@@ -856,7 +870,10 @@ class PhotoInfo:
if sidecar_json:
logging.debug("writing exiftool_json_sidecar")
sidecar_filename = dest.parent / pathlib.Path(f"{dest.stem}.json")
sidecar_str = self._exiftool_json_sidecar()
sidecar_str = self._exiftool_json_sidecar(
use_albums_as_keywords=use_albums_as_keywords,
use_persons_as_keywords=use_persons_as_keywords,
)
try:
self._write_sidecar(sidecar_filename, sidecar_str)
except Exception as e:
@@ -866,7 +883,10 @@ class PhotoInfo:
if sidecar_xmp:
logging.debug("writing xmp_sidecar")
sidecar_filename = dest.parent / pathlib.Path(f"{dest.stem}.xmp")
sidecar_str = self._xmp_sidecar()
sidecar_str = self._xmp_sidecar(
use_albums_as_keywords=use_albums_as_keywords,
use_persons_as_keywords=use_persons_as_keywords,
)
try:
self._write_sidecar(sidecar_filename, sidecar_str)
except Exception as e:
@@ -876,17 +896,28 @@ class PhotoInfo:
# if exiftool, write the metadata
if exiftool and exported_files:
for exported_file in exported_files:
self._write_exif_data(exported_file)
self._write_exif_data(
exported_file,
use_albums_as_keywords=use_albums_as_keywords,
use_persons_as_keywords=use_persons_as_keywords,
)
return exported_files
def _write_exif_data(self, filepath):
def _write_exif_data(
self, filepath, use_albums_as_keywords=False, use_persons_as_keywords=False
):
""" write exif data to image file at filepath
filepath: full path to the image file """
if not os.path.exists(filepath):
raise FileNotFoundError(f"Could not find file {filepath}")
exiftool = ExifTool(filepath)
exif_info = json.loads(self._exiftool_json_sidecar())[0]
exif_info = json.loads(
self._exiftool_json_sidecar(
use_albums_as_keywords=use_albums_as_keywords,
use_persons_as_keywords=use_persons_as_keywords,
)
)[0]
for exiftag, val in exif_info.items():
if type(val) == list:
# more than one, set first value the add additional values
@@ -897,7 +928,9 @@ class PhotoInfo:
else:
exiftool.setvalue(exiftag, val)
def _exiftool_json_sidecar(self):
def _exiftool_json_sidecar(
self, use_albums_as_keywords=False, use_persons_as_keywords=False
):
""" 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
Exports the following:
@@ -926,18 +959,30 @@ class PhotoInfo:
if self.title:
exif["XMP:Title"] = self.title
keyword_list = []
if self.keywords:
exif["XMP:TagsList"] = exif["IPTC:Keywords"] = list(self.keywords)
# Photos puts both keywords and persons in Subject when using "Export IPTC as XMP"
exif["XMP:Subject"] = list(self.keywords)
keyword_list.extend(self.keywords)
person_list = []
if self.persons:
exif["XMP:PersonInImage"] = self.persons
# filter out _UNKNOWN_PERSON
person_list = [p for p in self.persons if p != _UNKNOWN_PERSON]
if use_persons_as_keywords and person_list:
keyword_list.extend(person_list)
if use_albums_as_keywords and self.albums:
keyword_list.extend(self.albums)
if keyword_list:
exif["XMP:TagsList"] = exif["IPTC:Keywords"] = keyword_list
if person_list:
exif["XMP:PersonInImage"] = person_list
if self.keywords or person_list:
# Photos puts both keywords and persons in Subject when using "Export IPTC as XMP"
if "XMP:Subject" in exif:
exif["XMP:Subject"].extend(self.persons)
else:
exif["XMP:Subject"] = self.persons
exif["XMP:Subject"] = list(self.keywords) + person_list
# if self.favorite():
# exif["Rating"] = 5
@@ -971,14 +1016,41 @@ class PhotoInfo:
json_str = json.dumps([exif])
return json_str
def _xmp_sidecar(self):
def _xmp_sidecar(self, use_albums_as_keywords=False, use_persons_as_keywords=False):
""" returns string for XMP sidecar """
# TODO: add additional fields to XMP file?
xmp_template = Template(
filename=os.path.join(_TEMPLATE_DIR, _XMP_TEMPLATE_NAME)
)
xmp_str = xmp_template.render(photo=self)
keyword_list = []
if self.keywords:
keyword_list.extend(self.keywords)
person_list = []
if self.persons:
# filter out _UNKNOWN_PERSON
person_list = [p for p in self.persons if p != _UNKNOWN_PERSON]
if use_persons_as_keywords and person_list:
keyword_list.extend(person_list)
if use_albums_as_keywords and self.albums:
keyword_list.extend(self.albums)
subject_list = []
if self.keywords or person_list:
# Photos puts both keywords and persons in Subject when using "Export IPTC as XMP"
subject_list = list(self.keywords) + person_list
xmp_str = xmp_template.render(
photo=self,
keywords=keyword_list,
persons=person_list,
subjects=subject_list,
)
# remove extra lines that mako inserts from template
xmp_str = "\n".join(
[line for line in xmp_str.split("\n") if line.strip() != ""]

View File

@@ -19,17 +19,17 @@ from ._constants import (
_MOVIE_TYPE,
_PHOTO_TYPE,
_PHOTOS_3_VERSION,
_PHOTOS_4_ALBUM_KIND,
_PHOTOS_4_TOP_LEVEL_ALBUM,
_PHOTOS_4_VERSION,
_PHOTOS_5_ALBUM_KIND,
_PHOTOS_5_FOLDER_KIND,
_PHOTOS_5_ROOT_FOLDER_KIND,
_PHOTOS_5_SHARED_ALBUM_KIND,
_PHOTOS_5_VERSION,
_TESTED_DB_VERSIONS,
_TESTED_OS_VERSIONS,
_UNKNOWN_PERSON,
_PHOTOS_4_ALBUM_KIND,
_PHOTOS_4_TOP_LEVEL_ALBUM,
_PHOTOS_5_ROOT_FOLDER_KIND,
_PHOTOS_5_FOLDER_KIND,
_PHOTOS_5_ALBUM_KIND,
_PHOTOS_5_SHARED_ALBUM_KIND,
)
from ._version import __version__
from .albuminfo import AlbumInfo, FolderInfo
@@ -77,6 +77,12 @@ class PhotosDB:
# set up the data structures used to store all the Photo database info
# if True, will treat persons as keywords when exporting metadata
self.use_persons_as_keywords = False
# if True, will treat albums as keywords when exporting metadata
self.use_albums_as_keywords = False
# Path to the Photos library database file
# photos.db in the photos library database/ directory
self._dbfile = None
@@ -240,7 +246,7 @@ class PhotosDB:
self._db_version = self._get_db_version()
# If Photos >= 5, actual data isn't in photos.db but in Photos.sqlite
if int(self._db_version) >= int(_PHOTOS_5_VERSION):
if int(self._db_version) > int(_PHOTOS_4_VERSION):
dbpath = pathlib.Path(self._dbfile).parent
dbfile = dbpath / "Photos.sqlite"
if not _check_file_exists(dbfile):
@@ -259,7 +265,7 @@ class PhotosDB:
library_path = os.path.dirname(os.path.abspath(dbfile))
(library_path, _) = os.path.split(library_path) # drop /database from path
self._library_path = library_path
if int(self._db_version) < int(_PHOTOS_5_VERSION):
if int(self._db_version) <= int(_PHOTOS_4_VERSION):
masters_path = os.path.join(library_path, "Masters")
self._masters_path = masters_path
else:
@@ -528,7 +534,6 @@ class PhotosDB:
""" process the Photos database to extract info
works on Photos version <= 4.0 """
# TODO: Update strings to remove + (not needed)
# Epoch is Jan 1, 2001
td = (datetime(2001, 1, 1, 0, 0) - datetime(1970, 1, 1, 0, 0)).total_seconds()
@@ -656,8 +661,10 @@ class PhotosDB:
# build folder hierarchy
for album, details in self._dbalbum_details.items():
parent_folder = details["folderUuid"]
if parent_folder != _PHOTOS_4_TOP_LEVEL_ALBUM:
# logging.warning(f"album = {details['title']}, parent = {parent_folder}")
if (
details["albumSubclass"] == _PHOTOS_4_ALBUM_KIND
and parent_folder != _PHOTOS_4_TOP_LEVEL_ALBUM
):
folder_hierarchy = self._build_album_folder_hierarchy_4(parent_folder)
self._dbalbum_folders[album] = folder_hierarchy
else:
@@ -888,6 +895,7 @@ class PhotosDB:
# TODO: NOT YET USED -- PLACEHOLDER for RAW processing (currently only in _process_database5)
# original resource choice (e.g. RAW or jpeg)
self._dbphotos[uuid]["original_resource_choice"] = None
self._dbphotos[uuid]["raw_is_original"] = None
# associated RAW image info
self._dbphotos[uuid]["has_raw"] = True if row[25] == 7 else False
@@ -903,17 +911,17 @@ class PhotosDB:
# get additional details from RKMaster, needed for RAW processing
c.execute(
""" SELECT
RKMaster.uuid,
RKMaster.uuid,
RKMaster.volumeId,
RKMaster.imagePath,
RKMaster.isMissing,
RKMaster.isMissing,
RKMaster.originalFileName,
RKMaster.UTI,
RKMaster.modelID,
RKMaster.modelID,
RKMaster.fileSize,
RKMaster.isTrulyRaw,
RKMaster.alternateMasterUuid
FROM RKMaster
RKMaster.alternateMasterUuid
FROM RKMaster
"""
)
@@ -1607,7 +1615,12 @@ class PhotosDB:
info["momentID"] = row[26]
# original resource choice (e.g. RAW or jpeg)
# for images part of a RAW/jpeg pair,
# ZADDITIONALASSETATTRIBUTES.ZORIGINALRESOURCECHOICE
# = 0 if jpeg is selected as "original" in Photos (the default)
# = 1 if RAW is selected as "original" in Photos
info["original_resource_choice"] = row[27]
info["raw_is_original"] = True if row[27] == 1 else False
# associated RAW image info
# will be filled in later
@@ -1878,7 +1891,7 @@ class PhotosDB:
# folder with no parent (e.g. shared iCloud folders)
return folders
if self._db_version >= _PHOTOS_5_VERSION and parent == self._folder_root_pk:
if self._db_version > _PHOTOS_4_VERSION and parent == self._folder_root_pk:
# at the top of the folder hierarchy, we're done
return folders
@@ -2156,3 +2169,7 @@ class PhotosDB:
return self.__dict__ == other.__dict__
return False
def __len__(self):
""" returns number of photos in the database """
return len(self._dbphotos)

View File

@@ -10,6 +10,7 @@
# This code isn't elegant but it seems to work well. PRs gladly accepted.
import datetime
import locale
import os
import pathlib
import re
@@ -18,6 +19,9 @@ from typing import Tuple, List # pylint: disable=syntax-error
from .photoinfo import PhotoInfo
from ._constants import _UNKNOWN_PERSON
# ensure locale set to user's locale
locale.setlocale(locale.LC_ALL, "")
# Permitted substitutions (each of these returns a single value or None)
TEMPLATE_SUBSTITUTIONS = {
"{name}": "Filename of the photo",

View File

@@ -79,16 +79,16 @@
xmlns:photoshop="http://ns.adobe.com/photoshop/1.0/">
${dc_description(photo.description)}
${dc_title(photo.title)}
${dc_subject(photo.keywords + photo.persons)}
${dc_subject(subjects)}
${dc_datecreated(photo.date)}
</rdf:Description>
<rdf:Description rdf:about=''
xmlns:Iptc4xmpExt='http://iptc.org/std/Iptc4xmpExt/2008-02-29/'>
${iptc_personinimage(photo.persons)}
${iptc_personinimage(persons)}
</rdf:Description>
<rdf:Description rdf:about=''
xmlns:digiKam='http://www.digikam.org/ns/1.0/'>
${dk_tagslist(photo.keywords)}
${dk_tagslist(keywords)}
</rdf:Description>
<rdf:Description rdf:about=''
xmlns:xmp='http://ns.adobe.com/xap/1.0/'>

View File

@@ -3,7 +3,7 @@
#
# setup.py script for osxphotos
#
# Copyright (c) 2019 Rhet Turnbull, rturnbull+git@gmail.com
# Copyright (c) 2019, 2020 Rhet Turnbull, rturnbull+git@gmail.com
# All rights reserved.
#
# Permission is hereby granted, free of charge, to any person
@@ -27,23 +27,43 @@
# SOFTWARE.
import os
import platform
from setuptools import find_packages, setup
this_directory = os.path.abspath(os.path.dirname(__file__))
with open(os.path.join(this_directory, "README.md"), encoding="utf-8") as f:
long_description = f.read()
# python version as 2-digit float (e.g. 3.6)
py_ver = float(".".join(platform.python_version_tuple()[:2]))
# holds config info read from disk
about = {}
this_directory = os.path.abspath(os.path.dirname(__file__))
# get version info from _version
with open(
os.path.join(this_directory, "osxphotos", "_version.py"), mode="r", encoding="utf-8"
) as f:
exec(f.read(), about)
# read README.md into long_description
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__"],
description="Manipulate (read-only) Apple's Photos app library on Mac OS X",
long_description=long_description,
long_description=about["long_description"],
long_description_content_type="text/markdown",
author="Rhet Turnbull",
author_email="rturnbull+git@gmail.com",
@@ -58,7 +78,7 @@ setup(
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Operating System :: MacOS :: MacOS X",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.8",
"Topic :: Software Development :: Libraries :: Python Modules",
],
install_requires=[

View File

@@ -9,6 +9,13 @@ To run the tests, do the following from the main source folder:
Running the tests this way allows the library to be tested without installing it.
## Skipped Tests ##
A few tests will look for certain environment variables to determine if they should run.
Some of the export tests rely on photos in my local library and will look for `OSXPHOTOS_TEST_EXPORT=1` to determine if they should run.
One test for locale does not run on GitHub's automated workflow and will look for `OSXPHOTOS_TEST_LOCALE=1` to determine if it should be run. If you want to run this test, set the environment variable.
## Attribution ##
These tests utilize a test Photos library. The test library is populated with photos from [flickr](https://www.flickr.com). All images used are licensed under Creative Commons 2.0 Attribution [license](https://creativecommons.org/licenses/by/2.0/).

View File

@@ -3,8 +3,8 @@
<plist version="1.0">
<dict>
<key>PhotoAnalysisGraphLastBackgroundGraphRebuildJobDate</key>
<date>2020-04-18T18:01:02Z</date>
<date>2020-04-25T23:54:43Z</date>
<key>PhotoAnalysisGraphLastBackgroundMemoryGenerationJobDate</key>
<date>2020-04-18T17:22:55Z</date>
<date>2020-04-26T06:26:10Z</date>
</dict>
</plist>

View File

@@ -11,6 +11,6 @@
<key>PLLastRevGeoForcedProviderOutOfDateCheckVersionKey</key>
<integer>1</integer>
<key>PLLastRevGeoVerFileFetchDateKey</key>
<date>2020-04-17T17:49:52Z</date>
<date>2020-04-25T23:54:29Z</date>
</dict>
</plist>

View File

@@ -24,7 +24,7 @@
<key>SnapshotCompletedDate</key>
<date>2019-07-27T13:16:43Z</date>
<key>SnapshotLastValidated</key>
<date>2020-04-17T17:51:16Z</date>
<date>2020-04-25T23:56:35Z</date>
<key>SnapshotTables</key>
<dict/>
</dict>

View File

@@ -7,7 +7,7 @@
<key>hostuuid</key>
<string>9575E48B-8D5F-5654-ABAC-4431B1167324</string>
<key>pid</key>
<integer>900</integer>
<integer>2500</integer>
<key>processname</key>
<string>photolibraryd</string>
<key>uid</key>

View File

@@ -3,24 +3,24 @@
<plist version="1.0">
<dict>
<key>BackgroundHighlightCollection</key>
<date>2020-04-17T14:33:32Z</date>
<date>2020-04-26T23:00:16Z</date>
<key>BackgroundHighlightEnrichment</key>
<date>2020-04-17T14:33:32Z</date>
<date>2020-04-26T23:00:15Z</date>
<key>BackgroundJobAssetRevGeocode</key>
<date>2020-04-17T14:33:33Z</date>
<date>2020-04-26T23:00:16Z</date>
<key>BackgroundJobSearch</key>
<date>2020-04-17T14:33:33Z</date>
<date>2020-04-26T23:00:16Z</date>
<key>BackgroundPeopleSuggestion</key>
<date>2020-04-17T14:33:31Z</date>
<date>2020-04-26T23:00:14Z</date>
<key>BackgroundUserBehaviorProcessor</key>
<date>2020-04-17T07:32:04Z</date>
<date>2020-04-26T18:44:36Z</date>
<key>PhotoAnalysisGraphLastBackgroundGraphConsistencyUpdateJobDateKey</key>
<date>2020-04-17T14:33:37Z</date>
<date>2020-04-26T23:00:17Z</date>
<key>PhotoAnalysisGraphLastBackgroundGraphRebuildJobDate</key>
<date>2020-04-17T07:32:00Z</date>
<date>2020-04-26T18:44:35Z</date>
<key>PhotoAnalysisGraphLastBackgroundMemoryGenerationJobDate</key>
<date>2020-04-17T14:33:34Z</date>
<date>2020-04-26T21:06:44Z</date>
<key>SiriPortraitDonation</key>
<date>2020-04-17T07:32:04Z</date>
<date>2020-04-26T18:44:36Z</date>
</dict>
</plist>

View File

@@ -3,8 +3,8 @@
<plist version="1.0">
<dict>
<key>FaceIDModelLastGenerationKey</key>
<date>2020-04-17T07:32:07Z</date>
<date>2020-04-26T18:44:37Z</date>
<key>LastContactClassificationKey</key>
<date>2020-04-17T07:32:12Z</date>
<date>2020-04-26T18:44:46Z</date>
</dict>
</plist>

View File

@@ -7,7 +7,7 @@
<key>hostuuid</key>
<string>9575E48B-8D5F-5654-ABAC-4431B1167324</string>
<key>pid</key>
<integer>3212</integer>
<integer>434</integer>
<key>processname</key>
<string>photolibraryd</string>
<key>uid</key>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

View File

@@ -3,24 +3,24 @@
<plist version="1.0">
<dict>
<key>BackgroundHighlightCollection</key>
<date>2020-04-11T20:00:25Z</date>
<date>2020-04-19T15:27:34Z</date>
<key>BackgroundHighlightEnrichment</key>
<date>2020-04-11T20:00:25Z</date>
<date>2020-04-19T15:27:34Z</date>
<key>BackgroundJobAssetRevGeocode</key>
<date>2020-04-11T20:00:25Z</date>
<date>2020-04-19T15:27:35Z</date>
<key>BackgroundJobSearch</key>
<date>2020-04-11T20:00:25Z</date>
<date>2020-04-19T15:27:35Z</date>
<key>BackgroundPeopleSuggestion</key>
<date>2020-04-11T20:00:24Z</date>
<date>2020-04-19T15:27:34Z</date>
<key>BackgroundUserBehaviorProcessor</key>
<date>2020-04-11T20:00:25Z</date>
<date>2020-04-19T15:27:35Z</date>
<key>PhotoAnalysisGraphLastBackgroundGraphConsistencyUpdateJobDateKey</key>
<date>2020-04-11T20:10:27Z</date>
<key>PhotoAnalysisGraphLastBackgroundGraphRebuildJobDate</key>
<date>2020-04-11T20:00:24Z</date>
<key>PhotoAnalysisGraphLastBackgroundMemoryGenerationJobDate</key>
<date>2020-04-11T20:00:25Z</date>
<date>2020-04-19T15:27:35Z</date>
<key>SiriPortraitDonation</key>
<date>2020-04-11T20:00:25Z</date>
<date>2020-04-19T15:27:35Z</date>
</dict>
</plist>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

View File

@@ -118,6 +118,13 @@ def test_init5():
with pytest.raises(Exception):
assert osxphotos.PhotosDB()
def test_db_len():
import osxphotos
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
# assert photosdb.db_version in osxphotos._TESTED_DB_VERSIONS
assert len(photosdb) == 12
def test_db_version():
import osxphotos
@@ -368,7 +375,7 @@ def test_count():
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos()
assert len(photos) == 8
assert len(photos) == 12
def test_keyword_2():
@@ -771,7 +778,7 @@ def test_from_to_date():
photosdb = osxphotos.PhotosDB(PHOTOS_DB)
photos = photosdb.photos(from_date=dt.datetime(2018, 10, 28))
assert len(photos) == 2
assert len(photos) ==6
photos = photosdb.photos(to_date=dt.datetime(2018, 10, 28))
assert len(photos) == 6

View File

@@ -1,3 +1,4 @@
import os
import pytest
from click.testing import CliRunner
@@ -46,16 +47,22 @@ CLI_EXPORT_FILENAMES = [
CLI_EXPORT_FILENAMES_CURRENT = [
"1EB2B765-0765-43BA-A90C-0D0580E6172C.jpeg",
"3DD2C897-F19E-4CA6-8C22-B027D5A71907.jpeg",
"4D521201-92AC-43E5-8F7C-59BC41C37A96.cr2",
"4D521201-92AC-43E5-8F7C-59BC41C37A96.jpeg",
"6191423D-8DB8-4D4C-92BE-9BBBA308AAC4.jpeg",
"A92D9C26-3A50-4197-9388-CB5F7DB9FA91.cr2",
"A92D9C26-3A50-4197-9388-CB5F7DB9FA91.jpeg",
"D05A5FE3-15FB-49A1-A15D-AB3DA6F8B068.dng",
"D79B8D77-BFFC-460B-9312-034F2877D35B.jpeg",
"DC99FBDD-7A52-4100-A5BB-344131646C30.jpeg",
"DC99FBDD-7A52-4100-A5BB-344131646C30_edited.jpeg",
"E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51.jpeg",
"E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51_edited.jpeg",
"D79B8D77-BFFC-460B-9312-034F2877D35B.jpeg",
"F12384F6-CD17-4151-ACBA-AE0E3688539E.jpeg",
"6191423D-8DB8-4D4C-92BE-9BBBA308AAC4.jpeg",
"3DD2C897-F19E-4CA6-8C22-B027D5A71907.jpeg",
]
CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES1 = [
"2019/April/wedding.jpg",
"2019/July/Tulips.jpg",
@@ -65,6 +72,12 @@ CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES1 = [
"2018/September/Pumkins1.jpg",
]
CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES_LOCALE = [
"2019/September/IMG_9975.JPEG",
"2020/Februar/IMG_1064.JPEG",
"2016/März/IMG_3984.JPEG",
]
CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES_ALBUM1 = [
"Multi Keyword/wedding.jpg",
"_/Tulips.jpg",
@@ -589,6 +602,49 @@ def test_export_directory_template_album_2():
assert os.path.isfile(os.path.join(workdir, filepath))
@pytest.mark.skipif(
"OSXPHOTOS_TEST_LOCALE" not in os.environ,
reason="Skip if running in Github actions",
)
def test_export_directory_template_locale():
# test export using directory template in user locale non-US
import os
import glob
import locale
import os.path
import osxphotos
from osxphotos.__main__ import export
runner = CliRunner()
cwd = os.getcwd()
# pylint: disable=not-context-manager
with runner.isolated_filesystem():
# set locale environment
os.environ["LANG"] = "de_DE.UTF-8"
os.environ["LC_COLLATE"] = "de_DE.UTF-8"
os.environ["LC_CTYPE"] = "de_DE.UTF-8"
os.environ["LC_MESSAGES"] = "de_DE.UTF-8"
os.environ["LC_MONETARY"] = "de_DE.UTF-8"
os.environ["LC_NUMERIC"] = "de_DE.UTF-8"
os.environ["LC_TIME"] = "de_DE.UTF-8"
locale.setlocale(locale.LC_ALL, "")
result = runner.invoke(
export,
[
os.path.join(cwd, PLACES_PHOTOS_DB),
".",
"-V",
"--directory",
"{created.year}/{created.month}",
],
)
assert result.exit_code == 0
workdir = os.getcwd()
for filepath in CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES_LOCALE:
assert os.path.isfile(os.path.join(workdir, filepath))
def test_places():
import json
import os

View File

@@ -482,6 +482,88 @@ def test_exiftool_json_sidecar():
assert json_got[k] == v
def test_exiftool_json_sidecar_use_persons_keyword():
import osxphotos
import json
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["xmp"]])
json_expected = json.loads(
"""
[{"_CreatedBy": "osxphotos, https://github.com/RhetTbull/osxphotos",
"EXIF:ImageDescription": "Girls with pumpkins",
"XMP:Description": "Girls with pumpkins",
"XMP:Title": "Can we carry this?",
"XMP:TagsList": ["Kids", "Suzy", "Katie"],
"IPTC:Keywords": ["Kids", "Suzy", "Katie"],
"XMP:PersonInImage": ["Suzy", "Katie"],
"XMP:Subject": ["Kids", "Suzy", "Katie"],
"EXIF:DateTimeOriginal": "2018:09:28 15:35:49",
"EXIF:OffsetTimeOriginal": "-04:00",
"EXIF:ModifyDate": "2019:11:24 13:09:17"}]
"""
)[0]
json_got = photos[0]._exiftool_json_sidecar(use_persons_as_keywords=True)
json_got = json.loads(json_got)[0]
# some gymnastics to account for different sort order in different pythons
# some gymnastics to account for different sort order in different pythons
for k, v in json_got.items():
if type(v) in (list, tuple):
assert sorted(json_expected[k]) == sorted(v)
else:
assert json_expected[k] == v
for k, v in json_expected.items():
if type(v) in (list, tuple):
assert sorted(json_got[k]) == sorted(v)
else:
assert json_got[k] == v
def test_exiftool_json_sidecar_use_albums_keyword():
import osxphotos
import json
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["xmp"]])
json_expected = json.loads(
"""
[{"_CreatedBy": "osxphotos, https://github.com/RhetTbull/osxphotos",
"EXIF:ImageDescription": "Girls with pumpkins",
"XMP:Description": "Girls with pumpkins",
"XMP:Title": "Can we carry this?",
"XMP:TagsList": ["Kids", "Pumpkin Farm", "Test Album"],
"IPTC:Keywords": ["Kids", "Pumpkin Farm", "Test Album"],
"XMP:PersonInImage": ["Suzy", "Katie"],
"XMP:Subject": ["Kids", "Suzy", "Katie"],
"EXIF:DateTimeOriginal": "2018:09:28 15:35:49",
"EXIF:OffsetTimeOriginal": "-04:00",
"EXIF:ModifyDate": "2019:11:24 13:09:17"}]
"""
)[0]
json_got = photos[0]._exiftool_json_sidecar(use_albums_as_keywords=True)
json_got = json.loads(json_got)[0]
# some gymnastics to account for different sort order in different pythons
# some gymnastics to account for different sort order in different pythons
for k, v in json_got.items():
if type(v) in (list, tuple):
assert sorted(json_expected[k]) == sorted(v)
else:
assert json_expected[k] == v
for k, v in json_expected.items():
if type(v) in (list, tuple):
assert sorted(json_got[k]) == sorted(v)
else:
assert json_got[k] == v
def test_xmp_sidecar():
import osxphotos
@@ -539,3 +621,125 @@ def test_xmp_sidecar():
for line_expected, line_got in zip(xmp_expected_lines, xmp_got_lines):
assert line_expected == line_got
def test_xmp_sidecar_use_persons_keyword():
import osxphotos
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["xmp"]])
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>Girls with pumpkins</dc:description>
<dc:title>Can we carry this?</dc:title>
<!-- keywords and persons listed in <dc:subject> as Photos does -->
<dc:subject>
<rdf:Seq>
<rdf:li>Kids</rdf:li>
<rdf:li>Suzy</rdf:li>
<rdf:li>Katie</rdf:li>
</rdf:Seq>
</dc:subject>
<photoshop:DateCreated>2018-09-28T15:35:49.063000-04:00</photoshop:DateCreated>
</rdf:Description>
<rdf:Description rdf:about=''
xmlns:Iptc4xmpExt='http://iptc.org/std/Iptc4xmpExt/2008-02-29/'>
<Iptc4xmpExt:PersonInImage>
<rdf:Bag>
<rdf:li>Suzy</rdf:li>
<rdf:li>Katie</rdf:li>
</rdf:Bag>
</Iptc4xmpExt:PersonInImage>
</rdf:Description>
<rdf:Description rdf:about=''
xmlns:digiKam='http://www.digikam.org/ns/1.0/'>
<digiKam:TagsList>
<rdf:Seq>
<rdf:li>Kids</rdf:li>
<rdf:li>Suzy</rdf:li>
<rdf:li>Katie</rdf:li>
</rdf:Seq>
</digiKam:TagsList>
</rdf:Description>
<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:RDF>
</x:xmpmeta>"""
xmp_expected_lines = [line.strip() for line in xmp_expected.split("\n")]
xmp_got = photos[0]._xmp_sidecar(use_persons_as_keywords=True)
xmp_got_lines = [line.strip() for line in xmp_got.split("\n")]
for line_expected, line_got in zip(xmp_expected_lines, xmp_got_lines):
assert line_expected == line_got
def test_xmp_sidecar_use_albums_keyword():
import osxphotos
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["xmp"]])
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>Girls with pumpkins</dc:description>
<dc:title>Can we carry this?</dc:title>
<!-- keywords and persons listed in <dc:subject> as Photos does -->
<dc:subject>
<rdf:Seq>
<rdf:li>Kids</rdf:li>
<rdf:li>Suzy</rdf:li>
<rdf:li>Katie</rdf:li>
</rdf:Seq>
</dc:subject>
<photoshop:DateCreated>2018-09-28T15:35:49.063000-04:00</photoshop:DateCreated>
</rdf:Description>
<rdf:Description rdf:about=''
xmlns:Iptc4xmpExt='http://iptc.org/std/Iptc4xmpExt/2008-02-29/'>
<Iptc4xmpExt:PersonInImage>
<rdf:Bag>
<rdf:li>Suzy</rdf:li>
<rdf:li>Katie</rdf:li>
</rdf:Bag>
</Iptc4xmpExt:PersonInImage>
</rdf:Description>
<rdf:Description rdf:about=''
xmlns:digiKam='http://www.digikam.org/ns/1.0/'>
<digiKam:TagsList>
<rdf:Seq>
<rdf:li>Kids</rdf:li>
<rdf:li>Pumpkin Farm</rdf:li>
<rdf:li>Test Album</rdf:li>
</rdf:Seq>
</digiKam:TagsList>
</rdf:Description>
<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:RDF>
</x:xmpmeta>"""
xmp_expected_lines = [line.strip() for line in xmp_expected.split("\n")]
xmp_got = photos[0]._xmp_sidecar(use_albums_as_keywords=True)
xmp_got_lines = [line.strip() for line in xmp_got.split("\n")]
for line_expected, line_got in zip(xmp_expected_lines, xmp_got_lines):
assert line_expected == line_got

View File

@@ -58,6 +58,13 @@ def test_db_version():
assert photosdb.db_version in osxphotos._constants._TESTED_DB_VERSIONS
assert photosdb.db_version == "4025"
def test_db_len():
import osxphotos
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
# assert photosdb.db_version in osxphotos._TESTED_DB_VERSIONS
assert len(photosdb) == 7
def test_os_version():
import osxphotos

View File

@@ -53,6 +53,41 @@ TEMPLATE_VALUES = {
}
TEMPLATE_VALUES_DEU = {
"{name}": "128FB4C6-0B16-4E7D-9108-FB2E90DA1546",
"{original_name}": "IMG_1064",
"{title}": "Glen Ord",
"{descr}": "Jack Rose Dining Saloon",
"{created.date}": "2020-02-04",
"{created.year}": "2020",
"{created.yy}": "20",
"{created.mm}": "02",
"{created.month}": "Februar",
"{created.mon}": "Feb",
"{created.doy}": "035",
"{modified.date}": "2020-03-21",
"{modified.year}": "2020",
"{modified.yy}": "20",
"{modified.mm}": "03",
"{modified.month}": "März",
"{modified.mon}": "Mär",
"{modified.doy}": "081",
"{place.name}": "Washington, District of Columbia, United States",
"{place.country_code}": "US",
"{place.name.country}": "United States",
"{place.name.state_province}": "District of Columbia",
"{place.name.city}": "Washington",
"{place.name.area_of_interest}": "_",
"{place.address}": "2038 18th St NW, Washington, DC 20009, United States",
"{place.address.street}": "2038 18th St NW",
"{place.address.city}": "Washington",
"{place.address.state_province}": "DC",
"{place.address.postal_code}": "20009",
"{place.address.country}": "United States",
"{place.address.country_code}": "US",
}
def test_lookup():
""" Test that a lookup is returned for every possible value """
import re
@@ -87,6 +122,49 @@ def test_subst():
assert rendered[0] == TEMPLATE_VALUES[template]
def test_subst_locale_1():
""" Test that substitutions are correct in user locale"""
import locale
import osxphotos
# osxphotos.template sets local on load so set the environment first
# set locale to DE
locale.setlocale(locale.LC_ALL, "de_DE.UTF-8")
from osxphotos.template import render_filepath_template
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_PLACES)
photo = photosdb.photos(uuid=[UUID_DICT["place_dc"]])[0]
for template in TEMPLATE_VALUES_DEU:
rendered, _ = render_filepath_template(template, photo)
assert rendered[0] == TEMPLATE_VALUES_DEU[template]
def test_subst_locale_2():
""" Test that substitutions are correct in user locale"""
import locale
import os
import osxphotos
# osxphotos.template sets local on load so set the environment first
os.environ["LANG"] = "de_DE.UTF-8"
os.environ["LC_COLLATE"] = "de_DE.UTF-8"
os.environ["LC_CTYPE"] = "de_DE.UTF-8"
os.environ["LC_MESSAGES"] = "de_DE.UTF-8"
os.environ["LC_MONETARY"] = "de_DE.UTF-8"
os.environ["LC_NUMERIC"] = "de_DE.UTF-8"
os.environ["LC_TIME"] = "de_DE.UTF-8"
from osxphotos.template import render_filepath_template
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_PLACES)
photo = photosdb.photos(uuid=[UUID_DICT["place_dc"]])[0]
for template in TEMPLATE_VALUES_DEU:
rendered, _ = render_filepath_template(template, photo)
assert rendered[0] == TEMPLATE_VALUES_DEU[template]
def test_subst_default_val():
""" Test substitution with default value specified """
import locale