Compare commits

..

11 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
13 changed files with 214 additions and 11 deletions

View File

@@ -4,6 +4,29 @@ 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

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`
@@ -3608,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|

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))

View File

@@ -77,11 +77,12 @@ def place_folder(photo: osxphotos.PhotoInfo) -> str:
return ""
def photos_folders(photo: osxphotos.PhotoInfo, **kwargs) -> Union[List, str]:
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

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.60"
__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:

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",