Completed
Push — master ( a82114...a7c9a5 )
by Jaisen
12s
created

_verify()   A

Complexity

Conditions 4

Duplication

Lines 0
Ratio 0 %

Size

Total Lines 16

Importance

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