Merge remote-tracking branch 'jaimemf/format_spec_groups' (closes #6124)
authorJaime Marquínez Ferrándiz <jaime.marquinez.ferrandiz@gmail.com>
Mon, 3 Aug 2015 13:22:51 +0000 (15:22 +0200)
committerJaime Marquínez Ferrándiz <jaime.marquinez.ferrandiz@gmail.com>
Mon, 3 Aug 2015 13:22:51 +0000 (15:22 +0200)
1  2 
README.md
youtube_dl/YoutubeDL.py
youtube_dl/compat.py

diff --combined README.md
index 2db3139eecaf9a6dfa1b82ead6553ab7b684208f,4e6cb3fc71ac59bad118722ee5d50137eb51dbbe..02b9775f9743f2db53a70d2bd0f55d1453d93c2e
+++ b/README.md
@@@ -75,7 -75,7 +75,7 @@@ which means you can modify it, redistri
  ## Video Selection:
      --playlist-start NUMBER          Playlist video to start at (default is 1)
      --playlist-end NUMBER            Playlist video to end at (default is last)
 -    --playlist-items ITEM_SPEC       Playlist video items to download. Specify indices of the videos in the playlist seperated by commas like: "--playlist-items 1,2,5,8"
 +    --playlist-items ITEM_SPEC       Playlist video items to download. Specify indices of the videos in the playlist separated by commas like: "--playlist-items 1,2,5,8"
                                       if you want to download videos indexed 1, 2, 5, 8 in the playlist. You can specify range: "--playlist-items 1-3,7,10-13", it will
                                       download the videos at index 1, 2, 3, 7, 10, 11, 12 and 13.
      --match-title REGEX              Download only matching titles (regex or caseless sub-string)
      --playlist-reverse               Download playlist videos in reverse order
      --xattr-set-filesize             Set file xattribute ytdl.filesize with expected filesize (experimental)
      --hls-prefer-native              Use the native HLS downloader instead of ffmpeg (experimental)
 -    --external-downloader COMMAND    Use the specified external downloader. Currently supports aria2c,curl,wget
 +    --external-downloader COMMAND    Use the specified external downloader. Currently supports aria2c,curl,httpie,wget
      --external-downloader-args ARGS  Give these arguments to the external downloader
  
  ## Filesystem Options:
      --all-formats                    Download all available video formats
      --prefer-free-formats            Prefer free video formats unless a specific one is requested
      -F, --list-formats               List all available formats
 -    --youtube-skip-dash-manifest     Do not download the DASH manifest on YouTube videos
 -    --merge-output-format FORMAT     If a merge is required (e.g. bestvideo+bestaudio), output to given container format. One of mkv, mp4, ogg, webm, flv.Ignored if no
 +    --youtube-skip-dash-manifest     Do not download the DASH manifests and related data on YouTube videos
 +    --merge-output-format FORMAT     If a merge is required (e.g. bestvideo+bestaudio), output to given container format. One of mkv, mp4, ogg, webm, flv. Ignored if no
                                       merge is required
  
  ## Subtitle Options:
      --audio-format FORMAT            Specify audio format: "best", "aac", "vorbis", "mp3", "m4a", "opus", or "wav"; "best" by default
      --audio-quality QUALITY          Specify ffmpeg/avconv audio quality, insert a value between 0 (better) and 9 (worse) for VBR or a specific bitrate like 128K (default
                                       5)
 -    --recode-video FORMAT            Encode the video to another format if necessary (currently supported: mp4|flv|ogg|webm|mkv)
 +    --recode-video FORMAT            Encode the video to another format if necessary (currently supported: mp4|flv|ogg|webm|mkv|avi)
 +    --postprocessor-args ARGS        Give these arguments to the postprocessor
      -k, --keep-video                 Keep the video file on disk after the post-processing; the video is erased by default
      --no-post-overwrites             Do not overwrite post-processed files; the post-processed files are overwritten by default
      --embed-subs                     Embed subtitles in the video (only for mkv and mp4 videos)
  
  You can configure youtube-dl by placing default arguments (such as `--extract-audio --no-mtime` to always extract the audio and not copy the mtime) into `/etc/youtube-dl.conf` and/or `~/.config/youtube-dl/config`. On Windows, the configuration file locations are `%APPDATA%\youtube-dl\config.txt` and `C:\Users\<user name>\youtube-dl.conf`.
  
 +### Authentication with `.netrc` file ###
 +
 +You may also want to configure automatic credentials storage for extractors that support authentication (by providing login and password with `--username` and `--password`) in order not to pass credentials as command line arguments on every youtube-dl execution and prevent tracking plain text passwords in shell command history. You can achieve this using [`.netrc` file](http://stackoverflow.com/tags/.netrc/info) on per extractor basis. For that you will need to create `.netrc` file in your `$HOME` and restrict permissions to read/write by you only:
 +```
 +touch $HOME/.netrc
 +chmod a-rwx,u+rw $HOME/.netrc
 +```
 +After that you can add credentials for extractor in the following format, where *extractor* is the name of extractor in lowercase:
 +```
 +machine <extractor> login <login> password <password>
 +```
 +For example:
 +```
 +machine youtube login myaccount@gmail.com password my_youtube_password
 +machine twitch login my_twitch_account_name password my_twitch_password
 +```
 +To activate authentication with `.netrc` file you should pass `--netrc` to youtube-dl or to place it in [configuration file](#configuration).
 +
 +On Windows you may also need to setup `%HOME%` environment variable manually.
 +
  # OUTPUT TEMPLATE
  
  The `-o` option allows users to indicate a template for the output file names. The basic usage is not to set any template arguments when downloading a single file, like in `youtube-dl -o funny_video.flv "http://some/video"`. However, it may contain special sequences that will be replaced when downloading each video. The special sequences have the format `%(NAME)s`. To clarify, that is a percent symbol followed by a name in parenthesis, followed by a lowercase S. Allowed names are:
@@@ -289,7 -268,7 +289,7 @@@ youtube-dl_test_video_.mp4          # 
  By default youtube-dl tries to download the best quality, but sometimes you may want to download other format.
  The simplest case is requesting a specific format, for example `-f 22`. You can get the list of available formats using `--list-formats`, you can also use a file extension (currently it supports aac, m4a, mp3, mp4, ogg, wav, webm) or the special names `best`, `bestvideo`, `bestaudio` and `worst`.
  
- If you want to download multiple videos and they don't have the same formats available, you can specify the order of preference using slashes, as in `-f 22/17/18`. You can also filter the video results by putting a condition in brackets, as in `-f "best[height=720]"` (or `-f "[filesize>10M]"`).  This works for filesize, height, width, tbr, abr, vbr, asr, and fps and the comparisons <, <=, >, >=, =, != and for ext, acodec, vcodec, container, and protocol and the comparisons =, != . Formats for which the value is not known are excluded unless you put a question mark (?) after the operator. You can combine format filters, so  `-f "[height <=? 720][tbr>500]"` selects up to 720p videos (or videos where the height is not known) with a bitrate of at least 500 KBit/s. Use commas to download multiple formats, such as `-f 136/137/mp4/bestvideo,140/m4a/bestaudio`. You can merge the video and audio of two formats into a single file using `-f <video-format>+<audio-format>` (requires ffmpeg or avconv), for example `-f bestvideo+bestaudio`.
+ If you want to download multiple videos and they don't have the same formats available, you can specify the order of preference using slashes, as in `-f 22/17/18`. You can also filter the video results by putting a condition in brackets, as in `-f "best[height=720]"` (or `-f "[filesize>10M]"`).  This works for filesize, height, width, tbr, abr, vbr, asr, and fps and the comparisons <, <=, >, >=, =, != and for ext, acodec, vcodec, container, and protocol and the comparisons =, != . Formats for which the value is not known are excluded unless you put a question mark (?) after the operator. You can combine format filters, so  `-f "[height <=? 720][tbr>500]"` selects up to 720p videos (or videos where the height is not known) with a bitrate of at least 500 KBit/s. Use commas to download multiple formats, such as `-f 136/137/mp4/bestvideo,140/m4a/bestaudio`. You can merge the video and audio of two formats into a single file using `-f <video-format>+<audio-format>` (requires ffmpeg or avconv), for example `-f bestvideo+bestaudio`. Format selectors can also be grouped using parentheses, for example if you want to download the best mp4 and webm formats with a height lower than 480 you can use `-f '(mp4,webm)[height<480]'`.
  
  Since the end of April 2015 and version 2015.04.26 youtube-dl uses `-f bestvideo+bestaudio/best` as default format selection (see #5447, #5456). If ffmpeg or avconv are installed this results in downloading `bestvideo` and `bestaudio` separately and muxing them together into a single file giving the best overall quality available. Otherwise it falls back to `best` and results in downloading best available quality served as a single file. `best` is also needed for videos that don't come from YouTube because they don't provide the audio and video in two different files. If you want to only download some dash formats (for example if you are not interested in getting videos with a resolution higher than 1080p), you can add `-f bestvideo[height<=?1080]+bestaudio/best` to your configuration file. Note that if you use youtube-dl to stream to `stdout` (and most likely to pipe it to your media player then), i.e. you explicitly specify output template as `-o -`, youtube-dl still uses `-f best` format selection in order to start content delivery immediately to your player and not to wait until `bestvideo` and `bestaudio` are downloaded and muxed.
  
@@@ -439,12 -418,6 +439,12 @@@ Either prepend `http://www.youtube.com/
      youtube-dl -- -wNyEUrxzFU
      youtube-dl "http://www.youtube.com/watch?v=-wNyEUrxzFU"
  
 +### How do I pass cookies to youtube-dl?
 +
 +Use the `--cookies` option, for example `--cookies /path/to/cookies/file.txt`. Note that cookies file must be in Mozilla/Netscape format and the first line of cookies file must be either `# HTTP Cookie File` or `# Netscape HTTP Cookie File`. Make sure you have correct [newline format](https://en.wikipedia.org/wiki/Newline) in cookies file and convert newlines if necessary to correspond your OS, namely `CRLF` (`\r\n`) for Windows, `LF` (`\n`) for Linux and `CR` (`\r`) for Mac OS. `HTTP Error 400: Bad Request` when using `--cookies` is a good sign of invalid newline format.
 +
 +Passing cookies to youtube-dl is a good way to workaround login when particular extractor does not implement it explicitly.
 +
  ### Can you add support for this anime video site, or site which shows current movies for free?
  
  As a matter of policy (as well as legality), youtube-dl does not include support for services that specialize in infringing copyright. As a rule of thumb, if you cannot easily find a video that the service is quite obviously allowed to distribute (i.e. that has been uploaded by the creator, the creator's distributor, or is published under a free license), the service is probably unfit for inclusion to youtube-dl.
diff --combined youtube_dl/YoutubeDL.py
index 702a6ad50b6c6bf2d3f3bfbd8c873cb3a64c8e7b,da7c510083820353d64db31a84dec7ccfac85c3a..efa3254ceec68d1a9af9d82cb146be63ffbe9309
@@@ -21,6 -21,7 +21,7 @@@ import subproces
  import socket
  import sys
  import time
+ import tokenize
  import traceback
  
  if os.name == 'nt':
@@@ -34,6 -35,7 +35,7 @@@ from .compat import 
      compat_http_client,
      compat_kwargs,
      compat_str,
+     compat_tokenize_tokenize,
      compat_urllib_error,
      compat_urllib_request,
  )
@@@ -262,8 -264,6 +264,8 @@@ class YoutubeDL(object)
      The following options are used by the post processors:
      prefer_ffmpeg:     If True, use ffmpeg instead of avconv if both are available,
                         otherwise prefer avconv.
 +    postprocessor_args: A list of additional command-line arguments for the
 +                        postprocessor.
      """
  
      params = None
          else:
              raise Exception('Invalid result type: %s' % result_type)
  
-     def _apply_format_filter(self, format_spec, available_formats):
-         " Returns a tuple of the remaining format_spec and filtered formats "
+     def _build_format_filter(self, filter_spec):
+         " Returns a function to filter the formats according to the filter_spec "
  
          OPERATORS = {
              '<': operator.lt,
              '=': operator.eq,
              '!=': operator.ne,
          }
-         operator_rex = re.compile(r'''(?x)\s*\[
+         operator_rex = re.compile(r'''(?x)\s*
              (?P<key>width|height|tbr|abr|vbr|asr|filesize|fps)
              \s*(?P<op>%s)(?P<none_inclusive>\s*\?)?\s*
              (?P<value>[0-9.]+(?:[kKmMgGtTpPeEzZyY]i?[Bb]?)?)
-             \]$
+             $
              ''' % '|'.join(map(re.escape, OPERATORS.keys())))
-         m = operator_rex.search(format_spec)
+         m = operator_rex.search(filter_spec)
          if m:
              try:
                  comparison_value = int(m.group('value'))
                  if comparison_value is None:
                      raise ValueError(
                          'Invalid value %r in format specification %r' % (
-                             m.group('value'), format_spec))
+                             m.group('value'), filter_spec))
              op = OPERATORS[m.group('op')]
  
          if not m:
                  '=': operator.eq,
                  '!=': operator.ne,
              }
-             str_operator_rex = re.compile(r'''(?x)\s*\[
+             str_operator_rex = re.compile(r'''(?x)
                  \s*(?P<key>ext|acodec|vcodec|container|protocol)
                  \s*(?P<op>%s)(?P<none_inclusive>\s*\?)?
                  \s*(?P<value>[a-zA-Z0-9_-]+)
-                 \s*\]$
+                 \s*$
                  ''' % '|'.join(map(re.escape, STR_OPERATORS.keys())))
-             m = str_operator_rex.search(format_spec)
+             m = str_operator_rex.search(filter_spec)
              if m:
                  comparison_value = m.group('value')
                  op = STR_OPERATORS[m.group('op')]
  
          if not m:
-             raise ValueError('Invalid format specification %r' % format_spec)
+             raise ValueError('Invalid filter specification %r' % filter_spec)
  
          def _filter(f):
              actual_value = f.get(m.group('key'))
              if actual_value is None:
                  return m.group('none_inclusive')
              return op(actual_value, comparison_value)
-         new_formats = [f for f in available_formats if _filter(f)]
+         return _filter
+     def build_format_selector(self, format_spec):
+         def syntax_error(note, start):
+             message = (
+                 'Invalid format specification: '
+                 '{0}\n\t{1}\n\t{2}^'.format(note, format_spec, ' ' * start[1]))
+             return SyntaxError(message)
+         PICKFIRST = 'PICKFIRST'
+         MERGE = 'MERGE'
+         SINGLE = 'SINGLE'
+         GROUP = 'GROUP'
+         FormatSelector = collections.namedtuple('FormatSelector', ['type', 'selector', 'filters'])
+         def _parse_filter(tokens):
+             filter_parts = []
+             for type, string, start, _, _ in tokens:
+                 if type == tokenize.OP and string == ']':
+                     return ''.join(filter_parts)
+                 else:
+                     filter_parts.append(string)
+         def _parse_format_selection(tokens, inside_merge=False, inside_choice=False, inside_group=False):
+             selectors = []
+             current_selector = None
+             for type, string, start, _, _ in tokens:
+                 # ENCODING is only defined in python 3.x
+                 if type == getattr(tokenize, 'ENCODING', None):
+                     continue
+                 elif type in [tokenize.NAME, tokenize.NUMBER]:
+                     current_selector = FormatSelector(SINGLE, string, [])
+                 elif type == tokenize.OP:
+                     if string == ')':
+                         if not inside_group:
+                             # ')' will be handled by the parentheses group
+                             tokens.restore_last_token()
+                         break
+                     elif inside_merge and string in ['/', ',']:
+                         tokens.restore_last_token()
+                         break
+                     elif inside_choice and string == ',':
+                         tokens.restore_last_token()
+                         break
+                     elif string == ',':
+                         if not current_selector:
+                             raise syntax_error('"," must follow a format selector', start)
+                         selectors.append(current_selector)
+                         current_selector = None
+                     elif string == '/':
+                         first_choice = current_selector
+                         second_choice = _parse_format_selection(tokens, inside_choice=True)
+                         current_selector = FormatSelector(PICKFIRST, (first_choice, second_choice), [])
+                     elif string == '[':
+                         if not current_selector:
+                             current_selector = FormatSelector(SINGLE, 'best', [])
+                         format_filter = _parse_filter(tokens)
+                         current_selector.filters.append(format_filter)
+                     elif string == '(':
+                         if current_selector:
+                             raise syntax_error('Unexpected "("', start)
+                         group = _parse_format_selection(tokens, inside_group=True)
+                         current_selector = FormatSelector(GROUP, group, [])
+                     elif string == '+':
+                         video_selector = current_selector
+                         audio_selector = _parse_format_selection(tokens, inside_merge=True)
+                         if not video_selector or not audio_selector:
+                             raise syntax_error('"+" must be between two format selectors', start)
+                         current_selector = FormatSelector(MERGE, (video_selector, audio_selector), [])
+                     else:
+                         raise syntax_error('Operator not recognized: "{0}"'.format(string), start)
+                 elif type == tokenize.ENDMARKER:
+                     break
+             if current_selector:
+                 selectors.append(current_selector)
+             return selectors
+         def _build_selector_function(selector):
+             if isinstance(selector, list):
+                 fs = [_build_selector_function(s) for s in selector]
+                 def selector_function(formats):
+                     for f in fs:
+                         for format in f(formats):
+                             yield format
+                 return selector_function
+             elif selector.type == GROUP:
+                 selector_function = _build_selector_function(selector.selector)
+             elif selector.type == PICKFIRST:
+                 fs = [_build_selector_function(s) for s in selector.selector]
+                 def selector_function(formats):
+                     for f in fs:
+                         picked_formats = list(f(formats))
+                         if picked_formats:
+                             return picked_formats
+                     return []
+             elif selector.type == SINGLE:
+                 format_spec = selector.selector
+                 def selector_function(formats):
+                     formats = list(formats)
+                     if not formats:
+                         return
+                     if format_spec == 'all':
+                         for f in formats:
+                             yield f
+                     elif format_spec in ['best', 'worst', None]:
+                         format_idx = 0 if format_spec == 'worst' else -1
+                         audiovideo_formats = [
+                             f for f in formats
+                             if f.get('vcodec') != 'none' and f.get('acodec') != 'none']
+                         if audiovideo_formats:
+                             yield audiovideo_formats[format_idx]
+                         # for audio only (soundcloud) or video only (imgur) urls, select the best/worst audio format
+                         elif (all(f.get('acodec') != 'none' for f in formats) or
+                               all(f.get('vcodec') != 'none' for f in formats)):
+                             yield formats[format_idx]
+                     elif format_spec == 'bestaudio':
+                         audio_formats = [
+                             f for f in formats
+                             if f.get('vcodec') == 'none']
+                         if audio_formats:
+                             yield audio_formats[-1]
+                     elif format_spec == 'worstaudio':
+                         audio_formats = [
+                             f for f in formats
+                             if f.get('vcodec') == 'none']
+                         if audio_formats:
+                             yield audio_formats[0]
+                     elif format_spec == 'bestvideo':
+                         video_formats = [
+                             f for f in formats
+                             if f.get('acodec') == 'none']
+                         if video_formats:
+                             yield video_formats[-1]
+                     elif format_spec == 'worstvideo':
+                         video_formats = [
+                             f for f in formats
+                             if f.get('acodec') == 'none']
+                         if video_formats:
+                             yield video_formats[0]
+                     else:
+                         extensions = ['mp4', 'flv', 'webm', '3gp', 'm4a', 'mp3', 'ogg', 'aac', 'wav']
+                         if format_spec in extensions:
+                             filter_f = lambda f: f['ext'] == format_spec
+                         else:
+                             filter_f = lambda f: f['format_id'] == format_spec
+                         matches = list(filter(filter_f, formats))
+                         if matches:
+                             yield matches[-1]
+             elif selector.type == MERGE:
+                 def _merge(formats_info):
+                     format_1, format_2 = [f['format_id'] for f in formats_info]
+                     # The first format must contain the video and the
+                     # second the audio
+                     if formats_info[0].get('vcodec') == 'none':
+                         self.report_error('The first format must '
+                                           'contain the video, try using '
+                                           '"-f %s+%s"' % (format_2, format_1))
+                         return
+                     output_ext = (
+                         formats_info[0]['ext']
+                         if self.params.get('merge_output_format') is None
+                         else self.params['merge_output_format'])
+                     return {
+                         'requested_formats': formats_info,
+                         'format': '%s+%s' % (formats_info[0].get('format'),
+                                              formats_info[1].get('format')),
+                         'format_id': '%s+%s' % (formats_info[0].get('format_id'),
+                                                 formats_info[1].get('format_id')),
+                         'width': formats_info[0].get('width'),
+                         'height': formats_info[0].get('height'),
+                         'resolution': formats_info[0].get('resolution'),
+                         'fps': formats_info[0].get('fps'),
+                         'vcodec': formats_info[0].get('vcodec'),
+                         'vbr': formats_info[0].get('vbr'),
+                         'stretched_ratio': formats_info[0].get('stretched_ratio'),
+                         'acodec': formats_info[1].get('acodec'),
+                         'abr': formats_info[1].get('abr'),
+                         'ext': output_ext,
+                     }
+                 video_selector, audio_selector = map(_build_selector_function, selector.selector)
  
-         new_format_spec = format_spec[:-len(m.group(0))]
-         if not new_format_spec:
-             new_format_spec = 'best'
+                 def selector_function(formats):
+                     formats = list(formats)
+                     for pair in itertools.product(video_selector(formats), audio_selector(formats)):
+                         yield _merge(pair)
  
-         return (new_format_spec, new_formats)
+             filters = [self._build_format_filter(f) for f in selector.filters]
  
-     def select_format(self, format_spec, available_formats):
-         while format_spec.endswith(']'):
-             format_spec, available_formats = self._apply_format_filter(
-                 format_spec, available_formats)
-         if not available_formats:
-             return None
+             def final_selector(formats):
+                 for _filter in filters:
+                     formats = list(filter(_filter, formats))
+                 return selector_function(formats)
+             return final_selector
  
-         if format_spec in ['best', 'worst', None]:
-             format_idx = 0 if format_spec == 'worst' else -1
-             audiovideo_formats = [
-                 f for f in available_formats
-                 if f.get('vcodec') != 'none' and f.get('acodec') != 'none']
-             if audiovideo_formats:
-                 return audiovideo_formats[format_idx]
-             # for audio only (soundcloud) or video only (imgur) urls, select the best/worst audio format
-             elif (all(f.get('acodec') != 'none' for f in available_formats) or
-                   all(f.get('vcodec') != 'none' for f in available_formats)):
-                 return available_formats[format_idx]
-         elif format_spec == 'bestaudio':
-             audio_formats = [
-                 f for f in available_formats
-                 if f.get('vcodec') == 'none']
-             if audio_formats:
-                 return audio_formats[-1]
-         elif format_spec == 'worstaudio':
-             audio_formats = [
-                 f for f in available_formats
-                 if f.get('vcodec') == 'none']
-             if audio_formats:
-                 return audio_formats[0]
-         elif format_spec == 'bestvideo':
-             video_formats = [
-                 f for f in available_formats
-                 if f.get('acodec') == 'none']
-             if video_formats:
-                 return video_formats[-1]
-         elif format_spec == 'worstvideo':
-             video_formats = [
-                 f for f in available_formats
-                 if f.get('acodec') == 'none']
-             if video_formats:
-                 return video_formats[0]
-         else:
-             extensions = ['mp4', 'flv', 'webm', '3gp', 'm4a', 'mp3', 'ogg', 'aac', 'wav']
-             if format_spec in extensions:
-                 filter_f = lambda f: f['ext'] == format_spec
-             else:
-                 filter_f = lambda f: f['format_id'] == format_spec
-             matches = list(filter(filter_f, available_formats))
-             if matches:
-                 return matches[-1]
-         return None
+         stream = io.BytesIO(format_spec.encode('utf-8'))
+         try:
+             tokens = list(compat_tokenize_tokenize(stream.readline))
+         except tokenize.TokenError:
+             raise syntax_error('Missing closing/opening brackets or parenthesis', (0, len(format_spec)))
+         class TokenIterator(object):
+             def __init__(self, tokens):
+                 self.tokens = tokens
+                 self.counter = 0
+             def __iter__(self):
+                 return self
+             def __next__(self):
+                 if self.counter >= len(self.tokens):
+                     raise StopIteration()
+                 value = self.tokens[self.counter]
+                 self.counter += 1
+                 return value
+             next = __next__
+             def restore_last_token(self):
+                 self.counter -= 1
+         parsed_selector = _parse_format_selection(iter(TokenIterator(tokens)))
+         return _build_selector_function(parsed_selector)
  
      def _calc_headers(self, info_dict):
          res = std_headers.copy()
                  t.get('preference'), t.get('width'), t.get('height'),
                  t.get('id'), t.get('url')))
              for i, t in enumerate(thumbnails):
 -                if 'width' in t and 'height' in t:
 +                if t.get('width') and t.get('height'):
                      t['resolution'] = '%dx%d' % (t['width'], t['height'])
                  if t.get('id') is None:
                      t['id'] = '%d' % i
          if req_format is None:
              req_format_list = []
              if (self.params.get('outtmpl', DEFAULT_OUTTMPL) != '-' and
 -                    info_dict['extractor'] in ['youtube', 'ted']):
 +                    info_dict['extractor'] in ['youtube', 'ted'] and
 +                    not info_dict.get('is_live')):
                  merger = FFmpegMergerPP(self)
                  if merger.available and merger.can_merge():
                      req_format_list.append('bestvideo+bestaudio')
              req_format_list.append('best')
              req_format = '/'.join(req_format_list)
-         formats_to_download = []
-         if req_format == 'all':
-             formats_to_download = formats
-         else:
-             for rfstr in req_format.split(','):
-                 # We can accept formats requested in the format: 34/5/best, we pick
-                 # the first that is available, starting from left
-                 req_formats = rfstr.split('/')
-                 for rf in req_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):
-                             # The first format must contain the video and the
-                             # second the audio
-                             if formats_info[0].get('vcodec') == 'none':
-                                 self.report_error('The first format must '
-                                                   'contain the video, try using '
-                                                   '"-f %s+%s"' % (format_2, format_1))
-                                 return
-                             output_ext = (
-                                 formats_info[0]['ext']
-                                 if self.params.get('merge_output_format') is None
-                                 else self.params['merge_output_format'])
-                             selected_format = {
-                                 'requested_formats': formats_info,
-                                 'format': '%s+%s' % (formats_info[0].get('format'),
-                                                      formats_info[1].get('format')),
-                                 'format_id': '%s+%s' % (formats_info[0].get('format_id'),
-                                                         formats_info[1].get('format_id')),
-                                 'width': formats_info[0].get('width'),
-                                 'height': formats_info[0].get('height'),
-                                 'resolution': formats_info[0].get('resolution'),
-                                 'fps': formats_info[0].get('fps'),
-                                 'vcodec': formats_info[0].get('vcodec'),
-                                 'vbr': formats_info[0].get('vbr'),
-                                 'stretched_ratio': formats_info[0].get('stretched_ratio'),
-                                 'acodec': formats_info[1].get('acodec'),
-                                 'abr': formats_info[1].get('abr'),
-                                 'ext': output_ext,
-                             }
-                         else:
-                             selected_format = None
-                     else:
-                         selected_format = self.select_format(rf, formats)
-                     if selected_format is not None:
-                         formats_to_download.append(selected_format)
-                         break
+         format_selector = self.build_format_selector(req_format)
+         formats_to_download = list(format_selector(formats))
          if not formats_to_download:
              raise ExtractorError('requested format not available',
                                   expected=True)
diff --combined youtube_dl/compat.py
index e4b9286c06e12d967f60b5fbcf2c691802684163,bc218dd719074204b3e94e7ec124f02914b28f51..ace5bd716aac0161b356625260f1138687568314
@@@ -9,7 -9,6 +9,7 @@@ import shuti
  import socket
  import subprocess
  import sys
 +import itertools
  
  
  try:
@@@ -42,11 -41,6 +42,11 @@@ try
  except ImportError:  # Python 2
      import cookielib as compat_cookiejar
  
 +try:
 +    import http.cookies as compat_cookies
 +except ImportError:  # Python 2
 +    import Cookie as compat_cookies
 +
  try:
      import html.entities as compat_html_entities
  except ImportError:  # Python 2
@@@ -80,74 -74,42 +80,74 @@@ except ImportError
      import BaseHTTPServer as compat_http_server
  
  try:
 +    from urllib.parse import unquote_to_bytes as compat_urllib_parse_unquote_to_bytes
      from urllib.parse import unquote as compat_urllib_parse_unquote
 -except ImportError:
 -    def compat_urllib_parse_unquote(string, encoding='utf-8', errors='replace'):
 -        if string == '':
 +    from urllib.parse import unquote_plus as compat_urllib_parse_unquote_plus
 +except ImportError:  # Python 2
 +    _asciire = (compat_urllib_parse._asciire if hasattr(compat_urllib_parse, '_asciire')
 +                else re.compile('([\x00-\x7f]+)'))
 +
 +    # HACK: The following are the correct unquote_to_bytes, unquote and unquote_plus
 +    # implementations from cpython 3.4.3's stdlib. Python 2's version
 +    # is apparently broken (see https://github.com/rg3/youtube-dl/pull/6244)
 +
 +    def compat_urllib_parse_unquote_to_bytes(string):
 +        """unquote_to_bytes('abc%20def') -> b'abc def'."""
 +        # Note: strings are encoded as UTF-8. This is only an issue if it contains
 +        # unescaped non-ASCII characters, which URIs should not.
 +        if not string:
 +            # Is it a string-like object?
 +            string.split
 +            return b''
 +        if isinstance(string, unicode):
 +            string = string.encode('utf-8')
 +        bits = string.split(b'%')
 +        if len(bits) == 1:
              return string
 -        res = string.split('%')
 -        if len(res) == 1:
 +        res = [bits[0]]
 +        append = res.append
 +        for item in bits[1:]:
 +            try:
 +                append(compat_urllib_parse._hextochr[item[:2]])
 +                append(item[2:])
 +            except KeyError:
 +                append(b'%')
 +                append(item)
 +        return b''.join(res)
 +
 +    def compat_urllib_parse_unquote(string, encoding='utf-8', errors='replace'):
 +        """Replace %xx escapes by their single-character equivalent. The optional
 +        encoding and errors parameters specify how to decode percent-encoded
 +        sequences into Unicode characters, as accepted by the bytes.decode()
 +        method.
 +        By default, percent-encoded sequences are decoded with UTF-8, and invalid
 +        sequences are replaced by a placeholder character.
 +
 +        unquote('abc%20def') -> 'abc def'.
 +        """
 +        if '%' not in string:
 +            string.split
              return string
          if encoding is None:
              encoding = 'utf-8'
          if errors is None:
              errors = 'replace'
 -        # pct_sequence: contiguous sequence of percent-encoded bytes, decoded
 -        pct_sequence = b''
 -        string = res[0]
 -        for item in res[1:]:
 -            try:
 -                if not item:
 -                    raise ValueError
 -                pct_sequence += item[:2].decode('hex')
 -                rest = item[2:]
 -                if not rest:
 -                    # This segment was just a single percent-encoded character.
 -                    # May be part of a sequence of code units, so delay decoding.
 -                    # (Stored in pct_sequence).
 -                    continue
 -            except ValueError:
 -                rest = '%' + item
 -            # Encountered non-percent-encoded characters. Flush the current
 -            # pct_sequence.
 -            string += pct_sequence.decode(encoding, errors) + rest
 -            pct_sequence = b''
 -        if pct_sequence:
 -            # Flush the final pct_sequence
 -            string += pct_sequence.decode(encoding, errors)
 -        return string
 +        bits = _asciire.split(string)
 +        res = [bits[0]]
 +        append = res.append
 +        for i in range(1, len(bits), 2):
 +            append(compat_urllib_parse_unquote_to_bytes(bits[i]).decode(encoding, errors))
 +            append(bits[i + 1])
 +        return ''.join(res)
 +
 +    def compat_urllib_parse_unquote_plus(string, encoding='utf-8', errors='replace'):
 +        """Like unquote(), but also replace plus signs by spaces, as required for
 +        unquoting HTML form values.
 +
 +        unquote_plus('%7e/abc+def') -> '~/abc def'
 +        """
 +        string = string.replace('+', ' ')
 +        return compat_urllib_parse_unquote(string, encoding, errors)
  
  try:
      compat_str = unicode  # Python 2
@@@ -426,22 -388,16 +426,27 @@@ else
              pass
          return _terminal_size(columns, lines)
  
 +try:
 +    itertools.count(start=0, step=1)
 +    compat_itertools_count = itertools.count
 +except TypeError:  # Python 2.6
 +    def compat_itertools_count(start=0, step=1):
 +        n = start
 +        while True:
 +            yield n
 +            n += step
 +
+ if sys.version_info >= (3, 0):
+     from tokenize import tokenize as compat_tokenize_tokenize
+ else:
+     from tokenize import generate_tokens as compat_tokenize_tokenize
  __all__ = [
      'compat_HTTPError',
      'compat_basestring',
      'compat_chr',
      'compat_cookiejar',
 +    'compat_cookies',
      'compat_expanduser',
      'compat_get_terminal_size',
      'compat_getenv',
      'compat_html_entities',
      'compat_http_client',
      'compat_http_server',
 +    'compat_itertools_count',
      'compat_kwargs',
      'compat_ord',
      'compat_parse_qs',
      'compat_socket_create_connection',
      'compat_str',
      'compat_subprocess_get_DEVNULL',
+     'compat_tokenize_tokenize',
      'compat_urllib_error',
      'compat_urllib_parse',
      'compat_urllib_parse_unquote',
 +    'compat_urllib_parse_unquote_plus',
 +    'compat_urllib_parse_unquote_to_bytes',
      'compat_urllib_parse_urlparse',
      'compat_urllib_request',
      'compat_urlparse',