Browse Source

Release: f44d258110

master
Dirk Alders 4 years ago
parent
commit
88ed56a985
6 changed files with 2157 additions and 203 deletions
  1. 13
    203
      __init__.py
  2. 231
    0
      _testresults_/coverage.xml
  3. 1649
    0
      _testresults_/unittest.json
  4. BIN
      _testresults_/unittest.pdf
  5. 264
    0
      metadata.py
  6. BIN
      todo.tgz

+ 13
- 203
__init__.py View File

@@ -15,9 +15,7 @@ media (Media Tools)
15 15
 
16 16
 **Submodules:**
17 17
 
18
-* :mod:`mmod.module.sub1`
19
-* :class:`mmod.module.sub2`
20
-* :func:`mmod.module.sub2`
18
+* :func:`media.get_media_data`
21 19
 
22 20
 **Unittest:**
23 21
 
@@ -27,214 +25,26 @@ __DEPENDENCIES__ = []
27 25
 
28 26
 import logging
29 27
 
30
-
31
-logger_name = 'FSTOOLS'
28
+logger_name = 'MEDIA'
32 29
 logger = logging.getLogger(logger_name)
33 30
 
34 31
 
35 32
 __DESCRIPTION__ = """The Module {\\tt %s} is designed to help on all issues with media files, like tags (e.g. exif, id3) and transformations.
36 33
 For more Information read the documentation.""" % __name__.replace('_', '\_')
37 34
 """The Module Description"""
38
-__INTERPRETER__ = (2, 3)
35
+__INTERPRETER__ = (3, )
39 36
 """The Tested Interpreter-Versions"""
40 37
 
41 38
 
42
-def uid(pathname, max_staleness=3600):
43
-    """
44
-    Function returning a unique id for a given file or path.
45
-
46
-    :param str pathname: File or Path name for generation of the uid.
47
-    :param int max_staleness: If a file or path is older than that, we may consider
48
-                              it stale and return a different uid - this is a
49
-                              dirty trick to work around changes never being
50
-                              detected. Default is 3600 seconds, use None to
51
-                              disable this trickery. See below for more details.
52
-    :returns:  An object that changes value if the file changed,
53
-               None is returned if there were problems accessing the file
54
-    :rtype: str
55
-
56
-    .. note:: Depending on the operating system capabilities and the way the
57
-              file update is done, this function might return the same value
58
-              even if the file has changed. It should be better than just
59
-              using file's mtime though.
60
-              max_staleness tries to avoid the worst for these cases.
61
-
62
-    .. note:: If this function is used for a path, it will stat all pathes and files rekursively.
63
-
64
-    Using just the file's mtime to determine if the file has changed is
65
-    not reliable - if file updates happen faster than the file system's
66
-    mtime granularity, then the modification is not detectable because
67
-    the mtime is still the same.
68
-
69
-    This function tries to improve by using not only the mtime, but also
70
-    other metadata values like file size and inode to improve reliability.
71
-
72
-    For the calculation of this value, we of course only want to use data
73
-    that we can get rather fast, thus we use file metadata, not file data
74
-    (file content).
75
-
76
-    >>> print 'UID:', uid(__file__)
77
-    UID: 16a65cc78e1344e596ef1c9536dab2193a402934
78
-    """
79
-    if os.path.isdir(pathname):
80
-        pathlist = dirlist(pathname) + filelist(pathname)
81
-        pathlist.sort()
82
-    else:
83
-        pathlist = [pathname]
84
-    uid = []
85
-    for element in pathlist:
86
-        try:
87
-            st = os.stat(element)
88
-        except (IOError, OSError):
89
-            uid.append(None)    # for permanent errors on stat() this does not change, but
90
-            #                     having a changing value would be pointless because if we
91
-            #                     can't even stat the file, it is unlikely we can read it.
92
-        else:
93
-            fake_mtime = int(st.st_mtime)
94
-            if not st.st_ino and max_staleness:
95
-                # st_ino being 0 likely means that we run on a platform not
96
-                # supporting it (e.g. win32) - thus we likely need this dirty
97
-                # trick
98
-                now = int(time.time())
99
-                if now >= st.st_mtime + max_staleness:
100
-                    # keep same fake_mtime for each max_staleness interval
101
-                    fake_mtime = int(now / max_staleness) * max_staleness
102
-            uid.append((
103
-                st.st_mtime,    # might have a rather rough granularity, e.g. 2s
104
-                                # on FAT, 1s on ext3 and might not change on fast
105
-                                # updates
106
-                st.st_ino,      # inode number (will change if the update is done
107
-                                # by e.g. renaming a temp file to the real file).
108
-                                # not supported on win32 (0 ever)
109
-                st.st_size,     # likely to change on many updates, but not
110
-                                # sufficient alone
111
-                fake_mtime)     # trick to workaround file system / platform
112
-                                # limitations causing permanent trouble
113
-            )
114
-    if sys.version_info < (3, 0):
115
-        secret = ''
116
-        return hmac.new(secret, repr(uid), hashlib.sha1).hexdigest()
117
-    else:
118
-        secret = b''
119
-        return hmac.new(secret, bytes(repr(uid), 'latin-1'), hashlib.sha1).hexdigest()
120
-
121
-
122
-def uid_filelist(path='.', expression='*', rekursive=True):
123
-    SHAhash = hashlib.md5()
124
-    #
125
-    fl = filelist(path, expression, rekursive)
126
-    fl.sort()
127
-    for f in fl:
128
-        if sys.version_info < (3, 0):
129
-            with open(f, 'rb') as fh:
130
-                SHAhash.update(hashlib.md5(fh.read()).hexdigest())
131
-        else:
132
-            with open(f, mode='rb') as fh:
133
-                d = hashlib.md5()
134
-                for buf in iter(partial(fh.read, 128), b''):
135
-                    d.update(buf)
136
-            SHAhash.update(d.hexdigest().encode())
39
+def get_media_data(full_path):
40
+    from media import metadata
41
+    ft = metadata.get_filetype(full_path)
137 42
     #
