Completed
Push — master ( 7edcfe...8eaba9 )
by Jaisen
10s
created

import_file()   F

↳ Parent: Project

Complexity

Conditions 9

Duplication

Lines 0
Ratio 0 %

Size

Total Lines 33

Importance

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