Compare commits

..

16 Commits

Author SHA1 Message Date
Rhet Turnbull
173a0fce28 Added RenderOptions to {function} template, #496 2021-07-18 11:56:50 -07:00
Rhet Turnbull
b04ea8174d Added album_sort_order example 2021-07-18 09:07:16 -07:00
Rhet Turnbull
e40ecc45ad Update README.md 2021-07-18 07:57:34 -07:00
Rhet Turnbull
277b1614b9 Updated CHANGELOG.md [skip ci] 2021-07-16 20:09:48 -07:00
Rhet Turnbull
88099de688 Updated README.md [skip ci] 2021-07-16 20:07:50 -07:00
Rhet Turnbull
7d81b94c16 Upgraded osxmetadata to add new extended attributes 2021-07-16 19:45:19 -07:00
Rhet Turnbull
d627cfc4fa Update README.md 2021-07-15 20:25:28 -07:00
Rhet Turnbull
bf208bbe4b Updated tutorial with --regex example [skip ci] 2021-07-07 10:14:15 -07:00
Rhet Turnbull
79ba6f813f Updated CHANGELOG.md [skip ci] 2021-07-07 10:13:48 -07:00
Rhet Turnbull
141c0244e4 Added --selected, closes #489 2021-07-07 06:59:40 -07:00
Rhet Turnbull
7e0276beb7 Updated CHANGELOG.md [skip ci] 2021-07-06 22:03:48 -07:00
Rhet Turnbull
1bf11b0414 Fixed cleanup to delete empty folders, #491 2021-07-06 21:57:43 -07:00
allcontributors[bot]
c23f3fc5e4 docs: add mkirkland4874 as a contributor for example (#492)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2021-07-06 21:05:34 -07:00
Rhet Turnbull
016297d2ff Added example for {function} template 2021-07-06 21:00:16 -07:00
Rhet Turnbull
aa64283b55 Updated README.md [skip ci], closes #488 2021-07-06 12:17:02 -07:00
Rhet Turnbull
3973c27238 Updated CHANGELOG.md [skip ci] 2021-07-04 12:55:31 -07:00
14 changed files with 420 additions and 30 deletions

View File

@@ -229,7 +229,8 @@
"avatar_url": "https://avatars.githubusercontent.com/u/36466711?v=4",
"profile": "https://github.com/mkirkland4874",
"contributions": [
"bug"
"bug",
"example"
]
},
{

View File

@@ -4,6 +4,36 @@ All notable changes to this project will be documented in this file. Dates are d
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
#### [v0.42.62](https://github.com/RhetTbull/osxphotos/compare/v0.42.61...v0.42.62)
> 16 July 2021
- Upgraded osxmetadata to add new extended attributes [`7d81b94`](https://github.com/RhetTbull/osxphotos/commit/7d81b94c16623d11312aaf1b0c47fb580d01bc66)
- Updated tutorial with --regex example [skip ci] [`bf208bb`](https://github.com/RhetTbull/osxphotos/commit/bf208bbe4b965a2d39fc1836335b7b65f402af30)
- Update README.md [`d627cfc`](https://github.com/RhetTbull/osxphotos/commit/d627cfc4fa22497769babc3d686393c6043d1f37)
#### [v0.42.61](https://github.com/RhetTbull/osxphotos/compare/v0.42.60...v0.42.61)
> 7 July 2021
- Added --selected, closes #489 [`#489`](https://github.com/RhetTbull/osxphotos/issues/489)
#### [v0.42.60](https://github.com/RhetTbull/osxphotos/compare/v0.42.59...v0.42.60)
> 6 July 2021
- docs: add mkirkland4874 as a contributor for example [`#492`](https://github.com/RhetTbull/osxphotos/pull/492)
- Updated README.md [skip ci], closes #488 [`#488`](https://github.com/RhetTbull/osxphotos/issues/488)
- Added example for {function} template [`016297d`](https://github.com/RhetTbull/osxphotos/commit/016297d2ffcf2e8db0d659ccfe7411ecff3dd41b)
- Fixed cleanup to delete empty folders, #491 [`1bf11b0`](https://github.com/RhetTbull/osxphotos/commit/1bf11b0414a7fcf785c792b98f6231821bdad4d4)
#### [v0.42.59](https://github.com/RhetTbull/osxphotos/compare/v0.42.58...v0.42.59)
> 4 July 2021
- Re-enabled try/except in cli export [`d497b94`](https://github.com/RhetTbull/osxphotos/commit/d497b94ad506bf6cf044bbabe7fcbf4ab9d5b9e7)
- Added test for try/except block in cli export [`2e32d62`](https://github.com/RhetTbull/osxphotos/commit/2e32d62237f59b16a9be422104347d6a1332865c)
#### [v0.42.58](https://github.com/RhetTbull/osxphotos/compare/v0.42.57...v0.42.58)
> 4 July 2021

View File

@@ -3,6 +3,7 @@
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![tests](https://github.com/RhetTbull/osxphotos/workflows/Tests/badge.svg)](https://github.com/RhetTbull/osxphotos/workflows/Tests/badge.svg)
![PyPI - Python Version](https://img.shields.io/pypi/pyversions/osxphotos)
[![Downloads](https://static.pepy.tech/personalized-badge/osxphotos?period=month&units=international_system&left_color=black&right_color=brightgreen&left_text=downloads/month)](https://pepy.tech/project/osxphotos)
[![All Contributors](https://img.shields.io/badge/all_contributors-25-orange.svg?style=flat)](#contributors)
OSXPhotos provides the ability to interact with and query Apple's Photos.app library on macOS. You can query the Photos library database — for example, file name, file path, and metadata such as keywords/tags, persons/faces, albums, etc. You can also easily export both the original and edited photos.
@@ -409,6 +410,12 @@ To export only photos contained in the album "Summer Vacation":
`osxphotos export /path/to/export --album "Summer Vacation"`
In Photos, it's possible to have multiple albums with the same name. In this case, osxphotos would export photos from all albums matching the value passed to `--album`. If you wanted to export only one of the albums and this album is in a folder, the `--regex` option (short for "regular expression"), which does pattern matching, could be used with the `{folder_album}` template to match the specific album. For example, if you had a "Summer Vacation" album inside the folder "2018" and also one with the same name inside the folder "2019", you could export just the album "2018/Summer Vacation" using this command:
`osxphotos export /path/to/export --regex "2018/Summer Vacation" "{folder_album}"`
This command matches the pattern "2018/Summer Vacation" against the full folder/album path for every photo.
There are also a number of query options to export only certain types of photos. For example, to export only photos taken with iPhone "Portrait Mode":
`osxphotos export /path/to/export --portrait`
@@ -753,6 +760,9 @@ Options:
more than one regular expression match by
repeating '--regex' with different arguments.
--selected Filter for photos that are currently selected
in Photos.
--query-eval CRITERIA Evaluate CRITERIA to filter photos. CRITERIA
will be evaluated in context of the following
python list comprehension: `photos = [photo
@@ -1069,8 +1079,10 @@ Options:
--xattr-template ATTRIBUTE TEMPLATE
Set extended attribute ATTRIBUTE to TEMPLATE
value. Valid attributes are: 'authors',
'comment', 'copyright', 'description',
'findercomment', 'headline', 'keywords'. For
'comment', 'copyright', 'creator',
'description', 'findercomment', 'headline',
'keywords', 'participants', 'projects',
'rating', 'subject', 'title', 'version'. For
example, to set Finder comment to the photo's
title and description: '--xattr-template
findercomment "{title}; {descr}" See Extended
@@ -1302,6 +1314,10 @@ comment A comment related to the file. This differs from the Finder
copyright The copyright owner of the file contents. A string.
(com.apple.metadata:kMDItemCopyright)
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
@@ -1319,6 +1335,29 @@ keywords Keywords associated with this file. For example, “Birthday”,
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)
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
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
@@ -1776,7 +1815,7 @@ Substitution Description
{lf} A line feed: '\n', alias for {newline}
{cr} A carriage return: '\r'
{crlf} a carriage return + line feed: '\r\n'
{osxphotos_version} The osxphotos version, e.g. '0.42.59'
{osxphotos_version} The osxphotos version, e.g. '0.42.62'
{osxphotos_cmd_line} The full command line used to run osxphotos
The following substitutions may result in multiple values. Thus if specified for
@@ -2512,7 +2551,7 @@ Returns the absolute path to the edited photo on disk as a string. If the photo
**Note**: will also return None if the edited photo is missing on disk.
#### `path_derivatives`
Returns list of paths to any derivative preview images associated with the photo. The list of returned paths is sorted in descieding order by size (the largest, presumably highest quality) preview image will be the first element in the returned list. These will be named something like this on Photos 5+:
Returns list of paths to any derivative preview images associated with the photo. The list of returned paths is sorted in descending order by size (the largest, presumably highest quality) preview image will be the first element in the returned list. These will be named something like this on Photos 5+:
- `F19E06B8-A712-4B5C-907A-C007D37BDA16_1_101_o.jpeg`
- `F19E06B8-A712-4B5C-907A-C007D37BDA16_1_102_o.jpeg`
@@ -2843,24 +2882,31 @@ Then
If overwrite=False and increment=False, export will fail if destination file already exists
#### <a name="rendertemplate">`render_template()`</a>
`render_template(template_str, none_str = "_", path_sep = None, expand_inplace = False, inplace_sep = None, filename=False, dirname=False, strip=False)`
#### <a name="rendertemplate">`render_template(template_str, options=None)`</a>
Render template string for photo. none_str is used if template substitution results in None value and no default specified.
- `template_str`: str in osxphotos template language (OTL) format. See also [Template System](#template-system) table. See notes below regarding specific details of the syntax.
- `none_str`: optional str to use as substitution when template value is None and no default specified in the template string. default is "_".
- `path_sep`: optional character to use as path separator when joining path like fields such as `{folder_album}`; default is `os.path.sep`. May also be provided in the template itself. If provided both in the call to `render_template()` and in the template itself, the value in the template string takes precedence.
- `expand_inplace`: expand multi-valued substitutions in-place as a single string instead of returning individual strings
- `inplace_sep`: optional string to use as separator between multi-valued keywords with expand_inplace; default is ','
- `filename`: if True, template output will be sanitized to produce valid file name
- `dirname`: if True, template output will be sanitized to produce valid directory name
- `strip`: if True, leading/trailign whitespace will be stripped from rendered template strings
- `options`: an optional osxphotos.phototemplate.RenderOptions object specifying the options to pass to the rendering engine.
`RenderOptions` has the following properties:
- template: str template
- none_str: str to use default for None values, default is '_'
- path_sep: optional string to use as path separator, default is os.path.sep
- expand_inplace: expand multi-valued substitutions in-place as a single string instead of returning individual strings
- inplace_sep: optional string to use as separator between multi-valued keywords with expand_inplace; default is ','
- filename: if True, template output will be sanitized to produce valid file name
- dirname: if True, template output will be sanitized to produce valid directory name
- strip: if True, strips leading/trailing whitespace from rendered templates
- 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
- 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
Returns a tuple of (rendered, unmatched) where rendered is a list of rendered strings with all substitutions made and unmatched is a list of any strings that resembled a template substitution but did not match a known substitution. E.g. if template contained "{foo}", unmatched would be ["foo"]. If there are unmatched strings, rendered will be []. E.g. a template statement must fully match or will result in error and return all unmatched fields in unmatched.
e.g. `render_template("{created.year}/{foo}", photo)` would return `([],["foo"])`
e.g. `photo.render_template("{created.year}/{foo}")` would return `([],["foo"])`
Some substitutions, notably `album`, `keyword`, and `person` could return multiple values, hence a new string will be return for each possible substitution (hence why a list of rendered strings is returned). For example, a photo in 2 albums: 'Vacation' and 'Family' would result in the following rendered values if template was "{created.year}/{album}" and created.year == 2020: `["2020/Vacation","2020/Family"]`
@@ -3601,7 +3647,7 @@ The following template field substitutions are availabe for use the templating s
|{lf}|A line feed: '\n', alias for {newline}|
|{cr}|A carriage return: '\r'|
|{crlf}|a carriage return + line feed: '\r\n'|
|{osxphotos_version}|The osxphotos version, e.g. '0.42.59'|
|{osxphotos_version}|The osxphotos version, e.g. '0.42.62'|
|{osxphotos_cmd_line}|The full command line used to run osxphotos|
|{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|
@@ -3817,7 +3863,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<tr>
<td align="center"><a href="http://blog.dewost.com/"><img src="https://avatars.githubusercontent.com/u/17090228?v=4?s=75" width="75px;" alt=""/><br /><sub><b>Philippe Dewost</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=pdewost" title="Documentation">📖</a> <a href="#example-pdewost" title="Examples">💡</a> <a href="#ideas-pdewost" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/kaduskj"><img src="https://avatars.githubusercontent.com/u/983067?v=4?s=75" width="75px;" alt=""/><br /><sub><b>kaduskj</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Akaduskj" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/mkirkland4874"><img src="https://avatars.githubusercontent.com/u/36466711?v=4?s=75" width="75px;" alt=""/><br /><sub><b>mkirkland4874</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Amkirkland4874" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/mkirkland4874"><img src="https://avatars.githubusercontent.com/u/36466711?v=4?s=75" width="75px;" alt=""/><br /><sub><b>mkirkland4874</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Amkirkland4874" title="Bug reports">🐛</a> <a href="#example-mkirkland4874" title="Examples">💡</a></td>
<td align="center"><a href="https://github.com/jcommisso07"><img src="https://avatars.githubusercontent.com/u/3111054?v=4?s=75" width="75px;" alt=""/><br /><sub><b>Joseph Commisso</b></sub></a><br /><a href="#data-jcommisso07" title="Data">🔣</a></td>
</tr>
</table>

View File

@@ -0,0 +1,98 @@
""" Example function for use with osxphotos export --post-function option showing how to record album sort order """
import pathlib
from typing import Optional
from osxphotos import ExportResults, PhotoInfo
from osxphotos.albuminfo import AlbumInfo
def _get_album_sort_order(album: AlbumInfo, photo: PhotoInfo) -> Optional[int]:
"""Get the sort order of photo in album
Returns: sort order as int or None if photo not found in album
"""
# get the album sort order from the album_info
sort_order = 0 # change this to 1 if you want counting to start at 1
for album_photo in album.photos:
if album_photo.uuid == photo.uuid:
# found the photo we're processing
break
sort_order += 1
else:
# didn't find the photo, so skip this file
return None
return sort_order
def album_sort_order(
photo: PhotoInfo, results: ExportResults, verbose: callable, **kwargs
):
"""Call this with osxphotos export /path/to/export --post-function post_function.py::post_function
This will get called immediately after the photo has been exported
Args:
photo: PhotoInfo instance for the photo that's just been exported
results: ExportResults instance with information about the files associated with the exported photo
verbose: A function to print verbose output if --verbose is set; if --verbose is not set, acts as a no-op (nothing gets printed)
**kwargs: reserved for future use; recommend you include **kwargs so your function still works if additional arguments are added in future versions
Notes:
Use verbose(str) instead of print if you want your function to conditionally output text depending on --verbose flag
Any string printed with verbose that contains "warning" or "error" (case-insensitive) will be printed with the appropriate warning or error color
Will not be called if --dry-run flag is enabled
Will be called immediately after export and before any --post-command commands are executed
"""
# ExportResults has the following properties
# fields with filenames contain the full path to the file
# exported: list of all files exported
# new: list of all new files exported (--update)
# updated: list of all files updated (--update)
# skipped: list of all files skipped (--update)
# exif_updated: list of all files that were updated with --exiftool
# touched: list of all files that had date updated with --touch-file
# converted_to_jpeg: list of files converted to jpeg with --convert-to-jpeg
# sidecar_json_written: list of all JSON sidecar files written
# sidecar_json_skipped: list of all JSON sidecar files skipped (--update)
# sidecar_exiftool_written: list of all exiftool sidecar files written
# sidecar_exiftool_skipped: list of all exiftool sidecar files skipped (--update)
# sidecar_xmp_written: list of all XMP sidecar files written
# sidecar_xmp_skipped: list of all XMP sidecar files skipped (--update)
# missing: list of all missing files
# error: list tuples of (filename, error) for any errors generated during export
# exiftool_warning: list of tuples of (filename, warning) for any warnings generated by exiftool with --exiftool
# exiftool_error: list of tuples of (filename, error) for any errors generated by exiftool with --exiftool
# xattr_written: list of files that had extended attributes written
# xattr_skipped: list of files that where extended attributes were skipped (--update)
# deleted_files: list of deleted files
# deleted_directories: list of deleted directories
# exported_album: list of tuples of (filename, album_name) for exported files added to album with --add-exported-to-album
# skipped_album: list of tuples of (filename, album_name) for skipped files added to album with --add-skipped-to-album
# missing_album: list of tuples of (filename, album_name) for missing files added to album with --add-missing-to-album
for filepath in results.exported:
# do your processing here
filepath = pathlib.Path(filepath)
album_dir = filepath.parent.name
if album_dir not in photo.albums:
return
# get the first album that matches this name of which the photo is a member
album_info = None
for album in photo.album_info:
if album.title == album_dir:
album_info = album
break
else:
# didn't find the album, so skip this file
return
sort_order = _get_album_sort_order(album_info, photo)
if sort_order is None:
# didn't find the photo, so skip this file
return
verbose(f"Sort order for {filepath} in album {album_dir} is {sort_order}")
with open(str(filepath) + "_sort_order.txt", "w") as f:
f.write(str(sort_order))

173
examples/export_template.py Normal file
View File

@@ -0,0 +1,173 @@
""" Example showing how to use a custom function for osxphotos {function} template
to export photos in a folder structure similar to Photos' own structure
Use: osxphotos export /path/to/export --directory "{function:/path/to/export_template.py::photos_folders}"
This will likely export multiple copies of each photo. If using APFS file system, this should be
a non-issue as osxphotos will use copy-on-write so each exported photo doesn't take up additional space
unless you edit the photo.
Thank-you @mkirkland4874 for the inspiration for this example!
This will produce output similar to this:
Library
- Photos
-- {created.year}
---- {created.mm}
------ {created.dd}
- Favorites
- Hidden
- Recently Deleted
- People
- Places
- Imports
Media Types
- Videos
- Selfies
- Portrait
- Panoramas
- Time-lapse
- Slow-mo
- Bursts
- Screenshots
My Albums
-- Album 1
-- Album 2
-- Folder 1
---- Album 3
Shared Albums
-- Shared Album 1
-- Shared Album 2
"""
from typing import List, Union
import osxphotos
from osxphotos._constants import _UNKNOWN_PERSON
from osxphotos.datetime_formatter import DateTimeFormatter
from osxphotos.path_utils import sanitize_dirname
from osxphotos.phototemplate import RenderOptions
def place_folder(photo: osxphotos.PhotoInfo) -> str:
"""Return places as folder in format Country/State/City/etc."""
if not photo.place:
return ""
places = []
if photo.place.names.country:
places.append(photo.place.names.country[0])
if photo.place.names.state_province:
places.append(photo.place.names.state_province[0])
if photo.place.names.sub_administrative_area:
places.append(photo.place.names.sub_administrative_area[0])
if photo.place.names.additional_city_info:
places.append(photo.place.names.additional_city_info[0])
if photo.place.names.area_of_interest:
places.append(photo.place.names.area_of_interest[0])
if places:
return "Library/Places/" + "/".join(sanitize_dirname(place) for place in places)
else:
return ""
def photos_folders(photo: osxphotos.PhotoInfo, options: osxphotos.phototemplate.RenderOptions, **kwargs) -> Union[List, str]:
"""template function for use with --directory to export photos in a folder structure similar to Photos
Args:
photo: osxphotos.PhotoInfo object
options: RenderOptions instance
**kwargs: not currently used, placeholder to keep functions compatible with possible changes to {function}
Returns: list of directories for each photo
"""
rendered_date, _ = photo.render_template("{created.year}/{created.mm}/{created.dd}")
date_path = rendered_date[0]
def add_date_path(path):
"""add date path (year/mm/dd)"""
return f"{path}/{date_path}"
# Library
directories = []
if not photo.hidden and not photo.intrash and not photo.shared:
# set directories to [Library/Photos/year/mm/dd]
# render_template returns a tuple of [rendered value(s)], [unmatched]
# here, we can ignore the unmatched value, assigned to _, as we know template will match
directories, _ = photo.render_template(
"Library/Photos/{created.year}/{created.mm}/{created.dd}"
)
if photo.favorite:
directories.append(add_date_path("Library/Favorites"))
if photo.hidden:
directories.append(add_date_path("Library/Hidden"))
if photo.intrash:
directories.append(add_date_path("Library/Recently Deleted"))
directories.extend(
[
add_date_path(f"Library/People/{person}")
for person in photo.persons
if person != _UNKNOWN_PERSON
]
)
if photo.place:
directories.append(add_date_path(place_folder(photo)))
if photo.import_info:
dt = DateTimeFormatter(photo.import_info.creation_date)
directories.append(f"Library/Imports/{dt.year}/{dt.mm}/{dt.dd}")
# Media Types
if photo.ismovie:
directories.append(add_date_path("Media Types/Videos"))
if photo.selfie:
directories.append(add_date_path("Media Types/Selfies"))
if photo.live_photo:
directories.append(add_date_path("Media Types/Live Photos"))
if photo.portrait:
directories.append(add_date_path("Media Types/Portrait"))
if photo.panorama:
directories.append(add_date_path("Media Types/Panoramas"))
if photo.time_lapse:
directories.append(add_date_path("Media Types/Time-lapse"))
if photo.slow_mo:
directories.append(add_date_path("Media Types/Slo-mo"))
if photo.burst:
directories.append(add_date_path("Media Types/Bursts"))
if photo.screenshot:
directories.append(add_date_path("Media Types/Screenshots"))
# Albums
# render the folders and albums in folder/subfolder/album format
# the __NO_ALBUM__ is used as a sentinel to strip out photos not in an album
# use RenderOptions.dirname to force the rendered folder_album value to be sanitized as a valid path
# use RenderOptions.none_str to specify custom value for any photo that doesn't belong to an album so
# those can be filtered out; if not specified, none_str is "_"
folder_albums, _ = photo.render_template(
"{folder_album}", RenderOptions(dirname=True, none_str="__NO_ALBUM__")
)
root_directory = "Shared Albums/" if photo.shared else "My Albums/"
directories.extend(
[
root_directory + folder_album
for folder_album in folder_albums
if folder_album != "__NO_ALBUM__"
]
)
return directories

View File

@@ -227,10 +227,17 @@ EXTENDED_ATTRIBUTE_NAMES = [
"authors",
"comment",
"copyright",
"creator",
"description",
"findercomment",
"headline",
"keywords",
"participants",
"projects",
"rating",
"subject",
"title",
"version",
]
EXTENDED_ATTRIBUTE_NAMES_QUOTED = [f"'{x}'" for x in EXTENDED_ATTRIBUTE_NAMES]

View File

@@ -1,3 +1,3 @@
""" version info """
__version__ = "0.42.59"
__version__ = "0.42.63"

View File

@@ -536,6 +536,11 @@ def QUERY_OPTIONS(f):
"For example, to find photos in an album that begins with 'Beach': '--regex \"^Beach\" \"{album}\"'. "
"You may specify more than one regular expression match by repeating '--regex' with different arguments.",
),
o(
"--selected",
is_flag=True,
help="Filter for photos that are currently selected in Photos.",
),
o(
"--query-eval",
metavar="CRITERIA",
@@ -1182,6 +1187,7 @@ def export(
min_size,
max_size,
regex,
selected,
query_eval,
query_function,
duplicate,
@@ -1345,6 +1351,7 @@ def export(
min_size = cfg.min_size
max_size = cfg.max_size
regex = cfg.regex
selected = cfg.selected
query_eval = cfg.query_eval
query_function = cfg.query_function
duplicate = cfg.duplicate
@@ -1663,6 +1670,7 @@ def export(
min_size=min_size,
max_size=max_size,
regex=regex,
selected=selected,
query_eval=query_eval,
function=query_function,
duplicate=duplicate,
@@ -2071,6 +2079,7 @@ def query(
min_size,
max_size,
regex,
selected,
query_eval,
query_function,
add_to_album,
@@ -2105,6 +2114,7 @@ def query(
min_size,
max_size,
regex,
selected,
duplicate,
]
exclusive = [
@@ -2235,6 +2245,7 @@ def query(
query_eval=query_eval,
function=query_function,
regex=regex,
selected=selected,
duplicate=duplicate,
)
@@ -2830,7 +2841,7 @@ def export_photo_with_template(
results.missing.append(str(pathlib.Path(dest_path) / filename))
continue
render_options = RenderOptions(export_dir=export_dir)
render_options = RenderOptions(export_dir=export_dir, dest_path=dest_path)
tries = 0
while tries <= retry:
@@ -3280,13 +3291,13 @@ def cleanup_files(dest_path, files_to_keep, fileutil):
# delete empty directories
deleted_dirs = []
for p in pathlib.Path(dest_path).rglob("*"):
path = str(p).lower()
# if directory and directory is empty
if p.is_dir() and not next(p.iterdir(), False):
verbose_(f"Deleting empty directory {p}")
fileutil.rmdir(p)
deleted_dirs.append(str(p))
# walk directory tree bottom up and verify contents are empty
for dirpath, _, _ in os.walk(dest_path, topdown=False):
if not list(pathlib.Path(dirpath).glob("*")):
# directory and directory is empty
verbose_(f"Deleting empty directory {dirpath}")
fileutil.rmdir(dirpath)
deleted_dirs.append(str(dirpath))
return (deleted_files, deleted_dirs)

View File

@@ -17,6 +17,7 @@ from pprint import pformat
from typing import List
import bitmath
import photoscript
from .._constants import (
_DB_TABLE_NAMES,
@@ -3324,6 +3325,18 @@ class PhotosDB:
elif options.no_location:
photos = [p for p in photos if p.location == (None, None)]
if options.selected:
# photos selected in Photos app
try:
# catch AppleScript errors as the scripting interfce to Photos is flaky
selected = photoscript.PhotosLibrary().selection
selected_uuid = [p.uuid for p in selected]
photos = [p for p in photos if p.uuid in selected_uuid]
except Exception:
# no photos selected or a selected photo was "open"
# selection only works if photos selected in main media browser
photos = []
if options.function:
for function in options.function:
photos = function[0](photos)

View File

@@ -250,6 +250,7 @@ class RenderOptions:
strip: if True, strips leading/trailing whitespace from rendered templates
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
dest_path: set to the destination path of the photo (for use by {function} template), only valid with --filename
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
"""
@@ -263,6 +264,7 @@ class RenderOptions:
strip: bool = False
edited_version: bool = False
export_dir: Optional[str] = None
dest_path: Optional[str] = None
filepath: Optional[str] = None
quote: bool = False
@@ -354,8 +356,10 @@ class PhotoTemplate:
self.dirname = options.dirname
self.strip = options.strip
self.export_dir = options.export_dir
self.dest_path = options.dest_path
self.filepath = options.filepath
self.quote = options.quote
self.options = options
try:
model = self.parser.parse(template)
@@ -1182,7 +1186,7 @@ class PhotoTemplate:
raise ValueError(f"'{filename}' does not appear to be a file")
template_func = load_function(filename_validated, funcname)
values = template_func(self.photo)
values = template_func(self.photo, options=self.options)
if not isinstance(values, (str, list)):
raise TypeError(

View File

@@ -83,6 +83,7 @@ class QueryOptions:
location: Optional[bool] = None
no_location: Optional[bool] = None
function: Optional[List[Tuple[callable, str]]] = None
selected: Optional[bool] = None
def asdict(self):
return asdict(self)

View File

@@ -238,6 +238,12 @@ To export only photos contained in the album "Summer Vacation":
`osxphotos export /path/to/export --album "Summer Vacation"`
In Photos, it's possible to have multiple albums with the same name. In this case, osxphotos would export photos from all albums matching the value passed to `--album`. If you wanted to export only one of the albums and this album is in a folder, the `--regex` option (short for "regular expression"), which does pattern matching, could be used with the `{folder_album}` template to match the specific album. For example, if you had a "Summer Vacation" album inside the folder "2018" and also one with the same name inside the folder "2019", you could export just the album "2018/Summer Vacation" using this command:
`osxphotos export /path/to/export --regex "2018/Summer Vacation" "{folder_album}"`
This command matches the pattern "2018/Summer Vacation" against the full folder/album path for every photo.
There are also a number of query options to export only certain types of photos. For example, to export only photos taken with iPhone "Portrait Mode":
`osxphotos export /path/to/export --portrait`

View File

@@ -15,7 +15,7 @@ dataclasses==0.7;python_version<'3.7'
wurlitzer==2.1.0
photoscript==0.1.3
toml==0.10.2
osxmetadata==0.99.14
osxmetadata==0.99.25
textx==2.3.0
rich==10.2.2
bitmath==1.3.3.1

View File

@@ -90,7 +90,7 @@ setup(
"wurlitzer==2.1.0",
"photoscript==0.1.3",
"toml==0.10.2",
"osxmetadata==0.99.14",
"osxmetadata==0.99.25",
"textx==2.3.0",
"rich==10.2.2",
"bitmath==1.3.3.1",