138
-    return SHAhash.hexdigest()
139
-
140
-
141
-def filelist(path='.', expression='*', rekursive=True):
142
-    """
143
-    Function returning a list of files below a given path.
144
-
145
-    :param str path: folder which is the basepath for searching files.
146
-    :param str expression: expression to fit including shell-style wildcards.
147
-    :param bool rekursive: search all subfolders if True.
148
-    :returns: list of filenames including the pathe
149
-    :rtype: list
150
-
151
-    .. note:: The returned filenames could be relative pathes depending on argument path.
152
-
153
-    >>> for filename in filelist(path='.', expression='*.py*', rekursive=True):
154
-    ...     print filename
155
-    ./__init__.py
156
-    ./__init__.pyc
157
-    """
158
-    li = list()
159
-    if os.path.exists(path):
160
-        logger.debug('FILELIST: path (%s) exists - looking for files to append', path)
161
-        for filename in glob.glob(os.path.join(path, expression)):
162
-            if os.path.isfile(filename):
163
-                li.append(filename)
164
-        for directory in os.listdir(path):
165
-            directory = os.path.join(path, directory)
166
-            if os.path.isdir(directory) and rekursive and not os.path.islink(directory):
167
-                li.extend(filelist(directory, expression))
168
-    else:
169
-        logger.warning('FILELIST: path (%s) does not exist - empty filelist will be returned', path)
170
-    return li
171
-
172
-
173
-def dirlist(path='.', rekursive=True):
174
-    """
175
-    Function returning a list of directories below a given path.
176
-
177
-    :param str path: folder which is the basepath for searching files.
178
-    :param bool rekursive: search all subfolders if True.
179
-    :returns: list of filenames including the pathe
180
-    :rtype: list
181
-
182
-    .. note:: The returned filenames could be relative pathes depending on argument path.
183
-
184
-    >>> for dirname in dirlist(path='..', rekursive=True):
185
-    ...     print dirname
186
-    ../caching
187
-    ../fstools
188
-    """
189
-    li = list()
190
-    if os.path.exists(path):
191
-        logger.debug('DIRLIST: path (%s) exists - looking for directories to append', path)
192
-        for dirname in os.listdir(path):
193
-            fulldir = os.path.join(path, dirname)
194
-            if os.path.isdir(fulldir):
195
-                li.append(fulldir)
196
-                if rekursive:
197
-                    li.extend(dirlist(fulldir))
198
-    else:
199
-        logger.warning('DIRLIST: path (%s) does not exist - empty filelist will be returned', path)
200
-    return li
201
-
202
-
203
-def is_writeable(path):
204
-    """.. warning:: Needs to be documented
205
-    """
206
-    if os.access(path, os.W_OK):
207
-        # path is writable whatever it is, file or directory
208
-        return True
43
+    if ft == metadata.FILETYPE_AUDIO:
44
+        return metadata.get_audio_data(full_path)
45
+    elif ft == metadata.FILETYPE_IMAGE:
46
+        return metadata.get_image_data(full_path)
47
+    elif ft == metadata.FILETYPE_VIDEO:
48
+        return metadata.get_video_data(full_path)
209 49
     else:
