_update()   F
last analyzed

↳ Parent: Project

Complexity

Conditions 17

Duplication

Lines 0
Ratio 0 %

Size

Total Lines 106

Importance

Changes 5
Bugs 1 Features 0
Metric Value
cc 17
c 5
b 1
f 0
dl 0
loc 106
rs 2

How to fix   Long Method    Complexity   

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 _update() 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.

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