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() | ||||||
|  | |||||||
							
								
								
									
										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
	 Arian Maleki
						Arian Maleki