Passed
Push — master ( 3ad6c0...75e659 )
by Jaisen
01:58
created

elodie._import()   C

Complexity

Conditions 10

Size

Total Lines 62
Code Lines 50

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
eloc 50
nop 9
dl 0
loc 62
rs 5.8362
c 0
b 0
f 0

How to fix   Long Method    Complexity    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like elodie._import() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
#!/usr/bin/env python
2
3
from __future__ import print_function
4
import os
5
import re
6
import sys
7
from datetime import datetime
8
9
import click
10
from send2trash import send2trash
11
12
# Verify that external dependencies are present first, so the user gets a
13
# more user-friendly error instead of an ImportError traceback.
14
from elodie.dependencies import verify_dependencies
15
if not verify_dependencies():
16
    sys.exit(1)
17
18
from elodie import constants
19
from elodie import geolocation
20
from elodie import log
21
from elodie.compatability import _decode
22
from elodie.config import load_config
23
from elodie.filesystem import FileSystem
24
from elodie.localstorage import Db
25
from elodie.media.base import Base, get_all_subclasses
26
from elodie.media.media import Media
27
from elodie.media.text import Text
28
from elodie.media.audio import Audio
29
from elodie.media.photo import Photo
30
from elodie.media.video import Video
31
from elodie.plugins.plugins import Plugins
32
from elodie.result import Result
33
34
35
FILESYSTEM = FileSystem()
36
37
38
def import_file(_file, destination, album_from_folder, trash, allow_duplicates):
39
    
40
    _file = _decode(_file)
41
    destination = _decode(destination)
42
43
    """Set file metadata and move it to destination.
44
    """
45
    if not os.path.exists(_file):
46
        log.warn('Could not find %s' % _file)
47
        log.all('{"source":"%s", "error_msg":"Could not find %s"}' %
48
                  (_file, _file))
49
        return
50
    # Check if the source, _file, is a child folder within destination
51
    elif destination.startswith(os.path.abspath(os.path.dirname(_file))+os.sep):
52
        log.all('{"source": "%s", "destination": "%s", "error_msg": "Source cannot be in destination"}' % (
53
            _file, destination))
54
        return
55
56
57
    media = Media.get_class_by_file(_file, get_all_subclasses())
58
    if not media:
59
        log.warn('Not a supported file (%s)' % _file)
60
        log.all('{"source":"%s", "error_msg":"Not a supported file"}' % _file)
61
        return
62
63
    if album_from_folder:
64
        media.set_album_from_folder()
65
66
    dest_path = FILESYSTEM.process_file(_file, destination,
67
        media, allowDuplicate=allow_duplicates, move=False)
68
    if dest_path:
69
        log.all('%s -> %s' % (_file, dest_path))
70
    if trash:
71
        send2trash(_file)
72
73
    return dest_path or None
74
75
@click.command('batch')
76
@click.option('--debug', default=False, is_flag=True,
77
              help='Override the value in constants.py with True.')
78
def _batch(debug):
79
    """Run batch() for all plugins.
80
    """
81
    constants.debug = debug
82
    plugins = Plugins()
83
    plugins.run_batch()
84
       
85
86
@click.command('import')
87
@click.option('--destination', type=click.Path(file_okay=False),
88
              required=True, help='Copy imported files into this directory.')
89
@click.option('--source', type=click.Path(file_okay=False),
90
              help='Import files from this directory, if specified.')
91
@click.option('--file', type=click.Path(dir_okay=False),
92
              help='Import this file, if specified.')
93
@click.option('--album-from-folder', default=False, is_flag=True,
94
              help="Use images' folders as their album names.")
95
@click.option('--trash', default=False, is_flag=True,
96
              help='After copying files, move the old files to the trash.')
97
@click.option('--allow-duplicates', default=False, is_flag=True,
98
              help='Import the file even if it\'s already been imported.')
99
@click.option('--debug', default=False, is_flag=True,
100
              help='Override the value in constants.py with True.')
101
@click.option('--exclude-regex', default=set(), multiple=True,
102
              help='Regular expression for directories or files to exclude.')
103
@click.argument('paths', nargs=-1, type=click.Path())
104
def _import(destination, source, file, album_from_folder, trash, allow_duplicates, debug, exclude_regex, paths):
105
    """Import files or directories by reading their EXIF and organizing them accordingly.
106
    """
107
    constants.debug = debug
108
    has_errors = False
109
    result = Result()
110
111
    destination = _decode(destination)
112
    destination = os.path.abspath(os.path.expanduser(destination))
113
114
    files = set()
115
    paths = set(paths)
116
    if source:
117
        source = _decode(source)
118
        paths.add(source)
119
    if file:
120
        paths.add(file)
