Add interactive format selection with `-f -`

Closes #2065
This commit is contained in:
pukkandan 2021-12-21 17:02:13 +05:30
parent 1cefca9e44
commit fa9f30b802
No known key found for this signature in database
GPG Key ID: 0F00D95A001F4698
2 changed files with 58 additions and 37 deletions

View File

@ -1290,6 +1290,8 @@ The simplest case is requesting a specific format, for example with `-f 22` you
You can also use a file extension (currently `3gp`, `aac`, `flv`, `m4a`, `mp3`, `mp4`, `ogg`, `wav`, `webm` are supported) to download the best quality format of a particular file extension served as a single file, e.g. `-f webm` will download the best quality format with the `webm` extension served as a single file. You can also use a file extension (currently `3gp`, `aac`, `flv`, `m4a`, `mp3`, `mp4`, `ogg`, `wav`, `webm` are supported) to download the best quality format of a particular file extension served as a single file, e.g. `-f webm` will download the best quality format with the `webm` extension served as a single file.
You can use `-f -` to interactively provide the format selector *for each video*
You can also use special names to select particular edge case formats: You can also use special names to select particular edge case formats:
- `all`: Select **all formats** separately - `all`: Select **all formats** separately

View File

@ -624,7 +624,7 @@ class YoutubeDL(object):
# Creating format selector here allows us to catch syntax errors before the extraction # Creating format selector here allows us to catch syntax errors before the extraction
self.format_selector = ( self.format_selector = (
None if self.params.get('format') is None self.params.get('format') if self.params.get('format') in (None, '-')
else self.params['format'] if callable(self.params['format']) else self.params['format'] if callable(self.params['format'])
else self.build_format_selector(self.params['format'])) else self.build_format_selector(self.params['format']))
@ -818,14 +818,15 @@ class YoutubeDL(object):
if self.params.get('cookiefile') is not None: if self.params.get('cookiefile') is not None:
self.cookiejar.save(ignore_discard=True, ignore_expires=True) self.cookiejar.save(ignore_discard=True, ignore_expires=True)
def trouble(self, message=None, tb=None): def trouble(self, message=None, tb=None, is_error=True):
"""Determine action to take when a download problem appears. """Determine action to take when a download problem appears.
Depending on if the downloader has been configured to ignore Depending on if the downloader has been configured to ignore
download errors or not, this method may throw an exception or download errors or not, this method may throw an exception or
not when errors are found, after printing the message. not when errors are found, after printing the message.
tb, if given, is additional traceback information. @param tb If given, is additional traceback information
@param is_error Whether to raise error according to ignorerrors
""" """
if message is not None: if message is not None:
self.to_stderr(message) self.to_stderr(message)
@ -841,6 +842,8 @@ class YoutubeDL(object):
tb = ''.join(tb_data) tb = ''.join(tb_data)
if tb: if tb:
self.to_stderr(tb) self.to_stderr(tb)
if not is_error:
return
if not self.params.get('ignoreerrors'): if not self.params.get('ignoreerrors'):
if sys.exc_info()[0] and hasattr(sys.exc_info()[1], 'exc_info') and sys.exc_info()[1].exc_info[0]: if sys.exc_info()[0] and hasattr(sys.exc_info()[1], 'exc_info') and sys.exc_info()[1].exc_info[0]:
exc_info = sys.exc_info()[1].exc_info exc_info = sys.exc_info()[1].exc_info
@ -900,12 +903,12 @@ class YoutubeDL(object):
else: else:
self.to_stderr(f'{self._format_err("DeprecationWarning:", self.Styles.ERROR)} {message}', True) self.to_stderr(f'{self._format_err("DeprecationWarning:", self.Styles.ERROR)} {message}', True)
def report_error(self, message, tb=None): def report_error(self, message, *args, **kwargs):
''' '''
Do the same as trouble, but prefixes the message with 'ERROR:', colored Do the same as trouble, but prefixes the message with 'ERROR:', colored
in red if stderr is a tty file. in red if stderr is a tty file.
''' '''
self.trouble(f'{self._format_err("ERROR:", self.Styles.ERROR)} {message}', tb) self.trouble(f'{self._format_err("ERROR:", self.Styles.ERROR)} {message}', *args, **kwargs)
def write_debug(self, message, only_once=False): def write_debug(self, message, only_once=False):
'''Log debug message or Print message to stderr''' '''Log debug message or Print message to stderr'''
@ -2448,20 +2451,21 @@ class YoutubeDL(object):
# The pre-processors may have modified the formats # The pre-processors may have modified the formats
formats = info_dict.get('formats', [info_dict]) formats = info_dict.get('formats', [info_dict])
list_only = self.params.get('simulate') is None and (
self.params.get('list_thumbnails') or self.params.get('listformats') or self.params.get('listsubtitles'))
interactive_format_selection = not list_only and self.format_selector == '-'
if self.params.get('list_thumbnails'): if self.params.get('list_thumbnails'):
self.list_thumbnails(info_dict) self.list_thumbnails(info_dict)
if self.params.get('listformats'):
if not info_dict.get('formats') and not info_dict.get('url'):
self.to_screen('%s has no formats' % info_dict['id'])
else:
self.list_formats(info_dict)
if self.params.get('listsubtitles'): if self.params.get('listsubtitles'):
if 'automatic_captions' in info_dict: if 'automatic_captions' in info_dict:
self.list_subtitles( self.list_subtitles(
info_dict['id'], automatic_captions, 'automatic captions') info_dict['id'], automatic_captions, 'automatic captions')
self.list_subtitles(info_dict['id'], subtitles, 'subtitles') self.list_subtitles(info_dict['id'], subtitles, 'subtitles')
list_only = self.params.get('simulate') is None and ( if self.params.get('listformats') or interactive_format_selection:
self.params.get('list_thumbnails') or self.params.get('listformats') or self.params.get('listsubtitles')) if not info_dict.get('formats') and not info_dict.get('url'):
self.to_screen('%s has no formats' % info_dict['id'])
else:
self.list_formats(info_dict)
if list_only: if list_only:
# Without this printing, -F --print-json will not work # Without this printing, -F --print-json will not work
self.__forced_printings(info_dict, self.prepare_filename(info_dict), incomplete=True) self.__forced_printings(info_dict, self.prepare_filename(info_dict), incomplete=True)
@ -2473,33 +2477,48 @@ class YoutubeDL(object):
self.write_debug('Default format spec: %s' % req_format) self.write_debug('Default format spec: %s' % req_format)
format_selector = self.build_format_selector(req_format) format_selector = self.build_format_selector(req_format)
# While in format selection we may need to have an access to the original while True:
# format set in order to calculate some metrics or do some processing. if interactive_format_selection:
# For now we need to be able to guess whether original formats provided req_format = input(
# by extractor are incomplete or not (i.e. whether extractor provides only self._format_screen('\nEnter format selector: ', self.Styles.EMPHASIS))
# video-only or audio-only formats) for proper formats selection for try:
# extractors with such incomplete formats (see format_selector = self.build_format_selector(req_format)
# https://github.com/ytdl-org/youtube-dl/pull/5556). except SyntaxError as err:
# Since formats may be filtered during format selection and may not match self.report_error(err, tb=False, is_error=False)
# the original formats the results may be incorrect. Thus original formats continue
# or pre-calculated metrics should be passed to format selection routines
# as well.
# We will pass a context object containing all necessary additional data
# instead of just formats.
# This fixes incorrect format selection issue (see
# https://github.com/ytdl-org/youtube-dl/issues/10083).
incomplete_formats = (
# All formats are video-only or
all(f.get('vcodec') != 'none' and f.get('acodec') == 'none' for f in formats)
# all formats are audio-only
or all(f.get('vcodec') == 'none' and f.get('acodec') != 'none' for f in formats))
ctx = { # While in format selection we may need to have an access to the original
'formats': formats, # format set in order to calculate some metrics or do some processing.
'incomplete_formats': incomplete_formats, # For now we need to be able to guess whether original formats provided
} # by extractor are incomplete or not (i.e. whether extractor provides only
# video-only or audio-only formats) for proper formats selection for
# extractors with such incomplete formats (see
# https://github.com/ytdl-org/youtube-dl/pull/5556).
# Since formats may be filtered during format selection and may not match
# the original formats the results may be incorrect. Thus original formats
# or pre-calculated metrics should be passed to format selection routines
# as well.
# We will pass a context object containing all necessary additional data
# instead of just formats.
# This fixes incorrect format selection issue (see
# https://github.com/ytdl-org/youtube-dl/issues/10083).
incomplete_formats = (
# All formats are video-only or
all(f.get('vcodec') != 'none' and f.get('acodec') == 'none' for f in formats)
# all formats are audio-only
or all(f.get('vcodec') == 'none' and f.get('acodec') != 'none' for f in formats))
ctx = {
'formats': formats,
'incomplete_formats': incomplete_formats,
}
formats_to_download = list(format_selector(ctx))
if interactive_format_selection and not formats_to_download:
self.report_error('Requested format is not available', tb=False, is_error=False)
continue
break
formats_to_download = list(format_selector(ctx))
if not formats_to_download: if not formats_to_download:
if not self.params.get('ignore_no_formats_error'): if not self.params.get('ignore_no_formats_error'):
raise ExtractorError('Requested format is not available', expected=True, raise ExtractorError('Requested format is not available', expected=True,