Pass dest_path to template function via RenderOptions, enable implementation of #496

This commit is contained in:
Rhet Turnbull
2021-07-18 19:42:01 -07:00
parent 4f17c8fb23
commit 2d899ef045
6 changed files with 352 additions and 295 deletions

View File

@@ -1815,7 +1815,7 @@ Substitution Description
{lf} A line feed: '\n', alias for {newline} {lf} A line feed: '\n', alias for {newline}
{cr} A carriage return: '\r' {cr} A carriage return: '\r'
{crlf} a carriage return + line feed: '\r\n' {crlf} a carriage return + line feed: '\r\n'
{osxphotos_version} The osxphotos version, e.g. '0.42.62' {osxphotos_version} The osxphotos version, e.g. '0.42.64'
{osxphotos_cmd_line} The full command line used to run osxphotos {osxphotos_cmd_line} The full command line used to run osxphotos
The following substitutions may result in multiple values. Thus if specified for The following substitutions may result in multiple values. Thus if specified for
@@ -3038,8 +3038,13 @@ Returns a list of [FolderInfo](#FolderInfo) objects representing the sub-folders
#### `parent` #### `parent`
Returns a [FolderInfo](#FolderInfo) object representing the folder's parent folder or `None` if album is not a in a folder. Returns a [FolderInfo](#FolderInfo) object representing the folder's parent folder or `None` if album is not a in a folder.
#### `photo_index(photo)`
Returns index of photo in album (based on album sort order).
**Note**: FolderInfo and AlbumInfo objects effectively work as a linked list. The children of a folder are contained in `subfolders` and `album_info` and the parent object of both `AlbumInfo` and `FolderInfo` is represented by `parent`. For example: **Note**: FolderInfo and AlbumInfo objects effectively work as a linked list. The children of a folder are contained in `subfolders` and `album_info` and the parent object of both `AlbumInfo` and `FolderInfo` is represented by `parent`. For example:
```pycon ```pycon
>>> import osxphotos >>> import osxphotos
>>> photosdb = osxphotos.PhotosDB() >>> photosdb = osxphotos.PhotosDB()
@@ -3647,7 +3652,7 @@ The following template field substitutions are availabe for use the templating s
|{lf}|A line feed: '\n', alias for {newline}| |{lf}|A line feed: '\n', alias for {newline}|
|{cr}|A carriage return: '\r'| |{cr}|A carriage return: '\r'|
|{crlf}|a carriage return + line feed: '\r\n'| |{crlf}|a carriage return + line feed: '\r\n'|
|{osxphotos_version}|The osxphotos version, e.g. '0.42.62'| |{osxphotos_version}|The osxphotos version, e.g. '0.42.64'|
|{osxphotos_cmd_line}|The full command line used to run osxphotos| |{osxphotos_cmd_line}|The full command line used to run osxphotos|
|{album}|Album(s) photo is contained in| |{album}|Album(s) photo is contained in|
|{folder_album}|Folder path + album photo is contained in. e.g. 'Folder/Subfolder/Album' or just 'Album' if no enclosing folder| |{folder_album}|Folder path + album photo is contained in. e.g. 'Folder/Subfolder/Album' or just 'Album' if no enclosing folder|

View File

@@ -3,7 +3,7 @@
# script to help build osxphotos release # script to help build osxphotos release
# this is unique to my own dev setup # this is unique to my own dev setup
activate osxphotos source venv/bin/activate
rm -rf dist; rm -rf build rm -rf dist; rm -rf build
python3 utils/update_readme.py python3 utils/update_readme.py
(cd docsrc && make github && make pdf) (cd docsrc && make github && make pdf)

View File

@@ -5,6 +5,8 @@ from typing import Optional
from osxphotos import ExportResults, PhotoInfo from osxphotos import ExportResults, PhotoInfo
from osxphotos.albuminfo import AlbumInfo from osxphotos.albuminfo import AlbumInfo
from osxphotos.phototemplate import RenderOptions
from osxphotos.path_utils import sanitize_dirname
def _get_album_sort_order(album: AlbumInfo, photo: PhotoInfo) -> Optional[int]: def _get_album_sort_order(album: AlbumInfo, photo: PhotoInfo) -> Optional[int]:
@@ -25,6 +27,32 @@ def _get_album_sort_order(album: AlbumInfo, photo: PhotoInfo) -> Optional[int]:
return sort_order return sort_order
def album_sequence(photo: PhotoInfo, options: RenderOptions, **kwargs) -> str:
"""Call this with {function} template to get album sequence (sort order) when exporting with {folder_album} template
For example, calling this template function like the following prepends sequence#_ to each exported file if the file is in an album:
osxphotos export /path/to/export -V --directory "{folder_album}" --filename "{album?{function:examples/album_sort_order.py::album_sequence}_,}{original_name}"
"""
dest_path = options.dest_path
if not dest_path:
return ""
album_info = None
for album in photo.album_info:
# following code is how {folder_album} builds the folder path
folder = "/".join(sanitize_dirname(f) for f in album.folder_names)
folder += "/" + sanitize_dirname(album.title)
if dest_path.endswith(folder):
album_info = album
break
else:
# didn't find the album, so skip this file
return ""
return str(album_info.photo_index(photo))
def album_sort_order( def album_sort_order(
photo: PhotoInfo, results: ExportResults, verbose: callable, **kwargs photo: PhotoInfo, results: ExportResults, verbose: callable, **kwargs
): ):

View File

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

View File

@@ -24,7 +24,7 @@ from .datetime_utils import get_local_tz
def sort_list_by_keys(values, sort_keys): def sort_list_by_keys(values, sort_keys):
""" Sorts list values by a second list sort_keys """Sorts list values by a second list sort_keys
e.g. given ["a","c","b"], [1, 3, 2], returns ["a", "b", "c"] e.g. given ["a","c","b"], [1, 3, 2], returns ["a", "b", "c"]
Args: Args:
@@ -63,12 +63,12 @@ class AlbumInfoBaseClass:
@property @property
def uuid(self): def uuid(self):
""" return uuid of album """ """return uuid of album"""
return self._uuid return self._uuid
@property @property
def creation_date(self): def creation_date(self):
""" return creation date of album """ """return creation date of album"""
try: try:
return self._creation_date return self._creation_date
except AttributeError: except AttributeError:
@@ -90,8 +90,8 @@ class AlbumInfoBaseClass:
@property @property
def start_date(self): def start_date(self):
""" For Albums, return start date (earliest image) of album or None for albums with no images """For Albums, return start date (earliest image) of album or None for albums with no images
For Import Sessions, return start date of import session (when import began) """ For Import Sessions, return start date of import session (when import began)"""
try: try:
return self._start_date return self._start_date
except AttributeError: except AttributeError:
@@ -109,8 +109,8 @@ class AlbumInfoBaseClass:
@property @property
def end_date(self): def end_date(self):
""" For Albums, return end date (most recent image) of album or None for albums with no images """For Albums, return end date (most recent image) of album or None for albums with no images
For Import Sessions, return end date of import sessions (when import was completed) """ For Import Sessions, return end date of import sessions (when import was completed)"""
try: try:
return self._end_date return self._end_date
except AttributeError: except AttributeError:
@@ -131,7 +131,7 @@ class AlbumInfoBaseClass:
return [] return []
def __len__(self): def __len__(self):
""" return number of photos contained in album """ """return number of photos contained in album"""
return len(self.photos) return len(self.photos)
@@ -144,12 +144,12 @@ class AlbumInfo(AlbumInfoBaseClass):
@property @property
def title(self): def title(self):
""" return title / name of album """ """return title / name of album"""
return self._title return self._title
@property @property
def photos(self): def photos(self):
""" return list of photos contained in album sorted in same sort order as Photos """ """return list of photos contained in album sorted in same sort order as Photos"""
try: try:
return self._photos return self._photos
except AttributeError: except AttributeError:
@@ -163,10 +163,10 @@ class AlbumInfo(AlbumInfoBaseClass):
@property @property
def folder_names(self): def folder_names(self):
""" return hierarchical list of folders the album is contained in """return hierarchical list of folders the album is contained in
the folder list is in form: the folder list is in form:
["Top level folder", "sub folder 1", "sub folder 2", ...] ["Top level folder", "sub folder 1", "sub folder 2", ...]
returns empty list if album is not in any folders """ returns empty list if album is not in any folders"""
try: try:
return self._folder_names return self._folder_names
@@ -176,10 +176,10 @@ class AlbumInfo(AlbumInfoBaseClass):
@property @property
def folder_list(self): def folder_list(self):
""" return hierarchical list of folders the album is contained in """return hierarchical list of folders the album is contained in
as list of FolderInfo objects in form as list of FolderInfo objects in form
["Top level folder", "sub folder 1", "sub folder 2", ...] ["Top level folder", "sub folder 1", "sub folder 2", ...]
returns empty list if album is not in any folders """ returns empty list if album is not in any folders"""
try: try:
return self._folders return self._folders
@@ -189,7 +189,7 @@ class AlbumInfo(AlbumInfoBaseClass):
@property @property
def parent(self): def parent(self):
""" returns FolderInfo object for parent folder or None if no parent (e.g. top-level album) """ """returns FolderInfo object for parent folder or None if no parent (e.g. top-level album)"""
try: try:
return self._parent return self._parent
except AttributeError: except AttributeError:
@@ -209,11 +209,23 @@ class AlbumInfo(AlbumInfoBaseClass):
) )
return self._parent return self._parent
def photo_index(self, photo):
"""return index of photo in album (based on album sort order)"""
index = 0
for p in self.photos:
if p.uuid == photo.uuid:
return index
index += 1
else:
raise ValueError(
f"Photo with uuid {photo.uuid} does not appear to be in this album"
)
class ImportInfo(AlbumInfoBaseClass): class ImportInfo(AlbumInfoBaseClass):
@property @property
def photos(self): def photos(self):
""" return list of photos contained in import session """ """return list of photos contained in import session"""
try: try:
return self._photos return self._photos
except AttributeError: except AttributeError:
@@ -247,17 +259,17 @@ class FolderInfo:
@property @property
def title(self): def title(self):
""" return title / name of folder""" """return title / name of folder"""
return self._title return self._title
@property @property
def uuid(self): def uuid(self):
""" return uuid of folder """ """return uuid of folder"""
return self._uuid return self._uuid
@property @property
def album_info(self): def album_info(self):
""" return list of albums (as AlbumInfo objects) contained in the folder """ """return list of albums (as AlbumInfo objects) contained in the folder"""
try: try:
return self._albums return self._albums
except AttributeError: except AttributeError:
@@ -282,7 +294,7 @@ class FolderInfo:
@property @property
def parent(self): def parent(self):
""" returns FolderInfo object for parent or None if no parent (e.g. top-level folder) """ """returns FolderInfo object for parent or None if no parent (e.g. top-level folder)"""
try: try:
return self._parent return self._parent
except AttributeError: except AttributeError:
@@ -304,7 +316,7 @@ class FolderInfo:
@property @property
def subfolders(self): def subfolders(self):
""" return list of folders (as FolderInfo objects) contained in the folder """ """return list of folders (as FolderInfo objects) contained in the folder"""
try: try:
return self._folders return self._folders
except AttributeError: except AttributeError:
@@ -328,5 +340,5 @@ class FolderInfo:
return self._folders return self._folders
def __len__(self): def __len__(self):
""" returns count of folders + albums contained in the folder """ """returns count of folders + albums contained in the folder"""
return len(self.subfolders) + len(self.album_info) return len(self.subfolders) + len(self.album_info)

