Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a57da2346b | ||
|
|
3fe03cd127 | ||
|
|
5cc98c338b | ||
|
|
1c9d4f282b | ||
|
|
1ceda15134 | ||
|
|
a80071111f | ||
|
|
072a8d795e | ||
|
|
b35b071634 | ||
|
|
56a000609f | ||
|
|
54d5d4b7ba | ||
|
|
38137a1351 | ||
|
|
4b29a2e05f | ||
|
|
9be0f849b7 | ||
|
|
ccb5f252d1 | ||
|
|
d8a64c9573 | ||
|
|
81d4e392c3 | ||
|
|
85d2baac10 | ||
|
|
8a768e62ce | ||
|
|
1c8eb764f5 | ||
|
|
8e4b88ad1f | ||
|
|
3f80f786a3 | ||
|
|
a337e79e13 | ||
|
|
ec68feec49 | ||
|
|
9b9b54e590 | ||
|
|
22f1e8f2a6 | ||
|
|
1867c1d747 | ||
|
|
87eb84fddd | ||
|
|
15a3736b74 | ||
|
|
cf28cb6452 | ||
|
|
f20fadcef7 |
2
.github/workflows/pythonpackage.yml
vendored
@@ -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
|
||||
|
||||
46
CHANGELOG.md
@@ -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 < 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 <= 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
|
||||
|
||||
|
||||
26
README.md
@@ -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
@@ -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
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
""" version info """
|
||||
|
||||
__version__ = "0.28.2"
|
||||
__version__ = "0.28.10"
|
||||
|
||||
@@ -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() != ""]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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/'>
|
||||
|
||||
32
setup.py
@@ -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=[
|
||||
|
||||
@@ -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/).
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
After Width: | Height: | Size: 2.9 MiB |
|
After Width: | Height: | Size: 2.8 MiB |
@@ -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>
|
||||
|
||||
|
After Width: | Height: | Size: 2.3 MiB |
|
After Width: | Height: | Size: 218 KiB |
|
After Width: | Height: | Size: 249 KiB |
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 115 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 48 KiB |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||