Fixed --exiftool-path bug, issue #311, #313

This commit is contained in:
Rhet Turnbull
2020-12-30 20:21:05 -08:00
parent 27282af3b9
commit 3394c52768
6 changed files with 124 additions and 76 deletions

View File

@@ -2420,7 +2420,7 @@ 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
"""Run a query against PhotosDB to extract the photos based on user supply criteria used by query and export commands
Args:
photosdb: PhotosDB object
@@ -3201,7 +3201,7 @@ def load_uuid_from_file(filename):
def write_export_report(report_file, results):
""" write CSV report with results from export
"""write CSV report with results from export
Args:
report_file: path to report file
@@ -3331,7 +3331,7 @@ def write_export_report(report_file, results):
def cleanup_files(dest_path, files_to_keep, fileutil):
""" cleanup dest_path by deleting and files and empty directories
"""cleanup dest_path by deleting and files and empty directories
not in files_to_keep
Args:
@@ -3375,7 +3375,7 @@ def write_finder_tags(
exiftool_merge_keywords=None,
finder_tag_template=None,
):
""" Write Finder tags (extended attributes) to files; only writes attributes if attributes on file differ from what would be written
"""Write Finder tags (extended attributes) to files; only writes attributes if attributes on file differ from what would be written
Args:
photo: a PhotoInfo object

View File

@@ -1,5 +1,5 @@
""" version info """
__version__ = "0.39.0"
__version__ = "0.39.1"

View File

@@ -33,8 +33,8 @@ def get_exiftool_path():
class _ExifToolProc:
""" Runs exiftool in a subprocess via Popen
Creates a singleton object """
"""Runs exiftool in a subprocess via Popen
Creates a singleton object"""
def __new__(cls, *args, **kwargs):
""" create new object or return instance of already created singleton """
@@ -44,8 +44,8 @@ class _ExifToolProc:
return cls.instance
def __init__(self, exiftool=None):
""" construct _ExifToolProc singleton object or return instance of already created object
exiftool: optional path to exiftool binary (if not provided, will search path to find it) """
"""construct _ExifToolProc singleton object or return instance of already created object
exiftool: optional path to exiftool binary (if not provided, will search path to find it)"""
if hasattr(self, "_process_running") and self._process_running:
# already running
@@ -56,8 +56,8 @@ class _ExifToolProc:
)
return
self._exiftool = exiftool or get_exiftool_path()
self._process_running = False
self._exiftool = exiftool or get_exiftool_path()
self._start_proc()
@property
@@ -106,8 +106,8 @@ class _ExifToolProc:
def _stop_proc(self):
""" stop the exiftool process if it's running, otherwise, do nothing """
if not self._process_running:
logging.warning("exiftool process is not running")
return
self._process.stdin.write(b"-stay_open\n")
@@ -133,7 +133,7 @@ class ExifTool:
""" Basic exiftool interface for reading and writing EXIF tags """
def __init__(self, filepath, exiftool=None, overwrite=True, flags=None):
""" Create ExifTool object
"""Create ExifTool object
Args:
file: path to image file
@@ -157,7 +157,7 @@ class ExifTool:
self._read_exif()
def setvalue(self, tag, value):
""" Set tag to value(s); if value is None, will delete tag
"""Set tag to value(s); if value is None, will delete tag
Args:
tag: str; name of tag to set
@@ -184,7 +184,7 @@ class ExifTool:
return error is None
def addvalues(self, tag, *values):
""" Add one or more value(s) to tag
"""Add one or more value(s) to tag
If more than one value is passed, each value will be added to the tag
Args:
@@ -226,7 +226,7 @@ class ExifTool:
return error is None
def run_commands(self, *commands, no_file=False):
""" Run commands in the exiftool process and return result.
"""Run commands in the exiftool process and return result.
Args:
*commands: exiftool commands to run
@@ -301,7 +301,7 @@ class ExifTool:
return ver.decode("utf-8")
def asdict(self):
""" return dictionary of all EXIF tags and values from exiftool
"""return dictionary of all EXIF tags and values from exiftool
returns empty dict if no tags
"""
json_str, _, _ = self.run_commands("-json")

View File

@@ -86,8 +86,8 @@ class PhotoInfo:
@property
def original_filename(self):
""" original filename of the picture
Photos 5 mangles filenames upon import """
"""original filename of the picture
Photos 5 mangles filenames upon import"""
if (
self._db._db_version <= _PHOTOS_4_VERSION
and self.has_raw
@@ -106,8 +106,8 @@ class PhotoInfo:
@property
def date_modified(self):
""" image modification date as timezone aware datetime object
or None if no modification date set """
"""image modification date as timezone aware datetime object
or None if no modification date set"""
# Photos <= 4 provides no way to get date of adjustment and will update
# lastmodifieddate anytime photo database record is updated (e.g. adding tags)
@@ -492,7 +492,7 @@ class PhotoInfo:
@property
def ismissing(self):
""" returns true if photo is missing from disk (which means it's not been downloaded from iCloud)
"""returns true if photo is missing from disk (which means it's not been downloaded from iCloud)
NOTE: the photos.db database uses an asynchrounous write-ahead log so changes in Photos
do not immediately get written to disk. In particular, I've noticed that downloading
an image from the cloud does not force the database to be updated until something else
@@ -539,8 +539,8 @@ class PhotoInfo:
@property
def shared(self):
""" returns True if photos is in a shared iCloud album otherwise false
Only valid on Photos 5; returns None on older versions """
"""returns True if photos is in a shared iCloud album otherwise false
Only valid on Photos 5; returns None on older versions"""
if self._db._db_version > _PHOTOS_4_VERSION:
return self._info["shared"]
else:
@@ -548,7 +548,7 @@ class PhotoInfo:
@property
def uti(self):
""" Returns Uniform Type Identifier (UTI) for the image
"""Returns Uniform Type Identifier (UTI) for the image
for example: public.jpeg or com.apple.quicktime-movie
"""
if self._db._db_version <= _PHOTOS_4_VERSION:
@@ -564,7 +564,7 @@ class PhotoInfo:
@property
def uti_original(self):
""" Returns Uniform Type Identifier (UTI) for the original image
"""Returns Uniform Type Identifier (UTI) for the original image
for example: public.jpeg or com.apple.quicktime-movie
"""
if self._db._db_version <= _PHOTOS_4_VERSION and self._info["has_raw"]:
@@ -577,7 +577,7 @@ class PhotoInfo:
@property
def uti_edited(self):
""" Returns Uniform Type Identifier (UTI) for the edited image
"""Returns Uniform Type Identifier (UTI) for the edited image
if the photo has been edited, otherwise None;
for example: public.jpeg
"""
@@ -588,7 +588,7 @@ class PhotoInfo:
@property
def uti_raw(self):
""" Returns Uniform Type Identifier (UTI) for the RAW image if there is one
"""Returns Uniform Type Identifier (UTI) for the RAW image if there is one
for example: com.canon.cr2-raw-image
Returns None if no associated RAW image
"""
@@ -596,19 +596,17 @@ class PhotoInfo:
@property
def ismovie(self):
""" Returns True if file is a movie, otherwise False
"""
"""Returns True if file is a movie, otherwise False"""
return True if self._info["type"] == _MOVIE_TYPE else False
@property
def isphoto(self):
""" Returns True if file is an image, otherwise False
"""
"""Returns True if file is an image, otherwise False"""
return True if self._info["type"] == _PHOTO_TYPE else False
@property
def incloud(self):
""" Returns True if photo is cloud asset and is synched to cloud
"""Returns True if photo is cloud asset and is synched to cloud
False if photo is cloud asset and not yet synched to cloud
None if photo is not cloud asset
"""
@@ -616,7 +614,7 @@ class PhotoInfo:
@property
def iscloudasset(self):
""" Returns True if photo is a cloud asset (in an iCloud library),
"""Returns True if photo is a cloud asset (in an iCloud library),
otherwise False
"""
if self._db._db_version <= _PHOTOS_4_VERSION:
@@ -636,9 +634,9 @@ class PhotoInfo:
@property
def burst_photos(self):
""" If photo is a burst photo, returns list of PhotoInfo objects
"""If photo is a burst photo, returns list of PhotoInfo objects
that are part of the same burst photo set; otherwise returns empty list.
self is not included in the returned list """
self is not included in the returned list"""
if self._info["burst"]:
burst_uuid = self._info["burstUUID"]
return [
@@ -656,9 +654,9 @@ class PhotoInfo:
@property
def path_live_photo(self):
""" Returns path to the associated video file for a live photo
"""Returns path to the associated video file for a live photo
If photo is not a live photo, returns None
If photo is missing, returns None """
If photo is missing, returns None"""
photopath = None
if self._db._db_version <= _PHOTOS_4_VERSION:
@@ -785,9 +783,9 @@ class PhotoInfo:
@property
def raw_original(self):
""" returns True if associated raw image and the raw image is selected in Photos
"""returns True if associated raw image and the raw image is selected in Photos
via "Use RAW as Original "
otherwise returns False """
otherwise returns False"""
return self._info["raw_is_original"]
@property
@@ -854,7 +852,7 @@ class PhotoInfo:
Returns:
([rendered_strings], [unmatched]): tuple of list of rendered strings and list of unmatched template values
"""
template = PhotoTemplate(self)
template = PhotoTemplate(self, exiftool_path=self._db._exiftool_path)
return template.render(
template_str,
none_str=none_str,
@@ -877,7 +875,7 @@ class PhotoInfo:
return self._info["latitude"]
def _get_album_uuids(self):
""" Return list of album UUIDs this photo is found in
"""Return list of album UUIDs this photo is found in
Filters out albums in the trash and any special album types

View File

@@ -149,13 +149,15 @@ MULTI_VALUE_SUBSTITUTIONS = [
class PhotoTemplate:
""" PhotoTemplate class to render a template string from a PhotoInfo object """
def __init__(self, photo):
""" Inits PhotoTemplate class with photo, non_str, and path_sep
def __init__(self, photo, exiftool_path=None):
""" Inits PhotoTemplate class with photo
Args:
photo: a PhotoInfo instance.
exiftool_path: optional path to exiftool for use with {exiftool:} template; if not provided, will look for exiftool in $PATH
"""
self.photo = photo
self.exiftool_path = exiftool_path
# holds value of current date/time for {today.x} fields
# gets initialized in get_template_value
@@ -517,7 +519,7 @@ class PhotoTemplate:
if not self.photo.path:
values = [None]
else:
exif = ExifTool(self.photo.path)
exif = ExifTool(self.photo.path, exiftool=self.exiftool_path)
exifdict = exif.asdict()
exifdict = {k.lower(): v for (k, v) in exifdict.items()}
subfield = subfield.lower()

View File

@@ -357,6 +357,7 @@ CLI_EXIFTOOL = {
"XMP:TagsList": "Kids",
"XMP:Title": "I found one!",
"EXIF:ImageDescription": "Girl holding pumpkin",
"EXIF:Make": "Canon",
"XMP:Description": "Girl holding pumpkin",
"XMP:PersonInImage": "Katie",
"XMP:Subject": "Kids",
@@ -1114,6 +1115,54 @@ def test_export_exiftool_path():
assert exif[key] == CLI_EXIFTOOL[uuid][key]
@pytest.mark.skipif(exiftool is None, reason="exiftool not installed")
def test_export_exiftool_path_render_template():
""" test --exiftool-path with {exiftool:} template rendering """
import glob
import os
import os.path
import re
import shutil
import sys
import tempfile
from osxphotos.__main__ import export
from osxphotos.exiftool import ExifTool
from osxphotos.utils import noop
exiftool_source = osxphotos.exiftool.get_exiftool_path()
# monkey patch get_exiftool_path so it returns None
get_exiftool_path = osxphotos.exiftool.get_exiftool_path
osxphotos.exiftool.get_exiftool_path = noop
runner = CliRunner()
cwd = os.getcwd()
# pylint: disable=not-context-manager
with runner.isolated_filesystem():
tempdir = tempfile.TemporaryDirectory()
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",
"--filename",
"{original_name}_{exiftool:EXIF:Make}",
"--uuid",
f"{uuid}",
"--exiftool-path",
exiftool_path,
],
)
assert result.exit_code == 0
assert re.search(r"Exporting.*Canon", result.output)
osxphotos.exiftool.get_exiftool_path = get_exiftool_path
@pytest.mark.skipif(exiftool is None, reason="exiftool not installed")
def test_export_exiftool_ignore_date_modified():
import glob
@@ -5066,4 +5115,3 @@ def test_export_finder_tag_template_keywords():
persons = [persons] if type(persons) != list else persons
expected = [Tag(x) for x in keywords + persons]
assert sorted(md.tags) == sorted(expected)