Add support for camera make and model in directory path #254 (#255)

This commit is contained in:
Jaisen Mathai 2017-11-14 23:14:26 -08:00 committed by GitHub
parent 362b1b1363
commit 4cd91e9f2d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 161 additions and 23 deletions

View File

@ -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 #### 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`. 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.
* `%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.
* `%day` the day the photo was taken. * `%day` the day the photo was taken.
* `%month` the month the photo was taken. * `%month` the month the photo was taken.
* `%year` the year 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. 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`. * `%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 (photo) | XMP:Title | |
| Title (video, audio) | XMP:DisplayName | | | Title (video, audio) | XMP:DisplayName | |
| Album | XMP-xmpDM:Album, XMP:Album | XMP:Album is user defined in `configs/ExifTool_config` for backwards compatability | | 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 ## Using OpenStreetMap data from MapQuest

View File

@ -251,9 +251,9 @@ class FileSystem(object):
) )
path.append(parsed_folder_name) path.append(parsed_folder_name)
break break
elif part in ('album'): elif part in ('album', 'camera_make', 'camera_model'):
if metadata['album']: if metadata[part]:
path.append(metadata['album']) path.append(metadata[part])
break break
elif part.startswith('"') and part.endswith('"'): elif part.startswith('"') and part.endswith('"'):
path.append(part[1:-1]) path.append(part[1:-1])

View File

@ -68,6 +68,12 @@ class Base(object):
source = self.source source = self.source
return os.path.splitext(source)[1][1:].lower() 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): def get_metadata(self, update_cache=False):
"""Get a dictionary of metadata for any file. """Get a dictionary of metadata for any file.
@ -85,6 +91,8 @@ class Base(object):
self.metadata = { self.metadata = {
'date_taken': self.get_date_taken(), 'date_taken': self.get_date_taken(),
'camera_make': self.get_camera_make(),
'camera_model': self.get_camera_model(),
'latitude': self.get_coordinate('latitude'), 'latitude': self.get_coordinate('latitude'),
'longitude': self.get_coordinate('longitude'), 'longitude': self.get_coordinate('longitude'),
'album': self.get_album(), 'album': self.get_album(),

View File

@ -42,6 +42,8 @@ class Media(Base):
'EXIF:ModifyDate' '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.album_keys = ['XMP-xmpDM:Album', 'XMP:Album']
self.title_key = 'XMP:Title' self.title_key = 'XMP:Title'
self.latitude_keys = ['EXIF:GPSLatitude'] self.latitude_keys = ['EXIF:GPSLatitude']
@ -132,6 +134,44 @@ class Media(Base):
return metadata 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): def get_original_name(self):
"""Get the original name stored in EXIF. """Get the original name stored in EXIF.

View File

@ -226,23 +226,44 @@ def test_get_folder_path_with_location():
assert path == os.path.join('2015-12-Dec','Sunnyvale'), path assert path == os.path.join('2015-12-Dec','Sunnyvale'), path
def test_get_folder_path_with_int_in_source_path(): @mock.patch('elodie.config.config_file', '%s/config.ini-original-with-camera-make-and-model' % gettempdir())
# gh-239 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() filesystem = FileSystem()
temporary_folder, folder = helper.create_working_folder('int') media = Photo(helper.get_file('plain.jpg'))
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()) 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(): def test_get_folder_path_with_int_in_config_component():
# gh-239 # 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(""" f.write("""
[Directory] [Directory]
date=%Y date=%Y
@ -258,6 +279,19 @@ full_path=%date
assert path == os.path.join('2015'), path 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()) @mock.patch('elodie.config.config_file', '%s/config.ini-original-default-unknown-location' % gettempdir())
def test_get_folder_path_with_original_default_unknown_location(): def test_get_folder_path_with_original_default_unknown_location():
with open('%s/config.ini-original-default-with-unknown-location' % gettempdir(), 'w') as f: with open('%s/config.ini-original-default-with-unknown-location' % gettempdir(), 'w') as f:

View File

@ -35,6 +35,18 @@ def test_get_coordinate():
assert helper.isclose(coordinate, 29.758938), 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(): def test_get_coordinate_latitude():
audio = Audio(helper.get_file('audio.m4a')) audio = Audio(helper.get_file('audio.m4a'))
coordinate = audio.get_coordinate('latitude') coordinate = audio.get_coordinate('latitude')

View File

@ -125,6 +125,30 @@ def test_get_date_taken_without_exif():
assert date_taken == date_taken_from_file, date_taken 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(): def test_is_valid():
photo = Photo(helper.get_file('with-location.jpg')) photo = Photo(helper.get_file('with-location.jpg'))

View File

@ -35,6 +35,19 @@ def test_empty_album():
video = Video(helper.get_file('video.mov')) video = Video(helper.get_file('video.mov'))
assert video.get_album() is None 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(): def test_get_coordinate():
video = Video(helper.get_file('video.mov')) video = Video(helper.get_file('video.mov'))
coordinate = video.get_coordinate() coordinate = video.get_coordinate()