From 4cd91e9f2d5017e9f77235c4110feb6775865762 Mon Sep 17 00:00:00 2001 From: Jaisen Mathai Date: Tue, 14 Nov 2017 23:14:26 -0800 Subject: [PATCH] Add support for camera make and model in directory path #254 (#255) --- Readme.md | 25 +++++++++----- elodie/filesystem.py | 6 ++-- elodie/media/base.py | 8 +++++ elodie/media/media.py | 40 +++++++++++++++++++++++ elodie/tests/filesystem_test.py | 56 +++++++++++++++++++++++++------- elodie/tests/media/audio_test.py | 12 +++++++ elodie/tests/media/photo_test.py | 24 ++++++++++++++ elodie/tests/media/video_test.py | 13 ++++++++ 8 files changed, 161 insertions(+), 23 deletions(-) diff --git a/Readme.md b/Readme.md index b5b8bb0..e7c5b71 100644 --- a/Readme.md +++ b/Readme.md @@ -214,22 +214,27 @@ What this asks me to do is to name the last folder the same as the album I find #### How folder customization works -You can construct your folder structure using a combination of the location and dates. Under the `Directory` section of your `config.ini` file you can define placeholder names and assign each a value. For example, `date=%Y-%m` would create a date placeholder with a value of YYYY-MM which would be filled in with the date from the EXIF on the photo. +You can construct your folder structure using a combination of the location, dates and camera make/model. Under the `Directory` section of your `config.ini` file you can define placeholder names and assign each a value. For example, `date=%Y-%m` would create a date placeholder with a value of YYYY-MM which would be filled in with the date from the EXIF on the photo. -The placeholders can be used to define the folder structure you'd like to create. The example above happens to be the default structure and would look like `2015-07-Jul/Mountain View`. +The placeholders can be used to define the folder structure you'd like to create. The default structure would look like `2015-07-Jul/Mountain View`. -I have a few built-in location placeholders you can use. Use this to construct the `%location` you use in `full_path`. - -* `%city` the name of the city the photo was taken. Requires geolocation data in EXIF. -* `%state` the name of the state the photo was taken. Requires geolocation data in EXIF. -* `%country` the name of the country the photo was taken. Requires geolocation data in EXIF. - -I also have some date placeholders you can customize. You can use any of [the standard Python time directives](https://docs.python.org/2/library/datetime.html#strftime-and-strptime-behavior) to customize the date format to your liking. +I have some date placeholders you can customize. You can use any of [the standard Python time directives](https://docs.python.org/2/library/datetime.html#strftime-and-strptime-behavior) to customize the date format to your liking. * `%day` the day the photo was taken. * `%month` the month the photo was taken. * `%year` the year the photo was taken. +I have camera make and model placeholders which can be used to include the camera make and model into the folder path. + +* `%camera_make` the make of the camera which took the photo. +* `%camera_model` the model of the camera which took the photo. + +I also have a few built-in location placeholders you can use. Use this to construct the `%location` you use in `full_path`. + +* `%city` the name of the city the photo was taken. Requires geolocation data in EXIF. +* `%state` the name of the state the photo was taken. Requires geolocation data in EXIF. +* `%country` the name of the country the photo was taken. Requires geolocation data in EXIF. + In addition to my built-in and date placeholders you can combine them into a single folder name using my complex placeholders. * `%location` can be used to combine multiple values of `%city`, `%state` and `%country`. For example, `location=%city, %state` would result in folder names like `Sunnyvale, California`. @@ -322,6 +327,8 @@ When I organize photos I look at the embedded metadata. Here are the details of | Title (photo) | XMP:Title | | | Title (video, audio) | XMP:DisplayName | | | Album | XMP-xmpDM:Album, XMP:Album | XMP:Album is user defined in `configs/ExifTool_config` for backwards compatability | +| Camera Make (photo, video) | EXIF:Make, QuickTime:Make | | +| Camera Model (photo, video) | EXIF:Model, QuickTime:Model | | ## Using OpenStreetMap data from MapQuest diff --git a/elodie/filesystem.py b/elodie/filesystem.py index 92b241c..6c5d83e 100644 --- a/elodie/filesystem.py +++ b/elodie/filesystem.py @@ -251,9 +251,9 @@ class FileSystem(object): ) path.append(parsed_folder_name) break - elif part in ('album'): - if metadata['album']: - path.append(metadata['album']) + elif part in ('album', 'camera_make', 'camera_model'): + if metadata[part]: + path.append(metadata[part]) break elif part.startswith('"') and part.endswith('"'): path.append(part[1:-1]) diff --git a/elodie/media/base.py b/elodie/media/base.py index da9d466..83f29c5 100644 --- a/elodie/media/base.py +++ b/elodie/media/base.py @@ -68,6 +68,12 @@ class Base(object): source = self.source return os.path.splitext(source)[1][1:].lower() + def get_camera_make(self): + return None + + def get_camera_model(self): + return None + def get_metadata(self, update_cache=False): """Get a dictionary of metadata for any file. @@ -85,6 +91,8 @@ class Base(object): self.metadata = { 'date_taken': self.get_date_taken(), + 'camera_make': self.get_camera_make(), + 'camera_model': self.get_camera_model(), 'latitude': self.get_coordinate('latitude'), 'longitude': self.get_coordinate('longitude'), 'album': self.get_album(), diff --git a/elodie/media/media.py b/elodie/media/media.py index ae77711..b0c36c6 100644 --- a/elodie/media/media.py +++ b/elodie/media/media.py @@ -42,6 +42,8 @@ class Media(Base): 'EXIF:ModifyDate' ] } + self.camera_make_keys = ['EXIF:Make', 'QuickTime:Make'] + self.camera_model_keys = ['EXIF:Model', 'QuickTime:Model'] self.album_keys = ['XMP-xmpDM:Album', 'XMP:Album'] self.title_key = 'XMP:Title' self.latitude_keys = ['EXIF:GPSLatitude'] @@ -132,6 +134,44 @@ class Media(Base): return metadata + def get_camera_make(self): + """Get the camera make stored in EXIF. + + :returns: str + """ + if(not self.is_valid()): + return None + + exiftool_attributes = self.get_exiftool_attributes() + + if exiftool_attributes is None: + return None + + for camera_make_key in self.camera_make_keys: + if camera_make_key in exiftool_attributes: + return exiftool_attributes[camera_make_key] + + return None + + def get_camera_model(self): + """Get the camera make stored in EXIF. + + :returns: str + """ + if(not self.is_valid()): + return None + + exiftool_attributes = self.get_exiftool_attributes() + + if exiftool_attributes is None: + return None + + for camera_model_key in self.camera_model_keys: + if camera_model_key in exiftool_attributes: + return exiftool_attributes[camera_model_key] + + return None + def get_original_name(self): """Get the original name stored in EXIF. diff --git a/elodie/tests/filesystem_test.py b/elodie/tests/filesystem_test.py index c609789..ee28913 100644 --- a/elodie/tests/filesystem_test.py +++ b/elodie/tests/filesystem_test.py @@ -226,23 +226,44 @@ def test_get_folder_path_with_location(): assert path == os.path.join('2015-12-Dec','Sunnyvale'), path -def test_get_folder_path_with_int_in_source_path(): - # gh-239 +@mock.patch('elodie.config.config_file', '%s/config.ini-original-with-camera-make-and-model' % gettempdir()) +def test_get_folder_path_with_camera_make_and_model(): + with open('%s/config.ini-original-with-camera-make-and-model' % gettempdir(), 'w') as f: + f.write(""" +[Directory] +full_path=%camera_make/%camera_model + """) + if hasattr(load_config, 'config'): + del load_config.config filesystem = FileSystem() - temporary_folder, folder = helper.create_working_folder('int') - - origin = os.path.join(folder,'plain.jpg') - shutil.copyfile(helper.get_file('plain.jpg'), origin) - - media = Photo(origin) + media = Photo(helper.get_file('plain.jpg')) path = filesystem.get_folder_path(media.get_metadata()) + if hasattr(load_config, 'config'): + del load_config.config - assert path == os.path.join('2015-12-Dec','Unknown Location'), path + assert path == os.path.join('Canon', 'Canon EOS REBEL T2i'), path -@mock.patch('elodie.config.config_file', '%s/config.ini-int-in-path' % gettempdir()) +@mock.patch('elodie.config.config_file', '%s/config.ini-original-with-camera-make-and-model-fallback' % gettempdir()) +def test_get_folder_path_with_camera_make_and_model_fallback(): + with open('%s/config.ini-original-with-camera-make-and-model-fallback' % gettempdir(), 'w') as f: + f.write(""" +[Directory] +full_path=%camera_make|"nomake"/%camera_model|"nomodel" + """) + if hasattr(load_config, 'config'): + del load_config.config + filesystem = FileSystem() + media = Photo(helper.get_file('no-exif.jpg')) + path = filesystem.get_folder_path(media.get_metadata()) + if hasattr(load_config, 'config'): + del load_config.config + + assert path == os.path.join('nomake', 'nomodel'), path + +@mock.patch('elodie.config.config_file', '%s/config.ini-int-in-component-path' % gettempdir()) def test_get_folder_path_with_int_in_config_component(): # gh-239 - with open('%s/config.ini-int-in-path' % gettempdir(), 'w') as f: + with open('%s/config.ini-int-in-component-path' % gettempdir(), 'w') as f: f.write(""" [Directory] date=%Y @@ -258,6 +279,19 @@ full_path=%date assert path == os.path.join('2015'), path +def test_get_folder_path_with_int_in_source_path(): + # gh-239 + filesystem = FileSystem() + temporary_folder, folder = helper.create_working_folder('int') + + origin = os.path.join(folder,'plain.jpg') + shutil.copyfile(helper.get_file('plain.jpg'), origin) + + media = Photo(origin) + path = filesystem.get_folder_path(media.get_metadata()) + + assert path == os.path.join('2015-12-Dec','Unknown Location'), path + @mock.patch('elodie.config.config_file', '%s/config.ini-original-default-unknown-location' % gettempdir()) def test_get_folder_path_with_original_default_unknown_location(): with open('%s/config.ini-original-default-with-unknown-location' % gettempdir(), 'w') as f: diff --git a/elodie/tests/media/audio_test.py b/elodie/tests/media/audio_test.py index 8d7eafe..c66a0e9 100644 --- a/elodie/tests/media/audio_test.py +++ b/elodie/tests/media/audio_test.py @@ -35,6 +35,18 @@ def test_get_coordinate(): assert helper.isclose(coordinate, 29.758938), coordinate +def test_get_camera_make(): + audio = Audio(helper.get_file('audio.m4a')) + coordinate = audio.get_camera_make() + + assert coordinate is None, coordinate + +def test_get_camera_model(): + audio = Audio(helper.get_file('audio.m4a')) + coordinate = audio.get_camera_model() + + assert coordinate is None, coordinate + def test_get_coordinate_latitude(): audio = Audio(helper.get_file('audio.m4a')) coordinate = audio.get_coordinate('latitude') diff --git a/elodie/tests/media/photo_test.py b/elodie/tests/media/photo_test.py index 812d917..d214189 100644 --- a/elodie/tests/media/photo_test.py +++ b/elodie/tests/media/photo_test.py @@ -125,6 +125,30 @@ def test_get_date_taken_without_exif(): assert date_taken == date_taken_from_file, date_taken +def test_get_camera_make(): + photo = Photo(helper.get_file('with-location.jpg')) + make = photo.get_camera_make() + + assert make == 'Canon', make + +def test_get_camera_make_not_set(): + photo = Photo(helper.get_file('no-exif.jpg')) + make = photo.get_camera_make() + + assert make is None, make + +def test_get_camera_model(): + photo = Photo(helper.get_file('with-location.jpg')) + model = photo.get_camera_model() + + assert model == 'Canon EOS REBEL T2i', model + +def test_get_camera_model_not_set(): + photo = Photo(helper.get_file('no-exif.jpg')) + model = photo.get_camera_model() + + assert model is None, model + def test_is_valid(): photo = Photo(helper.get_file('with-location.jpg')) diff --git a/elodie/tests/media/video_test.py b/elodie/tests/media/video_test.py index 5b5fedc..759b39f 100644 --- a/elodie/tests/media/video_test.py +++ b/elodie/tests/media/video_test.py @@ -35,6 +35,19 @@ def test_empty_album(): video = Video(helper.get_file('video.mov')) assert video.get_album() is None +def test_get_camera_make(): + video = Video(helper.get_file('video.mov')) + print(video.get_metadata()) + make = video.get_camera_make() + + assert make == 'Apple', make + +def test_get_camera_model(): + video = Video(helper.get_file('video.mov')) + model = video.get_camera_model() + + assert model == 'iPhone 5', model + def test_get_coordinate(): video = Video(helper.get_file('video.mov')) coordinate = video.get_coordinate()