Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Add support for camera make and model in directory path #254 (#255)
  • Loading branch information
jmathai committed Nov 15, 2017
1 parent 362b1b1 commit 4cd91e9
Show file tree
Hide file tree
Showing 8 changed files with 159 additions and 21 deletions.
25 changes: 16 additions & 9 deletions Readme.md
Expand Up @@ -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`.
Expand Down Expand Up @@ -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

Expand Down
6 changes: 3 additions & 3 deletions elodie/filesystem.py
Expand Up @@ -251,9 +251,9 @@ def get_folder_path(self, metadata):
)
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])
Expand Down
8 changes: 8 additions & 0 deletions elodie/media/base.py
Expand Up @@ -68,6 +68,12 @@ def get_extension(self):
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.
Expand All @@ -85,6 +91,8 @@ def get_metadata(self, update_cache=False):

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(),
Expand Down
40 changes: 40 additions & 0 deletions elodie/media/media.py
Expand Up @@ -42,6 +42,8 @@ def __init__(self, source=None):
'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']
Expand Down Expand Up @@ -132,6 +134,44 @@ def get_exiftool_attributes(self):

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.
Expand Down
52 changes: 43 additions & 9 deletions elodie/tests/filesystem_test.py
Expand Up @@ -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')
media = Photo(helper.get_file('plain.jpg'))
path = filesystem.get_folder_path(media.get_metadata())
if hasattr(load_config, 'config'):
del load_config.config

origin = os.path.join(folder,'plain.jpg')
shutil.copyfile(helper.get_file('plain.jpg'), origin)
assert path == os.path.join('Canon', 'Canon EOS REBEL T2i'), path

media = Photo(origin)
@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('2015-12-Dec','Unknown Location'), path
assert path == os.path.join('nomake', 'nomodel'), path

@mock.patch('elodie.config.config_file', '%s/config.ini-int-in-path' % gettempdir())
@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
Expand All @@ -258,6 +279,19 @@ def test_get_folder_path_with_int_in_config_component():

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:
Expand Down
12 changes: 12 additions & 0 deletions elodie/tests/media/audio_test.py
Expand Up @@ -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')
Expand Down
24 changes: 24 additions & 0 deletions elodie/tests/media/photo_test.py
Expand Up @@ -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'))

Expand Down
13 changes: 13 additions & 0 deletions elodie/tests/media/video_test.py
Expand Up @@ -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()
Expand Down

0 comments on commit 4cd91e9

Please sign in to comment.