parent
362b1b1363
commit
4cd91e9f2d
25
Readme.md
25
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
|
#### 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
|
||||||
|
|
||||||
|
|
|
@ -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])
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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'))
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Reference in New Issue