Theme (#664)
* Initial theme manager, not yet done * Added rich_theme_manager * Updated rich-theme-manager * Switched to rich_theme_manager for theme management * Updated dependencies * Added rich paging to subtopic help * Fixed clone to clone only styles specified in cloned theme * Added placeholder for help colors * Updated config dir, help methods
This commit is contained in:
623
README.md
623
README.md
@@ -570,7 +570,6 @@ Another example: if you had `exiftool` installed and wanted to wipe all metadata
|
||||
|
||||
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
|
||||
|
||||
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!):
|
||||
@@ -605,9 +604,14 @@ Here's a comprehensive use case from an actual osxphotos user that integrates ma
|
||||
|
||||
`osxphotos export ~/Desktop/folder for exported videos/ --keyword Quik --only-movies --db /path to my.photoslibrary --touch-file --finder-tag-keywords --person-keyword --xattr-template findercomment "{title}{title?{descr?{newline},},}{descr}" --exiftool-merge-keywords --exiftool-merge-persons --exiftool --strip`
|
||||
|
||||
#### Color Themes
|
||||
|
||||
Some osxphotos commands such as export use color themes to colorize the output to make it more legible. The theme may be specified with the `--theme` option. For example: `osxphotos export /path/to/export --verbose --theme dark` uses a theme suited for dark terminals. If you don't specify the color theme, osxphotos will select a default theme based on the current terminal settings. You can also specify your own default theme. See `osxphotos help theme` for more information on themes and for commands to help manage themes. Themes are defined in `.theme` files in the `~/.osxphotos/themes` directory and use style specifications compatible with the [rich](https://rich.readthedocs.io/en/stable/style.html) library.
|
||||
|
||||
#### Conclusion
|
||||
|
||||
osxphotos is very flexible. If you merely want to backup your Photos library, then spending a few minutes to understand the `--directory` option is likely all you need and you can be up and running in minutes. However, if you have a more complex workflow, osxphotos likely provides options to implement your workflow. This tutorial does not attempt to cover every option offered by osxphotos but hopefully it provides a good understanding of what kinds of things are possible and where to explore if you want to learn more.<!-- OSXPHOTOS-TUTORIAL:END -->
|
||||
osxphotos is very flexible. If you merely want to backup your Photos library, then spending a few minutes to understand the `--directory` option is likely all you need and you can be up and running in minutes. However, if you have a more complex workflow, osxphotos likely provides options to implement your workflow. This tutorial does not attempt to cover every option offered by osxphotos but hopefully it provides a good understanding of what kinds of things are possible and where to explore if you want to learn more.
|
||||
<!-- OSXPHOTOS-TUTORIAL:END -->
|
||||
|
||||
### Command line reference: export
|
||||
|
||||
@@ -1245,19 +1249,19 @@ Options:
|
||||
'light' depending on system dark mode setting.
|
||||
-h, --help Show this message and exit.
|
||||
|
||||
** Export **
|
||||
Export
|
||||
|
||||
When exporting photos, osxphotos creates a database in the top-level export
|
||||
folder called '.osxphotos_export.db'. This database preserves state information
|
||||
used for determining which files need to be updated when run with --update. It
|
||||
is recommended that if you later move the export folder tree you also move the
|
||||
database file.
|
||||
folder called '.osxphotos_export.db'. This database preserves state
|
||||
information used for determining which files need to be updated when run with
|
||||
--update. It is recommended that if you later move the export folder tree you
|
||||
also move the database file.
|
||||
|
||||
The --update option will only copy new or updated files from the library to the
|
||||
export folder. If a file is changed in the export folder (for example, you
|
||||
edited the exported image), osxphotos will detect this as a difference and re-
|
||||
export the original image from the library thus overwriting the changes. If
|
||||
using --update, the exported library should be treated as a backup, not a
|
||||
The --update option will only copy new or updated files from the library to
|
||||
the export folder. If a file is changed in the export folder (for example,
|
||||
you edited the exported image), osxphotos will detect this as a difference and
|
||||
re-export the original image from the library thus overwriting the changes.
|
||||
If using --update, the exported library should be treated as a backup, not a
|
||||
working copy where you intend to make changes. If you do edit or process the
|
||||
exported files and do not want them to be overwritten withsubsequent --update,
|
||||
use --ignore-signature which will match filename but not file signature when
|
||||
@@ -1269,34 +1273,37 @@ are reported in the total photos exported.
|
||||
|
||||
Implementation note: To determine which files need to be updated, osxphotos
|
||||
stores file signature information in the '.osxphotos_export.db' database. The
|
||||
signature includes size, modification time, and filename. In order to minimize
|
||||
run time, --update does not do a full comparison (diff) of the files nor does it
|
||||
compare hashes of the files. In normal usage, this is sufficient for updating
|
||||
the library. You can always run export without the --update option to re-export
|
||||
the entire library thus rebuilding the '.osxphotos_export.db' database.
|
||||
signature includes size, modification time, and filename. In order to
|
||||
minimize run time, --update does not do a full comparison (diff) of the files
|
||||
nor does it compare hashes of the files. In normal usage, this is sufficient
|
||||
for updating the library. You can always run export without the --update
|
||||
option to re-export the entire library thus rebuilding the
|
||||
'.osxphotos_export.db' database.
|
||||
|
||||
|
||||
** Extended Attributes **
|
||||
Extended Attributes
|
||||
|
||||
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
|
||||
'-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.
|
||||
'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)
|
||||
Attribute Description
|
||||
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)
|
||||
@@ -1305,69 +1312,70 @@ copyright The copyright owner of the file contents. A string.
|
||||
creator Application used to create the document content (for example
|
||||
“Word”, “Pages”, and so on). A string.
|
||||
(com.apple.metadata:kMDItemCreator)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
(_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)
|
||||
participants The list of people who are visible in an image or movie or
|
||||
written about in a document. A list of strings.
|
||||
(com.apple.metadata:kMDItemParticipants)
|
||||
projects The list of projects that this file is part of. For example, if
|
||||
you were working on a movie all of the files could be marked as
|
||||
belonging to the project “My Movie”. A list of strings.
|
||||
(com.apple.metadata:kMDItemProjects)
|
||||
projects The list of projects that this file is part of. For example,
|
||||
if you were working on a movie all of the files could be
|
||||
marked as belonging to the project “My Movie”. A list of
|
||||
strings. (com.apple.metadata:kMDItemProjects)
|
||||
rating User rating of this item. For example, the stars rating of an
|
||||
iTunes track. An integer.
|
||||
(com.apple.metadata:kMDItemStarRating)
|
||||
subject Subject of the this item. A string.
|
||||
(com.apple.metadata:kMDItemSubject)
|
||||
title The title of the file. For example, this could be the title of
|
||||
a document, the name of a song, or the subject of an email
|
||||
title The title of the file. For example, this could be the title
|
||||
of a document, the name of a song, or the subject of an email
|
||||
message. A string. (com.apple.metadata:kMDItemTitle)
|
||||
version The version number of this file. A string.
|
||||
(com.apple.metadata:kMDItemVersion)
|
||||
|
||||
For additional information on extended attributes see: https://developer.apple.c
|
||||
om/documentation/coreservices/file_metadata/mditem/common_metadata_attribute_key
|
||||
s
|
||||
For additional information on extended attributes see: https://developer.apple
|
||||
.com/documentation/coreservices/file_metadata/mditem/common_metadata_attribute
|
||||
_keys
|
||||
|
||||
|
||||
** Templating System **
|
||||
Templating System
|
||||
|
||||
The templating system converts one or template statements, written in osxphotos
|
||||
metadata templating language, to one or more rendered values using information
|
||||
from the photo being processed.
|
||||
The templating system converts one or template statements, written in
|
||||
osxphotos metadata templating language, to one or more rendered values using
|
||||
information from the photo being processed.
|
||||
|
||||
In its simplest form, a template statement has the form: "{template_field}", for
|
||||
example "{title}" which would resolve to the title of the photo.
|
||||
In its simplest form, a template statement has the form: "{template_field}",
|
||||
for example "{title}" which would resolve to the title of the photo.
|
||||
|
||||
Template statements may contain one or more modifiers. The full syntax is:
|
||||
|
||||
"pretext{delim+template_field:subfield|filter(path_sep)[find,replace]
|
||||
conditional?bool_value,default}posttext"
|
||||
|
||||
Template statements are white-space sensitive meaning that white space (spaces,
|
||||
tabs) changes the meaning of the template statement.
|
||||
Template statements are white-space sensitive meaning that white space
|
||||
(spaces, tabs) changes the meaning of the template statement.
|
||||
|
||||
pretext and posttext are free form text. For example, if a photo has title "My
|
||||
Photo Title". the template statement "The title of the photo is {title}",
|
||||
pretext and posttext are free form text. For example, if a photo has title
|
||||
"My Photo Title" the template statement "The title of the photo is {title}",
|
||||
resolves to "The title of the photo is My Photo Title". The pretext in this
|
||||
example is "The title if the photo is " and the template_field is {title}.
|
||||
|
||||
delim: optional delimiter string to use when expanding multi-valued template
|
||||
values in-place
|
||||
|
||||
+: If present before template name, expands the template in place. If delim not
|
||||
provided, values are joined with no delimiter.
|
||||
+: If present before template name, expands the template in place. If delim
|
||||
not provided, values are joined with no delimiter.
|
||||
|
||||
e.g. if Photo keywords are ["foo","bar"]:
|
||||
|
||||
@@ -1382,9 +1390,9 @@ full list of template fields.
|
||||
:subfield: Some templates have sub-fields, For example, {exiftool:IPTC:Make};
|
||||
the template_field is exiftool and the sub-field is IPTC:Make.
|
||||
|
||||
|filter: You may optionally append one or more filter commands to the end of the
|
||||
template field using the vertical pipe ('|') symbol. Filters may be combined,
|
||||
separated by '|' as in: {keyword|capitalize|parens}.
|
||||
|filter: You may optionally append one or more filter commands to the end of
|
||||
the template field using the vertical pipe ('|') symbol. Filters may be
|
||||
combined, separated by '|' as in: {keyword|capitalize|parens}.
|
||||
|
||||
Valid filters are:
|
||||
|
||||
@@ -1398,11 +1406,11 @@ Valid filters are:
|
||||
• braces: Enclose value in curly braces, e.g. 'value => '{value}'.
|
||||
• parens: Enclose value in parentheses, 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.
|
||||
• 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:/path/to/file.py::function_name'. See example at https://github.c
|
||||
om/RhetTbull/osxphotos/blob/master/examples/template_filter.py
|
||||
|
||||
e.g. if Photo keywords are ["FOO","bar"]:
|
||||
|
||||
@@ -1424,12 +1432,12 @@ e.g. If Photo is in Album1 in Folder1:
|
||||
• "{folder_album(>)}" renders to ["Folder1>Album1"]
|
||||
• "{folder_album()}" renders to ["Folder1Album1"]
|
||||
|
||||
[find,replace]: optional text replacement to perform on rendered template value.
|
||||
For example, to replace "/" in an album name, you could use the template
|
||||
"{album[/,-]}". Multiple replacements can be made by appending "|" and adding
|
||||
another find|replace pair. e.g. to replace both "/" and ":" in album name:
|
||||
"{album[/,-|:,-]}". find/replace pairs are not limited to single characters.
|
||||
The "|" character cannot be used in a find/replace pair.
|
||||
[find,replace]: optional text replacement to perform on rendered template
|
||||
value. For example, to replace "/" in an album name, you could use the
|
||||
template "{album[/,-]}". Multiple replacements can be made by appending "|"
|
||||
and adding another find|replace pair. e.g. to replace both "/" and ":" in
|
||||
album name: "{album[/,-|:,-]}". find/replace pairs are not limited to single
|
||||
characters. The "|" character cannot be used in a find/replace pair.
|
||||
|
||||
conditional: optional conditional expression that is evaluated as boolean
|
||||
(True/False) for use with the ?bool_value modifier. Conditional expressions
|
||||
@@ -1450,9 +1458,9 @@ required if you use a conditional expression. Valid comparison operators are:
|
||||
• !=: template field does not equal value
|
||||
|
||||
The value part of the conditional expression is treated as a bare (unquoted)
|
||||
word/phrase. Multiple values may be separated by '|' (the pipe symbol). value
|
||||
is itself a template statement so you can use one or more template fields in
|
||||
value which will be resolved before the comparison occurs.
|
||||
word/phrase. Multiple values may be separated by '|' (the pipe symbol).
|
||||
value is itself a template statement so you can use one or more template
|
||||
fields in value which will be resolved before the comparison occurs.
|
||||
|
||||
For example:
|
||||
|
||||
@@ -1460,8 +1468,8 @@ For example:
|
||||
not match keyword 'BeachDay'.
|
||||
• {keyword contains Beach} resolves to True if any keyword contains the word
|
||||
'Beach' so it would match both 'Beach' and 'BeachDay'.
|
||||
• {photo.score.overall > 0.7} resolves to True if the photo's overall aesthetic
|
||||
score is greater than 0.7.
|
||||
• {photo.score.overall > 0.7} resolves to True if the photo's overall
|
||||
aesthetic score is greater than 0.7.
|
||||
• {keyword|lower contains beach} uses the lower case filter to do
|
||||
case-insensitive matching to match any keyword that contains the word
|
||||
'beach'.
|
||||
@@ -1475,24 +1483,25 @@ export command's --directory option:
|
||||
--directory "{keyword|lower matches
|
||||
travel|vacation?Travel-Photos,Not-Travel-Photos}"
|
||||
|
||||
This exports any photo that has keywords 'travel' or 'vacation' into a directory
|
||||
'Travel-Photos' and all other photos into directory 'Not-Travel-Photos'.
|
||||
This exports any photo that has keywords 'travel' or 'vacation' into a
|
||||
directory 'Travel-Photos' and all other photos into directory
|
||||
'Not-Travel-Photos'.
|
||||
|
||||
This can be used to rename files as well, for example: --filename
|
||||
"{favorite?Favorite-{original_name},{original_name}}"
|
||||
|
||||
This renames any photo that is a favorite as 'Favorite-ImageName.jpg' (where
|
||||
'ImageName.jpg' is the original name of the photo) and all other photos with the
|
||||
unmodified original name.
|
||||
'ImageName.jpg' is the original name of the photo) and all other photos with
|
||||
the unmodified original name.
|
||||
|
||||
?bool_value: Template fields may be evaluated as boolean (True/False) by
|
||||
appending "?" after the field name (and following "(path_sep)" or
|
||||
"[find/replace]". If a field is True (e.g. photo is HDR and field is "{hdr}")
|
||||
or has any value, the value following the "?" will be used to render the
|
||||
template instead of the actual field value. If the template field evaluates to
|
||||
False (e.g. in above example, photo is not HDR) or has no value (e.g. photo has
|
||||
no title and field is "{title}") then the default value following a "," will be
|
||||
used.
|
||||
template instead of the actual field value. If the template field evaluates
|
||||
to False (e.g. in above example, photo is not HDR) or has no value (e.g. photo
|
||||
has no title and field is "{title}") then the default value following a ","
|
||||
will be used.
|
||||
|
||||
e.g. if photo is an HDR image,
|
||||
|
||||
@@ -1502,10 +1511,10 @@ and if it is not an HDR image,
|
||||
|
||||
• "{hdr?ISHDR,NOTHDR}" renders to "NOTHDR"
|
||||
|
||||
,default: optional default value to use if the template name has no value. This
|
||||
modifier is also used for the value if False for boolean-type fields (see above)
|
||||
as well as to hold a sub-template for values like {created.strftime}. If no
|
||||
default value provided, "_" is used.
|
||||
,default: optional default value to use if the template name has no value.
|
||||
This modifier is also used for the value if False for boolean-type fields (see
|
||||
above) as well as to hold a sub-template for values like {created.strftime}.
|
||||
If no default value provided, "_" is used.
|
||||
|
||||
e.g., if photo has no title set,
|
||||
|
||||
@@ -1520,12 +1529,12 @@ e.g., if photo date is 4 February 2020, 19:07:38,
|
||||
• "{created.strftime,%Y-%m-%d-%H%M%S}" renders to "2020-02-04-190738"
|
||||
|
||||
Some template fields such as "{media_type}" use the default value to allow
|
||||
customization of the output. For example, "{media_type}" resolves to the special
|
||||
media type of the photo such as panorama or selfie. You may use the default
|
||||
value to override these in form:
|
||||
"{media_type,video=vidéo;time_lapse=vidéo_accélérée}". In this example, if photo
|
||||
was a time_lapse photo, media_type would resolve to vidéo_accélérée instead of
|
||||
time_lapse.
|
||||
customization of the output. For example, "{media_type}" resolves to the
|
||||
special media type of the photo such as panorama or selfie. You may use the
|
||||
default value to override these in form:
|
||||
"{media_type,video=vidéo;time_lapse=vidéo_accélérée}". In this example, if
|
||||
photo was a time_lapse photo, media_type would resolve to vidéo_accélérée
|
||||
instead of time_lapse.
|
||||
|
||||
Either or both bool_value or default (False value) may be empty which would
|
||||
result in empty string "" when rendered.
|
||||
@@ -1539,23 +1548,24 @@ e.g. "{created.year}/{openbrace}{title}{closebrace}" would result in
|
||||
With the --directory and --filename options you may specify a template for the
|
||||
export directory or filename, respectively. The directory will be appended to
|
||||
the export path specified in the export DEST argument to export. For example,
|
||||
if template is '{created.year}/{created.month}', and export destination DEST is
|
||||
'/Users/maria/Pictures/export', the actual export directory for a photo would be
|
||||
'/Users/maria/Pictures/export/2020/March' if the photo was created in March
|
||||
2020.
|
||||
if template is '{created.year}/{created.month}', and export destination DEST
|
||||
is '/Users/maria/Pictures/export', the actual export directory for a photo
|
||||
would be '/Users/maria/Pictures/export/2020/March' if the photo was created in
|
||||
March 2020.
|
||||
|
||||
The templating system may also be used with the --keyword-template option to set
|
||||
keywords on export (with --exiftool or --sidecar), for example, to set a new
|
||||
keyword in format 'folder/subfolder/album' to preserve the folder/album
|
||||
The templating system may also be used with the --keyword-template option to
|
||||
set keywords on export (with --exiftool or --sidecar), for example, to set a
|
||||
new keyword in format 'folder/subfolder/album' to preserve the folder/album
|
||||
structure, you can use --keyword-template "{folder_album}" or in the
|
||||
'folder>subfolder>album' format used in Lightroom Classic, --keyword-template
|
||||
"{folder_album(>)}".
|
||||
|
||||
In the template, valid template substitutions will be replaced by the
|
||||
corresponding value from the table below. Invalid substitutions will result in
|
||||
a an error and the script will abort.
|
||||
corresponding value from the table below. Invalid substitutions will result
|
||||
in a an error and the script will abort.
|
||||
|
||||
** Template Substitutions **
|
||||
|
||||
Template Substitutions
|
||||
|
||||
Substitution Description
|
||||
{name} Current filename of the photo
|
||||
@@ -1568,100 +1578,105 @@ Substitution Description
|
||||
slow_mo, screenshot, portrait, live_photo,
|
||||
burst, photo, video. Defaults to 'photo' or
|
||||
'video' if no special type. Customize one or
|
||||
more media types using format: '{media_type,vi
|
||||
deo=vidéo;time_lapse=vidéo_accélérée}'
|
||||
{photo_or_video} 'photo' or 'video' depending on what type the
|
||||
image is. To customize, use default value as
|
||||
in '{photo_or_video,photo=fotos;video=videos}'
|
||||
{hdr} Photo is HDR?; True/False value, use in format
|
||||
'{hdr?VALUE_IF_TRUE,VALUE_IF_FALSE}'
|
||||
more media types using format: '{media_type,
|
||||
video=vidéo;time_lapse=vidéo_accélérée}'
|
||||
{photo_or_video} 'photo' or 'video' depending on what type
|
||||
the image is. To customize, use default
|
||||
value as in
|
||||
'{photo_or_video,photo=fotos;video=videos}'
|
||||
{hdr} Photo is HDR?; True/False value, use in
|
||||
format '{hdr?VALUE_IF_TRUE,VALUE_IF_FALSE}'
|
||||
{edited} True if photo has been edited (has
|
||||
adjustments), otherwise False; use in format
|
||||
'{edited?VALUE_IF_TRUE,VALUE_IF_FALSE}'
|
||||
{edited_version} True if template is being rendered for the
|
||||
edited version of a photo, otherwise False.
|
||||
{favorite} Photo has been marked as favorite?; True/False
|
||||
value, use in format
|
||||
{favorite} Photo has been marked as favorite?;
|
||||
True/False value, use in format
|
||||
'{favorite?VALUE_IF_TRUE,VALUE_IF_FALSE}'
|
||||
{created.date} Photo's creation date in ISO format, e.g.
|
||||
'2020-03-22'
|
||||
{created.year} 4-digit year of photo creation time
|
||||
{created.yy} 2-digit year of photo creation time
|
||||
{created.mm} 2-digit month of the photo creation time (zero
|
||||
padded)
|
||||
{created.mm} 2-digit month of the photo creation time
|
||||
(zero padded)
|
||||
{created.month} Month name in user's locale of the photo
|
||||
creation time
|
||||
{created.mon} Month abbreviation in the user's locale of the
|
||||
photo creation time
|
||||
{created.mon} Month abbreviation in the user's locale of
|
||||
the photo creation time
|
||||
{created.dd} 2-digit day of the month (zero padded) of
|
||||
photo creation time
|
||||
{created.dow} Day of week in user's locale of the photo
|
||||
creation time
|
||||
{created.doy} 3-digit day of year (e.g Julian day) of photo
|
||||
creation time, starting from 1 (zero padded)
|
||||
{created.doy} 3-digit day of year (e.g Julian day) of
|
||||
photo creation time, starting from 1 (zero
|
||||
padded)
|
||||
{created.hour} 2-digit hour of the photo creation time
|
||||
{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
|
||||
{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'; 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.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
|
||||
(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
|
||||
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
|
||||
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.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.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,TEMPLATE} where TEMPLATE
|
||||
is a valid strftime template, e.g.
|
||||
{modified.strftime,%Y-%U} would result in
|
||||
year-week number of year: '2020-23'. If used
|
||||
with no template will return null value. Uses
|
||||
creation date if photo is not modified. See
|
||||
https://strftime.org/ for help on strftime
|
||||
templates.
|
||||
{today.date} Current date in iso format, e.g. '2020-03-22'
|
||||
with no template will return null value.
|
||||
Uses creation date if photo is not modified.
|
||||
See https://strftime.org/ for help on
|
||||
strftime templates.
|
||||
{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
|
||||
{today.mm} 2-digit month of the current date (zero
|
||||
padded)
|
||||
{today.month} Month name in user's locale of the current
|
||||
date
|
||||
{today.mon} Month abbreviation in the user's locale of the
|
||||
current date
|
||||
{today.mon} Month abbreviation in the user's locale of
|
||||
the current date
|
||||
{today.dd} 2-digit day of the month (zero padded) of
|
||||
current date
|
||||
{today.dow} Day of week in user's locale of the current
|
||||
@@ -1671,10 +1686,10 @@ Substitution Description
|
||||
{today.hour} 2-digit hour of the current date
|
||||
{today.min} 2-digit minute of the current date
|
||||
{today.sec} 2-digit second of the current date
|
||||
{today.strftime} Apply strftime template to current date/time.
|
||||
Should be used in form
|
||||
{today.strftime,TEMPLATE} where TEMPLATE is a
|
||||
valid strftime template, e.g.
|
||||
{today.strftime} Apply strftime template to current
|
||||
date/time. Should be used in form
|
||||
{today.strftime,TEMPLATE} where TEMPLATE is
|
||||
a valid strftime template, e.g.
|
||||
{today.strftime,%Y-%U} would result in year-
|
||||
week number of year: '2020-23'. If used with
|
||||
no template will return null value. See
|
||||
@@ -1682,22 +1697,22 @@ Substitution Description
|
||||
templates.
|
||||
{place.name} Place name from the photo's reverse
|
||||
geolocation data, as displayed in Photos
|
||||
{place.country_code} The ISO country code from the photo's reverse
|
||||
geolocation data
|
||||
{place.country_code} The ISO country code from the photo's
|
||||
reverse geolocation data
|
||||
{place.name.country} Country name from the photo's reverse
|
||||
geolocation data
|
||||
{place.name.state_province} State or province name from the photo's
|
||||
reverse geolocation data
|
||||
{place.name.city} City or locality name from the photo's reverse
|
||||
{place.name.city} City or locality name from the photo's
|
||||
reverse geolocation data
|
||||
{place.name.area_of_interest} Area of interest name (e.g. landmark or
|
||||
public place) from the photo's reverse
|
||||
geolocation data
|
||||
{place.name.area_of_interest} Area of interest name (e.g. landmark or public
|
||||
place) from the photo's reverse geolocation
|
||||
data
|
||||
{place.address} Postal address from the photo's reverse
|
||||
geolocation data, e.g. '2007 18th St NW,
|
||||
Washington, DC 20009, United States'
|
||||
{place.address.street} Street part of the postal address, e.g. '2007
|
||||
18th St NW'
|
||||
{place.address.street} Street part of the postal address, e.g.
|
||||
'2007 18th St NW'
|
||||
{place.address.city} City part of the postal address, e.g.
|
||||
'Washington'
|
||||
{place.address.state_province} State/province part of the postal address,
|
||||
@@ -1710,8 +1725,8 @@ Substitution Description
|
||||
'US'
|
||||
{searchinfo.season} Season of the year associated with a photo,
|
||||
e.g. 'Summer'; (Photos 5+ only, applied
|
||||
automatically by Photos' image categorization
|
||||
algorithms).
|
||||
automatically by Photos' image
|
||||
categorization algorithms).
|
||||
{exif.camera_make} Camera make from original photo's EXIF
|
||||
information as imported by Photos, e.g.
|
||||
'Apple'
|
||||
@@ -1721,59 +1736,60 @@ Substitution Description
|
||||
{exif.lens_model} Lens model from original photo's EXIF
|
||||
information as imported by Photos, e.g.
|
||||
'iPhone 6s back camera 4.15mm f/2.2'
|
||||
{uuid} Photo's internal universally unique identifier
|
||||
(UUID) for the photo, a 36-character string
|
||||
unique to the photo, e.g.
|
||||
'128FB4C6-0B16-4E7D-9108-FB2E90DA1546'
|
||||
{uuid} Photo's internal universally unique
|
||||
identifier (UUID) for the photo, a
|
||||
36-character string unique to the photo,
|
||||
e.g. '128FB4C6-0B16-4E7D-9108-FB2E90DA1546'
|
||||
{id} A unique number for the photo based on its
|
||||
primary key in the Photos database. A
|
||||
sequential integer, e.g. 1, 2, 3...etc. Each
|
||||
asset associated with a photo (e.g. an image
|
||||
and Live Photo preview) will share the same
|
||||
id. May be formatted using a python string
|
||||
format code. For example, to format as a
|
||||
5-digit integer and pad with zeros, use
|
||||
sequential integer, e.g. 1, 2, 3...etc.
|
||||
Each asset associated with a photo (e.g. an
|
||||
image and Live Photo preview) will share the
|
||||
same id. May be formatted using a python
|
||||
string format code. For example, to format
|
||||
as a 5-digit integer and pad with zeros, use
|
||||
'{id:05d}' which results in 00001, 00002,
|
||||
00003...etc.
|
||||
{album_seq} An integer, starting at 0, indicating the
|
||||
photo's index (sequence) in the containing
|
||||
album. Only valid when used in a '--filename'
|
||||
template and only when '{album}' or
|
||||
'{folder_album}' is used in the '--directory'
|
||||
template. For example '--directory
|
||||
"{folder_album}" --filename
|
||||
album. Only valid when used in a '--
|
||||
filename' template and only when '{album}'
|
||||
or '{folder_album}' is used in the '--
|
||||
directory' template. For example '--
|
||||
directory "{folder_album}" --filename
|
||||
"{album_seq}_{original_name}"'. To start
|
||||
counting at a value other than 0, append
|
||||
append a period and the starting value to the
|
||||
field name. For example, to start counting at
|
||||
1 instead of 0: '{album_seq.1}'. May be
|
||||
formatted using a python string format code.
|
||||
For example, to format as a 5-digit integer
|
||||
and pad with zeros, use '{album_seq:05d}'
|
||||
which results in 00000, 00001, 00002...etc.
|
||||
This may result in incorrect sequences if you
|
||||
have duplicate albums with the same name; see
|
||||
also '{folder_album_seq}'.
|
||||
append a period and the starting value to
|
||||
the field name. For example, to start
|
||||
counting at 1 instead of 0: '{album_seq.1}'.
|
||||
May be formatted using a python string
|
||||
format code. For example, to format as a
|
||||
5-digit integer and pad with zeros, use
|
||||
'{album_seq:05d}' which results in 00000,
|
||||
00001, 00002...etc. This may result in
|
||||
incorrect sequences if you have duplicate
|
||||
albums with the same name; see also
|
||||
'{folder_album_seq}'.
|
||||
{folder_album_seq} An integer, starting at 0, indicating the
|
||||
photo's index (sequence) in the containing
|
||||
album and folder path. Only valid when used in
|
||||
a '--filename' template and only when
|
||||
'{folder_album}' is used in the '--directory'
|
||||
template. For example '--directory
|
||||
"{folder_album}" --filename
|
||||
album and folder path. Only valid when used
|
||||
in a '--filename' template and only when
|
||||
'{folder_album}' is used in the '--
|
||||
directory' template. For example '--
|
||||
directory "{folder_album}" --filename
|
||||
"{folder_album_seq}_{original_name}"'. To
|
||||
start counting at a value other than 0, append
|
||||
append a period and the starting value to the
|
||||
field name. For example, to start counting at
|
||||
1 instead of 0: '{folder_album_seq.1}' May be
|
||||
formatted using a python string format code.
|
||||
For example, to format as a 5-digit integer
|
||||
and pad with zeros, use
|
||||
'{folder_album_seq:05d}' which results in
|
||||
00000, 00001, 00002...etc. This may result in
|
||||
incorrect sequences if you have duplicate
|
||||
albums with the same name in the same folder;
|
||||
see also '{album_seq}'.
|
||||
start counting at a value other than 0,
|
||||
append append a period and the starting
|
||||
value to the field name. For example, to
|
||||
start counting at 1 instead of 0:
|
||||
'{folder_album_seq.1}' May be formatted
|
||||
using a python string format code. For
|
||||
example, to format as a 5-digit integer and
|
||||
pad with zeros, use '{folder_album_seq:05d}'
|
||||
which results in 00000, 00001, 00002...etc.
|
||||
This may result in incorrect sequences if
|
||||
you have duplicate albums with the same name
|
||||
in the same folder; see also '{album_seq}'.
|
||||
{comma} A comma: ','
|
||||
{semicolon} A semicolon: ';'
|
||||
{questionmark} A question mark: '?'
|
||||
@@ -1791,8 +1807,8 @@ Substitution Description
|
||||
{osxphotos_version} The osxphotos version, e.g. '0.47.6'
|
||||
{osxphotos_cmd_line} The full command line used to run osxphotos
|
||||
|
||||
The following substitutions may result in multiple values. Thus if specified for
|
||||
--directory these could result in multiple copies of a photo being being
|
||||
The following substitutions may result in multiple values. Thus if specified
|
||||
for --directory these could result in multiple copies of a photo being being
|
||||
exported, one to each directory. For example: --directory
|
||||
'{created.year}/{album}' could result in the same photo being exported to each
|
||||
of the following directories if the photos were created in 2019 and were in
|
||||
@@ -1805,11 +1821,12 @@ Substitution Description
|
||||
enclosing folder
|
||||
{project} Project(s) photo is contained in (such as greeting
|
||||
cards, calendars, slideshows)
|
||||
{album_project} Album(s) and project(s) photo is contained in; treats
|
||||
projects as regular albums
|
||||
{album_project} Album(s) and project(s) photo is contained in;
|
||||
treats projects as regular albums
|
||||
{folder_album_project} Folder path + album (includes projects as albums)
|
||||
photo is contained in. e.g. 'Folder/Subfolder/Album'
|
||||
or just 'Album' if no enclosing folder
|
||||
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
|
||||
@@ -1819,17 +1836,17 @@ Substitution Description
|
||||
{keyword} which refers to the user-defined
|
||||
keywords/tags applied in Photos.
|
||||
{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)
|
||||
{comment} Comment(s) on shared Photos; format is 'Person
|
||||
name: comment text' (Photos 5+ only)
|
||||
{exiftool} Format: '{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.
|
||||
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
|
||||
@@ -1838,22 +1855,24 @@ Substitution Description
|
||||
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
|
||||
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.
|
||||
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.
|
||||
{detected_text} List of text strings found in the image after
|
||||
performing text detection. Using '{detected_text}'
|
||||
will cause osxphotos to perform text detection on
|
||||
@@ -1862,30 +1881,33 @@ Substitution Description
|
||||
results for each photo will be cached in the export
|
||||
database so that future exports with '--update' do
|
||||
not need to reprocess each photo. You may pass a
|
||||
confidence threshold value between 0.0 and 1.0 after
|
||||
a colon as in '{detected_text:0.5}'; The default
|
||||
confidence threshold is 0.75. '{detected_text}' works
|
||||
only on macOS Catalina (10.15) or later. Note: this
|
||||
feature is not the same thing as Live Text in macOS
|
||||
Monterey, which osxphotos does not yet support.
|
||||
confidence threshold value between 0.0 and 1.0
|
||||
after a colon as in '{detected_text:0.5}'; The
|
||||
default confidence threshold is 0.75.
|
||||
'{detected_text}' works only on macOS Catalina
|
||||
(10.15) or later. Note: this feature is not the
|
||||
same thing as Live Text in macOS Monterey, which
|
||||
osxphotos does not yet support.
|
||||
{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.
|
||||
shell, e.g. My file.jpeg => 'My file.jpeg'; only
|
||||
adds quotes if needed.
|
||||
{strip} Use in form '{strip,TEMPLATE}'; strips whitespace
|
||||
from begining and end of rendered TEMPLATE value(s).
|
||||
from begining and end of rendered TEMPLATE
|
||||
value(s).
|
||||
{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_name' is the name of the function to
|
||||
call. The function will be passed the PhotoInfo
|
||||
object for the photo. See https://github.com/RhetTb
|
||||
ull/osxphotos/blob/master/examples/template_functio
|
||||
n.py for an example of how to implement a template
|
||||
function.
|
||||
|
||||
The following substitutions are file or directory paths. You can access various
|
||||
parts of the path using the following modifiers:
|
||||
The following substitutions are file or directory paths. You can access
|
||||
various parts of the path using the following modifiers:
|
||||
|
||||
{path.parent}: the parent directory
|
||||
{path.name}: the name of the file or final sub-directory
|
||||
@@ -1906,33 +1928,35 @@ Substitution Description
|
||||
{filepath} The full path to the exported file
|
||||
|
||||
|
||||
** Post Command **
|
||||
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:
|
||||
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:
|
||||
|
||||
Category Description
|
||||
exported All exported files
|
||||
new When used with '--update', all newly 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
|
||||
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
|
||||
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_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,
|
||||
@@ -1943,42 +1967,43 @@ 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':
|
||||
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|shell_quote} >> {shell_quote,{export_dir}/exported.txt}"
|
||||
|
||||
In the above command, the 'shell_quote' filter is used to ensure '{filepath}' is
|
||||
properly quoted and the '{shell_quote}' template ensures the constructed path of
|
||||
'{exported_dir}/exported.txt' is properly quoted. If '{filepath}' is 'IMG
|
||||
1234.jpeg' and '{export_dir}' is '/Volumes/Photo Export', the command thus
|
||||
renders to:
|
||||
In the above command, the 'shell_quote' filter is used to ensure '{filepath}'
|
||||
is properly quoted and the '{shell_quote}' template ensures the constructed
|
||||
path of '{exported_dir}/exported.txt' is properly quoted. If '{filepath}' 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.
|
||||
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.
|
||||
|
||||
|
||||
** Post Function **
|
||||
You can run your own python functions on the exported photos for post-processing
|
||||
using the '--post-function' option. '--post-function' is passed the name a
|
||||
python file and the name of the function in the file to call using format
|
||||
'filename.py::function_name'. See the example function at
|
||||
https://github.com/RhetTbull/osxphotos/blob/master/examples/post_function.py You
|
||||
may specify multiple functions to run by repeating the --post-function option.
|
||||
All post functions will be called immediately after export of each photo and
|
||||
immediately before any --post-command commands. Post functions will not be
|
||||
called if the --dry-run flag is set.
|
||||
Post Function
|
||||
|
||||
You can run your own python functions on the exported photos for post-
|
||||
processing using the '--post-function' option. '--post-function' is passed the
|
||||
name a python file and the name of the function in the file to call using
|
||||
format 'filename.py::function_name'. See the example function at
|
||||
https://github.com/RhetTbull/osxphotos/blob/master/examples/post_function.py
|
||||
You may specify multiple functions to run by repeating the --post-function
|
||||
option. All post functions will be called immediately after export of each
|
||||
photo and immediately before any --post-command commands. Post functions will
|
||||
not be called if the --dry-run flag is set.
|
||||
|
||||
|
||||
|
||||
@@ -3481,7 +3506,7 @@ Template statements may contain one or more modifiers. The full syntax is:
|
||||
|
||||
Template statements are white-space sensitive meaning that white space (spaces, tabs) changes the meaning of the template statement.
|
||||
|
||||
`pretext` and `posttext` are free form text. For example, if a photo has title "My Photo Title". the template statement `"The title of the photo is {title}"`, resolves to `"The title of the photo is My Photo Title"`. The `pretext` in this example is `"The title if the photo is "` and the template_field is `{title}`.
|
||||
`pretext` and `posttext` are free form text. For example, if a photo has title "My Photo Title" the template statement `"The title of the photo is {title}"`, resolves to `"The title of the photo is My Photo Title"`. The `pretext` in this example is `"The title if the photo is "` and the template_field is `{title}`.
|
||||
|
||||
|
||||
`delim`: optional delimiter string to use when expanding multi-valued template values in-place
|
||||
|
||||
@@ -6,6 +6,8 @@ import os.path
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
|
||||
APP_NAME = "osxphotos"
|
||||
|
||||
OSXPHOTOS_URL = "https://github.com/RhetTbull/osxphotos"
|
||||
|
||||
# Time delta: add this to Photos times to get unix time
|
||||
|
||||
@@ -24,6 +24,7 @@ from .places import places
|
||||
from .query import query
|
||||
from .repl import repl
|
||||
from .snap_diff import diff, snap
|
||||
from .theme import theme
|
||||
from .tutorial import tutorial
|
||||
from .uuid import uuid
|
||||
|
||||
@@ -77,6 +78,7 @@ for command in [
|
||||
repl,
|
||||
run,
|
||||
snap,
|
||||
theme,
|
||||
tutorial,
|
||||
uninstall,
|
||||
uuid,
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import inspect
|
||||
import os
|
||||
import typing as t
|
||||
from io import StringIO
|
||||
|
||||
import click
|
||||
from rich.console import Console
|
||||
@@ -213,11 +212,9 @@ def rich_click_echo(
|
||||
# otherwise tests fail
|
||||
temp_console = Console()
|
||||
width = temp_console.width if temp_console.is_terminal else 10_000
|
||||
output = StringIO()
|
||||
console = Console(
|
||||
force_terminal=True,
|
||||
theme=theme or get_rich_theme(),
|
||||
file=output,
|
||||
width=width,
|
||||
)
|
||||
if markdown:
|
||||
@@ -227,8 +224,9 @@ def rich_click_echo(
|
||||
global _timestamp
|
||||
if _timestamp:
|
||||
message = time_stamp() + message
|
||||
console.print(message, end=end, highlight=highlight, **kwargs)
|
||||
click.echo(output.getvalue(), **echo_args)
|
||||
with console.capture() as capture:
|
||||
console.print(message, end=end, highlight=highlight, **kwargs)
|
||||
click.echo(capture.get(), **echo_args)
|
||||
|
||||
|
||||
def rich_echo_via_pager(
|
||||
@@ -259,11 +257,9 @@ def rich_echo_via_pager(
|
||||
except TypeError:
|
||||
text_or_generator = [text_or_generator]
|
||||
|
||||
console = _console or Console(theme=theme)
|
||||
console = _console.console or Console(theme=theme)
|
||||
|
||||
color = kwargs.pop("color", None)
|
||||
if color is None:
|
||||
color = bool(console.color_system)
|
||||
color = kwargs.pop("color", True)
|
||||
|
||||
with console.pager(styles=color):
|
||||
for x in text_or_generator:
|
||||
|
||||
@@ -1,19 +1,52 @@
|
||||
"""Support for colorized output for photos_time_warp"""
|
||||
"""Support for colorized output for osxphotos cli using rich"""
|
||||
|
||||
from typing import Optional
|
||||
import pathlib
|
||||
from typing import List, Optional
|
||||
|
||||
import click
|
||||
from rich.style import Style
|
||||
from rich.themes import Theme
|
||||
from rich_theme_manager import Theme, ThemeManager
|
||||
|
||||
from .common import noop
|
||||
from .common import get_config_dir, noop
|
||||
from .darkmode import is_dark_mode
|
||||
|
||||
__all__ = ["get_theme"]
|
||||
DEFAULT_THEME_NAME = "default"
|
||||
|
||||
__all__ = [
|
||||
"get_default_theme",
|
||||
"get_theme",
|
||||
"get_theme_dir",
|
||||
"get_theme_manager",
|
||||
DEFAULT_THEME_NAME,
|
||||
]
|
||||
|
||||
|
||||
THEME_STYLES = [
|
||||
"color",
|
||||
"count",
|
||||
"error",
|
||||
"filename",
|
||||
"filepath",
|
||||
"highlight",
|
||||
"num",
|
||||
"time",
|
||||
"uuid",
|
||||
"warning",
|
||||
"bar.back",
|
||||
"bar.complete",
|
||||
"bar.finished",
|
||||
"bar.pulse",
|
||||
"progress.elapsed",
|
||||
"progress.percentage",
|
||||
"progress.remaining",
|
||||
]
|
||||
|
||||
COLOR_THEMES = {
|
||||
"dark": Theme(
|
||||
{
|
||||
name="dark",
|
||||
description="Dark mode theme",
|
||||
tags=["dark"],
|
||||
styles={
|
||||
# color pallette from https://github.com/dracula/dracula-theme
|
||||
"color": Style(color="rgb(248,248,242)"),
|
||||
"count": Style(color="rgb(139,233,253)"),
|
||||
@@ -32,10 +65,15 @@ COLOR_THEMES = {
|
||||
"progress.elapsed": Style(color="rgb(139,233,253)"),
|
||||
"progress.percentage": Style(color="rgb(255,121,198)"),
|
||||
"progress.remaining": Style(color="rgb(139,233,253)"),
|
||||
}
|
||||
# "headers": Style(color="rgb(165,194,97)"),
|
||||
# "options": Style(color="rgb(255,198,109)"),
|
||||
# "metavar": Style(color="rgb(12,125,157)"),
|
||||
},
|
||||
),
|
||||
"light": Theme(
|
||||
{
|
||||
name="light",
|
||||
description="Light mode theme",
|
||||
styles={
|
||||
"color": Style(color="#000000"),
|
||||
"count": Style(color="#005cc5", bold=True),
|
||||
"error": Style(color="#b31d28", bold=True, underline=True, italic=True),
|
||||
@@ -53,10 +91,16 @@ COLOR_THEMES = {
|
||||
"progress.elapsed": Style(color="#032f62", bold=True),
|
||||
"progress.percentage": Style(color="#6f42c1", bold=True),
|
||||
"progress.remaining": Style(color="#032f62", bold=True),
|
||||
}
|
||||
# "headers": Style(color="rgb(254,212,66)"),
|
||||
# "options": Style(color="rgb(227,98,9)"),
|
||||
# "metavar": Style(color="rgb(111,66,193)"),
|
||||
},
|
||||
),
|
||||
"mono": Theme(
|
||||
{
|
||||
name="mono",
|
||||
description="Monochromatic theme",
|
||||
tags=["mono", "colorblind"],
|
||||
styles={
|
||||
"count": "bold",
|
||||
"error": "reverse italic",
|
||||
"filename": "bold",
|
||||
@@ -73,10 +117,16 @@ COLOR_THEMES = {
|
||||
"progress.elapsed": "",
|
||||
"progress.percentage": "bold",
|
||||
"progress.remaining": "bold",
|
||||
}
|
||||
# "headers": "bold",
|
||||
# "options": "bold",
|
||||
# "metavar": "bold",
|
||||
},
|
||||
),
|
||||
"plain": Theme(
|
||||
{
|
||||
name="plain",
|
||||
description="Plain theme with no colors",
|
||||
tags=["colorblind"],
|
||||
styles={
|
||||
"color": "",
|
||||
"count": "",
|
||||
"error": "",
|
||||
@@ -94,31 +144,51 @@ COLOR_THEMES = {
|
||||
"progress.elapsed": "",
|
||||
"progress.percentage": "",
|
||||
"progress.remaining": "",
|
||||
}
|
||||
# "headers": "",
|
||||
# "options": "",
|
||||
# "metavar": "",
|
||||
},
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def get_theme_dir() -> pathlib.Path:
|
||||
"""Return the theme config dir, creating it if necessary"""
|
||||
theme_dir = get_config_dir() / "themes"
|
||||
if not theme_dir.exists():
|
||||
theme_dir.mkdir()
|
||||
return theme_dir
|
||||
|
||||
|
||||
def get_theme_manager() -> ThemeManager:
|
||||
"""Return theme manager instance"""
|
||||
return ThemeManager(theme_dir=str(get_theme_dir()), themes=COLOR_THEMES.values())
|
||||
|
||||
|
||||
def get_theme(
|
||||
theme_name: Optional[str] = None,
|
||||
theme_file: Optional[str] = None,
|
||||
verbose=None,
|
||||
):
|
||||
"""Get the color theme based on the color flags or load from config file"""
|
||||
if not verbose:
|
||||
verbose = noop
|
||||
# figure out which color theme to use
|
||||
theme_name = theme_name or "default"
|
||||
if theme_name == "default" and theme_file and theme_file.is_file():
|
||||
# load theme from file
|
||||
verbose(f"Loading color theme from {theme_file}")
|
||||
try:
|
||||
theme = Theme.read(theme_file)
|
||||
except Exception as e:
|
||||
raise ValueError(f"Error reading theme file {theme_file}: {e}")
|
||||
elif theme_name == "default":
|
||||
# try to auto-detect dark/light mode
|
||||
theme = COLOR_THEMES["dark"] if is_dark_mode() else COLOR_THEMES["light"]
|
||||
else:
|
||||
theme = COLOR_THEMES[theme_name]
|
||||
return theme
|
||||
"""Get theme by name, or default theme if no name is provided"""
|
||||
|
||||
if theme_name is None:
|
||||
return get_default_theme()
|
||||
|
||||
theme_manager = get_theme_manager()
|
||||
try:
|
||||
return theme_manager.get(theme_name)
|
||||
except ValueError as e:
|
||||
raise click.ClickException(
|
||||
f"Theme '{theme_name}' not found. "
|
||||
f"Available themes: {', '.join(t.name for t in theme_manager.themes)}"
|
||||
) from e
|
||||
|
||||
|
||||
def get_default_theme():
|
||||
"""Get the default color theme"""
|
||||
theme_manager = get_theme_manager()
|
||||
try:
|
||||
return theme_manager.get(DEFAULT_THEME_NAME)
|
||||
except ValueError:
|
||||
return (
|
||||
theme_manager.get("dark") if is_dark_mode() else theme_manager.get("light")
|
||||
)
|
||||
|
||||
@@ -8,6 +8,7 @@ from datetime import datetime
|
||||
import click
|
||||
|
||||
import osxphotos
|
||||
from osxphotos._constants import APP_NAME
|
||||
from osxphotos._version import __version__
|
||||
|
||||
from .param_types import *
|
||||
@@ -33,6 +34,7 @@ __all__ = [
|
||||
"DELETED_OPTIONS",
|
||||
"JSON_OPTION",
|
||||
"QUERY_OPTIONS",
|
||||
"THEME_OPTION",
|
||||
"get_photos_db",
|
||||
"load_uuid_from_file",
|
||||
"noop",
|
||||
@@ -499,6 +501,16 @@ def DEBUG_OPTIONS(f):
|
||||
return f
|
||||
|
||||
|
||||
THEME_OPTION = click.option(
|
||||
"--theme",
|
||||
metavar="THEME",
|
||||
type=click.Choice(["dark", "light", "mono", "plain"], case_sensitive=False),
|
||||
help="Specify the color theme to use for --verbose output. "
|
||||
"Valid themes are 'dark', 'light', 'mono', and 'plain'. "
|
||||
"Defaults to 'dark' or 'light' depending on system dark mode setting.",
|
||||
)
|
||||
|
||||
|
||||
def load_uuid_from_file(filename):
|
||||
"""Load UUIDs from file. Does not validate UUIDs.
|
||||
Format is 1 UUID per line, any line beginning with # is ignored.
|
||||
@@ -524,3 +536,11 @@ def load_uuid_from_file(filename):
|
||||
if len(line) and line[0] != "#":
|
||||
uuid.append(line)
|
||||
return uuid
|
||||
|
||||
|
||||
def get_config_dir() -> pathlib.Path:
|
||||
"""Get the directory where config files are stored."""
|
||||
config_dir = pathlib.Path.home() / ".config" / APP_NAME
|
||||
if not config_dir.is_dir():
|
||||
config_dir.mkdir(parents=True)
|
||||
return config_dir
|
||||
|
||||
@@ -77,6 +77,7 @@ from .common import (
|
||||
OSXPHOTOS_CRASH_LOG,
|
||||
OSXPHOTOS_HIDDEN,
|
||||
QUERY_OPTIONS,
|
||||
THEME_OPTION,
|
||||
get_photos_db,
|
||||
load_uuid_from_file,
|
||||
noop,
|
||||
@@ -642,14 +643,7 @@ from .verbose import get_verbose_console, time_stamp, verbose_print
|
||||
f"Can be specified multiple times. Valid options are: {PROFILE_SORT_KEYS}. "
|
||||
"Default = 'cumulative'.",
|
||||
)
|
||||
@click.option(
|
||||
"--theme",
|
||||
metavar="THEME",
|
||||
type=click.Choice(["dark", "light", "mono", "plain"], case_sensitive=False),
|
||||
help="Specify the color theme to use for --verbose output. "
|
||||
"Valid themes are 'dark', 'light', 'mono', and 'plain'. "
|
||||
"Defaults to 'dark' or 'light' depending on system dark mode setting.",
|
||||
)
|
||||
@THEME_OPTION
|
||||
@DEBUG_OPTIONS
|
||||
@DB_ARGUMENT
|
||||
@click.argument("dest", nargs=1, type=click.Path(exists=True))
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Help text helper class for osxphotos CLI """
|
||||
|
||||
import inspect
|
||||
import io
|
||||
import re
|
||||
import typing as t
|
||||
|
||||
@@ -23,8 +22,12 @@ from osxphotos.phototemplate import (
|
||||
get_template_help,
|
||||
)
|
||||
|
||||
from .click_rich_echo import rich_echo
|
||||
from .click_rich_echo import rich_echo_via_pager
|
||||
from .color_themes import get_theme
|
||||
from .common import OSXPHOTOS_HIDDEN
|
||||
|
||||
HELP_WIDTH = 110
|
||||
HIGHLIGHT_COLOR = "yellow"
|
||||
|
||||
__all__ = [
|
||||
"ExportCommand",
|
||||
@@ -37,8 +40,6 @@ __all__ = [
|
||||
"get_help_msg",
|
||||
]
|
||||
|
||||
HIGHLIGHT_COLOR = "yellow"
|
||||
|
||||
|
||||
def get_help_msg(command):
|
||||
"""get help message for a Click command"""
|
||||
@@ -47,22 +48,50 @@ def get_help_msg(command):
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option(
|
||||
"--width",
|
||||
default=HELP_WIDTH,
|
||||
help="Width of help text",
|
||||
hidden=OSXPHOTOS_HIDDEN,
|
||||
)
|
||||
@click.argument("topic", default=None, required=False, nargs=1)
|
||||
@click.argument("subtopic", default=None, required=False, nargs=1)
|
||||
@click.pass_context
|
||||
def help(ctx, topic, subtopic, **kw):
|
||||
def help(ctx, topic, subtopic, width, **kw):
|
||||
"""Print help; for help on commands: help <command>."""
|
||||
if topic is None:
|
||||
click.echo(ctx.parent.get_help())
|
||||
return
|
||||
|
||||
global HELP_WIDTH
|
||||
HELP_WIDTH = width
|
||||
|
||||
wrap_text_original = click.formatting.wrap_text
|
||||
|
||||
def wrap_text(
|
||||
text: str,
|
||||
width: int = HELP_WIDTH,
|
||||
initial_indent: str = "",
|
||||
subsequent_indent: str = "",
|
||||
preserve_paragraphs: bool = False,
|
||||
) -> str:
|
||||
return wrap_text_original(
|
||||
text,
|
||||
width=width,
|
||||
initial_indent=initial_indent,
|
||||
subsequent_indent=subsequent_indent,
|
||||
preserve_paragraphs=preserve_paragraphs,
|
||||
)
|
||||
|
||||
click.formatting.wrap_text = wrap_text
|
||||
click.wrap_text = wrap_text
|
||||
|
||||
if subtopic:
|
||||
cmd = ctx.obj.group.commands[topic]
|
||||
theme = get_theme("light")
|
||||
rich_echo(
|
||||
rich_echo_via_pager(
|
||||
get_subtopic_help(cmd, ctx, subtopic),
|
||||
theme=theme,
|
||||
width=click.HelpFormatter().width,
|
||||
theme=get_theme(),
|
||||
width=HELP_WIDTH,
|
||||
)
|
||||
return
|
||||
|
||||
@@ -90,7 +119,7 @@ def get_subtopic_help(cmd: click.Command, ctx: click.Context, subtopic: str):
|
||||
options = get_matching_options(cmd, ctx, subtopic)
|
||||
|
||||
# format help text and options
|
||||
formatter = click.HelpFormatter()
|
||||
formatter = click.HelpFormatter(width=HELP_WIDTH)
|
||||
formatter.write(usage_str)
|
||||
formatter.write_paragraph()
|
||||
format_help_text(help_str, formatter)
|
||||
@@ -142,7 +171,7 @@ def format_options_help(
|
||||
str with formatted help
|
||||
|
||||
"""
|
||||
formatter = click.HelpFormatter()
|
||||
formatter = click.HelpFormatter(width=HELP_WIDTH)
|
||||
opt_help = [opt.get_help_record(ctx) for opt in options]
|
||||
if highlight:
|
||||
# convert list of tuples to list of lists
|
||||
@@ -182,11 +211,9 @@ class ExportCommand(click.Command):
|
||||
|
||||
def get_help(self, ctx):
|
||||
help_text = super().get_help(ctx)
|
||||
formatter = click.HelpFormatter()
|
||||
# passed to click.HelpFormatter.write_dl for formatting
|
||||
|
||||
formatter.write("\n\n")
|
||||
formatter.write(rich_text("[bold]** Export **[/bold]", width=formatter.width))
|
||||
formatter = click.HelpFormatter(width=HELP_WIDTH)
|
||||
formatter.write("\n")
|
||||
formatter.write(rich_text("## Export", width=formatter.width, markdown=True))
|
||||
formatter.write("\n")
|
||||
formatter.write_text(
|
||||
"When exporting photos, osxphotos creates a database in the top-level "
|
||||
@@ -221,9 +248,9 @@ class ExportCommand(click.Command):
|
||||
+ "You can always run export without the --update option to re-export the entire library thus "
|
||||
+ f"rebuilding the '{OSXPHOTOS_EXPORT_DB}' database."
|
||||
)
|
||||
formatter.write("\n\n")
|
||||
formatter.write("\n")
|
||||
formatter.write(
|
||||
rich_text("[bold]** Extended Attributes **[/bold]", width=formatter.width)
|
||||
rich_text("## Extended Attributes", width=formatter.width, markdown=True)
|
||||
)
|
||||
formatter.write("\n")
|
||||
formatter.write_text(
|
||||
@@ -244,25 +271,33 @@ The following attributes may be used with '--xattr-template':
|
||||
|
||||
"""
|
||||
)
|
||||
formatter.write_dl(
|
||||
[
|
||||
attr_tuples = [
|
||||
(
|
||||
rich_text("[bold]Attribute[/bold]", width=formatter.width),
|
||||
rich_text("[bold]Description[/bold]", width=formatter.width),
|
||||
),
|
||||
*[
|
||||
(
|
||||
attr,
|
||||
f"{osxmetadata.ATTRIBUTES[attr].help} ({osxmetadata.ATTRIBUTES[attr].constant})",
|
||||
)
|
||||
for attr in EXTENDED_ATTRIBUTE_NAMES
|
||||
]
|
||||
)
|
||||
],
|
||||
]
|
||||
formatter.write_dl(attr_tuples)
|
||||
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("\n")
|
||||
formatter.write(
|
||||
rich_text("[bold]** Templating System **[/bold]", width=formatter.width)
|
||||
rich_text("## Templating System", width=formatter.width, markdown=True)
|
||||
)
|
||||
formatter.write("\n")
|
||||
formatter.write(template_help(width=formatter.width))
|
||||
help_text += formatter.getvalue()
|
||||
help_text += template_help(width=formatter.width)
|
||||
formatter = click.HelpFormatter(width=HELP_WIDTH)
|
||||
|
||||
formatter.write("\n")
|
||||
formatter.write_text(
|
||||
"With the --directory and --filename options you may specify a template for the "
|
||||
@@ -290,12 +325,15 @@ The following attributes may be used with '--xattr-template':
|
||||
)
|
||||
formatter.write("\n")
|
||||
formatter.write(
|
||||
rich_text(
|
||||
"[bold]** Template Substitutions **[/bold]", width=formatter.width
|
||||
)
|
||||
rich_text("## Template Substitutions", width=formatter.width, markdown=True)
|
||||
)
|
||||
formatter.write("\n")
|
||||
templ_tuples = [("Substitution", "Description")]
|
||||
templ_tuples = [
|
||||
(
|
||||
rich_text("[bold]Substitution[/bold]", width=formatter.width),
|
||||
rich_text("[bold]Description[/bold]", width=formatter.width),
|
||||
)
|
||||
]
|
||||
templ_tuples.extend((k, v) for k, v in TEMPLATE_SUBSTITUTIONS.items())
|
||||
formatter.write_dl(templ_tuples)
|
||||
|
||||
@@ -310,7 +348,12 @@ The following attributes may be used with '--xattr-template':
|
||||
+ "2019/Vacation, 2019/Family"
|
||||
)
|
||||
formatter.write("\n")
|
||||
templ_tuples = [("Substitution", "Description")]
|
||||
templ_tuples = [
|
||||
(
|
||||
rich_text("[bold]Substitution[/bold]", width=formatter.width),
|
||||
rich_text("[bold]Description[/bold]", width=formatter.width),
|
||||
)
|
||||
]
|
||||
templ_tuples.extend(
|
||||
(k, v) for k, v in TEMPLATE_SUBSTITUTIONS_MULTI_VALUED.items()
|
||||
)
|
||||
@@ -348,10 +391,11 @@ The following attributes may be used with '--xattr-template':
|
||||
|
||||
formatter.write_dl(templ_tuples)
|
||||
|
||||
formatter.write("\n\n")
|
||||
formatter.write("\n")
|
||||
formatter.write(
|
||||
rich_text("[bold]** Post Command **[/bold]", width=formatter.width)
|
||||
rich_text("## Post Command", width=formatter.width, markdown=True)
|
||||
)
|
||||
formatter.write("\n")
|
||||
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. "
|
||||
@@ -394,10 +438,11 @@ The following attributes may be used with '--xattr-template':
|
||||
+ "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")
|
||||
formatter.write("\n")
|
||||
formatter.write(
|
||||
rich_text("[bold]** Post Function **[/bold]", width=formatter.width)
|
||||
rich_text("## Post Function", width=formatter.width, markdown=True)
|
||||
)
|
||||
formatter.write("\n")
|
||||
formatter.write_text(
|
||||
"You can run your own python functions on the exported photos for post-processing "
|
||||
+ "using the '--post-function' option. '--post-function' is passed the name a python file "
|
||||
@@ -415,23 +460,19 @@ The following attributes may be used with '--xattr-template':
|
||||
|
||||
def template_help(width=78):
|
||||
"""Return formatted string for template system"""
|
||||
sio = io.StringIO()
|
||||
console = Console(file=sio, force_terminal=True, width=width)
|
||||
template_help_md = strip_md_header_and_links(get_template_help())
|
||||
console.print(Markdown(template_help_md))
|
||||
help_str = sio.getvalue()
|
||||
sio.close()
|
||||
return help_str
|
||||
console = Console(force_terminal=True, width=width)
|
||||
with console.capture() as capture:
|
||||
console.print(Markdown(template_help_md))
|
||||
return capture.get()
|
||||
|
||||
|
||||
def rich_text(text, width=78):
|
||||
def rich_text(text, width=78, markdown=False):
|
||||
"""Return rich formatted text"""
|
||||
sio = io.StringIO()
|
||||
console = Console(file=sio, force_terminal=True, width=width)
|
||||
console.print(text)
|
||||
rich_text = sio.getvalue()
|
||||
sio.close()
|
||||
return rich_text
|
||||
console = Console(force_terminal=True, width=width)
|
||||
with console.capture() as capture:
|
||||
console.print(Markdown(text) if markdown else text, end="")
|
||||
return capture.get()
|
||||
|
||||
|
||||
def strip_md_header_and_links(md):
|
||||
|
||||
132
osxphotos/cli/theme.py
Normal file
132
osxphotos/cli/theme.py
Normal file
@@ -0,0 +1,132 @@
|
||||
"""theme command for osxphotos for managing color themes"""
|
||||
|
||||
import pathlib
|
||||
|
||||
import click
|
||||
from rich.console import Console
|
||||
from rich_theme_manager import Theme
|
||||
|
||||
from .click_rich_echo import rich_click_echo
|
||||
from .color_themes import get_default_theme, get_theme, get_theme_dir, get_theme_manager
|
||||
from .help import get_help_msg
|
||||
|
||||
|
||||
@click.command(name="theme")
|
||||
@click.pass_obj
|
||||
@click.pass_context
|
||||
@click.option("--default", is_flag=True, help="Show default theme.")
|
||||
@click.option("--list", "list_", is_flag=True, help="List all themes.")
|
||||
@click.option(
|
||||
"--config",
|
||||
metavar="[THEME]",
|
||||
is_flag=False,
|
||||
flag_value="_DEFAULT_",
|
||||
default=None,
|
||||
help="Print configuration for THEME (or default theme if not specified).",
|
||||
)
|
||||
@click.option(
|
||||
"--preview",
|
||||
metavar="[THEME]",
|
||||
is_flag=False,
|
||||
flag_value="_DEFAULT_",
|
||||
default=None,
|
||||
help="Preview THEME (or default theme if not specified).",
|
||||
)
|
||||
@click.option(
|
||||
"--edit",
|
||||
metavar="[THEME]",
|
||||
is_flag=False,
|
||||
flag_value="_DEFAULT_",
|
||||
default=None,
|
||||
help="Edit THEME (or default theme if not specified).",
|
||||
)
|
||||
@click.option(
|
||||
"--clone",
|
||||
metavar="THEME NEW_THEME",
|
||||
nargs=2,
|
||||
type=str,
|
||||
help="Clone THEME to NEW_THEME.",
|
||||
)
|
||||
@click.option("--delete", metavar="THEME", help="Delete THEME.")
|
||||
def theme(ctx, cli_obj, default, list_, config, preview, edit, clone, delete):
|
||||
"""Manage osxphotos color themes."""
|
||||
|
||||
subcommands = [default, list_, config, preview, edit, clone, delete]
|
||||
subcommand_names = (
|
||||
"--default, --list, --config, --preview, --edit, --clone, --delete"
|
||||
)
|
||||
if not any(subcommands):
|
||||
click.echo(
|
||||
f"Must specify exactly one of: {subcommand_names}\n",
|
||||
err=True,
|
||||
)
|
||||
rich_click_echo(get_help_msg(theme), err=True)
|
||||
return
|
||||
|
||||
if sum(bool(cmd) for cmd in subcommands) != 1:
|
||||
# only a single subcommand may be specified
|
||||
raise click.ClickException(f"Must specify exactly one of: {subcommand_names}")
|
||||
|
||||
theme_manager = get_theme_manager()
|
||||
console = Console(theme=get_default_theme())
|
||||
|
||||
if default:
|
||||
default = get_default_theme()
|
||||
theme_manager.list_themes(theme_names=[default.name])
|
||||
return
|
||||
|
||||
if list_:
|
||||
theme_manager.list_themes()
|
||||
return
|
||||
|
||||
if config:
|
||||
if config == "_DEFAULT_":
|
||||
print(get_default_theme().config)
|
||||
else:
|
||||
print(get_theme(config).config)
|
||||
return
|
||||
|
||||
if preview:
|
||||
theme_ = get_default_theme() if preview == "_DEFAULT_" else get_theme(preview)
|
||||
theme_manager.preview_theme(theme_)
|
||||
return
|
||||
|
||||
if edit:
|
||||
theme_ = get_default_theme() if edit == "_DEFAULT_" else get_theme(edit)
|
||||
config_file = pathlib.Path(theme_.path)
|
||||
console.print(f"Opening [filepath]{config_file}[/] in $EDITOR")
|
||||
click.edit(filename=str(config_file))
|
||||
return
|
||||
|
||||
if clone:
|
||||
src_theme = get_theme(clone[0])
|
||||
dest_path = get_theme_dir() / f"{clone[1]}.theme"
|
||||
if dest_path.exists():
|
||||
raise click.ClickException(
|
||||
f"Theme '{clone[1]}' already exists at {dest_path}"
|
||||
)
|
||||
dest_theme = Theme(
|
||||
name=clone[1],
|
||||
description=src_theme.description,
|
||||
inherit=src_theme.inherit,
|
||||
tags=src_theme.tags,
|
||||
styles={
|
||||
style_name: src_theme.styles[style_name]
|
||||
for style_name in src_theme.style_names
|
||||
},
|
||||
)
|
||||
theme_manager = get_theme_manager()
|
||||
theme_manager.add(dest_theme)
|
||||
theme_ = get_theme(dest_theme.name)
|
||||
console.print(
|
||||
f"Cloned theme '[filename]{clone[0]}[/]' to '[filename]{clone[1]}[/]' "
|
||||
f"at [filepath]{theme_.path}[/]"
|
||||
)
|
||||
return
|
||||
|
||||
if delete:
|
||||
theme_ = get_theme(delete)
|
||||
click.confirm(f"Are you sure you want to delete theme {delete}?", abort=True)
|
||||
theme_manager.remove(theme_)
|
||||
console.print(f"Deleted theme [filepath]{theme_.path}[/]")
|
||||
return
|
||||
@@ -8,7 +8,7 @@ Template statements may contain one or more modifiers. The full syntax is:
|
||||
|
||||
Template statements are white-space sensitive meaning that white space (spaces, tabs) changes the meaning of the template statement.
|
||||
|
||||
`pretext` and `posttext` are free form text. For example, if a photo has title "My Photo Title". the template statement `"The title of the photo is {title}"`, resolves to `"The title of the photo is My Photo Title"`. The `pretext` in this example is `"The title if the photo is "` and the template_field is `{title}`.
|
||||
`pretext` and `posttext` are free form text. For example, if a photo has title "My Photo Title" the template statement `"The title of the photo is {title}"`, resolves to `"The title of the photo is My Photo Title"`. The `pretext` in this example is `"The title if the photo is "` and the template_field is `{title}`.
|
||||
|
||||
|
||||
`delim`: optional delimiter string to use when expanding multi-valued template values in-place
|
||||
|
||||
@@ -355,7 +355,6 @@ Another example: if you had `exiftool` installed and wanted to wipe all metadata
|
||||
|
||||
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
|
||||
|
||||
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!):
|
||||
@@ -390,6 +389,10 @@ Here's a comprehensive use case from an actual osxphotos user that integrates ma
|
||||
|
||||
`osxphotos export ~/Desktop/folder for exported videos/ --keyword Quik --only-movies --db /path to my.photoslibrary --touch-file --finder-tag-keywords --person-keyword --xattr-template findercomment "{title}{title?{descr?{newline},},}{descr}" --exiftool-merge-keywords --exiftool-merge-persons --exiftool --strip`
|
||||
|
||||
### Color Themes
|
||||
|
||||
Some osxphotos commands such as export use color themes to colorize the output to make it more legible. The theme may be specified with the `--theme` option. For example: `osxphotos export /path/to/export --verbose --theme dark` uses a theme suited for dark terminals. If you don't specify the color theme, osxphotos will select a default theme based on the current terminal settings. You can also specify your own default theme. See `osxphotos help theme` for more information on themes and for commands to help manage themes. Themes are defined in `.theme` files in the `~/.osxphotos/themes` directory and use style specifications compatible with the [rich](https://rich.readthedocs.io/en/stable/style.html) library.
|
||||
|
||||
### Conclusion
|
||||
|
||||
osxphotos is very flexible. If you merely want to backup your Photos library, then spending a few minutes to understand the `--directory` option is likely all you need and you can be up and running in minutes. However, if you have a more complex workflow, osxphotos likely provides options to implement your workflow. This tutorial does not attempt to cover every option offered by osxphotos but hopefully it provides a good understanding of what kinds of things are possible and where to explore if you want to learn more.
|
||||
@@ -20,6 +20,7 @@ pyobjc-framework-Quartz>=7.3,<9.0
|
||||
pyobjc-framework-Vision>=7.3,<9.0
|
||||
PyYAML>=5.4.1,<6.0.0
|
||||
rich>=11.2.0,<12.0.0
|
||||
rich_theme_manager>=0.7.0
|
||||
textx>=2.3.0,<2.4.0
|
||||
toml>=0.10.2,<0.11.0
|
||||
wrapt>=1.13.3,<1.14.0
|
||||
|
||||
1
setup.py
1
setup.py
@@ -95,6 +95,7 @@ setup(
|
||||
"pyobjc-framework-Quartz>=7.3,<9.0",
|
||||
"pyobjc-framework-Vision>=7.3,<9.0",
|
||||
"rich>=11.2.0,<12.0.0",
|
||||
"rich_theme_manager>=0.7.0",
|
||||
"textx>=2.3.0,<3.0.0",
|
||||
"toml>=0.10.2,<0.11.0",
|
||||
"wrapt>=1.13.3,<1.14.0",
|
||||
|
||||
@@ -73,11 +73,11 @@ def generate_help_text(command):
|
||||
|
||||
# get current help text
|
||||
with runner.isolated_filesystem():
|
||||
result = runner.invoke(cli_main, ["help", command])
|
||||
result = runner.invoke(cli_main, ["help", command, "--width", 78])
|
||||
help_txt = result.output
|
||||
|
||||
# running the help command above doesn't output the full "Usage" line
|
||||
help_txt = help_txt.replace(f"Usage: cli-main", f"Usage: osxphotos")
|
||||
help_txt = help_txt.replace("Usage: cli-main", "Usage: osxphotos")
|
||||
return help_txt
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user