@@ -2668,6 +2668,9 @@ Returns the path to the live video component of a [live photo](#live_photo). If
|
|||||||
|
|
||||||
**Note**: will also return None if the live video component is missing on disk. It's possible that the original photo may be on disk ([ismissing](#ismissing)==False) but the video component is missing, likely because it has not been downloaded from iCloud.
|
**Note**: will also return None if the live video component is missing on disk. It's possible that the original photo may be on disk ([ismissing](#ismissing)==False) but the video component is missing, likely because it has not been downloaded from iCloud.
|
||||||
|
|
||||||
|
#### `path_edited_live_photo`
|
||||||
|
Returns the path to the edited live video component of an edited [live photo](#live_photo). If photo is not a live photo or not edited, returns None.
|
||||||
|
|
||||||
#### `portrait`
|
#### `portrait`
|
||||||
Returns True if photo was taken in iPhone portrait mode, otherwise False.
|
Returns True if photo was taken in iPhone portrait mode, otherwise False.
|
||||||
|
|
||||||
@@ -2779,11 +2782,11 @@ Returns a JSON representation of all photo info.
|
|||||||
Returns a dictionary representation of all photo info.
|
Returns a dictionary representation of all photo info.
|
||||||
|
|
||||||
#### `export()`
|
#### `export()`
|
||||||
`export(dest, *filename, 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, use_photos_export=False, 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.
|
Export photo from the Photos library to another destination on disk.
|
||||||
- dest: must be valid destination path as str (or exception raised).
|
- 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).
|
- 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)
|
- 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
|
- 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
|
- overwrite: boolean; if True (default=False), will overwrite files if they alreay exist
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
""" version info """
|
""" version info """
|
||||||
|
|
||||||
__version__ = "0.42.55"
|
__version__ = "0.42.56"
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import osxmetadata
|
|||||||
import photoscript
|
import photoscript
|
||||||
import yaml
|
import yaml
|
||||||
from rich import pretty
|
from rich import pretty
|
||||||
|
import rich.traceback
|
||||||
|
|
||||||
import osxphotos
|
import osxphotos
|
||||||
|
|
||||||
@@ -65,6 +66,8 @@ from .utils import expand_and_validate_filepath, load_function
|
|||||||
# set via --verbose/-V
|
# set via --verbose/-V
|
||||||
VERBOSE = False
|
VERBOSE = False
|
||||||
|
|
||||||
|
rich.traceback.install()
|
||||||
|
|
||||||
|
|
||||||
def verbose_(*args, **kwargs):
|
def verbose_(*args, **kwargs):
|
||||||
"""print output if verbose flag set"""
|
"""print output if verbose flag set"""
|
||||||
@@ -701,6 +704,12 @@ def cli(ctx, db, json_, debug):
|
|||||||
"a value of 0.0 specifies maximum compression. "
|
"a value of 0.0 specifies maximum compression. "
|
||||||
f"Defaults to {DEFAULT_JPEG_QUALITY}",
|
f"Defaults to {DEFAULT_JPEG_QUALITY}",
|
||||||
)
|
)
|
||||||
|
# @click.option(
|
||||||
|
# "--preview",
|
||||||
|
# is_flag=True,
|
||||||
|
# help="Export preview image generated by Photos. "
|
||||||
|
# "This is a lower-resolution image used by Photos to quickly preview the image.",
|
||||||
|
# )
|
||||||
@click.option(
|
@click.option(
|
||||||
"--download-missing",
|
"--download-missing",
|
||||||
is_flag=True,
|
is_flag=True,
|
||||||
@@ -1160,6 +1169,7 @@ def export(
|
|||||||
duplicate,
|
duplicate,
|
||||||
post_command,
|
post_command,
|
||||||
post_function,
|
post_function,
|
||||||
|
preview=False,
|
||||||
):
|
):
|
||||||
"""Export photos from the Photos database.
|
"""Export photos from the Photos database.
|
||||||
Export path DEST is required.
|
Export path DEST is required.
|
||||||
@@ -1726,6 +1736,7 @@ def export(
|
|||||||
replace_keywords=replace_keywords,
|
replace_keywords=replace_keywords,
|
||||||
retry=retry,
|
retry=retry,
|
||||||
export_dir=dest,
|
export_dir=dest,
|
||||||
|
export_preview=preview,
|
||||||
)
|
)
|
||||||
|
|
||||||
if post_function:
|
if post_function:
|
||||||
@@ -2380,6 +2391,7 @@ def export_photo(
|
|||||||
replace_keywords=False,
|
replace_keywords=False,
|
||||||
retry=0,
|
retry=0,
|
||||||
export_dir=None,
|
export_dir=None,
|
||||||
|
export_preview=False,
|
||||||
):
|
):
|
||||||
"""Helper function for export that does the actual export
|
"""Helper function for export that does the actual export
|
||||||
|
|
||||||
@@ -2421,6 +2433,7 @@ def export_photo(
|
|||||||
replace_keywords: if True, --keyword-template replaces keywords instead of adding keywords
|
replace_keywords: if True, --keyword-template replaces keywords instead of adding keywords
|
||||||
retry: retry up to retry # of times if there's an error
|
retry: retry up to retry # of times if there's an error
|
||||||
export_dir: top-level export directory for {export_dir} template
|
export_dir: top-level export directory for {export_dir} template
|
||||||
|
export_preview: export the preview image generated by Photos
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list of path(s) of exported photo or None if photo was missing
|
list of path(s) of exported photo or None if photo was missing
|
||||||
@@ -2580,6 +2593,7 @@ def export_photo(
|
|||||||
replace_keywords=replace_keywords,
|
replace_keywords=replace_keywords,
|
||||||
retry=retry,
|
retry=retry,
|
||||||
export_dir=export_dir,
|
export_dir=export_dir,
|
||||||
|
export_preview=export_preview,
|
||||||
)
|
)
|
||||||
|
|
||||||
if export_edited and photo.hasadjustments:
|
if export_edited and photo.hasadjustments:
|
||||||
@@ -2655,13 +2669,13 @@ def export_photo(
|
|||||||
dest=dest,
|
dest=dest,
|
||||||
dry_run=dry_run,
|
dry_run=dry_run,
|
||||||
strip=strip,
|
strip=strip,
|
||||||
export_original=export_original,
|
export_original=False,
|
||||||
missing=missing_edited,
|
missing=missing_edited,
|
||||||
verbose=verbose,
|
verbose=verbose,
|
||||||
sidecar_flags=sidecar_flags,
|
sidecar_flags=sidecar_flags if not export_original else 0,
|
||||||
sidecar_drop_ext=sidecar_drop_ext,
|
sidecar_drop_ext=sidecar_drop_ext,
|
||||||
export_live=export_live,
|
export_live=export_live,
|
||||||
export_raw=export_raw,
|
export_raw=not export_original and export_raw,
|
||||||
export_as_hardlink=export_as_hardlink,
|
export_as_hardlink=export_as_hardlink,
|
||||||
overwrite=overwrite,
|
overwrite=overwrite,
|
||||||
exiftool=exiftool,
|
exiftool=exiftool,
|
||||||
@@ -2685,6 +2699,7 @@ def export_photo(
|
|||||||
replace_keywords=replace_keywords,
|
replace_keywords=replace_keywords,
|
||||||
retry=retry,
|
retry=retry,
|
||||||
export_dir=export_dir,
|
export_dir=export_dir,
|
||||||
|
export_preview=not export_original and export_preview,
|
||||||
)
|
)
|
||||||
|
|
||||||
return results
|
return results
|
||||||
@@ -2730,6 +2745,7 @@ def export_photo_with_template(
|
|||||||
replace_keywords,
|
replace_keywords,
|
||||||
retry,
|
retry,
|
||||||
export_dir,
|
export_dir,
|
||||||
|
export_preview,
|
||||||
):
|
):
|
||||||
"""Evaluate directory template then export photo to each directory"""
|
"""Evaluate directory template then export photo to each directory"""
|
||||||
|
|
||||||
@@ -2789,8 +2805,10 @@ def export_photo_with_template(
|
|||||||
try:
|
try:
|
||||||
export_results = photo.export2(
|
export_results = photo.export2(
|
||||||
dest_path,
|
dest_path,
|
||||||
filename,
|
original_filename=filename,
|
||||||
edited=edited,
|
edited=edited,
|
||||||
|
original=export_original,
|
||||||
|
edited_filename=filename,
|
||||||
sidecar=sidecar_flags,
|
sidecar=sidecar_flags,
|
||||||
sidecar_drop_ext=sidecar_drop_ext,
|
sidecar_drop_ext=sidecar_drop_ext,
|
||||||
live_photo=export_live,
|
live_photo=export_live,
|
||||||
@@ -2820,6 +2838,7 @@ def export_photo_with_template(
|
|||||||
jpeg_ext=jpeg_ext,
|
jpeg_ext=jpeg_ext,
|
||||||
replace_keywords=replace_keywords,
|
replace_keywords=replace_keywords,
|
||||||
render_options=render_options,
|
render_options=render_options,
|
||||||
|
preview=export_preview,
|
||||||
)
|
)
|
||||||
for warning_ in export_results.exiftool_warning:
|
for warning_ in export_results.exiftool_warning:
|
||||||
verbose_(f"exiftool warning for file {warning_[0]}: {warning_[1]}")
|
verbose_(f"exiftool warning for file {warning_[0]}: {warning_[1]}")
|
||||||
@@ -3949,7 +3968,9 @@ def repl(ctx, cli_obj, db):
|
|||||||
)
|
)
|
||||||
print(f"\nThe following functions may be helpful:")
|
print(f"\nThe following functions may be helpful:")
|
||||||
print(f"- get_photo(uuid): return a PhotoInfo object for photo with uuid")
|
print(f"- get_photo(uuid): return a PhotoInfo object for photo with uuid")
|
||||||
print(f"- get_selected(): return list of PhotoInfo objects for photos selected in Photos")
|
print(
|
||||||
|
f"- get_selected(): return list of PhotoInfo objects for photos selected in Photos"
|
||||||
|
)
|
||||||
print(f"- show(photo): open a photo object in the default viewer")
|
print(f"- show(photo): open a photo object in the default viewer")
|
||||||
print(
|
print(
|
||||||
f"- help(object): print help text including list of methods for object; for example, help(PhotosDB)"
|
f"- help(object): print help text including list of methods for object; for example, help(PhotosDB)"
|
||||||
|
|||||||
@@ -379,7 +379,7 @@ def rename_jpeg_files(files, jpeg_ext, fileutil):
|
|||||||
def export(
|
def export(
|
||||||
self,
|
self,
|
||||||
dest,
|
dest,
|
||||||
*filename,
|
filename=None,
|
||||||
edited=False,
|
edited=False,
|
||||||
live_photo=False,
|
live_photo=False,
|
||||||
raw_photo=False,
|
raw_photo=False,
|
||||||
@@ -410,12 +410,12 @@ def export(
|
|||||||
silently ignored).
|
silently ignored).
|
||||||
e.g. to get the extension of the edited photo,
|
e.g. to get the extension of the edited photo,
|
||||||
reference PhotoInfo.path_edited
|
reference PhotoInfo.path_edited
|
||||||
edited: (boolean, default=False); if True will export the edited version of the photo
|
edited: (boolean, default=False); if True will export the edited version of the photo, otherwise exports the original version
|
||||||
(or raise exception if no edited version)
|
(or raise exception if no edited version)
|
||||||
live_photo: (boolean, default=False); if True, will also export the associted .mov for live photos
|
live_photo: (boolean, default=False); if True, will also export the associated .mov for live photos
|
||||||
raw_photo: (boolean, default=False); if True, will also export the associted RAW photo
|
raw_photo: (boolean, default=False); if True, will also export the associated RAW photo
|
||||||
export_as_hardlink: (boolean, default=False); if True, will hardlink files instead of copying them
|
export_as_hardlink: (boolean, default=False); if True, will hardlink files instead of copying them
|
||||||
overwrite: (boolean, default=False); if True will overwrite files if they alreay exist
|
overwrite: (boolean, default=False); if True will overwrite files if they already exist
|
||||||
increment: (boolean, default=True); if True, will increment file name until a non-existant name is found
|
increment: (boolean, default=True); if True, will increment file name until a non-existant name is found
|
||||||
if overwrite=False and increment=False, export will fail if destination file already exists
|
if overwrite=False and increment=False, export will fail if destination file already exists
|
||||||
sidecar_json: if set will write a json sidecar with data in format readable by exiftool
|
sidecar_json: if set will write a json sidecar with data in format readable by exiftool
|
||||||
@@ -449,10 +449,25 @@ def export(
|
|||||||
if sidecar_xmp:
|
if sidecar_xmp:
|
||||||
sidecar |= SIDECAR_XMP
|
sidecar |= SIDECAR_XMP
|
||||||
|
|
||||||
|
if not filename:
|
||||||
|
if not edited:
|
||||||
|
filename = self.original_filename
|
||||||
|
else:
|
||||||
|
original_name = pathlib.Path(self.original_filename)
|
||||||
|
if self.path_edited:
|
||||||
|
ext = pathlib.Path(self.path_edited).suffix
|
||||||
|
else:
|
||||||
|
uti = self.uti_edited if edited and self.uti_edited else self.uti
|
||||||
|
ext = get_preferred_uti_extension(uti)
|
||||||
|
ext = "." + ext
|
||||||
|
filename = original_name.stem + "_edited" + ext
|
||||||
|
|
||||||
results = self.export2(
|
results = self.export2(
|
||||||
dest,
|
dest,
|
||||||
*filename,
|
original=not edited,
|
||||||
|
original_filename=filename,
|
||||||
edited=edited,
|
edited=edited,
|
||||||
|
edited_filename=filename,
|
||||||
live_photo=live_photo,
|
live_photo=live_photo,
|
||||||
raw_photo=raw_photo,
|
raw_photo=raw_photo,
|
||||||
export_as_hardlink=export_as_hardlink,
|
export_as_hardlink=export_as_hardlink,
|
||||||
@@ -466,7 +481,7 @@ def export(
|
|||||||
use_persons_as_keywords=use_persons_as_keywords,
|
use_persons_as_keywords=use_persons_as_keywords,
|
||||||
keyword_template=keyword_template,
|
keyword_template=keyword_template,
|
||||||
description_template=description_template,
|
description_template=description_template,
|
||||||
render_options = render_options,
|
render_options=render_options,
|
||||||
)
|
)
|
||||||
|
|
||||||
return results.exported
|
return results.exported
|
||||||
@@ -475,8 +490,10 @@ def export(
|
|||||||
def export2(
|
def export2(
|
||||||
self,
|
self,
|
||||||
dest,
|
dest,
|
||||||
*filename,
|
original=True,
|
||||||
|
original_filename=None,
|
||||||
edited=False,
|
edited=False,
|
||||||
|
edited_filename=None,
|
||||||
live_photo=False,
|
live_photo=False,
|
||||||
raw_photo=False,
|
raw_photo=False,
|
||||||
export_as_hardlink=False,
|
export_as_hardlink=False,
|
||||||
@@ -509,7 +526,9 @@ def export2(
|
|||||||
persons=True,
|
persons=True,
|
||||||
location=True,
|
location=True,
|
||||||
replace_keywords=False,
|
replace_keywords=False,
|
||||||
render_options: Optional[RenderOptions] = None
|
preview=False,
|
||||||
|
preview_suffix="_preview",
|
||||||
|
render_options: Optional[RenderOptions] = None,
|
||||||
):
|
):
|
||||||
"""export photo, like export but with update and dry_run options
|
"""export photo, like export but with update and dry_run options
|
||||||
dest: must be valid destination path or exception raised
|
dest: must be valid destination path or exception raised
|
||||||
@@ -521,8 +540,8 @@ def export2(
|
|||||||
in which case export will use the extension provided by Photos upon export.
|
in which case export will use the extension provided by Photos upon export.
|
||||||
e.g. to get the extension of the edited photo,
|
e.g. to get the extension of the edited photo,
|
||||||
reference PhotoInfo.path_edited
|
reference PhotoInfo.path_edited
|
||||||
|
original: (boolean, default=True); if True, will export the original version of the photo
|
||||||
edited: (boolean, default=False); if True will export the edited version of the photo
|
edited: (boolean, default=False); if True will export the edited version of the photo
|
||||||
(or raise exception if no edited version)
|
|
||||||
live_photo: (boolean, default=False); if True, will also export the associated .mov for live photos
|
live_photo: (boolean, default=False); if True, will also export the associated .mov for live photos
|
||||||
raw_photo: (boolean, default=False); if True, will also export the associated RAW photo
|
raw_photo: (boolean, default=False); if True, will also export the associated RAW photo
|
||||||
export_as_hardlink: (boolean, default=False); if True, will hardlink files instead of copying them
|
export_as_hardlink: (boolean, default=False); if True, will hardlink files instead of copying them
|
||||||
@@ -565,6 +584,8 @@ def export2(
|
|||||||
persons: if True, include persons in exported metadata
|
persons: if True, include persons in exported metadata
|
||||||
location: if True, include location in exported metadata
|
location: if True, include location in exported metadata
|
||||||
replace_keywords: if True, keyword_template replaces any keywords, otherwise it's additive
|
replace_keywords: if True, keyword_template replaces any keywords, otherwise it's additive
|
||||||
|
preview: if True, also exports preview image
|
||||||
|
preview_suffix: optional string to append to end of filename for preview images, if not provided, uses "_preview"
|
||||||
render_options: optional osxphotos.phototemplate.RenderOptions instance to specify options for rendering templates
|
render_options: optional osxphotos.phototemplate.RenderOptions instance to specify options for rendering templates
|
||||||
|
|
||||||
Returns: ExportResults class
|
Returns: ExportResults class
|
||||||
@@ -609,206 +630,280 @@ def export2(
|
|||||||
|
|
||||||
self._render_options = render_options or RenderOptions()
|
self._render_options = render_options or RenderOptions()
|
||||||
|
|
||||||
# suffix to add to edited files
|
export_original = original
|
||||||
# e.g. name will be filename_edited.jpg
|
export_edited = edited
|
||||||
edited_identifier = "_edited"
|
|
||||||
|
|
||||||
# check edited and raise exception trying to export edited version of
|
|
||||||
# photo that hasn't been edited
|
|
||||||
if edited and not self.hasadjustments:
|
if edited and not self.hasadjustments:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Photo does not have adjustments, cannot export edited version"
|
"Photo does not have adjustments, cannot export edited version"
|
||||||
)
|
)
|
||||||
|
|
||||||
# check arguments and get destination path and filename (if provided)
|
|
||||||
if filename and len(filename) > 2:
|
|
||||||
raise TypeError(
|
|
||||||
"Too many positional arguments. Should be at most two: destination, filename."
|
|
||||||
)
|
|
||||||
|
|
||||||
# verify destination is a valid path
|
# verify destination is a valid path
|
||||||
if dest is None:
|
if dest is None:
|
||||||
raise ValueError("Destination must not be None")
|
raise ValueError("dest must not be None")
|
||||||
elif not dry_run and not os.path.isdir(dest):
|
elif not dry_run and not os.path.isdir(dest):
|
||||||
raise FileNotFoundError("Invalid path passed to export")
|
raise FileNotFoundError("Invalid path passed to export")
|
||||||
|
|
||||||
if filename and len(filename) == 1:
|
original_filename = original_filename or self.original_filename
|
||||||
# if filename passed, use it
|
dest_original = pathlib.Path(dest) / original_filename
|
||||||
fname = filename[0]
|
|
||||||
else:
|
if not edited_filename:
|
||||||
# no filename provided so use the default
|
if not edited:
|
||||||
# if edited file requested, use filename but add _edited
|
edited_filename = self.original_filename
|
||||||
# need to use file extension from edited file as Photos saves a jpeg once edited
|
|
||||||
if edited and not use_photos_export:
|
|
||||||
# verify we have a valid path_edited and use that to get filename
|
|
||||||
if not self.path_edited:
|
|
||||||
raise FileNotFoundError(
|
|
||||||
"edited=True but path_edited is none; hasadjustments: "
|
|
||||||
f" {self.hasadjustments}"
|
|
||||||
)
|
|
||||||
edited_name = pathlib.Path(self.path_edited).name
|
|
||||||
edited_suffix = pathlib.Path(edited_name).suffix
|
|
||||||
fname = (
|
|
||||||
pathlib.Path(self.original_filename).stem
|
|
||||||
+ edited_identifier
|
|
||||||
+ edited_suffix
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
fname = self.original_filename
|
original_name = pathlib.Path(self.original_filename)
|
||||||
|
if self.path_edited:
|
||||||
|
ext = pathlib.Path(self.path_edited).suffix
|
||||||
|
else:
|
||||||
|
uti = self.uti_edited if edited and self.uti_edited else self.uti
|
||||||
|
ext = get_preferred_uti_extension(uti)
|
||||||
|
ext = "." + ext
|
||||||
|
edited_filename = original_name.stem + "_edited" + ext
|
||||||
|
dest_edited = pathlib.Path(dest) / edited_filename
|
||||||
|
|
||||||
uti = self.uti if edited else self.uti_original
|
if convert_to_jpeg and self.isphoto:
|
||||||
if convert_to_jpeg and self.isphoto and uti != "public.jpeg":
|
something_to_convert = False
|
||||||
# not a jpeg but will convert to jpeg upon export so fix file extension
|
|
||||||
fname_new = pathlib.Path(fname)
|
|
||||||
ext = "." + jpeg_ext if jpeg_ext else ".jpeg"
|
ext = "." + jpeg_ext if jpeg_ext else ".jpeg"
|
||||||
fname = str(fname_new.parent / f"{fname_new.stem}{ext}")
|
if export_original and self.uti_original != "public.jpeg":
|
||||||
|
# not a jpeg but will convert to jpeg upon export so fix file extension
|
||||||
|
something_to_convert = True
|
||||||
|
dest_original = dest_original.parent / f"{dest_original.stem}{ext}"
|
||||||
|
if export_edited and self.uti != "public.jpeg":
|
||||||
|
# in Big Sur+, edited HEICs are HEIC
|
||||||
|
something_to_convert = True
|
||||||
|
dest_edited = dest_edited.parent / f"{dest_edited.stem}{ext}"
|
||||||
|
convert_to_jpeg = something_to_convert
|
||||||
else:
|
else:
|
||||||
# nothing to convert
|
|
||||||
convert_to_jpeg = False
|
convert_to_jpeg = False
|
||||||
|
|
||||||
# check destination path
|
|
||||||
dest = pathlib.Path(dest)
|
|
||||||
fname = pathlib.Path(fname)
|
|
||||||
dest = dest / fname
|
|
||||||
|
|
||||||
# check to see if file exists and if so, add (1), (2), etc until we find one that works
|
# check to see if file exists and if so, add (1), (2), etc until we find one that works
|
||||||
# Photos checks the stem and adds (1), (2), etc which avoids collision with sidecars
|
# Photos checks the stem and adds (1), (2), etc which avoids collision with sidecars
|
||||||
# e.g. exporting sidecar for file1.png and file1.jpeg
|
# e.g. exporting sidecar for file1.png and file1.jpeg
|
||||||
# if file1.png exists and exporting file1.jpeg,
|
# if file1.png exists and exporting file1.jpeg,
|
||||||
# dest will be file1 (1).jpeg even though file1.jpeg doesn't exist to prevent sidecar collision
|
# dest will be file1 (1).jpeg even though file1.jpeg doesn't exist to prevent sidecar collision
|
||||||
|
count = 0
|
||||||
if not update and increment and not overwrite:
|
if not update and increment and not overwrite:
|
||||||
count = 1
|
dest_files = findfiles(f"{dest_original.stem}*", str(dest_original.parent))
|
||||||
dest_files = findfiles(f"{dest.stem}*", str(dest.parent))
|
|
||||||
dest_files = [pathlib.Path(f).stem.lower() for f in dest_files]
|
dest_files = [pathlib.Path(f).stem.lower() for f in dest_files]
|
||||||
dest_new = dest.stem
|
dest_new = dest_original.stem
|
||||||
while dest_new.lower() in dest_files:
|
while dest_new.lower() in dest_files:
|
||||||
dest_new = f"{dest.stem} ({count})"
|
|
||||||
count += 1
|
count += 1
|
||||||
dest = dest.parent / f"{dest_new}{dest.suffix}"
|
dest_new = f"{dest_original.stem} ({count})"
|
||||||
|
dest_original = dest_original.parent / f"{dest_new}{dest_original.suffix}"
|
||||||
|
|
||||||
# if overwrite==False and #increment==False, export should fail if file exists
|
# if overwrite==False and #increment==False, export should fail if file exists
|
||||||
if dest.exists() and not update and not overwrite and not increment:
|
if (
|
||||||
|
dest_original.exists()
|
||||||
|
and export_original
|
||||||
|
and not update
|
||||||
|
and not overwrite
|
||||||
|
and not increment
|
||||||
|
):
|
||||||
raise FileExistsError(
|
raise FileExistsError(
|
||||||
f"destination exists ({dest}); overwrite={overwrite}, increment={increment}"
|
f"destination exists ({dest_original}); overwrite={overwrite}, increment={increment}"
|
||||||
)
|
)
|
||||||
|
|
||||||
self._render_options.filepath = str(dest)
|
if export_edited:
|
||||||
|
if not update and increment and not overwrite:
|
||||||
|
dest_files = findfiles(f"{dest_edited.stem}*", str(dest_edited.parent))
|
||||||
|
dest_files = [pathlib.Path(f).stem.lower() for f in dest_files]
|
||||||
|
dest_new = dest_edited.stem
|
||||||
|
if count:
|
||||||
|
# incremented above when checking original destination
|
||||||
|
dest_new = f"{dest_new} ({count})"
|
||||||
|
while dest_new.lower() in dest_files:
|
||||||
|
count += 1
|
||||||
|
dest_new = f"{dest.stem} ({count})"
|
||||||
|
dest_edited = dest_edited.parent / f"{dest_new}{dest_edited.suffix}"
|
||||||
|
|
||||||
|
# if overwrite==False and #increment==False, export should fail if file exists
|
||||||
|
if dest_edited.exists() and not update and not overwrite and not increment:
|
||||||
|
raise FileExistsError(
|
||||||
|
f"destination exists ({dest_edited}); overwrite={overwrite}, increment={increment}"
|
||||||
|
)
|
||||||
|
|
||||||
|
self._render_options.filepath = (
|
||||||
|
str(dest_original) if export_original else str(dest_edited)
|
||||||
|
)
|
||||||
all_results = ExportResults()
|
all_results = ExportResults()
|
||||||
if not use_photos_export:
|
|
||||||
|
if use_photos_export:
|
||||||
|
# TODO: collapse these into a single call (refactor _export_photo_with_photos_export)
|
||||||
|
if original:
|
||||||
|
self._export_photo_with_photos_export(
|
||||||
|
dest_original,
|
||||||
|
all_results,
|
||||||
|
fileutil,
|
||||||
|
export_db,
|
||||||
|
use_photokit=use_photokit,
|
||||||
|
dry_run=dry_run,
|
||||||
|
timeout=timeout,
|
||||||
|
jpeg_ext=jpeg_ext,
|
||||||
|
touch_file=touch_file,
|
||||||
|
update=update,
|
||||||
|
overwrite=overwrite,
|
||||||
|
live_photo=live_photo,
|
||||||
|
edited=False,
|
||||||
|
convert_to_jpeg=convert_to_jpeg,
|
||||||
|
jpeg_quality=jpeg_quality,
|
||||||
|
)
|
||||||
|
if edited:
|
||||||
|
self._export_photo_with_photos_export(
|
||||||
|
dest_edited,
|
||||||
|
all_results,
|
||||||
|
fileutil,
|
||||||
|
export_db,
|
||||||
|
use_photokit=use_photokit,
|
||||||
|
dry_run=dry_run,
|
||||||
|
timeout=timeout,
|
||||||
|
jpeg_ext=jpeg_ext,
|
||||||
|
touch_file=touch_file,
|
||||||
|
update=update,
|
||||||
|
overwrite=overwrite,
|
||||||
|
live_photo=live_photo,
|
||||||
|
edited=True,
|
||||||
|
convert_to_jpeg=convert_to_jpeg,
|
||||||
|
jpeg_quality=jpeg_quality,
|
||||||
|
)
|
||||||
|
else:
|
||||||
# find the source file on disk and export
|
# find the source file on disk and export
|
||||||
# get path to source file and verify it's not None and is valid file
|
# get path to source file and verify it's not None and is valid file
|
||||||
# TODO: how to handle ismissing or not hasadjustments and edited=True cases?
|
# TODO: how to handle ismissing or not hasadjustments and edited=True cases?
|
||||||
|
export_src_dest = []
|
||||||
if edited:
|
if edited:
|
||||||
if self.path_edited is not None:
|
if self.path_edited is not None:
|
||||||
src = self.path_edited
|
export_src_dest.append((self.path_edited, dest_edited))
|
||||||
else:
|
else:
|
||||||
raise FileNotFoundError(
|
raise FileNotFoundError(
|
||||||
f"Cannot export edited photo if path_edited is None"
|
f"Cannot export edited photo if path_edited is None"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
if self.path is not None:
|
if self.path is not None:
|
||||||
src = self.path
|
export_src_dest.append((self.path, dest_original))
|
||||||
else:
|
else:
|
||||||
raise FileNotFoundError("Cannot export photo if path is None")
|
raise FileNotFoundError("Cannot export photo if path is None")
|
||||||
|
|
||||||
if not os.path.isfile(src):
|
for src, dest in export_src_dest:
|
||||||
raise FileNotFoundError(f"{src} does not appear to exist")
|
if not pathlib.Path(src).is_file():
|
||||||
|
raise FileNotFoundError(f"{src} does not appear to exist")
|
||||||
|
|
||||||
# found source now try to find right destination
|
# found source now try to find right destination
|
||||||
if update and dest.exists():
|
if update and dest.exists():
|
||||||
# destination exists, check to see if destination is the right UUID
|
# destination exists, check to see if destination is the right UUID
|
||||||
dest_uuid = export_db.get_uuid_for_file(dest)
|
dest_uuid = export_db.get_uuid_for_file(dest)
|
||||||
if dest_uuid is None and fileutil.cmp(src, dest):
|
if dest_uuid is None and fileutil.cmp(src, dest):
|
||||||
# might be exporting into a pre-ExportDB folder or the DB got deleted
|
# might be exporting into a pre-ExportDB folder or the DB got deleted
|
||||||
dest_uuid = self.uuid
|
dest_uuid = self.uuid
|
||||||
export_db.set_data(
|
export_db.set_data(
|
||||||
filename=dest,
|
filename=dest,
|
||||||
uuid=self.uuid,
|
uuid=self.uuid,
|
||||||
orig_stat=fileutil.file_sig(dest),
|
orig_stat=fileutil.file_sig(dest),
|
||||||
exif_stat=(None, None, None),
|
exif_stat=(None, None, None),
|
||||||
converted_stat=(None, None, None),
|
converted_stat=(None, None, None),
|
||||||
edited_stat=(None, None, None),
|
edited_stat=(None, None, None),
|
||||||
info_json=self.json(),
|
info_json=self.json(),
|
||||||
exif_json=None,
|
exif_json=None,
|
||||||
)
|
)
|
||||||
if dest_uuid != self.uuid:
|
if dest_uuid != self.uuid:
|
||||||
# not the right file, find the right one
|
# not the right file, find the right one
|
||||||
count = 1
|
|
||||||
glob_str = str(dest.parent / f"{dest.stem} (*{dest.suffix}")
|
|
||||||
dest_files = glob.glob(glob_str)
|
|
||||||
found_match = False
|
|
||||||
for file_ in dest_files:
|
|
||||||
dest_uuid = export_db.get_uuid_for_file(file_)
|
|
||||||
if dest_uuid == self.uuid:
|
|
||||||
dest = pathlib.Path(file_)
|
|
||||||
found_match = True
|
|
||||||
break
|
|
||||||
elif dest_uuid is None and fileutil.cmp(src, file_):
|
|
||||||
# files match, update the UUID
|
|
||||||
dest = pathlib.Path(file_)
|
|
||||||
found_match = True
|
|
||||||
export_db.set_data(
|
|
||||||
filename=dest,
|
|
||||||
uuid=self.uuid,
|
|
||||||
orig_stat=fileutil.file_sig(dest),
|
|
||||||
exif_stat=(None, None, None),
|
|
||||||
converted_stat=(None, None, None),
|
|
||||||
edited_stat=(None, None, None),
|
|
||||||
info_json=self.json(),
|
|
||||||
exif_json=None,
|
|
||||||
)
|
|
||||||
break
|
|
||||||
|
|
||||||
if not found_match:
|
|
||||||
# increment the destination file
|
|
||||||
count = 1
|
count = 1
|
||||||
glob_str = str(dest.parent / f"{dest.stem}*")
|
glob_str = str(dest.parent / f"{dest.stem} (*{dest.suffix}")
|
||||||
dest_files = glob.glob(glob_str)
|
dest_files = glob.glob(glob_str)
|
||||||
dest_files = [pathlib.Path(f).stem for f in dest_files]
|
found_match = False
|
||||||
dest_new = dest.stem
|
for file_ in dest_files:
|
||||||
while dest_new in dest_files:
|
dest_uuid = export_db.get_uuid_for_file(file_)
|
||||||
dest_new = f"{dest.stem} ({count})"
|
if dest_uuid == self.uuid:
|
||||||
count += 1
|
dest = pathlib.Path(file_)
|
||||||
dest = dest.parent / f"{dest_new}{dest.suffix}"
|
found_match = True
|
||||||
|
break
|
||||||
|
elif dest_uuid is None and fileutil.cmp(src, file_):
|
||||||
|
# files match, update the UUID
|
||||||
|
dest = pathlib.Path(file_)
|
||||||
|
found_match = True
|
||||||
|
export_db.set_data(
|
||||||
|
filename=dest,
|
||||||
|
uuid=self.uuid,
|
||||||
|
orig_stat=fileutil.file_sig(dest),
|
||||||
|
exif_stat=(None, None, None),
|
||||||
|
converted_stat=(None, None, None),
|
||||||
|
edited_stat=(None, None, None),
|
||||||
|
info_json=self.json(),
|
||||||
|
exif_json=None,
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
# export the dest file
|
if not found_match:
|
||||||
results = self._export_photo(
|
# increment the destination file
|
||||||
src,
|
count = 1
|
||||||
dest,
|
glob_str = str(dest.parent / f"{dest.stem}*")
|
||||||
update,
|
dest_files = glob.glob(glob_str)
|
||||||
export_db,
|
dest_files = [pathlib.Path(f).stem for f in dest_files]
|
||||||
overwrite,
|
dest_new = dest.stem
|
||||||
export_as_hardlink,
|
while dest_new in dest_files:
|
||||||
exiftool,
|
dest_new = f"{dest.stem} ({count})"
|
||||||
touch_file,
|
count += 1
|
||||||
convert_to_jpeg,
|
dest = dest.parent / f"{dest_new}{dest.suffix}"
|
||||||
fileutil=fileutil,
|
|
||||||
edited=edited,
|
# export the dest file
|
||||||
jpeg_quality=jpeg_quality,
|
results = self._export_photo(
|
||||||
ignore_signature=ignore_signature,
|
src,
|
||||||
)
|
dest,
|
||||||
all_results += results
|
update,
|
||||||
|
export_db,
|
||||||
|
overwrite,
|
||||||
|
export_as_hardlink,
|
||||||
|
exiftool,
|
||||||
|
touch_file,
|
||||||
|
convert_to_jpeg,
|
||||||
|
fileutil=fileutil,
|
||||||
|
edited=edited,
|
||||||
|
jpeg_quality=jpeg_quality,
|
||||||
|
ignore_signature=ignore_signature,
|
||||||
|
)
|
||||||
|
all_results += results
|
||||||
|
|
||||||
|
dest = dest_original if export_original else dest_edited
|
||||||
|
|
||||||
# copy live photo associated .mov if requested
|
# copy live photo associated .mov if requested
|
||||||
if live_photo and self.live_photo:
|
if export_original and live_photo and self.live_photo and self.path_live_photo:
|
||||||
live_name = dest.parent / f"{dest.stem}.mov"
|
live_name = dest.parent / f"{dest.stem}.mov"
|
||||||
src_live = self.path_live_photo
|
src_live = self.path_live_photo
|
||||||
|
results = self._export_photo(
|
||||||
|
src_live,
|
||||||
|
live_name,
|
||||||
|
update,
|
||||||
|
export_db,
|
||||||
|
overwrite,
|
||||||
|
export_as_hardlink,
|
||||||
|
exiftool,
|
||||||
|
touch_file,
|
||||||
|
False,
|
||||||
|
fileutil=fileutil,
|
||||||
|
ignore_signature=ignore_signature,
|
||||||
|
)
|
||||||
|
all_results += results
|
||||||
|
|
||||||
if src_live is not None:
|
if (
|
||||||
results = self._export_photo(
|
export_edited
|
||||||
src_live,
|
and live_photo
|
||||||
live_name,
|
and self.live_photo
|
||||||
update,
|
and self.path_edited_live_photo
|
||||||
export_db,
|
):
|
||||||
overwrite,
|
live_name = dest.parent / f"{dest_edited.stem}.mov"
|
||||||
export_as_hardlink,
|
src_live = self.path_edited_live_photo
|
||||||
exiftool,
|
results = self._export_photo(
|
||||||
touch_file,
|
src_live,
|
||||||
False,
|
live_name,
|
||||||
fileutil=fileutil,
|
update,
|
||||||
ignore_signature=ignore_signature,
|
export_db,
|
||||||
)
|
overwrite,
|
||||||
all_results += results
|
export_as_hardlink,
|
||||||
|
exiftool,
|
||||||
|
touch_file,
|
||||||
|
False,
|
||||||
|
fileutil=fileutil,
|
||||||
|
ignore_signature=ignore_signature,
|
||||||
|
)
|
||||||
|
all_results += results
|
||||||
|
|
||||||
# copy associated RAW image if requested
|
# copy associated RAW image if requested
|
||||||
if raw_photo and self.has_raw:
|
if raw_photo and self.has_raw:
|
||||||
@@ -831,26 +926,30 @@ def export2(
|
|||||||
ignore_signature=ignore_signature,
|
ignore_signature=ignore_signature,
|
||||||
)
|
)
|
||||||
all_results += results
|
all_results += results
|
||||||
else:
|
|
||||||
self._export_photo_with_photos_export(
|
# copy preview image if requested
|
||||||
dest,
|
if preview and self.path_derivatives:
|
||||||
filename,
|
# Photos keeps multiple different derivatives and path_derivatives returns list of them
|
||||||
all_results,
|
# first derivative is the largest so export that one
|
||||||
fileutil,
|
preview_path = pathlib.Path(self.path_derivatives[0])
|
||||||
export_db,
|
preview_ext = preview_path.suffix
|
||||||
use_photokit=use_photokit,
|
preview_name = dest.parent / f"{dest.stem}{preview_suffix}{preview_ext}"
|
||||||
dry_run=dry_run,
|
if preview_path is not None:
|
||||||
timeout=timeout,
|
results = self._export_photo(
|
||||||
jpeg_ext=jpeg_ext,
|
preview_path,
|
||||||
touch_file=touch_file,
|
preview_name,
|
||||||
update=update,
|
update,
|
||||||
overwrite=overwrite,
|
export_db,
|
||||||
live_photo=live_photo,
|
overwrite,
|
||||||
edited=edited,
|
export_as_hardlink,
|
||||||
edited_identifier=edited_identifier,
|
exiftool,
|
||||||
convert_to_jpeg=convert_to_jpeg,
|
touch_file,
|
||||||
jpeg_quality=jpeg_quality,
|
convert_to_jpeg,
|
||||||
)
|
fileutil=fileutil,
|
||||||
|
jpeg_quality=jpeg_quality,
|
||||||
|
ignore_signature=ignore_signature,
|
||||||
|
)
|
||||||
|
all_results += results
|
||||||
|
|
||||||
# export metadata
|
# export metadata
|
||||||
sidecars = []
|
sidecars = []
|
||||||
@@ -861,6 +960,7 @@ def export2(
|
|||||||
sidecar_xmp_files_skipped = []
|
sidecar_xmp_files_skipped = []
|
||||||
sidecar_xmp_files_written = []
|
sidecar_xmp_files_written = []
|
||||||
|
|
||||||
|
dest = dest_original if export_original else dest_edited
|
||||||
dest_suffix = "" if sidecar_drop_ext else dest.suffix
|
dest_suffix = "" if sidecar_drop_ext else dest.suffix
|
||||||
if sidecar & SIDECAR_JSON:
|
if sidecar & SIDECAR_JSON:
|
||||||
sidecar_filename = dest.parent / pathlib.Path(f"{dest.stem}{dest_suffix}.json")
|
sidecar_filename = dest.parent / pathlib.Path(f"{dest.stem}{dest_suffix}.json")
|
||||||
@@ -1111,7 +1211,6 @@ def export2(
|
|||||||
def _export_photo_with_photos_export(
|
def _export_photo_with_photos_export(
|
||||||
self,
|
self,
|
||||||
dest,
|
dest,
|
||||||
filename,
|
|
||||||
all_results,
|
all_results,
|
||||||
fileutil,
|
fileutil,
|
||||||
export_db,
|
export_db,
|
||||||
@@ -1124,7 +1223,6 @@ def _export_photo_with_photos_export(
|
|||||||
overwrite=None,
|
overwrite=None,
|
||||||
live_photo=None,
|
live_photo=None,
|
||||||
edited=None,
|
edited=None,
|
||||||
edited_identifier=None,
|
|
||||||
convert_to_jpeg=None,
|
convert_to_jpeg=None,
|
||||||
jpeg_quality=1.0,
|
jpeg_quality=1.0,
|
||||||
):
|
):
|
||||||
@@ -1137,15 +1235,10 @@ def _export_photo_with_photos_export(
|
|||||||
# shared photos (in shared albums) show up as not having adjustments (not edited)
|
# 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
|
# 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
|
# so tell Photos to export the current version in this case
|
||||||
if filename:
|
# didn't get passed a filename, add _edited
|
||||||
# use filename stem provided
|
uti = self.uti_edited if edited and self.uti_edited else self.uti
|
||||||
filestem = dest.stem
|
ext = get_preferred_uti_extension(uti)
|
||||||
else:
|
dest = dest.parent / f"{dest.stem}.{ext}"
|
||||||
# didn't get passed a filename, add _edited
|
|
||||||
filestem = f"{dest.stem}{edited_identifier}"
|
|
||||||
uti = self.uti_edited if edited and self.uti_edited else self.uti
|
|
||||||
ext = get_preferred_uti_extension(uti)
|
|
||||||
dest = dest.parent / f"{filestem}{ext}"
|
|
||||||
|
|
||||||
if use_photokit:
|
if use_photokit:
|
||||||
photolib = PhotoLibrary()
|
photolib = PhotoLibrary()
|
||||||
@@ -1190,7 +1283,7 @@ def _export_photo_with_photos_export(
|
|||||||
exported = _export_photo_uuid_applescript(
|
exported = _export_photo_uuid_applescript(
|
||||||
self.uuid,
|
self.uuid,
|
||||||
dest.parent,
|
dest.parent,
|
||||||
filestem=filestem,
|
filestem=dest.stem,
|
||||||
original=False,
|
original=False,
|
||||||
edited=True,
|
edited=True,
|
||||||
live_photo=live_photo,
|
live_photo=live_photo,
|
||||||
@@ -1204,7 +1297,6 @@ def _export_photo_with_photos_export(
|
|||||||
all_results.error.append((str(dest), f"{e} ({lineno(__file__)})"))
|
all_results.error.append((str(dest), f"{e} ({lineno(__file__)})"))
|
||||||
else:
|
else:
|
||||||
# export original version and not edited
|
# export original version and not edited
|
||||||
filestem = dest.stem
|
|
||||||
if use_photokit:
|
if use_photokit:
|
||||||
photolib = PhotoLibrary()
|
photolib = PhotoLibrary()
|
||||||
photo = None
|
photo = None
|
||||||
@@ -1241,7 +1333,7 @@ def _export_photo_with_photos_export(
|
|||||||
exported = _export_photo_uuid_applescript(
|
exported = _export_photo_uuid_applescript(
|
||||||
self.uuid,
|
self.uuid,
|
||||||
dest.parent,
|
dest.parent,
|
||||||
filestem=filestem,
|
filestem=dest.stem,
|
||||||
original=True,
|
original=True,
|
||||||
edited=False,
|
edited=False,
|
||||||
live_photo=live_photo,
|
live_photo=live_photo,
|
||||||
@@ -1608,7 +1700,9 @@ def _exiftool_dict(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if description_template is not None:
|
if description_template is not None:
|
||||||
options = dataclasses.replace(self._render_options, expand_inplace=True, inplace_sep=", ")
|
options = dataclasses.replace(
|
||||||
|
self._render_options, expand_inplace=True, inplace_sep=", "
|
||||||
|
)
|
||||||
rendered = self.render_template(description_template, options)[0]
|
rendered = self.render_template(description_template, options)[0]
|
||||||
description = " ".join(rendered) if rendered else ""
|
description = " ".join(rendered) if rendered else ""
|
||||||
exif["EXIF:ImageDescription"] = description
|
exif["EXIF:ImageDescription"] = description
|
||||||
@@ -1647,7 +1741,9 @@ def _exiftool_dict(
|
|||||||
|
|
||||||
if keyword_template:
|
if keyword_template:
|
||||||
rendered_keywords = []
|
rendered_keywords = []
|
||||||
options = dataclasses.replace(self._render_options, none_str=_OSXPHOTOS_NONE_SENTINEL, path_sep="/")
|
options = dataclasses.replace(
|
||||||
|
self._render_options, none_str=_OSXPHOTOS_NONE_SENTINEL, path_sep="/"
|
||||||
|
)
|
||||||
for template_str in keyword_template:
|
for template_str in keyword_template:
|
||||||
rendered, unmatched = self.render_template(template_str, options)
|
rendered, unmatched = self.render_template(template_str, options)
|
||||||
if unmatched:
|
if unmatched:
|
||||||
@@ -1925,7 +2021,9 @@ def _xmp_sidecar(
|
|||||||
extension = extension.suffix[1:] if extension.suffix else None
|
extension = extension.suffix[1:] if extension.suffix else None
|
||||||
|
|
||||||
if description_template is not None:
|
if description_template is not None:
|
||||||
options = dataclasses.replace(self._render_options, expand_inplace=True, inplace_sep=", ")
|
options = dataclasses.replace(
|
||||||
|
self._render_options, expand_inplace=True, inplace_sep=", "
|
||||||
|
)
|
||||||
rendered = self.render_template(description_template, options)[0]
|
rendered = self.render_template(description_template, options)[0]
|
||||||
description = " ".join(rendered) if rendered else ""
|
description = " ".join(rendered) if rendered else ""
|
||||||
else:
|
else:
|
||||||
@@ -1958,7 +2056,9 @@ def _xmp_sidecar(
|
|||||||
|
|
||||||
if keyword_template:
|
if keyword_template:
|
||||||
rendered_keywords = []
|
rendered_keywords = []
|
||||||
options = dataclasses.replace(self._render_options, none_str=_OSXPHOTOS_NONE_SENTINEL, path_sep="/")
|
options = dataclasses.replace(
|
||||||
|
self._render_options, none_str=_OSXPHOTOS_NONE_SENTINEL, path_sep="/"
|
||||||
|
)
|
||||||
for template_str in keyword_template:
|
for template_str in keyword_template:
|
||||||
rendered, unmatched = self.render_template(template_str, options)
|
rendered, unmatched = self.render_template(template_str, options)
|
||||||
if unmatched:
|
if unmatched:
|
||||||
|
|||||||
@@ -342,6 +342,37 @@ class PhotoInfo:
|
|||||||
|
|
||||||
return photopath
|
return photopath
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path_edited_live_photo(self):
|
||||||
|
"""return path to edited version of live photo movie; only valid for Photos 5+"""
|
||||||
|
if self._db._db_version < _PHOTOS_5_VERSION:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
return self._path_edited_live_photo
|
||||||
|
except AttributeError:
|
||||||
|
self._path_edited_live_photo = self._path_edited_5_live_photo()
|
||||||
|
return self._path_edited_live_photo
|
||||||
|
|
||||||
|
def _path_edited_5_live_photo(self):
|
||||||
|
"""return path_edited_live_photo for Photos >= 5"""
|
||||||
|
if self._db._db_version < _PHOTOS_5_VERSION:
|
||||||
|
raise RuntimeError("Wrong database format!")
|
||||||
|
|
||||||
|
if self.live_photo and self._info["hasAdjustments"]:
|
||||||
|
library = self._db._library_path
|
||||||
|
directory = self._uuid[0] # first char of uuid
|
||||||
|
filename = f"{self._uuid}_2_100_a.mov"
|
||||||
|
photopath = os.path.join(
|
||||||
|
library, "resources", "renders", directory, filename
|
||||||
|
)
|
||||||
|
if not os.path.isfile(photopath):
|
||||||
|
photopath = None
|
||||||
|
else:
|
||||||
|
photopath = None
|
||||||
|
|
||||||
|
return photopath
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path_raw(self):
|
def path_raw(self):
|
||||||
"""absolute path of associated RAW image or None if there is not one"""
|
"""absolute path of associated RAW image or None if there is not one"""
|
||||||
|
|||||||
@@ -127,6 +127,8 @@ UUID_DICT_LOCAL = {
|
|||||||
"burst_not_selected": "89E235DD-B9AC-4E8D-BDA2-986981CA7582", # IMG_9813.JPG
|
"burst_not_selected": "89E235DD-B9AC-4E8D-BDA2-986981CA7582", # IMG_9813.JPG
|
||||||
"burst_default": "F5E6BD24-B493-44E9-BDA2-7AD9D2CC8C9D", # IMG_9816.JPG
|
"burst_default": "F5E6BD24-B493-44E9-BDA2-7AD9D2CC8C9D", # IMG_9816.JPG
|
||||||
"burst_not_default": "75154738-83AA-4DCD-A913-632D5D1C0FEE", # IMG_9814.JPG
|
"burst_not_default": "75154738-83AA-4DCD-A913-632D5D1C0FEE", # IMG_9814.JPG
|
||||||
|
"live_edited": "54A01B04-16D7-4FDE-8860-19F2A641E433", # IMG_3203.HEIC
|
||||||
|
"live": "8EC216A2-0032-4934-BD3F-04C6259B3304", # IMG_3259.HEIC
|
||||||
}
|
}
|
||||||
|
|
||||||
UUID_PUMPKIN_FARM = [
|
UUID_PUMPKIN_FARM = [
|
||||||
@@ -1273,6 +1275,20 @@ def test_burst_default_pic(photosdb_local):
|
|||||||
assert not photo.burst_default_pick
|
assert not photo.burst_default_pick
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(SKIP_TEST, reason="Skip if not running on author's local machine.")
|
||||||
|
def test_path_edited_live_photo(photosdb_local):
|
||||||
|
"""test path_edited_live_photo (needs image from local library)"""
|
||||||
|
photo = photosdb_local.get_photo(UUID_DICT_LOCAL["live_edited"])
|
||||||
|
assert photo.path_edited_live_photo is not None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(SKIP_TEST, reason="Skip if not running on author's local machine.")
|
||||||
|
def test_path_edited_live_photo_not_edited(photosdb_local):
|
||||||
|
"""test path_edited_live_photo for a live photo that's not edited (needs image from local library)"""
|
||||||
|
photo = photosdb_local.get_photo(UUID_DICT_LOCAL["live"])
|
||||||
|
assert photo.path_edited_live_photo is None
|
||||||
|
|
||||||
|
|
||||||
def test_is_reference(photosdb):
|
def test_is_reference(photosdb):
|
||||||
"""test isreference"""
|
"""test isreference"""
|
||||||
|
|
||||||
@@ -1377,7 +1393,7 @@ def test_duplicates_2(photosdb):
|
|||||||
|
|
||||||
|
|
||||||
def test_compound_query(photosdb):
|
def test_compound_query(photosdb):
|
||||||
""" test photos() with multiple query terms """
|
"""test photos() with multiple query terms"""
|
||||||
photos = photosdb.photos(persons=["Katie", "Maria"], albums=["Multi Keyword"])
|
photos = photosdb.photos(persons=["Katie", "Maria"], albums=["Multi Keyword"])
|
||||||
|
|
||||||
assert len(photos) == 2
|
assert len(photos) == 2
|
||||||
@@ -1386,21 +1402,21 @@ def test_compound_query(photosdb):
|
|||||||
|
|
||||||
|
|
||||||
def test_multi_keyword(photosdb):
|
def test_multi_keyword(photosdb):
|
||||||
""" test photos() with multiple keywords """
|
"""test photos() with multiple keywords"""
|
||||||
photos = photosdb.photos(keywords=["Kids", "wedding"])
|
photos = photosdb.photos(keywords=["Kids", "wedding"])
|
||||||
|
|
||||||
assert len(photos) == 6
|
assert len(photos) == 6
|
||||||
|
|
||||||
|
|
||||||
def test_multi_album(photosdb):
|
def test_multi_album(photosdb):
|
||||||
""" test photos() with multiple albums """
|
"""test photos() with multiple albums"""
|
||||||
photos = photosdb.photos(albums=["Pumpkin Farm", "Test Album"])
|
photos = photosdb.photos(albums=["Pumpkin Farm", "Test Album"])
|
||||||
|
|
||||||
assert len(photos) == 3
|
assert len(photos) == 3
|
||||||
|
|
||||||
|
|
||||||
def test_multi_uuid(photosdb):
|
def test_multi_uuid(photosdb):
|
||||||
""" test photos() with multiple uuids """
|
"""test photos() with multiple uuids"""
|
||||||
photos = photosdb.photos(uuid=[UUID_DICT["favorite"], UUID_DICT["not_favorite"]])
|
photos = photosdb.photos(uuid=[UUID_DICT["favorite"], UUID_DICT["not_favorite"]])
|
||||||
|
|
||||||
assert len(photos) == 2
|
assert len(photos) == 2
|
||||||
|
|||||||
Reference in New Issue
Block a user