Skip to content

Commit

Permalink
Add fallback folder support when configuring folder hierarchy. jmatha…
Browse files Browse the repository at this point in the history
…i#199 (jmathai#209)



* add test
  • Loading branch information
jmathai authored Apr 13, 2017
1 parent 6777c32 commit e312387
Show file tree
Hide file tree
Showing 4 changed files with 195 additions and 79 deletions.
18 changes: 15 additions & 3 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,18 +231,30 @@ year=%Y
date=%year-%month
full_path=%date/%location
# -> 2015-12/Sunnyvale, California
```

#### Using fallback folders

There are times when the EXIF needed to correctly name a folder doesn't exist on a photo. I came up with fallback folders to help you deal with situations such as this. Here's how it works.

You can specify a series of folder names by separating them with a `|`. That's a pipe, not an L. Let's look at an example.

full_path=%country/%state/%city
# -> US/California/Sunnyvale
```
month=%m
year=%Ykkkk
location=%city
full_path=%month/%year/%album|%location|%"Beats me"
```

What this asks me to do is to name the last folder the same as the album I find in EXIF. If I don't find an album in EXIF then I should use the location. If there's no GPS in the EXIf then I should name the last folder `Beats me`.

#### 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.

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`.

I have a few built-in location placeholders you can use.
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.
Expand Down
121 changes: 84 additions & 37 deletions elodie/filesystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,15 @@ class FileSystem(object):

def __init__(self):
# The default folder path is along the lines of 2015-01-Jan/Chicago
self.default_folder_path_definition = [
('date', '%Y-%m-%b'), ('location', '%city')
]
self.default_folder_path_definition = {
'date': '%Y-%m-%b',
'location': '%city',
'full_path': '%date/%album|%location|"{}"'.format(
geolocation.__DEFAULT_LOCATION__
),
}
self.cached_folder_path_definition = None
self.default_parts = ['album', 'city', 'state', 'country']