121
122
    # if no exclude list was passed in we check if there's a config
123
    if len(exclude_regex) == 0:
124
        config = load_config()
125
        if 'Exclusions' in config:
126
            exclude_regex = [value for key, value in config.items('Exclusions')]
127
128
    exclude_regex_list = set(exclude_regex)
129
130
    for path in paths:
131
        path = os.path.expanduser(path)
132
        if os.path.isdir(path):
133
            files.update(FILESYSTEM.get_all_files(path, None, exclude_regex_list))
134
        else:
135
            if not FILESYSTEM.should_exclude(path, exclude_regex_list, True):
136
                files.add(path)
137
138
    for current_file in files:
139
        dest_path = import_file(current_file, destination, album_from_folder,
140
                    trash, allow_duplicates)
141
        result.append((current_file, dest_path))
142
        has_errors = has_errors is True or not dest_path
143
144
    result.write()
145
146
    if has_errors:
147
        sys.exit(1)
148
149
150
@click.command('generate-db')
151
@click.option('--source', type=click.Path(file_okay=False),
152
              required=True, help='Source of your photo library.')
153
@click.option('--debug', default=False, is_flag=True,
154
              help='Override the value in constants.py with True.')
155
def _generate_db(source, debug):
156
    """Regenerate the hash.json database which contains all of the sha256 signatures of media files. The hash.json file is located at ~/.elodie/.
157
    """
158
    constants.debug = debug
159
    result = Result()
160
    source = os.path.abspath(os.path.expanduser(source))
161
162
    if not os.path.isdir(source):
163
        log.error('Source is not a valid directory %s' % source)
164
        sys.exit(1)
165
        
166
    db = Db()
167
    db.backup_hash_db()
168
    db.reset_hash_db()
169
170
    for current_file in FILESYSTEM.get_all_files(source):
171
        result.append((current_file, True))
172
        db.add_hash(db.checksum(current_file), current_file)
173
        log.progress()
174
    
175
    db.update_hash_db()
176
    log.progress('', True)
177
    result.write()
178
179
@click.command('verify')
180
@click.option('--debug', default=False, is_flag=True,
181
              help='Override the value in constants.py with True.')
182
def _verify(debug):
183
    constants.debug = debug
184
    result = Result()
185
    db = Db()
186
    for checksum, file_path in db.all():
187
        if not os.path.isfile(file_path):
188
            result.append((file_path, False))
189
            log.progress('x')
190
            continue
191
192
        actual_checksum = db.checksum(file_path)
193
        if checksum == actual_checksum:
194
            result.append((file_path, True))
195
            log.progress()
196
        else:
197
            result.append((file_path, False))
198
            log.progress('x')
199
200
    log.progress('', True)
201
    result.write()
202
203
204
def update_location(media, file_path, location_name):
205
    """Update location exif metadata of media.
206
    """
207
    location_coords = geolocation.coordinates_by_name(location_name)
208
209
    if location_coords and 'latitude' in location_coords and \
210
            'longitude' in location_coords:
211
        location_status = media.set_location(location_coords[
212
            'latitude'], location_coords['longitude'])
213
        if not location_status:
214
            log.error('Failed to update location')
215
            log.all(('{"source":"%s",' % file_path,
216
                       '"error_msg":"Failed to update location"}'))
217
            sys.exit(1)
218
    return True
219
220
221
def update_time(media, file_path, time_string):
222
    """Update time exif metadata of media.
223
    """
224
    time_format = '%Y-%m-%d %H:%M:%S'
225
    if re.match(r'^\d{4}-\d{2}-\d{2}$', time_string):
226
        time_string = '%s 00:00:00' % time_string
227
    elif re.match(r'^\d{4}-\d{2}-\d{2} \d{2}:\d{2}\d{2}$', time_string):
228
        msg = ('Invalid time format. Use YYYY-mm-dd hh:ii:ss or YYYY-mm-dd')
229
        log.error(msg)
230
        log.all('{"source":"%s", "error_msg":"%s"}' % (file_path, msg))
231
        sys.exit(1)
232
233
    time = datetime.strptime(time_string, time_format)
234
    media.set_date_taken(time)
235
    return True
236
237
238
@click.command('update')
239
@click.option('--album', help='Update the image album.')
240
@click.option('--location', help=('Update the image location. Location '
241
                                  'should be the name of a place, like "Las '
242
                                  'Vegas, NV".'))
243
@click.option('--time', help=('Update the image time. Time should be in '
244
                              'YYYY-mm-dd hh:ii:ss or YYYY-mm-dd format.'))
245
@click.option('--title', help='Update the image title.')
246
@click.option('--debug', default=False, is_flag=True,
247
              help='Override the value in constants.py with True.')
248
@click.argument('paths', nargs=-1,
249
                required=True)
