Optimize exiftool calls by adding an ExifTool singleton in pyexiftool library (#352)
This fix results in a 10x performance improvement [1] enabling a single exiftool subprocess to elimate spawing exiftool for each image. Closes #350 #347 [1] https://github.com/jmathai/elodie/issues/350#issuecomment-573412006
This commit is contained in:
parent
75e65901a9
commit
d8cee15f32
13
elodie.py
13
elodie.py
|
@ -30,11 +30,12 @@ from elodie.media.photo import Photo
|
||||||
from elodie.media.video import Video
|
from elodie.media.video import Video
|
||||||
from elodie.plugins.plugins import Plugins
|
from elodie.plugins.plugins import Plugins
|
||||||
from elodie.result import Result
|
from elodie.result import Result
|
||||||
|
from elodie.external.pyexiftool import ExifTool
|
||||||
|
from elodie.dependencies import get_exiftool
|
||||||
|
from elodie import constants
|
||||||
|
|
||||||
FILESYSTEM = FileSystem()
|
FILESYSTEM = FileSystem()
|
||||||
|
|
||||||
|
|
||||||
def import_file(_file, destination, album_from_folder, trash, allow_duplicates):
|
def import_file(_file, destination, album_from_folder, trash, allow_duplicates):
|
||||||
|
|
||||||
_file = _decode(_file)
|
_file = _decode(_file)
|
||||||
|
@ -368,4 +369,10 @@ main.add_command(_batch)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
#Initialize ExifTool Subprocess
|
||||||
|
exiftool_addedargs = [
|
||||||
|
u'-config',
|
||||||
|
u'"{}"'.format(constants.exiftool_config)
|
||||||
|
]
|
||||||
|
with ExifTool(executable_=get_exiftool(), addedargs=exiftool_addedargs) as et:
|
||||||
|
main()
|
||||||
|
|
|
@ -65,6 +65,8 @@ import warnings
|
||||||
import logging
|
import logging
|
||||||
import codecs
|
import codecs
|
||||||
|
|
||||||
|
from future.utils import with_metaclass
|
||||||
|
|
||||||
try: # Py3k compatibility
|
try: # Py3k compatibility
|
||||||
basestring
|
basestring
|
||||||
except NameError:
|
except NameError:
|
||||||
|
@ -151,8 +153,16 @@ def format_error (result):
|
||||||
else:
|
else:
|
||||||
return 'exiftool finished with error: "%s"' % strip_nl(result)
|
return 'exiftool finished with error: "%s"' % strip_nl(result)
|
||||||
|
|
||||||
|
class Singleton(type):
|
||||||
|
"""Metaclass to use the singleton [anti-]pattern"""
|
||||||
|
instance = None
|
||||||
|
|
||||||
class ExifTool(object):
|
def __call__(cls, *args, **kwargs):
|
||||||
|
if cls.instance is None:
|
||||||
|
cls.instance = super(Singleton, cls).__call__(*args, **kwargs)
|
||||||
|
return cls.instance
|
||||||
|
|
||||||
|
class ExifTool(object, with_metaclass(Singleton)):
|
||||||
"""Run the `exiftool` command-line tool and communicate to it.
|
"""Run the `exiftool` command-line tool and communicate to it.
|
||||||
|
|
||||||
You can pass two arguments to the constructor:
|
You can pass two arguments to the constructor:
|
||||||
|
|
|
@ -19,7 +19,6 @@ from elodie.localstorage import Db
|
||||||
from elodie.media.base import Base, get_all_subclasses
|
from elodie.media.base import Base, get_all_subclasses
|
||||||
from elodie.plugins.plugins import Plugins
|
from elodie.plugins.plugins import Plugins
|
||||||
|
|
||||||
|
|
||||||
class FileSystem(object):
|
class FileSystem(object):
|
||||||
"""A class for interacting with the file system."""
|
"""A class for interacting with the file system."""
|
||||||
|
|
||||||
|
@ -48,7 +47,6 @@ class FileSystem(object):
|
||||||
# Instantiate a plugins object
|
# Instantiate a plugins object
|
||||||
self.plugins = Plugins()
|
self.plugins = Plugins()
|
||||||
|
|
||||||
|
|
||||||
def create_directory(self, directory_path):
|
def create_directory(self, directory_path):
|
||||||
"""Create a directory if it does not already exist.
|
"""Create a directory if it does not already exist.
|
||||||
|
|
||||||
|
@ -644,4 +642,4 @@ class FileSystem(object):
|
||||||
compiled_list.append(re.compile(regex))
|
compiled_list.append(re.compile(regex))
|
||||||
regex_list = compiled_list
|
regex_list = compiled_list
|
||||||
|
|
||||||
return any(regex.search(path) for regex in regex_list)
|
return any(regex.search(path) for regex in regex_list)
|
|
@ -13,12 +13,9 @@ from __future__ import print_function
|
||||||
import os
|
import os
|
||||||
|
|
||||||
# load modules
|
# load modules
|
||||||
from elodie import constants
|
|
||||||
from elodie.dependencies import get_exiftool
|
|
||||||
from elodie.external.pyexiftool import ExifTool
|
from elodie.external.pyexiftool import ExifTool
|
||||||
from elodie.media.base import Base
|
from elodie.media.base import Base
|
||||||
|
|
||||||
|
|
||||||
class Media(Base):
|
class Media(Base):
|
||||||
|
|
||||||
"""The base class for all media objects.
|
"""The base class for all media objects.
|
||||||
|
@ -52,10 +49,7 @@ class Media(Base):
|
||||||
self.longitude_ref_key = 'EXIF:GPSLongitudeRef'
|
self.longitude_ref_key = 'EXIF:GPSLongitudeRef'
|
||||||
self.original_name_key = 'XMP:OriginalFileName'
|
self.original_name_key = 'XMP:OriginalFileName'
|
||||||
self.set_gps_ref = True
|
self.set_gps_ref = True
|
||||||
self.exiftool_addedargs = [
|
self.exif_metadata = None
|
||||||
u'-config',
|
|
||||||
u'"{}"'.format(constants.exiftool_config)
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_album(self):
|
def get_album(self):
|
||||||
"""Get album from EXIF
|
"""Get album from EXIF
|
||||||
|
@ -122,16 +116,15 @@ class Media(Base):
|
||||||
:returns: dict, or False if exiftool was not available.
|
:returns: dict, or False if exiftool was not available.
|
||||||
"""
|
"""
|
||||||
source = self.source
|
source = self.source
|
||||||
exiftool = get_exiftool()
|
|
||||||
if(exiftool is None):
|
#Cache exif metadata results and use if already exists for media
|
||||||
|
if(self.exif_metadata is None):
|
||||||
|
self.exif_metadata = ExifTool().get_metadata(source)
|
||||||
|
|
||||||
|
if not self.exif_metadata:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
with ExifTool(executable_=exiftool, addedargs=self.exiftool_addedargs) as et:
|
return self.exif_metadata
|
||||||
metadata = et.get_metadata(source)
|
|
||||||
if not metadata:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return metadata
|
|
||||||
|
|
||||||
def get_camera_make(self):
|
def get_camera_make(self):
|
||||||
"""Get the camera make stored in EXIF.
|
"""Get the camera make stored in EXIF.
|
||||||
|
@ -211,6 +204,7 @@ class Media(Base):
|
||||||
"""Resets any internal cache
|
"""Resets any internal cache
|
||||||
"""
|
"""
|
||||||
self.exiftool_attributes = None
|
self.exiftool_attributes = None
|
||||||
|
self.exif_metadata = None
|
||||||
super(Media, self).reset_cache()
|
super(Media, self).reset_cache()
|
||||||
|
|
||||||
def set_album(self, album):
|
def set_album(self, album):
|
||||||
|
@ -318,13 +312,8 @@ class Media(Base):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
source = self.source
|
source = self.source
|
||||||
exiftool = get_exiftool()
|
|
||||||
if(exiftool is None):
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
status = ''
|
status = ''
|
||||||
with ExifTool(executable_=exiftool, addedargs=self.exiftool_addedargs) as et:
|
status = ExifTool().set_tags(tags,source)
|
||||||
status = et.set_tags(tags, source)
|
|
||||||
|
|
||||||
return status != ''
|
return status != ''
|
||||||
|
|
|
@ -20,9 +20,21 @@ from elodie.media.media import Media
|
||||||
from elodie.media.photo import Photo
|
from elodie.media.photo import Photo
|
||||||
from elodie.media.video import Video
|
from elodie.media.video import Video
|
||||||
from nose.plugins.skip import SkipTest
|
from nose.plugins.skip import SkipTest
|
||||||
|
from elodie.external.pyexiftool import ExifTool
|
||||||
|
from elodie.dependencies import get_exiftool
|
||||||
|
from elodie import constants
|
||||||
|
|
||||||
os.environ['TZ'] = 'GMT'
|
os.environ['TZ'] = 'GMT'
|
||||||
|
|
||||||
|
def setup_module():
|
||||||
|
exiftool_addedargs = [
|
||||||
|
u'-config',
|
||||||
|
u'"{}"'.format(constants.exiftool_config)
|
||||||
|
]
|
||||||
|
ExifTool(executable_=get_exiftool(), addedargs=exiftool_addedargs).start()
|
||||||
|
|
||||||
|
def teardown_module():
|
||||||
|
ExifTool().terminate
|
||||||
|
|
||||||
def test_create_directory_success():
|
def test_create_directory_success():
|
||||||
filesystem = FileSystem()
|
filesystem = FileSystem()
|
||||||
|
@ -575,7 +587,7 @@ full_path=%year/%month/%location
|
||||||
path = filesystem.get_folder_path(media.get_metadata())
|
path = filesystem.get_folder_path(media.get_metadata())
|
||||||
if hasattr(load_config, 'config'):
|
if hasattr(load_config, 'config'):
|
||||||
del load_config.config
|
del load_config.config
|
||||||
|
|
||||||
assert path == os.path.join('2015','12','Sunnyvale, California'), path
|
assert path == os.path.join('2015','12','Sunnyvale, California'), path
|
||||||
|
|
||||||
@mock.patch('elodie.config.config_file', '%s/config.ini-location-date' % gettempdir())
|
@mock.patch('elodie.config.config_file', '%s/config.ini-location-date' % gettempdir())
|
||||||
|
|
|
@ -15,6 +15,8 @@ from datetime import datetime
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from elodie.compatability import _rename
|
from elodie.compatability import _rename
|
||||||
|
from elodie.external.pyexiftool import ExifTool
|
||||||
|
from elodie.dependencies import get_exiftool
|
||||||
from elodie import constants
|
from elodie import constants
|
||||||
|
|
||||||
def checksum(file_path, blocksize=65536):
|
def checksum(file_path, blocksize=65536):
|
||||||
|
@ -159,3 +161,14 @@ def restore_dbs():
|
||||||
# This is no longer needed. See gh-322
|
# This is no longer needed. See gh-322
|
||||||
# https://github.com/jmathai/elodie/issues/322
|
# https://github.com/jmathai/elodie/issues/322
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def setup_module():
|
||||||
|
exiftool_addedargs = [
|
||||||
|
u'-config',
|
||||||
|
u'"{}"'.format(constants.exiftool_config)
|
||||||
|
]
|
||||||
|
ExifTool(executable_=get_exiftool(), addedargs=exiftool_addedargs).start()
|
||||||
|
|
||||||
|
def teardown_module():
|
||||||
|
ExifTool().terminate
|
||||||
|
|
|
@ -16,9 +16,22 @@ import helper
|
||||||
from elodie.media.media import Media
|
from elodie.media.media import Media
|
||||||
from elodie.media.video import Video
|
from elodie.media.video import Video
|
||||||
from elodie.media.audio import Audio
|
from elodie.media.audio import Audio
|
||||||
|
from elodie.external.pyexiftool import ExifTool
|
||||||
|
from elodie.dependencies import get_exiftool
|
||||||
|
from elodie import constants
|
||||||
|
|
||||||
os.environ['TZ'] = 'GMT'
|
os.environ['TZ'] = 'GMT'
|
||||||
|
|
||||||
|
def setup_module():
|
||||||
|
exiftool_addedargs = [
|
||||||
|
u'-config',
|
||||||
|
u'"{}"'.format(constants.exiftool_config)
|
||||||
|
]
|
||||||
|
ExifTool(executable_=get_exiftool(), addedargs=exiftool_addedargs).start()
|
||||||
|
|
||||||
|
def teardown_module():
|
||||||
|
ExifTool().terminate
|
||||||
|
|
||||||
def test_audio_extensions():
|
def test_audio_extensions():
|
||||||
audio = Audio()
|
audio = Audio()
|
||||||
extensions = audio.extensions
|
extensions = audio.extensions
|
||||||
|
|
|
@ -23,6 +23,8 @@ from elodie.media.video import Video
|
||||||
|
|
||||||
os.environ['TZ'] = 'GMT'
|
os.environ['TZ'] = 'GMT'
|
||||||
|
|
||||||
|
setup_module = helper.setup_module
|
||||||
|
teardown_module = helper.teardown_module
|
||||||
|
|
||||||
def test_get_all_subclasses():
|
def test_get_all_subclasses():
|
||||||
subclasses = get_all_subclasses(Base)
|
subclasses = get_all_subclasses(Base)
|
||||||
|
|
|
@ -21,6 +21,8 @@ from elodie.media.video import Video
|
||||||
|
|
||||||
os.environ['TZ'] = 'GMT'
|
os.environ['TZ'] = 'GMT'
|
||||||
|
|
||||||
|
setup_module = helper.setup_module
|
||||||
|
teardown_module = helper.teardown_module
|
||||||
|
|
||||||
def test_get_file_path():
|
def test_get_file_path():
|
||||||
media = Media(helper.get_file('plain.jpg'))
|
media = Media(helper.get_file('plain.jpg'))
|
||||||
|
@ -86,7 +88,7 @@ def test_get_original_name_invalid_file():
|
||||||
original_name = media.get_original_name()
|
original_name = media.get_original_name()
|
||||||
|
|
||||||
assert original_name is None, original_name
|
assert original_name is None, original_name
|
||||||
|
|
||||||
def test_set_original_name_when_exists():
|
def test_set_original_name_when_exists():
|
||||||
temporary_folder, folder = helper.create_working_folder()
|
temporary_folder, folder = helper.create_working_folder()
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,9 @@ from elodie.media.photo import Photo
|
||||||
|
|
||||||
os.environ['TZ'] = 'GMT'
|
os.environ['TZ'] = 'GMT'
|
||||||
|
|
||||||
|
setup_module = helper.setup_module
|
||||||
|
teardown_module = helper.teardown_module
|
||||||
|
|
||||||
def test_photo_extensions():
|
def test_photo_extensions():
|
||||||
photo = Photo()
|
photo = Photo()
|
||||||
extensions = photo.extensions
|
extensions = photo.extensions
|
||||||
|
|
|
@ -30,9 +30,8 @@ config_string_fmt = config_string.format(
|
||||||
secrets_file
|
secrets_file
|
||||||
)
|
)
|
||||||
|
|
||||||
sample_photo = Photo(helper.get_file('plain.jpg'))
|
setup_module = helper.setup_module
|
||||||
sample_metadata = sample_photo.get_metadata()
|
teardown_module = helper.teardown_module
|
||||||
sample_metadata['original_name'] = 'foobar'
|
|
||||||
|
|
||||||
@mock.patch('elodie.config.config_file', '%s/config.ini-googlephotos-set-session' % gettempdir())
|
@mock.patch('elodie.config.config_file', '%s/config.ini-googlephotos-set-session' % gettempdir())
|
||||||
def test_googlephotos_set_session():
|
def test_googlephotos_set_session():
|
||||||
|
@ -57,6 +56,9 @@ def test_googlephotos_after_supported():
|
||||||
if hasattr(load_config, 'config'):
|
if hasattr(load_config, 'config'):
|
||||||
del load_config.config
|
del load_config.config
|
||||||
|
|
||||||
|
sample_photo = Photo(helper.get_file('plain.jpg'))
|
||||||
|
sample_metadata = sample_photo.get_metadata()
|
||||||
|
sample_metadata['original_name'] = 'foobar'
|
||||||
final_file_path = helper.get_file('plain.jpg')
|
final_file_path = helper.get_file('plain.jpg')
|
||||||
gp = GooglePhotos()
|
gp = GooglePhotos()
|
||||||
gp.after('', '', final_file_path, sample_metadata)
|
gp.after('', '', final_file_path, sample_metadata)
|
||||||
|
@ -162,6 +164,9 @@ def test_googlephotos_batch():
|
||||||
if hasattr(load_config, 'config'):
|
if hasattr(load_config, 'config'):
|
||||||
del load_config.config
|
del load_config.config
|
||||||
|
|
||||||
|
sample_photo = Photo(helper.get_file('plain.jpg'))
|
||||||
|
sample_metadata = sample_photo.get_metadata()
|
||||||
|
sample_metadata['original_name'] = 'foobar'
|
||||||
final_file_path = helper.get_file('plain.jpg')
|
final_file_path = helper.get_file('plain.jpg')
|
||||||
gp = GooglePhotos()
|
gp = GooglePhotos()
|
||||||
gp.after('', '', final_file_path, sample_metadata)
|
gp.after('', '', final_file_path, sample_metadata)
|
||||||
|
|
Loading…
Reference in New Issue