210
-        # path is not writable whatever it is, file or directory
211
-        return False
212
-
213
-
214
-def mkdir(path):
215
-    """.. warning:: Needs to be documented
216
-    """
217
-    path = os.path.abspath(path)
218
-    if not os.path.exists(os.path.dirname(path)):
219
-        mkdir(os.path.dirname(path))
220
-    if not os.path.exists(path):
221
-        os.mkdir(path)
222
-    return os.path.isdir(path)
223
-
224
-
225
-def open_locked_non_blocking(*args, **kwargs):
226
-    """.. warning:: Needs to be documented (acquire exclusive lock file access). Throws an exception, if file is locked!
227
-    """
228
-    import fcntl
229
-    locked_file_descriptor = open(*args, **kwargs)
230
-    fcntl.lockf(locked_file_descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB)
231
-    return locked_file_descriptor
232
-
233
-
234
-def open_locked_blocking(*args, **kwargs):
235
-    """.. warning:: Needs to be documented (acquire exclusive lock file access). Blocks until file is free. deadlock!
236
-    """
237
-    import fcntl
238
-    locked_file_descriptor = open(*args, **kwargs)
239
-    fcntl.lockf(locked_file_descriptor, fcntl.LOCK_EX)
240
-    return locked_file_descriptor
50
+        logger.warning('Filetype not known: %s', full_path)

+ 231
- 0
_testresults_/coverage.xml View File

