Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05f111a287 | ||
|
|
83915c65ab | ||
|
|
22f44f7f40 | ||
|
|
02ef0f9a25 | ||
|
|
6347d94dfb | ||
|
|
a32c102d62 | ||
|
|
38842ff924 |
@@ -118,6 +118,15 @@
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "synox",
|
||||
"name": "Aravindo Wingeier",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/2250964?v=4",
|
||||
"profile": "https://github.com/synox",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7
|
||||
|
||||
@@ -4,6 +4,12 @@ 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.39.5](https://github.com/RhetTbull/osxphotos/compare/v0.39.3...v0.39.5)
|
||||
|
||||
> 3 January 2021
|
||||
|
||||
- Implemented text replacement for templates, issue #316 [`478715a`](https://github.com/RhetTbull/osxphotos/commit/478715a363f5009e4a38148e832bf0ad3c4cc4f8)
|
||||
|
||||
#### [v0.39.3](https://github.com/RhetTbull/osxphotos/compare/v0.39.2...v0.39.3)
|
||||
|
||||
> 31 December 2020
|
||||
|
||||
76
README.md
76
README.md
@@ -3,7 +3,7 @@
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://github.com/RhetTbull/osxphotos/workflows/Python%20package/badge.svg)
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||
[](#contributors-)
|
||||
[](#contributors-)
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||
|
||||
- [OSXPhotos](#osxphotos)
|
||||
@@ -40,31 +40,29 @@
|
||||
|
||||
## What is osxphotos?
|
||||
|
||||
OSXPhotos provides the ability to interact with and query Apple's Photos.app library database on MacOS. Using this package you can query the Photos database for information about the photos stored in a Photos library on your Mac--for example, file name, file path, and metadata such as keywords/tags, persons/faces, albums, etc. You can also easily export both the original and edited photos.
|
||||
OSXPhotos provides the ability to interact with and query Apple's Photos.app library on macOS. You can query the Photos library database -- for example, file name, file path, and metadata such as keywords/tags, persons/faces, albums, etc. You can also easily export both the original and edited photos.
|
||||
|
||||
## Supported operating systems
|
||||
|
||||
Only works on MacOS (aka Mac OS X). Tested on MacOS 10.12.6 / Photos 2.0, 10.13.6 / Photos 3.0, MacOS 10.14.5, 10.14.6 / Photos 4.0, MacOS 10.15.1 - 10.15.7 / Photos 5.0.
|
||||
Only works on macOS (aka Mac OS X). Tested on macOS Sierra (10.12.6) until macOS Catalina (10.15.7).
|
||||
|
||||
Beta support for MacOS 10.16/MacOS 11 Big Sur Beta / Photos 6.0. Not tested on M1 / Apple silicon Macs.
|
||||
| macOS Version | macOS name | Photos.app version |
|
||||
| ----------------- |------------|:-------------------|
|
||||
| 10.16 | Big Sur | 6.0 ⚠️ Beta support, not tested on M1 / Apple silicon Macs. |
|
||||
| 10.15.1 - 10.15.7 | Catalina | 5.0 ✅ |
|
||||
| 10.14.5, 10.14.6 | Mojave | 4.0 ✅ |
|
||||
| 10.13.6 | High Sierra| 3.0 ✅ |
|
||||
| 10.12.6 | Sierra | 2.0 ✅ |
|
||||
|
||||
Requires python >= 3.7.
|
||||
This package will read Photos databases for any supported version on any supported macOS version. E.g. you can read a database created with Photos 5.0 on MacOS 10.15 on a machine running macOS 10.12 and vice versa.
|
||||
|
||||
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 5.0 on MacOS 10.15 on a machine running MacOS 10.12 and vice versa.
|
||||
Requires python >= `3.7`.
|
||||
|
||||
|
||||
## Installation instructions
|
||||
|
||||
OSXPhotos uses setuptools, thus simply run:
|
||||
|
||||
python3 setup.py install
|
||||
|
||||
You can also install directly from [pypi](https://pypi.org/project/osxphotos/):
|
||||
|
||||
pip install osxphotos
|
||||
|
||||
I recommend you create a [virtual environment](https://docs.python.org/3/tutorial/venv.html) before installing osxphotos.
|
||||
## Installation
|
||||
If you are new to python, I recommend you to install using pipx. See other advanced options below.
|
||||
|
||||
### Installation using pipx
|
||||
If you aren't familiar with installing python applications, I recommend you install `osxphotos` with [pipx](https://github.com/pipxproject/pipx). If you use `pipx`, you will not need to create a virtual environment as `pipx` takes care of this. The easiest way to do this on a Mac is to use [homebrew](https://brew.sh/):
|
||||
|
||||
- Open `Terminal` (search for `Terminal` in Spotlight or look in `Applications/Utilities`)
|
||||
@@ -73,7 +71,21 @@ If you aren't familiar with installing python applications, I recommend you inst
|
||||
- Then type this: `pipx install osxphotos`
|
||||
- Now you should be able to run `osxphotos` by typing: `osxphotos`
|
||||
|
||||
**WARNING** The git repo for this project is very large (> 1GB) because it contains multiple Photos libraries used for testing on different versions of MacOS. If you just want to use the osxphotos package in your own code, I recommend you install the latest version from [PyPI](https://pypi.org/project/osxphotos/) which does not include all the test libraries. If you just want to use the command line utility, you can download a pre-built executable of the latest [release](https://github.com/RhetTbull/osxphotos/releases) or you can install via `pip` which also installs the command line app. If you aren't comfortable with running python on your Mac, start with the pre-built executable or `pipx` as described above.
|
||||
### Installation using pip
|
||||
You can also install directly from [pypi](https://pypi.org/project/osxphotos/):
|
||||
|
||||
pip install osxphotos
|
||||
|
||||
### Installation from git repository
|
||||
OSXPhotos uses setuptools, thus simply run:
|
||||
|
||||
git clone https://github.com/RhetTbull/osxphotos.git
|
||||
cd osxphotos
|
||||
python3 setup.py install
|
||||
|
||||
I recommend you create a [virtual environment](https://docs.python.org/3/tutorial/venv.html) before installing osxphotos.
|
||||
|
||||
**WARNING** The git repo for this project is very large (> 1GB) because it contains multiple Photos libraries used for testing on different versions of macOS. If you just want to use the osxphotos package in your own code, I recommend you install the latest version from [PyPI](https://pypi.org/project/osxphotos/) which does not include all the test libraries. If you just want to use the command line utility, you can download a pre-built executable of the latest [release](https://github.com/RhetTbull/osxphotos/releases) or you can install via `pip` which also installs the command line app. If you aren't comfortable with running python on your Mac, start with the pre-built executable or `pipx` as described above.
|
||||
|
||||
## Command Line Usage
|
||||
|
||||
@@ -446,6 +458,13 @@ Options:
|
||||
do not include an extension in the FILENAME
|
||||
template. See below for additional details
|
||||
on templating system.
|
||||
--strip Optionally strip leading and trailing
|
||||
whitespace from any rendered templates. For
|
||||
example, if --filename template is "{title,}
|
||||
{original_name}" and image has no title,
|
||||
resulting file would have a leading space
|
||||
but if used with --strip, this will be
|
||||
removed.
|
||||
--edited-suffix SUFFIX Optional suffix template for naming edited
|
||||
photos. Default name for edited photos is
|
||||
in form 'photoname_edited.ext'. For example,
|
||||
@@ -885,6 +904,19 @@ Substitution Description
|
||||
e.g. 'Summer'; (Photos 5+ only, applied
|
||||
automatically by Photos' image
|
||||
categorization algorithms).
|
||||
{exif.camera_make} Camera make from original photo's EXIF
|
||||
inormation as imported by Photos, e.g.
|
||||
'Apple'
|
||||
{exif.camera_model} Camera model from original photo's EXIF
|
||||
inormation as imported by Photos, e.g.
|
||||
'iPhone 6s'
|
||||
{exif.lens_model} Lens model from original photo's EXIF
|
||||
inormation as imported by Photos, e.g.
|
||||
'iPhone 6s back camera 4.15mm f/2.2'
|
||||
{uuid} Photo's internal universally unique
|
||||
identifier (UUID) for the photo, a
|
||||
36-character string unique to the photo,
|
||||
e.g. '128FB4C6-0B16-4E7D-9108-FB2E90DA1546'
|
||||
|
||||
The following substitutions may result in multiple values. Thus if specified
|
||||
for --directory these could result in multiple copies of a photo being being
|
||||
@@ -1778,7 +1810,7 @@ If overwrite=False and increment=False, export will fail if destination file alr
|
||||
|
||||
#### <a name="rendertemplate">`render_template()`</a>
|
||||
|
||||
`render_template(template_str, none_str = "_", path_sep = None, expand_inplace = False, inplace_sep = None, filename=False, dirname=False)`
|
||||
`render_template(template_str, none_str = "_", path_sep = None, expand_inplace = False, inplace_sep = None, filename=False, dirname=False, strip=False)`
|
||||
|
||||
Render template string for photo. none_str is used if template substitution results in None value and no default specified.
|
||||
|
||||
@@ -1789,6 +1821,7 @@ Render template string for photo. none_str is used if template substitution res
|
||||
- `inplace_sep`: optional string to use as separator between multi-valued keywords with expand_inplace; default is ','
|
||||
- `filename`: if True, template output will be sanitized to produce valid file name
|
||||
- `dirname`: if True, template output will be sanitized to produce valid directory name
|
||||
- `strip`: if True, leading/trailign whitespace will be stripped from rendered template strings
|
||||
|
||||
Returns a tuple of (rendered, unmatched) where rendered is a list of rendered strings with all substitutions made and unmatched is a list of any strings that resembled a template substitution but did not match a known substitution. E.g. if template contained "{foo}", unmatched would be ["foo"].
|
||||
|
||||
@@ -2401,6 +2434,10 @@ The following template field substitutions are availabe for use with `PhotoInfo.
|
||||
|{place.address.country}|Country name of the postal address, e.g. 'United States'|
|
||||
|{place.address.country_code}|ISO country code of the postal address, e.g. 'US'|
|
||||
|{searchinfo.season}|Season of the year associated with a photo, e.g. 'Summer'; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).|
|
||||
|{exif.camera_make}|Camera make from original photo's EXIF inormation as imported by Photos, e.g. 'Apple'|
|
||||
|{exif.camera_model}|Camera model from original photo's EXIF inormation as imported by Photos, e.g. 'iPhone 6s'|
|
||||
|{exif.lens_model}|Lens model from original photo's EXIF inormation as imported by Photos, e.g. 'iPhone 6s back camera 4.15mm f/2.2'|
|
||||
|{uuid}|Photo's internal universally unique identifier (UUID) for the photo, a 36-character string unique to the photo, e.g. '128FB4C6-0B16-4E7D-9108-FB2E90DA1546'|
|
||||
|{album}|Album(s) photo is contained in|
|
||||
|{folder_album}|Folder path + album photo is contained in. e.g. 'Folder/Subfolder/Album' or just 'Album' if no enclosing folder|
|
||||
|{keyword}|Keyword(s) assigned to photo|
|
||||
@@ -2536,6 +2573,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||
<td align="center"><a href="https://github.com/hhoeck"><img src="https://avatars1.githubusercontent.com/u/6313998?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Horst Höck</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=hhoeck" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/jstrine"><img src="https://avatars1.githubusercontent.com/u/33943447?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Jonathan Strine</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=jstrine" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/finestream"><img src="https://avatars1.githubusercontent.com/u/16638513?v=4?s=100" width="100px;" alt=""/><br /><sub><b>finestream</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=finestream" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/synox"><img src="https://avatars2.githubusercontent.com/u/2250964?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Aravindo Wingeier</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=synox" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
@@ -1586,6 +1586,14 @@ def query(
|
||||
"File extension will be added automatically--do not include an extension in the FILENAME template. "
|
||||
"See below for additional details on templating system.",
|
||||
)
|
||||
@click.option(
|
||||
"--strip",
|
||||
is_flag=True,
|
||||
help="Optionally strip leading and trailing whitespace from any rendered templates. "
|
||||
'For example, if --filename template is "{title,} {original_name}" and image has no '
|
||||
"title, resulting file would have a leading space but if used with --strip, this will "
|
||||
"be removed.",
|
||||
)
|
||||
@click.option(
|
||||
"--edited-suffix",
|
||||
metavar="SUFFIX",
|
||||
@@ -1749,6 +1757,7 @@ def export(
|
||||
has_raw,
|
||||
directory,
|
||||
filename_template,
|
||||
strip,
|
||||
edited_suffix,
|
||||
original_suffix,
|
||||
place,
|
||||
@@ -1887,6 +1896,7 @@ def export(
|
||||
has_raw = cfg.has_raw
|
||||
directory = cfg.directory
|
||||
filename_template = cfg.filename_template
|
||||
strip = cfg.strip
|
||||
edited_suffix = cfg.edited_suffix
|
||||
original_suffix = cfg.original_suffix
|
||||
place = cfg.place
|
||||
@@ -2252,6 +2262,7 @@ def export(
|
||||
ignore_date_modified=ignore_date_modified,
|
||||
use_photokit=use_photokit,
|
||||
exiftool_option=exiftool_option,
|
||||
strip=strip,
|
||||
)
|
||||
results += export_results
|
||||
|
||||
@@ -2276,13 +2287,14 @@ def export(
|
||||
person_keyword=person_keyword,
|
||||
exiftool_merge_keywords=exiftool_merge_keywords,
|
||||
finder_tag_template=finder_tag_template,
|
||||
strip=strip,
|
||||
)
|
||||
results.xattr_written.extend(tags_written)
|
||||
results.xattr_skipped.extend(tags_skipped)
|
||||
|
||||
if xattr_template:
|
||||
xattr_written, xattr_skipped = write_extended_attributes(
|
||||
p, photo_files, xattr_template
|
||||
p, photo_files, xattr_template, strip=strip
|
||||
)
|
||||
results.xattr_written.extend(xattr_written)
|
||||
results.xattr_skipped.extend(xattr_skipped)
|
||||
@@ -2822,6 +2834,7 @@ def export_photo(
|
||||
ignore_date_modified=False,
|
||||
use_photokit=False,
|
||||
exiftool_option=None,
|
||||
strip=False,
|
||||
):
|
||||
"""Helper function for export that does the actual export
|
||||
|
||||
@@ -2912,12 +2925,14 @@ def export_photo(
|
||||
if photo.hasadjustments and photo.path_edited is None:
|
||||
missing_edited = True
|
||||
|
||||
filenames = get_filenames_from_template(photo, filename_template, original_name)
|
||||
filenames = get_filenames_from_template(
|
||||
photo, filename_template, original_name, strip=strip
|
||||
)
|
||||
for filename in filenames:
|
||||
if original_suffix:
|
||||
try:
|
||||
rendered_suffix, unmatched = photo.render_template(
|
||||
original_suffix, filename=True
|
||||
original_suffix, filename=True, strip=strip
|
||||
)
|
||||
except ValueError:
|
||||
raise click.BadOptionUsage(
|
||||
@@ -2950,7 +2965,7 @@ def export_photo(
|
||||
)
|
||||
|
||||
dest_paths = get_dirnames_from_template(
|
||||
photo, directory, export_by_date, dest, dry_run
|
||||
photo, directory, export_by_date, dest, dry_run, strip=strip
|
||||
)
|
||||
|
||||
sidecar = [s.lower() for s in sidecar]
|
||||
@@ -3062,7 +3077,7 @@ def export_photo(
|
||||
if edited_suffix:
|
||||
try:
|
||||
rendered_suffix, unmatched = photo.render_template(
|
||||
edited_suffix, filename=True
|
||||
edited_suffix, filename=True, strip=strip
|
||||
)
|
||||
except ValueError:
|
||||
raise click.BadOptionUsage(
|
||||
@@ -3167,7 +3182,7 @@ def export_photo(
|
||||
return results
|
||||
|
||||
|
||||
def get_filenames_from_template(photo, filename_template, original_name):
|
||||
def get_filenames_from_template(photo, filename_template, original_name, strip=False):
|
||||
"""get list of export filenames for a photo
|
||||
|
||||
Args:
|
||||
@@ -3185,7 +3200,7 @@ def get_filenames_from_template(photo, filename_template, original_name):
|
||||
photo_ext = pathlib.Path(photo.original_filename).suffix
|
||||
try:
|
||||
filenames, unmatched = photo.render_template(
|
||||
filename_template, path_sep="_", filename=True
|
||||
filename_template, path_sep="_", filename=True, strip=strip
|
||||
)
|
||||
except ValueError:
|
||||
raise click.BadOptionUsage(
|
||||
@@ -3208,7 +3223,9 @@ def get_filenames_from_template(photo, filename_template, original_name):
|
||||
return filenames
|
||||
|
||||
|
||||
def get_dirnames_from_template(photo, directory, export_by_date, dest, dry_run):
|
||||
def get_dirnames_from_template(
|
||||
photo, directory, export_by_date, dest, dry_run, strip=False
|
||||
):
|
||||
"""get list of directories to export a photo into, creates directories if they don't exist
|
||||
|
||||
Args:
|
||||
@@ -3236,7 +3253,9 @@ def get_dirnames_from_template(photo, directory, export_by_date, dest, dry_run):
|
||||
elif directory:
|
||||
# got a directory template, render it and check results are valid
|
||||
try:
|
||||
dirnames, unmatched = photo.render_template(directory, dirname=True)
|
||||
dirnames, unmatched = photo.render_template(
|
||||
directory, dirname=True, strip=strip
|
||||
)
|
||||
except ValueError:
|
||||
raise click.BadOptionUsage("directory", f"Invalid template '{directory}'")
|
||||
if not dirnames or unmatched:
|
||||
@@ -3498,6 +3517,7 @@ def write_finder_tags(
|
||||
person_keyword=None,
|
||||
exiftool_merge_keywords=None,
|
||||
finder_tag_template=None,
|
||||
strip=False,
|
||||
):
|
||||
"""Write Finder tags (extended attributes) to files; only writes attributes if attributes on file differ from what would be written
|
||||
|
||||
@@ -3537,7 +3557,10 @@ def write_finder_tags(
|
||||
for template_str in finder_tag_template:
|
||||
try:
|
||||
rendered, unmatched = photo.render_template(
|
||||
template_str, none_str=_OSXPHOTOS_NONE_SENTINEL, path_sep="/"
|
||||
template_str,
|
||||
none_str=_OSXPHOTOS_NONE_SENTINEL,
|
||||
path_sep="/",
|
||||
strip=strip,
|
||||
)
|
||||
except ValueError:
|
||||
raise click.BadOptionUsage(
|
||||
@@ -3575,7 +3598,7 @@ def write_finder_tags(
|
||||
return (written, skipped)
|
||||
|
||||
|
||||
def write_extended_attributes(photo, files, xattr_template):
|
||||
def write_extended_attributes(photo, files, xattr_template, strip=False):
|
||||
""" Writes extended attributes to exported files
|
||||
|
||||
Args:
|
||||
@@ -3590,7 +3613,10 @@ def write_extended_attributes(photo, files, xattr_template):
|
||||
for xattr, template_str in xattr_template:
|
||||
try:
|
||||
rendered, unmatched = photo.render_template(
|
||||
template_str, none_str=_OSXPHOTOS_NONE_SENTINEL, path_sep="/"
|
||||
template_str,
|
||||
none_str=_OSXPHOTOS_NONE_SENTINEL,
|
||||
path_sep="/",
|
||||
strip=strip,
|
||||
)
|
||||
except ValueError:
|
||||
raise click.BadOptionUsage(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
""" version info """
|
||||
|
||||
__version__ = "0.39.4"
|
||||
__version__ = "0.39.6"
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
# reference: https://stackoverflow.com/questions/59330149/coreimage-ciimage-write-jpg-is-shifting-colors-macos/59334308#59334308
|
||||
|
||||
import logging
|
||||
import pathlib
|
||||
|
||||
import Metal
|
||||
@@ -16,6 +15,11 @@ from Foundation import NSDictionary
|
||||
from wurlitzer import pipes
|
||||
|
||||
|
||||
class ImageConversionError(Exception):
|
||||
"""Base class for exceptions in this module. """
|
||||
|
||||
pass
|
||||
|
||||
class ImageConverter:
|
||||
""" Convert images to jpeg. This class is a singleton
|
||||
which will re-use the Core Image CIContext to avoid
|
||||
@@ -60,6 +64,7 @@ class ImageConverter:
|
||||
Raises:
|
||||
ValueError if compression quality not in range 0.0 to 1.0
|
||||
FileNotFoundError if input_path doesn't exist
|
||||
ImageConversionError if error during conversion
|
||||
"""
|
||||
|
||||
# accept input_path or output_path as pathlib.Path
|
||||
@@ -89,8 +94,7 @@ class ImageConverter:
|
||||
input_image = Quartz.CIImage.imageWithContentsOfURL_(input_url)
|
||||
|
||||
if input_image is None:
|
||||
logging.debug(f"Could not create CIImage for {input_path}")
|
||||
return False
|
||||
raise ImageConversionError(f"Could not create CIImage for {input_path}")
|
||||
|
||||
output_colorspace = input_image.colorSpace() or Quartz.CGColorSpaceCreateWithName(
|
||||
Quartz.CoreGraphics.kCGColorSpaceSRGB
|
||||
@@ -105,8 +109,7 @@ class ImageConverter:
|
||||
if not error:
|
||||
return True
|
||||
else:
|
||||
logging.debug(
|
||||
raise ImageConversionError(
|
||||
"Error converting file {input_path} to jpeg at {output_path}: {error}"
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
@@ -832,6 +832,7 @@ class PhotoInfo:
|
||||
inplace_sep=None,
|
||||
filename=False,
|
||||
dirname=False,
|
||||
strip=False,
|
||||
):
|
||||
"""Renders a template string for PhotoInfo instance using PhotoTemplate
|
||||
|
||||
@@ -846,6 +847,7 @@ class PhotoInfo:
|
||||
with expand_inplace; default is ','
|
||||
filename: if True, template output will be sanitized to produce valid file name
|
||||
dirname: if True, template output will be sanitized to produce valid directory name
|
||||
strip: if True, strips leading/trailing white space from resulting template
|
||||
|
||||
Returns:
|
||||
([rendered_strings], [unmatched]): tuple of list of rendered strings and list of unmatched template values
|
||||
@@ -859,6 +861,7 @@ class PhotoInfo:
|
||||
inplace_sep=inplace_sep,
|
||||
filename=filename,
|
||||
dirname=dirname,
|
||||
strip=strip,
|
||||
)
|
||||
|
||||
@property
|
||||
|
||||
@@ -118,6 +118,10 @@ TEMPLATE_SUBSTITUTIONS = {
|
||||
"{place.address.country}": "Country name of the postal address, e.g. 'United States'",
|
||||
"{place.address.country_code}": "ISO country code of the postal address, e.g. 'US'",
|
||||
"{searchinfo.season}": "Season of the year associated with a photo, e.g. 'Summer'; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).",
|
||||
"{exif.camera_make}": "Camera make from original photo's EXIF inormation as imported by Photos, e.g. 'Apple'",
|
||||
"{exif.camera_model}": "Camera model from original photo's EXIF inormation as imported by Photos, e.g. 'iPhone 6s'",
|
||||
"{exif.lens_model}": "Lens model from original photo's EXIF inormation as imported by Photos, e.g. 'iPhone 6s back camera 4.15mm f/2.2'",
|
||||
"{uuid}": "Photo's internal universally unique identifier (UUID) for the photo, a 36-character string unique to the photo, e.g. '128FB4C6-0B16-4E7D-9108-FB2E90DA1546'",
|
||||
}
|
||||
|
||||
# Permitted multi-value substitutions (each of these returns None or 1 or more values)
|
||||
@@ -251,6 +255,7 @@ class PhotoTemplate:
|
||||
inplace_sep=None,
|
||||
filename=False,
|
||||
dirname=False,
|
||||
strip=False,
|
||||
):
|
||||
""" Render a filename or directory template
|
||||
|
||||
@@ -264,6 +269,7 @@ class PhotoTemplate:
|
||||
with expand_inplace; default is ','
|
||||
filename: if True, template output will be sanitized to produce valid file name
|
||||
dirname: if True, template output will be sanitized to produce valid directory name
|
||||
strip: if True, strips leading/trailing whitespace from rendered templates
|
||||
|
||||
Returns:
|
||||
([rendered_strings], [unmatched]): tuple of list of rendered strings and list of unmatched template values
|
||||
@@ -364,6 +370,11 @@ class PhotoTemplate:
|
||||
sanitize_filename(rendered_str) for rendered_str in rendered_strings
|
||||
]
|
||||
|
||||
if strip:
|
||||
rendered_strings = [
|
||||
rendered_str.strip() for rendered_str in rendered_strings
|
||||
]
|
||||
|
||||
return rendered_strings, unmatched
|
||||
|
||||
def _render_multi_valued_templates(
|
||||
@@ -890,6 +901,14 @@ class PhotoTemplate:
|
||||
)
|
||||
elif field == "searchinfo.season":
|
||||
value = self.photo.search_info.season if self.photo.search_info else None
|
||||
elif field == "exif.camera_make":
|
||||
value = self.photo.exif_info.camera_make if self.photo.exif_info else None
|
||||
elif field == "exif.camera_model":
|
||||
value = self.photo.exif_info.camera_model if self.photo.exif_info else None
|
||||
elif field == "exif.lens_model":
|
||||
value = self.photo.exif_info.lens_model if self.photo.exif_info else None
|
||||
elif field == "uuid":
|
||||
value = self.photo.uuid
|
||||
else:
|
||||
# if here, didn't get a match
|
||||
raise ValueError(f"Unhandled template value: {field}")
|
||||
|
||||
2
setup.py
2
setup.py
@@ -51,7 +51,7 @@ with open(os.path.join(this_directory, "README.md"), encoding="utf-8") as f:
|
||||
setup(
|
||||
name="osxphotos",
|
||||
version=about["__version__"],
|
||||
description="Manipulate (read-only) Apple's Photos app library on Mac OS X",
|
||||
description="Export photos from Apple's macOS Photos app and query the Photos library database to access metadata about images.",
|
||||
long_description=about["long_description"],
|
||||
long_description_content_type="text/markdown",
|
||||
author="Rhet Turnbull",
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -2880,7 +2880,6 @@ def test_export_filename_template_1():
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
workdir = os.getcwd()
|
||||
files = glob.glob("*.*")
|
||||
assert sorted(files) == sorted(CLI_EXPORTED_FILENAME_TEMPLATE_FILENAMES1)
|
||||
|
||||
@@ -2915,6 +2914,37 @@ def test_export_filename_template_2():
|
||||
assert sorted(files) == sorted(CLI_EXPORTED_FILENAME_TEMPLATE_FILENAMES2)
|
||||
|
||||
|
||||
def test_export_filename_template_strip():
|
||||
""" export photos using filename template with --strip """
|
||||
import glob
|
||||
import locale
|
||||
import os
|
||||
import os.path
|
||||
import osxphotos
|
||||
from osxphotos.__main__ import export
|
||||
|
||||
locale.setlocale(locale.LC_ALL, "en_US")
|
||||
|
||||
runner = CliRunner()
|
||||
cwd = os.getcwd()
|
||||
# pylint: disable=not-context-manager
|
||||
with runner.isolated_filesystem():
|
||||
result = runner.invoke(
|
||||
export,
|
||||
[
|
||||
os.path.join(cwd, CLI_PHOTOS_DB),
|
||||
".",
|
||||
"-V",
|
||||
"--filename",
|
||||
"{searchinfo.venue,} {created.year}-{original_name}",
|
||||
"--strip",
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
files = glob.glob("*.*")
|
||||
assert sorted(files) == sorted(CLI_EXPORTED_FILENAME_TEMPLATE_FILENAMES1)
|
||||
|
||||
|
||||
def test_export_filename_template_pathsep_in_name_1():
|
||||
""" export photos using filename template with folder_album and "/" in album name """
|
||||
import locale
|
||||
|
||||
@@ -89,14 +89,15 @@ def test_image_converter_bad_file():
|
||||
""" Try to convert a file that's not an image """
|
||||
import pathlib
|
||||
import tempfile
|
||||
from osxphotos.imageconverter import ImageConverter
|
||||
from osxphotos.imageconverter import ImageConverter, ImageConversionError
|
||||
|
||||
converter = ImageConverter()
|
||||
tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
|
||||
with tempdir:
|
||||
imgfile = pathlib.Path(TEST_NOT_AN_IMAGE)
|
||||
outfile = pathlib.Path(tempdir.name) / f"{imgfile.stem}.jpeg"
|
||||
assert not converter.write_jpeg(imgfile, outfile)
|
||||
with pytest.raises(ImageConversionError):
|
||||
converter.write_jpeg(imgfile, outfile)
|
||||
|
||||
|
||||
def test_image_converter_missing_file():
|
||||
|
||||
@@ -136,6 +136,10 @@ TEMPLATE_VALUES = {
|
||||
"{place.address.postal_code}": "20009",
|
||||
"{place.address.country}": "United States",
|
||||
"{place.address.country_code}": "US",
|
||||
"{uuid}": "128FB4C6-0B16-4E7D-9108-FB2E90DA1546",
|
||||
"{exif.camera_make}": "Apple",
|
||||
"{exif.camera_model}": "iPhone 6s",
|
||||
"{exif.lens_model}": "iPhone 6s back camera 4.15mm f/2.2",
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user