1
0
Fork 0
mirror of https://github.com/yt-dlp/yt-dlp.git synced 2025-03-09 12:50:23 -05:00
This commit is contained in:
Paul Storkman 2025-03-07 23:03:32 +01:00 committed by GitHub
commit d4d58d0170
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 93 additions and 46 deletions

View file

@ -352,9 +352,13 @@ ## General Options:
(Experimental)
--no-live-from-start Download livestreams from the current time
(default)
--wait-for-video MIN[-MAX] Wait for scheduled streams to become
--wait-for-video MIN[-MAX][:RETRIES]
Wait for scheduled streams to become
available. Pass the minimum number of
seconds (or range) to wait between retries
seconds (or range) to wait between retries.
RETRIES is the maximum number of additional
attempts if the video is still unavailable
after waiting (default is infinite)
--no-wait-for-video Do not wait for scheduled streams (default)
--mark-watched Mark videos watched (even with --simulate)
--no-mark-watched Do not mark videos watched (default)

View file

@ -105,6 +105,7 @@
Popen,
PostProcessingError,
ReExtractInfo,
ReExtractInfoLater,
RejectedVideoReached,
SameFileError,
UnavailableVideoError,
@ -1646,11 +1647,27 @@ def extract_info(self, url, download=True, ie_key=None, extra_info=None,
def _handle_extraction_exceptions(func):
@functools.wraps(func)
def wrapper(self, *args, **kwargs):
wait_retries = 0
wait_range = self.params.get('wait_for_video')
max_retries = float('inf')
if wait_range and wait_range[2] is not None:
max_retries = wait_range[2]
while True:
try:
return func(self, *args, **kwargs)
except (CookieLoadError, DownloadCancelled, LazyList.IndexError, PagedList.IndexError):
raise
except ReExtractInfoLater as e:
if wait_retries > max_retries:
if max_retries > 0:
self.report_error(f'[wait] Giving up after {wait_retries - 1} {"retries" if wait_retries != 2 else "retry"} while waiting.')
else:
self.report_error('[wait] Video is still unavailable after waiting.')
return
self._wait_until(e.time)
wait_retries += 1
self.to_screen('[wait] Re-extracting data')
continue
except ReExtractInfo as e:
if e.expected:
self.to_screen(f'{e}; Re-extracting data')
@ -1675,12 +1692,7 @@ def wrapper(self, *args, **kwargs):
break
return wrapper
def _wait_for_video(self, ie_result={}):
if (not self.params.get('wait_for_video')
or ie_result.get('_type', 'video') != 'video'
or ie_result.get('formats') or ie_result.get('url')):
return
def _wait_until(self, till):
format_dur = lambda dur: '%02d:%02d:%02d' % timetuple_from_msec(dur * 1000)[:-1]
last_msg = ''
@ -1694,7 +1706,31 @@ def progress(msg):
self.to_screen(full_msg, skip_eol=True)
last_msg = msg
min_wait, max_wait = self.params.get('wait_for_video')
diff = till - time.time()
self.to_screen(f'[wait] Waiting for {format_dur(diff)} - Press Ctrl+C to interrupt')
try:
while True:
diff = till - time.time()
if diff <= 0:
progress('')
self.to_screen('[wait] Wait period ended')
return
progress(f'[wait] Remaining time until next attempt: {self._format_screen(format_dur(diff), self.Styles.EMPHASIS)}')
time.sleep(1)
except KeyboardInterrupt:
progress('')
self.to_screen('[wait] Interrupted by user')
except BaseException:
self.to_screen('')
raise
def _wait_for_video(self, ie_result={}):
if (not self.params.get('wait_for_video')
or ie_result.get('_type', 'video') != 'video'
or ie_result.get('formats') or ie_result.get('url')):
return
min_wait, max_wait, _ = self.params.get('wait_for_video')
diff = try_get(ie_result, lambda x: x['release_timestamp'] - time.time())
if diff is None and ie_result.get('live_status') == 'is_upcoming':
diff = round(random.uniform(min_wait, max_wait) if (max_wait and min_wait) else (max_wait or min_wait), 0)
@ -1702,24 +1738,7 @@ def progress(msg):
elif ie_result and (diff or 0) <= 0:
self.report_warning('Video should already be available according to extracted info')
diff = min(max(diff or 0, min_wait or 0), max_wait or float('inf'))
self.to_screen(f'[wait] Waiting for {format_dur(diff)} - Press Ctrl+C to try now')
wait_till = time.time() + diff
try:
while True:
diff = wait_till - time.time()
if diff <= 0:
progress('')
raise ReExtractInfo('[wait] Wait period ended', expected=True)
progress(f'[wait] Remaining time until next attempt: {self._format_screen(format_dur(diff), self.Styles.EMPHASIS)}')
time.sleep(1)
except KeyboardInterrupt:
progress('')
raise ReExtractInfo('[wait] Interrupted by user', expected=True)
except BaseException as e:
if not isinstance(e, ReExtractInfo):
self.to_screen('')
raise
raise ReExtractInfoLater(time.time() + diff)
def _load_cookies(self, data, *, autoscope=True):
"""Loads cookies from a `Cookie` header

View file

@ -224,12 +224,37 @@ def validate_minmax(min_val, max_val, min_name, max_name=None):
else:
validate_minmax(opts.sleep_interval, opts.max_sleep_interval, 'sleep interval')
def parse_retries(name, value):
if value is None:
return None
elif value in ('inf', 'infinite'):
return float('inf')
try:
int_value = int(value)
except (TypeError, ValueError):
validate(False, f'{name} retry count', value)
validate_positive(f'{name} retry count', int_value)
return int_value
def parse_range_with_arg(name, arg_name, value,
parse_limits=parse_duration, parse_arg=parse_retries):
# syntax: MIN[-MAX][:N]
m = re.fullmatch(r'([^-:]+)(-[^:]+)?(:.+)?', value)
validate(m, name, value)
min_val, max_val, arg_val = m.groups()
min_lim, max_lim = map(parse_limits, [min_val, (max_val and max_val[1:])])
validate(min_lim is not None, name, value)
validate(max_val is None or max_lim is not None, name, value)
validate_minmax(min_lim, max_lim, name)
parsed_arg = parse_arg(arg_name, arg_val and arg_val[1:])
return (min_lim, max_lim, parsed_arg)
if opts.wait_for_video is not None:
min_wait, max_wait, *_ = map(parse_duration, [*opts.wait_for_video.split('-', 1), None])
validate(min_wait is not None and not (max_wait is None and '-' in opts.wait_for_video),
'time range to wait for video', opts.wait_for_video)
validate_minmax(min_wait, max_wait, 'time range to wait for video')
opts.wait_for_video = (min_wait, max_wait)
min_wait, max_wait, wait_retries = parse_range_with_arg(
'time range to wait for video', 'waiting', opts.wait_for_video)
opts.wait_for_video = (min_wait, max_wait, wait_retries)
# Format sort
for f in opts.format_sort:
@ -254,18 +279,6 @@ def validate_minmax(min_val, max_val, min_name, max_name=None):
validate_positive('audio quality', int_or_none(float_or_none(opts.audioquality), default=0))
# Retries
def parse_retries(name, value):
if value is None:
return None
elif value in ('inf', 'infinite'):
return float('inf')
try:
int_value = int(value)
except (TypeError, ValueError):
validate(False, f'{name} retry count', value)
validate_positive(f'{name} retry count', int_value)
return int_value
opts.retries = parse_retries('download', opts.retries)
opts.fragment_retries = parse_retries('fragment', opts.fragment_retries)
opts.extractor_retries = parse_retries('extractor', opts.extractor_retries)

View file

@ -445,10 +445,12 @@ def _alias_callback(option, opt_str, value, parser, opts, nargs):
help='Download livestreams from the current time (default)')
general.add_option(
'--wait-for-video',
dest='wait_for_video', metavar='MIN[-MAX]', default=None,
dest='wait_for_video', metavar='MIN[-MAX][:RETRIES]', default=None,
help=(
'Wait for scheduled streams to become available. '
'Pass the minimum number of seconds (or range) to wait between retries'))
'Pass the minimum number of seconds (or range) to wait between retries. '
'RETRIES is the maximum number of additional attempts if the video '
'is still unavailable after waiting (default is infinite)'))
general.add_option(
'--no-wait-for-video',
dest='wait_for_video', action='store_const', const=None,

View file

@ -1115,6 +1115,15 @@ def __init__(self, msg, expected=False):
self.expected = expected
class ReExtractInfoLater(ReExtractInfo):
""" Video info needs to be re-extracted after a waiting period. """
msg = 'Video is not available yet'
def __init__(self, time):
super().__init__(self.msg, expected=True)
self.time = time
class ThrottledDownload(ReExtractInfo):
""" Download speed below --throttled-rate. """
msg = 'The download speed is below throttle limit'