@@ -0,0 +1,231 @@
1
+<?xml version="1.0" ?>
2
+<coverage branch-rate="0.975" branches-covered="39" branches-valid="40" complexity="0" line-rate="0.9856" lines-covered="206" lines-valid="209" timestamp="1580418394944" version="4.5">
3
+	<!-- Generated by coverage.py: https://coverage.readthedocs.io -->
4
+	<!-- Based on https://raw.githubusercontent.com/cobertura/web/master/htdocs/xml/coverage-04.dtd -->
5
+	<sources/>
6
+	<packages>
7
+		<package branch-rate="0.975" complexity="0" line-rate="0.9856" name=".user_data.data.dirk.prj.unittest.media.pylibs.media">
8
+			<classes>
9
+				<class branch-rate="1" complexity="0" filename="/user_data/data/dirk/prj/unittest/media/pylibs/media/__init__.py" line-rate="1" name="__init__.py">
10
+					<methods/>
11
+					<lines>
12
+						<line hits="1" number="4"/>
13
+						<line hits="1" number="24"/>
14
+						<line hits="1" number="26"/>
15
+						<line hits="1" number="28"/>
16
+						<line hits="1" number="29"/>
17
+						<line hits="1" number="32"/>
18
+						<line hits="1" number="35"/>
19
+						<line hits="1" number="39"/>
20
+						<line hits="1" number="40"/>
21
+						<line hits="1" number="41"/>
22
+						<line branch="true" condition-coverage="100% (2/2)" hits="1" number="43"/>
23
+						<line hits="1" number="44"/>
24
+						<line branch="true" condition-coverage="100% (2/2)" hits="1" number="45"/>
25
+						<line hits="1" number="46"/>
26
+						<line branch="true" condition-coverage="100% (2/2)" hits="1" number="47"/>
27
+						<line hits="1" number="48"/>
28
+						<line hits="1" number="50"/>
29
+					</lines>
30
+				</class>
31
+				<class branch-rate="0.9706" complexity="0" filename="/user_data/data/dirk/prj/unittest/media/pylibs/media/metadata.py" line-rate="0.9844" name="metadata.py">
32
+					<methods/>
33
+					<lines>
34
+						<line hits="1" number="1"/>
35
+						<line hits="1" number="2"/>
36
+						<line hits="1" number="3"/>
37
+						<line hits="1" number="4"/>
38
+						<line hits="1" number="5"/>
39
+						<line hits="1" number="8"/>
40
+						<line hits="1" number="9"/>
41
+						<line hits="1" number="11"/>
42
+						<line hits="1" number="12"/>
43
+						<line hits="1" number="13"/>
44
+						<line hits="1" number="15"/>
45
+						<line hits="1" number="16"/>
46
+						<line hits="1" number="17"/>
47
+						<line hits="1" number="19"/>
48
+						<line hits="1" number="20"/>
49
+						<line hits="1" number="21"/>
50
+						<line hits="1" number="22"/>
51
+						<line hits="1" number="23"/>
52
+						<line hits="1" number="24"/>
53
+						<line hits="1" number="25"/>
54
+						<line hits="1" number="26"/>
55
+						<line hits="1" number="27"/>
56
+						<line hits="1" number="28"/>
57
+						<line hits="1" number="29"/>
58
+						<line hits="1" number="30"/>
59
+						<line hits="1" number="31"/>
60
+						<line hits="1" number="32"/>
61
+						<line hits="1" number="33"/>
62
+						<line hits="1" number="34"/>
63
+						<line hits="1" number="35"/>
64
+						<line hits="1" number="36"/>
65
+						<line hits="1" number="37"/>
66
+						<line hits="1" number="38"/>
67
+						<line hits="1" number="39"/>
68
+						<line hits="1" number="40"/>
69
+						<line hits="1" number="41"/>
70
+						<line hits="1" number="43"/>
71
+						<line hits="1" number="44"/>
72
+						<line hits="1" number="47"/>
73
+						<line hits="1" number="48"/>
74
+						<line branch="true" condition-coverage="100% (2/2)" hits="1" number="49"/>
75
+						<line hits="1" number="50"/>
76
+						<line branch="true" condition-coverage="100% (2/2)" hits="1" number="51"/>
77
+						<line hits="1" number="52"/>
78
+						<line branch="true" condition-coverage="100% (2/2)" hits="1" number="53"/>
79
+						<line hits="1" number="54"/>
80
+						<line hits="1" number="57"/>
81
+						<line hits="1" number="58"/>
82
+						<line hits="1" number="59"/>
83
+						<line hits="1" number="60"/>
84
+						<line hits="1" number="61"/>
85
+						<line hits="1" number="62"/>
86
+						<line hits="1" number="63"/>
87
+						<line hits="1" number="64"/>
88
+						<line hits="1" number="65"/>
89
+						<line hits="1" number="66"/>
90
+						<line hits="1" number="67"/>
91
+						<line hits="1" number="68"/>
92
+						<line hits="1" number="69"/>
93
+						<line hits="1" number="70"/>
94
+						<line hits="1" number="71"/>
95
+						<line hits="1" number="72"/>
96
+						<line hits="1" number="73"/>
97
+						<line hits="1" number="76"/>
98
+						<line hits="1" number="77"/>
99
+						<line hits="1" number="78"/>
100
+						<line hits="1" number="79"/>
101
+						<line hits="1" number="80"/>
102
+						<line hits="1" number="81"/>
103
+						<line hits="1" number="82"/>
104
+						<line hits="1" number="83"/>
105
+						<line hits="1" number="84"/>
106
+						<line hits="1" number="85"/>
107
+						<line hits="1" number="88"/>
108
+						<line hits="1" number="89"/>
109
+						<line hits="1" number="92"/>
110
+						<line hits="1" number="93"/>
111
+						<line branch="true" condition-coverage="100% (2/2)" hits="1" number="95"/>
112
+						<line hits="1" number="96"/>
113
+						<line hits="1" number="97"/>
114
+						<line hits="1" number="98"/>
115
+						<line branch="true" condition-coverage="100% (2/2)" hits="1" number="100"/>
116
+						<line branch="true" condition-coverage="100% (2/2)" hits="1" number="101"/>
117
+						<line hits="1" number="103"/>
118
+						<line hits="1" number="104"/>
119
+						<line hits="1" number="106"/>
120
+						<line hits="1" number="107"/>
121
+						<line hits="1" number="109"/>
122
+						<line hits="1" number="110"/>
123
+						<line hits="1" number="111"/>
124
+						<line hits="1" number="114"/>
125
+						<line hits="1" number="115"/>
126
+						<line hits="1" number="116"/>
127
+						<line hits="1" number="118"/>
128
+						<line hits="1" number="119"/>
129
+						<line hits="1" number="121"/>
130
+						<line hits="1" number="122"/>
131
+						<line hits="1" number="123"/>
132
+						<line hits="1" number="124"/>
133
+						<line hits="1" number="125"/>
134
+						<line hits="0" number="126"/>
135
+						<line hits="0" number="127"/>
136
+						<line hits="0" number="128"/>
137
+						<line hits="1" number="130"/>
138
+						<line branch="true" condition-coverage="100% (2/2)" hits="1" number="131"/>
139
+						<line hits="1" number="132"/>
140
+						<line branch="true" condition-coverage="100% (2/2)" hits="1" number="133"/>
141
+						<line hits="1" number="134"/>
142
+						<line hits="1" number="135"/>
143
+						<line branch="true" condition-coverage="100% (2/2)" hits="1" number="137"/>
144
+						<line hits="1" number="138"/>
145
+						<line hits="1" number="139"/>
146
+						<line hits="1" number="140"/>
147
+						<line hits="1" number="141"/>
148
+						<line hits="1" number="142"/>
149
+						<line hits="1" number="143"/>
150
+						<line hits="1" number="146"/>
151
+						<line hits="1" number="147"/>
152
+						<line hits="1" number="148"/>
153
+						<line hits="1" number="149"/>
154
+						<line hits="1" number="150"/>
155
+						<line hits="1" number="151"/>
156
+						<line hits="1" number="152"/>
157
+						<line hits="1" number="154"/>
158
+						<line hits="1" number="156"/>
159
+						<line hits="1" number="157"/>
160
+						<line hits="1" number="158"/>
161
+						<line hits="1" number="159"/>
162
+						<line hits="1" number="160"/>
163
+						<line hits="1" number="161"/>
164
+						<line hits="1" number="162"/>
165
+						<line hits="1" number="163"/>
166
+						<line hits="1" number="164"/>
167
+						<line hits="1" number="165"/>
168
+						<line hits="1" number="166"/>
169
+						<line hits="1" number="167"/>
170
+						<line hits="1" number="168"/>
171
+						<line branch="true" condition-coverage="100% (2/2)" hits="1" number="169"/>
172
+						<line branch="true" condition-coverage="100% (2/2)" hits="1" number="170"/>
173
+						<line hits="1" number="171"/>
174
+						<line hits="1" number="172"/>
175
+						<line branch="true" condition-coverage="100% (2/2)" hits="1" number="173"/>
176
+						<line hits="1" number="174"/>
177
+						<line hits="1" number="175"/>
178
+						<line hits="1" number="179"/>
179
+						<line hits="1" number="180"/>
180
+						<line hits="1" number="181"/>
181
+						<line hits="1" number="184"/>
182
+						<line hits="1" number="185"/>
183
+						<line hits="1" number="186"/>
184
+						<line hits="1" number="187"/>
185
+						<line hits="1" number="188"/>
186
+						<line hits="1" number="189"/>
187
+						<line branch="true" condition-coverage="100% (2/2)" hits="1" number="190"/>
188
+						<line hits="1" number="191"/>
189
+						<line hits="1" number="192"/>
190
+						<line hits="1" number="193"/>
191
+						<line hits="1" number="196"/>
192
+						<line hits="1" number="197"/>
193
+						<line hits="1" number="210"/>
194
+						<line hits="1" number="211"/>
195
+						<line hits="1" number="228"/>
196
+						<line hits="1" number="229"/>
197
+						<line hits="1" number="230"/>
198
+						<line hits="1" number="231"/>
199
+						<line branch="true" condition-coverage="100% (2/2)" hits="1" number="232"/>
200
+						<line hits="1" number="233"/>
201
+						<line branch="true" condition-coverage="100% (2/2)" hits="1" number="234"/>
202
+						<line hits="1" number="235"/>
203
+						<line hits="1" number="236"/>
204
+						<line hits="1" number="239"/>
205
+						<line hits="1" number="240"/>
206
+						<line hits="1" number="241"/>
207
+						<line hits="1" number="244"/>
208
+						<line hits="1" number="245"/>
209
+						<line hits="1" number="246"/>
210
+						<line hits="1" number="247"/>
211
+						<line branch="true" condition-coverage="100% (2/2)" hits="1" number="248"/>
212
+						<line hits="1" number="249"/>
213
+						<line hits="1" number="250"/>
214
+						<line hits="1" number="251"/>
215
+						<line hits="1" number="252"/>
216
+						<line hits="1" number="253"/>
217
+						<line hits="1" number="254"/>
218
+						<line branch="true" condition-coverage="50% (1/2)" hits="1" missing-branches="exit" number="255"/>
219
+						<line hits="1" number="256"/>
220
+						<line hits="1" number="257"/>
221
+						<line hits="1" number="258"/>
222
+						<line hits="1" number="261"/>
223
+						<line hits="1" number="262"/>
224
+						<line hits="1" number="263"/>
225
+						<line hits="1" number="264"/>
226
+					</lines>
227
+				</class>
228
+			</classes>
229
+		</package>
230
+	</packages>
231
+</coverage>

