diff options
Diffstat (limited to 'mvc/converter.py')
-rw-r--r-- | mvc/converter.py | 278 |
1 files changed, 0 insertions, 278 deletions
diff --git a/mvc/converter.py b/mvc/converter.py deleted file mode 100644 index f845826..0000000 --- a/mvc/converter.py +++ /dev/null @@ -1,278 +0,0 @@ -import json -import logging -import os -import re -import shutil - -from mvc import resources, settings, utils -from mvc.utils import hms_to_seconds - -from mvc.qtfaststart import processor -from mvc.qtfaststart.exceptions import FastStartException - -logger = logging.getLogger(__name__) - -NON_WORD_CHARS = re.compile(r"[^a-zA-Z0-9]+") - -class ConverterInfo(object): - """Describes a particular output converter - - ConverterInfo is the base class for all converters. Subclasses must - implement get_executable() and get_arguments() - - :attribue name: user-friendly name for this converter - :attribute identifier: unique id for this converter - :attribute width: output width for this converter, or None to copy the - input width. This attribute is set to a default on construction, but can - be changed to reflect the user overriding the default. - :attribute height: output height for this converter. Works just like - width - :attribute dont_upsize: should we allow upsizing for conversions? - """ - media_type = None - bitrate = None - extension = None - audio_only = False - - def __init__(self, name, width=None, height=None, dont_upsize=True): - self.name = name - self.identifier = NON_WORD_CHARS.sub("", name).lower() - self.width = width - self.height = height - self.dont_upsize = dont_upsize - - def get_executable(self): - raise NotImplementedError - - def get_arguments(self, video, output): - raise NotImplementedError - - def get_output_filename(self, video): - basename = os.path.basename(video.filename) - name, ext = os.path.splitext(basename) - if ext and ext[0] == '.': - ext = ext[1:] - extension = self.extension if self.extension else ext - return '%s.%s.%s' % (name, self.identifier, extension) - - def get_output_size_guess(self, video): - if not self.bitrate or not video.duration: - return None - if video.duration: - return self.bitrate * video.duration / 8 - - def finalize(self, temp_output, output): - err = None - needs_remove = False - if self.media_type == 'format' and self.extension == 'mp4': - needs_remove = True - logging.debug('generic mp4 format detected. ' - 'Running qtfaststart...') - try: - processor.process(temp_output, output) - except FastStartException: - logging.exception('qtfaststart: exception occurred') - err = EnvironmentError('qtfaststart exception') - else: - try: - shutil.move(temp_output, output) - except EnvironmentError, e: - needs_remove = True - err = e - # If it didn't work for some reason try to clean up the stale stuff. - # And if that doesn't work ... just log, and re-raise the original - # error. - if needs_remove: - try: - os.remove(temp_output) - except EnvironmentError, e: - logging.error('finalize(): cannot remove stale file %r', - temp_output) - if err: - logging.error('finalize(): removal was in response to ' - 'error: %s', str(err)) - raise err - - def get_target_size(self, video): - """Get the size that we will convert to for a given video. - - :returns: (width, height) tuple - """ - return utils.rescale_video((video.width, video.height), - (self.width, self.height), - dont_upsize=self.dont_upsize) - - def process_status_line(self, line): - raise NotImplementedError - -class FFmpegConverterInfo(ConverterInfo): - """Base class for all ffmpeg-based conversions. - - Subclasses must override the parameters attribute and supply it with the - ffmpeg command line for the conversion. parameters can either be a list - of arguments, or a string in which case split() will be called to create - the list. - """ - DURATION_RE = re.compile(r'\W*Duration: (\d\d):(\d\d):(\d\d)\.(\d\d)' - '(, start:.*)?(, bitrate:.*)?') - PROGRESS_RE = re.compile(r'(?:frame=.* fps=.* q=.* )?size=.* time=(.*) ' - 'bitrate=(.*)') - LAST_PROGRESS_RE = re.compile(r'frame=.* fps=.* q=.* Lsize=.* time=(.*) ' - 'bitrate=(.*)') - - extension = None - parameters = None - - def get_executable(self): - return settings.get_ffmpeg_executable_path() - - def get_arguments(self, video, output): - args = ['-i', utils.convert_path_for_subprocess(video.filename), - '-strict', 'experimental'] - args.extend(settings.customize_ffmpeg_parameters( - self.get_parameters(video))) - if not (self.audio_only or video.audio_only): - width, height = self.get_target_size(video) - args.append("-s") - args.append('%ix%i' % (width, height)) - args.extend(self.get_extra_arguments(video, output)) - args.append(self.convert_output_path(output)) - return args - - def convert_output_path(self, output_path): - """Convert our output path so that it can be passed to ffmpeg.""" - # this is a bit tricky, because output_path doesn't exist on windows - # yet, so we can't just call convert_path_for_subprocess(). Instead, - # call convert_path_for_subprocess() on the output directory, and - # assume that the filename only contains safe characters - output_dir = os.path.dirname(output_path) - output_filename = os.path.basename(output_path) - return os.path.join(utils.convert_path_for_subprocess(output_dir), - output_filename) - - def get_extra_arguments(self, video, output): - """Subclasses can override this to add argumenst to the ffmpeg command - line. - """ - return [] - - def get_parameters(self, video): - if self.parameters is None: - raise ValueError("%s: parameters is None" % self) - elif isinstance(self.parameters, basestring): - return self.parameters.split() - else: - return list(self.parameters) - - @staticmethod - def _check_for_errors(line): - if line.startswith('Unknown'): - return line - if line.startswith("Error"): - if not line.startswith("Error while decoding stream"): - return line - - @classmethod - def process_status_line(klass, video, line): - error = klass._check_for_errors(line) - if error: - return {'finished': True, 'error': error} - - match = klass.DURATION_RE.match(line) - if match is not None: - hours, minutes, seconds, centi = [ - int(m) for m in match.groups()[:4]] - return {'duration': hms_to_seconds(hours, minutes, - seconds + 0.01 * centi)} - - match = klass.PROGRESS_RE.match(line) - if match is not None: - t = match.group(1) - if ':' in t: - hours, minutes, seconds = [float(m) for m in t.split(':')[:3]] - return {'progress': hms_to_seconds(hours, minutes, seconds)} - else: - return {'progress': float(t)} - - match = klass.LAST_PROGRESS_RE.match(line) - if match is not None: - return {'finished': True} - -class FFmpegConverterInfo1080p(FFmpegConverterInfo): - def __init__(self, name): - FFmpegConverterInfo.__init__(self, name, 1920, 1080) - -class FFmpegConverterInfo720p(FFmpegConverterInfo): - def __init__(self, name): - FFmpegConverterInfo.__init__(self, name, 1080, 720) - -class FFmpegConverterInfo480p(FFmpegConverterInfo): - def __init__(self, name): - FFmpegConverterInfo.__init__(self, name, 720, 480) - -class ConverterManager(object): - def __init__(self): - self.converters = {} - # converter -> brand reverse map. XXX: this code, really, really sucks - # and not very scalable. - self.brand_rmap = {} - self.brand_map = {} - - def add_converter(self, converter): - self.converters[converter.identifier] = converter - - def startup(self): - self.load_simple_converters() - self.load_converters(resources.converter_scripts()) - - def brand_to_converters(self, brand): - try: - return self.brand_map[brand] - except KeyError: - return None - - def converter_to_brand(self, converter): - try: - return self.brand_rmap[converter] - except KeyError: - return None - - def load_simple_converters(self): - from mvc import basicconverters - for converter in basicconverters.converters: - if isinstance(converter, tuple): - brand, realconverters = converter - for realconverter in realconverters: - self.brand_rmap[realconverter] = brand - self.brand_map.setdefault(brand, []).append(realconverter) - self.add_converter(realconverter) - else: - self.brand_rmap[converter] = None - self.brand_map.setdefault(None, []).append(converter) - self.add_converter(converter) - - def load_converters(self, converters): - for converter_file in converters: - global_dict = {} - execfile(converter_file, global_dict) - if 'converters' in global_dict: - for converter in global_dict['converters']: - if isinstance(converter, tuple): - brand, realconverters = converter - for realconverter in realconverters: - self.brand_rmap[realconverter] = brand - self.brand_map.setdefault(brand, []).append(realconverter) - self.add_converter(realconverter) - else: - self.brand_rmap[converter] = None - self.brand_map.setdefault(None, []).append(converter) - self.add_converter(converter) - logger.info('load_converters: loaded %i from %r', - len(global_dict['converters']), - converter_file) - - def list_converters(self): - return self.converters.values() - - def get_by_id(self, id_): - return self.converters[id_] |