Completed
Push — master ( 9950e8...a82114 )
by Jaisen
8s
created

_generate_db()   B

Complexity

Conditions 5

Duplication

Lines 0
Ratio 0 %

Size

Total Lines 34

Importance

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