Release: f44d258110e088423a3132bd52534f53

This commit is contained in:
Dirk Alders 2020-01-30 22:07:50 +01:00
parent 06a67dc238
commit 88ed56a985
6 changed files with 2157 additions and 203 deletions

View File

@ -15,9 +15,7 @@ media (Media Tools)
**Submodules:**
* :mod:`mmod.module.sub1`
* :class:`mmod.module.sub2`
* :func:`mmod.module.sub2`
* :func:`media.get_media_data`
**Unittest:**
@ -27,214 +25,26 @@ __DEPENDENCIES__ = []
import logging
logger_name = 'FSTOOLS'
logger_name = 'MEDIA'
logger = logging.getLogger(logger_name)
__DESCRIPTION__ = """The Module {\\tt %s} is designed to help on all issues with media files, like tags (e.g. exif, id3) and transformations.
For more Information read the documentation.""" % __name__.replace('_', '\_')
"""The Module Description"""
__INTERPRETER__ = (2, 3)
__INTERPRETER__ = (3, )
"""The Tested Interpreter-Versions"""
def uid(pathname, max_staleness=3600):
"""
Function returning a unique id for a given file or path.
:param str pathname: File or Path name for generation of the uid.
:param int max_staleness: If a file or path is older than that, we may consider
it stale and return a different uid - this is a
dirty trick to work around changes never being
detected. Default is 3600 seconds, use None to
disable this trickery. See below for more details.
:returns: An object that changes value if the file changed,
None is returned if there were problems accessing the file
:rtype: str
.. note:: Depending on the operating system capabilities and the way the
file update is done, this function might return the same value
even if the file has changed. It should be better than just
using file's mtime though.
max_staleness tries to avoid the worst for these cases.
.. note:: If this function is used for a path, it will stat all pathes and files rekursively.
Using just the file's mtime to determine if the file has changed is
not reliable - if file updates happen faster than the file system's
mtime granularity, then the modification is not detectable because
the mtime is still the same.
This function tries to improve by using not only the mtime, but also
other metadata values like file size and inode to improve reliability.
For the calculation of this value, we of course only want to use data
that we can get rather fast, thus we use file metadata, not file data
(file content).
>>> print 'UID:', uid(__file__)
UID: 16a65cc78e1344e596ef1c9536dab2193a402934
"""
if os.path.isdir(pathname):
pathlist = dirlist(pathname) + filelist(pathname)
pathlist.sort()
else:
pathlist = [pathname]
uid = []
for element in pathlist:
try:
st = os.stat(element)
except (IOError, OSError):
uid.append(None) # for permanent errors on stat() this does not change, but
# having a changing value would be pointless because if we
# can't even stat the file, it is unlikely we can read it.
else:
fake_mtime = int(st.st_mtime)
if not st.st_ino and max_staleness:
# st_ino being 0 likely means that we run on a platform not
# supporting it (e.g. win32) - thus we likely need this dirty
# trick
now = int(time.time())
if now >= st.st_mtime + max_staleness:
# keep same fake_mtime for each max_staleness interval
fake_mtime = int(now / max_staleness) * max_staleness
uid.append((
st.st_mtime, # might have a rather rough granularity, e.g. 2s
# on FAT, 1s on ext3 and might not change on fast
# updates
st.st_ino, # inode number (will change if the update is done
# by e.g. renaming a temp file to the real file).
# not supported on win32 (0 ever)
st.st_size, # likely to change on many updates, but not
# sufficient alone
fake_mtime) # trick to workaround file system / platform
# limitations causing permanent trouble
)
if sys.version_info < (3, 0):
secret = ''
return hmac.new(secret, repr(uid), hashlib.sha1).hexdigest()
else:
secret = b''
return hmac.new(secret, bytes(repr(uid), 'latin-1'), hashlib.sha1).hexdigest()
def uid_filelist(path='.', expression='*', rekursive=True):
SHAhash = hashlib.md5()
def get_media_data(full_path):
from media import metadata
ft = metadata.get_filetype(full_path)
#
fl = filelist(path, expression, rekursive)
fl.sort()
for f in fl:
if sys.version_info < (3, 0):
with open(f, 'rb') as fh:
SHAhash.update(hashlib.md5(fh.read()).hexdigest())
else:
with open(f, mode='rb') as fh:
d = hashlib.md5()
for buf in iter(partial(fh.read, 128), b''):
d.update(buf)
SHAhash.update(d.hexdigest().encode())
#
return SHAhash.hexdigest()
def filelist(path='.', expression='*', rekursive=True):
"""
Function returning a list of files below a given path.
:param str path: folder which is the basepath for searching files.
:param str expression: expression to fit including shell-style wildcards.
:param bool rekursive: search all subfolders if True.
:returns: list of filenames including the pathe
:rtype: list
.. note:: The returned filenames could be relative pathes depending on argument path.
>>> for filename in filelist(path='.', expression='*.py*', rekursive=True):
... print filename
./__init__.py
./__init__.pyc
"""
li = list()
if os.path.exists(path):
logger.debug('FILELIST: path (%s) exists - looking for files to append', path)
for filename in glob.glob(os.path.join(path, expression)):
if os.path.isfile(filename):
li.append(filename)
for directory in os.listdir(path):
directory = os.path.join(path, directory)
if os.path.isdir(directory) and rekursive and not os.path.islink(directory):
li.extend(filelist(directory, expression))
if ft == metadata.FILETYPE_AUDIO:
return metadata.get_audio_data(full_path)
elif ft == metadata.FILETYPE_IMAGE:
return metadata.get_image_data(full_path)
elif ft == metadata.FILETYPE_VIDEO:
return metadata.get_video_data(full_path)
else:
logger.warning('FILELIST: path (%s) does not exist - empty filelist will be returned', path)
return li
def dirlist(path='.', rekursive=True):
"""
Function returning a list of directories below a given path.
:param str path: folder which is the basepath for searching files.
:param bool rekursive: search all subfolders if True.
:returns: list of filenames including the pathe
:rtype: list
.. note:: The returned filenames could be relative pathes depending on argument path.
>>> for dirname in dirlist(path='..', rekursive=True):
... print dirname
../caching
../fstools
"""
li = list()
if os.path.exists(path):
logger.debug('DIRLIST: path (%s) exists - looking for directories to append', path)
for dirname in os.listdir(path):
fulldir = os.path.join(path, dirname)
if os.path.isdir(fulldir):
li.append(fulldir)
if rekursive:
li.extend(dirlist(fulldir))
else:
logger.warning('DIRLIST: path (%s) does not exist - empty filelist will be returned', path)
return li
def is_writeable(path):
""".. warning:: Needs to be documented
"""
if os.access(path, os.W_OK):
# path is writable whatever it is, file or directory
return True
else:
# path is not writable whatever it is, file or directory
return False
def mkdir(path):
""".. warning:: Needs to be documented
"""
path = os.path.abspath(path)
if not os.path.exists(os.path.dirname(path)):
mkdir(os.path.dirname(path))
if not os.path.exists(path):
os.mkdir(path)
return os.path.isdir(path)
def open_locked_non_blocking(*args, **kwargs):
""".. warning:: Needs to be documented (acquire exclusive lock file access). Throws an exception, if file is locked!
"""
import fcntl
locked_file_descriptor = open(*args, **kwargs)
fcntl.lockf(locked_file_descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB)
return locked_file_descriptor
def open_locked_blocking(*args, **kwargs):
""".. warning:: Needs to be documented (acquire exclusive lock file access). Blocks until file is free. deadlock!
"""
import fcntl
locked_file_descriptor = open(*args, **kwargs)
fcntl.lockf(locked_file_descriptor, fcntl.LOCK_EX)
return locked_file_descriptor
logger.warning('Filetype not known: %s', full_path)

