Merge remote-tracking branch 'jaimeMF/merge-formats'
authorPhilipp Hagemeister <phihag@phihag.de>
Sun, 5 Jan 2014 01:06:48 +0000 (02:06 +0100)
committerPhilipp Hagemeister <phihag@phihag.de>
Sun, 5 Jan 2014 01:06:48 +0000 (02:06 +0100)
1  2 
youtube_dl/YoutubeDL.py

diff --combined youtube_dl/YoutubeDL.py
index f70c356c07bacea38de21956d4d4fac8c232af2c,8731279c114923a3ab91173ee2f75d0734a8244a..3786799da7967ca8522462758ee81318f1e06214
@@@ -1,7 -1,7 +1,7 @@@
  #!/usr/bin/env python
  # -*- coding: utf-8 -*-
  
 -from __future__ import absolute_import
 +from __future__ import absolute_import, unicode_literals
  
  import collections
  import errno
@@@ -51,9 -51,11 +51,11 @@@ from .utils import 
      write_json_file,
      write_string,
      YoutubeDLHandler,
+     prepend_extension,
  )
  from .extractor import get_info_extractor, gen_extractors
  from .downloader import get_suitable_downloader
+ from .PostProcessor import FFmpegMergerPP
  from .version import __version__
  
  
@@@ -200,7 -202,7 +202,7 @@@ class YoutubeDL(object)
                  self._output_channel = os.fdopen(master, 'rb')
              except OSError as ose:
                  if ose.errno == 2:
 -                    self.report_warning(u'Could not find fribidi executable, ignoring --bidi-workaround . Make sure that  fribidi  is an executable file in one of the directories in your $PATH.')
 +                    self.report_warning('Could not find fribidi executable, ignoring --bidi-workaround . Make sure that  fribidi  is an executable file in one of the directories in your $PATH.')
                  else:
                      raise
  
                  and not params['restrictfilenames']):
              # On Python 3, the Unicode filesystem API will throw errors (#1474)
              self.report_warning(
 -                u'Assuming --restrict-filenames since file system encoding '
 -                u'cannot encode all charactes. '
 -                u'Set the LC_ALL environment variable to fix this.')
 +                'Assuming --restrict-filenames since file system encoding '
 +                'cannot encode all charactes. '
 +                'Set the LC_ALL environment variable to fix this.')
              self.params['restrictfilenames'] = True
  
          if '%(stitle)s' in self.params.get('outtmpl', ''):
 -            self.report_warning(u'%(stitle)s is deprecated. Use the %(title)s and the --restrict-filenames flag(which also secures %(uploader)s et al) instead.')
 +            self.report_warning('%(stitle)s is deprecated. Use the %(title)s and the --restrict-filenames flag(which also secures %(uploader)s et al) instead.')
  
          self._setup_opener()
  
              return message
  
          assert hasattr(self, '_output_process')
 -        assert type(message) == type(u'')
 -        line_count = message.count(u'\n') + 1
 -        self._output_process.stdin.write((message + u'\n').encode('utf-8'))
 +        assert type(message) == type('')
 +        line_count = message.count('\n') + 1
 +        self._output_process.stdin.write((message + '\n').encode('utf-8'))
          self._output_process.stdin.flush()
 -        res = u''.join(self._output_channel.readline().decode('utf-8')
 +        res = ''.join(self._output_channel.readline().decode('utf-8')
                         for _ in range(line_count))
 -        return res[:-len(u'\n')]
 +        return res[:-len('\n')]
  
      def to_screen(self, message, skip_eol=False):
          """Print message to stdout if not in quiet mode."""
              self.params['logger'].debug(message)
          elif not check_quiet or not self.params.get('quiet', False):
              message = self._bidi_workaround(message)
 -            terminator = [u'\n', u''][skip_eol]
 +            terminator = ['\n', ''][skip_eol]
              output = message + terminator
  
              write_string(output, self._screen_file)
  
      def to_stderr(self, message):
          """Print message to stderr."""
 -        assert type(message) == type(u'')
 +        assert type(message) == type('')
          if self.params.get('logger'):
              self.params['logger'].error(message)
          else:
              message = self._bidi_workaround(message)
 -            output = message + u'\n'
 +            output = message + '\n'
              write_string(output, self._err_file)
  
      def to_console_title(self, message):
              # already of type unicode()
              ctypes.windll.kernel32.SetConsoleTitleW(ctypes.c_wchar_p(message))
          elif 'TERM' in os.environ:
 -            write_string(u'\033]0;%s\007' % message, self._screen_file)
 +            write_string('\033]0;%s\007' % message, self._screen_file)
  
      def save_console_title(self):
          if not self.params.get('consoletitle', False):
              return
          if 'TERM' in os.environ:
              # Save the title on stack
 -            write_string(u'\033[22;0t', self._screen_file)
 +            write_string('\033[22;0t', self._screen_file)
  
      def restore_console_title(self):
          if not self.params.get('consoletitle', False):
              return
          if 'TERM' in os.environ:
              # Restore the title from stack
 -            write_string(u'\033[23;0t', self._screen_file)
 +            write_string('\033[23;0t', self._screen_file)
  
      def __enter__(self):
          self.save_console_title()
          if self.params.get('verbose'):
              if tb is None:
                  if sys.exc_info()[0]:  # if .trouble has been called from an except block
 -                    tb = u''
 +                    tb = ''
                      if hasattr(sys.exc_info()[1], 'exc_info') and sys.exc_info()[1].exc_info[0]:
 -                        tb += u''.join(traceback.format_exception(*sys.exc_info()[1].exc_info))
 +                        tb += ''.join(traceback.format_exception(*sys.exc_info()[1].exc_info))
                      tb += compat_str(traceback.format_exc())
                  else:
                      tb_data = traceback.format_list(traceback.extract_stack())
 -                    tb = u''.join(tb_data)
 +                    tb = ''.join(tb_data)
              self.to_stderr(tb)
          if not self.params.get('ignoreerrors', False):
              if sys.exc_info()[0] and hasattr(sys.exc_info()[1], 'exc_info') and sys.exc_info()[1].exc_info[0]:
          If stderr is a tty file the 'WARNING:' will be colored
          '''
          if self._err_file.isatty() and os.name != 'nt':
 -            _msg_header = u'\033[0;33mWARNING:\033[0m'
 +            _msg_header = '\033[0;33mWARNING:\033[0m'
          else:
 -            _msg_header = u'WARNING:'
 -        warning_message = u'%s %s' % (_msg_header, message)
 +            _msg_header = 'WARNING:'
 +        warning_message = '%s %s' % (_msg_header, message)
          self.to_stderr(warning_message)
  
      def report_error(self, message, tb=None):
          in red if stderr is a tty file.
          '''
          if self._err_file.isatty() and os.name != 'nt':
 -            _msg_header = u'\033[0;31mERROR:\033[0m'
 +            _msg_header = '\033[0;31mERROR:\033[0m'
          else:
 -            _msg_header = u'ERROR:'
 -        error_message = u'%s %s' % (_msg_header, message)
 +            _msg_header = 'ERROR:'
 +        error_message = '%s %s' % (_msg_header, message)
          self.trouble(error_message, tb)
  
      def report_file_already_downloaded(self, file_name):
          """Report file has already been fully downloaded."""
          try:
 -            self.to_screen(u'[download] %s has already been downloaded' % file_name)
 +            self.to_screen('[download] %s has already been downloaded' % file_name)
          except UnicodeEncodeError:
 -            self.to_screen(u'[download] The file has already been downloaded')
 +            self.to_screen('[download] The file has already been downloaded')
  
      def increment_downloads(self):
          """Increment the ordinal that assigns a number to each file."""
              autonumber_size = self.params.get('autonumber_size')
              if autonumber_size is None:
                  autonumber_size = 5
 -            autonumber_templ = u'%0' + str(autonumber_size) + u'd'
 +            autonumber_templ = '%0' + str(autonumber_size) + 'd'
              template_dict['autonumber'] = autonumber_templ % self._num_downloads
              if template_dict.get('playlist_index') is not None:
 -                template_dict['playlist_index'] = u'%05d' % template_dict['playlist_index']
 +                template_dict['playlist_index'] = '%05d' % template_dict['playlist_index']
  
              sanitize = lambda k, v: sanitize_filename(
                  compat_str(v),
                  restricted=self.params.get('restrictfilenames'),
 -                is_id=(k == u'id'))
 +                is_id=(k == 'id'))
              template_dict = dict((k, sanitize(k, v))
                                   for k, v in template_dict.items()
                                   if v is not None)
 -            template_dict = collections.defaultdict(lambda: u'NA', template_dict)
 +            template_dict = collections.defaultdict(lambda: 'NA', template_dict)
  
              tmpl = os.path.expanduser(self.params['outtmpl'])
              filename = tmpl % template_dict
              return filename
          except ValueError as err:
 -            self.report_error(u'Error in output template: ' + str(err) + u' (encoding: ' + repr(preferredencoding()) + ')')
 +            self.report_error('Error in output template: ' + str(err) + ' (encoding: ' + repr(preferredencoding()) + ')')
              return None
  
      def _match_entry(self, info_dict):
          """ Returns None iff the file should be downloaded """
  
 -        video_title = info_dict.get('title', info_dict.get('id', u'video'))
 +        video_title = info_dict.get('title', info_dict.get('id', 'video'))
          if 'title' in info_dict:
              # This can happen when we're just evaluating the playlist
              title = info_dict['title']
              matchtitle = self.params.get('matchtitle', False)
              if matchtitle:
                  if not re.search(matchtitle, title, re.IGNORECASE):
 -                    return u'"' + title + '" title did not match pattern "' + matchtitle + '"'
 +                    return '"' + title + '" title did not match pattern "' + matchtitle + '"'
              rejecttitle = self.params.get('rejecttitle', False)
              if rejecttitle:
                  if re.search(rejecttitle, title, re.IGNORECASE):
 -                    return u'"' + title + '" title matched reject pattern "' + rejecttitle + '"'
 +                    return '"' + title + '" title matched reject pattern "' + rejecttitle + '"'
          date = info_dict.get('upload_date', None)
          if date is not None:
              dateRange = self.params.get('daterange', DateRange())
              if date not in dateRange:
 -                return u'%s upload date is not in range %s' % (date_from_str(date).isoformat(), dateRange)
 +                return '%s upload date is not in range %s' % (date_from_str(date).isoformat(), dateRange)
          view_count = info_dict.get('view_count', None)
          if view_count is not None:
              min_views = self.params.get('min_views')
              if min_views is not None and view_count < min_views:
 -                return u'Skipping %s, because it has not reached minimum view count (%d/%d)' % (video_title, view_count, min_views)
 +                return 'Skipping %s, because it has not reached minimum view count (%d/%d)' % (video_title, view_count, min_views)
              max_views = self.params.get('max_views')
              if max_views is not None and view_count > max_views:
 -                return u'Skipping %s, because it has exceeded the maximum view count (%d/%d)' % (video_title, view_count, max_views)
 +                return 'Skipping %s, because it has exceeded the maximum view count (%d/%d)' % (video_title, view_count, max_views)
          age_limit = self.params.get('age_limit')
          if age_limit is not None:
              if age_limit < info_dict.get('age_limit', 0):
 -                return u'Skipping "' + title + '" because it is age restricted'
 +                return 'Skipping "' + title + '" because it is age restricted'
          if self.in_download_archive(info_dict):
 -            return u'%s has already been recorded in archive' % video_title
 +            return '%s has already been recorded in archive' % video_title
          return None
  
      @staticmethod
                  continue
  
              if not ie.working():
 -                self.report_warning(u'The program functionality for this site has been marked as broken, '
 -                                    u'and will probably not work.')
 +                self.report_warning('The program functionality for this site has been marked as broken, '
 +                                    'and will probably not work.')
  
              try:
                  ie_result = ie.extract(url)
                  else:
                      raise
          else:
 -            self.report_error(u'no suitable InfoExtractor: %s' % url)
 +            self.report_error('no suitable InfoExtractor: %s' % url)
  
      def process_ie_result(self, ie_result, download=True, extra_info={}):
          """
          elif result_type == 'playlist':
              # We process each entry in the playlist
              playlist = ie_result.get('title', None) or ie_result.get('id', None)
 -            self.to_screen(u'[download] Downloading playlist: %s' % playlist)
 +            self.to_screen('[download] Downloading playlist: %s' % playlist)
  
              playlist_results = []
  
              n_entries = len(entries)
  
              self.to_screen(
 -                u"[%s] playlist '%s': Collected %d video ids (downloading %d of them)" %
 +                "[%s] playlist '%s': Collected %d video ids (downloading %d of them)" %
                  (ie_result['extractor'], playlist, n_all_entries, n_entries))
  
              for i, entry in enumerate(entries, 1):
 -                self.to_screen(u'[download] Downloading video #%s of %s' % (i, n_entries))
 +                self.to_screen('[download] Downloading video #%s of %s' % (i, n_entries))
                  extra = {
                      'playlist': playlist,
                      'playlist_index': i + playliststart,
  
                  reason = self._match_entry(entry)
                  if reason is not None:
 -                    self.to_screen(u'[download] ' + reason)
 +                    self.to_screen('[download] ' + reason)
                      continue
  
                  entry_result = self.process_ie_result(entry,
          elif format_spec == 'worst':
              return available_formats[0]
          else:
 -            extensions = [u'mp4', u'flv', u'webm', u'3gp']
 +            extensions = ['mp4', 'flv', 'webm', '3gp']
              if format_spec in extensions:
                  filter_f = lambda f: f['ext'] == format_spec
              else:
              info_dict['playlist_index'] = None
  
          # This extractors handle format selection themselves
 -        if info_dict['extractor'] in [u'Youku']:
 +        if info_dict['extractor'] in ['Youku']:
              if download:
                  self.process_info(info_dict)
              return info_dict
              if format.get('format_id') is None:
                  format['format_id'] = compat_str(i)
              if format.get('format') is None:
 -                format['format'] = u'{id} - {res}{note}'.format(
 +                format['format'] = '{id} - {res}{note}'.format(
                      id=format['format_id'],
                      res=self.format_resolution(format),
 -                    note=u' ({0})'.format(format['format_note']) if format.get('format_note') is not None else '',
 +                    note=' ({0})'.format(format['format_note']) if format.get('format_note') is not None else '',
                  )
              # Automatically determine file extension if missing
              if 'ext' not in format:
              # the first that is available, starting from left
              req_formats = req_format.split('/')
              for rf in req_formats:
-                 selected_format = self.select_format(rf, formats)
+                 if re.match(r'.+?\+.+?', rf) is not None:
+                     # Two formats have been requested like '137+139'
+                     format_1, format_2 = rf.split('+')
+                     formats_info = (self.select_format(format_1, formats),
+                         self.select_format(format_2, formats))
+                     if all(formats_info):
+                         selected_format = {'requested_formats': formats_info}
+                     else:
+                         selected_format = None
+                 else:
+                     selected_format = self.select_format(rf, formats)
                  if selected_format is not None:
                      formats_to_download = [selected_format]
                      break
          if not formats_to_download:
 -            raise ExtractorError(u'requested format not available',
 +            raise ExtractorError('requested format not available',
                                   expected=True)
  
          if download:
              if len(formats_to_download) > 1:
 -                self.to_screen(u'[info] %s: downloading video in %s formats' % (info_dict['id'], len(formats_to_download)))
 +                self.to_screen('[info] %s: downloading video in %s formats' % (info_dict['id'], len(formats_to_download)))
              for format in formats_to_download:
                  new_info = dict(info_dict)
                  new_info.update(format)
  
          info_dict['fulltitle'] = info_dict['title']
          if len(info_dict['title']) > 200:
 -            info_dict['title'] = info_dict['title'][:197] + u'...'
 +            info_dict['title'] = info_dict['title'][:197] + '...'
  
          # Keep for backwards compatibility
          info_dict['stitle'] = info_dict['title']
  
          reason = self._match_entry(info_dict)
          if reason is not None:
 -            self.to_screen(u'[download] ' + reason)
 +            self.to_screen('[download] ' + reason)
              return
  
          max_downloads = self.params.get('max_downloads')
              self.to_stdout(info_dict['id'])
          if self.params.get('forceurl', False):
              # For RTMP URLs, also include the playpath
 -            self.to_stdout(info_dict['url'] + info_dict.get('play_path', u''))
 +            self.to_stdout(info_dict['url'] + info_dict.get('play_path', ''))
          if self.params.get('forcethumbnail', False) and info_dict.get('thumbnail') is not None:
              self.to_stdout(info_dict['thumbnail'])
          if self.params.get('forcedescription', False) and info_dict.get('description') is not None:
              if dn != '' and not os.path.exists(dn):
                  os.makedirs(dn)
          except (OSError, IOError) as err:
 -            self.report_error(u'unable to create directory ' + compat_str(err))
 +            self.report_error('unable to create directory ' + compat_str(err))
              return
  
          if self.params.get('writedescription', False):
 -            descfn = filename + u'.description'
 +            descfn = filename + '.description'
              if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(descfn)):
 -                self.to_screen(u'[info] Video description is already present')
 +                self.to_screen('[info] Video description is already present')
              else:
                  try:
 -                    self.to_screen(u'[info] Writing video description to: ' + descfn)
 +                    self.to_screen('[info] Writing video description to: ' + descfn)
                      with io.open(encodeFilename(descfn), 'w', encoding='utf-8') as descfile:
                          descfile.write(info_dict['description'])
                  except (KeyError, TypeError):
 -                    self.report_warning(u'There\'s no description to write.')
 +                    self.report_warning('There\'s no description to write.')
                  except (OSError, IOError):
 -                    self.report_error(u'Cannot write description file ' + descfn)
 +                    self.report_error('Cannot write description file ' + descfn)
                      return
  
          if self.params.get('writeannotations', False):
 -            annofn = filename + u'.annotations.xml'
 +            annofn = filename + '.annotations.xml'
              if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(annofn)):
 -                self.to_screen(u'[info] Video annotations are already present')
 +                self.to_screen('[info] Video annotations are already present')
              else:
                  try:
 -                    self.to_screen(u'[info] Writing video annotations to: ' + annofn)
 +                    self.to_screen('[info] Writing video annotations to: ' + annofn)
                      with io.open(encodeFilename(annofn), 'w', encoding='utf-8') as annofile:
                          annofile.write(info_dict['annotations'])
                  except (KeyError, TypeError):
 -                    self.report_warning(u'There are no annotations to write.')
 +                    self.report_warning('There are no annotations to write.')
                  except (OSError, IOError):
 -                    self.report_error(u'Cannot write annotations file: ' + annofn)
 +                    self.report_error('Cannot write annotations file: ' + annofn)
                      return
  
          subtitles_are_requested = any([self.params.get('writesubtitles', False),
                  try:
                      sub_filename = subtitles_filename(filename, sub_lang, sub_format)
                      if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(sub_filename)):
 -                        self.to_screen(u'[info] Video subtitle %s.%s is already_present' % (sub_lang, sub_format))
 +                        self.to_screen('[info] Video subtitle %s.%s is already_present' % (sub_lang, sub_format))
                      else:
 -                        self.to_screen(u'[info] Writing video subtitles to: ' + sub_filename)
 +                        self.to_screen('[info] Writing video subtitles to: ' + sub_filename)
                          with io.open(encodeFilename(sub_filename), 'w', encoding='utf-8') as subfile:
                                  subfile.write(sub)
                  except (OSError, IOError):
 -                    self.report_error(u'Cannot write subtitles file ' + descfn)
 +                    self.report_error('Cannot write subtitles file ' + descfn)
                      return
  
          if self.params.get('writeinfojson', False):
 -            infofn = os.path.splitext(filename)[0] + u'.info.json'
 +            infofn = os.path.splitext(filename)[0] + '.info.json'
              if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(infofn)):
 -                self.to_screen(u'[info] Video description metadata is already present')
 +                self.to_screen('[info] Video description metadata is already present')
              else:
 -                self.to_screen(u'[info] Writing video description metadata as JSON to: ' + infofn)
 +                self.to_screen('[info] Writing video description metadata as JSON to: ' + infofn)
                  try:
                      write_json_file(info_dict, encodeFilename(infofn))
                  except (OSError, IOError):
 -                    self.report_error(u'Cannot write metadata to JSON file ' + infofn)
 +                    self.report_error('Cannot write metadata to JSON file ' + infofn)
                      return
  
          if self.params.get('writethumbnail', False):
              if info_dict.get('thumbnail') is not None:
 -                thumb_format = determine_ext(info_dict['thumbnail'], u'jpg')
 -                thumb_filename = os.path.splitext(filename)[0] + u'.' + thumb_format
 +                thumb_format = determine_ext(info_dict['thumbnail'], 'jpg')
 +                thumb_filename = os.path.splitext(filename)[0] + '.' + thumb_format
                  if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(thumb_filename)):
 -                    self.to_screen(u'[%s] %s: Thumbnail is already present' %
 +                    self.to_screen('[%s] %s: Thumbnail is already present' %
                                     (info_dict['extractor'], info_dict['id']))
                  else:
 -                    self.to_screen(u'[%s] %s: Downloading thumbnail ...' %
 +                    self.to_screen('[%s] %s: Downloading thumbnail ...' %
                                     (info_dict['extractor'], info_dict['id']))
                      try:
                          uf = compat_urllib_request.urlopen(info_dict['thumbnail'])
                          with open(thumb_filename, 'wb') as thumbf:
                              shutil.copyfileobj(uf, thumbf)
 -                        self.to_screen(u'[%s] %s: Writing thumbnail to: %s' %
 +                        self.to_screen('[%s] %s: Writing thumbnail to: %s' %
                              (info_dict['extractor'], info_dict['id'], thumb_filename))
                      except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err:
 -                        self.report_warning(u'Unable to download thumbnail "%s": %s' %
 +                        self.report_warning('Unable to download thumbnail "%s": %s' %
                              (info_dict['thumbnail'], compat_str(err)))
  
          if not self.params.get('skip_download', False):
                  success = True
              else:
                  try:
-                     fd = get_suitable_downloader(info_dict)(self, self.params)
-                     for ph in self._progress_hooks:
-                         fd.add_progress_hook(ph)
-                     success = fd.download(filename, info_dict)
+                     def dl(name, info):
+                         fd = get_suitable_downloader(info)(self, self.params)
+                         for ph in self._progress_hooks:
+                             fd.add_progress_hook(ph)
+                         return fd.download(name, info)
+                     if info_dict.get('requested_formats') is not None:
+                         downloaded = []
+                         success = True
+                         for f in info_dict['requested_formats']:
+                             new_info = dict(info_dict)
+                             new_info.update(f)
+                             fname = self.prepare_filename(new_info)
+                             fname = prepend_extension(fname, 'f%s' % f['format_id'])
+                             downloaded.append(fname)
+                             partial_success = dl(fname, new_info)
+                             success = success and partial_success
+                         info_dict['__postprocessors'] = [FFmpegMergerPP(self)]
+                         info_dict['__files_to_merge'] = downloaded
+                     else:
+                         # Just a single file
+                         success = dl(filename, info_dict)
                  except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err:
 -                    self.report_error(u'unable to download video data: %s' % str(err))
 +                    self.report_error('unable to download video data: %s' % str(err))
                      return
                  except (OSError, IOError) as err:
                      raise UnavailableVideoError(err)
                  except (ContentTooShortError, ) as err:
 -                    self.report_error(u'content too short (expected %s bytes and served %s)' % (err.expected, err.downloaded))
 +                    self.report_error('content too short (expected %s bytes and served %s)' % (err.expected, err.downloaded))
                      return
  
              if success:
                  try:
                      self.post_process(filename, info_dict)
                  except (PostProcessingError) as err:
 -                    self.report_error(u'postprocessing: %s' % str(err))
 +                    self.report_error('postprocessing: %s' % str(err))
                      return
  
          self.record_download_archive(info_dict)
                  #It also downloads the videos
                  self.extract_info(url)
              except UnavailableVideoError:
 -                self.report_error(u'unable to download video')
 +                self.report_error('unable to download video')
              except MaxDownloadsReached:
 -                self.to_screen(u'[info] Maximum number of downloaded files reached.')
 +                self.to_screen('[info] Maximum number of downloaded files reached.')
                  raise
  
          return self._download_retcode
          except DownloadError:
              webpage_url = info.get('webpage_url')
              if webpage_url is not None:
 -                self.report_warning(u'The info failed to download, trying with "%s"' % webpage_url)
 +                self.report_warning('The info failed to download, trying with "%s"' % webpage_url)
                  return self.download([webpage_url])
              else:
                  raise
          info = dict(ie_info)
          info['filepath'] = filename
          keep_video = None
-         for pp in self._pps:
+         pps_chain = []
+         if ie_info.get('__postprocessors') is not None:
+             pps_chain.extend(ie_info['__postprocessors'])
+         pps_chain.extend(self._pps)
+         for pp in pps_chain:
              try:
                  keep_video_wish, new_info = pp.run(info)
                  if keep_video_wish is not None:
                  self.report_error(e.msg)
          if keep_video is False and not self.params.get('keepvideo', False):
              try:
 -                self.to_screen(u'Deleting original file %s (pass -k to keep)' % filename)
 +                self.to_screen('Deleting original file %s (pass -k to keep)' % filename)
                  os.remove(encodeFilename(filename))
              except (IOError, OSError):
 -                self.report_warning(u'Unable to remove downloaded video file')
 +                self.report_warning('Unable to remove downloaded video file')
  
      def _make_archive_id(self, info_dict):
          # Future-proof against any change in case
                  extractor = info_dict.get('ie_key')  # key in a playlist
          if extractor is None:
              return None  # Incomplete video information
 -        return extractor.lower() + u' ' + info_dict['id']
 +        return extractor.lower() + ' ' + info_dict['id']
  
      def in_download_archive(self, info_dict):
          fn = self.params.get('download_archive')
          vid_id = self._make_archive_id(info_dict)
          assert vid_id
          with locked_file(fn, 'a', encoding='utf-8') as archive_file:
 -            archive_file.write(vid_id + u'\n')
 +            archive_file.write(vid_id + '\n')
  
      @staticmethod
      def format_resolution(format, default='unknown'):
              return format['resolution']
          if format.get('height') is not None:
              if format.get('width') is not None:
 -                res = u'%sx%s' % (format['width'], format['height'])
 +                res = '%sx%s' % (format['width'], format['height'])
              else:
 -                res = u'%sp' % format['height']
 +                res = '%sp' % format['height']
          elif format.get('width') is not None:
 -            res = u'?x%d' % format['width']
 +            res = '?x%d' % format['width']
          else:
              res = default
          return res
  
      def list_formats(self, info_dict):
          def format_note(fdict):
 -            res = u''
 +            res = ''
              if fdict.get('ext') in ['f4f', 'f4m']:
 -                res += u'(unsupported) '
 +                res += '(unsupported) '
              if fdict.get('format_note') is not None:
 -                res += fdict['format_note'] + u' '
 +                res += fdict['format_note'] + ' '
              if fdict.get('tbr') is not None:
 -                res += u'%4dk ' % fdict['tbr']
 +                res += '%4dk ' % fdict['tbr']
              if (fdict.get('vcodec') is not None and
                      fdict.get('vcodec') != 'none'):
 -                res += u'%-5s@' % fdict['vcodec']
 +                res += '%-5s@' % fdict['vcodec']
              elif fdict.get('vbr') is not None and fdict.get('abr') is not None:
 -                res += u'video@'
 +                res += 'video@'
              if fdict.get('vbr') is not None:
 -                res += u'%4dk' % fdict['vbr']
 +                res += '%4dk' % fdict['vbr']
              if fdict.get('acodec') is not None:
                  if res:
 -                    res += u', '
 -                res += u'%-5s' % fdict['acodec']
 +                    res += ', '
 +                res += '%-5s' % fdict['acodec']
              elif fdict.get('abr') is not None:
                  if res:
 -                    res += u', '
 +                    res += ', '
                  res += 'audio'
              if fdict.get('abr') is not None:
 -                res += u'@%3dk' % fdict['abr']
 +                res += '@%3dk' % fdict['abr']
              if fdict.get('filesize') is not None:
                  if res:
 -                    res += u', '
 +                    res += ', '
                  res += format_bytes(fdict['filesize'])
              return res
  
          def line(format, idlen=20):
 -            return ((u'%-' + compat_str(idlen + 1) + u's%-10s%-12s%s') % (
 +            return (('%-' + compat_str(idlen + 1) + 's%-10s%-12s%s') % (
                  format['format_id'],
                  format['ext'],
                  self.format_resolution(format),
              ))
  
          formats = info_dict.get('formats', [info_dict])
 -        idlen = max(len(u'format code'),
 +        idlen = max(len('format code'),
                      max(len(f['format_id']) for f in formats))
          formats_s = [line(f, idlen) for f in formats]
          if len(formats) > 1:
              formats_s[-1] += (' ' if format_note(formats[-1]) else '') + '(best)'
  
          header_line = line({
 -            'format_id': u'format code', 'ext': u'extension',
 -            'resolution': u'resolution', 'format_note': u'note'}, idlen=idlen)
 -        self.to_screen(u'[info] Available formats for %s:\n%s\n%s' %
 -                       (info_dict['id'], header_line, u"\n".join(formats_s)))
 +            'format_id': 'format code', 'ext': 'extension',
 +            'resolution': 'resolution', 'format_note': 'note'}, idlen=idlen)
 +        self.to_screen('[info] Available formats for %s:\n%s\n%s' %
 +                       (info_dict['id'], header_line, '\n'.join(formats_s)))
  
      def urlopen(self, req):
          """ Start an HTTP download """
      def print_debug_header(self):
          if not self.params.get('verbose'):
              return
 -        write_string(u'[debug] youtube-dl version ' + __version__ + u'\n')
 +        write_string('[debug] youtube-dl version ' + __version__ + '\n')
          try:
              sp = subprocess.Popen(
                  ['git', 'rev-parse', '--short', 'HEAD'],
              out, err = sp.communicate()
              out = out.decode().strip()
              if re.match('[0-9a-f]+', out):
 -                write_string(u'[debug] Git HEAD: ' + out + u'\n')
 +                write_string('[debug] Git HEAD: ' + out + '\n')
          except:
              try:
                  sys.exc_clear()
              except:
                  pass
 -        write_string(u'[debug] Python version %s - %s' %
 -                     (platform.python_version(), platform_name()) + u'\n')
 +        write_string('[debug] Python version %s - %s' %
 +                     (platform.python_version(), platform_name()) + '\n')
  
          proxy_map = {}
          for handler in self._opener.handlers:
              if hasattr(handler, 'proxies'):
                  proxy_map.update(handler.proxies)
 -        write_string(u'[debug] Proxy map: ' + compat_str(proxy_map) + u'\n')
 +        write_string('[debug] Proxy map: ' + compat_str(proxy_map) + '\n')
  
      def _setup_opener(self):
          timeout_val = self.params.get('socket_timeout')