Passed
Push — master ( 75e659...d8cee1 )
by Jaisen
01:58
created

elodie.media.media   D

Complexity

Total Complexity 59

Size/Duplication

Total Lines 320
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 166
dl 0
loc 320
rs 4.08
c 0
b 0
f 0
wmc 59

15 Methods

Rating   Name   Duplication   Size   Complexity  
D Media.get_coordinate() 0 40 12
A Media.__init__() 0 20 1
A Media.get_album() 0 17 5
A Media.get_original_name() 0 17 4
A Media.set_date_taken() 0 17 3
A Media.get_title() 0 17 4
A Media.set_title() 0 17 3
A Media.__set_tags() 0 10 2
A Media.set_location() 0 26 5
A Media.get_camera_model() 0 18 5
A Media.get_camera_make() 0 18 5
A Media.set_original_name() 0 21 4
A Media.set_album() 0 14 2
A Media.reset_cache() 0 6 1
A Media.get_exiftool_attributes() 0 15 3

How to fix   Complexity   

Complexity

Complex classes like elodie.media.media 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
"""
2
The media module provides a base :class:`Media` class for media objects that
3
are tracked by Elodie. The Media class provides some base functionality used
4
by all the media types, but isn't itself used to represent anything. Its
5
sub-classes (:class:`~elodie.media.audio.Audio`,
6
:class:`~elodie.media.photo.Photo`, and :class:`~elodie.media.video.Video`)
7
are used to represent the actual files.
8
9
.. moduleauthor:: Jaisen Mathai <[email protected]>
10
"""
11
from __future__ import print_function
12
13
import os
14
15
# load modules
16
from elodie.external.pyexiftool import ExifTool
17
from elodie.media.base import Base
18
19
class Media(Base):
20
21
    """The base class for all media objects.
22
23
    :param str source: The fully qualified path to the video file.
24
    """
25
26
    __name__ = 'Media'
27
28
    d_coordinates = {
29
        'latitude': 'latitude_ref',
30
        'longitude': 'longitude_ref'
31
    }
32
33
    def __init__(self, source=None):
34
        super(Media, self).__init__(source)
35
        self.exif_map = {
36
            'date_taken': [
37
                'EXIF:DateTimeOriginal',
38
                'EXIF:CreateDate',
39
                'EXIF:ModifyDate'
40
            ]
41
        }
42
        self.camera_make_keys = ['EXIF:Make', 'QuickTime:Make']
43
        self.camera_model_keys = ['EXIF:Model', 'QuickTime:Model']
44
        self.album_keys = ['XMP-xmpDM:Album', 'XMP:Album']
45
        self.title_key = 'XMP:Title'
46
        self.latitude_keys = ['EXIF:GPSLatitude']
47
        self.longitude_keys = ['EXIF:GPSLongitude']
48
        self.latitude_ref_key = 'EXIF:GPSLatitudeRef'
49
        self.longitude_ref_key = 'EXIF:GPSLongitudeRef'
50
        self.original_name_key = 'XMP:OriginalFileName'
51
        self.set_gps_ref = True
52
        self.exif_metadata = None
53
54
    def get_album(self):
55
        """Get album from EXIF
56
57
        :returns: None or string
58
        """
59
        if(not self.is_valid()):
60
            return None
61
62
        exiftool_attributes = self.get_exiftool_attributes()
63
        if exiftool_attributes is None:
64
            return None
65
66
        for album_key in self.album_keys:
67
            if album_key in exiftool_attributes:
68
                return exiftool_attributes[album_key]
69
70
        return None
71
72
    def get_coordinate(self, type='latitude'):
73
        """Get latitude or longitude of media from EXIF
74
75
        :param str type: Type of coordinate to get. Either "latitude" or
76
            "longitude".
77
        :returns: float or None if not present in EXIF or a non-photo file
78
        """
79
80
        exif = self.get_exiftool_attributes()
81
        if not exif:
82
            return None
83
84
        # The lat/lon _keys array has an order of precedence.
85
        # The first key is writable and we will give the writable
86
        #   key precence when reading.
87
        direction_multiplier = 1.0
88
        for key in self.latitude_keys + self.longitude_keys:
89
            if key not in exif:
90
                continue
91
92
            # Cast coordinate to a float due to a bug in exiftool's
93
            #   -json output format.
94
            # https://github.com/jmathai/elodie/issues/171
95
            # http://u88.n24.queensu.ca/exiftool/forum/index.php/topic,7952.0.html  # noqa
96
            this_coordinate = float(exif[key])
97
98
            # TODO: verify that we need to check ref key
99
            #   when self.set_gps_ref != True
100
            if type == 'latitude' and key in self.latitude_keys:
101
                if self.latitude_ref_key in exif and \
102
                        exif[self.latitude_ref_key] == 'S':
103
                    direction_multiplier = -1.0
104
                return this_coordinate * direction_multiplier
105
            elif type == 'longitude' and key in self.longitude_keys:
106
                if self.longitude_ref_key in exif and \
107
                        exif[self.longitude_ref_key] == 'W':
108
                    direction_multiplier = -1.0
109
                return this_coordinate * direction_multiplier
110
111
        return None
112
113
    def get_exiftool_attributes(self):
114
        """Get attributes for the media object from exiftool.
115
116
        :returns: dict, or False if exiftool was not available.
117
        """