231
_testresults_/coverage.xml Normal file
View File

@ -0,0 +1,231 @@
<?xml version="1.0" ?>
<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">
<!-- Generated by coverage.py: https://coverage.readthedocs.io -->
<!-- Based on https://raw.githubusercontent.com/cobertura/web/master/htdocs/xml/coverage-04.dtd -->
<sources/>
<packages>
<package branch-rate="0.975" complexity="0" line-rate="0.9856" name=".user_data.data.dirk.prj.unittest.media.pylibs.media">
<classes>
<class branch-rate="1" complexity="0" filename="/user_data/data/dirk/prj/unittest/media/pylibs/media/__init__.py" line-rate="1" name="__init__.py">
<methods/>
<lines>
<line hits="1" number="4"/>
<line hits="1" number="24"/>
<line hits="1" number="26"/>
<line hits="1" number="28"/>
<line hits="1" number="29"/>
<line hits="1" number="32"/>
<line hits="1" number="35"/>
<line hits="1" number="39"/>
<line hits="1" number="40"/>
<line hits="1" number="41"/>
<line branch="true" condition-coverage="100% (2/2)" hits="1" number="43"/>
<line hits="1" number="44"/>
<line branch="true" condition-coverage="100% (2/2)" hits="1" number="45"/>
<line hits="1" number="46"/>
<line branch="true" condition-coverage="100% (2/2)" hits="1" number="47"/>
<line hits="1" number="48"/>
<line hits="1" number="50"/>
</lines>
</class>
<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">
<methods/>
<lines>
<line hits="1" number="1"/>
<line hits="1" number="2"/>
<line hits="1" number="3"/>
<line hits="1" number="4"/>
<line hits="1" number="5"/>
<line hits="1" number="8"/>
<line hits="1" number="9"/>
<line hits="1" number="11"/>
<line hits="1" number="12"/>
<line hits="1" number="13"/>
<line hits="1" number="15"/>
<line hits="1" number="16"/>
<line hits="1" number="17"/>
<line hits="1" number="19"/>
<line hits="1" number="20"/>
<line hits="1" number="21"/>
<line hits="1" number="22"/>
<line hits="1" number="23"/>
<line hits="1" number="24"/>
<line hits="1" number="25"/>
<line hits="1" number="26"/>
<line hits="1" number="27"/>
<line hits="1" number="28"/>
<line hits="1" number="29"/>
<line hits="1" number="30"/>
<line hits="1" number="31"/>
<line hits="1" number="32"/>
<line hits="1" number="33"/>
<line hits="1" number="34"/>
<line hits="1" number="35"/>
<line hits="1" number="36"/>
<line hits="1" number="37"/>
<line hits="1" number="38"/>
<line hits="1" number="39"/>
<line hits="1" number="40"/>
<line hits="1" number="41"/>
<line hits="1" number="43"/>
<line hits="1" number="44"/>
<line hits="1" number="47"/>
<line hits="1" number="48"/>
<line branch="true" condition-coverage="100% (2/2)" hits="1" number="49"/>
<line hits="1" number="50"/>
<line branch="true" condition-coverage="100% (2/2)" hits="1" number="51"/>
<line hits="1" number="52"/>
<line branch="true" condition-coverage="100% (2/2)" hits="1" number="53"/>
<line hits="1" number="54"/>
<line hits="1" number="57"/>
<line hits="1" number="58"/>
<line hits="1" number="59"/>
<line hits="1" number="60"/>
<line hits="1" number="61"/>
<line hits="1" number="62"/>
<line hits="1" number="63"/>
<line hits="1" number="64"/>
<line hits="1" number="65"/>
<line hits="1" number="66"/>
<line hits="1" number="67"/>
<line hits="1" number="68"/>
<line hits="1" number="69"/>
<line hits="1" number="70"/>
<line hits="1" number="71"/>
<line hits="1" number="72"/>
<line hits="1" number="73"/>
<line hits="1" number="76"/>
<line hits="1" number="77"/>
<line hits="1" number="78"/>
<line hits="1" number="79"/>
<line hits="1" number="80"/>
<line hits="1" number="81"/>
<line hits="1" number="82"/>
<line hits="1" number="83"/>
<line hits="1" number="84"/>
<line hits="1" number="85"/>
<line hits="1" number="88"/>
<line hits="1" number="89"/>
<line hits="1" number="92"/>
<line hits="1" number="93"/>
<line branch="true" condition-coverage="100% (2/2)" hits="1" number="95"/>
<line hits="1" number="96"/>
<line hits="1" number="97"/>
<line hits="1" number="98"/>
<line branch="true" condition-coverage="100% (2/2)" hits="1" number="100"/>
<line branch="true" condition-coverage="100% (2/2)" hits="1" number="101"/>
<line hits="1" number="103"/>
<line hits="1" number="104"/>
<line hits="1" number="106"/>
<line hits="1" number="107"/>
<line hits="1" number="109"/>
<line hits="1" number="110"/>
<line hits="1" number="111"/>
<line hits="1" number="114"/>
<line hits="1" number="115"/>
<line hits="1" number="116"/>
<line hits="1" number="118"/>
<line hits="1" number="119"/>
<line hits="1" number="121"/>
<line hits="1" number="122"/>
<line hits="1" number="123"/>
<line hits="1" number="124"/>
<line hits="1" number="125"/>
<line hits="0" number="126"/>
<line hits="0" number="127"/>
<line hits="0" number="128"/>
<line hits="1" number="130"/>
<line branch="true" condition-coverage="100% (2/2)" hits="1" number="131"/>
<line hits="1" number="132"/>
<line branch="true" condition-coverage="100% (2/2)" hits="1" number="133"/>
<line hits="1" number="134"/>
<line hits="1" number="135"/>
<line branch="true" condition-coverage="100% (2/2)" hits="1" number="137"/>
<line hits="1" number="138"/>
<line hits="1" number="139"/>
<line hits="1" number="140"/>
<line hits="1" number="141"/>
<line hits="1" number="142"/>
<line hits="1" number="143"/>
<line hits="1" number="146"/>
<line hits="1" number="147"/>
<line hits="1" number="148"/>
<line hits="1" number="149"/>
<line hits="1" number="150"/>
<line hits="1" number="151"/>
<line hits="1" number="152"/>
<line hits="1" number="154"/>
<line hits="1" number="156"/>
<line hits="1" number="157"/>
<line hits="1" number="158"/>
<line hits="1" number="159"/>
<line hits="1" number="160"/>
<line hits="1" number="161"/>
<line hits="1" number="162"/>
<line hits="1" number="163"/>
<line hits="1" number="164"/>
<line hits="1" number="165"/>
<line hits="1" number="166"/>
<line hits="1" number="167"/>
<line hits="1" number="168"/>
<line branch="true" condition-coverage="100% (2/2)" hits="1" number="169"/>
<line branch="true" condition-coverage="100% (2/2)" hits="1" number="170"/>
<line hits="1" number="171"/>
<line hits="1" number="172"/>
<line branch="true" condition-coverage="100% (2/2)" hits="1" number="173"/>
<line hits="1" number="174"/>
<line hits="1" number="175"/>
<line hits="1" number="179"/>
<line hits="1" number="180"/>
<line hits="1" number="181"/>
<line hits="1" number="184"/>
<line hits="1" number="185"/>
<line hits="1" number="186"/>
<line hits="1" number="187"/>
<line hits="1" number="188"/>
<line hits="1" number="189"/>
<line branch="true" condition-coverage="100% (2/2)" hits="1" number="190"/>
<line hits="1" number="191"/>
<line hits="1" number="192"/>
<line hits="1" number="193"/>
<line hits="1" number="196"/>
<line hits="1" number="197"/>
<line hits="1" number="210"/>
<line hits="1" number="211"/>
<line hits="1" number="228"/>
<line hits="1" number="229"/>
<line hits="1" number="230"/>
<line hits="1" number="231"/>
<line branch="true" condition-coverage="100% (2/2)" hits="1" number="232"/>
<line hits="1" number="233"/>
<line branch="true" condition-coverage="100% (2/2)" hits="1" number="234"/>
<line hits="1" number="235"/>
<line hits="1" number="236"/>
<line hits="1" number="239"/>
<line hits="1" number="240"/>
<line hits="1" number="241"/>
<line hits="1" number="244"/>
<line hits="1" number="245"/>
<line hits="1" number="246"/>
<line hits="1" number="247"/>
<line branch="true" condition-coverage="100% (2/2)" hits="1" number="248"/>
<line hits="1" number="249"/>
<line hits="1" number="250"/>
<line hits="1" number="251"/>
<line hits="1" number="252"/>
<line hits="1" number="253"/>
<line hits="1" number="254"/>
<line branch="true" condition-coverage="50% (1/2)" hits="1" missing-branches="exit" number="255"/>
<line hits="1" number="256"/>
<line hits="1" number="257"/>
<line hits="1" number="258"/>
<line hits="1" number="261"/>
<line hits="1" number="262"/>
<line hits="1" number="263"/>
<line hits="1" number="264"/>
</lines>
</class>
</classes>
</package>
</packages>
</coverage>