View File

@@ -2556,9 +2556,14 @@ def export_photo(
) )
results = ExportResults() results = ExportResults()
filenames = get_filenames_from_template( dest_paths = get_dirnames_from_template(
photo, filename_template, original_name, strip=strip photo, directory, export_by_date, dest, dry_run, strip=strip, edited=False
) )
for dest_path in dest_paths:
filenames = get_filenames_from_template(
photo, filename_template, dest, dest_path, original_name, strip=strip
)
for filename in filenames: for filename in filenames:
original_filename = pathlib.Path(filename) original_filename = pathlib.Path(filename)
file_ext = original_filename.suffix file_ext = original_filename.suffix
@@ -2566,7 +2571,8 @@ def export_photo(
# change the file extension to correct jpeg extension if needed # change the file extension to correct jpeg extension if needed
file_ext = ( file_ext = (
"." + jpeg_ext "." + jpeg_ext
if jpeg_ext and (photo.uti_original == "public.jpeg" or convert_to_jpeg) if jpeg_ext
and (photo.uti_original == "public.jpeg" or convert_to_jpeg)
else ".jpeg" else ".jpeg"
if convert_to_jpeg and photo.uti_original != "public.jpeg" if convert_to_jpeg and photo.uti_original != "public.jpeg"
else original_filename.suffix else original_filename.suffix
@@ -2581,16 +2587,14 @@ def export_photo(
f"Exporting {photo.original_filename} ({photo.filename}) as {original_filename}" f"Exporting {photo.original_filename} ({photo.filename}) as {original_filename}"
) )
results += export_photo_with_template( results += export_photo_to_directory(
photo=photo, photo=photo,
filename=original_filename, filename=original_filename,
directory=directory, dest_path=dest_path,
edited=False, edited=False,
use_photos_export=use_photos_export, use_photos_export=use_photos_export,
export_by_date=export_by_date,
dest=dest, dest=dest,
dry_run=dry_run, dry_run=dry_run,
strip=strip,
export_original=export_original, export_original=export_original,
missing=missing_original, missing=missing_original,
verbose=verbose, verbose=verbose,
@@ -2627,9 +2631,13 @@ def export_photo(
) )
if export_edited and photo.hasadjustments: if export_edited and photo.hasadjustments:
dest_paths = get_dirnames_from_template(
photo, directory, export_by_date, dest, dry_run, strip=strip, edited=True
)
for dest_path in dest_paths:
# if export-edited, also export the edited version # if export-edited, also export the edited version
edited_filenames = get_filenames_from_template( edited_filenames = get_filenames_from_template(
photo, filename_template, original_name, strip=strip, edited=True photo, filename_template, dest, dest_path, original_name, strip=strip, edited=True
) )
for edited_filename in edited_filenames: for edited_filename in edited_filenames:
edited_filename = pathlib.Path(edited_filename) edited_filename = pathlib.Path(edited_filename)
@@ -2643,7 +2651,11 @@ def export_photo(
else pathlib.Path(photo.filename).suffix else pathlib.Path(photo.filename).suffix
) )
if photo.isphoto and jpeg_ext and edited_ext.lower() in [".jpg", ".jpeg"]: if (
photo.isphoto
and jpeg_ext
and edited_ext.lower() in [".jpg", ".jpeg"]
):
edited_ext = "." + jpeg_ext edited_ext = "." + jpeg_ext
# Big Sur uses .heic for some edited photos so need to check # Big Sur uses .heic for some edited photos so need to check
@@ -2656,7 +2668,12 @@ def export_photo(
edited_ext = "." + jpeg_ext if jpeg_ext else ".jpeg" edited_ext = "." + jpeg_ext if jpeg_ext else ".jpeg"
rendered_edited_suffix = _render_suffix_template( rendered_edited_suffix = _render_suffix_template(
edited_suffix, "edited_suffix", "--edited-suffix", strip, dest, photo edited_suffix,
"edited_suffix",
"--edited-suffix",
strip,
dest,
photo,
) )
edited_filename = ( edited_filename = (
f"{edited_filename.stem}{rendered_edited_suffix}{edited_ext}" f"{edited_filename.stem}{rendered_edited_suffix}{edited_ext}"
@@ -2666,16 +2683,14 @@ def export_photo(
f"Exporting edited version of {photo.original_filename} ({photo.filename}) as {edited_filename}" f"Exporting edited version of {photo.original_filename} ({photo.filename}) as {edited_filename}"
) )
results += export_photo_with_template( results += export_photo_to_directory(
photo=photo, photo=photo,
filename=edited_filename, filename=edited_filename,
directory=directory, dest_path=dest_path,
edited=True, edited=True,
use_photos_export=use_photos_export, use_photos_export=use_photos_export,
export_by_date=export_by_date,
dest=dest, dest=dest,
dry_run=dry_run, dry_run=dry_run,
strip=strip,
export_original=False, export_original=False,
missing=missing_edited, missing=missing_edited,
verbose=verbose, verbose=verbose,
@@ -2744,16 +2759,14 @@ def _render_suffix_template(suffix_template, var_name, option_name, strip, dest,
return rendered_suffix[0] return rendered_suffix[0]
def export_photo_with_template( def export_photo_to_directory(
photo, photo,
filename, filename,
directory, dest_path,
edited, edited,
use_photos_export, use_photos_export,
export_by_date,
dest, dest,
dry_run, dry_run,
strip,
export_original, export_original,
missing, missing,
verbose, verbose,
@@ -2788,15 +2801,9 @@ def export_photo_with_template(
preview_suffix, preview_suffix,
preview_if_missing, preview_if_missing,
): ):
"""Evaluate directory template then export photo to each directory""" """Export photo to directory dest_path"""
results = ExportResults() results = ExportResults()
dest_paths = get_dirnames_from_template(
photo, directory, export_by_date, dest, dry_run, strip=strip, edited=edited
)
# export the photo to each path in dest_paths
for dest_path in dest_paths:
if export_original: if export_original:
if missing and not preview_if_missing: if missing and not preview_if_missing:
space = " " if not verbose else "" space = " " if not verbose else ""
@@ -2816,17 +2823,17 @@ def export_photo_with_template(
f"{space}Skipping missing deleted photo {photo.original_filename} ({photo.uuid})" f"{space}Skipping missing deleted photo {photo.original_filename} ({photo.uuid})"
) )
results.missing.append(str(pathlib.Path(dest_path) / filename)) results.missing.append(str(pathlib.Path(dest_path) / filename))
continue return results
elif not edited: elif not edited:
verbose_(f"Skipping original version of {photo.original_filename}") verbose_(f"Skipping original version of {photo.original_filename}")
continue return results
else: else:
# exporting the edited version # exporting the edited version
if missing and not preview_if_missing: if missing and not preview_if_missing:
space = " " if not verbose else "" space = " " if not verbose else ""
verbose_(f"{space}Skipping missing edited photo for {filename}") verbose_(f"{space}Skipping missing edited photo for {filename}")
results.missing.append(str(pathlib.Path(dest_path) / filename)) results.missing.append(str(pathlib.Path(dest_path) / filename))
continue return results
elif ( elif (
photo.intrash photo.intrash
and (not photo.path_edited or use_photos_export) and (not photo.path_edited or use_photos_export)
@@ -2839,7 +2846,7 @@ def export_photo_with_template(
f"{space}Skipping missing deleted photo {photo.original_filename} ({photo.uuid})" f"{space}Skipping missing deleted photo {photo.original_filename} ({photo.uuid})"
) )
results.missing.append(str(pathlib.Path(dest_path) / filename)) results.missing.append(str(pathlib.Path(dest_path) / filename))
continue return results
render_options = RenderOptions(export_dir=export_dir, dest_path=dest_path) render_options = RenderOptions(export_dir=export_dir, dest_path=dest_path)
@@ -2948,6 +2955,8 @@ def export_photo_with_template(
def get_filenames_from_template( def get_filenames_from_template(
photo, photo,
filename_template, filename_template,
export_dir,
dest_path,
original_name, original_name,
strip=False, strip=False,
edited=False, edited=False,
@@ -2958,6 +2967,7 @@ def get_filenames_from_template(
photo: a PhotoInfo instance photo: a PhotoInfo instance
filename_template: a PhotoTemplate template string, may be None filename_template: a PhotoTemplate template string, may be None
original_name: boolean; if True, use photo's original filename instead of current filename original_name: boolean; if True, use photo's original filename instead of current filename
dest_path: the path the photo will be exported to
strip: if True, strips leading/trailing white space from resulting template strip: if True, strips leading/trailing white space from resulting template
edited: if True, sets {edited_version} field to True, otherwise it gets set to False; set if you want template evaluated for edited version edited: if True, sets {edited_version} field to True, otherwise it gets set to False; set if you want template evaluated for edited version
@@ -2975,6 +2985,8 @@ def get_filenames_from_template(
filename=True, filename=True,
strip=strip, strip=strip,
edited_version=edited, edited_version=edited,
export_dir=export_dir,
dest_path=dest_path,
) )
filenames, unmatched = photo.render_template(filename_template, options) filenames, unmatched = photo.render_template(filename_template, options)
except ValueError as e: except ValueError as e: