|
|
|
|
|
|
|
import json
|
|
|
|
import logging
|
|
|
|
from collections import defaultdict
|
|
|
|
from logging import NullHandler, getLogger
|
|
|
|
import enzyme
|
|
|
|
|
|
|
|
from knowit.core import Property
|
|
|
|
from knowit.properties import (
|
|
|
|
AudioCodec,
|
|
|
|
Basic,
|
|
|
|
Duration,
|
|
|
|
Language,
|
|
|
|
Quantity,
|
|
|
|
VideoCodec,
|
|
|
|
YesNo,
|
|
|
|
)
|
|
|
|
from knowit.provider import (
|
|
|
|
MalformedFileError,
|
|
|
|
Provider,
|
|
|
|
)
|
|
|
|
from knowit.rules import (
|
|
|
|
AudioChannelsRule,
|
|
|
|
ClosedCaptionRule,
|
|
|
|
HearingImpairedRule,
|
|
|
|
LanguageRule,
|
|
|
|
ResolutionRule,
|
|
|
|
)
|
|
|
|
from knowit.rules.general import GuessTitleRule
|
|
|
|
from knowit.serializer import get_json_encoder
|
|
|
|
from knowit.units import units
|
|
|
|
from knowit.utils import to_dict
|
|
|
|
|
|
|
|
logger = getLogger(__name__)
|
|
|
|
logger.addHandler(NullHandler())
|
|
|
|
|
|
|
|
|
|
|
|
class EnzymeProvider(Provider):
|
|
|
|
"""Enzyme Provider."""
|
|
|
|
|
|
|
|
def __init__(self, config, *args, **kwargs):
|
|
|
|
"""Init method."""
|
|
|
|
super().__init__(config, {
|
|
|
|
'general': {
|
|
|
|
'title': Property('title', description='media title'),
|
|
|
|
'duration': Duration('duration', description='media duration'),
|
|
|
|
},
|
|
|
|
'video': {
|
|
|
|
'id': Basic('number', data_type=int, description='video track number'),
|
|
|
|
'name': Property('name', description='video track name'),
|
|
|
|
'language': Language('language', description='video language'),
|
|
|
|
'width': Quantity('width', unit=units.pixel),
|
|
|
|
'height': Quantity('height', unit=units.pixel),
|
|
|
|
'scan_type': YesNo('interlaced', yes='Interlaced', no='Progressive', default='Progressive',
|
|
|
|
config=config, config_key='ScanType',
|
|
|
|
description='video scan type'),
|
|
|
|
'resolution': None, # populated with ResolutionRule
|
|
|
|
# 'bit_depth', Property('bit_depth', Integer('video bit depth')),
|
|
|
|
'codec': VideoCodec(config, 'codec_id', description='video codec'),
|
|
|
|
'forced': YesNo('forced', hide_value=False, description='video track forced'),
|
|
|
|
'default': YesNo('default', hide_value=False, description='video track default'),
|
|
|
|
'enabled': YesNo('enabled', hide_value=True, description='video track enabled'),
|
|
|
|
},
|
|
|
|
'audio': {
|
|
|
|
'id': Basic('number', data_type=int, description='audio track number'),
|
|
|
|
'name': Property('name', description='audio track name'),
|
|
|
|
'language': Language('language', description='audio language'),
|
|
|
|
'codec': AudioCodec(config, 'codec_id', description='audio codec'),
|
|
|
|
'channels_count': Basic('channels', data_type=int, description='audio channels count'),
|
|
|
|
'channels': None, # populated with AudioChannelsRule
|
|
|
|
'forced': YesNo('forced', hide_value=False, description='audio track forced'),
|
|
|
|
'default': YesNo('default', hide_value=False, description='audio track default'),
|
|
|
|
'enabled': YesNo('enabled', hide_value=True, description='audio track enabled'),
|
|
|
|
},
|
|
|
|
'subtitle': {
|
|
|
|
'id': Basic('number', data_type=int, description='subtitle track number'),
|
|
|
|
'name': Property('name', description='subtitle track name'),
|
|
|
|
'language': Language('language', description='subtitle language'),
|
|
|
|
'hearing_impaired': None, # populated with HearingImpairedRule
|
|
|
|
'closed_caption': None, # populated with ClosedCaptionRule
|
|
|
|
'forced': YesNo('forced', hide_value=False, description='subtitle track forced'),
|
|
|
|
'default': YesNo('default', hide_value=False, description='subtitle track default'),
|
|
|
|
'enabled': YesNo('enabled', hide_value=True, description='subtitle track enabled'),
|
|
|
|
},
|
|
|
|
}, {
|
|
|
|
'video': {
|
|
|
|
'guessed': GuessTitleRule('guessed properties', private=True),
|
|
|
|
'language': LanguageRule('video language', override=True),
|
|
|
|
'resolution': ResolutionRule('video resolution'),
|
|
|
|
},
|
|
|
|
'audio': {
|
|
|
|
'guessed': GuessTitleRule('guessed properties', private=True),
|
|
|
|
'language': LanguageRule('audio language', override=True),
|
|
|
|
'channels': AudioChannelsRule('audio channels'),
|
|
|
|
},
|
|
|
|
'subtitle': {
|
|
|
|
'guessed': GuessTitleRule('guessed properties', private=True),
|
|
|
|
'language': LanguageRule('subtitle language', override=True),
|
|
|
|
'hearing_impaired': HearingImpairedRule('subtitle hearing impaired', override=True),
|
|
|
|
'closed_caption': ClosedCaptionRule('closed caption', override=True),
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
def accepts(self, video_path):
|
|
|
|
"""Accept only MKV files."""
|
|
|
|
return video_path.lower().endswith('.mkv')
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def extract_info(cls, video_path):
|
|
|
|
"""Extract info from the video."""
|
|
|
|
with open(video_path, 'rb') as f:
|
|
|
|
return to_dict(enzyme.MKV(f))
|
|
|
|
|
|
|
|
def describe(self, video_path, context):
|
|
|
|
"""Return video metadata."""
|
|
|
|
try:
|
|
|
|
data = defaultdict(dict)
|
|
|
|
ff = self.extract_info(video_path)
|
|
|
|
|
|
|
|
def debug_data():
|
|
|
|
"""Debug data."""
|
|
|
|
return json.dumps(ff, cls=get_json_encoder(context), indent=4, ensure_ascii=False)
|
|
|
|
context['debug_data'] = debug_data
|
|
|
|
|
|
|
|
if logger.isEnabledFor(logging.DEBUG):
|
|
|
|
logger.debug('Video %r scanned using enzyme %r has raw data:\n%s',
|
|
|
|
video_path, enzyme.__version__, debug_data)
|
|
|
|
|
|
|
|
data.update(ff)
|
|
|
|
if 'info' in data and data['info'] is None:
|
|
|
|
return {}
|
|
|
|
except enzyme.MalformedMKVError: # pragma: no cover
|
|
|
|
raise MalformedFileError
|
|
|
|
|
|
|
|
if logger.level == logging.DEBUG:
|
|
|
|
logger.debug('Video {video_path} scanned using Enzyme {version} has raw data:\n{data}',
|
|
|
|
video_path=video_path, version=enzyme.__version__,
|
|
|
|
data=json.dumps(data, cls=get_json_encoder(context), indent=4, ensure_ascii=False))
|
|
|
|
|
|
|
|
result = self._describe_tracks(video_path, data.get('info', {}), data.get('video_tracks'),
|
|
|
|
data.get('audio_tracks'), data.get('subtitle_tracks'), context)
|
|
|
|
|
|
|
|
if not result:
|
|
|
|
raise MalformedFileError
|
|
|
|
|
|
|
|
result['provider'] = {
|
|
|
|
'name': 'enzyme',
|
|
|
|
'version': self.version
|
|
|
|
}
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
@property
|
|
|
|
def version(self):
|
|
|
|
"""Return enzyme version information."""
|
|
|
|
return {'enzyme': enzyme.__version__}
|