1649
_testresults_/unittest.json Normal file

File diff suppressed because it is too large Load Diff

BIN
_testresults_/unittest.pdf Normal file

Binary file not shown.

264
metadata.py Normal file
View File

@ -0,0 +1,264 @@
import logging
import os
from PIL import Image
import subprocess
import time
logger_name = 'MEDIA'
logger = logging.getLogger(logger_name)
FILETYPE_AUDIO = 'audio'
FILETYPE_IMAGE = 'image'
FILETYPE_VIDEO = 'video'
EXTENTIONS_AUDIO = ['.mp3', ]
EXTENTIONS_IMAGE = ['.jpg', '.jpeg', '.jpe', '.png', '.tif', '.tiff', '.gif', ]
EXTENTIONS_VIDEO = ['.avi', '.mpg', '.mpeg', '.mpe', '.mov', '.qt', '.mp4', '.webm', '.ogv', '.flv', '.3gp', ]
KEY_ALBUM = 'album'
KEY_APERTURE = 'aperture'
KEY_ARTIST = 'artist'
KEY_BITRATE = 'bitrate'
KEY_CAMERA = 'camera'
KEY_DURATION = 'duration'
KEY_EXPOSURE_PROGRAM = 'exposure_program'
KEY_EXPOSURE_TIME = 'exposure_time'
KEY_FLASH = 'flash'
KEY_FOCAL_LENGTH = 'focal_length'
KEY_GENRE = 'genre'
KEY_GPS = 'gps'
KEY_HEIGHT = 'height'
KEY_ISO = 'iso'
KEY_ORIENTATION = 'orientation'
KEY_RATIO = 'ratio'
KEY_SIZE = 'size'
KEY_TIME = 'time' # USE time.localtime(value) or datetime.fromtimestamp(value) to convert the timestamp
KEY_TIME_IS_SUBSTITUTION = 'tm_is_subst'
KEY_TITLE = 'title'
KEY_TRACK = 'track'
KEY_WIDTH = 'width'
KEY_YEAR = 'year'
__KEY_CAMERA_VENDOR__ = 'camera_vendor'
__KEY_CAMERA_MODEL__ = 'camera_model'
def get_filetype(full_path):
ext = os.path.splitext(full_path)[1]
if ext in EXTENTIONS_AUDIO:
return FILETYPE_AUDIO
elif ext in EXTENTIONS_IMAGE:
return FILETYPE_IMAGE
elif ext in EXTENTIONS_VIDEO:
return FILETYPE_VIDEO
def get_audio_data(full_path):
conv_key_dict = {}
conv_key_dict['album'] = (str, KEY_ALBUM)
conv_key_dict['TAG:album'] = (str, KEY_ALBUM)
conv_key_dict['TAG:artist'] = (str, KEY_ARTIST)
conv_key_dict['artist'] = (str, KEY_ARTIST)
conv_key_dict['bit_rate'] = (__int_conv__, KEY_BITRATE)
conv_key_dict['duration'] = (float, KEY_DURATION)
conv_key_dict['TAG:genre'] = (str, KEY_GENRE)
conv_key_dict['genre'] = (str, KEY_GENRE)
conv_key_dict['TAG:title'] = (str, KEY_TITLE)
conv_key_dict['title'] = (str, KEY_TITLE)
conv_key_dict['TAG:track'] = (__int_conv__, KEY_TRACK)
conv_key_dict['track'] = (__int_conv__, KEY_TRACK)
conv_key_dict['TAG:date'] = (__int_conv__, KEY_YEAR)
conv_key_dict['date'] = (__int_conv__, KEY_YEAR)
return __adapt__data__(__get_xxprobe_data__(full_path, conv_key_dict), full_path)
def get_video_data(full_path):
conv_key_dict = {}
conv_key_dict['creation_time'] = (__vid_datetime_conv__, KEY_TIME)
conv_key_dict['TAG:creation_time'] = (__vid_datetime_conv__, KEY_TIME)
conv_key_dict['bit_rate'] = (__int_conv__, KEY_BITRATE)
conv_key_dict['duration'] = (float, KEY_DURATION)
conv_key_dict['height'] = (__int_conv__, KEY_HEIGHT)
conv_key_dict['width'] = (__int_conv__, KEY_WIDTH)
conv_key_dict['display_aspect_ratio'] = (__ratio_conv__, KEY_RATIO)
return __adapt__data__(__get_xxprobe_data__(full_path, conv_key_dict), full_path)
def get_image_data(full_path):
return __adapt__data__(__get_exif_data__(full_path), full_path)
def __adapt__data__(data, full_path):
data[KEY_SIZE] = os.path.getsize(full_path)
# Join Camera Vendor and Camera Model
if __KEY_CAMERA_MODEL__ in data and __KEY_CAMERA_VENDOR__ in data:
model = data.pop(__KEY_CAMERA_MODEL__)
vendor = data.pop(__KEY_CAMERA_VENDOR__)
data[KEY_CAMERA] = '%s: %s' % (vendor, model)
# Add time if not exists
if KEY_TIME not in data:
if KEY_YEAR in data and KEY_TRACK in data:
# Use a date where track 1 is the newest in the given year
minute = int(data[KEY_TRACK] / 60)
second = (data[KEY_TRACK] - 60 * minute) % 60
#
data[KEY_TIME] = int(time.mktime((data[KEY_YEAR], 1, 1, 0, 59 - minute, 59 - second, 0, 0, 0)))
data[KEY_TIME_IS_SUBSTITUTION] = True
else:
data[KEY_TIME] = int(os.path.getmtime(full_path))
data[KEY_TIME_IS_SUBSTITUTION] = True
return data
def __get_xxprobe_data__(full_path, conv_key_dict):
def _ffprobe_command(full_path):
return ['ffprobe', '-v', 'quiet', '-show_format', '-show_streams', full_path]
def _avprobe_command(full_path):
return ['avprobe', '-v', 'quiet', '-show_format', '-show_streams', full_path]
try:
xxprobe_text = subprocess.check_output(_avprobe_command(full_path))
except FileNotFoundError:
try:
xxprobe_text = subprocess.check_output(_ffprobe_command(full_path))
except FileNotFoundError:
logger.warning('ffprobe and avprobe seem to be not installed')
return {}
#
rv = {}
for line in xxprobe_text.decode('utf-8').splitlines():
try:
key, val = [snippet.strip() for snippet in line.split('=')]
except ValueError:
continue
else:
if key in conv_key_dict:
tp, name = conv_key_dict[key]
try:
rv[name] = tp(val)
except ValueError:
logger.log(logging.WARNING if val else logger.INFO, 'Can\'t convert %s (%s) for %s', repr(val), name, name)
return rv
def __get_exif_data__(full_path):
rv = {}
im = Image.open(full_path)
try:
exif = dict(im._getexif().items())
except AttributeError:
logger.debug('%s does not have any exif information', full_path)
else:
conv_key_dict = {}
# IMAGE
conv_key_dict[0x9003] = (__datetime_conv__, KEY_TIME)
conv_key_dict[0x8822] = (__exposure_program_conv__, KEY_EXPOSURE_PROGRAM)
conv_key_dict[0x829A] = (__num_denum_conv__, KEY_EXPOSURE_TIME)
conv_key_dict[0x9209] = (__flash_conv__, KEY_FLASH)
conv_key_dict[0x829D] = (__num_denum_conv__, KEY_APERTURE)
conv_key_dict[0x920A] = (__num_denum_conv__, KEY_FOCAL_LENGTH)
conv_key_dict[0x8825] = (__gps_conv__, KEY_GPS)
conv_key_dict[0xA003] = (__int_conv__, KEY_HEIGHT)
conv_key_dict[0x8827] = (__int_conv__, KEY_ISO)
conv_key_dict[0x010F] = (str, __KEY_CAMERA_VENDOR__)
conv_key_dict[0x0110] = (str, __KEY_CAMERA_MODEL__)
conv_key_dict[0x0112] = (__int_conv__, KEY_ORIENTATION)
conv_key_dict[0xA002] = (__int_conv__, KEY_WIDTH)
for key in conv_key_dict:
if key in exif:
tp, name = conv_key_dict[key]
value = tp(exif[key])
if value is not None:
rv[name] = value
return rv
# TODO: Join datetime converter __datetime_conv__ and __vid_datetime_conv_
def __datetime_conv__(dt):
format_string = "%Y:%m:%d %H:%M:%S"
return int(time.mktime(time.strptime(dt, format_string)))
def __vid_datetime_conv__(dt):
try:
dt = dt[:dt.index('.')]
except ValueError:
pass # time string seems to have no '.'
dt = dt.replace('T', ' ').replace('/', '').replace('\\', '')
if len(dt) == 16:
dt += ':00'
format_string = '%Y-%m-%d %H:%M:%S'
return int(time.mktime(time.strptime(dt, format_string)))
def __exposure_program_conv__(n):
return {
0: 'Unidentified',
1: 'Manual',
2: 'Program Normal',
3: 'Aperture Priority',
4: 'Shutter Priority',
5: 'Program Creative',
6: 'Program Action',
7: 'Portrait Mode',
8: 'Landscape Mode'
}.get(n, None)
def __flash_conv__(n):
return {
0: 'No',
1: 'Fired',
5: 'Fired (?)', # no return sensed
7: 'Fired (!)', # return sensed
9: 'Fill Fired',
13: 'Fill Fired (?)',
15: 'Fill Fired (!)',
16: 'Off',
24: 'Auto Off',
25: 'Auto Fired',
29: 'Auto Fired (?)',
31: 'Auto Fired (!)',
32: 'Not Available'
}.get(n, None)
def __int_conv__(value):
try:
return int(value)
except ValueError:
for c in ['.', '/', '-']:
p = value.find(c)
if p >= 0:
value = value[:p]
return int(value)
def __num_denum_conv__(data):
num, denum = data
return num / denum
def __gps_conv__(data):
def lat_lon_cal(lon_or_lat):
lon_lat = 0.
fac = 1.
for num, denum in lon_or_lat:
lon_lat += float(num) / float(denum) * fac
fac *= 1. / 60.
return lon_lat
try:
lon = lat_lon_cal(data[0x0004])
lat = lat_lon_cal(data[0x0002])
if lon != 0 or lat != 0: # do not use lon and lat equal 0, caused by motorola gps weakness
return {'lon': lon, 'lat': lat}
except KeyError:
logger.warning('GPS data extraction failed for %s', repr(data))
def __ratio_conv__(ratio):
ratio = ratio.replace('\\', '')
num, denum = ratio.split(':')
return float(num) / float(denum)

BIN
todo.tgz

Binary file not shown.