diff --git a/README.md b/README.md index def29c58..bc0b521d 100644 --- a/README.md +++ b/README.md @@ -356,6 +356,16 @@ Options: to a filesystem that doesn't support Mac OS extended attributes. Only use this if you get an error while exporting. + --use-photos-export Force the use of AppleScript or PhotoKit to + export even if not missing (see also '-- + download-missing' and '--use-photokit'). + --use-photokit Use with '--download-missing' or '--use- + photos-export' to use direct Photos + interface instead of AppleScript to export. + Highly experimental alpha feature; does not + work with iTerm2 (use with Terminal.app). + This is faster and more reliable than the + default AppleScript interface. -h, --help Show this message and exit. ** Export ** diff --git a/osxphotos/__main__.py b/osxphotos/__main__.py index 19b8f4ff..9b811040 100644 --- a/osxphotos/__main__.py +++ b/osxphotos/__main__.py @@ -28,6 +28,7 @@ from .export_db import ExportDB, ExportDBInMemory from .fileutil import FileUtil, FileUtilNoOp from .path_utils import is_valid_filepath, sanitize_filename, sanitize_filepath from .photoinfo import ExportResults +from .photokit import check_photokit_authorization, request_photokit_authorization from .phototemplate import TEMPLATE_SUBSTITUTIONS, TEMPLATE_SUBSTITUTIONS_MULTI_VALUED # global variable to control verbose output @@ -1394,15 +1395,15 @@ def query( "--use-photos-export", is_flag=True, default=False, - hidden=True, - help="Force the use of AppleScript to export even if not missing (see also --download-missing).", + help="Force the use of AppleScript or PhotoKit to export even if not missing (see also '--download-missing' and '--use-photokit').", ) @click.option( "--use-photokit", is_flag=True, default=False, - hidden=True, - help="Use PhotoKit interface instead of AppleScript to export. Highly experimental alpha feature.", + help="Use with '--download-missing' or '--use-photos-export' to use direct Photos interface instead of AppleScript to export. " + "Highly experimental alpha feature; does not work with iTerm2 (use with Terminal.app). " + "This is faster and more reliable than the default AppleScript interface.", ) @DB_ARGUMENT @click.argument("dest", nargs=1, type=click.Path(exists=True)) @@ -1545,6 +1546,18 @@ def export( click.echo(cli.commands["export"].get_help(ctx), err=True) return + if use_photokit and not check_photokit_authorization(): + click.echo( + "Requesting access to use your Photos library. Click 'OK' on the dialog box to grant access." + ) + request_photokit_authorization() + click.confirm("Have you granted access?") + if not check_photokit_authorization(): + click.echo( + "Failed to get access to the Photos library which is needed with `--use-photokit`." + ) + return + # initialize export flags # by default, will export all versions of photos unless skip flag is set (export_edited, export_bursts, export_live, export_raw) = [ diff --git a/osxphotos/_version.py b/osxphotos/_version.py index 6c595ed0..0b0571c2 100644 --- a/osxphotos/_version.py +++ b/osxphotos/_version.py @@ -1,4 +1,4 @@ """ version info """ -__version__ = "0.36.20" +__version__ = "0.36.21" diff --git a/osxphotos/photokit.py b/osxphotos/photokit.py index 1527280e..72ac7b55 100644 --- a/osxphotos/photokit.py +++ b/osxphotos/photokit.py @@ -80,6 +80,58 @@ def path_to_NSURL(path): return url +def check_photokit_authorization(): + """ Check authorization to use user's Photos Library + + Returns: + True if user has authorized access to the Photos library, otherwise False + """ + + auth_status = Photos.PHPhotoLibrary.authorizationStatus() + return auth_status == Photos.PHAuthorizationStatusAuthorized + + +def request_photokit_authorization(): + """ Request authorization to user's Photos Library + + Returns: + authorization status + + Note: In actual practice, the terminal process running the python code + will do the actual request. + """ + + (_, major, _) = _get_os_version() + + def handler(status): + pass + + auth_status = 0 + if int(major) < 16: + auth_status = Photos.PHPhotoLibrary.authorizationStatus() + if auth_status != Photos.PHAuthorizationStatusAuthorized: + # it seems the first try fails after Terminal prompts user for access so try again + for _ in range(2): + Photos.PHPhotoLibrary.requestAuthorization_(handler) + auth_status = Photos.PHPhotoLibrary.authorizationStatus() + if auth_status == Photos.PHAuthorizationStatusAuthorized: + break + else: + # requestAuthorization deprecated in 10.16/11.0 + # but requestAuthorizationForAccessLevel not yet implemented in pyobjc (will be in ver 7.0) + # https://developer.apple.com/documentation/photokit/phphotolibrary/3616053-requestauthorizationforaccesslev?language=objc + auth_status = Photos.PHPhotoLibrary.authorizationStatus() + if auth_status != Photos.PHAuthorizationStatusAuthorized: + # it seems the first try fails after Terminal prompts user for access so try again + for _ in range(2): + Photos.PHPhotoLibrary.requestAuthorization_(handler) + auth_status = Photos.PHPhotoLibrary.authorizationStatus() + if auth_status == Photos.PHAuthorizationStatusAuthorized: + break + + return auth_status + + ### exceptions class PhotoKitError(Exception): """Base class for exceptions in this module. """ @@ -1051,12 +1103,6 @@ class PhotoLibrary: # get image manager and request options self._phimagemanager = Photos.PHCachingImageManager.defaultManager() - def _auth_status(self, status): - """ Handler for requestAuthorization_ """ - # This doesn't actually get called but requestAuthorization needs a callable handler - # The Terminal will handle the actual authorization when called - pass - def request_authorization(self): """ Request authorization to user's Photos Library @@ -1064,33 +1110,8 @@ class PhotoLibrary: authorization status """ - (_, major, _) = _get_os_version() - - auth_status = 0 - if int(major) < 16: - auth_status = Photos.PHPhotoLibrary.authorizationStatus() - if auth_status != Photos.PHAuthorizationStatusAuthorized: - # it seems the first try fails after Terminal prompts user for access so try again - for _ in range(2): - Photos.PHPhotoLibrary.requestAuthorization_(self._auth_status) - auth_status = Photos.PHPhotoLibrary.authorizationStatus() - if auth_status == Photos.PHAuthorizationStatusAuthorized: - break - else: - # requestAuthorization deprecated in 10.16/11.0 - # but requestAuthorizationForAccessLevel not yet implemented in pyobjc (will be in ver 7.0) - # https://developer.apple.com/documentation/photokit/phphotolibrary/3616053-requestauthorizationforaccesslev?language=objc - auth_status = Photos.PHPhotoLibrary.authorizationStatus() - if auth_status != Photos.PHAuthorizationStatusAuthorized: - # it seems the first try fails after Terminal prompts user for access so try again - for _ in range(2): - Photos.PHPhotoLibrary.requestAuthorization_(self._auth_status) - auth_status = Photos.PHPhotoLibrary.authorizationStatus() - if auth_status == Photos.PHAuthorizationStatusAuthorized: - break - - self.auth_status = auth_status - return auth_status + self.auth_status = request_photokit_authorization() + return self.auth_status def fetch_uuid_list(self, uuid_list): """ fetch PHAssets with uuids in uuid_list