mirror of https://github.com/yt-dlp/yt-dlp.git
Merge remote-tracking branch 'jaimemf/format_spec_groups' (closes #6124)
This commit is contained in:
commit
03950c90f7
|
@ -289,7 +289,7 @@ youtube-dl_test_video_.mp4 # A simple file name
|
||||||
By default youtube-dl tries to download the best quality, but sometimes you may want to download other format.
|
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`.
|
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.
|
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.
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ from youtube_dl import YoutubeDL
|
||||||
from youtube_dl.compat import compat_str
|
from youtube_dl.compat import compat_str
|
||||||
from youtube_dl.extractor import YoutubeIE
|
from youtube_dl.extractor import YoutubeIE
|
||||||
from youtube_dl.postprocessor.common import PostProcessor
|
from youtube_dl.postprocessor.common import PostProcessor
|
||||||
from youtube_dl.utils import match_filter_func
|
from youtube_dl.utils import ExtractorError, match_filter_func
|
||||||
|
|
||||||
TEST_URL = 'http://localhost/sample.mp4'
|
TEST_URL = 'http://localhost/sample.mp4'
|
||||||
|
|
||||||
|
@ -229,21 +229,70 @@ class TestFormatSelection(unittest.TestCase):
|
||||||
'141', '172', '140', '171', '139',
|
'141', '172', '140', '171', '139',
|
||||||
]
|
]
|
||||||
|
|
||||||
for f1id, f2id in zip(order, order[1:]):
|
def format_info(f_id):
|
||||||
f1 = YoutubeIE._formats[f1id].copy()
|
info = YoutubeIE._formats[f_id].copy()
|
||||||
f1['format_id'] = f1id
|
info['format_id'] = f_id
|
||||||
f1['url'] = 'url:' + f1id
|
info['url'] = 'url:' + f_id
|
||||||
f2 = YoutubeIE._formats[f2id].copy()
|
return info
|
||||||
f2['format_id'] = f2id
|
formats_order = [format_info(f_id) for f_id in order]
|
||||||
f2['url'] = 'url:' + f2id
|
|
||||||
|
|
||||||
|
info_dict = _make_result(list(formats_order), extractor='youtube')
|
||||||
|
ydl = YDL({'format': 'bestvideo+bestaudio'})
|
||||||
|
yie = YoutubeIE(ydl)
|
||||||
|
yie._sort_formats(info_dict['formats'])
|
||||||
|
ydl.process_ie_result(info_dict)
|
||||||
|
downloaded = ydl.downloaded_info_dicts[0]
|
||||||
|
self.assertEqual(downloaded['format_id'], '137+141')
|
||||||
|
self.assertEqual(downloaded['ext'], 'mp4')
|
||||||
|
|
||||||
|
info_dict = _make_result(list(formats_order), extractor='youtube')
|
||||||
|
ydl = YDL({'format': 'bestvideo[height>=999999]+bestaudio/best'})
|
||||||
|
yie = YoutubeIE(ydl)
|
||||||
|
yie._sort_formats(info_dict['formats'])
|
||||||
|
ydl.process_ie_result(info_dict)
|
||||||
|
downloaded = ydl.downloaded_info_dicts[0]
|
||||||
|
self.assertEqual(downloaded['format_id'], '38')
|
||||||
|
|
||||||
|
info_dict = _make_result(list(formats_order), extractor='youtube')
|
||||||
|
ydl = YDL({'format': 'bestvideo/best,bestaudio'})
|
||||||
|
yie = YoutubeIE(ydl)
|
||||||
|
yie._sort_formats(info_dict['formats'])
|
||||||
|
ydl.process_ie_result(info_dict)
|
||||||
|
downloaded_ids = [info['format_id'] for info in ydl.downloaded_info_dicts]
|
||||||
|
self.assertEqual(downloaded_ids, ['137', '141'])
|
||||||
|
|
||||||
|
info_dict = _make_result(list(formats_order), extractor='youtube')
|
||||||
|
ydl = YDL({'format': '(bestvideo[ext=mp4],bestvideo[ext=webm])+bestaudio'})
|
||||||
|
yie = YoutubeIE(ydl)
|
||||||
|
yie._sort_formats(info_dict['formats'])
|
||||||
|
ydl.process_ie_result(info_dict)
|
||||||
|
downloaded_ids = [info['format_id'] for info in ydl.downloaded_info_dicts]
|
||||||
|
self.assertEqual(downloaded_ids, ['137+141', '248+141'])
|
||||||
|
|
||||||
|
info_dict = _make_result(list(formats_order), extractor='youtube')
|
||||||
|
ydl = YDL({'format': '(bestvideo[ext=mp4],bestvideo[ext=webm])[height<=720]+bestaudio'})
|
||||||
|
yie = YoutubeIE(ydl)
|
||||||
|
yie._sort_formats(info_dict['formats'])
|
||||||
|
ydl.process_ie_result(info_dict)
|
||||||
|
downloaded_ids = [info['format_id'] for info in ydl.downloaded_info_dicts]
|
||||||
|
self.assertEqual(downloaded_ids, ['136+141', '247+141'])
|
||||||
|
|
||||||
|
info_dict = _make_result(list(formats_order), extractor='youtube')
|
||||||
|
ydl = YDL({'format': '(bestvideo[ext=none]/bestvideo[ext=webm])+bestaudio'})
|
||||||
|
yie = YoutubeIE(ydl)
|
||||||
|
yie._sort_formats(info_dict['formats'])
|
||||||
|
ydl.process_ie_result(info_dict)
|
||||||
|
downloaded_ids = [info['format_id'] for info in ydl.downloaded_info_dicts]
|
||||||
|
self.assertEqual(downloaded_ids, ['248+141'])
|
||||||
|
|
||||||
|
for f1, f2 in zip(formats_order, formats_order[1:]):
|
||||||
info_dict = _make_result([f1, f2], extractor='youtube')
|
info_dict = _make_result([f1, f2], extractor='youtube')
|
||||||
ydl = YDL({'format': 'best/bestvideo'})
|
ydl = YDL({'format': 'best/bestvideo'})
|
||||||
yie = YoutubeIE(ydl)
|
yie = YoutubeIE(ydl)
|
||||||
yie._sort_formats(info_dict['formats'])
|
yie._sort_formats(info_dict['formats'])
|
||||||
ydl.process_ie_result(info_dict)
|
ydl.process_ie_result(info_dict)
|
||||||
downloaded = ydl.downloaded_info_dicts[0]
|
downloaded = ydl.downloaded_info_dicts[0]
|
||||||
self.assertEqual(downloaded['format_id'], f1id)
|
self.assertEqual(downloaded['format_id'], f1['format_id'])
|
||||||
|
|
||||||
info_dict = _make_result([f2, f1], extractor='youtube')
|
info_dict = _make_result([f2, f1], extractor='youtube')
|
||||||
ydl = YDL({'format': 'best/bestvideo'})
|
ydl = YDL({'format': 'best/bestvideo'})
|
||||||
|
@ -251,7 +300,17 @@ class TestFormatSelection(unittest.TestCase):
|
||||||
yie._sort_formats(info_dict['formats'])
|
yie._sort_formats(info_dict['formats'])
|
||||||
ydl.process_ie_result(info_dict)
|
ydl.process_ie_result(info_dict)
|
||||||
downloaded = ydl.downloaded_info_dicts[0]
|
downloaded = ydl.downloaded_info_dicts[0]
|
||||||
self.assertEqual(downloaded['format_id'], f1id)
|
self.assertEqual(downloaded['format_id'], f1['format_id'])
|
||||||
|
|
||||||
|
def test_invalid_format_specs(self):
|
||||||
|
def assert_syntax_error(format_spec):
|
||||||
|
ydl = YDL({'format': format_spec})
|
||||||
|
info_dict = _make_result([{'format_id': 'foo', 'url': TEST_URL}])
|
||||||
|
self.assertRaises(SyntaxError, ydl.process_ie_result, info_dict)
|
||||||
|
|
||||||
|
assert_syntax_error('bestvideo,,best')
|
||||||
|
assert_syntax_error('+bestaudio')
|
||||||
|
assert_syntax_error('bestvideo+')
|
||||||
|
|
||||||
def test_format_filtering(self):
|
def test_format_filtering(self):
|
||||||
formats = [
|
formats = [
|
||||||
|
@ -308,6 +367,18 @@ class TestFormatSelection(unittest.TestCase):
|
||||||
downloaded = ydl.downloaded_info_dicts[0]
|
downloaded = ydl.downloaded_info_dicts[0]
|
||||||
self.assertEqual(downloaded['format_id'], 'G')
|
self.assertEqual(downloaded['format_id'], 'G')
|
||||||
|
|
||||||
|
ydl = YDL({'format': 'all[width>=400][width<=600]'})
|
||||||
|
ydl.process_ie_result(info_dict)
|
||||||
|
downloaded_ids = [info['format_id'] for info in ydl.downloaded_info_dicts]
|
||||||
|
self.assertEqual(downloaded_ids, ['B', 'C', 'D'])
|
||||||
|
|
||||||
|
ydl = YDL({'format': 'best[height<40]'})
|
||||||
|
try:
|
||||||
|
ydl.process_ie_result(info_dict)
|
||||||
|
except ExtractorError:
|
||||||
|
pass
|
||||||
|
self.assertEqual(ydl.downloaded_info_dicts, [])
|
||||||
|
|
||||||
|
|
||||||
class TestYoutubeDL(unittest.TestCase):
|
class TestYoutubeDL(unittest.TestCase):
|
||||||
def test_subtitles(self):
|
def test_subtitles(self):
|
||||||
|
|
|
@ -21,6 +21,7 @@ import subprocess
|
||||||
import socket
|
import socket
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
import tokenize
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
if os.name == 'nt':
|
if os.name == 'nt':
|
||||||
|
@ -34,6 +35,7 @@ from .compat import (
|
||||||
compat_http_client,
|
compat_http_client,
|
||||||
compat_kwargs,
|
compat_kwargs,
|
||||||
compat_str,
|
compat_str,
|
||||||
|
compat_tokenize_tokenize,
|
||||||
compat_urllib_error,
|
compat_urllib_error,
|
||||||
compat_urllib_request,
|
compat_urllib_request,
|
||||||
)
|
)
|
||||||
|
@ -853,8 +855,8 @@ class YoutubeDL(object):
|
||||||
else:
|
else:
|
||||||
raise Exception('Invalid result type: %s' % result_type)
|
raise Exception('Invalid result type: %s' % result_type)
|
||||||
|
|
||||||
def _apply_format_filter(self, format_spec, available_formats):
|
def _build_format_filter(self, filter_spec):
|
||||||
" Returns a tuple of the remaining format_spec and filtered formats "
|
" Returns a function to filter the formats according to the filter_spec "
|
||||||
|
|
||||||
OPERATORS = {
|
OPERATORS = {
|
||||||
'<': operator.lt,
|
'<': operator.lt,
|
||||||
|
@ -864,13 +866,13 @@ class YoutubeDL(object):
|
||||||
'=': operator.eq,
|
'=': operator.eq,
|
||||||
'!=': operator.ne,
|
'!=': 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)
|
(?P<key>width|height|tbr|abr|vbr|asr|filesize|fps)
|
||||||
\s*(?P<op>%s)(?P<none_inclusive>\s*\?)?\s*
|
\s*(?P<op>%s)(?P<none_inclusive>\s*\?)?\s*
|
||||||
(?P<value>[0-9.]+(?:[kKmMgGtTpPeEzZyY]i?[Bb]?)?)
|
(?P<value>[0-9.]+(?:[kKmMgGtTpPeEzZyY]i?[Bb]?)?)
|
||||||
\]$
|
$
|
||||||
''' % '|'.join(map(re.escape, OPERATORS.keys())))
|
''' % '|'.join(map(re.escape, OPERATORS.keys())))
|
||||||
m = operator_rex.search(format_spec)
|
m = operator_rex.search(filter_spec)
|
||||||
if m:
|
if m:
|
||||||
try:
|
try:
|
||||||
comparison_value = int(m.group('value'))
|
comparison_value = int(m.group('value'))
|
||||||
|
@ -881,7 +883,7 @@ class YoutubeDL(object):
|
||||||
if comparison_value is None:
|
if comparison_value is None:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
'Invalid value %r in format specification %r' % (
|
'Invalid value %r in format specification %r' % (
|
||||||
m.group('value'), format_spec))
|
m.group('value'), filter_spec))
|
||||||
op = OPERATORS[m.group('op')]
|
op = OPERATORS[m.group('op')]
|
||||||
|
|
||||||
if not m:
|
if not m:
|
||||||
|
@ -889,85 +891,250 @@ class YoutubeDL(object):
|
||||||
'=': operator.eq,
|
'=': operator.eq,
|
||||||
'!=': operator.ne,
|
'!=': 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<key>ext|acodec|vcodec|container|protocol)
|
||||||
\s*(?P<op>%s)(?P<none_inclusive>\s*\?)?
|
\s*(?P<op>%s)(?P<none_inclusive>\s*\?)?
|
||||||
\s*(?P<value>[a-zA-Z0-9_-]+)
|
\s*(?P<value>[a-zA-Z0-9_-]+)
|
||||||
\s*\]$
|
\s*$
|
||||||
''' % '|'.join(map(re.escape, STR_OPERATORS.keys())))
|
''' % '|'.join(map(re.escape, STR_OPERATORS.keys())))
|
||||||
m = str_operator_rex.search(format_spec)
|
m = str_operator_rex.search(filter_spec)
|
||||||
if m:
|
if m:
|
||||||
comparison_value = m.group('value')
|
comparison_value = m.group('value')
|
||||||
op = STR_OPERATORS[m.group('op')]
|
op = STR_OPERATORS[m.group('op')]
|
||||||
|
|
||||||
if not m:
|
if not m:
|
||||||
raise ValueError('Invalid format specification %r' % format_spec)
|
raise ValueError('Invalid filter specification %r' % filter_spec)
|
||||||
|
|
||||||
def _filter(f):
|
def _filter(f):
|
||||||
actual_value = f.get(m.group('key'))
|
actual_value = f.get(m.group('key'))
|
||||||
if actual_value is None:
|
if actual_value is None:
|
||||||
return m.group('none_inclusive')
|
return m.group('none_inclusive')
|
||||||
return op(actual_value, comparison_value)
|
return op(actual_value, comparison_value)
|
||||||
new_formats = [f for f in available_formats if _filter(f)]
|
return _filter
|
||||||
|
|
||||||
new_format_spec = format_spec[:-len(m.group(0))]
|
def build_format_selector(self, format_spec):
|
||||||
if not new_format_spec:
|
def syntax_error(note, start):
|
||||||
new_format_spec = 'best'
|
message = (
|
||||||
|
'Invalid format specification: '
|
||||||
|
'{0}\n\t{1}\n\t{2}^'.format(note, format_spec, ' ' * start[1]))
|
||||||
|
return SyntaxError(message)
|
||||||
|
|
||||||
return (new_format_spec, new_formats)
|
PICKFIRST = 'PICKFIRST'
|
||||||
|
MERGE = 'MERGE'
|
||||||
|
SINGLE = 'SINGLE'
|
||||||
|
GROUP = 'GROUP'
|
||||||
|
FormatSelector = collections.namedtuple('FormatSelector', ['type', 'selector', 'filters'])
|
||||||
|
|
||||||
def select_format(self, format_spec, available_formats):
|
def _parse_filter(tokens):
|
||||||
while format_spec.endswith(']'):
|
filter_parts = []
|
||||||
format_spec, available_formats = self._apply_format_filter(
|
for type, string, start, _, _ in tokens:
|
||||||
format_spec, available_formats)
|
if type == tokenize.OP and string == ']':
|
||||||
if not available_formats:
|
return ''.join(filter_parts)
|
||||||
return None
|
else:
|
||||||
|
filter_parts.append(string)
|
||||||
|
|
||||||
if format_spec in ['best', 'worst', None]:
|
def _parse_format_selection(tokens, inside_merge=False, inside_choice=False, inside_group=False):
|
||||||
format_idx = 0 if format_spec == 'worst' else -1
|
selectors = []
|
||||||
audiovideo_formats = [
|
current_selector = None
|
||||||
f for f in available_formats
|
for type, string, start, _, _ in tokens:
|
||||||
if f.get('vcodec') != 'none' and f.get('acodec') != 'none']
|
# ENCODING is only defined in python 3.x
|
||||||
if audiovideo_formats:
|
if type == getattr(tokenize, 'ENCODING', None):
|
||||||
return audiovideo_formats[format_idx]
|
continue
|
||||||
# for audio only (soundcloud) or video only (imgur) urls, select the best/worst audio format
|
elif type in [tokenize.NAME, tokenize.NUMBER]:
|
||||||
elif (all(f.get('acodec') != 'none' for f in available_formats) or
|
current_selector = FormatSelector(SINGLE, string, [])
|
||||||
all(f.get('vcodec') != 'none' for f in available_formats)):
|
elif type == tokenize.OP:
|
||||||
return available_formats[format_idx]
|
if string == ')':
|
||||||
elif format_spec == 'bestaudio':
|
if not inside_group:
|
||||||
audio_formats = [
|
# ')' will be handled by the parentheses group
|
||||||
f for f in available_formats
|
tokens.restore_last_token()
|
||||||
if f.get('vcodec') == 'none']
|
break
|
||||||
if audio_formats:
|
elif inside_merge and string in ['/', ',']:
|
||||||
return audio_formats[-1]
|
tokens.restore_last_token()
|
||||||
elif format_spec == 'worstaudio':
|
break
|
||||||
audio_formats = [
|
elif inside_choice and string == ',':
|
||||||
f for f in available_formats
|
tokens.restore_last_token()
|
||||||
if f.get('vcodec') == 'none']
|
break
|
||||||
if audio_formats:
|
elif string == ',':
|
||||||
return audio_formats[0]
|
if not current_selector:
|
||||||
elif format_spec == 'bestvideo':
|
raise syntax_error('"," must follow a format selector', start)
|
||||||
video_formats = [
|
selectors.append(current_selector)
|
||||||
f for f in available_formats
|
current_selector = None
|
||||||
if f.get('acodec') == 'none']
|
elif string == '/':
|
||||||
if video_formats:
|
first_choice = current_selector
|
||||||
return video_formats[-1]
|
second_choice = _parse_format_selection(tokens, inside_choice=True)
|
||||||
elif format_spec == 'worstvideo':
|
current_selector = FormatSelector(PICKFIRST, (first_choice, second_choice), [])
|
||||||
video_formats = [
|
elif string == '[':
|
||||||
f for f in available_formats
|
if not current_selector:
|
||||||
if f.get('acodec') == 'none']
|
current_selector = FormatSelector(SINGLE, 'best', [])
|
||||||
if video_formats:
|
format_filter = _parse_filter(tokens)
|
||||||
return video_formats[0]
|
current_selector.filters.append(format_filter)
|
||||||
else:
|
elif string == '(':
|
||||||
extensions = ['mp4', 'flv', 'webm', '3gp', 'm4a', 'mp3', 'ogg', 'aac', 'wav']
|
if current_selector:
|
||||||
if format_spec in extensions:
|
raise syntax_error('Unexpected "("', start)
|
||||||
filter_f = lambda f: f['ext'] == format_spec
|
group = _parse_format_selection(tokens, inside_group=True)
|
||||||
else:
|
current_selector = FormatSelector(GROUP, group, [])
|
||||||
filter_f = lambda f: f['format_id'] == format_spec
|
elif string == '+':
|
||||||
matches = list(filter(filter_f, available_formats))
|
video_selector = current_selector
|
||||||
if matches:
|
audio_selector = _parse_format_selection(tokens, inside_merge=True)
|
||||||
return matches[-1]
|
if not video_selector or not audio_selector:
|
||||||
return None
|
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)
|
||||||
|
|
||||||
|
def selector_function(formats):
|
||||||
|
formats = list(formats)
|
||||||
|
for pair in itertools.product(video_selector(formats), audio_selector(formats)):
|
||||||
|
yield _merge(pair)
|
||||||
|
|
||||||
|
filters = [self._build_format_filter(f) for f in selector.filters]
|
||||||
|
|
||||||
|
def final_selector(formats):
|
||||||
|
for _filter in filters:
|
||||||
|
formats = list(filter(_filter, formats))
|
||||||
|
return selector_function(formats)
|
||||||
|
return final_selector
|
||||||
|
|
||||||
|
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):
|
def _calc_headers(self, info_dict):
|
||||||
res = std_headers.copy()
|
res = std_headers.copy()
|
||||||
|
@ -1111,56 +1278,8 @@ class YoutubeDL(object):
|
||||||
req_format_list.append('bestvideo+bestaudio')
|
req_format_list.append('bestvideo+bestaudio')
|
||||||
req_format_list.append('best')
|
req_format_list.append('best')
|
||||||
req_format = '/'.join(req_format_list)
|
req_format = '/'.join(req_format_list)
|
||||||
formats_to_download = []
|
format_selector = self.build_format_selector(req_format)
|
||||||
if req_format == 'all':
|
formats_to_download = list(format_selector(formats))
|
||||||
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
|
|
||||||
if not formats_to_download:
|
if not formats_to_download:
|
||||||
raise ExtractorError('requested format not available',
|
raise ExtractorError('requested format not available',
|
||||||
expected=True)
|
expected=True)
|
||||||
|
|
|
@ -436,6 +436,11 @@ except TypeError: # Python 2.6
|
||||||
yield n
|
yield n
|
||||||
n += step
|
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__ = [
|
__all__ = [
|
||||||
'compat_HTTPError',
|
'compat_HTTPError',
|
||||||
'compat_basestring',
|
'compat_basestring',
|
||||||
|
@ -457,6 +462,7 @@ __all__ = [
|
||||||
'compat_socket_create_connection',
|
'compat_socket_create_connection',
|
||||||
'compat_str',
|
'compat_str',
|
||||||
'compat_subprocess_get_DEVNULL',
|
'compat_subprocess_get_DEVNULL',
|
||||||
|
'compat_tokenize_tokenize',
|
||||||
'compat_urllib_error',
|
'compat_urllib_error',
|
||||||
'compat_urllib_parse',
|
'compat_urllib_parse',
|
||||||
'compat_urllib_parse_unquote',
|
'compat_urllib_parse_unquote',
|
||||||
|
|
Loading…
Reference in New Issue