118
        source = self.source
119
120
        #Cache exif metadata results and use if already exists for media
121
        if(self.exif_metadata is None):
122
            self.exif_metadata = ExifTool().get_metadata(source)
123
124
        if not self.exif_metadata:
125
            return False
126
127
        return self.exif_metadata
128
129
    def get_camera_make(self):
130
        """Get the camera make stored in EXIF.
131
132
        :returns: str
133
        """
134
        if(not self.is_valid()):
135
            return None
136
137
        exiftool_attributes = self.get_exiftool_attributes()
138
139
        if exiftool_attributes is None:
140
            return None
141
142
        for camera_make_key in self.camera_make_keys:
143
            if camera_make_key in exiftool_attributes:
144
                return exiftool_attributes[camera_make_key]
145
146
        return None
147
148
    def get_camera_model(self):
149
        """Get the camera make stored in EXIF.
150
151
        :returns: str
152
        """
153
        if(not self.is_valid()):
154
            return None
155
156
        exiftool_attributes = self.get_exiftool_attributes()
157
158
        if exiftool_attributes is None:
159
            return None
160
161
        for camera_model_key in self.camera_model_keys:
162
            if camera_model_key in exiftool_attributes:
163
                return exiftool_attributes[camera_model_key]
164
165
        return None
166
167
    def get_original_name(self):
168
        """Get the original name stored in EXIF.
169
170
        :returns: str
171
        """
172
        if(not self.is_valid()):
173
            return None
174
175
        exiftool_attributes = self.get_exiftool_attributes()
176
177
        if exiftool_attributes is None:
178
            return None
179
180
        if(self.original_name_key not in exiftool_attributes):
181
            return None
182
183
        return exiftool_attributes[self.original_name_key]
184
185
    def get_title(self):
186
        """Get the title for a photo of video
187
188
        :returns: str or None if no title is set or not a valid media type
189
        """
190
        if(not self.is_valid()):
191
            return None
192
193
        exiftool_attributes = self.get_exiftool_attributes()
194
195
        if exiftool_attributes is None:
196
            return None
197
198
        if(self.title_key not in exiftool_attributes):
199
            return None
200
201
        return exiftool_attributes[self.title_key]
202
203
    def reset_cache(self):
204
        """Resets any internal cache
205
        """
206
        self.exiftool_attributes = None
207
        self.exif_metadata = None
208
        super(Media, self).reset_cache()
209
210
    def set_album(self, album):
211
        """Set album for a photo
212
213
        :param str name: Name of album
214
        :returns: bool
215
        """
216
        if(not self.is_valid()):
217
            return None
218
219
        tags = {self.album_keys[0]: album}
220
        status = self.__set_tags(tags)
221
        self.reset_cache()
222
223
        return status
224
225
    def set_date_taken(self, time):
226
        """Set the date/time a photo was taken.
227
228
        :param datetime time: datetime object of when the photo was taken
229
        :returns: bool
230
        """
231
        if(time is None):
232
            return False
233
234
        tags = {}
235
        formatted_time = time.strftime('%Y:%m:%d %H:%M:%S')
236
        for key in self.exif_map['date_taken']:
237
            tags[key] = formatted_time
238
239
        status = self.__set_tags(tags)
240
        self.reset_cache()
241
        return status
242
243
    def set_location(self, latitude, longitude):
244
        if(not self.is_valid()):
245
            return None
246
247
        # The lat/lon _keys array has an order of precedence.
248
        # The first key is writable and we will give the writable
249
        #   key precence when reading.
250
        tags = {
251
            self.latitude_keys[0]: latitude,
252
            self.longitude_keys[0]: longitude,
253
        }
254
255
        # If self.set_gps_ref == True then it means we are writing an EXIF
256
        #   GPS tag which requires us to set the reference key.
257
        # That's because the lat/lon are absolute values.
258
        if self.set_gps_ref:
259
            if latitude < 0:
260
                tags[self.latitude_ref_key] = 'S'
261
262
            if longitude < 0:
263
                tags[self.longitude_ref_key] = 'W'
264
265
        status = self.__set_tags(tags)
266
        self.reset_cache()
267
268
        return status
269
270
    def set_original_name(self, name=None):
271
        """Sets the original name EXIF tag if not already set.
272
273
        :returns: True, False, None
274
        """
275
        if(not self.is_valid()):
276
            return None
277
278
        # If EXIF original name tag is set then we return.
279
        if self.get_original_name() is not None:
280
            return None
281
282
        source = self.source
283
284
        if not name:
285
            name = os.path.basename(source)
286
287
        tags = {self.original_name_key: name}
288
        status = self.__set_tags(tags)
289
        self.reset_cache()
290
        return status
291
292
    def set_title(self, title):
293
        """Set title for a photo.
294
295
        :param str title: Title of the photo.
296
        :returns: bool
297
        """
298
        if(not self.is_valid()):
299
            return None
300
301
        if(title is None):
302
            return None
303
304
        tags = {self.title_key: title}
305
        status = self.__set_tags(tags)
306
        self.reset_cache()
307
308
        return status
309
310
    def __set_tags(self, tags):
311
        if(not self.is_valid()):
312
            return None
313
314
        source = self.source
315
316
        status = ''
317
        status = ExifTool().set_tags(tags,source)
318
319
        return status != ''
320