Skip to content

Commit

Permalink
Improved progress reporting (See desc) (yt-dlp#1125)
Browse files Browse the repository at this point in the history
* Separate `--console-title` and `--no-progress`
* Add option `--progress` to show progress-bar even in quiet mode
* Fix and refactor `minicurses`
* Use `minicurses` for all progress reporting
* Standardize use of terminal sequences and enable color support for windows 10
* Add option `--progress-template` to customize progress-bar and console-title
* Add postprocessor hooks and progress reporting

Closes: yt-dlp#906, yt-dlp#901, yt-dlp#1085, yt-dlp#1170
  • Loading branch information
pukkandan authored Oct 8, 2021
1 parent fee3f44 commit 819e053
Show file tree
Hide file tree
Showing 14 changed files with 293 additions and 198 deletions.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -604,7 +604,18 @@ Then simply run `make`. You can also run `make yt-dlp` instead to compile only t
(Alias: --force-download-archive)
--newline Output progress bar as new lines
--no-progress Do not print progress bar
--progress Show progress bar, even if in quiet mode
--console-title Display progress in console titlebar
--progress-template [TYPES:]TEMPLATE
Template for progress outputs, optionally
prefixed with one of "download:" (default),
"download-title:" (the console title),
"postprocess:", or "postprocess-title:".
The video's fields are accessible under the
"info" key and the progress attributes are
accessible under "progress" key. Eg:
--console-title --progress-template
"download-title:%(info.id)s-%(progress.eta)s"
-v, --verbose Print various debugging information
--dump-pages Print downloaded pages encoded using base64
to debug problems (very verbose)
Expand Down
3 changes: 1 addition & 2 deletions test/test_YoutubeDL.py
Original file line number Diff line number Diff line change
Expand Up @@ -666,8 +666,7 @@ def test(tmpl, expected, *, info=None, **params):
ydl._num_downloads = 1
self.assertEqual(ydl.validate_outtmpl(tmpl), None)

outtmpl, tmpl_dict = ydl.prepare_outtmpl(tmpl, info or self.outtmpl_info)
out = ydl.escape_outtmpl(outtmpl) % tmpl_dict
out = ydl.evaluate_outtmpl(tmpl, info or self.outtmpl_info)
fname = ydl.prepare_filename(info or self.outtmpl_info)

if not isinstance(expected, (list, tuple)):
Expand Down
77 changes: 48 additions & 29 deletions yt_dlp/YoutubeDL.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
compat_urllib_error,
compat_urllib_request,
compat_urllib_request_DataHandler,
windows_enable_vt_mode,
)
from .cookies import load_cookies
from .utils import (
Expand All @@ -67,8 +68,6 @@
float_or_none,
format_bytes,
format_field,
STR_FORMAT_RE_TMPL,
STR_FORMAT_TYPES,
formatSeconds,
GeoRestrictedError,
HEADRequest,
Expand Down Expand Up @@ -101,9 +100,13 @@
sanitize_url,
sanitized_Request,
std_headers,
STR_FORMAT_RE_TMPL,
STR_FORMAT_TYPES,
str_or_none,
strftime_or_none,
subtitles_filename,
supports_terminal_sequences,
TERMINAL_SEQUENCES,
ThrottledDownload,
to_high_limit_path,
traverse_obj,
Expand Down Expand Up @@ -248,6 +251,7 @@ class YoutubeDL(object):
rejecttitle: Reject downloads for matching titles.
logger: Log messages to a logging.Logger instance.
logtostderr: Log messages to stderr instead of stdout.
consoletitle: Display progress in console window's titlebar.
writedescription: Write the video description to a .description file
writeinfojson: Write the video description to a .info.json file
clean_infojson: Remove private fields from the infojson
Expand Down Expand Up @@ -353,6 +357,15 @@ class YoutubeDL(object):
Progress hooks are guaranteed to be called at least once
(with status "finished") if the download is successful.
postprocessor_hooks: A list of functions that get called on postprocessing
progress, with a dictionary with the entries
* status: One of "started", "processing", or "finished".
Check this first and ignore unknown values.
* postprocessor: Name of the postprocessor
* info_dict: The extracted info_dict
Progress hooks are guaranteed to be called at least twice
(with status "started" and "finished") if the processing is successful.
merge_output_format: Extension to use when merging formats.
final_ext: Expected final extension; used to detect when the file was
already downloaded and converted. "merge_output_format" is
Expand Down Expand Up @@ -412,11 +425,15 @@ class YoutubeDL(object):
filename, abort-on-error, multistreams, no-live-chat,
no-clean-infojson, no-playlist-metafiles, no-keep-subs.
Refer __init__.py for their implementation
progress_template: Dictionary of templates for progress outputs.
Allowed keys are 'download', 'postprocess',
'download-title' (console title) and 'postprocess-title'.
The template is mapped on a dictionary with keys 'progress' and 'info'
The following parameters are not used by YoutubeDL itself, they are used by
the downloader (see yt_dlp/downloader/common.py):
nopart, updatetime, buffersize, ratelimit, throttledratelimit, min_filesize,
max_filesize, test, noresizebuffer, retries, continuedl, noprogress, consoletitle,
max_filesize, test, noresizebuffer, retries, continuedl, noprogress,
xattr_set_filesize, external_downloader_args, hls_use_mpegts, http_chunk_size.
The following options are used by the post processors:
Expand Down Expand Up @@ -484,26 +501,27 @@ def __init__(self, params=None, auto_init=True):
self._first_webpage_request = True
self._post_hooks = []
self._progress_hooks = []
self._postprocessor_hooks = []
self._download_retcode = 0
self._num_downloads = 0
self._screen_file = [sys.stdout, sys.stderr][params.get('logtostderr', False)]
self._err_file = sys.stderr
self.params = {
# Default parameters
'nocheckcertificate': False,
}
self.params.update(params)
self.params = params
self.cache = Cache(self)

windows_enable_vt_mode()
self.params['no_color'] = self.params.get('no_color') or not supports_terminal_sequences(self._err_file)

if sys.version_info < (3, 6):
self.report_warning(
'Python version %d.%d is not supported! Please update to Python 3.6 or above' % sys.version_info[:2])

if self.params.get('allow_unplayable_formats'):
self.report_warning(
'You have asked for unplayable formats to be listed/downloaded. '
'This is a developer option intended for debugging. '
'If you experience any issues while using this option, DO NOT open a bug report')
f'You have asked for {self._color_text("unplayable formats", "blue")} to be listed/downloaded. '
'This is a developer option intended for debugging. \n'
' If you experience any issues while using this option, '
f'{self._color_text("DO NOT", "red")} open a bug report')