+ 1649
- 0
_testresults_/unittest.json
File diff suppressed because it is too large
View File


BIN
_testresults_/unittest.pdf View File


+ 264
- 0
metadata.py View File

@@ -0,0 +1,264 @@
1
+import logging
2
+import os
3
+from PIL import Image
4
+import subprocess
5
+import time
6
+
7
+
8
+logger_name = 'MEDIA'
9
+logger = logging.getLogger(logger_name)
10
+
11
+FILETYPE_AUDIO = 'audio'
12
+FILETYPE_IMAGE = 'image'
13
+FILETYPE_VIDEO = 'video'
14
+
15
+EXTENTIONS_AUDIO = ['.mp3', ]
16
+EXTENTIONS_IMAGE = ['.jpg', '.jpeg', '.jpe', '.png', '.tif', '.tiff', '.gif', ]
17
+EXTENTIONS_VIDEO = ['.avi', '.mpg', '.mpeg', '.mpe', '.mov', '.qt', '.mp4', '.webm', '.ogv', '.flv', '.3gp', ]
18
+
19
+KEY_ALBUM = 'album'
20
+KEY_APERTURE = 'aperture'
21
+KEY_ARTIST = 'artist'
22
+KEY_BITRATE = 'bitrate'
23
+KEY_CAMERA = 'camera'
24
+KEY_DURATION = 'duration'
25
+KEY_EXPOSURE_PROGRAM = 'exposure_program'
26
+KEY_EXPOSURE_TIME = 'exposure_time'
27
+KEY_FLASH = 'flash'
28
+KEY_FOCAL_LENGTH = 'focal_length'
29
+KEY_GENRE = 'genre'
30
+KEY_GPS = 'gps'
31
+KEY_HEIGHT = 'height'
32
+KEY_ISO = 'iso'
33
+KEY_ORIENTATION = 'orientation'
34
+KEY_RATIO = 'ratio'
35
+KEY_SIZE = 'size'
36
+KEY_TIME = 'time'   # USE time.localtime(value) or datetime.fromtimestamp(value) to convert the timestamp
37
+KEY_TIME_IS_SUBSTITUTION = 'tm_is_subst'
38
+KEY_TITLE = 'title'
39
+KEY_TRACK = 'track'
40
+KEY_WIDTH = 'width'
41
+KEY_YEAR = 'year'
42
+
43
+__KEY_CAMERA_VENDOR__ = 'camera_vendor'
44
+__KEY_CAMERA_MODEL__ = 'camera_model'
45
+
46
+
47
+def get_filetype(full_path):
48
+    ext = os.path.splitext(full_path)[1]
49
+    if ext in EXTENTIONS_AUDIO:
50
+        return FILETYPE_AUDIO
51
+    elif ext in EXTENTIONS_IMAGE:
52
+        return FILETYPE_IMAGE
53
+    elif ext in EXTENTIONS_VIDEO:
54
+        return FILETYPE_VIDEO
55
+
56
+
57
+def get_audio_data(full_path):
58
+    conv_key_dict = {}
59
+    conv_key_dict['album'] = (str, KEY_ALBUM)
60
+    conv_key_dict['TAG:album'] = (str, KEY_ALBUM)
61
+    conv_key_dict['TAG:artist'] = (str, KEY_ARTIST)
62
+    conv_key_dict['artist'] = (str, KEY_ARTIST)
63
+    conv_key_dict['bit_rate'] = (__int_conv__, KEY_BITRATE)
64
+    conv_key_dict['duration'] = (float, KEY_DURATION)
65
+    conv_key_dict['TAG:genre'] = (str, KEY_GENRE)
66
+    conv_key_dict['genre'] = (str, KEY_GENRE)
67
+    conv_key_dict['TAG:title'] = (str, KEY_TITLE)
68
+    conv_key_dict['title'] = (str, KEY_TITLE)
69
+    conv_key_dict['TAG:track'] = (__int_conv__, KEY_TRACK)
70
+    conv_key_dict['track'] = (__int_conv__, KEY_TRACK)
71
+    conv_key_dict['TAG:date'] = (__int_conv__, KEY_YEAR)
72
+    conv_key_dict['date'] = (__int_conv__, KEY_YEAR)
73
+    return __adapt__data__(__get_xxprobe_data__(full_path, conv_key_dict), full_path)
74
+
75
+
76
+def get_video_data(full_path):
77
+    conv_key_dict = {}
78
+    conv_key_dict['creation_time'] = (__vid_datetime_conv__, KEY_TIME)
79
+    conv_key_dict['TAG:creation_time'] = (__vid_datetime_conv__, KEY_TIME)
80
+    conv_key_dict['bit_rate'] = (__int_conv__, KEY_BITRATE)
81
+    conv_key_dict['duration'] = (float, KEY_DURATION)
82
+    conv_key_dict['height'] = (__int_conv__, KEY_HEIGHT)
83
+    conv_key_dict['width'] = (__int_conv__, KEY_WIDTH)
84
+    conv_key_dict['display_aspect_ratio'] = (__ratio_conv__, KEY_RATIO)
85
+    return __adapt__data__(__get_xxprobe_data__(full_path, conv_key_dict), full_path)
86
+
87
+
88
+def get_image_data(full_path):
89
+    return __adapt__data__(__get_exif_data__(full_path), full_path)
90
+
91
+
92
+def __adapt__data__(data, full_path):
93
+    data[KEY_SIZE] = os.path.getsize(full_path)
94
+    # Join Camera Vendor and Camera Model
95
+    if __KEY_CAMERA_MODEL__ in data and __KEY_CAMERA_VENDOR__ in data:
96
+        model = data.pop(__KEY_CAMERA_MODEL__)
97
+        vendor = data.pop(__KEY_CAMERA_VENDOR__)
98
+        data[KEY_CAMERA] = '%s: %s' % (vendor, model)
99
+    # Add time if not exists
100
+    if KEY_TIME not in data:
101
+        if KEY_YEAR in data and KEY_TRACK in data:
102
+            # Use a date where track 1 is the newest in the given year
103
+            minute = int(data[KEY_TRACK] / 60)
104
+            second = (data[KEY_TRACK] - 60 * minute) % 60
105
+            #
106
+            data[KEY_TIME] = int(time.mktime((data[KEY_YEAR], 1, 1, 0, 59 - minute, 59 - second, 0, 0, 0)))
107
+            data[KEY_TIME_IS_SUBSTITUTION] = True
108
+        else:
109
+            data[KEY_TIME] = int(os.path.getmtime(full_path))
110
+            data[KEY_TIME_IS_SUBSTITUTION] = True
111
+    return data
112
+
113
+
114
+def __get_xxprobe_data__(full_path, conv_key_dict):
115
+    def _ffprobe_command(full_path):
116
+        return ['ffprobe', '-v', 'quiet', '-show_format', '-show_streams', full_path]
117
+
118
+    def _avprobe_command(full_path):
119
+        return ['avprobe', '-v', 'quiet', '-show_format', '-show_streams', full_path]
120
+
121
+    try:
122
+        xxprobe_text = subprocess.check_output(_avprobe_command(full_path))
123
+    except FileNotFoundError:
124
+        try:
125
+            xxprobe_text = subprocess.check_output(_ffprobe_command(full_path))
126
+        except FileNotFoundError:
127
+            logger.warning('ffprobe and avprobe seem to be not installed')
128
+            return {}
129
+    #
130
+    rv = {}
131
+    for line in xxprobe_text.decode('utf-8').splitlines():
132
+        try:
133
+            key, val = [snippet.strip() for snippet in line.split('=')]
134
+        except ValueError:
135
+            continue
136
+        else:
137
+            if key in conv_key_dict:
138
+                tp, name = conv_key_dict[key]
139
+                try:
140
+                    rv[name] = tp(val)
141
+                except ValueError:
142
+                    logger.log(logging.WARNING if val else logger.INFO, 'Can\'t convert %s (%s) for %s', repr(val), name, name)
143
+    return rv
144
+
145
+
146
+def __get_exif_data__(full_path):
147
+    rv = {}
148
+    im = Image.open(full_path)
149
+    try:
150
+        exif = dict(im._getexif().items())
151
+    except AttributeError:
152
+        logger.debug('%s does not have any exif information', full_path)
153
+    else:
154
+        conv_key_dict = {}
155
+        # IMAGE
156
+        conv_key_dict[0x9003] = (__datetime_conv__, KEY_TIME)
157
+        conv_key_dict[0x8822] = (__exposure_program_conv__, KEY_EXPOSURE_PROGRAM)
158
+        conv_key_dict[0x829A] = (__num_denum_conv__, KEY_EXPOSURE_TIME)
159
+        conv_key_dict[0x9209] = (__flash_conv__, KEY_FLASH)
160
+        conv_key_dict[0x829D] = (__num_denum_conv__, KEY_APERTURE)
161
+        conv_key_dict[0x920A] = (__num_denum_conv__, KEY_FOCAL_LENGTH)
162
+        conv_key_dict[0x8825] = (__gps_conv__, KEY_GPS)
163
+        conv_key_dict[0xA003] = (__int_conv__, KEY_HEIGHT)
164
+        conv_key_dict[0x8827] = (__int_conv__, KEY_ISO)
165
+        conv_key_dict[0x010F] = (str, __KEY_CAMERA_VENDOR__)
166
+        conv_key_dict[0x0110] = (str, __KEY_CAMERA_MODEL__)
167
+        conv_key_dict[0x0112] = (__int_conv__, KEY_ORIENTATION)
168
+        conv_key_dict[0xA002] = (__int_conv__, KEY_WIDTH)
169
+        for key in conv_key_dict:
170
+            if key in exif:
171
+                tp, name = conv_key_dict[key]
172
+                value = tp(exif[key])
173
+                if value is not None:
174
+                    rv[name] = value
175
+    return rv
176
+
177
+
178
+# TODO: Join datetime converter __datetime_conv__ and __vid_datetime_conv_
179
+def __datetime_conv__(dt):
180
+    format_string = "%Y:%m:%d %H:%M:%S"
181
+    return int(time.mktime(time.strptime(dt, format_string)))
182
+
183
+
184
+def __vid_datetime_conv__(dt):
185
+    try:
186
+        dt = dt[:dt.index('.')]
187
+    except ValueError:
188
+        pass  # time string seems to have no '.'
189
+    dt = dt.replace('T', ' ').replace('/', '').replace('\\', '')
190
+    if len(dt) == 16:
191
+        dt += ':00'
192
+    format_string = '%Y-%m-%d %H:%M:%S'
193
+    return int(time.mktime(time.strptime(dt, format_string)))
194
+
195
+
196
+def __exposure_program_conv__(n):
197
+    return {
198
+        0: 'Unidentified',
199
+        1: 'Manual',
200
+        2: 'Program Normal',
201
+        3: 'Aperture Priority',
202
+        4: 'Shutter Priority',
203
+        5: 'Program Creative',
204
+        6: 'Program Action',
205
+        7: 'Portrait Mode',
206
+        8: 'Landscape Mode'
207
+    }.get(n, None)
208
+
209
+
210
+def __flash_conv__(n):
211
+    return {
212
+        0: 'No',
213
+        1: 'Fired',
214
+        5: 'Fired (?)',  # no return sensed
215
+        7: 'Fired (!)',  # return sensed
216
+        9: 'Fill Fired',
217
+        13: 'Fill Fired (?)',
218
+        15: 'Fill Fired (!)',
219
+        16: 'Off',
220
+        24: 'Auto Off',
221
+        25: 'Auto Fired',
222
+        29: 'Auto Fired (?)',
223
+        31: 'Auto Fired (!)',
224
+        32: 'Not Available'
225
+    }.get(n, None)
226
+
227
+
228
+def __int_conv__(value):
229
+    try:
230
+        return int(value)
231
+    except ValueError:
232
+        for c in ['.', '/', '-']:
233
+            p = value.find(c)
234
+            if p >= 0:
235
+                value = value[:p]
236
+    return int(value)
237
+
238
+
239
+def __num_denum_conv__(data):
240
+    num, denum = data
241
+    return num / denum
242
+
243
+
244
+def __gps_conv__(data):
245
+    def lat_lon_cal(lon_or_lat):
246
+        lon_lat = 0.
247
+        fac = 1.
248
+        for num, denum in lon_or_lat:
249
+            lon_lat += float(num) / float(denum) * fac
250
+            fac *= 1. / 60.
251
+        return lon_lat
252
+    try:
253
+        lon = lat_lon_cal(data[0x0004])
254
+        lat = lat_lon_cal(data[0x0002])
255
+        if lon != 0 or lat != 0:    # do not use lon and lat equal 0, caused by motorola gps weakness
256
+            return {'lon': lon, 'lat': lat}
257
+    except KeyError:
258
+        logger.warning('GPS data extraction failed for %s', repr(data))
259
+
260
+
261
+def __ratio_conv__(ratio):
262
+    ratio = ratio.replace('\\', '')
263
+    num, denum = ratio.split(':')
264
+    return float(num) / float(denum)

BIN
todo.tgz View File


Loading…
Cancel
Save