Completed
Push — master ( 3fc43c...86313e )
by Jaisen
10s
created

import_file()   C

Complexity

Conditions 7

Duplication

Lines 0
Ratio 0 %

Size

Total Lines 31

Importance

Changes 6
Bugs 1 Features 0
Metric Value
cc 7
c 6
b 1
f 0
dl 0
loc 31
rs 5.5
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.media.base import Base
22
from elodie.media.media import Media
23
from elodie.media.text import Text
24
from elodie.media.audio import Audio
25
from elodie.media.photo import Photo
26
from elodie.media.video import Video
27
from elodie.filesystem import FileSystem
28
from elodie.localstorage import Db
29
30
31
DB = Db()
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": "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
    destination = os.path.abspath(os.path.expanduser(destination))
86
87
    files = set()
88
    paths = set(paths)
89
    if source:
90
        paths.add(source)
91
    if file:
92
        paths.add(file)
93
    for path in paths:
94
        path = os.path.expanduser(path)
95
        if os.path.isdir(path):
96
            files.update(FILESYSTEM.get_all_files(path, None))
97
        else:
98
            files.add(path)
99
100
    for current_file in files:
101
        import_file(current_file, destination, album_from_folder,
102
                    trash, allow_duplicates)
103
104
105
def update_location(media, file_path, location_name):
106
    """Update location exif metadata of media.
107
    """
108
    location_coords = geolocation.coordinates_by_name(location_name)
109
110
    if location_coords and 'latitude' in location_coords and \
111
            'longitude' in location_coords:
112
        location_status = media.set_location(location_coords[
113
            'latitude'], location_coords['longitude'])
114
        if not location_status:
115
            log.error('Failed to update location')
116
            print(('{"source":"%s",' % file_path,
117
                '"error_msg":"Failed to update location"}'))
118
            sys.exit(1)
119
    return True
120
121
122
def update_time(media, file_path, time_string):
123
    """Update time exif metadata of media.
124
    """
125
    time_format = '%Y-%m-%d %H:%M:%S'
126
    if re.match(r'^\d{4}-\d{2}-\d{2}$', time_string):
127
        time_string = '%s 00:00:00' % time_string
128
    elif re.match(r'^\d{4}-\d{2}-\d{2} \d{2}:\d{2}\d{2}$', time_string):
129
        msg = ('Invalid time format. Use YYYY-mm-dd hh:ii:ss or YYYY-mm-dd')
130
        log.error(msg)
131
        print('{"source":"%s", "error_msg":"%s"}' % (file_path, msg))
132
        sys.exit(1)
133
134
    time = datetime.strptime(time_string, time_format)
135
    media.set_date_taken(time)
136
    return True
137
138
139
@click.command('update')
140
@click.option('--album', help='Update the image album.')
141
@click.option('--location', help=('Update the image location. Location '
142
                                  'should be the name of a place, like "Las '
143
                                  'Vegas, NV".'))
144
@click.option('--time', help=('Update the image time. Time should be in '
145
                              'YYYY-mm-dd hh:ii:ss or YYYY-mm-dd format.'))
146
@click.option('--title', help='Update the image title.')
147
@click.argument('files', nargs=-1, type=click.Path(dir_okay=False),
148
                required=True)
149
def _update(album, location, time, title, files):
150
    """Update a file's EXIF. Automatically modifies the file's location and file name accordingly.
151
    """
152
    for file_path in files:
153
        if not os.path.exists(file_path):
154
            log.warn('Could not find %s' % file_path)
155
            print('{"source":"%s", "error_msg":"Could not find %s"}' % \
156
                (file_path, file_path))
157
            continue
158
159
        file_path = os.path.expanduser(file_path)
160
        destination = os.path.expanduser(os.path.dirname(os.path.dirname(
161
                                         os.path.dirname(file_path))))
162
163
        media = Media.get_class_by_file(file_path, [Text, Audio, Photo, Video])
164
        if not media:
165
            continue
166
167
        updated = False
168
        if location:
169
            update_location(media, file_path, location)
170
            updated = True
171
        if time:
172
            update_time(media, file_path, time)
173
            updated = True
174
        if album:
175
            media.set_album(album)
176
            updated = True
177
178
        # Updating a title can be problematic when doing it 2+ times on a file.
179
        # You would end up with img_001.jpg -> img_001-first-title.jpg ->
180
        # img_001-first-title-second-title.jpg.
181
        # To resolve that we have to track the prior title (if there was one.
182
        # Then we massage the updated_media's metadata['base_name'] to remove
183
        # the old title.
184
        # Since FileSystem.get_file_name() relies on base_name it will properly
185
        #  rename the file by updating the title instead of appending it.
186
        remove_old_title_from_name = False
187
        if title:
188
            # We call get_metadata() to cache it before making any changes
189
            metadata = media.get_metadata()
190
            title_update_status = media.set_title(title)
191
            original_title = metadata['title']
192
            if title_update_status and original_title:
193
                # @TODO: We should move this to a shared method since
194
                # FileSystem.get_file_name() does it too.
195
                original_title = re.sub(r'\W+', '-', original_title.lower())
196
                original_base_name = metadata['base_name']
197
                remove_old_title_from_name = True
198
            updated = True
199
200
        if updated:
201
            updated_media = Media.get_class_by_file(file_path,
202
                                                    [Text, Audio, Photo, Video])
203
            # See comments above on why we have to do this when titles
204
            # get updated.
205
            if remove_old_title_from_name and len(original_title) > 0:
206
                updated_media.get_metadata()
207
                updated_media.set_metadata_basename(
208
                    original_base_name.replace('-%s' % original_title, ''))
209
210
            dest_path = FILESYSTEM.process_file(file_path, destination,
211
                updated_media, move=True, allowDuplicate=True)
212
            log.info(u'%s -> %s' % (file_path, dest_path))
213
            print('{"source":"%s", "destination":"%s"}' % (file_path,
214
                dest_path))
215
            # If the folder we moved the file out of or its parent are empty
216
            # we delete it.
217
            FILESYSTEM.delete_directory_if_empty(os.path.dirname(file_path))
218
            FILESYSTEM.delete_directory_if_empty(
219
                os.path.dirname(os.path.dirname(file_path)))
220
221
222
@click.group()
223
def main():
224
    pass
225
226
227
main.add_command(_import)
228
main.add_command(_update)
229
230
231
if __name__ == '__main__':
232
    main()
233