More refactoring of export code, #462

This commit is contained in:
Rhet Turnbull
2022-01-02 22:38:22 -08:00
parent a73dc72558
commit 147b30f973
3 changed files with 237 additions and 225 deletions

View File

@@ -4207,8 +4207,12 @@ def repl(ctx, cli_obj, db, emacs):
from osxphotos import ExifTool, PhotoInfo, PhotosDB from osxphotos import ExifTool, PhotoInfo, PhotosDB
from osxphotos.albuminfo import AlbumInfo from osxphotos.albuminfo import AlbumInfo
from osxphotos.momentinfo import MomentInfo
from osxphotos.photoexporter import ExportResults, PhotoExporter
from osxphotos.placeinfo import PlaceInfo from osxphotos.placeinfo import PlaceInfo
from osxphotos.queryoptions import QueryOptions from osxphotos.queryoptions import QueryOptions
from osxphotos.scoreinfo import ScoreInfo
from osxphotos.searchinfo import SearchInfo
logger = logging.getLogger() logger = logging.getLogger()
logger.disabled = True logger.disabled = True
@@ -4241,7 +4245,9 @@ def repl(ctx, cli_obj, db, emacs):
print(f"Found {len(photos)} photos in {tictoc:0.2f} seconds\n") print(f"Found {len(photos)} photos in {tictoc:0.2f} seconds\n")
print("The following classes have been imported from osxphotos:") print("The following classes have been imported from osxphotos:")
print("- AlbumInfo, ExifTool, PhotoInfo, PhotosDB, PlaceInfo, QueryOptions\n") print(
"- AlbumInfo, ExifTool, PhotoInfo, PhotoExporter, ExportResults, PhotosDB, PlaceInfo, QueryOptions, MomentInfo, ScoreInfo, SearchInfo\n"
)
print("The following variables are defined:") print("The following variables are defined:")
print(f"- photosdb: PhotosDB() instance for {photosdb.library_path}") print(f"- photosdb: PhotosDB() instance for {photosdb.library_path}")
print( print(

View File

@@ -369,7 +369,7 @@ class PhotoExporter:
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 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 (only one of original or edited can be used)
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
@@ -445,7 +445,6 @@ class PhotoExporter:
# NOTE: This function is very complex and does a lot of things. # NOTE: This function is very complex and does a lot of things.
# Don't modify this code if you don't fully understand everything it does. # Don't modify this code if you don't fully understand everything it does.
# TODO: This is a good candidate for refactoring.
# when called from export(), won't get an export_db, so use no-op version # when called from export(), won't get an export_db, so use no-op version
if export_db is None: if export_db is None:
@@ -461,7 +460,9 @@ class PhotoExporter:
export_original = original export_original = original
export_edited = edited export_edited = edited
if edited and not self.photo.hasadjustments: if export_original and export_edited:
raise ValueError("Cannot export both original and edited photos")
if export_edited and not self.photo.hasadjustments:
raise ValueError( raise ValueError(
"Photo does not have adjustments, cannot export edited version" "Photo does not have adjustments, cannot export edited version"
) )
@@ -475,24 +476,12 @@ class PhotoExporter:
original_filename = original_filename or self.photo.original_filename original_filename = original_filename or self.photo.original_filename
dest_original = pathlib.Path(dest) / original_filename dest_original = pathlib.Path(dest) / original_filename
if not edited_filename: edited_filename = edited_filename or self._get_edited_filename(
if not edited: original_filename
edited_filename = self.photo.original_filename
else:
original_name = pathlib.Path(self.photo.original_filename)
if self.photo.path_edited:
ext = pathlib.Path(self.photo.path_edited).suffix
else:
uti = (
self.photo.uti_edited
if edited and self.photo.uti_edited
else self.photo.uti
) )
ext = get_preferred_uti_extension(uti)
ext = "." + ext
edited_filename = original_name.stem + "_edited" + ext
dest_edited = pathlib.Path(dest) / edited_filename dest_edited = pathlib.Path(dest) / edited_filename
# Is there something to convert?
if convert_to_jpeg and self.photo.isphoto: if convert_to_jpeg and self.photo.isphoto:
something_to_convert = False something_to_convert = False
ext = "." + jpeg_ext if jpeg_ext else ".jpeg" ext = "." + jpeg_ext if jpeg_ext else ".jpeg"
@@ -508,56 +497,33 @@ class PhotoExporter:
else: else:
convert_to_jpeg = False convert_to_jpeg = False
# check to see if file exists and if so, add (1), (2), etc until we find one that works # TODO: need to look at this to see what happens if original not being exported but edited exists and already has an increment
# Photos checks the stem and adds (1), (2), etc which avoids collision with sidecars dest_original, increment_file_count = self._validate_dest_path(
# e.g. exporting sidecar for file1.png and file1.jpeg dest_original, increment=increment, update=update, overwrite=overwrite
# 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
increment_file_count = 0
if increment and not update and not overwrite:
dest_original, increment_file_count = increment_filename_with_count(
dest_original
) )
dest_original = pathlib.Path(dest_original) dest_original = pathlib.Path(dest_original)
# if overwrite==False and #increment==False, export should fail if file exists
if (
dest_original.exists()
and export_original
and not update
and not overwrite
and not increment
):
raise FileExistsError(
f"destination exists ({dest_original}); overwrite={overwrite}, increment={increment}"
)
if export_edited: if export_edited:
if increment and not update and not overwrite: dest_edited, increment_file_count = self._validate_dest_path(
dest_edited, increment_file_count = increment_filename_with_count( dest_edited,
dest_edited, increment_file_count increment=increment,
update=update,
overwrite=overwrite,
count=increment_file_count,
) )
dest_edited = pathlib.Path(dest_edited) dest_edited = pathlib.Path(dest_edited)
# 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 = ( self._render_options.filepath = (
str(dest_original) if export_original else str(dest_edited) str(dest_original) if export_original else str(dest_edited)
) )
all_results = ExportResults() all_results = ExportResults()
if 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( self._export_photo_with_photos_export(
dest_original, dest=dest_original if export_original else dest_edited,
all_results, all_results=all_results,
fileutil, fileutil=fileutil,
export_db, export_db=export_db,
use_photokit=use_photokit, use_photokit=use_photokit,
dry_run=dry_run, dry_run=dry_run,
timeout=timeout, timeout=timeout,
@@ -566,25 +532,7 @@ class PhotoExporter:
update=update, update=update,
overwrite=overwrite, overwrite=overwrite,
live_photo=live_photo, live_photo=live_photo,
edited=False, edited=export_edited,
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, convert_to_jpeg=convert_to_jpeg,
jpeg_quality=jpeg_quality, jpeg_quality=jpeg_quality,
) )
@@ -598,6 +546,7 @@ class PhotoExporter:
elif not edited and self.photo.path is not None: elif not edited and self.photo.path is not None:
export_src_dest.append((self.photo.path, dest_original)) export_src_dest.append((self.photo.path, dest_original))
# TODO: this for loop not necessary
for src, dest in export_src_dest: for src, dest in export_src_dest:
if not pathlib.Path(src).is_file(): if not pathlib.Path(src).is_file():
raise FileNotFoundError(f"{src} does not appear to exist") raise FileNotFoundError(f"{src} does not appear to exist")
@@ -1044,6 +993,49 @@ class PhotoExporter:
return all_results return all_results
def _get_edited_filename(self, original_filename):
"""Return the filename for the exported edited photo
(used when filename isn't provided in call to export2)"""
# need to get the right extension for edited file
original_filename = pathlib.Path(original_filename)
if self.photo.path_edited:
ext = pathlib.Path(self.photo.path_edited).suffix
else:
uti = self.photo.uti_edited if self.photo.uti_edited else self.photo.uti
ext = get_preferred_uti_extension(uti)
ext = "." + ext
edited_filename = original_filename.stem + "_edited" + ext
return edited_filename
def _validate_dest_path(self, dest, increment, update, overwrite, count=0):
"""If destination exists, add (1), (2), and so on to filename to get a valid destination
Args:
dest (str): Destination path
increment (bool): Whether to increment the filename if it already exists
update (bool): Whether running in update mode
overwrite (bool): Whether running in overwrite mode
count: optional counter to start from (if 0, start from 1)
Returns:
new dest path (pathlib.Path), increment count (int)
"""
# 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
# e.g. exporting sidecar for file1.png and 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
if increment and not update and not overwrite:
dest, count = increment_filename_with_count(dest, count=count)
dest = pathlib.Path(dest)
# if overwrite==False and #increment==False, export should fail if file exists
if dest.exists() and all([not x for x in [increment, update, overwrite]]):
raise FileExistsError(
f"destination exists ({dest}); overwrite={overwrite}, increment={increment}"
)
return dest, count
def _export_photo_with_photos_export( def _export_photo_with_photos_export(
self, self,
dest, dest,

View File

@@ -30,6 +30,7 @@ import Photos
import Quartz import Quartz
from Foundation import NSNotificationCenter, NSObject from Foundation import NSNotificationCenter, NSObject
from PyObjCTools import AppHelper from PyObjCTools import AppHelper
from wurlitzer import pipes
from .fileutil import FileUtil from .fileutil import FileUtil
from .uti import get_preferred_uti_extension from .uti import get_preferred_uti_extension
@@ -495,6 +496,7 @@ class PhotoAsset:
""" """
with objc.autorelease_pool(): with objc.autorelease_pool():
with pipes() as (out, err):
filename = ( filename = (
pathlib.Path(filename) pathlib.Path(filename)
if filename if filename
@@ -513,7 +515,10 @@ class PhotoAsset:
# export the raw component # export the raw component
resources = self._resources() resources = self._resources()
for resource in resources: for resource in resources:
if resource.type() == Photos.PHAssetResourceTypeAlternatePhoto: if (
resource.type()
== Photos.PHAssetResourceTypeAlternatePhoto
):
data = self._request_resource_data(resource) data = self._request_resource_data(resource)
ext = pathlib.Path(self.raw_filename).suffix[1:] ext = pathlib.Path(self.raw_filename).suffix[1:]
break break
@@ -807,10 +812,14 @@ class VideoAsset(PhotoAsset):
""" """
with objc.autorelease_pool(): with objc.autorelease_pool():
with pipes() as (out, err):
if self.slow_mo and version == PHOTOS_VERSION_CURRENT: if self.slow_mo and version == PHOTOS_VERSION_CURRENT:
return [ return [
self._export_slow_mo( self._export_slow_mo(
dest, filename=filename, version=version, overwrite=overwrite dest,
filename=filename,
version=version,
overwrite=overwrite,
) )
] ]
@@ -1053,6 +1062,7 @@ class LivePhotoAsset(PhotoAsset):
""" """
with objc.autorelease_pool(): with objc.autorelease_pool():
with pipes() as (out, err):
filename = ( filename = (
pathlib.Path(filename) pathlib.Path(filename)
if filename if filename
@@ -1091,8 +1101,12 @@ class LivePhotoAsset(PhotoAsset):
video_output_file = dest / f"{filename.stem}.{video_ext}" video_output_file = dest / f"{filename.stem}.{video_ext}"
if not overwrite: if not overwrite:
photo_output_file = pathlib.Path(increment_filename(photo_output_file)) photo_output_file = pathlib.Path(
video_output_file = pathlib.Path(increment_filename(video_output_file)) increment_filename(photo_output_file)
)
video_output_file = pathlib.Path(
increment_filename(video_output_file)
)
exported = [] exported = []
if photo: if photo: