Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f57abd23c | ||
|
|
f9a43b92c1 | ||
|
|
bf2a55d7f6 | ||
|
|
34bb7f2cdc |
12
CHANGELOG.md
12
CHANGELOG.md
@@ -4,6 +4,18 @@ 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.2](https://github.com/RhetTbull/osxphotos/compare/v0.39.1...v0.39.2)
|
||||
|
||||
> 31 December 2020
|
||||
|
||||
- Added --xattr-template, closes #242 [`#242`](https://github.com/RhetTbull/osxphotos/issues/242)
|
||||
|
||||
#### [v0.39.1](https://github.com/RhetTbull/osxphotos/compare/v0.39.0...v0.39.1)
|
||||
|
||||
> 31 December 2020
|
||||
|
||||
- Fixed --exiftool-path bug, issue #311, #313 [`3394c52`](https://github.com/RhetTbull/osxphotos/commit/3394c527682d8fdd2f20f4f778d802dab86b6372)
|
||||
|
||||
#### [v0.39.0](https://github.com/RhetTbull/osxphotos/compare/v0.38.22...v0.39.0)
|
||||
|
||||
> 30 December 2020
|
||||
|
||||
145
README.md
145
README.md
@@ -412,20 +412,30 @@ Options:
|
||||
could specify --description-template
|
||||
"{descr} exported with osxphotos on
|
||||
{today.date}" See Templating System below.
|
||||
--finder-tag-template TEMPLATE Set Finder tags to TEMPLATE. These tags can
|
||||
be searched in the Finder or Spotlight with
|
||||
'tag:tagname' format. For example, '--
|
||||
finder-tag-template "{label}"' to set Finder
|
||||
tags to photo labels. You may specify
|
||||
multiple TEMPLATE values by using '--finder-
|
||||
tag-template' multiple times. See also '--
|
||||
finder-tag-keywords and Extended Attributes
|
||||
below.'.
|
||||
--finder-tag-keywords Set Finder tags to keywords; any keywords
|
||||
specified via '--keyword-template', '--
|
||||
person-keyword', etc. will also be used as
|
||||
Finder tags. See also '--finder-tag-template
|
||||
and Extended Attributes below.'.
|
||||
--finder-tag-template TEMPLATE Set MacOS Finder tags to TEMPLATE. These
|
||||
tags can be searched in the Finder or
|
||||
Spotlight with 'tag:tagname' format. For
|
||||
example, '--finder-tag-template "{label}"'
|
||||
to set Finder tags to photo labels. You may
|
||||
specify multiple TEMPLATE values by using '
|
||||
--finder-tag-template' multiple times. See
|
||||
also '--finder-tag-keywords and Extended
|
||||
Attributes below.'.
|
||||
--finder-tag-keywords Set MacOS Finder tags to keywords; any
|
||||
keywords specified via '--keyword-template',
|
||||
'--person-keyword', etc. will also be used
|
||||
as Finder tags. See also '--finder-tag-
|
||||
template and Extended Attributes below.'.
|
||||
--xattr-template ATTRIBUTE TEMPLATE
|
||||
Set extended attribute ATTRIBUTE to TEMPLATE
|
||||
value. Valid attributes are: 'authors',
|
||||
'comment', 'copyright', 'description',
|
||||
'findercomment', 'headline', 'keywords'. For
|
||||
example, to set Finder comment to the
|
||||
photo's title and description: '--xattr-
|
||||
template findercomment "{title}; {descr}"
|
||||
See Extended Attributes below for additional
|
||||
details on this option.
|
||||
--directory DIRECTORY Optional template for specifying name of
|
||||
output directory in the form
|
||||
'{name,DEFAULT}'. See below for additional
|
||||
@@ -530,21 +540,50 @@ option to re-export the entire library thus rebuilding the
|
||||
|
||||
** Extended Attributes **
|
||||
|
||||
Some options (currently '--finder-tag-template' and '--finder-tag-keywords')
|
||||
write additional metadata to extended attributes in the file. These options
|
||||
will only work if the destination filesystem supports extended attributes
|
||||
(most do). For example, --finder-tag-keyword writes all keywords (including
|
||||
any specified by '--keyword-template' or other options) to Finder tags that
|
||||
are searchable in Spotlight using the syntax: 'tag:tagname'. For example, if
|
||||
you have images with keyword "Travel" then using '--finder-tag-keywords' you
|
||||
could quickly find those images in the Finder by typing 'tag:Travel' in the
|
||||
Spotlight search bar. Finder tags are written to the
|
||||
Some options (currently '--finder-tag-template', '--finder-tag-keywords',
|
||||
'-xattr-template') write additional metadata to extended attributes in the
|
||||
file. These options will only work if the destination filesystem supports
|
||||
extended attributes (most do). For example, --finder-tag-keyword writes all
|
||||
keywords (including any specified by '--keyword-template' or other options) to
|
||||
Finder tags that are searchable in Spotlight using the syntax: 'tag:tagname'.
|
||||
For example, if you have images with keyword "Travel" then using '--finder-
|
||||
tag-keywords' you could quickly find those images in the Finder by typing
|
||||
'tag:Travel' in the Spotlight search bar. Finder tags are written to the
|
||||
'com.apple.metadata:_kMDItemUserTags' extended attribute. Unlike EXIF
|
||||
metadata, extended attributes do not modify the actual file. Most cloud
|
||||
storage services do not synch extended attributes. Dropbox does sync them and
|
||||
any changes to a file's extended attributes will cause Dropbox to re-sync the
|
||||
files.
|
||||
|
||||
The following attributes may be used with '--xattr-template':
|
||||
|
||||
|
||||
authors The author, or authors, of the contents of the file. A list
|
||||
of strings. (com.apple.metadata:kMDItemAuthors)
|
||||
comment A comment related to the file. This differs from the Finder
|
||||
comment, kMDItemFinderComment. A string.
|
||||
(com.apple.metadata:kMDItemComment)
|
||||
copyright The copyright owner of the file contents. A string.
|
||||
(com.apple.metadata:kMDItemCopyright)
|
||||
description A description of the content of the resource. The
|
||||
description may include an abstract, table of contents,
|
||||
reference to a graphical representation of content or a free-
|
||||
text account of the content. A string.
|
||||
(com.apple.metadata:kMDItemDescription)
|
||||
findercomment Finder comments for this file. A string.
|
||||
(com.apple.metadata:kMDItemFinderComment)
|
||||
headline A publishable entry providing a synopsis of the contents of
|
||||
the file. A string. (com.apple.metadata:kMDItemHeadline)
|
||||
keywords Keywords associated with this file. For example, “Birthday”,
|
||||
“Important”, etc. This differs from Finder tags
|
||||
(_kMDItemUserTags) which are keywords/tags shown in the
|
||||
Finder and searchable in Spotlight using "tag:tag_name". A
|
||||
list of strings. (com.apple.metadata:kMDItemKeywords)
|
||||
|
||||
For additional information on extended attributes see: https://developer.apple
|
||||
.com/documentation/coreservices/file_metadata/mditem/common_metadata_attribute
|
||||
_keys
|
||||
|
||||
|
||||
** Templating System **
|
||||
|
||||
@@ -701,27 +740,39 @@ Substitution Description
|
||||
https://strftime.org/ for help on strftime
|
||||
templates.
|
||||
{modified.date} Photo's modification date in ISO format,
|
||||
e.g. '2020-03-22'
|
||||
{modified.year} 4-digit year of photo modification time
|
||||
{modified.yy} 2-digit year of photo modification time
|
||||
e.g. '2020-03-22'; uses creation date if
|
||||
photo is not modified
|
||||
{modified.year} 4-digit year of photo modification time;
|
||||
uses creation date if photo is not modified
|
||||
{modified.yy} 2-digit year of photo modification time;
|
||||
uses creation date if photo is not modified
|
||||
{modified.mm} 2-digit month of the photo modification time
|
||||
(zero padded)
|
||||
(zero padded); uses creation date if photo
|
||||
is not modified
|
||||
{modified.month} Month name in user's locale of the photo
|
||||
modification time
|
||||
modification time; uses creation date if
|
||||
photo is not modified
|
||||
{modified.mon} Month abbreviation in the user's locale of
|
||||
the photo modification time
|
||||
the photo modification time; uses creation
|
||||
date if photo is not modified
|
||||
{modified.dd} 2-digit day of the month (zero padded) of
|
||||
the photo modification time
|
||||
the photo modification time; uses creation
|
||||
date if photo is not modified
|
||||
{modified.dow} Day of week in user's locale of the photo
|
||||
modification time
|
||||
modification time; uses creation date if
|
||||
photo is not modified
|
||||
{modified.doy} 3-digit day of year (e.g Julian day) of
|
||||
photo modification time, starting from 1
|
||||
(zero padded)
|
||||
{modified.hour} 2-digit hour of the photo modification time
|
||||
(zero padded); uses creation date if photo
|
||||
is not modified
|
||||
{modified.hour} 2-digit hour of the photo modification time;
|
||||
uses creation date if photo is not modified
|
||||
{modified.min} 2-digit minute of the photo modification
|
||||
time
|
||||
time; uses creation date if photo is not
|
||||
modified
|
||||
{modified.sec} 2-digit second of the photo modification
|
||||
time
|
||||
time; uses creation date if photo is not
|
||||
modified
|
||||
{today.date} Current date in iso format, e.g.
|
||||
'2020-03-22'
|
||||
{today.year} 4-digit year of current date
|
||||
@@ -2258,18 +2309,18 @@ The following template field substitutions are availabe for use with `PhotoInfo.
|
||||
|{created.min}|2-digit minute of the photo creation time|
|
||||
|{created.sec}|2-digit second of the photo creation time|
|
||||
|{created.strftime}|Apply strftime template to file creation date/time. Should be used in form {created.strftime,TEMPLATE} where TEMPLATE is a valid strftime template, e.g. {created.strftime,%Y-%U} would result in year-week number of year: '2020-23'. If used with no template will return null value. See https://strftime.org/ for help on strftime templates.|
|
||||
|{modified.date}|Photo's modification date in ISO format, e.g. '2020-03-22'|
|
||||
|{modified.year}|4-digit year of photo modification time|
|
||||
|{modified.yy}|2-digit year of photo modification time|
|
||||
|{modified.mm}|2-digit month of the photo modification time (zero padded)|
|
||||
|{modified.month}|Month name in user's locale of the photo modification time|
|
||||
|{modified.mon}|Month abbreviation in the user's locale of the photo modification time|
|
||||
|{modified.dd}|2-digit day of the month (zero padded) of the photo modification time|
|
||||
|{modified.dow}|Day of week in user's locale of the photo modification time|
|
||||
|{modified.doy}|3-digit day of year (e.g Julian day) of photo modification time, starting from 1 (zero padded)|
|
||||
|{modified.hour}|2-digit hour of the photo modification time|
|
||||
|{modified.min}|2-digit minute of the photo modification time|
|
||||
|{modified.sec}|2-digit second of the photo modification time|
|
||||
|{modified.date}|Photo's modification date in ISO format, e.g. '2020-03-22'; uses creation date if photo is not modified|
|
||||
|{modified.year}|4-digit year of photo modification time; uses creation date if photo is not modified|
|
||||
|{modified.yy}|2-digit year of photo modification time; uses creation date if photo is not modified|
|
||||
|{modified.mm}|2-digit month of the photo modification time (zero padded); uses creation date if photo is not modified|
|
||||
|{modified.month}|Month name in user's locale of the photo modification time; uses creation date if photo is not modified|
|
||||
|{modified.mon}|Month abbreviation in the user's locale of the photo modification time; uses creation date if photo is not modified|
|
||||
|{modified.dd}|2-digit day of the month (zero padded) of the photo modification time; uses creation date if photo is not modified|
|
||||
|{modified.dow}|Day of week in user's locale of the photo modification time; uses creation date if photo is not modified|
|
||||
|{modified.doy}|3-digit day of year (e.g Julian day) of photo modification time, starting from 1 (zero padded); uses creation date if photo is not modified|
|
||||
|{modified.hour}|2-digit hour of the photo modification time; uses creation date if photo is not modified|
|
||||
|{modified.min}|2-digit minute of the photo modification time; uses creation date if photo is not modified|
|
||||
|{modified.sec}|2-digit second of the photo modification time; uses creation date if photo is not modified|
|
||||
|{today.date}|Current date in iso format, e.g. '2020-03-22'|
|
||||
|{today.year}|4-digit year of current date|
|
||||
|{today.yy}|2-digit year of current date|
|
||||
|
||||
@@ -11,21 +11,23 @@ import time
|
||||
import unicodedata
|
||||
|
||||
import click
|
||||
import osxmetadata
|
||||
import yaml
|
||||
|
||||
import osxmetadata
|
||||
import osxphotos
|
||||
|
||||
from ._constants import (
|
||||
_EXIF_TOOL_URL,
|
||||
_OSXPHOTOS_NONE_SENTINEL,
|
||||
_PHOTOS_4_VERSION,
|
||||
_UNKNOWN_PLACE,
|
||||
_OSXPHOTOS_NONE_SENTINEL,
|
||||
CLI_COLOR_ERROR,
|
||||
CLI_COLOR_WARNING,
|
||||
DEFAULT_EDITED_SUFFIX,
|
||||
DEFAULT_JPEG_QUALITY,
|
||||
DEFAULT_ORIGINAL_SUFFIX,
|
||||
EXTENDED_ATTRIBUTE_NAMES,
|
||||
EXTENDED_ATTRIBUTE_NAMES_QUOTED,
|
||||
SIDECAR_EXIFTOOL,
|
||||
SIDECAR_JSON,
|
||||
SIDECAR_XMP,
|
||||
@@ -186,7 +188,7 @@ class ExportCommand(click.Command):
|
||||
formatter.write("\n")
|
||||
formatter.write_text(
|
||||
"""
|
||||
Some options (currently '--finder-tag-template' and '--finder-tag-keywords') write
|
||||
Some options (currently '--finder-tag-template', '--finder-tag-keywords', '-xattr-template') write
|
||||
additional metadata to extended attributes in the file. These options will only work
|
||||
if the destination filesystem supports extended attributes (most do).
|
||||
For example, --finder-tag-keyword writes all keywords (including any specified by '--keyword-template'
|
||||
@@ -197,9 +199,24 @@ Finder tags are written to the 'com.apple.metadata:_kMDItemUserTags' extended at
|
||||
Unlike EXIF metadata, extended attributes do not modify the actual file. Most cloud storage services
|
||||
do not synch extended attributes. Dropbox does sync them and any changes to a file's extended attributes
|
||||
will cause Dropbox to re-sync the files.
|
||||
|
||||
The following attributes may be used with '--xattr-template':
|
||||
|
||||
"""
|
||||
)
|
||||
|
||||
formatter.write_dl(
|
||||
[
|
||||
(
|
||||
attr,
|
||||
f"{osxmetadata.ATTRIBUTES[attr].help} ({osxmetadata.ATTRIBUTES[attr].constant})",
|
||||
)
|
||||
for attr in EXTENDED_ATTRIBUTE_NAMES
|
||||
]
|
||||
)
|
||||
formatter.write("\n")
|
||||
formatter.write_text(
|
||||
"For additional information on extended attributes see: https://developer.apple.com/documentation/coreservices/file_metadata/mditem/common_metadata_attribute_keys"
|
||||
)
|
||||
formatter.write("\n\n")
|
||||
formatter.write_text("** Templating System **")
|
||||
formatter.write("\n")
|
||||
@@ -1490,6 +1507,17 @@ def query(
|
||||
help="Set MacOS Finder tags to keywords; any keywords specified via '--keyword-template', '--person-keyword', etc. "
|
||||
"will also be used as Finder tags. See also '--finder-tag-template and Extended Attributes below.'.",
|
||||
)
|
||||
@click.option(
|
||||
"--xattr-template",
|
||||
nargs=2,
|
||||
metavar="ATTRIBUTE TEMPLATE",
|
||||
multiple=True,
|
||||
help="Set extended attribute ATTRIBUTE to TEMPLATE value. Valid attributes are: "
|
||||
f"{', '.join(EXTENDED_ATTRIBUTE_NAMES_QUOTED)}. "
|
||||
"For example, to set Finder comment to the photo's title and description: "
|
||||
"'--xattr-template findercomment \"{title}; {descr}\" "
|
||||
"See Extended Attributes below for additional details on this option.",
|
||||
)
|
||||
@click.option(
|
||||
"--directory",
|
||||
metavar="DIRECTORY",
|
||||
@@ -1632,6 +1660,7 @@ def export(
|
||||
description_template,
|
||||
finder_tag_template,
|
||||
finder_tag_keywords,
|
||||
xattr_template,
|
||||
current_name,
|
||||
convert_to_jpeg,
|
||||
jpeg_quality,
|
||||
@@ -1770,6 +1799,7 @@ def export(
|
||||
description_template = cfg.description_template
|
||||
finder_tag_template = cfg.finder_tag_template
|
||||
finder_tag_keywords = cfg.finder_tag_keywords
|
||||
xattr_template = cfg.xattr_template
|
||||
current_name = cfg.current_name
|
||||
convert_to_jpeg = cfg.convert_to_jpeg
|
||||
jpeg_quality = cfg.jpeg_quality
|
||||
@@ -1884,6 +1914,19 @@ def export(
|
||||
)
|
||||
raise click.Abort()
|
||||
|
||||
if xattr_template:
|
||||
for attr, _ in xattr_template:
|
||||
if attr not in EXTENDED_ATTRIBUTE_NAMES:
|
||||
click.echo(
|
||||
click.style(
|
||||
f"Invalid attribute '{attr}' for --xattr-template; "
|
||||
f"valid values are {', '.join(EXTENDED_ATTRIBUTE_NAMES_QUOTED)}",
|
||||
fg=CLI_COLOR_ERROR,
|
||||
),
|
||||
err=True,
|
||||
)
|
||||
raise click.Abort()
|
||||
|
||||
if save_config:
|
||||
verbose_(f"Saving options to file {save_config}")
|
||||
cfg.write_to_file(save_config)
|
||||
@@ -2160,18 +2203,21 @@ def export(
|
||||
)
|
||||
results += export_results
|
||||
|
||||
# all photo files (not including sidecars) that are part of this export set
|
||||
# used below for applying Finder tags, etc.
|
||||
photo_files = set(
|
||||
export_results.exported
|
||||
+ export_results.new
|
||||
+ export_results.updated
|
||||
+ export_results.exif_updated
|
||||
+ export_results.converted_to_jpeg
|
||||
+ export_results.skipped
|
||||
)
|
||||
|
||||
if finder_tag_keywords or finder_tag_template:
|
||||
files = set(
|
||||
export_results.exported
|
||||
+ export_results.new
|
||||
+ export_results.updated
|
||||
+ export_results.exif_updated
|
||||
+ export_results.converted_to_jpeg
|
||||
+ export_results.skipped
|
||||
)
|
||||
tags_written, tags_skipped = write_finder_tags(
|
||||
p,
|
||||
files,
|
||||
photo_files,
|
||||
keywords=finder_tag_keywords,
|
||||
keyword_template=keyword_template,
|
||||
album_keyword=album_keyword,
|
||||
@@ -2182,6 +2228,13 @@ def export(
|
||||
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
|
||||
)
|
||||
results.xattr_written.extend(xattr_written)
|
||||
results.xattr_skipped.extend(xattr_skipped)
|
||||
|
||||
if fp is not None:
|
||||
fp.close()
|
||||
|
||||
@@ -3444,5 +3497,64 @@ def write_finder_tags(
|
||||
return (written, skipped)
|
||||
|
||||
|
||||
def write_extended_attributes(photo, files, xattr_template):
|
||||
""" Writes extended attributes to exported files
|
||||
|
||||
Args:
|
||||
photo: a PhotoInfo object
|
||||
xattr_template: list of tuples: (attribute name, attribute template)
|
||||
|
||||
Returns:
|
||||
tuple(list of file paths that were updated with new attributes, list of file paths skipped because attributes didn't need updating)
|
||||
"""
|
||||
|
||||
attributes = {}
|
||||
for xattr, template_str in xattr_template:
|
||||
rendered, unmatched = photo.render_template(
|
||||
template_str, none_str=_OSXPHOTOS_NONE_SENTINEL, path_sep="/"
|
||||
)
|
||||
if unmatched:
|
||||
click.echo(
|
||||
click.style(
|
||||
f"Warning: unmatched template substitution for template: {template_str} {unmatched}",
|
||||
fg=CLI_COLOR_WARNING,
|
||||
),
|
||||
err=True,
|
||||
)
|
||||
# filter out any template values that didn't match by looking for sentinel
|
||||
rendered = [
|
||||
value for value in rendered if _OSXPHOTOS_NONE_SENTINEL not in value
|
||||
]
|
||||
try:
|
||||
attributes[xattr].extend(rendered)
|
||||
except KeyError:
|
||||
attributes[xattr] = rendered
|
||||
|
||||
written = set()
|
||||
skipped = set()
|
||||
for f in files:
|
||||
md = osxmetadata.OSXMetaData(f)
|
||||
for attr, value in attributes.items():
|
||||
islist = osxmetadata.ATTRIBUTES[attr].list
|
||||
if value:
|
||||
value = ", ".join(value) if not islist else sorted(value)
|
||||
file_value = md.get_attribute(attr)
|
||||
|
||||
if file_value and islist:
|
||||
file_value = sorted(file_value)
|
||||
|
||||
if (not file_value and not value) or file_value == value:
|
||||
# if both not set or both equal, nothing to do
|
||||
# get_attribute returns None if not set and value will be [] if not set so can't directly compare
|
||||
verbose_(f"Skipping extended attribute {attr} for {f}: nothing to do")
|
||||
skipped.add(f)
|
||||
else:
|
||||
verbose_(f"Writing extended attribute {attr} to {f}")
|
||||
md.set_attribute(attr, value)
|
||||
written.add(f)
|
||||
|
||||
return list(written), [f for f in skipped if f not in written]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cli() # pylint: disable=no-value-for-parameter
|
||||
|
||||
@@ -108,7 +108,7 @@ SEARCH_CATEGORY_NEIGHBORHOOD = 3
|
||||
SEARCH_CATEGORY_LOCALITY_4 = 4
|
||||
SEARCH_CATEGORY_SUB_LOCALITY_5 = 5
|
||||
SEARCH_CATEGORY_SUB_LOCALITY_6 = 6
|
||||
SEARCH_CATEGORY_CITY = 7
|
||||
SEARCH_CATEGORY_CITY = 7
|
||||
SEARCH_CATEGORY_LOCALITY_8 = 8
|
||||
SEARCH_CATEGORY_NAMED_AREA = 9
|
||||
SEARCH_CATEGORY_ALL_LOCALITY = [
|
||||
@@ -182,4 +182,16 @@ CLI_COLOR_WARNING = "yellow"
|
||||
# Bit masks for --sidecar
|
||||
SIDECAR_JSON = 0x1
|
||||
SIDECAR_EXIFTOOL = 0x2
|
||||
SIDECAR_XMP = 0x4
|
||||
SIDECAR_XMP = 0x4
|
||||
|
||||
# supported attributes for --xattr-template
|
||||
EXTENDED_ATTRIBUTE_NAMES = [
|
||||
"authors",
|
||||
"comment",
|
||||
"copyright",
|
||||
"description",
|
||||
"findercomment",
|
||||
"headline",
|
||||
"keywords",
|
||||
]
|
||||
EXTENDED_ATTRIBUTE_NAMES_QUOTED = [f"'{x}'" for x in EXTENDED_ATTRIBUTE_NAMES]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
""" version info """
|
||||
|
||||
__version__ = "0.39.1"
|
||||
__version__ = "0.39.3"
|
||||
|
||||
|
||||
|
||||
@@ -70,18 +70,18 @@ TEMPLATE_SUBSTITUTIONS = {
|
||||
+ "{created.strftime,%Y-%U} would result in year-week number of year: '2020-23'. "
|
||||
+ "If used with no template will return null value. "
|
||||
+ "See https://strftime.org/ for help on strftime templates.",
|
||||
"{modified.date}": "Photo's modification date in ISO format, e.g. '2020-03-22'",
|
||||
"{modified.year}": "4-digit year of photo modification time",
|
||||
"{modified.yy}": "2-digit year of photo modification time",
|
||||
"{modified.mm}": "2-digit month of the photo modification time (zero padded)",
|
||||
"{modified.month}": "Month name in user's locale of the photo modification time",
|
||||
"{modified.mon}": "Month abbreviation in the user's locale of the photo modification time",
|
||||
"{modified.dd}": "2-digit day of the month (zero padded) of the photo modification time",
|
||||
"{modified.dow}": "Day of week in user's locale of the photo modification time",
|
||||
"{modified.doy}": "3-digit day of year (e.g Julian day) of photo modification time, starting from 1 (zero padded)",
|
||||
"{modified.hour}": "2-digit hour of the photo modification time",
|
||||
"{modified.min}": "2-digit minute of the photo modification time",
|
||||
"{modified.sec}": "2-digit second of the photo modification time",
|
||||
"{modified.date}": "Photo's modification date in ISO format, e.g. '2020-03-22'; uses creation date if photo is not modified",
|
||||
"{modified.year}": "4-digit year of photo modification time; uses creation date if photo is not modified",
|
||||
"{modified.yy}": "2-digit year of photo modification time; uses creation date if photo is not modified",
|
||||
"{modified.mm}": "2-digit month of the photo modification time (zero padded); uses creation date if photo is not modified",
|
||||
"{modified.month}": "Month name in user's locale of the photo modification time; uses creation date if photo is not modified",
|
||||
"{modified.mon}": "Month abbreviation in the user's locale of the photo modification time; uses creation date if photo is not modified",
|
||||
"{modified.dd}": "2-digit day of the month (zero padded) of the photo modification time; uses creation date if photo is not modified",
|
||||
"{modified.dow}": "Day of week in user's locale of the photo modification time; uses creation date if photo is not modified",
|
||||
"{modified.doy}": "3-digit day of year (e.g Julian day) of photo modification time, starting from 1 (zero padded); uses creation date if photo is not modified",
|
||||
"{modified.hour}": "2-digit hour of the photo modification time; uses creation date if photo is not modified",
|
||||
"{modified.min}": "2-digit minute of the photo modification time; uses creation date if photo is not modified",
|
||||
"{modified.sec}": "2-digit second of the photo modification time; uses creation date if photo is not modified",
|
||||
# "{modified.strftime}": "Apply strftime template to file modification date/time. Should be used in form "
|
||||
# + "{modified.strftime,TEMPLATE} where TEMPLATE is a valid strftime template, e.g. "
|
||||
# + "{modified.strftime,%Y-%U} would result in year-week number of year: '2020-23'. "
|
||||
@@ -679,73 +679,73 @@ class PhotoTemplate:
|
||||
value = (
|
||||
DateTimeFormatter(self.photo.date_modified).date
|
||||
if self.photo.date_modified
|
||||
else None
|
||||
else DateTimeFormatter(self.photo.date).date
|
||||
)
|
||||
elif field == "modified.year":
|
||||
value = (
|
||||
DateTimeFormatter(self.photo.date_modified).year
|
||||
if self.photo.date_modified
|
||||
else None
|
||||
else DateTimeFormatter(self.photo.date).year
|
||||
)
|
||||
elif field == "modified.yy":
|
||||
value = (
|
||||
DateTimeFormatter(self.photo.date_modified).yy
|
||||
if self.photo.date_modified
|
||||
else None
|
||||
else DateTimeFormatter(self.photo.date).yy
|
||||
)
|
||||
elif field == "modified.mm":
|
||||
value = (
|
||||
DateTimeFormatter(self.photo.date_modified).mm
|
||||
if self.photo.date_modified
|
||||
else None
|
||||
else DateTimeFormatter(self.photo.date).mm
|
||||
)
|
||||
elif field == "modified.month":
|
||||
value = (
|
||||
DateTimeFormatter(self.photo.date_modified).month
|
||||
if self.photo.date_modified
|
||||
else None
|
||||
else DateTimeFormatter(self.photo.date).month
|
||||
)
|
||||
elif field == "modified.mon":
|
||||
value = (
|
||||
DateTimeFormatter(self.photo.date_modified).mon
|
||||
if self.photo.date_modified
|
||||
else None
|
||||
else DateTimeFormatter(self.photo.date).mon
|
||||
)
|
||||
elif field == "modified.dd":
|
||||
value = (
|
||||
DateTimeFormatter(self.photo.date_modified).dd
|
||||
if self.photo.date_modified
|
||||
else None
|
||||
else DateTimeFormatter(self.photo.date).dd
|
||||
)
|
||||
elif field == "modified.dow":
|
||||
value = (
|
||||
DateTimeFormatter(self.photo.date_modified).dow
|
||||
if self.photo.date_modified
|
||||
else None
|
||||
else DateTimeFormatter(self.photo.date).dow
|
||||
)
|
||||
elif field == "modified.doy":
|
||||
value = (
|
||||
DateTimeFormatter(self.photo.date_modified).doy
|
||||
if self.photo.date_modified
|
||||
else None
|
||||
else DateTimeFormatter(self.photo.date).doy
|
||||
)
|
||||
elif field == "modified.hour":
|
||||
value = (
|
||||
DateTimeFormatter(self.photo.date_modified).hour
|
||||
if self.photo.date_modified
|
||||
else None
|
||||
else DateTimeFormatter(self.photo.date).hour
|
||||
)
|
||||
elif field == "modified.min":
|
||||
value = (
|
||||
DateTimeFormatter(self.photo.date_modified).min
|
||||
if self.photo.date_modified
|
||||
else None
|
||||
else DateTimeFormatter(self.photo.date).min
|
||||
)
|
||||
elif field == "modified.sec":
|
||||
value = (
|
||||
DateTimeFormatter(self.photo.date_modified).sec
|
||||
if self.photo.date_modified
|
||||
else None
|
||||
else DateTimeFormatter(self.photo.date).sec
|
||||
)
|
||||
elif field == "today.date":
|
||||
value = DateTimeFormatter(self.today).date
|
||||
|
||||
2
setup.py
2
setup.py
@@ -81,7 +81,7 @@ setup(
|
||||
"wurlitzer>=2.0.1",
|
||||
"photoscript>=0.1.0",
|
||||
"toml>=0.10.0",
|
||||
"osxmetadata>=0.99.11",
|
||||
"osxmetadata>=0.99.13",
|
||||
],
|
||||
entry_points={"console_scripts": ["osxphotos=osxphotos.__main__:cli"]},
|
||||
include_package_data=True,
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -5115,3 +5115,96 @@ def test_export_finder_tag_template_keywords():
|
||||
persons = [persons] if type(persons) != list else persons
|
||||
expected = [Tag(x) for x in keywords + persons]
|
||||
assert sorted(md.tags) == sorted(expected)
|
||||
|
||||
|
||||
def test_export_xattr_template():
|
||||
""" test --xattr template """
|
||||
import glob
|
||||
import os
|
||||
import os.path
|
||||
|
||||
from osxmetadata import OSXMetaData
|
||||
from osxphotos.__main__ import export
|
||||
|
||||
runner = CliRunner()
|
||||
cwd = os.getcwd()
|
||||
# pylint: disable=not-context-manager
|
||||
with runner.isolated_filesystem():
|
||||
for uuid in CLI_FINDER_TAGS:
|
||||
result = runner.invoke(
|
||||
export,
|
||||
[
|
||||
os.path.join(cwd, PHOTOS_DB_15_7),
|
||||
".",
|
||||
"-V",
|
||||
"--xattr-template",
|
||||
"keywords",
|
||||
"{person}",
|
||||
"--xattr-template",
|
||||
"comment",
|
||||
"{title}",
|
||||
"--uuid",
|
||||
f"{uuid}",
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
|
||||
md = OSXMetaData(CLI_FINDER_TAGS[uuid]["File:FileName"])
|
||||
expected = CLI_FINDER_TAGS[uuid]["XMP:PersonInImage"]
|
||||
expected = [expected] if type(expected) != list else expected
|
||||
assert sorted(md.keywords) == sorted(expected)
|
||||
assert md.comment == CLI_FINDER_TAGS[uuid]["XMP:Title"]
|
||||
|
||||
# run again with --update, should skip writing extended attributes
|
||||
result = runner.invoke(
|
||||
export,
|
||||
[
|
||||
os.path.join(cwd, PHOTOS_DB_15_7),
|
||||
".",
|
||||
"-V",
|
||||
"--xattr-template",
|
||||
"keywords",
|
||||
"{person}",
|
||||
"--xattr-template",
|
||||
"comment",
|
||||
"{title}",
|
||||
"--uuid",
|
||||
f"{uuid}",
|
||||
"--update",
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert "Skipping extended attribute keywords" in result.output
|
||||
assert "Skipping extended attribute comment" in result.output
|
||||
|
||||
# clear tags and run again, should update extended attributes
|
||||
md.keywords = None
|
||||
md.comment = None
|
||||
|
||||
result = runner.invoke(
|
||||
export,
|
||||
[
|
||||
os.path.join(cwd, PHOTOS_DB_15_7),
|
||||
".",
|
||||
"-V",
|
||||
"--xattr-template",
|
||||
"keywords",
|
||||
"{person}",
|
||||
"--xattr-template",
|
||||
"comment",
|
||||
"{title}",
|
||||
"--uuid",
|
||||
f"{uuid}",
|
||||
"--update",
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert "Writing extended attribute keyword" in result.output
|
||||
assert "Writing extended attribute comment" in result.output
|
||||
|
||||
md = OSXMetaData(CLI_FINDER_TAGS[uuid]["File:FileName"])
|
||||
expected = CLI_FINDER_TAGS[uuid]["XMP:PersonInImage"]
|
||||
expected = [expected] if type(expected) != list else expected
|
||||
assert sorted(md.keywords) == sorted(expected)
|
||||
assert md.comment == CLI_FINDER_TAGS[uuid]["XMP:Title"]
|
||||
|
||||
|
||||
@@ -170,17 +170,21 @@ TEMPLATE_VALUES_DATE_MODIFIED = {
|
||||
}
|
||||
|
||||
TEMPLATE_VALUES_DATE_NOT_MODIFIED = {
|
||||
# uses creation date instead of modified date
|
||||
"{name}": "128FB4C6-0B16-4E7D-9108-FB2E90DA1546",
|
||||
"{original_name}": "IMG_1064",
|
||||
"{modified.date}": "_",
|
||||
"{modified.year}": "_",
|
||||
"{modified.yy}": "_",
|
||||
"{modified.mm}": "_",
|
||||
"{modified.month}": "_",
|
||||
"{modified.mon}": "_",
|
||||
"{modified.dd}": "_",
|
||||
"{modified.doy}": "_",
|
||||
"{modified.dow}": "_",
|
||||
"{modified.date}": "2020-02-04",
|
||||
"{modified.year}": "2020",
|
||||
"{modified.yy}": "20",
|
||||
"{modified.mm}": "02",
|
||||
"{modified.month}": "February",
|
||||
"{modified.mon}": "Feb",
|
||||
"{modified.dd}": "04",
|
||||
"{modified.dow}": "Tuesday",
|
||||
"{modified.doy}": "035",
|
||||
"{modified.hour}": "19",
|
||||
"{modified.min}": "07",
|
||||
"{modified.sec}": "38",
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user