250
def _update(album, location, time, title, paths, debug):
251
    """Update a file's EXIF. Automatically modifies the file's location and file name accordingly.
252
    """
253
    constants.debug = debug
254
    has_errors = False
255
    result = Result()
256
257
    files = set()
258
    for path in paths:
259
        path = os.path.expanduser(path)
260
        if os.path.isdir(path):
261
            files.update(FILESYSTEM.get_all_files(path, None))
262
        else:
263
            files.add(path)
264
265
    for current_file in files:
266
        if not os.path.exists(current_file):
267
            has_errors = True
268
            result.append((current_file, False))
269
            log.warn('Could not find %s' % current_file)
270
            log.all('{"source":"%s", "error_msg":"Could not find %s"}' %
271
                      (current_file, current_file))
272
            continue
273
274
        current_file = os.path.expanduser(current_file)
275
276
        # The destination folder structure could contain any number of levels
277
        #  So we calculate that and traverse up the tree.
278
        # '/path/to/file/photo.jpg' -> '/path/to/file' ->
279
        #  ['path','to','file'] -> ['path','to'] -> '/path/to'
280
        current_directory = os.path.dirname(current_file)
281
        destination_depth = -1 * len(FILESYSTEM.get_folder_path_definition())
282
        destination = os.sep.join(
283
                          os.path.normpath(
284
                              current_directory
285
                          ).split(os.sep)[:destination_depth]
286
                      )
287
288
        media = Media.get_class_by_file(current_file, get_all_subclasses())
289
        if not media:
290
            continue
291
292
        updated = False
293
        if location:
294
            update_location(media, current_file, location)
295
            updated = True
296
        if time:
297
            update_time(media, current_file, time)
298
            updated = True
299
        if album:
300
            media.set_album(album)
301
            updated = True
302
303
        # Updating a title can be problematic when doing it 2+ times on a file.
304
        # You would end up with img_001.jpg -> img_001-first-title.jpg ->
305
        # img_001-first-title-second-title.jpg.
306
        # To resolve that we have to track the prior title (if there was one.
307
        # Then we massage the updated_media's metadata['base_name'] to remove
308
        # the old title.
309
        # Since FileSystem.get_file_name() relies on base_name it will properly
310
        #  rename the file by updating the title instead of appending it.
311
        remove_old_title_from_name = False
312
        if title:
313
            # We call get_metadata() to cache it before making any changes
314
            metadata = media.get_metadata()
315
            title_update_status = media.set_title(title)
316
            original_title = metadata['title']
317
            if title_update_status and original_title:
318
                # @TODO: We should move this to a shared method since
319
                # FileSystem.get_file_name() does it too.
320
                original_title = re.sub(r'\W+', '-', original_title.lower())
321
                original_base_name = metadata['base_name']
322
                remove_old_title_from_name = True
323
            updated = True
324
325
        if updated:
326
            updated_media = Media.get_class_by_file(current_file,
327
                                                    get_all_subclasses())
328
            # See comments above on why we have to do this when titles
329
            # get updated.
330
            if remove_old_title_from_name and len(original_title) > 0:
0 ignored issues
show
introduced by
The variable original_title does not seem to be defined for all execution paths.
Loading history...
331
                updated_media.get_metadata()
332
                updated_media.set_metadata_basename(
333
                    original_base_name.replace('-%s' % original_title, ''))
0 ignored issues
show
introduced by
The variable original_base_name does not seem to be defined for all execution paths.
Loading history...
334
335
            dest_path = FILESYSTEM.process_file(current_file, destination,
336
                updated_media, move=True, allowDuplicate=True)
337
            log.info(u'%s -> %s' % (current_file, dest_path))
338
            log.all('{"source":"%s", "destination":"%s"}' % (current_file,
339
                                                               dest_path))
340
            # If the folder we moved the file out of or its parent are empty
341
            # we delete it.
342
            FILESYSTEM.delete_directory_if_empty(os.path.dirname(current_file))
343
            FILESYSTEM.delete_directory_if_empty(
344
                os.path.dirname(os.path.dirname(current_file)))
345
            result.append((current_file, dest_path))
346
            # Trip has_errors to False if it's already False or dest_path is.
347
            has_errors = has_errors is True or not dest_path
348
        else:
349
            has_errors = False
350
            result.append((current_file, False))
351
352
    result.write()
353
    
354
    if has_errors:
355
        sys.exit(1)
356
357
358
@click.group()
359
def main():
360
    pass
361
362
363
main.add_command(_import)
364
main.add_command(_update)
365
main.add_command(_generate_db)
366
main.add_command(_verify)
367
main.add_command(_batch)
368
369
370
if __name__ == '__main__':
371
    main()
372