diff --git a/README.md b/README.md index 02b83160..9b09dac6 100644 --- a/README.md +++ b/README.md @@ -363,6 +363,8 @@ Options: (see also --ignore-date-modified); QuickTime:GPSCoordinates; UserData:GPSCoordinates. + --exiftool-path EXIFTOOL_PATH Optionally specify path to exiftool; if not + provided, will look for exiftool in $PATH. --exiftool-option OPTION Optional flag/option to pass to exiftool when using --exiftool. For example, --exiftool-option '-m' to ignore minor diff --git a/osxphotos/__main__.py b/osxphotos/__main__.py index 77d96a5f..cc71c42f 100644 --- a/osxphotos/__main__.py +++ b/osxphotos/__main__.py @@ -1157,8 +1157,9 @@ def query( _list_libraries() return + photosdb = osxphotos.PhotosDB(dbfile=db, verbose=verbose_) photos = _query( - db=db, + photosdb=photosdb, keyword=keyword, person=person, album=album, @@ -1383,6 +1384,12 @@ def query( "(video files only): QuickTime:CreationDate; QuickTime:CreateDate; QuickTime:ModifyDate (see also --ignore-date-modified); " "QuickTime:GPSCoordinates; UserData:GPSCoordinates.", ) +@click.option( + "--exiftool-path", + metavar="EXIFTOOL_PATH", + type=click.Path(exists=True), + help="Optionally specify path to exiftool; if not provided, will look for exiftool in $PATH.", +) @click.option( "--exiftool-option", multiple=True, @@ -1601,6 +1608,7 @@ def export( download_missing, dest, exiftool, + exiftool_path, exiftool_option, exiftool_merge_keywords, exiftool_merge_persons, @@ -1735,6 +1743,7 @@ def export( not_live = cfg.not_live download_missing = cfg.download_missing exiftool = cfg.exiftool + exiftool_path = cfg.exiftool_path exiftool_option = cfg.exiftool_option exiftool_merge_keywords = cfg.exiftool_merge_keywords exiftool_merge_persons = cfg.exiftool_merge_persons @@ -1813,6 +1822,7 @@ def export( ("exiftool_option", ("exiftool")), ("exiftool_merge_keywords", ("exiftool", "sidecar")), ("exiftool_merge_persons", ("exiftool", "sidecar")), + ("exiftool_path", ("exiftool")), ] try: cfg.validate(exclusive=exclusive_options, dependent=dependent_options, cli=True) @@ -1881,10 +1891,10 @@ def export( not x for x in [skip_edited, skip_bursts, skip_live, skip_raw] ] - # verify exiftool installed an in path - if exiftool: + # verify exiftool installed and in path if path not provided + if exiftool and not exiftool_path: try: - _ = get_exiftool_path() + exiftool_path = get_exiftool_path() except FileNotFoundError: click.echo( click.style( @@ -1896,6 +1906,9 @@ def export( ) ctx.exit(2) + if exiftool: + verbose_(f"exiftool path: {exiftool_path}") + isphoto = ismovie = True # default searches for everything if only_movies: isphoto = False @@ -1977,8 +1990,9 @@ def export( f"Upgraded export database {export_db_path} from version {upgraded[0]} to {upgraded[1]}" ) + photosdb = osxphotos.PhotosDB(dbfile=db, verbose=verbose_, exiftool=exiftool_path) photos = _query( - db=db, + photosdb=photosdb, keyword=keyword, person=person, album=album, @@ -2327,7 +2341,7 @@ def print_photo_info(photos, json=False): def _query( - db=None, + photosdb, keyword=None, person=None, album=None, @@ -2386,12 +2400,12 @@ def _query( has_likes=False, no_likes=False, ): - """run a query against PhotosDB to extract the photos based on user supply criteria - used by query and export commands - arguments must be passed in same order as query and export - if either is modified, need to ensure all three functions are updated""" + """ Run a query against PhotosDB to extract the photos based on user supply criteria used by query and export commands + + Args: + photosdb: PhotosDB object + """ - photosdb = osxphotos.PhotosDB(dbfile=db, verbose=verbose_) if deleted or deleted_only: photos = photosdb.photos( uuid=uuid, diff --git a/osxphotos/_version.py b/osxphotos/_version.py index 23a6c2b9..88b59c42 100644 --- a/osxphotos/_version.py +++ b/osxphotos/_version.py @@ -1,5 +1,5 @@ """ version info """ -__version__ = "0.38.19" +__version__ = "0.38.20" diff --git a/osxphotos/exiftool.py b/osxphotos/exiftool.py index 777d19b4..cc31fc36 100644 --- a/osxphotos/exiftool.py +++ b/osxphotos/exiftool.py @@ -49,7 +49,7 @@ class _ExifToolProc: if hasattr(self, "_process_running") and self._process_running: # already running - if exiftool is not None: + if exiftool != self._exiftool: logging.warning( f"exiftool subprocess already running, " f"ignoring exiftool={exiftool}" @@ -266,7 +266,7 @@ class ExifTool: + b"\n" + b"-execute\n" ) - + # send the command self._process.stdin.write(command_str) self._process.stdin.flush() diff --git a/osxphotos/photoinfo/_photoinfo_exiftool.py b/osxphotos/photoinfo/_photoinfo_exiftool.py index 74e10da0..381bcc31 100644 --- a/osxphotos/photoinfo/_photoinfo_exiftool.py +++ b/osxphotos/photoinfo/_photoinfo_exiftool.py @@ -18,12 +18,11 @@ def exiftool(self): return self._exiftool except AttributeError: try: - exiftool_path = get_exiftool_path() + exiftool_path = self._db._exiftool_path or get_exiftool_path() if self.path is not None and os.path.isfile(self.path): - exiftool = ExifTool(self.path) + exiftool = ExifTool(self.path, exiftool=exiftool_path) else: exiftool = None - logging.debug(f"exiftool: missing path {self.uuid}") except FileNotFoundError: # get_exiftool_path raises FileNotFoundError if exiftool not found exiftool = None diff --git a/osxphotos/photoinfo/_photoinfo_export.py b/osxphotos/photoinfo/_photoinfo_export.py index fbd9a940..1ad76d75 100644 --- a/osxphotos/photoinfo/_photoinfo_export.py +++ b/osxphotos/photoinfo/_photoinfo_export.py @@ -1358,7 +1358,7 @@ def _write_exif_data( merge_exif_persons=merge_exif_persons, ) - with ExifTool(filepath, flags=flags) as exiftool: + with ExifTool(filepath, flags=flags, exiftool=self._db._exiftool_path) as exiftool: for exiftag, val in exif_info.items(): if type(val) == list: for v in val: diff --git a/osxphotos/photosdb/photosdb.py b/osxphotos/photosdb/photosdb.py index f85df9a6..c4d3a0b3 100644 --- a/osxphotos/photosdb/photosdb.py +++ b/osxphotos/photosdb/photosdb.py @@ -70,12 +70,13 @@ class PhotosDB: from ._photosdb_process_scoreinfo import _process_scoreinfo from ._photosdb_process_comments import _process_comments - def __init__(self, dbfile=None, verbose=None): + def __init__(self, dbfile=None, verbose=None, exiftool=None): """ Create a new PhotosDB object. Args: dbfile: specify full path to photos library or photos.db; if None, will attempt to locate last library opened by Photos. verbose: optional callable function to use for printing verbose text during processing; if None (default), does not print output. + exiftool: optional path to exiftool for methods that require this (e.g. PhotoInfo.exiftool); if not provided, will search PATH Raises: FileNotFoundError if dbfile is not a valid Photos library. @@ -98,6 +99,8 @@ class PhotosDB: raise TypeError("verbose must be callable") self._verbose = verbose + self._exiftool_path = exiftool + # create a temporary directory # tempfile.TemporaryDirectory gets cleaned up when the object does self._tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_") diff --git a/tests/test_cli.py b/tests/test_cli.py index 58d2a6ba..4642245c 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1055,6 +1055,52 @@ def test_export_exiftool(): assert exif[key] == CLI_EXIFTOOL[uuid][key] +@pytest.mark.skipif(exiftool is None, reason="exiftool not installed") +def test_export_exiftool_path(): + """ test --exiftool with --exiftool-path """ + import glob + import os + import os.path + import shutil + import tempfile + from osxphotos.__main__ import export + from osxphotos.exiftool import ExifTool, get_exiftool_path + + runner = CliRunner() + cwd = os.getcwd() + # pylint: disable=not-context-manager + with runner.isolated_filesystem(): + tempdir = tempfile.TemporaryDirectory() + exiftool_source = get_exiftool_path() + exiftool_path = os.path.join(tempdir.name, "myexiftool") + shutil.copy2(exiftool_source, exiftool_path) + for uuid in CLI_EXIFTOOL: + result = runner.invoke( + export, + [ + os.path.join(cwd, PHOTOS_DB_15_6), + ".", + "-V", + "--exiftool", + "--uuid", + f"{uuid}", + "--exiftool-path", + exiftool_path, + ], + ) + assert result.exit_code == 0 + assert f"exiftool path: {exiftool_path}" in result.output + files = glob.glob("*") + assert sorted(files) == sorted([CLI_EXIFTOOL[uuid]["File:FileName"]]) + + exif = ExifTool(CLI_EXIFTOOL[uuid]["File:FileName"]).asdict() + for key in CLI_EXIFTOOL[uuid]: + if type(exif[key]) == list: + assert sorted(exif[key]) == sorted(CLI_EXIFTOOL[uuid][key]) + else: + assert exif[key] == CLI_EXIFTOOL[uuid][key] + + @pytest.mark.skipif(exiftool is None, reason="exiftool not installed") def test_export_exiftool_ignore_date_modified(): import glob