def check_deprecated(param, option, suggestion):
if self.params.get(param) is not None:
Expand Down Expand Up @@ -675,9 +693,13 @@ def add_post_hook(self, ph):
self._post_hooks.append(ph)

def add_progress_hook(self, ph):
"""Add the progress hook (currently only for the file downloader)"""
"""Add the download progress hook"""
self._progress_hooks.append(ph)

def add_postprocessor_hook(self, ph):
"""Add the postprocessing progress hook"""
self._postprocessor_hooks.append(ph)

def _bidi_workaround(self, message):
if not hasattr(self, '_output_channel'):
return message
Expand Down Expand Up @@ -790,6 +812,11 @@ def to_screen(self, message, skip_eol=False):
self.to_stdout(
message, skip_eol, quiet=self.params.get('quiet', False))

def _color_text(self, text, color):
if self.params.get('no_color'):
return text
return f'{TERMINAL_SEQUENCES[color.upper()]}{text}{TERMINAL_SEQUENCES["RESET_STYLE"]}'

def report_warning(self, message, only_once=False):
'''
Print the message to stderr, it will be prefixed with 'WARNING:'
Expand All @@ -800,24 +827,14 @@ def report_warning(self, message, only_once=False):
else:
if self.params.get('no_warnings'):
return
if not self.params.get('no_color') and self._err_file.isatty() and compat_os_name != 'nt':
_msg_header = '\033[0;33mWARNING:\033[0m'
else:
_msg_header = 'WARNING:'
warning_message = '%s %s' % (_msg_header, message)
self.to_stderr(warning_message, only_once)
self.to_stderr(f'{self._color_text("WARNING:", "yellow")} {message}', only_once)

def report_error(self, message, tb=None):
'''
Do the same as trouble, but prefixes the message with 'ERROR:', colored
in red if stderr is a tty file.
'''
if not self.params.get('no_color') and self._err_file.isatty() and compat_os_name != 'nt':
_msg_header = '\033[0;31mERROR:\033[0m'
else:
_msg_header = 'ERROR:'
error_message = '%s %s' % (_msg_header, message)
self.trouble(error_message, tb)
self.trouble(f'{self._color_text("ERROR:", "red")} {message}', tb)

def write_debug(self, message, only_once=False):
'''Log debug message or Print message to stderr'''
Expand Down Expand Up @@ -919,7 +936,7 @@ def validate_outtmpl(cls, outtmpl):
return err

def prepare_outtmpl(self, outtmpl, info_dict, sanitize=None):
""" Make the template and info_dict suitable for substitution : ydl.outtmpl_escape(outtmpl) % info_dict """
""" Make the outtmpl and info_dict suitable for substitution: ydl.escape_outtmpl(outtmpl) % info_dict """
info_dict.setdefault('epoch', int(time.time())) # keep epoch consistent once set

info_dict = dict(info_dict) # Do not sanitize so as not to consume LazyList
Expand Down Expand Up @@ -1073,6 +1090,10 @@ def create_key(outer_mobj):

return EXTERNAL_FORMAT_RE.sub(create_key, outtmpl), TMPL_DICT

def evaluate_outtmpl(self, outtmpl, info_dict, *args, **kwargs):
outtmpl, info_dict = self.prepare_outtmpl(outtmpl, info_dict, *args, **kwargs)
return self.escape_outtmpl(outtmpl) % info_dict

def _prepare_filename(self, info_dict, tmpl_type='default'):
try:
sanitize = lambda k, v: sanitize_filename(
Expand Down Expand Up @@ -2431,10 +2452,8 @@ def print_optional(field):
if self.params.get('forceprint') or self.params.get('forcejson'):
self.post_extract(info_dict)
for tmpl in self.params.get('forceprint', []):
if re.match(r'\w+$', tmpl):
tmpl = '%({})s'.format(tmpl)
tmpl, info_copy = self.prepare_outtmpl(tmpl, info_dict)
self.to_stdout(self.escape_outtmpl(tmpl) % info_copy)
self.to_stdout(self.evaluate_outtmpl(
f'%({tmpl})s' if re.match(r'\w+$', tmpl) else tmpl, info_dict))

print_mandatory('title')
print_mandatory('id')
Expand Down
8 changes: 6 additions & 2 deletions yt_dlp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,11 +302,14 @@ def validate_outtmpl(tmpl, msg):
parser.error('invalid %s %r: %s' % (msg, tmpl, error_to_compat_str(err)))

for k, tmpl in opts.outtmpl.items():
validate_outtmpl(tmpl, '%s output template' % k)
validate_outtmpl(tmpl, f'{k} output template')
opts.forceprint = opts.forceprint or []
for tmpl in opts.forceprint or []:
validate_outtmpl(tmpl, 'print template')
validate_outtmpl(opts.sponsorblock_chapter_title, 'SponsorBlock chapter title')
for k, tmpl in opts.progress_template.items():
k = f'{k[:-6]} console title' if '-title' in k else f'{k} progress'
validate_outtmpl(tmpl, f'{k} template')

if opts.extractaudio and not opts.keepvideo and opts.format is None:
opts.format = 'bestaudio/best'
Expand Down Expand Up @@ -633,8 +636,9 @@ def report_args_compat(arg, name):
'noresizebuffer': opts.noresizebuffer,
'http_chunk_size': opts.http_chunk_size,
'continuedl': opts.continue_dl,
'noprogress': opts.noprogress,
'noprogress': opts.quiet if opts.noprogress is None else opts.noprogress,
'progress_with_newline': opts.progress_with_newline,
'progress_template': opts.progress_template,
'playliststart': opts.playliststart,
'playlistend': opts.playlistend,
'playlistreverse': opts.playlist_reverse,
Expand Down
7 changes: 7 additions & 0 deletions yt_dlp/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,12 @@ def compat_expanduser(path):
compat_pycrypto_AES = None


def windows_enable_vt_mode(): # TODO: Do this the proper way https://bugs.python.org/issue30075
if compat_os_name != 'nt':
return
os.system('')


# Deprecated

compat_basestring = str
Expand Down Expand Up @@ -281,5 +287,6 @@ def compat_expanduser(path):
'compat_xml_parse_error',
'compat_xpath',
'compat_zip',
'windows_enable_vt_mode',
'workaround_optparse_bug9161',
]
Loading

0 comments on commit 819e053

Please sign in to comment.