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
					
				
							
								
								
									
										11
									
								
								elodie.py
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								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__':
 | 
				
			||||||
 | 
					    #Initialize ExifTool Subprocess
 | 
				
			||||||
 | 
					    exiftool_addedargs = [
 | 
				
			||||||
 | 
					       u'-config',
 | 
				
			||||||
 | 
					        u'"{}"'.format(constants.exiftool_config)
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					    with ExifTool(executable_=get_exiftool(), addedargs=exiftool_addedargs) as et:
 | 
				
			||||||
        main()
 | 
					        main()
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										12
									
								
								elodie/external/pyexiftool.py
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										12
									
								
								elodie/external/pyexiftool.py
									
									
									
									
										vendored
									
									
								
							@ -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.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -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()
 | 
				
			||||||
 | 
				
			|||||||
@ -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'))
 | 
				
			||||||
 | 
				
			|||||||
@ -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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user