Added --post-command, implements #443
This commit is contained in:
129
README.md
129
README.md
@@ -482,6 +482,40 @@ Then the next to you run osxphotos, you can simply do this:
|
|||||||
|
|
||||||
The configuration file is a plain text file in [TOML](https://toml.io/en/) format so the `.toml` extension is standard but you can name the file anything you like.
|
The configuration file is a plain text file in [TOML](https://toml.io/en/) format so the `.toml` extension is standard but you can name the file anything you like.
|
||||||
|
|
||||||
|
#### Run commands on exported photos for post-processing
|
||||||
|
|
||||||
|
You can use the `--post-command` option to run one or more commands against exported files. The `--post-command` option takes two arguments: CATEGORY and COMMAND. CATEGORY is a string that describes which category of file to run the command against. The available categories are described in the help text available via: `osxphotos help export`. For example, the `exported` category includes all exported photos and the `skipped` category includes all photos that were skipped when running export with `--update`. COMMAND is an osxphotos template string which will be rendered then passed to the shell for execution.
|
||||||
|
|
||||||
|
For example, the following command generates a log of all exported files and their associated keywords:
|
||||||
|
|
||||||
|
`osxphotos export /path/to/export --post-command exported "echo {shell_quote,{filepath}{comma}{,+keyword,}} >> {shell_quote,{export_dir}/exported.txt}"`
|
||||||
|
|
||||||
|
The special template field `{shell_quote}` ensures a string is properly quoted for execution in the shell. For example, it's possible that a file path or keyword in this example has a space in the value and if not properly quoted, this would cause an error in the execution of the command. When running commands, the template `{filepath}` is set to the full path of the exported file and `{export_dir}` is set to the full path of the base export directory.
|
||||||
|
|
||||||
|
Explanation of the template string:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
{shell_quote,{filepath}{comma}{,+keyword,}}
|
||||||
|
│ │ │ │ │
|
||||||
|
│ │ │ | │
|
||||||
|
└──> quote everything after comma for proper execution in the shell
|
||||||
|
│ │ │ │
|
||||||
|
└───> filepath of the exported file
|
||||||
|
│ │ │
|
||||||
|
└───> insert a comma
|
||||||
|
│ │
|
||||||
|
└───> join the list of keywords together with a ","
|
||||||
|
│
|
||||||
|
└───> if no keywords, insert nothing (empty string: "")
|
||||||
|
```
|
||||||
|
|
||||||
|
Another example: if you had `exiftool` installed and wanted to wipe all metadata from all exported files, you could use the following:
|
||||||
|
|
||||||
|
`osxphotos export /path/to/export --post-command exported "/usr/local/bin/exiftool -all= {filepath|shell_quote}"`
|
||||||
|
|
||||||
|
This command uses the `|shell_quote` template filter instead of the `{shell_quote}` template because the only thing that needs to be quoted is the path to the exported file. Template filters filter the value of the rendered template field. A number of other filters are available and are described in the help text.
|
||||||
|
|
||||||
|
|
||||||
#### An example from an actual osxphotos user
|
#### An example from an actual osxphotos user
|
||||||
|
|
||||||
Here's a comprehensive use case from an actual osxphotos user that integrates many of the concepts discussed in this tutorial (thank-you Philippe for contributing this!):
|
Here's a comprehensive use case from an actual osxphotos user that integrates many of the concepts discussed in this tutorial (thank-you Philippe for contributing this!):
|
||||||
@@ -1009,6 +1043,24 @@ Options:
|
|||||||
feature is currently experimental. I don't
|
feature is currently experimental. I don't
|
||||||
know how well it will work on large export
|
know how well it will work on large export
|
||||||
sets.
|
sets.
|
||||||
|
--post-command CATEGORY COMMAND
|
||||||
|
Run COMMAND on exported files of category
|
||||||
|
CATEGORY. CATEGORY can be one of: exported,
|
||||||
|
new, updated, skipped, missing, exif_updated,
|
||||||
|
touched, converted_to_jpeg,
|
||||||
|
sidecar_json_written, sidecar_json_skipped,
|
||||||
|
sidecar_exiftool_written,
|
||||||
|
sidecar_exiftool_skipped, sidecar_xmp_written,
|
||||||
|
sidecar_xmp_skipped, error. COMMAND is an
|
||||||
|
osxphotos template string, for example: '--
|
||||||
|
post-command exported "echo
|
||||||
|
{filepath|shell_quote} >>
|
||||||
|
{export_dir}/exported.txt"', which appends the
|
||||||
|
full path of all exported files to the file
|
||||||
|
'exported.txt'. You can run more than one
|
||||||
|
command by repeating the '--post-command'
|
||||||
|
option with different arguments. See Post
|
||||||
|
Command below.
|
||||||
--exportdb EXPORTDB_FILE Specify alternate name for database file which
|
--exportdb EXPORTDB_FILE Specify alternate name for database file which
|
||||||
stores state information for export and
|
stores state information for export and
|
||||||
--update. If --exportdb is not specified,
|
--update. If --exportdb is not specified,
|
||||||
@@ -1166,6 +1218,8 @@ Valid filters are:
|
|||||||
• braces: Enclose value in curly braces, e.g. 'value => '{value}'.
|
• braces: Enclose value in curly braces, e.g. 'value => '{value}'.
|
||||||
• parens: Enclose value in parentheses, e.g. 'value' => '(value')
|
• parens: Enclose value in parentheses, e.g. 'value' => '(value')
|
||||||
• brackets: Enclose value in brackets, e.g. 'value' => '[value]'
|
• brackets: Enclose value in brackets, e.g. 'value' => '[value]'
|
||||||
|
• shell_quote: Quotes the value for safe usage in the shell, e.g. My file.jpeg
|
||||||
|
=> 'My file.jpeg'; only adds quotes if needed.
|
||||||
• function: Run custom python function to filter value; use in format
|
• function: Run custom python function to filter value; use in format
|
||||||
'function:/path/to/file.py::function_name'. See example at https://github.com
|
'function:/path/to/file.py::function_name'. See example at https://github.com
|
||||||
/RhetTbull/osxphotos/blob/master/examples/template_filter.py
|
/RhetTbull/osxphotos/blob/master/examples/template_filter.py
|
||||||
@@ -1505,7 +1559,7 @@ Substitution Description
|
|||||||
{lf} A line feed: '\n', alias for {newline}
|
{lf} A line feed: '\n', alias for {newline}
|
||||||
{cr} A carriage return: '\r'
|
{cr} A carriage return: '\r'
|
||||||
{crlf} a carriage return + line feed: '\r\n'
|
{crlf} a carriage return + line feed: '\r\n'
|
||||||
{osxphotos_version} The osxphotos version, e.g. '0.42.34'
|
{osxphotos_version} The osxphotos version, e.g. '0.42.35'
|
||||||
{osxphotos_cmd_line} The full command line used to run osxphotos
|
{osxphotos_cmd_line} The full command line used to run osxphotos
|
||||||
|
|
||||||
The following substitutions may result in multiple values. Thus if specified for
|
The following substitutions may result in multiple values. Thus if specified for
|
||||||
@@ -1564,6 +1618,10 @@ Substitution Description
|
|||||||
underlying PhotoInfo class. See
|
underlying PhotoInfo class. See
|
||||||
https://rhettbull.github.io/osxphotos/ for additional
|
https://rhettbull.github.io/osxphotos/ for additional
|
||||||
documentation on the PhotoInfo class.
|
documentation on the PhotoInfo class.
|
||||||
|
{shell_quote} Use in form '{shell_quote,TEMPLATE}'; quotes the
|
||||||
|
rendered TEMPLATE value(s) for safe usage in the
|
||||||
|
shell, e.g. My file.jpeg => 'My file.jpeg'; only adds
|
||||||
|
quotes if needed.
|
||||||
{function} Execute a python function from an external file and
|
{function} Execute a python function from an external file and
|
||||||
use return value as template substitution. Use in
|
use return value as template substitution. Use in
|
||||||
format: {function:file.py::function_name} where
|
format: {function:file.py::function_name} where
|
||||||
@@ -1596,6 +1654,71 @@ Substitution Description
|
|||||||
{filepath} The full path to the exported file
|
{filepath} The full path to the exported file
|
||||||
|
|
||||||
|
|
||||||
|
** Post Command **
|
||||||
|
You can run commands on the exported photos for post-processing using the '--
|
||||||
|
post-command' option. '--post-command' is passed a CATEGORY and a COMMAND.
|
||||||
|
COMMAND is an osxphotos template string which will be rendered and passed to the
|
||||||
|
shell for execution. CATEGORY is the category of file to pass to COMMAND. The
|
||||||
|
following categories are available:
|
||||||
|
|
||||||
|
Catgory Description
|
||||||
|
exported All exported files
|
||||||
|
new When used with '--update', all newly exported files
|
||||||
|
updated When used with '--update', all files which were
|
||||||
|
previously exported but updated this time
|
||||||
|
skipped When used with '--update', all files which were
|
||||||
|
skipped (because they were previously exported and
|
||||||
|
didn't change)
|
||||||
|
missing All files which were not exported because they were
|
||||||
|
missing from the Photos library
|
||||||
|
exif_updated When used with '--exiftool', all files on which
|
||||||
|
exiftool updated the metadata
|
||||||
|
touched When used with '--touch-file', all files where the
|
||||||
|
date was touched
|
||||||
|
converted_to_jpeg When used with '--convert-to-jpeg', all files which
|
||||||
|
were converted to jpeg
|
||||||
|
sidecar_json_written When used with '--sidecar json', all JSON sidecar
|
||||||
|
files which were written
|
||||||
|
sidecar_json_skipped When used with '--sidecar json' and '--update', all
|
||||||
|
JSON sidecar files which were skipped
|
||||||
|
sidecar_exiftool_written When used with '--sidecar exiftool', all exiftool
|
||||||
|
sidecar files which were written
|
||||||
|
sidecar_exiftool_skipped When used with '--sidecar exiftool' and '--update,
|
||||||
|
all exiftool sidecar files which were skipped
|
||||||
|
sidecar_xmp_written When used with '--sidecar xmp', all XMP sidecar
|
||||||
|
files which were written
|
||||||
|
sidecar_xmp_skipped When used with '--sidecar xmp' and '--update', all
|
||||||
|
XMP sidecar files which were skipped
|
||||||
|
error All files which produced an error during export
|
||||||
|
|
||||||
|
In addition to all normal template fields, the template fields '{filepath}' and
|
||||||
|
'{export_dir}' will be available to your command template. Both of these are
|
||||||
|
path-type templates which means their various parts can be accessed using the
|
||||||
|
available properties, e.g. '{filepath.name}' provides just the file name without
|
||||||
|
path and '{filepath.suffix}' is the file extension (suffix) of the file. When
|
||||||
|
using paths in your command template, it is important to properly quote the
|
||||||
|
paths as they will be passed to the shell and path names may contain spaces.
|
||||||
|
Both the '{shell_quote}' template and the '|shell_quote' template filter are
|
||||||
|
available for this purpose. For example, the following command outputs the full
|
||||||
|
path of newly exported files to file 'new.txt':
|
||||||
|
|
||||||
|
--post-command new "echo {filepath.name|shell_quote} >> {shell_quote,{export_dir}/exported.txt}"
|
||||||
|
|
||||||
|
In the above command, the 'shell_quote' filter is used to ensure
|
||||||
|
'{filepath.name}' is properly quoted and the '{shell_quote}' template ensures
|
||||||
|
the constructed path of '{exported_dir}/exported.txt' is properly quoted. If
|
||||||
|
'{filepath.name}' is 'IMG 1234.jpeg' and '{export_dir}' is '/Volumes/Photo
|
||||||
|
Export', the command thus renders to:
|
||||||
|
|
||||||
|
echo 'IMG 1234.jpeg' >> '/Volumes/Photo Export/exported.txt'
|
||||||
|
|
||||||
|
It is highly recommended that you run osxphotos with '--dry-run --verbose' first
|
||||||
|
to ensure your commands are as expected. This will not actually run the commands
|
||||||
|
but will print out the exact command string which would be executed.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
```
|
```
|
||||||
<!-- OSXPHOTOS-EXPORT-USAGE:END -->
|
<!-- OSXPHOTOS-EXPORT-USAGE:END -->
|
||||||
|
|
||||||
@@ -3047,6 +3170,7 @@ Valid filters are:
|
|||||||
- braces: Enclose value in curly braces, e.g. 'value => '{value}'.
|
- braces: Enclose value in curly braces, e.g. 'value => '{value}'.
|
||||||
- parens: Enclose value in parentheses, e.g. 'value' => '(value')
|
- parens: Enclose value in parentheses, e.g. 'value' => '(value')
|
||||||
- brackets: Enclose value in brackets, e.g. 'value' => '[value]'
|
- brackets: Enclose value in brackets, e.g. 'value' => '[value]'
|
||||||
|
- shell_quote: Quotes the value for safe usage in the shell, e.g. My file.jpeg => 'My file.jpeg'; only adds quotes if needed.
|
||||||
- function: Run custom python function to filter value; use in format 'function:/path/to/file.py::function_name'. See example at https://github.com/RhetTbull/osxphotos/blob/master/examples/template_filter.py
|
- function: Run custom python function to filter value; use in format 'function:/path/to/file.py::function_name'. See example at https://github.com/RhetTbull/osxphotos/blob/master/examples/template_filter.py
|
||||||
<!-- OSXPHOTOS-FILTER-TABLE:END -->
|
<!-- OSXPHOTOS-FILTER-TABLE:END -->
|
||||||
|
|
||||||
@@ -3223,7 +3347,7 @@ The following template field substitutions are availabe for use the templating s
|
|||||||
|{lf}|A line feed: '\n', alias for {newline}|
|
|{lf}|A line feed: '\n', alias for {newline}|
|
||||||
|{cr}|A carriage return: '\r'|
|
|{cr}|A carriage return: '\r'|
|
||||||
|{crlf}|a carriage return + line feed: '\r\n'|
|
|{crlf}|a carriage return + line feed: '\r\n'|
|
||||||
|{osxphotos_version}|The osxphotos version, e.g. '0.42.34'|
|
|{osxphotos_version}|The osxphotos version, e.g. '0.42.35'|
|
||||||
|{osxphotos_cmd_line}|The full command line used to run osxphotos|
|
|{osxphotos_cmd_line}|The full command line used to run osxphotos|
|
||||||
|{album}|Album(s) photo is contained in|
|
|{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|
|
|{folder_album}|Folder path + album photo is contained in. e.g. 'Folder/Subfolder/Album' or just 'Album' if no enclosing folder|
|
||||||
@@ -3238,6 +3362,7 @@ The following template field substitutions are availabe for use the templating s
|
|||||||
|{searchinfo.venue}|Venues associated with a photo, e.g. name of restaurant; (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).|
|
|{searchinfo.venue_type}|Venue types associated with a photo, e.g. 'Restaurant'; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).|
|
||||||
|{photo}|Provides direct access to the PhotoInfo object for the photo. Must be used in format '{photo.property}' where 'property' represents a PhotoInfo property. For example: '{photo.favorite}' is the same as '{favorite}' and '{photo.place.name}' is the same as '{place.name}'. '{photo}' provides access to properties that are not available as separate template fields but it assumes some knowledge of the underlying PhotoInfo class. See https://rhettbull.github.io/osxphotos/ for additional documentation on the PhotoInfo class.|
|
|{photo}|Provides direct access to the PhotoInfo object for the photo. Must be used in format '{photo.property}' where 'property' represents a PhotoInfo property. For example: '{photo.favorite}' is the same as '{favorite}' and '{photo.place.name}' is the same as '{place.name}'. '{photo}' provides access to properties that are not available as separate template fields but it assumes some knowledge of the underlying PhotoInfo class. See https://rhettbull.github.io/osxphotos/ for additional documentation on the PhotoInfo class.|
|
||||||
|
|{shell_quote}|Use in form '{shell_quote,TEMPLATE}'; quotes the rendered TEMPLATE value(s) for safe usage in the shell, e.g. My file.jpeg => 'My file.jpeg'; only adds quotes if needed.|
|
||||||
|{function}|Execute a python function from an external file and use return value as template substitution. Use in format: {function:file.py::function_name} where 'file.py' is the name of the python file and 'function_name' is the name of the function to call. The function will be passed the PhotoInfo object for the photo. See https://github.com/RhetTbull/osxphotos/blob/master/examples/template_function.py for an example of how to implement a template function.|
|
|{function}|Execute a python function from an external file and use return value as template substitution. Use in format: {function:file.py::function_name} where 'file.py' is the name of the python file and 'function_name' is the name of the function to call. The function will be passed the PhotoInfo object for the photo. See https://github.com/RhetTbull/osxphotos/blob/master/examples/template_function.py for an example of how to implement a template function.|
|
||||||
<!-- OSXPHOTOS-TEMPLATE-TABLE:END -->
|
<!-- OSXPHOTOS-TEMPLATE-TABLE:END -->
|
||||||
|
|
||||||
|
|||||||
@@ -315,6 +315,40 @@ Then the next to you run osxphotos, you can simply do this:
|
|||||||
|
|
||||||
The configuration file is a plain text file in [TOML](https://toml.io/en/) format so the `.toml` extension is standard but you can name the file anything you like.
|
The configuration file is a plain text file in [TOML](https://toml.io/en/) format so the `.toml` extension is standard but you can name the file anything you like.
|
||||||
|
|
||||||
|
### Run commands on exported photos for post-processing
|
||||||
|
|
||||||
|
You can use the `--post-command` option to run one or more commands against exported files. The `--post-command` option takes two arguments: CATEGORY and COMMAND. CATEGORY is a string that describes which category of file to run the command against. The available categories are described in the help text available via: `osxphotos help export`. For example, the `exported` category includes all exported photos and the `skipped` category includes all photos that were skipped when running export with `--update`. COMMAND is an osxphotos template string which will be rendered then passed to the shell for execution.
|
||||||
|
|
||||||
|
For example, the following command generates a log of all exported files and their associated keywords:
|
||||||
|
|
||||||
|
`osxphotos export /path/to/export --post-command exported "echo {shell_quote,{filepath}{comma}{,+keyword,}} >> {shell_quote,{export_dir}/exported.txt}"`
|
||||||
|
|
||||||
|
The special template field `{shell_quote}` ensures a string is properly quoted for execution in the shell. For example, it's possible that a file path or keyword in this example has a space in the value and if not properly quoted, this would cause an error in the execution of the command. When running commands, the template `{filepath}` is set to the full path of the exported file and `{export_dir}` is set to the full path of the base export directory.
|
||||||
|
|
||||||
|
Explanation of the template string:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
{shell_quote,{filepath}{comma}{,+keyword,}}
|
||||||
|
│ │ │ │ │
|
||||||
|
│ │ │ | │
|
||||||
|
└──> quote everything after comma for proper execution in the shell
|
||||||
|
│ │ │ │
|
||||||
|
└───> filepath of the exported file
|
||||||
|
│ │ │
|
||||||
|
└───> insert a comma
|
||||||
|
│ │
|
||||||
|
└───> join the list of keywords together with a ","
|
||||||
|
│
|
||||||
|
└───> if no keywords, insert nothing (empty string: "")
|
||||||
|
```
|
||||||
|
|
||||||
|
Another example: if you had `exiftool` installed and wanted to wipe all metadata from all exported files, you could use the following:
|
||||||
|
|
||||||
|
`osxphotos export /path/to/export --post-command exported "/usr/local/bin/exiftool -all= {filepath|shell_quote}"`
|
||||||
|
|
||||||
|
This command uses the `|shell_quote` template filter instead of the `{shell_quote}` template because the only thing that needs to be quoted is the path to the exported file. Template filters filter the value of the rendered template field. A number of other filters are available and are described in the help text.
|
||||||
|
|
||||||
|
|
||||||
### An example from an actual osxphotos user
|
### An example from an actual osxphotos user
|
||||||
|
|
||||||
Here's a comprehensive use case from an actual osxphotos user that integrates many of the concepts discussed in this tutorial (thank-you Philippe for contributing this!):
|
Here's a comprehensive use case from an actual osxphotos user that integrates many of the concepts discussed in this tutorial (thank-you Philippe for contributing this!):
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from ._version import __version__
|
from ._version import __version__
|
||||||
from .photoinfo import PhotoInfo
|
from .exiftool import ExifTool
|
||||||
|
from .photoinfo import ExportResults, PhotoInfo
|
||||||
from .photosdb import PhotosDB
|
from .photosdb import PhotosDB
|
||||||
from .photosdb._photosdb_process_comments import CommentInfo, LikeInfo
|
from .photosdb._photosdb_process_comments import CommentInfo, LikeInfo
|
||||||
from .phototemplate import PhotoTemplate
|
from .phototemplate import PhotoTemplate
|
||||||
@@ -7,5 +8,4 @@ from .queryoptions import QueryOptions
|
|||||||
from .utils import _debug, _get_logger, _set_debug
|
from .utils import _debug, _get_logger, _set_debug
|
||||||
|
|
||||||
# TODO: Add test for imageTimeZoneOffsetSeconds = None
|
# TODO: Add test for imageTimeZoneOffsetSeconds = None
|
||||||
# TODO: Add test for __str__ and to_json
|
|
||||||
# TODO: Add special albums and magic albums
|
# TODO: Add special albums and magic albums
|
||||||
|
|||||||
@@ -220,3 +220,24 @@ BURST_KEY = 0b10000 # 16: burst image is the key photo (top of burst stack)
|
|||||||
BURST_UNKNOWN = 0b100000 # 32: this is almost always set with BURST_DEFAULT_PICK and never if BURST_DEFAULT_PICK is not set. I think this has something to do with what algorithm Photos used to pick the default image
|
BURST_UNKNOWN = 0b100000 # 32: this is almost always set with BURST_DEFAULT_PICK and never if BURST_DEFAULT_PICK is not set. I think this has something to do with what algorithm Photos used to pick the default image
|
||||||
|
|
||||||
LIVE_VIDEO_EXTENSIONS = [".mov"]
|
LIVE_VIDEO_EXTENSIONS = [".mov"]
|
||||||
|
|
||||||
|
# categories that --post-command can be used with; these map to ExportResults fields
|
||||||
|
POST_COMMAND_CATEGORIES = {
|
||||||
|
"exported": "All exported files",
|
||||||
|
"new": "When used with '--update', all newly exported files",
|
||||||
|
"updated": "When used with '--update', all files which were previously exported but updated this time",
|
||||||
|
"skipped": "When used with '--update', all files which were skipped (because they were previously exported and didn't change)",
|
||||||
|
"missing": "All files which were not exported because they were missing from the Photos library",
|
||||||
|
"exif_updated": "When used with '--exiftool', all files on which exiftool updated the metadata",
|
||||||
|
"touched": "When used with '--touch-file', all files where the date was touched",
|
||||||
|
"converted_to_jpeg": "When used with '--convert-to-jpeg', all files which were converted to jpeg",
|
||||||
|
"sidecar_json_written": "When used with '--sidecar json', all JSON sidecar files which were written",
|
||||||
|
"sidecar_json_skipped": "When used with '--sidecar json' and '--update', all JSON sidecar files which were skipped",
|
||||||
|
"sidecar_exiftool_written": "When used with '--sidecar exiftool', all exiftool sidecar files which were written",
|
||||||
|
"sidecar_exiftool_skipped": "When used with '--sidecar exiftool' and '--update, all exiftool sidecar files which were skipped",
|
||||||
|
"sidecar_xmp_written": "When used with '--sidecar xmp', all XMP sidecar files which were written",
|
||||||
|
"sidecar_xmp_skipped": "When used with '--sidecar xmp' and '--update', all XMP sidecar files which were skipped",
|
||||||
|
"error": "All files which produced an error during export",
|
||||||
|
# "deleted_files": "When used with '--cleanup', all files deleted during the export",
|
||||||
|
# "deleted_directories": "When used with '--cleanup', all directories deleted during the export",
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
""" version info """
|
""" version info """
|
||||||
|
|
||||||
__version__ = "0.42.34"
|
__version__ = "0.42.35"
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import os
|
|||||||
import os.path
|
import os.path
|
||||||
import pathlib
|
import pathlib
|
||||||
import pprint
|
import pprint
|
||||||
|
import shlex
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
@@ -31,10 +33,10 @@ from ._constants import (
|
|||||||
EXTENDED_ATTRIBUTE_NAMES_QUOTED,
|
EXTENDED_ATTRIBUTE_NAMES_QUOTED,
|
||||||
OSXPHOTOS_EXPORT_DB,
|
OSXPHOTOS_EXPORT_DB,
|
||||||
OSXPHOTOS_URL,
|
OSXPHOTOS_URL,
|
||||||
|
POST_COMMAND_CATEGORIES,
|
||||||
SIDECAR_EXIFTOOL,
|
SIDECAR_EXIFTOOL,
|
||||||
SIDECAR_JSON,
|
SIDECAR_JSON,
|
||||||
SIDECAR_XMP,
|
SIDECAR_XMP,
|
||||||
UNICODE_FORMAT,
|
|
||||||
)
|
)
|
||||||
from ._version import __version__
|
from ._version import __version__
|
||||||
from .cli_help import ExportCommand
|
from .cli_help import ExportCommand
|
||||||
@@ -51,7 +53,7 @@ from .path_utils import is_valid_filepath, sanitize_filename, sanitize_filepath
|
|||||||
from .photoinfo import ExportResults
|
from .photoinfo import ExportResults
|
||||||
from .photokit import check_photokit_authorization, request_photokit_authorization
|
from .photokit import check_photokit_authorization, request_photokit_authorization
|
||||||
from .photosalbum import PhotosAlbum
|
from .photosalbum import PhotosAlbum
|
||||||
from .phototemplate import RenderOptions
|
from .phototemplate import PhotoTemplate, RenderOptions
|
||||||
from .queryoptions import QueryOptions
|
from .queryoptions import QueryOptions
|
||||||
from .utils import get_preferred_uti_extension
|
from .utils import get_preferred_uti_extension
|
||||||
|
|
||||||
@@ -911,6 +913,19 @@ def cli(ctx, db, json_, debug):
|
|||||||
"This only works if the Photos library being exported is the last-opened (default) library in Photos. "
|
"This only works if the Photos library being exported is the last-opened (default) library in Photos. "
|
||||||
"This feature is currently experimental. I don't know how well it will work on large export sets.",
|
"This feature is currently experimental. I don't know how well it will work on large export sets.",
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
"--post-command",
|
||||||
|
metavar="CATEGORY COMMAND",
|
||||||
|
nargs=2,
|
||||||
|
type=(click.Choice(POST_COMMAND_CATEGORIES, case_sensitive=False), str),
|
||||||
|
multiple=True,
|
||||||
|
help="Run COMMAND on exported files of category CATEGORY. CATEGORY can be one of: "
|
||||||
|
f"{', '.join(list(POST_COMMAND_CATEGORIES.keys()))}. "
|
||||||
|
"COMMAND is an osxphotos template string, for example: '--post-command exported \"echo {filepath|shell_quote} >> {export_dir}/exported.txt\"', "
|
||||||
|
"which appends the full path of all exported files to the file 'exported.txt'. "
|
||||||
|
"You can run more than one command by repeating the '--post-command' option with different arguments. "
|
||||||
|
"See Post Command below.",
|
||||||
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--exportdb",
|
"--exportdb",
|
||||||
metavar="EXPORTDB_FILE",
|
metavar="EXPORTDB_FILE",
|
||||||
@@ -1075,6 +1090,7 @@ def export(
|
|||||||
regex,
|
regex,
|
||||||
query_eval,
|
query_eval,
|
||||||
duplicate,
|
duplicate,
|
||||||
|
post_command,
|
||||||
):
|
):
|
||||||
"""Export photos from the Photos database.
|
"""Export photos from the Photos database.
|
||||||
Export path DEST is required.
|
Export path DEST is required.
|
||||||
@@ -1230,6 +1246,7 @@ def export(
|
|||||||
regex = cfg.regex
|
regex = cfg.regex
|
||||||
query_eval = cfg.query_eval
|
query_eval = cfg.query_eval
|
||||||
duplicate = cfg.duplicate
|
duplicate = cfg.duplicate
|
||||||
|
post_command = cfg.post_command
|
||||||
|
|
||||||
# config file might have changed verbose
|
# config file might have changed verbose
|
||||||
VERBOSE = bool(verbose)
|
VERBOSE = bool(verbose)
|
||||||
@@ -1633,6 +1650,15 @@ def export(
|
|||||||
export_dir=dest,
|
export_dir=dest,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
run_post_command(
|
||||||
|
photo=p,
|
||||||
|
post_command=post_command,
|
||||||
|
export_results=export_results,
|
||||||
|
export_dir=dest,
|
||||||
|
dry_run=dry_run,
|
||||||
|
exiftool_path=exiftool_path,
|
||||||
|
)
|
||||||
|
|
||||||
if album_export and export_results.exported:
|
if album_export and export_results.exported:
|
||||||
try:
|
try:
|
||||||
album_export.add(p)
|
album_export.add(p)
|
||||||
@@ -3275,6 +3301,46 @@ def write_extended_attributes(
|
|||||||
return list(written), [f for f in skipped if f not in written]
|
return list(written), [f for f in skipped if f not in written]
|
||||||
|
|
||||||
|
|
||||||
|
def run_post_command(
|
||||||
|
photo, post_command, export_results, export_dir, dry_run, exiftool_path
|
||||||
|
):
|
||||||
|
# todo: pass in RenderOptions from export? (e.g. so it contains strip, etc?)
|
||||||
|
# todo: need a shell_quote template type:
|
||||||
|
# {shell_quote,{filepath}/foo/bar}
|
||||||
|
# that quotes everything in the default value
|
||||||
|
for category, command_template in post_command:
|
||||||
|
files = getattr(export_results, category)
|
||||||
|
for f in files:
|
||||||
|
# some categories, like error, return a tuple of (file, error str)
|
||||||
|
if isinstance(f, tuple):
|
||||||
|
f = f[0]
|
||||||
|
render_options = RenderOptions(export_dir=export_dir, filepath=f)
|
||||||
|
template = PhotoTemplate(photo, exiftool_path=exiftool_path)
|
||||||
|
command, _ = template.render(command_template, options=render_options)
|
||||||
|
command = command[0] if command else None
|
||||||
|
if command:
|
||||||
|
verbose_(f'Running command: "{command}"')
|
||||||
|
if not dry_run:
|
||||||
|
args = shlex.split(command)
|
||||||
|
cwd = pathlib.Path(f).parent
|
||||||
|
run_error = None
|
||||||
|
run_results = None
|
||||||
|
try:
|
||||||
|
run_results = subprocess.run(command, shell=True, cwd=cwd)
|
||||||
|
except Exception as e:
|
||||||
|
run_error = e
|
||||||
|
finally:
|
||||||
|
run_error = run_error or run_results.returncode
|
||||||
|
if run_error:
|
||||||
|
click.echo(
|
||||||
|
click.style(
|
||||||
|
f'Error running command "{command}": {run_error}',
|
||||||
|
fg=CLI_COLOR_ERROR,
|
||||||
|
),
|
||||||
|
err=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@cli.command(hidden=True)
|
@cli.command(hidden=True)
|
||||||
@DB_OPTION
|
@DB_OPTION
|
||||||
@DB_ARGUMENT
|
@DB_ARGUMENT
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from ._constants import (
|
|||||||
EXTENDED_ATTRIBUTE_NAMES,
|
EXTENDED_ATTRIBUTE_NAMES,
|
||||||
EXTENDED_ATTRIBUTE_NAMES_QUOTED,
|
EXTENDED_ATTRIBUTE_NAMES_QUOTED,
|
||||||
OSXPHOTOS_EXPORT_DB,
|
OSXPHOTOS_EXPORT_DB,
|
||||||
|
POST_COMMAND_CATEGORIES,
|
||||||
)
|
)
|
||||||
from .phototemplate import (
|
from .phototemplate import (
|
||||||
TEMPLATE_SUBSTITUTIONS,
|
TEMPLATE_SUBSTITUTIONS,
|
||||||
@@ -21,6 +22,7 @@ from .phototemplate import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: The following help text could probably be done as mako template
|
||||||
class ExportCommand(click.Command):
|
class ExportCommand(click.Command):
|
||||||
"""Custom click.Command that overrides get_help() to show additional help info for export"""
|
"""Custom click.Command that overrides get_help() to show additional help info for export"""
|
||||||
|
|
||||||
@@ -175,11 +177,13 @@ The following attributes may be used with '--xattr-template':
|
|||||||
)
|
)
|
||||||
formatter.write("\n")
|
formatter.write("\n")
|
||||||
formatter.write(
|
formatter.write(
|
||||||
"For example, if the field {export_dir} is '/Shared/Backup/Photos':\n")
|
"For example, if the field {export_dir} is '/Shared/Backup/Photos':\n"
|
||||||
|
)
|
||||||
formatter.write("{export_dir.parent} is '/Shared/Backup'\n")
|
formatter.write("{export_dir.parent} is '/Shared/Backup'\n")
|
||||||
formatter.write("\n")
|
formatter.write("\n")
|
||||||
formatter.write(
|
formatter.write(
|
||||||
"If the field {filepath} is '/Shared/Backup/Photos/IMG_1234.JPG':\n")
|
"If the field {filepath} is '/Shared/Backup/Photos/IMG_1234.JPG':\n"
|
||||||
|
)
|
||||||
formatter.write("{filepath.parent} is '/Shared/Backup/Photos'\n")
|
formatter.write("{filepath.parent} is '/Shared/Backup/Photos'\n")
|
||||||
formatter.write("{filepath.name} is 'IMG_1234.JPG'\n")
|
formatter.write("{filepath.name} is 'IMG_1234.JPG'\n")
|
||||||
formatter.write("{filepath.stem} is 'IMG_1234'\n")
|
formatter.write("{filepath.stem} is 'IMG_1234'\n")
|
||||||
@@ -190,6 +194,54 @@ The following attributes may be used with '--xattr-template':
|
|||||||
|
|
||||||
formatter.write_dl(templ_tuples)
|
formatter.write_dl(templ_tuples)
|
||||||
|
|
||||||
|
formatter.write("\n\n")
|
||||||
|
formatter.write(
|
||||||
|
rich_text("[bold]** Post Command **[/bold]", width=formatter.width)
|
||||||
|
)
|
||||||
|
formatter.write_text(
|
||||||
|
"You can run commands on the exported photos for post-processing "
|
||||||
|
+ "using the '--post-command' option. '--post-command' is passed a CATEGORY and a COMMAND. "
|
||||||
|
+ "COMMAND is an osxphotos template string which will be rendered and passed to the shell "
|
||||||
|
+ "for execution. CATEGORY is the category of file to pass to COMMAND. "
|
||||||
|
+ "The following categories are available: "
|
||||||
|
)
|
||||||
|
formatter.write("\n")
|
||||||
|
templ_tuples = [("Catgory", "Description")]
|
||||||
|
templ_tuples.extend((k, v) for k, v in POST_COMMAND_CATEGORIES.items())
|
||||||
|
formatter.write_dl(templ_tuples)
|
||||||
|
formatter.write("\n")
|
||||||
|
formatter.write_text(
|
||||||
|
"In addition to all normal template fields, the template fields "
|
||||||
|
+ "'{filepath}' and '{export_dir}' will be available to your command template. "
|
||||||
|
+ "Both of these are path-type templates which means their various parts can be accessed using "
|
||||||
|
+ "the available properties, e.g. '{filepath.name}' provides just the file name without path "
|
||||||
|
+ "and '{filepath.suffix}' is the file extension (suffix) of the file. "
|
||||||
|
+ "When using paths in your command template, it is important to properly quote the paths "
|
||||||
|
+ "as they will be passed to the shell and path names may contain spaces. "
|
||||||
|
+ "Both the '{shell_quote}' template and the '|shell_quote' template filter are available for "
|
||||||
|
+ "this purpose. For example, the following command outputs the full path of newly exported files to file 'new.txt': "
|
||||||
|
)
|
||||||
|
formatter.write("\n")
|
||||||
|
formatter.write(
|
||||||
|
'--post-command new "echo {filepath.name|shell_quote} >> {shell_quote,{export_dir}/exported.txt}"'
|
||||||
|
)
|
||||||
|
formatter.write("\n\n")
|
||||||
|
formatter.write_text(
|
||||||
|
"In the above command, the 'shell_quote' filter is used to ensure '{filepath.name}' is properly quoted "
|
||||||
|
+ "and the '{shell_quote}' template ensures the constructed path of '{exported_dir}/exported.txt' is properly quoted. "
|
||||||
|
"If '{filepath.name}' is 'IMG 1234.jpeg' and '{export_dir}' is '/Volumes/Photo Export', the command "
|
||||||
|
"thus renders to: "
|
||||||
|
)
|
||||||
|
formatter.write("\n")
|
||||||
|
formatter.write("echo 'IMG 1234.jpeg' >> '/Volumes/Photo Export/exported.txt'")
|
||||||
|
formatter.write("\n\n")
|
||||||
|
formatter.write_text(
|
||||||
|
"It is highly recommended that you run osxphotos with '--dry-run --verbose' "
|
||||||
|
+ "first to ensure your commands are as expected. This will not actually run the commands but will "
|
||||||
|
+ "print out the exact command string which would be executed."
|
||||||
|
)
|
||||||
|
formatter.write("\n\n")
|
||||||
|
|
||||||
help_text += formatter.getvalue()
|
help_text += formatter.getvalue()
|
||||||
return help_text
|
return help_text
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ Valid filters are:
|
|||||||
- braces: Enclose value in curly braces, e.g. 'value => '{value}'.
|
- braces: Enclose value in curly braces, e.g. 'value => '{value}'.
|
||||||
- parens: Enclose value in parentheses, e.g. 'value' => '(value')
|
- parens: Enclose value in parentheses, e.g. 'value' => '(value')
|
||||||
- brackets: Enclose value in brackets, e.g. 'value' => '[value]'
|
- brackets: Enclose value in brackets, e.g. 'value' => '[value]'
|
||||||
|
- shell_quote: Quotes the value for safe usage in the shell, e.g. My file.jpeg => 'My file.jpeg'; only adds quotes if needed.
|
||||||
- function: Run custom python function to filter value; use in format 'function:/path/to/file.py::function_name'. See example at https://github.com/RhetTbull/osxphotos/blob/master/examples/template_filter.py
|
- function: Run custom python function to filter value; use in format 'function:/path/to/file.py::function_name'. See example at https://github.com/RhetTbull/osxphotos/blob/master/examples/template_filter.py
|
||||||
<!-- OSXPHOTOS-FILTER-TABLE:END -->
|
<!-- OSXPHOTOS-FILTER-TABLE:END -->
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import locale
|
|||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
import sys
|
import sys
|
||||||
|
import shlex
|
||||||
|
|
||||||
from textx import TextXSyntaxError, metamodel_from_file
|
from textx import TextXSyntaxError, metamodel_from_file
|
||||||
|
|
||||||
@@ -173,6 +174,7 @@ TEMPLATE_SUBSTITUTIONS_MULTI_VALUED = {
|
|||||||
+ "For example: '{photo.favorite}' is the same as '{favorite}' and '{photo.place.name}' is the same as '{place.name}'. "
|
+ "For example: '{photo.favorite}' is the same as '{favorite}' and '{photo.place.name}' is the same as '{place.name}'. "
|
||||||
+ "'{photo}' provides access to properties that are not available as separate template fields but it assumes some knowledge of "
|
+ "'{photo}' provides access to properties that are not available as separate template fields but it assumes some knowledge of "
|
||||||
+ "the underlying PhotoInfo class. See https://rhettbull.github.io/osxphotos/ for additional documentation on the PhotoInfo class.",
|
+ "the underlying PhotoInfo class. See https://rhettbull.github.io/osxphotos/ for additional documentation on the PhotoInfo class.",
|
||||||
|
"{shell_quote}": "Use in form '{shell_quote,TEMPLATE}'; quotes the rendered TEMPLATE value(s) for safe usage in the shell, e.g. My file.jpeg => 'My file.jpeg'; only adds quotes if needed.",
|
||||||
"{function}": "Execute a python function from an external file and use return value as template substitution. "
|
"{function}": "Execute a python function from an external file and use return value as template substitution. "
|
||||||
+ "Use in format: {function:file.py::function_name} where 'file.py' is the name of the python file and 'function_name' is the name of the function to call. "
|
+ "Use in format: {function:file.py::function_name} where 'file.py' is the name of the python file and 'function_name' is the name of the function to call. "
|
||||||
+ "The function will be passed the PhotoInfo object for the photo. "
|
+ "The function will be passed the PhotoInfo object for the photo. "
|
||||||
@@ -188,6 +190,7 @@ FILTER_VALUES = {
|
|||||||
"braces": "Enclose value in curly braces, e.g. 'value => '{value}'.",
|
"braces": "Enclose value in curly braces, e.g. 'value => '{value}'.",
|
||||||
"parens": "Enclose value in parentheses, e.g. 'value' => '(value')",
|
"parens": "Enclose value in parentheses, e.g. 'value' => '(value')",
|
||||||
"brackets": "Enclose value in brackets, e.g. 'value' => '[value]'",
|
"brackets": "Enclose value in brackets, e.g. 'value' => '[value]'",
|
||||||
|
"shell_quote": "Quotes the value for safe usage in the shell, e.g. My file.jpeg => 'My file.jpeg'; only adds quotes if needed.",
|
||||||
"function": "Run custom python function to filter value; use in format 'function:/path/to/file.py::function_name'. See example at https://github.com/RhetTbull/osxphotos/blob/master/examples/template_filter.py",
|
"function": "Run custom python function to filter value; use in format 'function:/path/to/file.py::function_name'. See example at https://github.com/RhetTbull/osxphotos/blob/master/examples/template_filter.py",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,6 +251,7 @@ class RenderOptions:
|
|||||||
edited_version: set to True if you want {edited_version} to resolve to True (e.g. exporting edited version of photo)
|
edited_version: set to True if you want {edited_version} to resolve to True (e.g. exporting edited version of photo)
|
||||||
export_dir: set to the export directory if you want to evalute {export_dir} template
|
export_dir: set to the export directory if you want to evalute {export_dir} template
|
||||||
filepath: set to value for filepath of the exported photo if you want to evaluate {filepath} template
|
filepath: set to value for filepath of the exported photo if you want to evaluate {filepath} template
|
||||||
|
quote: quote path templates for execution in the shell
|
||||||
"""
|
"""
|
||||||
|
|
||||||
none_str: str = "_"
|
none_str: str = "_"
|
||||||
@@ -260,6 +264,7 @@ class RenderOptions:
|
|||||||
edited_version: bool = False
|
edited_version: bool = False
|
||||||
export_dir: Optional[str] = None
|
export_dir: Optional[str] = None
|
||||||
filepath: Optional[str] = None
|
filepath: Optional[str] = None
|
||||||
|
quote: bool = False
|
||||||
|
|
||||||
|
|
||||||
class PhotoTemplateParser:
|
class PhotoTemplateParser:
|
||||||
@@ -320,6 +325,7 @@ class PhotoTemplate:
|
|||||||
self.strip = options.strip
|
self.strip = options.strip
|
||||||
self.export_dir = options.export_dir
|
self.export_dir = options.export_dir
|
||||||
self.filepath = options.filepath
|
self.filepath = options.filepath
|
||||||
|
self.quote = options.quote
|
||||||
|
|
||||||
def render(
|
def render(
|
||||||
self,
|
self,
|
||||||
@@ -349,6 +355,7 @@ class PhotoTemplate:
|
|||||||
self.strip = options.strip
|
self.strip = options.strip
|
||||||
self.export_dir = options.export_dir
|
self.export_dir = options.export_dir
|
||||||
self.filepath = options.filepath
|
self.filepath = options.filepath
|
||||||
|
self.quote = options.quote
|
||||||
|
|
||||||
try:
|
try:
|
||||||
model = self.parser.parse(template)
|
model = self.parser.parse(template)
|
||||||
@@ -501,8 +508,7 @@ class PhotoTemplate:
|
|||||||
)
|
)
|
||||||
elif field in MULTI_VALUE_SUBSTITUTIONS or field.startswith("photo"):
|
elif field in MULTI_VALUE_SUBSTITUTIONS or field.startswith("photo"):
|
||||||
vals = self.get_template_value_multi(
|
vals = self.get_template_value_multi(
|
||||||
field,
|
field, path_sep=path_sep, default=default
|
||||||
path_sep=path_sep,
|
|
||||||
)
|
)
|
||||||
elif field.split(".")[0] in PATHLIB_SUBSTITUTIONS:
|
elif field.split(".")[0] in PATHLIB_SUBSTITUTIONS:
|
||||||
vals = self.get_template_value_pathlib(field)
|
vals = self.get_template_value_pathlib(field)
|
||||||
@@ -952,7 +958,7 @@ class PhotoTemplate:
|
|||||||
except AttributeError:
|
except AttributeError:
|
||||||
raise ValueError(f"Unknown path-like field: {field_stem}")
|
raise ValueError(f"Unknown path-like field: {field_stem}")
|
||||||
|
|
||||||
value = _get_pathlib_value(field, field_value)
|
value = _get_pathlib_value(field, field_value, self.quote)
|
||||||
|
|
||||||
if self.filename:
|
if self.filename:
|
||||||
value = sanitize_pathpart(value)
|
value = sanitize_pathpart(value)
|
||||||
@@ -1002,22 +1008,24 @@ class PhotoTemplate:
|
|||||||
value = ["[" + v + "]" for v in values]
|
value = ["[" + v + "]" for v in values]
|
||||||
else:
|
else:
|
||||||
value = ["[" + values + "]"] if values else []
|
value = ["[" + values + "]"] if values else []
|
||||||
|
elif filter_ == "shell_quote":
|
||||||
|
if values and type(values) == list:
|
||||||
|
value = [shlex.quote(v) for v in values]
|
||||||
|
else:
|
||||||
|
value = [shlex.quote(values)] if values else []
|
||||||
elif filter_.startswith("function:"):
|
elif filter_.startswith("function:"):
|
||||||
value = self.get_template_value_filter_function(filter_, values)
|
value = self.get_template_value_filter_function(filter_, values)
|
||||||
else:
|
else:
|
||||||
value = []
|
value = []
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def get_template_value_multi(
|
def get_template_value_multi(self, field, path_sep, default):
|
||||||
self,
|
|
||||||
field,
|
|
||||||
path_sep,
|
|
||||||
):
|
|
||||||
"""lookup value for template field (multi-value template substitutions)
|
"""lookup value for template field (multi-value template substitutions)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
field: template field to find value for.
|
field: template field to find value for.
|
||||||
path_sep: path separator to use for folder_album field
|
path_sep: path separator to use for folder_album field
|
||||||
|
default: value of default field
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of the matching template values or [].
|
List of the matching template values or [].
|
||||||
@@ -1084,6 +1092,8 @@ class PhotoTemplate:
|
|||||||
values = (
|
values = (
|
||||||
self.photo.search_info.venue_types if self.photo.search_info else []
|
self.photo.search_info.venue_types if self.photo.search_info else []
|
||||||
)
|
)
|
||||||
|
elif field == "shell_quote":
|
||||||
|
values = [shlex.quote(v) for v in default if v]
|
||||||
elif field.startswith("photo"):
|
elif field.startswith("photo"):
|
||||||
# provide access to PhotoInfo object
|
# provide access to PhotoInfo object
|
||||||
properties = field.split(".")
|
properties = field.split(".")
|
||||||
@@ -1292,12 +1302,18 @@ def get_template_help():
|
|||||||
return md
|
return md
|
||||||
|
|
||||||
|
|
||||||
def _get_pathlib_value(field, value):
|
def _get_pathlib_value(field, value, quote):
|
||||||
"""Get the value for a pathlib.Path type template"""
|
"""Get the value for a pathlib.Path type template
|
||||||
|
|
||||||
|
Args:
|
||||||
|
field: the path field, e.g. "filename.stem"
|
||||||
|
value: the value for the path component
|
||||||
|
quote: bool; if true, quotes the returned path for safe execution in the shell
|
||||||
|
"""
|
||||||
parts = field.split(".")
|
parts = field.split(".")
|
||||||
|
|
||||||
if len(parts) == 1:
|
if len(parts) == 1:
|
||||||
return value
|
return shlex.quote(value) if quote else value
|
||||||
|
|
||||||
if len(parts) > 2:
|
if len(parts) > 2:
|
||||||
raise ValueError(f"Illegal value for path template: {field}")
|
raise ValueError(f"Illegal value for path template: {field}")
|
||||||
@@ -1307,6 +1323,9 @@ def _get_pathlib_value(field, value):
|
|||||||
path = pathlib.Path(value)
|
path = pathlib.Path(value)
|
||||||
try:
|
try:
|
||||||
val = getattr(path, attribute)
|
val = getattr(path, attribute)
|
||||||
return str(val)
|
val_str = str(val)
|
||||||
|
if quote:
|
||||||
|
val_str = shlex.quote(val_str)
|
||||||
|
return val_str
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
raise ValueError("Illegal value for path template: {attribute}")
|
raise ValueError("Illegal value for path template: {attribute}")
|
||||||
|
|||||||
@@ -6363,3 +6363,83 @@ def test_export_filepath_template():
|
|||||||
assert exifdata[0]["XMP:Description"] == os.path.join(
|
assert exifdata[0]["XMP:Description"] == os.path.join(
|
||||||
isolated_cwd, CLI_TEMPLATE_FILENAME
|
isolated_cwd, CLI_TEMPLATE_FILENAME
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_post_command():
|
||||||
|
"""Test --post-command"""
|
||||||
|
import os.path
|
||||||
|
from osxphotos.cli 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, PHOTOS_DB_15_7),
|
||||||
|
".",
|
||||||
|
"--post-command",
|
||||||
|
"exported",
|
||||||
|
"echo {filepath.name|shell_quote} >> {export_dir}/exported.txt",
|
||||||
|
"--name",
|
||||||
|
"Park",
|
||||||
|
"--skip-original-if-edited",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
with open("exported.txt") as f:
|
||||||
|
lines = [line.strip() for line in f]
|
||||||
|
assert lines[0] == "St James Park_edited.jpeg"
|
||||||
|
|
||||||
|
# run again with --update to test skipped
|
||||||
|
result = runner.invoke(
|
||||||
|
cli,
|
||||||
|
[
|
||||||
|
"export",
|
||||||
|
"--db",
|
||||||
|
os.path.join(cwd, PHOTOS_DB_15_7),
|
||||||
|
".",
|
||||||
|
"--post-command",
|
||||||
|
"skipped",
|
||||||
|
"echo {filepath.name|shell_quote} >> {export_dir}/skipped.txt",
|
||||||
|
"--name",
|
||||||
|
"Park",
|
||||||
|
"--skip-original-if-edited",
|
||||||
|
"--update",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
with open("skipped.txt") as f:
|
||||||
|
lines = [line.strip() for line in f]
|
||||||
|
assert lines[0] == "St James Park_edited.jpeg"
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_post_command_bad_command():
|
||||||
|
"""Test --post-command with bad command"""
|
||||||
|
import os.path
|
||||||
|
from osxphotos.cli 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, PHOTOS_DB_15_7),
|
||||||
|
".",
|
||||||
|
"--post-command",
|
||||||
|
"exported",
|
||||||
|
"foobar {filepath.name|shell_quote} >> {export_dir}/exported.txt",
|
||||||
|
"--name",
|
||||||
|
"Park",
|
||||||
|
"--skip-original-if-edited",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert 'Error running command "foobar' in result.output
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ TEMPLATE_VALUES_MULTI_KEYWORDS = {
|
|||||||
"{keyword|lower}": ["flowers", "wedding"],
|
"{keyword|lower}": ["flowers", "wedding"],
|
||||||
"{keyword|titlecase}": ["Flowers", "Wedding"],
|
"{keyword|titlecase}": ["Flowers", "Wedding"],
|
||||||
"{keyword|capitalize}": ["Flowers", "Wedding"],
|
"{keyword|capitalize}": ["Flowers", "Wedding"],
|
||||||
|
"{keyword|shell_quote}": ["flowers", "wedding"],
|
||||||
"{+keyword}": ["flowerswedding"],
|
"{+keyword}": ["flowerswedding"],
|
||||||
"{+keyword|titlecase}": ["Flowerswedding"],
|
"{+keyword|titlecase}": ["Flowerswedding"],
|
||||||
"{+keyword|capitalize}": ["Flowerswedding"],
|
"{+keyword|capitalize}": ["Flowerswedding"],
|
||||||
@@ -81,6 +82,7 @@ TEMPLATE_VALUES_TITLE = {
|
|||||||
"{title|titlecase}": ["Tulips Tied Together At A Flower Shop"],
|
"{title|titlecase}": ["Tulips Tied Together At A Flower Shop"],
|
||||||
"{title|upper}": ["TULIPS TIED TOGETHER AT A FLOWER SHOP"],
|
"{title|upper}": ["TULIPS TIED TOGETHER AT A FLOWER SHOP"],
|
||||||
"{title|titlecase|lower|upper}": ["TULIPS TIED TOGETHER AT A FLOWER SHOP"],
|
"{title|titlecase|lower|upper}": ["TULIPS TIED TOGETHER AT A FLOWER SHOP"],
|
||||||
|
"{title|titlecase|lower|upper|shell_quote}": ["'TULIPS TIED TOGETHER AT A FLOWER SHOP'"],
|
||||||
"{title|upper|titlecase}": ["Tulips Tied Together At A Flower Shop"],
|
"{title|upper|titlecase}": ["Tulips Tied Together At A Flower Shop"],
|
||||||
"{title|capitalize}": ["Tulips tied together at a flower shop"],
|
"{title|capitalize}": ["Tulips tied together at a flower shop"],
|
||||||
"{title[ ,_]}": ["Tulips_tied_together_at_a_flower_shop"],
|
"{title[ ,_]}": ["Tulips_tied_together_at_a_flower_shop"],
|
||||||
@@ -90,6 +92,7 @@ TEMPLATE_VALUES_TITLE = {
|
|||||||
"{+title}": ["Tulips tied together at a flower shop"],
|
"{+title}": ["Tulips tied together at a flower shop"],
|
||||||
"{,+title}": ["Tulips tied together at a flower shop"],
|
"{,+title}": ["Tulips tied together at a flower shop"],
|
||||||
"{, +title}": ["Tulips tied together at a flower shop"],
|
"{, +title}": ["Tulips tied together at a flower shop"],
|
||||||
|
"{title|shell_quote}": ["'Tulips tied together at a flower shop'"],
|
||||||
}
|
}
|
||||||
|
|
||||||
# Boolean type values that render to True
|
# Boolean type values that render to True
|
||||||
@@ -385,7 +388,7 @@ def test_lookup_multi(photosdb_places):
|
|||||||
lookup_str = re.match(r"\{([^\\,}]+)\}", subst).group(1)
|
lookup_str = re.match(r"\{([^\\,}]+)\}", subst).group(1)
|
||||||
if subst in ["{exiftool}", "{photo}", "{function}"]:
|
if subst in ["{exiftool}", "{photo}", "{function}"]:
|
||||||
continue
|
continue
|
||||||
lookup = template.get_template_value_multi(lookup_str, path_sep=os.path.sep)
|
lookup = template.get_template_value_multi(lookup_str, path_sep=os.path.sep, default=[])
|
||||||
assert isinstance(lookup, list)
|
assert isinstance(lookup, list)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user