Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dce002cdfe | ||
|
|
7bd189e9b2 | ||
|
|
baa86c77f6 | ||
|
|
0d086bf851 |
14
CHANGELOG.md
14
CHANGELOG.md
@@ -4,6 +4,20 @@ 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.38.16](https://github.com/RhetTbull/osxphotos/compare/v0.38.15...v0.38.16)
|
||||
|
||||
> 28 December 2020
|
||||
|
||||
- Added searchinfo templates, issue #302 [`0d086bf`](https://github.com/RhetTbull/osxphotos/commit/0d086bf85102ce78b3111c64bfa88673fbc19559)
|
||||
|
||||
#### [v0.38.15](https://github.com/RhetTbull/osxphotos/compare/v0.38.14...v0.38.15)
|
||||
|
||||
> 28 December 2020
|
||||
|
||||
- Added --sidecar exiftool, issue #303 [`d833c14`](https://github.com/RhetTbull/osxphotos/commit/d833c14ef4b3f9375a85034cf0fb0f85a68cabb4)
|
||||
- Refactored sidecar code [`ade98fc`](https://github.com/RhetTbull/osxphotos/commit/ade98fc15051684bfb54d0199d9c370481b70dcc)
|
||||
- Refactored export2 to use sidecar bit field [`0d66759`](https://github.com/RhetTbull/osxphotos/commit/0d66759b1c200f1ecda202e28c259f88fd3db599)
|
||||
|
||||
#### [v0.38.14](https://github.com/RhetTbull/osxphotos/compare/v0.38.13...v0.38.14)
|
||||
|
||||
> 27 December 2020
|
||||
|
||||
47
README.md
47
README.md
@@ -328,6 +328,18 @@ Options:
|
||||
photoname.ext.json; For a list of tags
|
||||
exported in the JSON and exiftool sidecar,
|
||||
see '--exiftool'.
|
||||
--sidecar-drop-ext Drop the photo's extension when naming
|
||||
sidecar files. By default, sidecar files are
|
||||
named in format
|
||||
'photo_filename.photo_ext.sidecar_ext', e.g.
|
||||
'IMG_1234.JPG.json'. Use '--sidecar-drop-
|
||||
ext' to ignore the photo extension.
|
||||
Resulting sidecar files will have name in
|
||||
format 'IMG_1234.json'. Warning: this may
|
||||
result in sidecar filename collisions if
|
||||
there are files of different types but the
|
||||
same name in the output directory, e.g.
|
||||
'IMG_1234.JPG' and 'IMG_1234.MOV'.
|
||||
--exiftool Use exiftool to write metadata directly to
|
||||
exported photos. To use this option,
|
||||
exiftool must be installed and in the path.
|
||||
@@ -726,6 +738,10 @@ Substitution Description
|
||||
'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).
|
||||
|
||||
The following substitutions may result in multiple values. Thus if specified
|
||||
for --directory these could result in multiple copies of a photo being being
|
||||
@@ -742,10 +758,10 @@ Substitution Description
|
||||
{keyword} Keyword(s) assigned to photo
|
||||
{person} Person(s) / face(s) in a photo
|
||||
{label} Image categorization label associated with a photo
|
||||
(Photos 5 only)
|
||||
{label_normalized} All lower case version of 'label' (Photos 5 only)
|
||||
(Photos 5+ only)
|
||||
{label_normalized} All lower case version of 'label' (Photos 5+ only)
|
||||
{comment} Comment(s) on shared Photos; format is 'Person
|
||||
name: comment text' (Photos 5 only)
|
||||
name: comment text' (Photos 5+ only)
|
||||
{exiftool:GROUP:TAGNAME} Use exiftool (https://exiftool.org) to extract
|
||||
metadata, in form GROUP:TAGNAME, from image. E.g.
|
||||
'{exiftool:EXIF:Make}' to get camera make, or
|
||||
@@ -754,6 +770,20 @@ Substitution Description
|
||||
tag names. You must specify group (e.g. EXIF,
|
||||
IPTC, etc) as used in `exiftool -G`. exiftool must
|
||||
be installed in the path to use this template.
|
||||
{searchinfo.holiday} Holiday names associated with a photo, e.g.
|
||||
'Christmas Day'; (Photos 5+ only, applied
|
||||
automatically by Photos' image categorization
|
||||
algorithms).
|
||||
{searchinfo.activity} Activities associated with a photo, e.g. 'Sporting
|
||||
Event'; (Photos 5+ only, applied automatically by
|
||||
Photos' image categorization algorithms).
|
||||
{searchinfo.venue} Venues associated with a photo, e.g. name of
|
||||
restaurant; (Photos 5+ only, applied automatically
|
||||
by Photos' image categorization algorithms).
|
||||
{searchinfo.venue_type} Venue types associated with a photo, e.g.
|
||||
'Restaurant'; (Photos 5+ only, applied
|
||||
automatically by Photos' image categorization
|
||||
algorithms).
|
||||
```
|
||||
|
||||
Example: export all photos to ~/Desktop/export group in folders by date created
|
||||
@@ -2226,14 +2256,19 @@ The following template field substitutions are availabe for use with `PhotoInfo.
|
||||
|{place.address.postal_code}|Postal code part of the postal address, e.g. '20009'|
|
||||
|{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).|
|
||||
|{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|
|
||||
|{person}|Person(s) / face(s) in a photo|
|
||||
|{label}|Image categorization label associated with a photo (Photos 5 only)|
|
||||
|{label_normalized}|All lower case version of 'label' (Photos 5 only)|
|
||||
|{comment}|Comment(s) on shared Photos; format is 'Person name: comment text' (Photos 5 only)|
|
||||
|{label}|Image categorization label associated with a photo (Photos 5+ only)|
|
||||
|{label_normalized}|All lower case version of 'label' (Photos 5+ only)|
|
||||
|{comment}|Comment(s) on shared Photos; format is 'Person name: comment text' (Photos 5+ only)|
|
||||
|{exiftool:GROUP:TAGNAME}|Use exiftool (https://exiftool.org) to extract metadata, in form GROUP:TAGNAME, from image. E.g. '{exiftool:EXIF:Make}' to get camera make, or {exiftool:IPTC:Keywords} to extract keywords. See https://exiftool.org/TagNames/ for list of valid tag names. You must specify group (e.g. EXIF, IPTC, etc) as used in `exiftool -G`. exiftool must be installed in the path to use this template.|
|
||||
|{searchinfo.holiday}|Holiday names associated with a photo, e.g. 'Christmas Day'; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).|
|
||||
|{searchinfo.activity}|Activities associated with a photo, e.g. 'Sporting Event'; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).|
|
||||
|{searchinfo.venue}|Venues associated with a photo, e.g. name of restaurant; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).|
|
||||
|{searchinfo.venue_type}|Venue types associated with a photo, e.g. 'Restaurant'; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).|
|
||||
|
||||
### Utility Functions
|
||||
|
||||
|
||||
@@ -1357,6 +1357,16 @@ def query(
|
||||
"Sidecar filename is in format photoname.ext.json; "
|
||||
"For a list of tags exported in the JSON and exiftool sidecar, see '--exiftool'.",
|
||||
)
|
||||
@click.option(
|
||||
"--sidecar-drop-ext",
|
||||
is_flag=True,
|
||||
help="Drop the photo's extension when naming sidecar files. "
|
||||
"By default, sidecar files are named in format 'photo_filename.photo_ext.sidecar_ext', "
|
||||
"e.g. 'IMG_1234.JPG.json'. Use '--sidecar-drop-ext' to ignore the photo extension. "
|
||||
"Resulting sidecar files will have name in format 'IMG_1234.json'. "
|
||||
"Warning: this may result in sidecar filename collisions if there are files of different "
|
||||
"types but the same name in the output directory, e.g. 'IMG_1234.JPG' and 'IMG_1234.MOV'.",
|
||||
)
|
||||
@click.option(
|
||||
"--exiftool",
|
||||
is_flag=True,
|
||||
@@ -1366,7 +1376,7 @@ def query(
|
||||
"Cannot be used with --export-as-hardlink. Writes the following metadata: "
|
||||
"EXIF:ImageDescription, XMP:Description (see also --description-template); "
|
||||
"XMP:Title; XMP:TagsList, IPTC:Keywords, XMP:Subject "
|
||||
"(see also --keyword-template, --person-keyword, --album-keyword); "
|
||||
"(see also --keyword-template, --person-keyword, --album-keyword); "
|
||||
"XMP:PersonInImage; EXIF:GPSLatitudeRef; EXIF:GPSLongitudeRef; EXIF:GPSLatitude; EXIF:GPSLongitude; "
|
||||
"EXIF:GPSPosition; EXIF:DateTimeOriginal; EXIF:OffsetTimeOriginal; "
|
||||
"EXIF:ModifyDate (see --ignore-date-modified); IPTC:DateCreated; IPTC:TimeCreated; "
|
||||
@@ -1571,6 +1581,7 @@ def export(
|
||||
convert_to_jpeg,
|
||||
jpeg_quality,
|
||||
sidecar,
|
||||
sidecar_drop_ext,
|
||||
only_photos,
|
||||
only_movies,
|
||||
burst,
|
||||
@@ -2036,6 +2047,7 @@ def export(
|
||||
verbose=verbose,
|
||||
export_by_date=export_by_date,
|
||||
sidecar=sidecar,
|
||||
sidecar_drop_ext=sidecar_drop_ext,
|
||||
update=update,
|
||||
ignore_signature=ignore_signature,
|
||||
export_as_hardlink=export_as_hardlink,
|
||||
@@ -2084,6 +2096,7 @@ def export(
|
||||
verbose=verbose,
|
||||
export_by_date=export_by_date,
|
||||
sidecar=sidecar,
|
||||
sidecar_drop_ext=sidecar_drop_ext,
|
||||
update=update,
|
||||
ignore_signature=ignore_signature,
|
||||
export_as_hardlink=export_as_hardlink,
|
||||
@@ -2616,6 +2629,7 @@ def export_photo(
|
||||
verbose=None,
|
||||
export_by_date=None,
|
||||
sidecar=None,
|
||||
sidecar_drop_ext=False,
|
||||
update=None,
|
||||
ignore_signature=None,
|
||||
export_as_hardlink=None,
|
||||
@@ -2654,6 +2668,7 @@ def export_photo(
|
||||
verbose: boolean; print verbose output
|
||||
export_by_date: boolean; create export folder in form dest/YYYY/MM/DD
|
||||
sidecar: list zero, 1 or 2 of ["json","xmp"] of sidecar variety to export
|
||||
sidecar_drop_ext: boolean; if True, drops photo extension from sidecar name
|
||||
export_as_hardlink: boolean; hardlink files instead of copying them
|
||||
overwrite: boolean; overwrite dest file if it already exists
|
||||
original_name: boolean; use original filename instead of current filename
|
||||
@@ -2804,6 +2819,7 @@ def export_photo(
|
||||
dest_path,
|
||||
original_filename,
|
||||
sidecar=sidecar_flags,
|
||||
sidecar_drop_ext=sidecar_drop_ext,
|
||||
live_photo=export_live,
|
||||
raw_photo=export_raw,
|
||||
export_as_hardlink=export_as_hardlink,
|
||||
@@ -2908,6 +2924,7 @@ def export_photo(
|
||||
dest_path,
|
||||
edited_filename,
|
||||
sidecar=sidecar_flags,
|
||||
sidecar_drop_ext=sidecar_drop_ext,
|
||||
export_as_hardlink=export_as_hardlink,
|
||||
overwrite=overwrite,
|
||||
edited=True,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
""" version info """
|
||||
|
||||
__version__ = "0.38.15"
|
||||
__version__ = "0.38.17"
|
||||
|
||||
|
||||
|
||||
@@ -430,6 +430,7 @@ def export2(
|
||||
overwrite=False,
|
||||
increment=True,
|
||||
sidecar=0,
|
||||
sidecar_drop_ext=False,
|
||||
use_photos_export=False,
|
||||
timeout=120,
|
||||
exiftool=False,
|
||||
@@ -475,6 +476,7 @@ def export2(
|
||||
sidecar filename will be dest/filename.json; does not include exiftool tag group names (e.g. `exiftool -j`)
|
||||
SIDECAR_XMP: if set will write an XMP sidecar with IPTC data
|
||||
sidecar filename will be dest/filename.xmp
|
||||
sidecar_drop_ext: (boolean, default=False); if True, drops the photo's extension from sidecar filename (e.g. 'IMG_1234.json' instead of 'IMG_1234.JPG.json')
|
||||
use_photos_export: (boolean, default=False); if True will attempt to export photo via applescript interaction with Photos
|
||||
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
|
||||
@@ -893,8 +895,9 @@ def export2(
|
||||
sidecar_xmp_files_skipped = []
|
||||
sidecar_xmp_files_written = []
|
||||
|
||||
dest_suffix = "" if sidecar_drop_ext else dest.suffix
|
||||
if sidecar & SIDECAR_JSON:
|
||||
sidecar_filename = dest.parent / pathlib.Path(f"{dest.stem}{dest.suffix}.json")
|
||||
sidecar_filename = dest.parent / pathlib.Path(f"{dest.stem}{dest_suffix}.json")
|
||||
sidecar_str = self._exiftool_json_sidecar(
|
||||
use_albums_as_keywords=use_albums_as_keywords,
|
||||
use_persons_as_keywords=use_persons_as_keywords,
|
||||
@@ -913,7 +916,7 @@ def export2(
|
||||
)
|
||||
|
||||
if sidecar & SIDECAR_EXIFTOOL:
|
||||
sidecar_filename = dest.parent / pathlib.Path(f"{dest.stem}{dest.suffix}.json")
|
||||
sidecar_filename = dest.parent / pathlib.Path(f"{dest.stem}{dest_suffix}.json")
|
||||
sidecar_str = self._exiftool_json_sidecar(
|
||||
use_albums_as_keywords=use_albums_as_keywords,
|
||||
use_persons_as_keywords=use_persons_as_keywords,
|
||||
@@ -933,7 +936,7 @@ def export2(
|
||||
)
|
||||
|
||||
if sidecar & SIDECAR_XMP:
|
||||
sidecar_filename = dest.parent / pathlib.Path(f"{dest.stem}{dest.suffix}.xmp")
|
||||
sidecar_filename = dest.parent / pathlib.Path(f"{dest.stem}{dest_suffix}.xmp")
|
||||
sidecar_str = self._xmp_sidecar(
|
||||
use_albums_as_keywords=use_albums_as_keywords,
|
||||
use_persons_as_keywords=use_persons_as_keywords,
|
||||
|
||||
@@ -117,6 +117,7 @@ TEMPLATE_SUBSTITUTIONS = {
|
||||
"{place.address.postal_code}": "Postal code part of the postal address, e.g. '20009'",
|
||||
"{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).",
|
||||
}
|
||||
|
||||
# Permitted multi-value substitutions (each of these returns None or 1 or more values)
|
||||
@@ -125,13 +126,17 @@ TEMPLATE_SUBSTITUTIONS_MULTI_VALUED = {
|
||||
"{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",
|
||||
"{person}": "Person(s) / face(s) in a photo",
|
||||
"{label}": "Image categorization label associated with a photo (Photos 5 only)",
|
||||
"{label_normalized}": "All lower case version of 'label' (Photos 5 only)",
|
||||
"{comment}": "Comment(s) on shared Photos; format is 'Person name: comment text' (Photos 5 only)",
|
||||
"{label}": "Image categorization label associated with a photo (Photos 5+ only)",
|
||||
"{label_normalized}": "All lower case version of 'label' (Photos 5+ only)",
|
||||
"{comment}": "Comment(s) on shared Photos; format is 'Person name: comment text' (Photos 5+ only)",
|
||||
"{exiftool:GROUP:TAGNAME}": "Use exiftool (https://exiftool.org) to extract metadata, in form GROUP:TAGNAME, from image. "
|
||||
"E.g. '{exiftool:EXIF:Make}' to get camera make, or {exiftool:IPTC:Keywords} to extract keywords. "
|
||||
"See https://exiftool.org/TagNames/ for list of valid tag names. You must specify group (e.g. EXIF, IPTC, etc) "
|
||||
"as used in `exiftool -G`. exiftool must be installed in the path to use this template.",
|
||||
"{searchinfo.holiday}": "Holiday names associated with a photo, e.g. 'Christmas Day'; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).",
|
||||
"{searchinfo.activity}": "Activities associated with a photo, e.g. 'Sporting Event'; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).",
|
||||
"{searchinfo.venue}": "Venues associated with a photo, e.g. name of restaurant; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).",
|
||||
"{searchinfo.venue_type}": "Venue types associated with a photo, e.g. 'Restaurant'; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).",
|
||||
}
|
||||
|
||||
# Just the multi-valued substitution names without the braces
|
||||
@@ -842,6 +847,8 @@ class PhotoTemplate:
|
||||
if self.photo.place and self.photo.place.address.iso_country_code
|
||||
else None
|
||||
)
|
||||
elif field == "searchinfo.season":
|
||||
value = self.photo.search_info.season if self.photo.search_info else None
|
||||
else:
|
||||
# if here, didn't get a match
|
||||
raise ValueError(f"Unhandled template value: {field}")
|
||||
@@ -914,6 +921,16 @@ class PhotoTemplate:
|
||||
values = [
|
||||
f"{comment.user}: {comment.text}" for comment in self.photo.comments
|
||||
]
|
||||
elif field == "searchinfo.holiday":
|
||||
values = self.photo.search_info.holidays if self.photo.search_info else []
|
||||
elif field == "searchinfo.activity":
|
||||
values = self.photo.search_info.activities if self.photo.search_info else []
|
||||
elif field == "searchinfo.venue":
|
||||
values = self.photo.search_info.venues if self.photo.search_info else []
|
||||
elif field == "searchinfo.venue_type":
|
||||
values = (
|
||||
self.photo.search_info.venue_types if self.photo.search_info else []
|
||||
)
|
||||
elif not field.startswith("exiftool:"):
|
||||
raise ValueError(f"Unhandled template value: {field}")
|
||||
|
||||
|
||||
@@ -306,6 +306,11 @@ CLI_EXPORT_BY_DATE_NEED_TOUCH_TIMES = [1538165227, 1539436692]
|
||||
CLI_EXPORT_BY_DATE = ["2018/09/28/Pumpkins3.jpg", "2018/09/28/Pumkins1.jpg"]
|
||||
|
||||
CLI_EXPORT_SIDECAR_FILENAMES = ["Pumkins2.jpg", "Pumkins2.jpg.json", "Pumkins2.jpg.xmp"]
|
||||
CLI_EXPORT_SIDECAR_DROP_EXT_FILENAMES = [
|
||||
"Pumkins2.jpg",
|
||||
"Pumkins2.json",
|
||||
"Pumkins2.xmp",
|
||||
]
|
||||
|
||||
CLI_EXPORT_LIVE = [
|
||||
"51F2BEF7-431A-4D31-8AC1-3284A57826AE.jpeg",
|
||||
@@ -1974,6 +1979,7 @@ def test_query_deleted_4():
|
||||
|
||||
|
||||
def test_export_sidecar():
|
||||
""" test --sidecar """
|
||||
import glob
|
||||
import os
|
||||
import os.path
|
||||
@@ -2003,6 +2009,38 @@ def test_export_sidecar():
|
||||
assert sorted(files) == sorted(CLI_EXPORT_SIDECAR_FILENAMES)
|
||||
|
||||
|
||||
def test_export_sidecar_drop_ext():
|
||||
""" test --sidecar with --sidecar-drop-ext option """
|
||||
import glob
|
||||
import os
|
||||
import os.path
|
||||
import osxphotos
|
||||
|
||||
from osxphotos.__main__ import cli
|
||||
|
||||
runner = CliRunner()
|
||||
cwd = os.getcwd()
|
||||
# pylint: disable=not-context-manager
|
||||
with runner.isolated_filesystem():
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[
|
||||
"export",
|
||||
"--db",
|
||||
os.path.join(cwd, CLI_PHOTOS_DB),
|
||||
".",
|
||||
"--sidecar=json",
|
||||
"--sidecar=xmp",
|
||||
"--sidecar-drop-ext",
|
||||
f"--uuid={CLI_EXPORT_UUID}",
|
||||
"-V",
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
files = glob.glob("*.*")
|
||||
assert sorted(files) == sorted(CLI_EXPORT_SIDECAR_DROP_EXT_FILENAMES)
|
||||
|
||||
|
||||
def test_export_sidecar_exiftool():
|
||||
""" test --sidecar exiftool """
|
||||
import glob
|
||||
|
||||
Reference in New Issue
Block a user