Passed
Pull Request — master (#310)
by Jaisen
01:25
created

elodie.media.base.Base.get_date_taken_adjusted()   B

Complexity

Conditions 6

Size

Total Lines 28
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 17
nop 4
dl 0
loc 28
rs 8.6166
c 0
b 0
f 0
1
"""
2
The base module provides a base :class:`Base` class for all objects that
3
are tracked by Elodie. The Base 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`, :class:`~elodie.media.video.Video`, and
7
:class:`~elodie.media.text.Text`)
8
are used to represent the actual files.
9
10
.. moduleauthor:: Jaisen Mathai <[email protected]>
11
"""
12
13
import mimetypes
14
import os
15
from datetime import datetime, timedelta
16
from pytz import timezone
17
from time import mktime
18
from tzwhere.tzwhere import tzwhere
19
20
from elodie.config import load_config
21
22
try:        # Py3k compatibility
23
    basestring
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable basestring does not seem to be defined.
Loading history...
24
except NameError:
25
    basestring = (bytes, str)
26
27
28
class Base(object):
29
30
    """The base class for all media objects.
31
32
    :param str source: The fully qualified path to the video file.
33
    """
34
35
    __name__ = 'Base'
36
37
    extensions = ()
38
39
    def __init__(self, source=None):
40
        self.source = source
41
        self.reset_cache()
42
43
    def format_metadata(self, **kwargs):
44
        """Method to consistently return a populated metadata dictionary.
45
46
        :returns: dict
47
        """
48
49
    def get_album(self):
50
        """Base method for getting an album
51
52
        :returns: None
53
        """
54
        return None
55
56
    def get_camera_make(self):
57
        return None
58
59
    def get_camera_model(self):
60
        return None
61
62
    def get_date_taken_adjusted(self, date_taken, latitude, longitude):
63
        """Returns date taken and adjust based on time zone if needed.
64
        Time zone conversion is based on a configurable value from config.ini.
65
66
        :returns: int
67
        """
68
        config = load_config()
69
70
        # Check if use_location config is true and file has lat/lon
71
        # Else default to returning date_taken directly from file
72
        if(
73
            'Timezone' in config and
74
            'use_location' in config['Timezone'] and
75
            config['Timezone'].getboolean('use_location') is True and
76
            latitude is not None and
77
            longitude is not None
78
        ):
79
            timezone_string = tzwhere().tzNameAt(
80
                                latitude,
81
                                longitude
82
                              )
83
            date_taken_as_time = datetime.fromtimestamp(mktime(date_taken))
84
            now_in_timezone = datetime.now(timezone(timezone_string))
85
            seconds_offset = now_in_timezone.utcoffset().total_seconds()
86
            adjusted_date_taken = date_taken_as_time - timedelta(seconds = seconds_offset)
87
            return adjusted_date_taken.timetuple()
88
89
        return date_taken
90
91
    def get_coordinate(self, type):
92
        return None
93
94
    def get_extension(self):
95
        """Get the file extension as a lowercased string.
96
97
        :returns: string or None for a non-video
98
        """
99
        if(not self.is_valid()):
100
            return None
101
102
        source = self.source
103
        return os.path.splitext(source)[1][1:].lower()
104
105
    def get_file_path(self):
106
        """Get the full path to the video.
107
108
        :returns: string
109
        """
110
        return self.source
111
112
    def get_metadata(self, update_cache=False):
113
        """Get a dictionary of metadata for any file.
114
115
        All keys will be present and have a value of None if not obtained.
116
117
        :returns: dict or None for non-text files
118
        """
119
        if(not self.is_valid()):
120
            return None
121
122
        if(isinstance(self.metadata, dict) and update_cache is False):
123
            return self.metadata
124
125
        source = self.source
126
127
        self.metadata = {
128
            'date_taken': self.get_date_taken(),
129
            'camera_make': self.get_camera_make(),
130
            'camera_model': self.get_camera_model(),
131
            'latitude': self.get_coordinate('latitude'),
132
            'longitude': self.get_coordinate('longitude'),
133
            'album': self.get_album(),
134
            'title': self.get_title(),
135
            'mime_type': self.get_mimetype(),
136
            'original_name': self.get_original_name(),
137
            'base_name': os.path.splitext(os.path.basename(source))[0],
138
            'extension': self.get_extension(),
139
            'directory_path': os.path.dirname(source)
140
        }
141
        self.metadata['date_taken_adjusted'] = self.get_date_taken_adjusted(
142
                                                self.metadata['date_taken'],
143
                                                self.metadata['latitude'],
144
                                                self.metadata['longitude']
145
                                               )
146
        return self.metadata
147
148
    def get_mimetype(self):
149
        """Get the mimetype of the file.
150
151
        :returns: str or None for unsupported files.
152
        """
153
        if(not self.is_valid()):
154
            return None
155
156
        source = self.source
157
        mimetype = mimetypes.guess_type(source)
158
        if(mimetype is None):
159
            return None
160
161
        return mimetype[0]
162
163
    def get_original_name(self):
164
        """Get the original name of the file from before it was imported.
165
        Does not include the extension.
166
        Overridden by Media class for files with EXIF.
167
168
        :returns: str or None for unsupported files.
169
        """
170
        return None
171
172
    def get_title(self):
173
        """Base method for getting the title of a file
174
175
        :returns: None
176
        """
177
        return None
178
179
    def is_valid(self):
180
        """Check the file extension against valid file extensions.
181
182
        The list of valid file extensions come from self.extensions.
183
184
        :returns: bool
185
        """
186
        source = self.source
187
        return os.path.splitext(source)[1][1:].lower() in self.extensions
188
189
    def reset_cache(self):
190
        """Resets any internal cache
191
        """
192
        self.metadata = None
193
194
    def set_album(self, name):
195
        """Base method for setting the album of a file
196
197
        :returns: None
198
        """
199
        return None
200
201
    def set_album_from_folder(self):
202
        """Set the album attribute based on the leaf folder name
203
204
        :returns: bool
205
        """
206
        metadata = self.get_metadata()
207
208
        # If this file has an album already set we do not overwrite EXIF
209
        if(not isinstance(metadata, dict) or metadata['album'] is not None):
210
            return False
211
212
        folder = os.path.basename(metadata['directory_path'])
213
        # If folder is empty we skip
214
        if(len(folder) == 0):
215
            return False
216
217
        self.set_album(folder)
218
        return True
219
220
    def set_metadata_basename(self, new_basename):
221
        """Update the basename attribute in the metadata dict for this instance.
222
223
        This is used for when we update the EXIF title of a media file. Since
224
        that determines the name of a file if we update the title of a file
225
        more than once it appends to the file name.
226
227
        i.e. 2015-12-31_00-00-00-my-first-title-my-second-title.jpg
228
229
        :param str new_basename: New basename of file (with the old title
230
            removed).
231
        """
232
        self.get_metadata()
233
        self.metadata['base_name'] = new_basename
234
235
    def set_metadata(self, **kwargs):
236
        """Method to manually update attributes in metadata.
237
238
        :params dict kwargs: Named parameters to update.
239
        """
240
        metadata = self.get_metadata()
241
        for key in kwargs:
242
            if(key in metadata):
243
                self.metadata[key] = kwargs[key]
244
245
    def set_original_name(self):
246
        """Stores the original file name into EXIF/metadata.
247
        :returns: bool
248
        """
249
        return False
250
251
    @classmethod
252
    def get_class_by_file(cls, _file, classes):
253
        """Static method to get a media object by file.
254
        """
255
        if not isinstance(_file, basestring) or not os.path.isfile(_file):
256
            return None
257
258
        extension = os.path.splitext(_file)[1][1:].lower()
259
260
        if len(extension) > 0:
261
            for i in classes:
262
                if(extension in i.extensions):
263
                    return i(_file)
264
265
        return None
266
267
    @classmethod
268
    def get_valid_extensions(cls):
269
        """Static method to access static extensions variable.
270
271
        :returns: tuple(str)
272
        """
273
        return cls.extensions
274
275
276
def get_all_subclasses(cls=None):
277
    """Module method to get all subclasses of Base.
278
    """
279
    subclasses = set()
280
281
    this_class = Base
282
    if cls is not None:
283
        this_class = cls
284
285
    subclasses.add(this_class)
286
287
    this_class_subclasses = this_class.__subclasses__()
288
    for child_class in this_class_subclasses:
289
        subclasses.update(get_all_subclasses(child_class))
290
291
    return subclasses
292