Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
173a0fce28 | ||
|
|
b04ea8174d | ||
|
|
e40ecc45ad | ||
|
|
277b1614b9 | ||
|
|
88099de688 | ||
|
|
7d81b94c16 | ||
|
|
d627cfc4fa | ||
|
|
bf208bbe4b | ||
|
|
79ba6f813f | ||
|
|
141c0244e4 | ||
|
|
7e0276beb7 | ||
|
|
1bf11b0414 | ||
|
|
c23f3fc5e4 | ||
|
|
016297d2ff | ||
|
|
aa64283b55 | ||
|
|
3973c27238 |
@@ -229,7 +229,8 @@
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/36466711?v=4",
|
||||
"profile": "https://github.com/mkirkland4874",
|
||||
"contributions": [
|
||||
"bug"
|
||||
"bug",
|
||||
"example"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
30
CHANGELOG.md
30
CHANGELOG.md
@@ -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
|
||||
|
||||
80
README.md
80
README.md
@@ -3,6 +3,7 @@
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://github.com/RhetTbull/osxphotos/workflows/Tests/badge.svg)
|
||||

|
||||
[](https://pepy.tech/project/osxphotos)
|
||||
[](#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>
|
||||
|
||||
98
examples/album_sort_order.py
Normal file
98
examples/album_sort_order.py
Normal 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
173
examples/export_template.py
Normal 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
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
""" version info """
|
||||
|
||||
__version__ = "0.42.59"
|
||||
__version__ = "0.42.63"
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user