More refactoring of export code, #462
This commit is contained in:
parent
3bafdf7bfd
commit
c2d726beaf
44
README.md
44
README.md
@ -482,7 +482,7 @@ Explanation of the template string:
|
||||
(or nothing if no description)
|
||||
```
|
||||
|
||||
In this example, `title?` demonstrates use of the boolean (True/False) feature of the template system. `title?` is read as "Is the title True (or not blank/empty)? If so, then the value immediately following the `?` is used in place of `title`. If `title` is blank, then the value immediately following the comma is used instead. The format for boolean fields is `field?value if true,value if false`. Either `value if true` or `value if false` may be blank, in which case a blank string ("") is used for the value and both may also be an entirely new template string as seen in the above example. Using this format, template strings may be nested inside each other to form complex `if-then-else` statements.
|
||||
In this example, `title?` demonstrates use of the bool (True/False) feature of the template system. `title?` is read as "Is the title True (or not blank/empty)? If so, then the value immediately following the `?` is used in place of `title`. If `title` is blank, then the value immediately following the comma is used instead. The format for bool fields is `field?value if true,value if false`. Either `value if true` or `value if false` may be blank, in which case a blank string ("") is used for the value and both may also be an entirely new template string as seen in the above example. Using this format, template strings may be nested inside each other to form complex `if-then-else` statements.
|
||||
|
||||
The above example, while complex to read, shows how flexible the osxphotos template system is. If you invest a little time learning how to use the template system you can easily handle almost any use case you have.
|
||||
|
||||
@ -1366,7 +1366,7 @@ another find|replace pair. e.g. to replace both "/" and ":" in album name:
|
||||
"{album[/,-|:,-]}". find/replace pairs are not limited to single characters.
|
||||
The "|" character cannot be used in a find/replace pair.
|
||||
|
||||
conditional: optional conditional expression that is evaluated as boolean
|
||||
conditional: optional conditional expression that is evaluated as bool
|
||||
(True/False) for use with the ?bool_value modifier. Conditional expressions
|
||||
take the form ' not operator value' where not is an optional modifier that
|
||||
negates the operator. Note: the space before the conditional expression is
|
||||
@ -1420,7 +1420,7 @@ This renames any photo that is a favorite as 'Favorite-ImageName.jpg' (where
|
||||
'ImageName.jpg' is the original name of the photo) and all other photos with the
|
||||
unmodified original name.
|
||||
|
||||
?bool_value: Template fields may be evaluated as boolean (True/False) by
|
||||
?bool_value: Template fields may be evaluated as bool (True/False) by
|
||||
appending "?" after the field name (and following "(path_sep)" or
|
||||
"[find/replace]". If a field is True (e.g. photo is HDR and field is "{hdr}")
|
||||
or has any value, the value following the "?" will be used to render the
|
||||
@ -1438,7 +1438,7 @@ and if it is not an HDR image,
|
||||
• "{hdr?ISHDR,NOTHDR}" renders to "NOTHDR"
|
||||
|
||||
,default: optional default value to use if the template name has no value. This
|
||||
modifier is also used for the value if False for boolean-type fields (see above)
|
||||
modifier is also used for the value if False for bool-type fields (see above)
|
||||
as well as to hold a sub-template for values like {created.strftime}. If no
|
||||
default value provided, "_" is used.
|
||||
|
||||
@ -2766,25 +2766,27 @@ Returns a JSON representation of all photo info.
|
||||
Returns a dictionary representation of all photo info.
|
||||
|
||||
#### `export()`
|
||||
`export(dest, filename=None, edited=False, live_photo=False, export_as_hardlink=False, overwrite=False, increment=True, sidecar_json=False, sidecar_exiftool=False, sidecar_xmp=False, use_photos_export=False, timeout=120, exiftool=False, use_albums_as_keywords=False, use_persons_as_keywords=False)`
|
||||
`export(dest, filename=None, edited=False, live_photo=False, export_as_hardlink=False, overwrite=False, increment=True, sidecar_json=False, sidecar_exiftool=False, sidecar_xmp=False, download_missing=False, use_photos_export=False, use_photokit=True, timeout=120, exiftool=False, use_albums_as_keywords=False, use_persons_as_keywords=False)`
|
||||
|
||||
Export photo from the Photos library to another destination on disk.
|
||||
- dest: must be valid destination path as str (or exception raised).
|
||||
- filename (optional): name of picture as str; if not provided, will use current filename. **NOTE**: if provided, user must ensure file extension (suffix) is correct. For example, if photo is .CR2 file, edited image may be .jpeg. If you provide an extension different than what the actual file is, export will print a warning but will happily export the photo using the incorrect file extension. e.g. to get the extension of the edited photo, look at [PhotoInfo.path_edited](#path_edited).
|
||||
- edited: boolean; if True (default=False), will export the edited version of the photo (or raise exception if no edited version)
|
||||
- export_as_hardlink: boolean; if True (default=False), will hardlink files instead of copying them
|
||||
- overwrite: boolean; if True (default=False), will overwrite files if they alreay exist
|
||||
- live_photo: boolean; if True (default=False), will also export the associted .mov for live photos; exported live photo will be named filename.mov
|
||||
- increment: boolean; if True (default=True), will increment file name until a non-existent name is found
|
||||
- sidecar_json: (boolean, default = False); if True will also write a json sidecar with metadata in format readable by exiftool; sidecar filename will be dest/filename.json where filename is the stem of the photo name
|
||||
- sidecar_json: (boolean, default = False); if True will also write a json sidecar with metadata in format readable by exiftool; sidecar filename will be dest/filename.json where filename is the stem of the photo name; resulting json file will include tag group names (e.g. `exiftool -G -j`)
|
||||
- sidecar_exiftool: (boolean, default = False); if True will also write a json sidecar with metadata in format readable by exiftool; sidecar filename will be dest/filename.json where filename is the stem of the photo name; resulting json file will not include tag group names (e.g. `exiftool -j`)
|
||||
- sidecar_xmp: (boolean, default = False); if True will also write a XMP sidecar with metadata; sidecar filename will be dest/filename.xmp where filename is the stem of the photo name
|
||||
- use_photos_export: boolean; (default=False), if True will attempt to export photo via applescript interaction with Photos; useful for forcing download of missing photos. This only works if the Photos library being used is the default library (last opened by Photos) as applescript will directly interact with whichever library Photos is currently using.
|
||||
- edited: bool; if True (default=False), will export the edited version of the photo (or raise exception if no edited version)
|
||||
- export_as_hardlink: bool; if True (default=False), will hardlink files instead of copying them
|
||||
- overwrite: bool; if True (default=False), will overwrite files if they alreay exist
|
||||
- live_photo: bool; if True (default=False), will also export the associted .mov for live photos; exported live photo will be named filename.mov
|
||||
- increment: bool; if True (default=True), will increment file name until a non-existent name is found
|
||||
- sidecar_json: (bool, default = False); if True will also write a json sidecar with metadata in format readable by exiftool; sidecar filename will be dest/filename.json where filename is the stem of the photo name
|
||||
- sidecar_json: (bool, default = False); if True will also write a json sidecar with metadata in format readable by exiftool; sidecar filename will be dest/filename.json where filename is the stem of the photo name; resulting json file will include tag group names (e.g. `exiftool -G -j`)
|
||||
- sidecar_exiftool: (bool, default = False); if True will also write a json sidecar with metadata in format readable by exiftool; sidecar filename will be dest/filename.json where filename is the stem of the photo name; resulting json file will not include tag group names (e.g. `exiftool -j`)
|
||||
- sidecar_xmp: (bool, default = False); if True will also write a XMP sidecar with metadata; sidecar filename will be dest/filename.xmp where filename is the stem of the photo name
|
||||
- use_photos_export: (bool, default=False); if True will attempt to export photo via AppleScript or PhotoKit interaction with Photos
|
||||
- download_missing: (bool, default=False); if True will attempt to export photo via AppleScript or PhotoKit interaction with Photos if missing
|
||||
- use_photokit: (bool, default=True); if True will attempt to export photo via photokit instead of AppleScript when used with use_photos_export or download_missing
|
||||
- timeout: (int, default=120) timeout in seconds used with use_photos_export
|
||||
- exiftool: (boolean, default = False) if True, will use [exiftool](https://exiftool.org/) to write metadata directly to the exported photo; exiftool must be installed and in the system path
|
||||
- use_albums_as_keywords: (boolean, default = False); if True, will use album names as keywords when exporting metadata with exiftool or sidecar
|
||||
- use_persons_as_keywords: (boolean, default = False); if True, will use person names as keywords when exporting metadata with exiftool or sidecar
|
||||
- exiftool: (bool, default = False) if True, will use [exiftool](https://exiftool.org/) to write metadata directly to the exported photo; exiftool must be installed and in the system path
|
||||
- use_albums_as_keywords: (bool, default = False); if True, will use album names as keywords when exporting metadata with exiftool or sidecar
|
||||
- use_persons_as_keywords: (bool, default = False); if True, will use person names as keywords when exporting metadata with exiftool or sidecar
|
||||
|
||||
Returns: list of paths to exported files. More than one file could be exported, for example if live_photo=True, both the original image and the associated .mov file will be exported
|
||||
|
||||
@ -3470,7 +3472,7 @@ e.g. If Photo is in `Album1` in `Folder1`:
|
||||
|
||||
`[find,replace]`: optional text replacement to perform on rendered template value. For example, to replace "/" in an album name, you could use the template `"{album[/,-]}"`. Multiple replacements can be made by appending "|" and adding another find|replace pair. e.g. to replace both "/" and ":" in album name: `"{album[/,-|:,-]}"`. find/replace pairs are not limited to single characters. The "|" character cannot be used in a find/replace pair.
|
||||
|
||||
`conditional`: optional conditional expression that is evaluated as boolean (True/False) for use with the `?bool_value` modifier. Conditional expressions take the form '` not operator value`' where `not` is an optional modifier that negates the `operator`. Note: the space before the conditional expression is required if you use a conditional expression. Valid comparison operators are:
|
||||
`conditional`: optional conditional expression that is evaluated as bool (True/False) for use with the `?bool_value` modifier. Conditional expressions take the form '` not operator value`' where `not` is an optional modifier that negates the `operator`. Note: the space before the conditional expression is required if you use a conditional expression. Valid comparison operators are:
|
||||
|
||||
- `contains`: template field contains value, similar to python's `in`
|
||||
- `matches`: template field contains exactly value, unlike `contains`: does not match partial matches
|
||||
@ -3504,7 +3506,7 @@ This can be used to rename files as well, for example:
|
||||
|
||||
This renames any photo that is a favorite as 'Favorite-ImageName.jpg' (where 'ImageName.jpg' is the original name of the photo) and all other photos with the unmodified original name.
|
||||
|
||||
`?bool_value`: Template fields may be evaluated as boolean (True/False) by appending "?" after the field name (and following "(path_sep)" or "[find/replace]". If a field is True (e.g. photo is HDR and field is `"{hdr}"`) or has any value, the value following the "?" will be used to render the template instead of the actual field value. If the template field evaluates to False (e.g. in above example, photo is not HDR) or has no value (e.g. photo has no title and field is `"{title}"`) then the default value following a "," will be used.
|
||||
`?bool_value`: Template fields may be evaluated as bool (True/False) by appending "?" after the field name (and following "(path_sep)" or "[find/replace]". If a field is True (e.g. photo is HDR and field is `"{hdr}"`) or has any value, the value following the "?" will be used to render the template instead of the actual field value. If the template field evaluates to False (e.g. in above example, photo is not HDR) or has no value (e.g. photo has no title and field is `"{title}"`) then the default value following a "," will be used.
|
||||
|
||||
e.g. if photo is an HDR image,
|
||||
|
||||
@ -3514,7 +3516,7 @@ and if it is not an HDR image,
|
||||
|
||||
- `"{hdr?ISHDR,NOTHDR}"` renders to `"NOTHDR"`
|
||||
|
||||
`,default`: optional default value to use if the template name has no value. This modifier is also used for the value if False for boolean-type fields (see above) as well as to hold a sub-template for values like `{created.strftime}`. If no default value provided, "_" is used.
|
||||
`,default`: optional default value to use if the template name has no value. This modifier is also used for the value if False for bool-type fields (see above) as well as to hold a sub-template for values like `{created.strftime}`. If no default value provided, "_" is used.
|
||||
|
||||
e.g., if photo has no title set,
|
||||
|
||||
|
||||
258
osxphotos/cli.py
258
osxphotos/cli.py
@ -2659,17 +2659,6 @@ def export_photo(
|
||||
export_db,
|
||||
)
|
||||
|
||||
# if download_missing and the photo is missing or path doesn't exist,
|
||||
# try to download with Photos
|
||||
use_photos_export = use_photos_export or (
|
||||
download_missing
|
||||
and (
|
||||
photo.ismissing
|
||||
or photo.path is None
|
||||
or (export_edited and photo.path_edited is None)
|
||||
)
|
||||
)
|
||||
|
||||
results = ExportResults()
|
||||
dest_paths = get_dirnames_from_template(
|
||||
photo,
|
||||
@ -2716,46 +2705,47 @@ def export_photo(
|
||||
)
|
||||
|
||||
results += export_photo_to_directory(
|
||||
photo=photo,
|
||||
filename=original_filename,
|
||||
album_keyword=album_keyword,
|
||||
convert_to_jpeg=convert_to_jpeg,
|
||||
description_template=description_template,
|
||||
dest_path=dest_path,
|
||||
edited=False,
|
||||
use_photos_export=use_photos_export,
|
||||
dest=dest,
|
||||
download_missing=download_missing,
|
||||
dry_run=dry_run,
|
||||
export_original=export_original,
|
||||
missing=missing_original,
|
||||
verbose=verbose,
|
||||
sidecar_flags=sidecar_flags,
|
||||
sidecar_drop_ext=sidecar_drop_ext,
|
||||
export_live=export_live,
|
||||
export_raw=export_raw,
|
||||
export_as_hardlink=export_as_hardlink,
|
||||
overwrite=overwrite,
|
||||
exiftool=exiftool,
|
||||
edited=False,
|
||||
exiftool_merge_keywords=exiftool_merge_keywords,
|
||||
exiftool_merge_persons=exiftool_merge_persons,
|
||||
album_keyword=album_keyword,
|
||||
person_keyword=person_keyword,
|
||||
keyword_template=keyword_template,
|
||||
description_template=description_template,
|
||||
update=update,
|
||||
ignore_signature=ignore_signature,
|
||||
export_db=export_db,
|
||||
fileutil=fileutil,
|
||||
touch_file=touch_file,
|
||||
convert_to_jpeg=convert_to_jpeg,
|
||||
jpeg_quality=jpeg_quality,
|
||||
ignore_date_modified=ignore_date_modified,
|
||||
use_photokit=use_photokit,
|
||||
exiftool_option=exiftool_option,
|
||||
exiftool=exiftool,
|
||||
export_as_hardlink=export_as_hardlink,
|
||||
export_db=export_db,
|
||||
export_dir=export_dir,
|
||||
export_live=export_live,
|
||||
export_original=export_original,
|
||||
export_preview=export_preview,
|
||||
export_raw=export_raw,
|
||||
filename=original_filename,
|
||||
fileutil=fileutil,
|
||||
ignore_date_modified=ignore_date_modified,
|
||||
ignore_signature=ignore_signature,
|
||||
jpeg_ext=jpeg_ext,
|
||||
jpeg_quality=jpeg_quality,
|
||||
keyword_template=keyword_template,
|
||||
missing=missing_original,
|
||||
overwrite=overwrite,
|
||||
person_keyword=person_keyword,
|
||||
photo=photo,
|
||||
preview_if_missing=preview_if_missing,
|
||||
preview_suffix=rendered_preview_suffix,
|
||||
replace_keywords=replace_keywords,
|
||||
retry=retry,
|
||||
export_dir=export_dir,
|
||||
export_preview=export_preview,
|
||||
preview_suffix=rendered_preview_suffix,
|
||||
preview_if_missing=preview_if_missing,
|
||||
sidecar_drop_ext=sidecar_drop_ext,
|
||||
sidecar_flags=sidecar_flags,
|
||||
touch_file=touch_file,
|
||||
update=update,
|
||||
use_photos_export=use_photos_export,
|
||||
use_photokit=use_photokit,
|
||||
verbose=verbose,
|
||||
)
|
||||
|
||||
if export_edited and photo.hasadjustments:
|
||||
@ -2827,46 +2817,47 @@ def export_photo(
|
||||
)
|
||||
|
||||
results += export_photo_to_directory(
|
||||
photo=photo,
|
||||
filename=edited_filename,
|
||||
album_keyword=album_keyword,
|
||||
convert_to_jpeg=convert_to_jpeg,
|
||||
description_template=description_template,
|
||||
dest_path=dest_path,
|
||||
edited=True,
|
||||
use_photos_export=use_photos_export,
|
||||
dest=dest,
|
||||
download_missing=download_missing,
|
||||
dry_run=dry_run,
|
||||
export_original=False,
|
||||
missing=missing_edited,
|
||||
verbose=verbose,
|
||||
sidecar_flags=sidecar_flags if not export_original else 0,
|
||||
sidecar_drop_ext=sidecar_drop_ext,
|
||||
export_live=export_live,
|
||||
export_raw=not export_original and export_raw,
|
||||
export_as_hardlink=export_as_hardlink,
|
||||
overwrite=overwrite,
|
||||
exiftool=exiftool,
|
||||
edited=True,
|
||||
exiftool_merge_keywords=exiftool_merge_keywords,
|
||||
exiftool_merge_persons=exiftool_merge_persons,
|
||||
album_keyword=album_keyword,
|
||||
person_keyword=person_keyword,
|
||||
keyword_template=keyword_template,
|
||||
description_template=description_template,
|
||||
update=update,
|
||||
ignore_signature=ignore_signature,
|
||||
export_db=export_db,
|
||||
fileutil=fileutil,
|
||||
touch_file=touch_file,
|
||||
convert_to_jpeg=convert_to_jpeg,
|
||||
jpeg_quality=jpeg_quality,
|
||||
ignore_date_modified=ignore_date_modified,
|
||||
use_photokit=use_photokit,
|
||||
exiftool_option=exiftool_option,
|
||||
exiftool=exiftool,
|
||||
export_as_hardlink=export_as_hardlink,
|
||||
export_db=export_db,
|
||||
export_dir=export_dir,
|
||||
export_live=export_live,
|
||||
export_original=False,
|
||||
export_preview=not export_original and export_preview,
|
||||
export_raw=not export_original and export_raw,
|
||||
filename=edited_filename,
|
||||
fileutil=fileutil,
|
||||
ignore_date_modified=ignore_date_modified,
|
||||
ignore_signature=ignore_signature,
|
||||
jpeg_ext=jpeg_ext,
|
||||
jpeg_quality=jpeg_quality,
|
||||
keyword_template=keyword_template,
|
||||
missing=missing_edited,
|
||||
overwrite=overwrite,
|
||||
person_keyword=person_keyword,
|
||||
photo=photo,
|
||||
preview_if_missing=preview_if_missing,
|
||||
preview_suffix=rendered_preview_suffix,
|
||||
replace_keywords=replace_keywords,
|
||||
retry=retry,
|
||||
export_dir=export_dir,
|
||||
export_preview=not export_original and export_preview,
|
||||
preview_suffix=rendered_preview_suffix,
|
||||
preview_if_missing=preview_if_missing,
|
||||
sidecar_drop_ext=sidecar_drop_ext,
|
||||
sidecar_flags=sidecar_flags if not export_original else 0,
|
||||
touch_file=touch_file,
|
||||
update=update,
|
||||
use_photos_export=use_photos_export,
|
||||
use_photokit=use_photokit,
|
||||
verbose=verbose,
|
||||
)
|
||||
|
||||
return results
|
||||
@ -2909,50 +2900,52 @@ def _render_suffix_template(
|
||||
|
||||
|
||||
def export_photo_to_directory(
|
||||
photo,
|
||||
filename,
|
||||
album_keyword,
|
||||
convert_to_jpeg,
|
||||
description_template,
|
||||
dest_path,
|
||||
edited,
|
||||
use_photos_export,
|
||||
dest,
|
||||
download_missing,
|
||||
dry_run,
|
||||
export_original,
|
||||
missing,
|
||||
verbose,
|
||||
sidecar_flags,
|
||||
sidecar_drop_ext,
|
||||
export_live,
|
||||
export_raw,
|
||||
export_as_hardlink,
|
||||
overwrite,
|
||||
exiftool,
|
||||
edited,
|
||||
exiftool_merge_keywords,
|
||||
exiftool_merge_persons,
|
||||
album_keyword,
|
||||
person_keyword,
|
||||
keyword_template,
|
||||
description_template,
|
||||
update,
|
||||
ignore_signature,
|
||||
export_db,
|
||||
fileutil,
|
||||
touch_file,
|
||||
convert_to_jpeg,
|
||||
jpeg_quality,
|
||||
ignore_date_modified,
|
||||
use_photokit,
|
||||
exiftool_option,
|
||||
exiftool,
|
||||
export_as_hardlink,
|
||||
export_db,
|
||||
export_dir,
|
||||
export_live,
|
||||
export_original,
|
||||
export_preview,
|
||||
export_raw,
|
||||
filename,
|
||||
fileutil,
|
||||
ignore_date_modified,
|
||||
ignore_signature,
|
||||
jpeg_ext,
|
||||
jpeg_quality,
|
||||
keyword_template,
|
||||
missing,
|
||||
overwrite,
|
||||
person_keyword,
|
||||
photo,
|
||||
preview_if_missing,
|
||||
preview_suffix,
|
||||
replace_keywords,
|
||||
retry,
|
||||
export_dir,
|
||||
export_preview,
|
||||
preview_suffix,
|
||||
preview_if_missing,
|
||||
sidecar_drop_ext,
|
||||
sidecar_flags,
|
||||
touch_file,
|
||||
update,
|
||||
use_photos_export,
|
||||
use_photokit,
|
||||
verbose,
|
||||
):
|
||||
"""Export photo to directory dest_path"""
|
||||
|
||||
results = ExportResults()
|
||||
# TODO: can be updated to let export2 do all the missing logic
|
||||
if export_original:
|
||||
if missing and not preview_if_missing:
|
||||
space = " " if not verbose else ""
|
||||
@ -2962,7 +2955,7 @@ def export_photo_to_directory(
|
||||
results.missing.append(str(pathlib.Path(dest_path) / filename))
|
||||
elif (
|
||||
photo.intrash
|
||||
and (not photo.path or use_photos_export)
|
||||
and (not photo.path or (download_missing or use_photos_export))
|
||||
and not preview_if_missing
|
||||
):
|
||||
# skip deleted files if they're missing or using use_photos_export
|
||||
@ -2985,7 +2978,7 @@ def export_photo_to_directory(
|
||||
return results
|
||||
elif (
|
||||
photo.intrash
|
||||
and (not photo.path_edited or use_photos_export)
|
||||
and (not photo.path_edited or (download_missing or use_photos_export))
|
||||
and not preview_if_missing
|
||||
):
|
||||
# skip deleted files if they're missing or using use_photos_export
|
||||
@ -3007,38 +3000,39 @@ def export_photo_to_directory(
|
||||
error = 0
|
||||
try:
|
||||
export_options = ExportOptions(
|
||||
edited=edited,
|
||||
sidecar=sidecar_flags,
|
||||
sidecar_drop_ext=sidecar_drop_ext,
|
||||
live_photo=export_live,
|
||||
raw_photo=export_raw,
|
||||
export_as_hardlink=export_as_hardlink,
|
||||
overwrite=overwrite,
|
||||
use_photos_export=use_photos_export,
|
||||
exiftool=exiftool,
|
||||
merge_exif_keywords=exiftool_merge_keywords,
|
||||
merge_exif_persons=exiftool_merge_persons,
|
||||
use_albums_as_keywords=album_keyword,
|
||||
use_persons_as_keywords=person_keyword,
|
||||
keyword_template=keyword_template,
|
||||
convert_to_jpeg=convert_to_jpeg,
|
||||
description_template=description_template,
|
||||
update=update,
|
||||
ignore_signature=ignore_signature,
|
||||
download_missing=download_missing,
|
||||
dry_run=dry_run,
|
||||
edited=edited,
|
||||
exiftool_flags=exiftool_option,
|
||||
exiftool=exiftool,
|
||||
export_as_hardlink=export_as_hardlink,
|
||||
export_db=export_db,
|
||||
fileutil=fileutil,
|
||||
dry_run=dry_run,
|
||||
touch_file=touch_file,
|
||||
convert_to_jpeg=convert_to_jpeg,
|
||||
jpeg_quality=jpeg_quality,
|
||||
ignore_date_modified=ignore_date_modified,
|
||||
use_photokit=use_photokit,
|
||||
verbose=verbose_,
|
||||
exiftool_flags=exiftool_option,
|
||||
ignore_signature=ignore_signature,
|
||||
jpeg_ext=jpeg_ext,
|
||||
replace_keywords=replace_keywords,
|
||||
render_options=render_options,
|
||||
preview=export_preview or (missing and preview_if_missing),
|
||||
jpeg_quality=jpeg_quality,
|
||||
keyword_template=keyword_template,
|
||||
live_photo=export_live,
|
||||
merge_exif_keywords=exiftool_merge_keywords,
|
||||
merge_exif_persons=exiftool_merge_persons,
|
||||
overwrite=overwrite,
|
||||
preview_suffix=preview_suffix,
|
||||
preview=export_preview or (missing and preview_if_missing),
|
||||
raw_photo=export_raw,
|
||||
render_options=render_options,
|
||||
replace_keywords=replace_keywords,
|
||||
sidecar_drop_ext=sidecar_drop_ext,
|
||||
sidecar=sidecar_flags,
|
||||
touch_file=touch_file,
|
||||
update=update,
|
||||
use_albums_as_keywords=album_keyword,
|
||||
use_persons_as_keywords=person_keyword,
|
||||
use_photokit=use_photokit,
|
||||
use_photos_export=use_photos_export,
|
||||
verbose=verbose_,
|
||||
)
|
||||
exporter = PhotoExporter(photo)
|
||||
export_results = exporter.export2(
|
||||
|
||||
@ -60,7 +60,7 @@ __all__ = [
|
||||
if TYPE_CHECKING:
|
||||
from .photoinfo import PhotoInfo
|
||||
|
||||
# retry if use_photos_export fails the first time (which sometimes it does)
|
||||
# retry if download_missing/use_photos_export fails the first time (which sometimes it does)
|
||||
MAX_PHOTOSCRIPT_RETRIES = 3
|
||||
|
||||
|
||||
@ -77,6 +77,7 @@ class ExportOptions:
|
||||
Attributes:
|
||||
convert_to_jpeg (bool): if True, converts non-jpeg images to jpeg
|
||||
description_template (str): optional template string that will be rendered for use as photo description
|
||||
download_missing: (bool, default=False): if True will attempt to export photo via applescript interaction with Photos if missing (see also use_photokit, use_photos_export)
|
||||
dry_run: (bool, default=False): set to True to run in "dry run" mode
|
||||
edited: (bool, default=False): if True will export the edited version of the photo otherwise exports the original version
|
||||
exiftool_flags (list of str): optional list of flags to pass to exiftool when using exiftool option, e.g ["-m", "-F"]
|
||||
@ -114,13 +115,14 @@ class ExportOptions:
|
||||
update (bool, default=False): if True export will run in update mode, that is, it will not export the photo if the current version already exists in the destination
|
||||
use_albums_as_keywords (bool, default = False): if True, will include album names in keywords when exporting metadata with exiftool or sidecar
|
||||
use_persons_as_keywords (bool, default = False): if True, will include person names in keywords when exporting metadata with exiftool or sidecar
|
||||
use_photos_export (bool, default=False): if True will attempt to export photo via applescript interaction with Photos (see also use_photokit)
|
||||
use_photos_export (bool, default=False): if True will attempt to export photo via applescript interaction with Photos even if not missing (see also use_photokit, download_missing)
|
||||
use_photokit (bool, default=False): if True, will use photokit to export photos when use_photos_export is True
|
||||
verbose (Callable): optional callable function to use for printing verbose text during processing; if None (default), does not print output.
|
||||
"""
|
||||
|
||||
convert_to_jpeg: bool = False
|
||||
description_template: Optional[str] = None
|
||||
download_missing: bool = False
|
||||
dry_run: bool = False
|
||||
edited: bool = False
|
||||
exiftool_flags: Optional[List] = None
|
||||
@ -369,7 +371,9 @@ class PhotoExporter:
|
||||
sidecar_json=False,
|
||||
sidecar_exiftool=False,
|
||||
sidecar_xmp=False,
|
||||
download_missing=False,
|
||||
use_photos_export=False,
|
||||
use_photokit=True,
|
||||
timeout=120,
|
||||
exiftool=False,
|
||||
use_albums_as_keywords=False,
|
||||
@ -404,7 +408,9 @@ class PhotoExporter:
|
||||
sidecar filename will be dest/filename.json; does not include exiftool tag group names (e.g. `exiftool -j`)
|
||||
sidecar_xmp: if set will write an XMP sidecar with IPTC data
|
||||
sidecar filename will be dest/filename.xmp
|
||||
use_photos_export: (boolean, default=False); if True will attempt to export photo via applescript interaction with Photos
|
||||
use_photos_export: (boolean, default=False); if True will attempt to export photo via AppleScript or PhotoKit interaction with Photos
|
||||
download_missing: (boolean, default=False); if True will attempt to export photo via AppleScript or PhotoKit interaction with Photos if missing
|
||||
use_photokit: (boolean, default=True); if True will attempt to export photo via photokit instead of AppleScript when used with use_photos_export or download_missing
|
||||
timeout: (int, default=120) timeout in seconds used with use_photos_export
|
||||
exiftool: (boolean, default = False); if True, will use exiftool to write metadata to export file
|
||||
returns list of full paths to the exported files
|
||||
@ -448,6 +454,7 @@ class PhotoExporter:
|
||||
|
||||
options = ExportOptions(
|
||||
description_template=description_template,
|
||||
download_missing=download_missing,
|
||||
edited=edited,
|
||||
exiftool=exiftool,
|
||||
export_as_hardlink=export_as_hardlink,
|
||||
@ -461,6 +468,7 @@ class PhotoExporter:
|
||||
timeout=timeout,
|
||||
use_albums_as_keywords=use_albums_as_keywords,
|
||||
use_persons_as_keywords=use_persons_as_keywords,
|
||||
use_photokit=use_photokit,
|
||||
use_photos_export=use_photos_export,
|
||||
)
|
||||
|
||||
@ -504,9 +512,11 @@ class PhotoExporter:
|
||||
if verbose and not callable(verbose):
|
||||
raise TypeError("verbose must be callable")
|
||||
|
||||
# can't use export_as_hardlink with use_photos_export as can't hardlink the temporary files downloaded
|
||||
if options.export_as_hardlink and options.use_photos_export:
|
||||
raise ValueError("Cannot use export_as_hardlink with use_photos_export")
|
||||
# can't use export_as_hardlink with download_missing, use_photos_export as can't hardlink the temporary files downloaded
|
||||
if options.export_as_hardlink and options.download_missing:
|
||||
raise ValueError(
|
||||
"Cannot use export_as_hardlink with download_missing or use_photos_export"
|
||||
)
|
||||
|
||||
# when called from export(), won't get an export_db, so use no-op version
|
||||
options.export_db = options.export_db or ExportDBNoOp()
|
||||
@ -768,14 +778,19 @@ class PhotoExporter:
|
||||
"""Stages photos for export
|
||||
|
||||
If photo is present on disk in the library, uses path to the photo on disk.
|
||||
If photo is missing and use_photos_export is true, downloads the photo from iCloud to temporary location.
|
||||
If photo is missing and download_missing is true, downloads the photo from iCloud to temporary location.
|
||||
"""
|
||||
|
||||
# TODO: this changes behavior in that Photos download is only called if file is actually missing
|
||||
# Need an option to force download if user wants to only use Photos export
|
||||
|
||||
staged = StagedFiles()
|
||||
|
||||
if options.use_photos_export:
|
||||
# use Photos AppleScript or PhotoKit to do the export
|
||||
return (
|
||||
self._stage_photo_for_export_with_photokit(options=options)
|
||||
if options.use_photokit
|
||||
else self._stage_photo_for_export_with_applescript(options=options)
|
||||
)
|
||||
|
||||
if options.raw_photo and self.photo.has_raw:
|
||||
staged.raw = self.photo.path_raw
|
||||
|
||||
@ -796,7 +811,7 @@ class PhotoExporter:
|
||||
staged.edited_live = self.photo.path_edited_live_photo
|
||||
|
||||
# download any missing files
|
||||
if options.use_photos_export:
|
||||
if options.download_missing:
|
||||
live_photo = staged.edited_live if options.edited else staged.original_live
|
||||
missing_options = ExportOptions(
|
||||
edited=options.edited,
|
||||
@ -816,195 +831,6 @@ class PhotoExporter:
|
||||
staged |= missing_staged
|
||||
return staged
|
||||
|
||||
# def _export_photo_with_photos_export(
|
||||
# self,
|
||||
# dest: pathlib.Path,
|
||||
# all_results: ExportResults,
|
||||
# options: ExportOptions,
|
||||
# ):
|
||||
# # TODO: if using applescript and exporting edited with live_photo doesn't seem to export the edited live photo
|
||||
# # this does work with photokit, but not with applescript
|
||||
# # TODO: duplicative code with the if edited/else--remove it
|
||||
# fileutil = options.fileutil
|
||||
# export_db = options.export_db
|
||||
|
||||
# # export live_photo .mov file?
|
||||
# live_photo = bool(options.live_photo and self.photo.live_photo)
|
||||
# overwrite = options.overwrite or options.update
|
||||
# if options.edited or self.photo.shared:
|
||||
# # exported edited version and not original
|
||||
# # shared photos (in shared albums) show up as not having adjustments (not edited)
|
||||
# # but Photos is unable to export the "original" as only a jpeg copy is shared in iCloud
|
||||
# # so tell Photos to export the current version in this case
|
||||
# # didn't get passed a filename, add _edited
|
||||
# uti = (
|
||||
# self.photo.uti_edited
|
||||
# if options.edited and self.photo.uti_edited
|
||||
# else self.photo.uti
|
||||
# )
|
||||
# ext = get_preferred_uti_extension(uti)
|
||||
# dest = dest.parent / f"{dest.stem}.{ext}"
|
||||
|
||||
# if options.use_photokit:
|
||||
# photolib = PhotoLibrary()
|
||||
# photo = None
|
||||
# try:
|
||||
# photo = photolib.fetch_uuid(self.photo.uuid)
|
||||
# except PhotoKitFetchFailed as e:
|
||||
# # if failed to find UUID, might be a burst photo
|
||||
# if self.photo.burst and self.photo._info["burstUUID"]:
|
||||
# bursts = photolib.fetch_burst_uuid(
|
||||
# self.photo._info["burstUUID"], all=True
|
||||
# )
|
||||
# # PhotoKit UUIDs may contain "/L0/001" so only look at beginning
|
||||
# photo = [
|
||||
# p for p in bursts if p.uuid.startswith(self.photo.uuid)
|
||||
# ]
|
||||
# photo = photo[0] if photo else None
|
||||
# if not photo:
|
||||
# all_results.error.append(
|
||||
# (
|
||||
# str(dest),
|
||||
# f"PhotoKitFetchFailed exception exporting photo {self.photo.uuid}: {e} ({lineno(__file__)})",
|
||||
# )
|
||||
# )
|
||||
# if photo:
|
||||
# if options.dry_run:
|
||||
# # dry_run, don't actually export
|
||||
# all_results.exported.append(str(dest))
|
||||
# else:
|
||||
# try:
|
||||
# exported = photo.export(
|
||||
# dest.parent,
|
||||
# dest.name,
|
||||
# version=PHOTOS_VERSION_CURRENT,
|
||||
# overwrite=overwrite,
|
||||
# video=live_photo,
|
||||
# )
|
||||
# all_results.exported.extend(exported)
|
||||
# except Exception as e:
|
||||
# all_results.error.append(
|
||||
# (str(dest), f"{e} ({lineno(__file__)})")
|
||||
# )
|
||||
# else:
|
||||
# try:
|
||||
# exported = _export_photo_uuid_applescript(
|
||||
# self.photo.uuid,
|
||||
# dest.parent,
|
||||
# filestem=dest.stem,
|
||||
# original=False,
|
||||
# edited=True,
|
||||
# live_photo=live_photo,
|
||||
# timeout=options.timeout,
|
||||
# burst=self.photo.burst,
|
||||
# dry_run=options.dry_run,
|
||||
# overwrite=overwrite,
|
||||
# )
|
||||
# all_results.exported.extend(exported)
|
||||
# except ExportError as e:
|
||||
# all_results.error.append((str(dest), f"{e} ({lineno(__file__)})"))
|
||||
# else:
|
||||
# # export original version and not edited
|
||||
# if options.use_photokit:
|
||||
# photolib = PhotoLibrary()
|
||||
# photo = None
|
||||
# try:
|
||||
# photo = photolib.fetch_uuid(self.photo.uuid)
|
||||
# except PhotoKitFetchFailed:
|
||||
# # if failed to find UUID, might be a burst photo
|
||||
# if self.photo.burst and self.photo._info["burstUUID"]:
|
||||
# bursts = photolib.fetch_burst_uuid(
|
||||
# self.photo._info["burstUUID"], all=True
|
||||
# )
|
||||
# # PhotoKit UUIDs may contain "/L0/001" so only look at beginning
|
||||
# photo = [
|
||||
# p for p in bursts if p.uuid.startswith(self.photo.uuid)
|
||||
# ]
|
||||
# photo = photo[0] if photo else None
|
||||
# if photo:
|
||||
# if not options.dry_run:
|
||||
# try:
|
||||
# exported = photo.export(
|
||||
# dest.parent,
|
||||
# dest.name,
|
||||
# version=PHOTOS_VERSION_ORIGINAL,
|
||||
# overwrite=overwrite,
|
||||
# video=live_photo,
|
||||
# )
|
||||
# all_results.exported.extend(exported)
|
||||
# except Exception as e:
|
||||
# all_results.error.append(
|
||||
# (str(dest), f"{e} ({lineno(__file__)})")
|
||||
# )
|
||||
# else:
|
||||
# # dry_run, don't actually export
|
||||
# all_results.exported.append(str(dest))
|
||||
# else:
|
||||
# try:
|
||||
# exported = _export_photo_uuid_applescript(
|
||||
# self.photo.uuid,
|
||||
# dest.parent,
|
||||
# filestem=dest.stem,
|
||||
# original=True,
|
||||
# edited=False,
|
||||
# live_photo=live_photo,
|
||||
# timeout=options.timeout,
|
||||
# burst=self.photo.burst,
|
||||
# dry_run=options.dry_run,
|
||||
# overwrite=overwrite,
|
||||
# )
|
||||
# all_results.exported.extend(exported)
|
||||
# except ExportError as e:
|
||||
# all_results.error.append((str(dest), f"{e} ({lineno(__file__)})"))
|
||||
# if all_results.exported:
|
||||
# for idx, photopath in enumerate(all_results.exported):
|
||||
# converted_stat = (None, None, None)
|
||||
# photopath = pathlib.Path(photopath)
|
||||
# if (
|
||||
# options.convert_to_jpeg
|
||||
# and self.photo.isphoto
|
||||
# and photopath.suffix.lower() not in LIVE_VIDEO_EXTENSIONS
|
||||
# ):
|
||||
# dest_str = photopath.parent / f"{photopath.stem}.jpeg"
|
||||
# fileutil.convert_to_jpeg(
|
||||
# photopath,
|
||||
# dest_str,
|
||||
# compression_quality=options.jpeg_quality,
|
||||
# )
|
||||
# converted_stat = fileutil.file_sig(dest_str)
|
||||
# fileutil.unlink(photopath)
|
||||
# all_results.exported[idx] = dest_str
|
||||
# all_results.converted_to_jpeg.append(dest_str)
|
||||
# photopath = dest_str
|
||||
|
||||
# photopath = str(photopath)
|
||||
# export_db.set_data(
|
||||
# filename=photopath,
|
||||
# uuid=self.photo.uuid,
|
||||
# orig_stat=fileutil.file_sig(photopath),
|
||||
# exif_stat=(None, None, None),
|
||||
# converted_stat=converted_stat,
|
||||
# edited_stat=(None, None, None),
|
||||
# info_json=self.photo.json(),
|
||||
# exif_json=None,
|
||||
# )
|
||||
|
||||
# # todo: handle signatures
|
||||
# if options.jpeg_ext:
|
||||
# # use_photos_export (both PhotoKit and AppleScript) don't use the
|
||||
# # file extension provided (instead they use extension for UTI)
|
||||
# # so if jpeg_ext is set, rename any non-conforming jpegs
|
||||
# all_results.exported = rename_jpeg_files(
|
||||
# all_results.exported, options.jpeg_ext, fileutil
|
||||
# )
|
||||
# if options.touch_file:
|
||||
# for exported_file in all_results.exported:
|
||||
# all_results.touched.append(exported_file)
|
||||
# ts = int(self.photo.date.timestamp())
|
||||
# fileutil.utime(exported_file, (ts, ts))
|
||||
# if options.update:
|
||||
# all_results.new.extend(all_results.exported)
|
||||
|
||||
def _stage_photo_for_export_with_photokit(
|
||||
self,
|
||||
options: ExportOptions,
|
||||
|
||||
@ -6868,7 +6868,7 @@ def test_export_download_missing_file_exists():
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert "exported: 1" in result.output
|
||||
assert "skipped: 1" in result.output
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
|
||||
@ -73,7 +73,6 @@ def test_export_default_name(photosdb):
|
||||
|
||||
filename = photos[0].original_filename
|
||||
expected_dest = pathlib.Path(dest) / filename
|
||||
expected_dest = expected_dest.parent / f"{expected_dest.stem}.jpeg"
|
||||
got_dest = photos[0].export(dest, use_photos_export=True)[0]
|
||||
|
||||
assert got_dest == str(expected_dest)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user