def create_directory(self, directory_path):
"""Create a directory if it does not already exist.
Expand Down Expand Up @@ -149,6 +154,22 @@ def get_file_name(self, media):
return file_name.lower()

def get_folder_path_definition(self):
"""Returns a list of folder definitions.
Each element in the list represents a folder.
Fallback folders are supported and are nested lists.
Return values take the following form.
[
('date', '%Y-%m-%d'),
[
('location', '%city'),
('album', ''),
('"Unknown Location", '')
]
]
:returns: list
"""
# If we've done this already then return it immediately without
# incurring any extra work
if self.cached_folder_path_definition is not None:
Expand All @@ -158,60 +179,86 @@ def get_folder_path_definition(self):

# If Directory is in the config we assume full_path and its
# corresponding values (date, location) are also present
if('Directory' not in config):
return self.default_folder_path_definition

config_directory = config['Directory']
config_directory = self.default_folder_path_definition
if('Directory' in config):
config_directory = config['Directory']

# Find all subpatterns of full_path that map to directories.
# I.e. %foo/%bar => ['foo', 'bar']
# I.e. %foo/%bar|%example|"something" => ['foo', 'bar|example|"something"']
path_parts = re.findall(
'\%([a-z]+)',
'(\%[^/]+)',
config_directory['full_path']
)

if not path_parts or len(path_parts) == 0:
return self.default_folder_path_definition

self.cached_folder_path_definition = [
(part, config_directory[part]) for part in path_parts
]
self.cached_folder_path_definition = []
for part in path_parts:
if part in config_directory:
part = part[1:]
self.cached_folder_path_definition.append(
[(part, config_directory[part])]
)
elif part in self.default_parts:
part = part[1:]
self.cached_folder_path_definition.append(
[(part, '')]
)
else:
this_part = []
for p in part.split('|'):
p = p[1:]
this_part.append(
(p, config_directory[p] if p in config_directory else '')
)
self.cached_folder_path_definition.append(this_part)

return self.cached_folder_path_definition

def get_folder_path(self, metadata):
"""Get folder path by various parameters.
"""Given a media's metadata this function returns the folder path as a string.
:param metadata dict: Metadata dictionary.
:returns: str
"""
path_parts = self.get_folder_path_definition()
path = []
for path_part in path_parts:
part, mask = path_part
if part in ('date', 'day', 'month', 'year'):
path.append(time.strftime(mask, metadata['date_taken']))
elif part in ('location', 'city', 'state', 'country'):
place_name = geolocation.place_name(
metadata['latitude'],
metadata['longitude']
)

location_parts = re.findall('(%[^%]+)', mask)
parsed_folder_name = self.parse_mask_for_location(
mask,
location_parts,
place_name,
)
path.append(parsed_folder_name)

# For now we always make the leaf folder an album if it's in the EXIF.
# This is to preserve backwards compatability until we figure out how
# to include %album in the config.ini syntax.
if(metadata['album'] is not None):
if(len(path) == 1):
path.append(metadata['album'])
elif(len(path) == 2):
path[1] = metadata['album']
# We support fallback values so that
# 'album|city|"Unknown Location"
# %album|%city|"Unknown Location" results in
# My Album - when an album exists
# Sunnyvale - when no album exists but a city exists
# Unknown Location - when neither an album nor location exist
for this_part in path_part:
part, mask = this_part
if part in ('date', 'day', 'month', 'year'):
path.append(
time.strftime(mask, metadata['date_taken'])
)
break
elif part in ('location', 'city', 'state', 'country'):
place_name = geolocation.place_name(
metadata['latitude'],
metadata['longitude']
)

location_parts = re.findall('(%[^%]+)', mask)
parsed_folder_name = self.parse_mask_for_location(
mask,
location_parts,
place_name,
)
path.append(parsed_folder_name)
break
elif part in ('album'):
if metadata['album']:
path.append(metadata['album'])
break
elif part.startswith('"') and part.endswith('"'):
path.append(part[1:-1])

return os.path.join(*path)

Expand Down
32 changes: 0 additions & 32 deletions elodie/tests/elodie_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -430,38 +430,6 @@ def test_update_time_on_video():
assert metadata['date_taken'] != metadata_processed['date_taken']
assert metadata_processed['date_taken'] == helper.time_convert((2000, 1, 1, 12, 0, 0, 5, 1, 0)), metadata_processed['date_taken']

@mock.patch('elodie.config.config_file', '%s/config.ini-multiple-directories' % gettempdir())
def test_update_with_more_than_two_levels_of_directories():
with open('%s/config.ini-multiple-directories' % gettempdir(), 'w') as f:
f.write("""
[Directory]
year=%Y
month=%m
day=%d
full_path=%year/%month/%day
""")

temporary_folder, folder = helper.create_working_folder()
temporary_folder_destination, folder_destination = helper.create_working_folder()

origin = '%s/plain.jpg' % folder
shutil.copyfile(helper.get_file('plain.jpg'), origin)

if hasattr(load_config, 'config'):
del load_config.config
cfg = load_config()
helper.reset_dbs()
runner = CliRunner()
result = runner.invoke(elodie._import, ['--destination', folder_destination, folder])
runner2 = CliRunner()
result = runner2.invoke(elodie._update, ['--title', 'test title', folder_destination])
helper.restore_dbs()
if hasattr(load_config, 'config'):
del load_config.config

updated_file_path = '{}/2015/12/05/2015-12-05_00-59-26-plain-test-title.jpg'.format(folder_destination)
assert os.path.isfile(updated_file_path), updated_file_path

def test_update_with_directory_passed_in():
temporary_folder, folder = helper.create_working_folder()
temporary_folder_destination, folder_destination = helper.create_working_folder()
Expand Down
Loading

0 comments on commit e312387

Please sign in to comment.