Release: f44d258110e088423a3132bd52534f53
This commit is contained in:
parent
06a67dc238
commit
88ed56a985
216
__init__.py
216
__init__.py
@ -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
231
_testresults_/coverage.xml
Normal 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
1649
_testresults_/unittest.json
Normal file
File diff suppressed because it is too large
Load Diff
BIN
_testresults_/unittest.pdf
Normal file
BIN
_testresults_/unittest.pdf
Normal file
Binary file not shown.
264
metadata.py
Normal file
264
metadata.py
Normal 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)
|
Loading…
x
Reference in New Issue
Block a user