Added --exiftool-path to CLI
This commit is contained in:
@@ -363,6 +363,8 @@ Options:
|
|||||||
(see also --ignore-date-modified);
|
(see also --ignore-date-modified);
|
||||||
QuickTime:GPSCoordinates;
|
QuickTime:GPSCoordinates;
|
||||||
UserData: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
|
--exiftool-option OPTION Optional flag/option to pass to exiftool
|
||||||
when using --exiftool. For example,
|
when using --exiftool. For example,
|
||||||
--exiftool-option '-m' to ignore minor
|
--exiftool-option '-m' to ignore minor
|
||||||
|
|||||||
@@ -1157,8 +1157,9 @@ def query(
|
|||||||
_list_libraries()
|
_list_libraries()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
photosdb = osxphotos.PhotosDB(dbfile=db, verbose=verbose_)
|
||||||
photos = _query(
|
photos = _query(
|
||||||
db=db,
|
photosdb=photosdb,
|
||||||
keyword=keyword,
|
keyword=keyword,
|
||||||
person=person,
|
person=person,
|
||||||
album=album,
|
album=album,
|
||||||
@@ -1383,6 +1384,12 @@ def query(
|
|||||||
"(video files only): QuickTime:CreationDate; QuickTime:CreateDate; QuickTime:ModifyDate (see also --ignore-date-modified); "
|
"(video files only): QuickTime:CreationDate; QuickTime:CreateDate; QuickTime:ModifyDate (see also --ignore-date-modified); "
|
||||||
"QuickTime:GPSCoordinates; UserData:GPSCoordinates.",
|
"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(
|
@click.option(
|
||||||
"--exiftool-option",
|
"--exiftool-option",
|
||||||
multiple=True,
|
multiple=True,
|
||||||
@@ -1601,6 +1608,7 @@ def export(
|
|||||||
download_missing,
|
download_missing,
|
||||||
dest,
|
dest,
|
||||||
exiftool,
|
exiftool,
|
||||||
|
exiftool_path,
|
||||||
exiftool_option,
|
exiftool_option,
|
||||||
exiftool_merge_keywords,
|
exiftool_merge_keywords,
|
||||||
exiftool_merge_persons,
|
exiftool_merge_persons,
|
||||||
@@ -1735,6 +1743,7 @@ def export(
|
|||||||
not_live = cfg.not_live
|
not_live = cfg.not_live
|
||||||
download_missing = cfg.download_missing
|
download_missing = cfg.download_missing
|
||||||
exiftool = cfg.exiftool
|
exiftool = cfg.exiftool
|
||||||
|
exiftool_path = cfg.exiftool_path
|
||||||
exiftool_option = cfg.exiftool_option
|
exiftool_option = cfg.exiftool_option
|
||||||
exiftool_merge_keywords = cfg.exiftool_merge_keywords
|
exiftool_merge_keywords = cfg.exiftool_merge_keywords
|
||||||
exiftool_merge_persons = cfg.exiftool_merge_persons
|
exiftool_merge_persons = cfg.exiftool_merge_persons
|
||||||
@@ -1813,6 +1822,7 @@ def export(
|
|||||||
("exiftool_option", ("exiftool")),
|
("exiftool_option", ("exiftool")),
|
||||||
("exiftool_merge_keywords", ("exiftool", "sidecar")),
|
("exiftool_merge_keywords", ("exiftool", "sidecar")),
|
||||||
("exiftool_merge_persons", ("exiftool", "sidecar")),
|
("exiftool_merge_persons", ("exiftool", "sidecar")),
|
||||||
|
("exiftool_path", ("exiftool")),
|
||||||
]
|
]
|
||||||
try:
|
try:
|
||||||
cfg.validate(exclusive=exclusive_options, dependent=dependent_options, cli=True)
|
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]
|
not x for x in [skip_edited, skip_bursts, skip_live, skip_raw]
|
||||||
]
|
]
|
||||||
|
|
||||||
# verify exiftool installed an in path
|
# verify exiftool installed and in path if path not provided
|
||||||
if exiftool:
|
if exiftool and not exiftool_path:
|
||||||
try:
|
try:
|
||||||
_ = get_exiftool_path()
|
exiftool_path = get_exiftool_path()
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
click.echo(
|
click.echo(
|
||||||
click.style(
|
click.style(
|
||||||
@@ -1896,6 +1906,9 @@ def export(
|
|||||||
)
|
)
|
||||||
ctx.exit(2)
|
ctx.exit(2)
|
||||||
|
|
||||||
|
if exiftool:
|
||||||
|
verbose_(f"exiftool path: {exiftool_path}")
|
||||||
|
|
||||||
isphoto = ismovie = True # default searches for everything
|
isphoto = ismovie = True # default searches for everything
|
||||||
if only_movies:
|
if only_movies:
|
||||||
isphoto = False
|
isphoto = False
|
||||||
@@ -1977,8 +1990,9 @@ def export(
|
|||||||
f"Upgraded export database {export_db_path} from version {upgraded[0]} to {upgraded[1]}"
|
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(
|
photos = _query(
|
||||||
db=db,
|
photosdb=photosdb,
|
||||||
keyword=keyword,
|
keyword=keyword,
|
||||||
person=person,
|
person=person,
|
||||||
album=album,
|
album=album,
|
||||||
@@ -2327,7 +2341,7 @@ def print_photo_info(photos, json=False):
|
|||||||
|
|
||||||
|
|
||||||
def _query(
|
def _query(
|
||||||
db=None,
|
photosdb,
|
||||||
keyword=None,
|
keyword=None,
|
||||||
person=None,
|
person=None,
|
||||||
album=None,
|
album=None,
|
||||||
@@ -2386,12 +2400,12 @@ def _query(
|
|||||||
has_likes=False,
|
has_likes=False,
|
||||||
no_likes=False,
|
no_likes=False,
|
||||||
):
|
):
|
||||||
"""run a query against PhotosDB to extract the photos based on user supply criteria
|
""" Run a query against PhotosDB to extract the photos based on user supply criteria used by query and export commands
|
||||||
used by query and export commands
|
|
||||||
arguments must be passed in same order as query and export
|
Args:
|
||||||
if either is modified, need to ensure all three functions are updated"""
|
photosdb: PhotosDB object
|
||||||
|
"""
|
||||||
|
|
||||||
photosdb = osxphotos.PhotosDB(dbfile=db, verbose=verbose_)
|
|
||||||
if deleted or deleted_only:
|
if deleted or deleted_only:
|
||||||
photos = photosdb.photos(
|
photos = photosdb.photos(
|
||||||
uuid=uuid,
|
uuid=uuid,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
""" version info """
|
""" version info """
|
||||||
|
|
||||||
__version__ = "0.38.19"
|
__version__ = "0.38.20"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ class _ExifToolProc:
|
|||||||
|
|
||||||
if hasattr(self, "_process_running") and self._process_running:
|
if hasattr(self, "_process_running") and self._process_running:
|
||||||
# already running
|
# already running
|
||||||
if exiftool is not None:
|
if exiftool != self._exiftool:
|
||||||
logging.warning(
|
logging.warning(
|
||||||
f"exiftool subprocess already running, "
|
f"exiftool subprocess already running, "
|
||||||
f"ignoring exiftool={exiftool}"
|
f"ignoring exiftool={exiftool}"
|
||||||
|
|||||||
@@ -18,12 +18,11 @@ def exiftool(self):
|
|||||||
return self._exiftool
|
return self._exiftool
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
try:
|
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):
|
if self.path is not None and os.path.isfile(self.path):
|
||||||
exiftool = ExifTool(self.path)
|
exiftool = ExifTool(self.path, exiftool=exiftool_path)
|
||||||
else:
|
else:
|
||||||
exiftool = None
|
exiftool = None
|
||||||
logging.debug(f"exiftool: missing path {self.uuid}")
|
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
# get_exiftool_path raises FileNotFoundError if exiftool not found
|
# get_exiftool_path raises FileNotFoundError if exiftool not found
|
||||||
exiftool = None
|
exiftool = None
|
||||||
|
|||||||
@@ -1358,7 +1358,7 @@ def _write_exif_data(
|
|||||||
merge_exif_persons=merge_exif_persons,
|
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():
|
for exiftag, val in exif_info.items():
|
||||||
if type(val) == list:
|
if type(val) == list:
|
||||||
for v in val:
|
for v in val:
|
||||||
|
|||||||
@@ -70,12 +70,13 @@ class PhotosDB:
|
|||||||
from ._photosdb_process_scoreinfo import _process_scoreinfo
|
from ._photosdb_process_scoreinfo import _process_scoreinfo
|
||||||
from ._photosdb_process_comments import _process_comments
|
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.
|
""" Create a new PhotosDB object.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
dbfile: specify full path to photos library or photos.db; if None, will attempt to locate last library opened by Photos.
|
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.
|
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:
|
Raises:
|
||||||
FileNotFoundError if dbfile is not a valid Photos library.
|
FileNotFoundError if dbfile is not a valid Photos library.
|
||||||
@@ -98,6 +99,8 @@ class PhotosDB:
|
|||||||
raise TypeError("verbose must be callable")
|
raise TypeError("verbose must be callable")
|
||||||
self._verbose = verbose
|
self._verbose = verbose
|
||||||
|
|
||||||
|
self._exiftool_path = exiftool
|
||||||
|
|
||||||
# create a temporary directory
|
# create a temporary directory
|
||||||
# tempfile.TemporaryDirectory gets cleaned up when the object does
|
# tempfile.TemporaryDirectory gets cleaned up when the object does
|
||||||
self._tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
|
self._tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
|
||||||
|
|||||||
@@ -1055,6 +1055,52 @@ def test_export_exiftool():
|
|||||||
assert exif[key] == CLI_EXIFTOOL[uuid][key]
|
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")
|
@pytest.mark.skipif(exiftool is None, reason="exiftool not installed")
|
||||||
def test_export_exiftool_ignore_date_modified():
|
def test_export_exiftool_ignore_date_modified():
|
||||||
import glob
|
import glob
|
||||||
|
|||||||
Reference in New Issue
Block a user