From 591bb9d3553a4d7b453777c1e28e0948741e3b50 Mon Sep 17 00:00:00 2001 From: pukkandan Date: Tue, 17 May 2022 18:36:29 +0530 Subject: Fix color in `-q -F` and convert `ydl._out_files`/`ydl._allow_colors` to `Namespace` Closes #3761 --- yt_dlp/YoutubeDL.py | 73 ++++++++++++++++++++++----------------------- yt_dlp/cookies.py | 2 +- yt_dlp/downloader/common.py | 6 ++-- yt_dlp/utils.py | 16 ++++++++-- 4 files changed, 54 insertions(+), 43 deletions(-) diff --git a/yt_dlp/YoutubeDL.py b/yt_dlp/YoutubeDL.py index 31af51195..c9de2437d 100644 --- a/yt_dlp/YoutubeDL.py +++ b/yt_dlp/YoutubeDL.py @@ -545,17 +545,18 @@ class YoutubeDL: self.cache = Cache(self) windows_enable_vt_mode() - self._out_files = { - 'error': sys.stderr, - 'print': sys.stderr if self.params.get('logtostderr') else sys.stdout, - 'console': None if compat_os_name == 'nt' else next( + stdout = sys.stderr if self.params.get('logtostderr') else sys.stdout + self._out_files = Namespace( + out=stdout, + error=sys.stderr, + screen=sys.stderr if self.params.get('quiet') else stdout, + console=None if compat_os_name == 'nt' else next( filter(supports_terminal_sequences, (sys.stderr, sys.stdout)), None) - } - self._out_files['screen'] = sys.stderr if self.params.get('quiet') else self._out_files['print'] - self._allow_colors = { - type_: not self.params.get('no_color') and supports_terminal_sequences(self._out_files[type_]) - for type_ in ('screen', 'error') - } + ) + self._allow_colors = Namespace(**{ + type_: not self.params.get('no_color') and supports_terminal_sequences(stream) + for type_, stream in self._out_files.items_ if type_ != 'console' + }) if sys.version_info < (3, 6): self.report_warning( @@ -612,14 +613,8 @@ class YoutubeDL: import pty master, slave = pty.openpty() width = compat_get_terminal_size().columns - if width is None: - width_args = [] - else: - width_args = ['-w', str(width)] - sp_kwargs = dict( - stdin=subprocess.PIPE, - stdout=slave, - stderr=self._out_files['error']) + width_args = [] if width is None else ['-w', str(width)] + sp_kwargs = {'stdin': subprocess.PIPE, 'stdout': slave, 'stderr': self._out_files.error} try: self._output_process = Popen(['bidiv'] + width_args, **sp_kwargs) except OSError: @@ -792,7 +787,7 @@ class YoutubeDL: self.deprecation_warning('"YoutubeDL.to_stdout" no longer accepts the argument quiet. Use "YoutubeDL.to_screen" instead') self._write_string( '%s%s' % (self._bidi_workaround(message), ('' if skip_eol else '\n')), - self._out_files['print']) + self._out_files.out) def to_screen(self, message, skip_eol=False, quiet=None): """Print message to screen if not in quiet mode""" @@ -803,7 +798,7 @@ class YoutubeDL: return self._write_string( '%s%s' % (self._bidi_workaround(message), ('' if skip_eol else '\n')), - self._out_files['screen']) + self._out_files.screen) def to_stderr(self, message, only_once=False): """Print message to stderr""" @@ -811,12 +806,12 @@ class YoutubeDL: if self.params.get('logger'): self.params['logger'].error(message) else: - self._write_string('%s\n' % self._bidi_workaround(message), self._out_files['error'], only_once=only_once) + self._write_string(f'{self._bidi_workaround(message)}\n' , self._out_files.error, only_once=only_once) def _send_console_code(self, code): - if compat_os_name == 'nt' or not self._out_files['console']: + if compat_os_name == 'nt' or not self._out_files.console: return - self._write_string(code, self._out_files['console']) + self._write_string(code, self._out_files.console) def to_console_title(self, message): if not self.params.get('consoletitle', False): @@ -906,13 +901,14 @@ class YoutubeDL: text = fallback return format_text(text, f) if allow_colors else text if fallback is None else fallback + def _format_out(self, *args, **kwargs): + return self._format_text(self._out_files.out, self._allow_colors.out, *args, **kwargs) + def _format_screen(self, *args, **kwargs): - return self._format_text( - self._out_files['screen'], self._allow_colors['screen'], *args, **kwargs) + return self._format_text(self._out_files.screen, self._allow_colors.screen, *args, **kwargs) def _format_err(self, *args, **kwargs): - return self._format_text( - self._out_files['error'], self._allow_colors['error'], *args, **kwargs) + return self._format_text(self._out_files.error, self._allow_colors.error, *args, **kwargs) def report_warning(self, message, only_once=False): ''' @@ -3438,7 +3434,7 @@ class YoutubeDL: def _list_format_headers(self, *headers): if self.params.get('listformats_table', True) is not False: - return [self._format_screen(header, self.Styles.HEADERS) for header in headers] + return [self._format_out(header, self.Styles.HEADERS) for header in headers] return headers def _format_note(self, fdict): @@ -3516,10 +3512,10 @@ class YoutubeDL: ] for f in formats if f.get('preference') is None or f['preference'] >= -1000] return render_table(['format code', 'extension', 'resolution', 'note'], table, extra_gap=1) - delim = self._format_screen('\u2502', self.Styles.DELIM, '|', test_encoding=True) + delim = self._format_out('\u2502', self.Styles.DELIM, '|', test_encoding=True) table = [ [ - self._format_screen(format_field(f, 'format_id'), self.Styles.ID), + self._format_out(format_field(f, 'format_id'), self.Styles.ID), format_field(f, 'ext'), format_field(f, func=self.format_resolution, ignore=('audio only', 'images')), format_field(f, 'fps', '\t%d'), @@ -3531,15 +3527,15 @@ class YoutubeDL: delim, format_field(f, 'vcodec', default='unknown').replace( 'none', 'images' if f.get('acodec') == 'none' - else self._format_screen('audio only', self.Styles.SUPPRESS)), + else self._format_out('audio only', self.Styles.SUPPRESS)), format_field(f, 'vbr', '\t%dk'), format_field(f, 'acodec', default='unknown').replace( 'none', '' if f.get('vcodec') == 'none' - else self._format_screen('video only', self.Styles.SUPPRESS)), + else self._format_out('video only', self.Styles.SUPPRESS)), format_field(f, 'abr', '\t%dk'), format_field(f, 'asr', '\t%dHz'), join_nonempty( - self._format_screen('UNSUPPORTED', 'light red') if f.get('ext') in ('f4f', 'f4m') else None, + self._format_out('UNSUPPORTED', 'light red') if f.get('ext') in ('f4f', 'f4m') else None, format_field(f, 'language', '[%s]'), join_nonempty(format_field(f, 'format_note'), format_field(f, 'container', ignore=(None, f.get('ext'))), @@ -3552,7 +3548,7 @@ class YoutubeDL: return render_table( header_line, table, hide_empty=True, - delim=self._format_screen('\u2500', self.Styles.DELIM, '-', test_encoding=True)) + delim=self._format_out('\u2500', self.Styles.DELIM, '-', test_encoding=True)) def render_thumbnails_table(self, info_dict): thumbnails = list(info_dict.get('thumbnails') or []) @@ -3610,11 +3606,14 @@ class YoutubeDL: ret += ' (No VT)' if WINDOWS_VT_MODE is False else ' (No ANSI)' return ret - encoding_str = 'Encodings: locale %s, fs %s, out %s, err %s, pref %s' % ( + encoding_str = 'Encodings: locale %s, fs %s, pref %s, %s' % ( locale.getpreferredencoding(), sys.getfilesystemencoding(), - get_encoding(self._out_files['screen']), get_encoding(self._out_files['error']), - self.get_encoding()) + self.get_encoding(), + ', '.join( + f'{key} {get_encoding(stream)}' for key, stream in self._out_files.items_ + if stream is not None and key != 'console') + ) logger = self.params.get('logger') if logger: diff --git a/yt_dlp/cookies.py b/yt_dlp/cookies.py index 1598828f2..f427c8bfe 100644 --- a/yt_dlp/cookies.py +++ b/yt_dlp/cookies.py @@ -63,7 +63,7 @@ class YDLLogger: # Do not print to files/pipes, loggers, or when --no-progress is used if not self._ydl or self._ydl.params.get('noprogress') or self._ydl.params.get('logger'): return - file = self._ydl._out_files['error'] + file = self._ydl._out_files.error try: if not file.isatty(): return diff --git a/yt_dlp/downloader/common.py b/yt_dlp/downloader/common.py index 1f14ebb3a..fad4e6664 100644 --- a/yt_dlp/downloader/common.py +++ b/yt_dlp/downloader/common.py @@ -282,9 +282,9 @@ class FileDownloader: elif self.ydl.params.get('logger'): self._multiline = MultilineLogger(self.ydl.params['logger'], lines) elif self.params.get('progress_with_newline'): - self._multiline = BreaklineStatusPrinter(self.ydl._out_files['screen'], lines) + self._multiline = BreaklineStatusPrinter(self.ydl._out_files.screen, lines) else: - self._multiline = MultilinePrinter(self.ydl._out_files['screen'], lines, not self.params.get('quiet')) + self._multiline = MultilinePrinter(self.ydl._out_files.screen, lines, not self.params.get('quiet')) self._multiline.allow_colors = self._multiline._HAVE_FULLCAP and not self.params.get('no_color') def _finish_multiline_status(self): @@ -301,7 +301,7 @@ class FileDownloader: ) def _report_progress_status(self, s, default_template): - for name, style in self.ProgressStyles._asdict().items(): + for name, style in self.ProgressStyles.items_: name = f'_{name}_str' if name not in s: continue diff --git a/yt_dlp/utils.py b/yt_dlp/utils.py index 8a9567de4..1249c0100 100644 --- a/yt_dlp/utils.py +++ b/yt_dlp/utils.py @@ -5322,8 +5322,20 @@ class classproperty: return self.f(cls) -def Namespace(**kwargs): - return collections.namedtuple('Namespace', kwargs)(**kwargs) +class Namespace: + """Immutable namespace""" + @property + def items_(self): + return self._dict.items() + + def __init__(self, **kwargs): + self._dict = kwargs + + def __getattr__(self, attr): + return self._dict[attr] + + def __repr__(self): + return f'{type(self).__name__}({", ".join(f"{k}={v}" for k, v in self.items_)})' # Deprecated -- cgit v1.2.3 From 7a96d0b39c34c6c8c42cc6aaac90dd8d0f5a51d7 Mon Sep 17 00:00:00 2001 From: pukkandan Date: Tue, 17 May 2022 19:40:54 +0530 Subject: [build] More test-runners * GHA does not cache python 3.6 for Windows, so use 3.8 instead * Add tests for 3.11, PyPy3.8/3.9 * CPython 3.10 is now stable * Do not pin Ubuntu to 18.04 --- .github/workflows/core.yml | 8 ++++---- .github/workflows/download.yml | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/core.yml b/.github/workflows/core.yml index 4fb65e0c1..78a75cd1c 100644 --- a/.github/workflows/core.yml +++ b/.github/workflows/core.yml @@ -8,14 +8,14 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-18.04] - # py3.9 is in quick-test - python-version: [3.7, 3.8, 3.10-dev, pypy-3.6, pypy-3.7] + os: [ubuntu-latest] + # CPython 3.9 is in quick-test + python-version: ['3.6', '3.7', '3.10', 3.11-dev, pypy-3.6, pypy-3.7, pypy-3.8, pypy-3.9] run-tests-ext: [sh] include: # atleast one of the tests must be in windows - os: windows-latest - python-version: 3.6 + python-version: 3.8 run-tests-ext: bat steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/download.yml b/.github/workflows/download.yml index dd242fa56..3b696549a 100644 --- a/.github/workflows/download.yml +++ b/.github/workflows/download.yml @@ -8,12 +8,12 @@ jobs: strategy: fail-fast: true matrix: - os: [ubuntu-18.04] - python-version: [3.7, 3.8, 3.9, 3.10-dev, pypy-3.6, pypy-3.7] + os: [ubuntu-latest] + python-version: ['3.6', '3.7', '3.9', '3.10', 3.11-dev, pypy-3.6, pypy-3.7, pypy-3.8, pypy-3.9] run-tests-ext: [sh] include: - os: windows-latest - python-version: 3.6 + python-version: 3.8 run-tests-ext: bat steps: - uses: actions/checkout@v2 -- cgit v1.2.3 From 5792c950bfd9f8b6730659b3046b41c1aea64c98 Mon Sep 17 00:00:00 2001 From: pukkandan Date: Tue, 17 May 2022 19:39:28 +0530 Subject: [compat] Implement `compat.imghdr` Python 3.11 deprecates `imghdr` module --- yt_dlp/YoutubeDL.py | 2 +- yt_dlp/compat/imghdr.py | 14 ++++++++++++++ yt_dlp/postprocessor/embedthumbnail.py | 2 +- yt_dlp/postprocessor/ffmpeg.py | 19 +++++++++---------- 4 files changed, 25 insertions(+), 12 deletions(-) create mode 100644 yt_dlp/compat/imghdr.py diff --git a/yt_dlp/YoutubeDL.py b/yt_dlp/YoutubeDL.py index c9de2437d..d1094a01b 100644 --- a/yt_dlp/YoutubeDL.py +++ b/yt_dlp/YoutubeDL.py @@ -806,7 +806,7 @@ class YoutubeDL: if self.params.get('logger'): self.params['logger'].error(message) else: - self._write_string(f'{self._bidi_workaround(message)}\n' , self._out_files.error, only_once=only_once) + self._write_string(f'{self._bidi_workaround(message)}\n', self._out_files.error, only_once=only_once) def _send_console_code(self, code): if compat_os_name == 'nt' or not self._out_files.console: diff --git a/yt_dlp/compat/imghdr.py b/yt_dlp/compat/imghdr.py new file mode 100644 index 000000000..734b0d876 --- /dev/null +++ b/yt_dlp/compat/imghdr.py @@ -0,0 +1,14 @@ +tests = { + 'webp': lambda h: h[0:4] == b'RIFF' and h[8:] == b'WEBP', + 'png': lambda h: h[:8] == b'\211PNG\r\n\032\n', + 'jpeg': lambda h: h[6:10] in (b'JFIF', b'Exif'), +} + + +def what(path): + """Detect format of image (Currently supports jpeg, png, webp only) + Ref: https://github.com/python/cpython/blob/3.10/Lib/imghdr.py + """ + with open(path, 'rb') as f: + head = f.read(12) + return next((type_ for type_, test in tests.items() if test(head)), None) diff --git a/yt_dlp/postprocessor/embedthumbnail.py b/yt_dlp/postprocessor/embedthumbnail.py index d36e0008e..e031d344f 100644 --- a/yt_dlp/postprocessor/embedthumbnail.py +++ b/yt_dlp/postprocessor/embedthumbnail.py @@ -1,11 +1,11 @@ import base64 -import imghdr import os import re import subprocess from .common import PostProcessor from .ffmpeg import FFmpegPostProcessor, FFmpegThumbnailsConvertorPP +from ..compat import imghdr from ..dependencies import mutagen from ..utils import ( Popen, diff --git a/yt_dlp/postprocessor/ffmpeg.py b/yt_dlp/postprocessor/ffmpeg.py index d1d8e1687..09eb33b8d 100644 --- a/yt_dlp/postprocessor/ffmpeg.py +++ b/yt_dlp/postprocessor/ffmpeg.py @@ -7,7 +7,7 @@ import subprocess import time from .common import AudioConversionError, PostProcessor -from ..compat import compat_str +from ..compat import imghdr from ..utils import ( ISO639Utils, Popen, @@ -27,6 +27,7 @@ from ..utils import ( traverse_obj, variadic, write_json_file, + write_string, ) EXT_TO_OUT_FORMATS = { @@ -1030,8 +1031,8 @@ class FFmpegSplitChaptersPP(FFmpegPostProcessor): self.to_screen('Chapter %03d; Destination: %s' % (number, destination)) return ( destination, - ['-ss', compat_str(chapter['start_time']), - '-t', compat_str(chapter['end_time'] - chapter['start_time'])]) + ['-ss', str(chapter['start_time']), + '-t', str(chapter['end_time'] - chapter['start_time'])]) @PostProcessor._restrict_to(images=False) def run(self, info): @@ -1059,18 +1060,16 @@ class FFmpegThumbnailsConvertorPP(FFmpegPostProcessor): super().__init__(downloader) self.format = format - @staticmethod - def is_webp(path): - with open(encodeFilename(path), 'rb') as f: - b = f.read(12) - return b[0:4] == b'RIFF' and b[8:] == b'WEBP' + @classmethod + def is_webp(cls, path): + write_string(f'DeprecationWarning: {cls.__module__}.{cls.__name__}.is_webp is deprecated') + return imghdr.what(path) == 'webp' def fixup_webp(self, info, idx=-1): thumbnail_filename = info['thumbnails'][idx]['filepath'] _, thumbnail_ext = os.path.splitext(thumbnail_filename) if thumbnail_ext: - thumbnail_ext = thumbnail_ext[1:].lower() - if thumbnail_ext != 'webp' and self.is_webp(thumbnail_filename): + if thumbnail_ext.lower() != '.webp' and imghdr.what(thumbnail_filename) == 'webp': self.to_screen('Correcting thumbnail "%s" extension to webp' % thumbnail_filename) webp_filename = replace_extension(thumbnail_filename, 'webp') os.replace(thumbnail_filename, webp_filename) -- cgit v1.2.3 From 7896214c42db91bbf62853b5c7359c9e83064cf1 Mon Sep 17 00:00:00 2001 From: pukkandan Date: Tue, 17 May 2022 22:08:12 +0530 Subject: Bugfix for 591bb9d3553a4d7b453777c1e28e0948741e3b50 Closes #3769 --- yt_dlp/YoutubeDL.py | 4 ++-- yt_dlp/downloader/common.py | 2 +- yt_dlp/utils.py | 11 +++++++---- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/yt_dlp/YoutubeDL.py b/yt_dlp/YoutubeDL.py index d1094a01b..31624f181 100644 --- a/yt_dlp/YoutubeDL.py +++ b/yt_dlp/YoutubeDL.py @@ -555,7 +555,7 @@ class YoutubeDL: ) self._allow_colors = Namespace(**{ type_: not self.params.get('no_color') and supports_terminal_sequences(stream) - for type_, stream in self._out_files.items_ if type_ != 'console' + for type_, stream in self._out_files if type_ != 'console' }) if sys.version_info < (3, 6): @@ -3611,7 +3611,7 @@ class YoutubeDL: sys.getfilesystemencoding(), self.get_encoding(), ', '.join( - f'{key} {get_encoding(stream)}' for key, stream in self._out_files.items_ + f'{key} {get_encoding(stream)}' for key, stream in self._out_files if stream is not None and key != 'console') ) diff --git a/yt_dlp/downloader/common.py b/yt_dlp/downloader/common.py index fad4e6664..465b5ef99 100644 --- a/yt_dlp/downloader/common.py +++ b/yt_dlp/downloader/common.py @@ -301,7 +301,7 @@ class FileDownloader: ) def _report_progress_status(self, s, default_template): - for name, style in self.ProgressStyles.items_: + for name, style in self.ProgressStyles: name = f'_{name}_str' if name not in s: continue diff --git a/yt_dlp/utils.py b/yt_dlp/utils.py index 1249c0100..48a94415d 100644 --- a/yt_dlp/utils.py +++ b/yt_dlp/utils.py @@ -5324,9 +5324,6 @@ class classproperty: class Namespace: """Immutable namespace""" - @property - def items_(self): - return self._dict.items() def __init__(self, **kwargs): self._dict = kwargs @@ -5334,8 +5331,14 @@ class Namespace: def __getattr__(self, attr): return self._dict[attr] + def __contains__(self, item): + return item in self._dict.values() + + def __iter__(self): + return iter(self._dict.items()) + def __repr__(self): - return f'{type(self).__name__}({", ".join(f"{k}={v}" for k, v in self.items_)})' + return f'{type(self).__name__}({", ".join(f"{k}={v}" for k, v in self)})' # Deprecated -- cgit v1.2.3 From d6bf1161db0aa316229c0bc79352917d27fafa09 Mon Sep 17 00:00:00 2001 From: pukkandan Date: Wed, 18 May 2022 04:14:13 +0530 Subject: [generic] Refactor `_extract_rss` Closes #3738 --- yt_dlp/extractor/generic.py | 49 +++++++++++++-------------------------------- 1 file changed, 14 insertions(+), 35 deletions(-) diff --git a/yt_dlp/extractor/generic.py b/yt_dlp/extractor/generic.py index f594d02c2..54d9f61c9 100644 --- a/yt_dlp/extractor/generic.py +++ b/yt_dlp/extractor/generic.py @@ -129,6 +129,7 @@ from ..utils import ( sanitized_Request, smuggle_url, str_or_none, + try_call, unescapeHTML, unified_timestamp, unsmuggle_url, @@ -2536,66 +2537,44 @@ class GenericIE(InfoExtractor): self._downloader.write_debug(f'Identified a {name}') def _extract_rss(self, url, video_id, doc): - playlist_title = doc.find('./channel/title').text - playlist_desc_el = doc.find('./channel/description') - playlist_desc = None if playlist_desc_el is None else playlist_desc_el.text - NS_MAP = { 'itunes': 'http://www.itunes.com/dtds/podcast-1.0.dtd', } entries = [] for it in doc.findall('./channel/item'): - next_url = None - enclosure_nodes = it.findall('./enclosure') - for e in enclosure_nodes: - next_url = e.attrib.get('url') - if next_url: - break - - if not next_url: - next_url = xpath_text(it, 'link', fatal=False) - + next_url = next( + (e.attrib.get('url') for e in it.findall('./enclosure')), + xpath_text(it, 'link', fatal=False)) if not next_url: continue - if it.find('guid').text is not None: - next_url = smuggle_url(next_url, {'force_videoid': it.find('guid').text}) + guid = try_call(lambda: it.find('guid').text) + if guid: + next_url = smuggle_url(next_url, {'force_videoid': guid}) def itunes(key): - return xpath_text( - it, xpath_with_ns('./itunes:%s' % key, NS_MAP), - default=None) - - duration = itunes('duration') - explicit = (itunes('explicit') or '').lower() - if explicit in ('true', 'yes'): - age_limit = 18 - elif explicit in ('false', 'no'): - age_limit = 0 - else: - age_limit = None + return xpath_text(it, xpath_with_ns(f'./itunes:{key}', NS_MAP), default=None) entries.append({ '_type': 'url_transparent', 'url': next_url, - 'title': it.find('title').text, + 'title': try_call(lambda: it.find('title').text), 'description': xpath_text(it, 'description', default=None), - 'timestamp': unified_timestamp( - xpath_text(it, 'pubDate', default=None)), - 'duration': int_or_none(duration) or parse_duration(duration), + 'timestamp': unified_timestamp(xpath_text(it, 'pubDate', default=None)), + 'duration': parse_duration(itunes('duration')), 'thumbnail': url_or_none(xpath_attr(it, xpath_with_ns('./itunes:image', NS_MAP), 'href')), 'episode': itunes('title'), 'episode_number': int_or_none(itunes('episode')), 'season_number': int_or_none(itunes('season')), - 'age_limit': age_limit, + 'age_limit': {'true': 18, 'yes': 18, 'false': 0, 'no': 0}.get((itunes('explicit') or '').lower()), }) return { '_type': 'playlist', 'id': url, - 'title': playlist_title, - 'description': playlist_desc, + 'title': try_call(lambda: doc.find('./channel/title').text), + 'description': try_call(lambda: doc.find('./channel/description').text), 'entries': entries, } -- cgit v1.2.3 From aedaa455d9874f14662023f21b254168ecd55579 Mon Sep 17 00:00:00 2001 From: pukkandan Date: Wed, 18 May 2022 05:11:47 +0530 Subject: [vimeo] Fix extractors Closes #3037, Closes #2858, Closes #2880, Closes #3712 May also fix #3602, #3360 --- yt_dlp/extractor/dropout.py | 2 +- yt_dlp/extractor/generic.py | 2 +- yt_dlp/extractor/vimeo.py | 32 +++++++++++++++++++------------- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/yt_dlp/extractor/dropout.py b/yt_dlp/extractor/dropout.py index 475825eb8..096216418 100644 --- a/yt_dlp/extractor/dropout.py +++ b/yt_dlp/extractor/dropout.py @@ -137,7 +137,7 @@ class DropoutIE(InfoExtractor): return { '_type': 'url_transparent', 'ie_key': VHXEmbedIE.ie_key(), - 'url': embed_url, + 'url': VHXEmbedIE._smuggle_referrer(embed_url, 'https://www.dropout.tv'), 'id': self._search_regex(r'embed\.vhx\.tv/videos/(.+?)\?', embed_url, 'id'), 'display_id': display_id, 'title': title, diff --git a/yt_dlp/extractor/generic.py b/yt_dlp/extractor/generic.py index 54d9f61c9..dda2b1eef 100644 --- a/yt_dlp/extractor/generic.py +++ b/yt_dlp/extractor/generic.py @@ -2954,7 +2954,7 @@ class GenericIE(InfoExtractor): if vimeo_urls: return self.playlist_from_matches(vimeo_urls, video_id, video_title, ie=VimeoIE.ie_key()) - vhx_url = VHXEmbedIE._extract_url(webpage) + vhx_url = VHXEmbedIE._extract_url(url, webpage) if vhx_url: return self.url_result(vhx_url, VHXEmbedIE.ie_key()) diff --git a/yt_dlp/extractor/vimeo.py b/yt_dlp/extractor/vimeo.py index 59c5353ab..961734345 100644 --- a/yt_dlp/extractor/vimeo.py +++ b/yt_dlp/extractor/vimeo.py @@ -40,6 +40,18 @@ class VimeoBaseInfoExtractor(InfoExtractor): _LOGIN_REQUIRED = False _LOGIN_URL = 'https://vimeo.com/log_in' + @staticmethod + def _smuggle_referrer(url, referrer_url): + return smuggle_url(url, {'http_headers': {'Referer': referrer_url}}) + + def _unsmuggle_headers(self, url): + """@returns (url, smuggled_data, headers)""" + url, data = unsmuggle_url(url, {}) + headers = self.get_param('http_headers').copy() + if 'http_headers' in data: + headers.update(data['http_headers']) + return url, data, headers + def _perform_login(self, username, password): webpage = self._download_webpage( self._LOGIN_URL, None, 'Downloading login page') @@ -717,10 +729,6 @@ class VimeoIE(VimeoBaseInfoExtractor): # vimeo embed with check-password page protected by Referer header ] - @staticmethod - def _smuggle_referrer(url, referrer_url): - return smuggle_url(url, {'http_headers': {'Referer': referrer_url}}) - @staticmethod def _extract_urls(url, webpage): urls = [] @@ -754,8 +762,8 @@ class VimeoIE(VimeoBaseInfoExtractor): 'Content-Type': 'application/x-www-form-urlencoded', }) checked = self._download_json( - url + '/check-password', video_id, - 'Verifying the password', data=data, headers=headers) + f'{compat_urlparse.urlsplit(url)._replace(query=None).geturl()}/check-password', + video_id, 'Verifying the password', data=data, headers=headers) if checked is False: raise ExtractorError('Wrong video password', expected=True) return checked @@ -830,10 +838,7 @@ class VimeoIE(VimeoBaseInfoExtractor): raise def _real_extract(self, url): - url, data = unsmuggle_url(url, {}) - headers = self.get_param('http_headers').copy() - if 'http_headers' in data: - headers.update(data['http_headers']) + url, data, headers = self._unsmuggle_headers(url) if 'Referer' not in headers: headers['Referer'] = url @@ -1383,14 +1388,15 @@ class VHXEmbedIE(VimeoBaseInfoExtractor): _VALID_URL = r'https?://embed\.vhx\.tv/videos/(?P\d+)' @staticmethod - def _extract_url(webpage): + def _extract_url(url, webpage): mobj = re.search( r']+src="(https?://embed\.vhx\.tv/videos/\d+[^"]*)"', webpage) - return unescapeHTML(mobj.group(1)) if mobj else None + return VimeoIE._smuggle_referrer(unescapeHTML(mobj.group(1)), url) if mobj else None def _real_extract(self, url): video_id = self._match_id(url) - webpage = self._download_webpage(url, video_id) + url, _, headers = self._unsmuggle_headers(url) + webpage = self._download_webpage(url, video_id, headers=headers) config_url = self._parse_json(self._search_regex( r'window\.OTTData\s*=\s*({.+})', webpage, 'ott data'), video_id, js_to_json)['config_url'] -- cgit v1.2.3 From 80e8493ee7c3083f4e215794e4a67ba5265f24f7 Mon Sep 17 00:00:00 2001 From: pukkandan Date: Wed, 18 May 2022 06:42:43 +0530 Subject: [utils] `is_html`: Handle double BOM Closes #2885 --- yt_dlp/extractor/generic.py | 15 +++++++++++++++ yt_dlp/utils.py | 11 +++++------ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/yt_dlp/extractor/generic.py b/yt_dlp/extractor/generic.py index dda2b1eef..b0fc176ef 100644 --- a/yt_dlp/extractor/generic.py +++ b/yt_dlp/extractor/generic.py @@ -2527,6 +2527,21 @@ class GenericIE(InfoExtractor): 'upload_date': '20220504', }, }, + { + # Webpage contains double BOM + 'url': 'https://www.filmarkivet.se/movies/paris-d-moll/', + 'md5': 'df02cadc719dcc63d43288366f037754', + 'info_dict': { + 'id': 'paris-d-moll', + 'ext': 'mp4', + 'upload_date': '20220518', + 'title': 'Paris d-moll', + 'description': 'md5:319e37ea5542293db37e1e13072fe330', + 'thumbnail': 'https://www.filmarkivet.se/wp-content/uploads/parisdmoll2.jpg', + 'timestamp': 1652833414, + 'age_limit': 0, + } + } ] def report_following_redirect(self, new_url): diff --git a/yt_dlp/utils.py b/yt_dlp/utils.py index 48a94415d..3b0e6750c 100644 --- a/yt_dlp/utils.py +++ b/yt_dlp/utils.py @@ -3290,14 +3290,13 @@ def is_html(first_bytes): (b'\xff\xfe', 'utf-16-le'), (b'\xfe\xff', 'utf-16-be'), ] + + encoding = 'utf-8' for bom, enc in BOMS: - if first_bytes.startswith(bom): - s = first_bytes[len(bom):].decode(enc, 'replace') - break - else: - s = first_bytes.decode('utf-8', 'replace') + while first_bytes.startswith(bom): + encoding, first_bytes = enc, first_bytes[len(bom):] - return re.match(r'^\s*<', s) + return re.match(r'^\s*<', first_bytes.decode(encoding, 'replace')) def determine_protocol(info_dict): -- cgit v1.2.3 From 21633673c33f082c6673bc245e4a90d880729a58 Mon Sep 17 00:00:00 2001 From: pukkandan Date: Wed, 18 May 2022 09:04:30 +0530 Subject: [cleanup] Minor fixes --- Changelog.md | 8 ++++---- devscripts/make_lazy_extractors.py | 2 +- yt_dlp/YoutubeDL.py | 2 +- yt_dlp/__init__.py | 1 + yt_dlp/extractor/common.py | 18 ++++++++++++------ yt_dlp/extractor/fc2.py | 3 +-- yt_dlp/extractor/voicy.py | 6 +++--- yt_dlp/utils.py | 4 +++- 8 files changed, 26 insertions(+), 18 deletions(-) diff --git a/Changelog.md b/Changelog.md index 3fb6260b8..52ea03367 100644 --- a/Changelog.md +++ b/Changelog.md @@ -785,7 +785,7 @@ * [build] Improvements * Build standalone MacOS packages by [smplayer-dev](https://github.com/smplayer-dev) * Release windows exe built with `py2exe` - * Enable lazy-extractors in releases. + * Enable lazy-extractors in releases * Set env var `YTDLP_NO_LAZY_EXTRACTORS` to forcefully disable this (experimental) * Clean up error reporting in update * Refactor `pyinst.py`, misc cleanup and improve docs @@ -1393,7 +1393,7 @@ * [youtube] Non-fatal alert reporting for unavailable videos page by [coletdjnz](https://github.com/coletdjnz) * [twitcasting] Websocket support by [nao20010128nao](https://github.com/nao20010128nao) * [mediasite] Extract slides by [fstirlitz](https://github.com/fstirlitz) -* [funimation] Extract subtitles +* [funimation] Extract subtitles * [pornhub] Extract `cast` * [hotstar] Use server time for authentication instead of local time * [EmbedThumbnail] Fix for already downloaded thumbnail @@ -1489,7 +1489,7 @@ ### 2021.05.20 -* **Youtube improvements**: +* **Youtube improvements**: * Support youtube music `MP`, `VL` and `browse` pages * Extract more formats for youtube music by [craftingmod](https://github.com/craftingmod), [coletdjnz](https://github.com/coletdjnz) and [pukkandan](https://github.com/pukkandan) * Extract multiple subtitles in same language by [pukkandan](https://github.com/pukkandan) and [tpikonen](https://github.com/tpikonen) @@ -2031,7 +2031,7 @@ * **Format Sort:** Added `--format-sort` (`-S`), `--format-sort-force` (`--S-force`) - See [Sorting Formats](README.md#sorting-formats) for details * **Format Selection:** See [Format Selection](README.md#format-selection) for details * New format selectors: `best*`, `worst*`, `bestvideo*`, `bestaudio*`, `worstvideo*`, `worstaudio*` - * Changed video format sorting to show video only files and video+audio files together. + * Changed video format sorting to show video only files and video+audio files together * Added `--video-multistreams`, `--no-video-multistreams`, `--audio-multistreams`, `--no-audio-multistreams` * Added `b`,`w`,`v`,`a` as alias for `best`, `worst`, `video` and `audio` respectively * Shortcut Options: Added `--write-link`, `--write-url-link`, `--write-webloc-link`, `--write-desktop-link` by [h-h-h-h](https://github.com/h-h-h-h) - See [Internet Shortcut Options](README.md#internet-shortcut-options) for details diff --git a/devscripts/make_lazy_extractors.py b/devscripts/make_lazy_extractors.py index 8c481bc2d..cd1985c8e 100644 --- a/devscripts/make_lazy_extractors.py +++ b/devscripts/make_lazy_extractors.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import os import optparse +import os import sys from inspect import getsource diff --git a/yt_dlp/YoutubeDL.py b/yt_dlp/YoutubeDL.py index 31624f181..ba08f6a7d 100644 --- a/yt_dlp/YoutubeDL.py +++ b/yt_dlp/YoutubeDL.py @@ -1924,7 +1924,7 @@ class YoutubeDL: and download and ( not can_merge() - or info_dict.get('is_live', False) + or info_dict.get('is_live') and not self.params.get('live_from_start') or self.outtmpl_dict['default'] == '-')) compat = ( prefer_best diff --git a/yt_dlp/__init__.py b/yt_dlp/__init__.py index 0a8bf37b6..8f890b34a 100644 --- a/yt_dlp/__init__.py +++ b/yt_dlp/__init__.py @@ -869,6 +869,7 @@ def main(argv=None): from .extractor import gen_extractors, list_extractors + __all__ = [ 'main', 'YoutubeDL', diff --git a/yt_dlp/extractor/common.py b/yt_dlp/extractor/common.py index ebeca4395..6a451c20b 100644 --- a/yt_dlp/extractor/common.py +++ b/yt_dlp/extractor/common.py @@ -1343,7 +1343,7 @@ class InfoExtractor: return self._og_search_property('url', html, **kargs) def _html_extract_title(self, html, name='title', *, fatal=False, **kwargs): - return self._html_search_regex(r'(?s)([^<]+)', html, name, fatal=fatal, **kwargs) + return self._html_search_regex(r'(?s)]*>([^<]+)', html, name, fatal=fatal, **kwargs) def _html_search_meta(self, name, html, display_name=None, fatal=False, **kwargs): name = variadic(name) @@ -1509,8 +1509,9 @@ class InfoExtractor: 'url': url_or_none(e.get('contentUrl')), 'title': unescapeHTML(e.get('name')), 'description': unescapeHTML(e.get('description')), - 'thumbnails': [{'url': url_or_none(url)} - for url in variadic(traverse_obj(e, 'thumbnailUrl', 'thumbnailURL'))], + 'thumbnails': [{'url': url} + for url in variadic(traverse_obj(e, 'thumbnailUrl', 'thumbnailURL')) + if url_or_none(url)], 'duration': parse_duration(e.get('duration')), 'timestamp': unified_timestamp(e.get('uploadDate')), # author can be an instance of 'Organization' or 'Person' types. @@ -2803,13 +2804,18 @@ class InfoExtractor: mime_type = representation_attrib['mimeType'] content_type = representation_attrib.get('contentType', mime_type.split('/')[0]) - codecs = parse_codecs(representation_attrib.get('codecs', '')) + codec_str = representation_attrib.get('codecs', '') + # Some kind of binary subtitle found in some youtube livestreams + if mime_type == 'application/x-rawcc': + codecs = {'scodec': codec_str} + else: + codecs = parse_codecs(codec_str) if content_type not in ('video', 'audio', 'text'): if mime_type == 'image/jpeg': content_type = mime_type - elif codecs['vcodec'] != 'none': + elif codecs.get('vcodec', 'none') != 'none': content_type = 'video' - elif codecs['acodec'] != 'none': + elif codecs.get('acodec', 'none') != 'none': content_type = 'audio' elif codecs.get('scodec', 'none') != 'none': content_type = 'text' diff --git a/yt_dlp/extractor/fc2.py b/yt_dlp/extractor/fc2.py index 225677b00..54b136ec7 100644 --- a/yt_dlp/extractor/fc2.py +++ b/yt_dlp/extractor/fc2.py @@ -10,7 +10,6 @@ from ..utils import ( WebSocketsWrapper, js_to_json, sanitized_Request, - std_headers, traverse_obj, update_url_query, urlencode_postdata, @@ -207,7 +206,7 @@ class FC2LiveIE(InfoExtractor): 'Cookie': str(self._get_cookies('https://live.fc2.com/'))[12:], 'Origin': 'https://live.fc2.com', 'Accept': '*/*', - 'User-Agent': std_headers['User-Agent'], + 'User-Agent': self.get_param('http_headers')['User-Agent'], }) self.write_debug('[debug] Sending HLS server request') diff --git a/yt_dlp/extractor/voicy.py b/yt_dlp/extractor/voicy.py index e4570a03a..feab79138 100644 --- a/yt_dlp/extractor/voicy.py +++ b/yt_dlp/extractor/voicy.py @@ -1,3 +1,5 @@ +import itertools + from .common import InfoExtractor from ..compat import compat_str from ..utils import ( @@ -9,8 +11,6 @@ from ..utils import ( unsmuggle_url, ) -import itertools - class VoicyBaseIE(InfoExtractor): def _extract_from_playlist_data(self, value): @@ -105,7 +105,7 @@ class VoicyChannelIE(VoicyBaseIE): @classmethod def suitable(cls, url): - return not VoicyIE.suitable(url) and super(VoicyChannelIE, cls).suitable(url) + return not VoicyIE.suitable(url) and super().suitable(url) def _entries(self, channel_id): pager = '' diff --git a/yt_dlp/utils.py b/yt_dlp/utils.py index 3b0e6750c..bcdb7d55b 100644 --- a/yt_dlp/utils.py +++ b/yt_dlp/utils.py @@ -714,7 +714,9 @@ def sanitize_path(s, force=False): def sanitize_url(url): # Prepend protocol-less URLs with `http:` scheme in order to mitigate # the number of unwanted failures due to missing protocol - if url.startswith('//'): + if url is None: + return + elif url.startswith('//'): return 'http:%s' % url # Fix some common typos seen so far COMMON_TYPOS = ( -- cgit v1.2.3 From b14d523558c04dd0c3ae17877f22f707a805cf5b Mon Sep 17 00:00:00 2001 From: pukkandan Date: Wed, 18 May 2022 08:28:18 +0530 Subject: Release 2022.05.18 --- CONTRIBUTORS | 17 ++++++++ Changelog.md | 118 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 7 ++-- supportedsites.md | 33 +++++++++++---- 4 files changed, 165 insertions(+), 10 deletions(-) diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 9b29acb0c..14d7e2b71 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -231,3 +231,20 @@ Fam0r bohwaz dodrian vvto33 +ca-za +connercsbn +diegorodriguezv +ekangmonyet +elyse0 +evansp +GiedriusS +HE7086 +JordanWeatherby +m4tu4g +MarwenDallel +nevack +putnam +rand-net +vertan +Wikidepia +Yipten diff --git a/Changelog.md b/Changelog.md index 52ea03367..ad81905da 100644 --- a/Changelog.md +++ b/Changelog.md @@ -11,6 +11,124 @@ --> +### 2022.05.18 + +* Add support for SSL client certificate authentication by [coletdjnz](https://github.com/coletdjnz), [dirkf](https://github.com/dirkf) + * Adds `--client-certificate`, `--client-certificate-key`, `--client-certificate-password` +* Add `--match-filter -` to interactively ask for each video +* `--max-downloads` should obey `--break-per-input` +* Allow use of weaker ciphers with `--legacy-server-connect` +* Don't imply `-s` for later stages of `-O` +* Fix `--date today` +* Fix `--skip-unavailable-fragments` +* Fix color in `-q -F` +* Fix redirect HTTP method handling by [coletdjnz](https://github.com/coletdjnz) +* Improve `--clean-infojson` +* Remove warning for videos with an empty title +* Run `FFmpegFixupM3u8PP` for live-streams if needed +* Show name of downloader in verbose log +* [cookies] Allow `cookiefile` to be a text stream +* [cookies] Report progress when importing cookies +* [downloader/ffmpeg] Specify headers for each URL by [elyse0](https://github.com/elyse0) +* [fragment] Do not change chunk-size when `--test` +* [fragment] Make single thread download work for `--live-from-start` by [Lesmiscore](https://github.com/Lesmiscore) +* [hls] Fix `byte_range` for `EXT-X-MAP` fragment by [fstirlitz](https://github.com/fstirlitz) +* [http] Fix retrying on read timeout by [coletdjnz](https://github.com/coletdjnz) +* [ffmpeg] Fix features detection +* [EmbedSubtitle] Enable for more video extensions +* [EmbedThumbnail] Disable thumbnail conversion for mkv by [evansp](https://github.com/evansp) +* [EmbedThumbnail] Do not obey `-k` +* [EmbedThumbnail] Do not remove id3v1 tags +* [FFmpegMetadata] Remove `\0` from metadata +* [FFmpegMetadata] Remove filename from attached info-json +* [FixupM3u8] Obey `--hls-prefer-mpegts` +* [Sponsorblock] Don't crash when duration is unknown +* [XAttrMetadata] Refactor and document dependencies +* [extractor] Document netrc machines +* [extractor] Update `manifest_url`s after redirect by [elyse0](https://github.com/elyse0) +* [extractor] Update dash `manifest_url` after redirects by [elyse0](https://github.com/elyse0) +* [extractor] Use `classmethod`/`property` where possible +* [generic] Refactor `_extract_rss` +* [utils] `is_html`: Handle double BOM +* [utils] `locked_file`: Ignore illegal seek on `truncate` by [jakeogh](https://github.com/jakeogh) +* [utils] `sanitize_path`: Fix when path is empty string +* [utils] `write_string`: Workaround newline issue in `conhost` +* [utils] `certifi`: Make sure the pem file exists +* [utils] Fix `WebSocketsWrapper` +* [utils] `locked_file`: Do not give executable bits for newly created files by [Lesmiscore](https://github.com/Lesmiscore) +* [utils] `YoutubeDLCookieJar`: Detect and reject JSON file by [Lesmiscore](https://github.com/Lesmiscore) +* [test] Convert warnings into errors and fix some existing warnings by [fstirlitz](https://github.com/fstirlitz) +* [dependencies] Create module with all dependency imports +* [compat] Split into sub-modules by [fstirlitz](https://github.com/fstirlitz), [pukkandan](https://github.com/pukkandan) +* [compat] Implement `compat.imghdr` +* [build] Add `make uninstall` by [MrRawes](https://github.com/MrRawes) +* [build] Avoid use of `install -D` +* [build] Fix `Makefile` by [putnam](https://github.com/putnam) +* [build] Fix `--onedir` on macOS +* [build] Add more test-runners +* [cleanup] Deprecate some compat vars by [fstirlitz](https://github.com/fstirlitz), [pukkandan](https://github.com/pukkandan) +* [cleanup] Remove unused code paths, extractors, scripts and tests by [fstirlitz](https://github.com/fstirlitz) +* [cleanup] Upgrade syntax (`pyupgrade`) and sort imports (`isort`) +* [cleanup, docs, build] Misc fixes +* [BilibiliLive] Add extractor by [HE7086](https://github.com/HE7086), [pukkandan](https://github.com/pukkandan) +* [Fifa] Add Extractor by [Bricio](https://github.com/Bricio) +* [goodgame] Add extractor by [nevack](https://github.com/nevack) +* [gronkh] Add playlist extractors by [hatienl0i261299](https://github.com/hatienl0i261299) +* [icareus] Add extractor by [tpikonen](https://github.com/tpikonen), [pukkandan](https://github.com/pukkandan) +* [iwara] Add playlist extractors by [i6t](https://github.com/i6t) +* [Likee] Add extractor by [hatienl0i261299](https://github.com/hatienl0i261299) +* [masters] Add extractor by [m4tu4g](https://github.com/m4tu4g) +* [nebula] Add support for subscriptions by [hheimbuerger](https://github.com/hheimbuerger) +* [Podchaser] Add extractors by [connercsbn](https://github.com/connercsbn) +* [rokfin:search] Add extractor by [P-reducible](https://github.com/P-reducible), [pukkandan](https://github.com/pukkandan) +* [youtube] Add `:ytnotifications` extractor by [krichbanana](https://github.com/krichbanana) +* [youtube] Add YoutubeStoriesIE (`ytstories:`) by [coletdjnz](https://github.com/coletdjnz) +* [ZingMp3] Add chart and user extractors by [hatienl0i261299](https://github.com/hatienl0i261299) +* [adn] Update AES key by [elyse0](https://github.com/elyse0) +* [adobepass] Allow cookies for authenticating MSO +* [bandcamp] Exclude merch links by [Yipten](https://github.com/Yipten) +* [chingari] Fix archiving and tests +* [DRTV] Improve `_VALID_URL` by [vertan](https://github.com/vertan) +* [facebook] Improve thumbnail extraction by [Wikidepia](https://github.com/Wikidepia) +* [fc2] Stop heatbeating once FFmpeg finishes by [Lesmiscore](https://github.com/Lesmiscore) +* [Gofile] Fix extraction and support password-protected links by [mehq](https://github.com/mehq) +* [hotstar, cleanup] Refactor extractors +* [InfoQ] Don't fail on missing audio format by [evansp](https://github.com/evansp) +* [Jamendo] Extract more metadata by [evansp](https://github.com/evansp) +* [kaltura] Update API calls by [flashdagger](https://github.com/flashdagger) +* [KhanAcademy] Fix extractor by [rand-net](https://github.com/rand-net) +* [LCI] Fix extractor by [MarwenDallel](https://github.com/MarwenDallel) +* [lrt] Support livestreams by [GiedriusS](https://github.com/GiedriusS) +* [niconico] Set `expected_protocol` to a public field +* [Niconico] Support 2FA by [ekangmonyet](https://github.com/ekangmonyet) +* [Olympics] Fix format extension +* [openrec:movie] Enable fallback for /movie/ URLs +* [PearVideo] Add fallback for formats by [hatienl0i261299](https://github.com/hatienl0i261299) +* [radiko] Fix extractor by [Lesmiscore](https://github.com/Lesmiscore) +* [rai] Add `release_year` +* [reddit] Prevent infinite loop +* [rokfin] Implement login by [P-reducible](https://github.com/P-reducible), [pukkandan](https://github.com/pukkandan) +* [ruutu] Support hs.fi embeds by [tpikonen](https://github.com/tpikonen), [pukkandan](https://github.com/pukkandan) +* [spotify] Detect iframe embeds by [fstirlitz](https://github.com/fstirlitz) +* [telegram] Fix metadata extraction +* [tmz, cleanup] Update tests by [diegorodriguezv](https://github.com/diegorodriguezv) +* [toggo] Fix `_VALID_URL` by [ca-za](https://github.com/ca-za) +* [trovo] Update to new API by [nyuszika7h](https://github.com/nyuszika7h) +* [TVer] Improve extraction by [Lesmiscore](https://github.com/Lesmiscore) +* [twitcasting] Pass headers for each formats by [Lesmiscore](https://github.com/Lesmiscore) +* [VideocampusSachsen] Improve extractor by [FestplattenSchnitzel](https://github.com/FestplattenSchnitzel) +* [vimeo] Fix extractors +* [wat] Fix extraction of multi-language videos and subtitles by [elyse0](https://github.com/elyse0) +* [wistia] Fix `_VALID_URL` by [dirkf](https://github.com/dirkf) +* [youtube, cleanup] Minor refactoring by [coletdjnz](https://github.com/coletdjnz), [pukkandan](https://github.com/pukkandan) +* [youtube] Added piped instance urls by [JordanWeatherby](https://github.com/JordanWeatherby) +* [youtube] Deprioritize auto-generated thumbnails +* [youtube] Deprioritize format 22 (often damaged) +* [youtube] Fix episode metadata extraction +* [zee5] Fix extractor by [Ashish0804](https://github.com/Ashish0804) +* [zingmp3, cleanup] Refactor extractors + + ### 2022.04.08 * Use certificates from `certifi` if installed by [coletdjnz](https://github.com/coletdjnz) diff --git a/README.md b/README.md index 7809c389a..256388f30 100644 --- a/README.md +++ b/README.md @@ -472,9 +472,10 @@ You can also fork the project on github and run your fork's [build workflow](.gi a file that is in the archive --break-on-reject Stop the download process when encountering a file that has been filtered out - --break-per-input Make --break-on-existing and --break-on- - reject act only on the current input URL - --no-break-per-input --break-on-existing and --break-on-reject + --break-per-input Make --break-on-existing, --break-on-reject + and --max-downloads act only on the current + input URL + --no-break-per-input --break-on-existing and similar options terminates the entire download queue --skip-playlist-after-errors N Number of allowed failures until the rest of the playlist is skipped diff --git a/supportedsites.md b/supportedsites.md index 7663c09d4..bbbfc6f94 100644 --- a/supportedsites.md +++ b/supportedsites.md @@ -376,6 +376,7 @@ - **fc2:embed** - **fc2:live** - **Fczenit** + - **Fifa** - **Filmmodu** - **filmon** - **filmon:channel** @@ -447,6 +448,7 @@ - **GodTube** - **Gofile** - **Golem** + - **goodgame:stream** - **google:podcasts** - **google:podcasts:feed** - **GoogleDrive** @@ -455,6 +457,8 @@ - **GoToStage** - **GPUTechConf** - **Gronkh** + - **gronkh:feed** + - **gronkh:vods** - **Groupon** - **hbo** - **HearThisAt** @@ -492,6 +496,7 @@ - **HungamaSong** - **huya:live**: huya.com - **Hypem** + - **Icareus** - **ign.com** - **IGNArticle** - **IGNVideo** @@ -528,6 +533,8 @@ - **ivi:compilation**: ivi.ru compilations - **ivideon**: Ivideon TV - **Iwara** + - **iwara:playlist** + - **iwara:user** - **Izlesene** - **Jable** - **JablePlaylist** @@ -587,6 +594,8 @@ - **Libsyn** - **life**: Life.ru - **life:embed** + - **likee** + - **likee:user** - **limelight** - **limelight:channel** - **limelight:channel_list** @@ -605,7 +614,8 @@ - **loc**: Library of Congress - **LocalNews8** - **LoveHomePorn** - - **lrt.lt** + - **LRTStream** + - **LRTVOD** - **lynda**: [lynda] lynda.com videos - **lynda:course**: [lynda] lynda.com online courses - **m6** @@ -626,6 +636,7 @@ - **Markiza** - **MarkizaPage** - **massengeschmack.tv** + - **Masters** - **MatchTV** - **MDR**: MDR.DE and KiKA - **MedalTV** @@ -747,7 +758,8 @@ - **ndr:embed:base** - **NDTV** - **Nebula**: [watchnebula] - - **nebula:collection**: [watchnebula] + - **nebula:channel**: [watchnebula] + - **nebula:subscriptions**: [watchnebula] - **NerdCubedFeed** - **netease:album**: 网易云音乐 - 专辑 - **netease:djradio**: 网易云音乐 - 电台 @@ -927,6 +939,7 @@ - **pluralsight**: [pluralsight] - **pluralsight:course** - **PlutoTV** + - **Podchaser** - **podomatic** - **Pokemon** - **PokemonSoundLibrary** @@ -1026,9 +1039,10 @@ - **RICE** - **RMCDecouverte** - **RockstarGames** - - **Rokfin** - - **rokfin:channel** - - **rokfin:stack** + - **Rokfin**: [rokfin] + - **rokfin:channel**: Rokfin Channels + - **rokfin:search**: Rokfin Search; "rkfnsearch:" prefix + - **rokfin:stack**: Rokfin Stacks - **RoosterTeeth**: [roosterteeth] - **RoosterTeethSeries**: [roosterteeth] - **RottenTomatoes** @@ -1370,8 +1384,6 @@ - **video.google:search**: Google Video search; "gvsearch:" prefix - **video.sky.it** - **video.sky.it:live** - - **VideocampusSachsen** - - **VideocampusSachsenEmbed** - **VideoDetective** - **videofy.me** - **videomore** @@ -1400,6 +1412,7 @@ - **vimeo:watchlater**: [vimeo] Vimeo watch later list, ":vimeowatchlater" keyword (requires authentication) - **Vimm:recording** - **Vimm:stream** + - **Vimp** - **Vimple**: Vimple - one-click video hosting - **Vine** - **vine:user** @@ -1525,11 +1538,13 @@ - **youtube:favorites**: YouTube liked videos; ":ytfav" keyword (requires cookies) - **youtube:history**: Youtube watch history; ":ythis" keyword (requires cookies) - **youtube:music:search_url**: YouTube music search URLs with selectable sections (Eg: #songs) + - **youtube:notif**: YouTube notifications; ":ytnotif" keyword (requires cookies) - **youtube:playlist**: YouTube playlists - **youtube:recommended**: YouTube recommended videos; ":ytrec" keyword - **youtube:search**: YouTube search; "ytsearch:" prefix - **youtube:search:date**: YouTube search, newest videos first; "ytsearchdate:" prefix - **youtube:search_url**: YouTube search URLs with sorting and filter support + - **youtube:stories**: YouTube channel stories; "ytstories:" prefix - **youtube:subscriptions**: YouTube subscriptions feed; ":ytsubs" keyword (requires cookies) - **youtube:tab**: YouTube Tabs - **youtube:user**: YouTube user videos; "ytuser:" prefix @@ -1550,6 +1565,10 @@ - **Zhihu** - **zingmp3**: zingmp3.vn - **zingmp3:album** + - **zingmp3:chart-home** + - **zingmp3:chart-music-video** + - **zingmp3:user** + - **zingmp3:week-chart** - **zoom** - **Zype** - **generic**: Generic downloader that works on some sites -- cgit v1.2.3 From 926ccc84ef91498f3147b07d15eb5f40cd070471 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 18 May 2022 03:37:32 +0000 Subject: [version] update Created by: pukkandan :ci skip all --- .github/ISSUE_TEMPLATE/1_broken_site.yml | 6 +++--- .github/ISSUE_TEMPLATE/2_site_support_request.yml | 6 +++--- .github/ISSUE_TEMPLATE/3_site_feature_request.yml | 6 +++--- .github/ISSUE_TEMPLATE/4_bug_report.yml | 6 +++--- .github/ISSUE_TEMPLATE/5_feature_request.yml | 2 +- yt_dlp/version.py | 4 ++-- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/1_broken_site.yml b/.github/ISSUE_TEMPLATE/1_broken_site.yml index 9a89bbd57..b34699ca4 100644 --- a/.github/ISSUE_TEMPLATE/1_broken_site.yml +++ b/.github/ISSUE_TEMPLATE/1_broken_site.yml @@ -11,7 +11,7 @@ body: options: - label: I'm reporting a broken site required: true - - label: I've verified that I'm running yt-dlp version **2022.04.08** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit) + - label: I've verified that I'm running yt-dlp version **2022.05.18** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit) required: true - label: I've checked that all provided URLs are playable in a browser with the same IP and same login details required: true @@ -51,12 +51,12 @@ body: [debug] Portable config file: yt-dlp.conf [debug] Portable config: ['-i'] [debug] Encodings: locale cp1252, fs utf-8, stdout utf-8, stderr utf-8, pref cp1252 - [debug] yt-dlp version 2022.04.08 (exe) + [debug] yt-dlp version 2022.05.18 (exe) [debug] Python version 3.8.8 (CPython 64bit) - Windows-10-10.0.19041-SP0 [debug] exe versions: ffmpeg 3.0.1, ffprobe 3.0.1 [debug] Optional libraries: Cryptodome, keyring, mutagen, sqlite, websockets [debug] Proxy map: {} - yt-dlp is up to date (2022.04.08) + yt-dlp is up to date (2022.05.18) render: shell validations: diff --git a/.github/ISSUE_TEMPLATE/2_site_support_request.yml b/.github/ISSUE_TEMPLATE/2_site_support_request.yml index 1f5f91c03..2ab0965d3 100644 --- a/.github/ISSUE_TEMPLATE/2_site_support_request.yml +++ b/.github/ISSUE_TEMPLATE/2_site_support_request.yml @@ -11,7 +11,7 @@ body: options: - label: I'm reporting a new site support request required: true - - label: I've verified that I'm running yt-dlp version **2022.04.08** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit) + - label: I've verified that I'm running yt-dlp version **2022.05.18** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit) required: true - label: I've checked that all provided URLs are playable in a browser with the same IP and same login details required: true @@ -62,12 +62,12 @@ body: [debug] Portable config file: yt-dlp.conf [debug] Portable config: ['-i'] [debug] Encodings: locale cp1252, fs utf-8, stdout utf-8, stderr utf-8, pref cp1252 - [debug] yt-dlp version 2022.04.08 (exe) + [debug] yt-dlp version 2022.05.18 (exe) [debug] Python version 3.8.8 (CPython 64bit) - Windows-10-10.0.19041-SP0 [debug] exe versions: ffmpeg 3.0.1, ffprobe 3.0.1 [debug] Optional libraries: Cryptodome, keyring, mutagen, sqlite, websockets [debug] Proxy map: {} - yt-dlp is up to date (2022.04.08) + yt-dlp is up to date (2022.05.18) render: shell validations: diff --git a/.github/ISSUE_TEMPLATE/3_site_feature_request.yml b/.github/ISSUE_TEMPLATE/3_site_feature_request.yml index 5c4287a3d..7a81cede6 100644 --- a/.github/ISSUE_TEMPLATE/3_site_feature_request.yml +++ b/.github/ISSUE_TEMPLATE/3_site_feature_request.yml @@ -11,7 +11,7 @@ body: options: - label: I'm reporting a site feature request required: true - - label: I've verified that I'm running yt-dlp version **2022.04.08** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit) + - label: I've verified that I'm running yt-dlp version **2022.05.18** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit) required: true - label: I've checked that all provided URLs are playable in a browser with the same IP and same login details required: true @@ -60,12 +60,12 @@ body: [debug] Portable config file: yt-dlp.conf [debug] Portable config: ['-i'] [debug] Encodings: locale cp1252, fs utf-8, stdout utf-8, stderr utf-8, pref cp1252 - [debug] yt-dlp version 2022.04.08 (exe) + [debug] yt-dlp version 2022.05.18 (exe) [debug] Python version 3.8.8 (CPython 64bit) - Windows-10-10.0.19041-SP0 [debug] exe versions: ffmpeg 3.0.1, ffprobe 3.0.1 [debug] Optional libraries: Cryptodome, keyring, mutagen, sqlite, websockets [debug] Proxy map: {} - yt-dlp is up to date (2022.04.08) + yt-dlp is up to date (2022.05.18) render: shell validations: diff --git a/.github/ISSUE_TEMPLATE/4_bug_report.yml b/.github/ISSUE_TEMPLATE/4_bug_report.yml index 6fbc36173..347ddd5d3 100644 --- a/.github/ISSUE_TEMPLATE/4_bug_report.yml +++ b/.github/ISSUE_TEMPLATE/4_bug_report.yml @@ -11,7 +11,7 @@ body: options: - label: I'm reporting a bug unrelated to a specific site required: true - - label: I've verified that I'm running yt-dlp version **2022.04.08** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit) + - label: I've verified that I'm running yt-dlp version **2022.05.18** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit) required: true - label: I've checked that all provided URLs are playable in a browser with the same IP and same login details required: true @@ -45,12 +45,12 @@ body: [debug] Portable config file: yt-dlp.conf [debug] Portable config: ['-i'] [debug] Encodings: locale cp1252, fs utf-8, stdout utf-8, stderr utf-8, pref cp1252 - [debug] yt-dlp version 2022.04.08 (exe) + [debug] yt-dlp version 2022.05.18 (exe) [debug] Python version 3.8.8 (CPython 64bit) - Windows-10-10.0.19041-SP0 [debug] exe versions: ffmpeg 3.0.1, ffprobe 3.0.1 [debug] Optional libraries: Cryptodome, keyring, mutagen, sqlite, websockets [debug] Proxy map: {} - yt-dlp is up to date (2022.04.08) + yt-dlp is up to date (2022.05.18) render: shell validations: diff --git a/.github/ISSUE_TEMPLATE/5_feature_request.yml b/.github/ISSUE_TEMPLATE/5_feature_request.yml index 59c8dd88e..08115f799 100644 --- a/.github/ISSUE_TEMPLATE/5_feature_request.yml +++ b/.github/ISSUE_TEMPLATE/5_feature_request.yml @@ -13,7 +13,7 @@ body: required: true - label: I've looked through the [README](https://github.com/yt-dlp/yt-dlp#readme) required: true - - label: I've verified that I'm running yt-dlp version **2022.04.08** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit) + - label: I've verified that I'm running yt-dlp version **2022.05.18** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit) required: true - label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar issues including closed ones. DO NOT post duplicates required: true diff --git a/yt_dlp/version.py b/yt_dlp/version.py index fb3ec8c6d..5ee7234d8 100644 --- a/yt_dlp/version.py +++ b/yt_dlp/version.py @@ -1,5 +1,5 @@ # Autogenerated by devscripts/update-version.py -__version__ = '2022.04.08' +__version__ = '2022.05.18' -RELEASE_GIT_HEAD = '7884ade65' +RELEASE_GIT_HEAD = 'b14d52355' -- cgit v1.2.3 From 9e491463521c65ca4d1d44a757e0a115f62834f5 Mon Sep 17 00:00:00 2001 From: pukkandan Date: Thu, 19 May 2022 19:45:21 +0530 Subject: Add option `--alias` --- Makefile | 2 +- README.md | 16 +++++++ yt_dlp/__init__.py | 40 +++++++++------- yt_dlp/options.py | 135 ++++++++++++++++++++++++++++++++++++++++++++--------- yt_dlp/utils.py | 22 +++++---- 5 files changed, 167 insertions(+), 48 deletions(-) diff --git a/Makefile b/Makefile index 8ce2e94c5..0ff5626ad 100644 --- a/Makefile +++ b/Makefile @@ -92,7 +92,7 @@ yt-dlp: yt_dlp/*.py yt_dlp/*/*.py chmod a+x yt-dlp README.md: yt_dlp/*.py yt_dlp/*/*.py - COLUMNS=80 $(PYTHON) yt_dlp/__main__.py --help | $(PYTHON) devscripts/make_readme.py + COLUMNS=80 $(PYTHON) yt_dlp/__main__.py --ignore-config --help | $(PYTHON) devscripts/make_readme.py CONTRIBUTING.md: README.md $(PYTHON) devscripts/make_contributing.py README.md CONTRIBUTING.md diff --git a/README.md b/README.md index 256388f30..d1e44365a 100644 --- a/README.md +++ b/README.md @@ -381,6 +381,22 @@ You can also fork the project on github and run your fork's [build workflow](.gi configurations by reverting some of the changes made in yt-dlp. See "Differences in default behavior" for details + --alias ALIASES OPTIONS Create aliases for an option string. Unless + an alias starts with a dash "-", it is + prefixed with "--". Arguments are parsed + according to the Python string formatting + mini-language. Eg: --alias get-audio,-X + "-S=aext:{0},abr -x --audio-format {0}" + creates options "--get-audio" and "-X" that + takes an argument (ARG0) and expands to + "-S=aext:ARG0,abr -x --audio-format ARG0". + All defined aliases are listed in the + --help output. Alias options can trigger + more aliases; so be carefull to avoid + defining recursive options. As a safety + measure, each alias may be triggered a + maximum of 100 times. This option can be + used multiple times ## Network Options: --proxy URL Use the specified HTTP/HTTPS/SOCKS proxy. diff --git a/yt_dlp/__init__.py b/yt_dlp/__init__.py index 8f890b34a..81b1716df 100644 --- a/yt_dlp/__init__.py +++ b/yt_dlp/__init__.py @@ -4,6 +4,7 @@ f'You are using an unsupported version of Python. Only Python versions 3.6 and a __license__ = 'Public Domain' import itertools +import optparse import os import re import sys @@ -45,11 +46,18 @@ from .utils import ( setproctitle, std_headers, traverse_obj, + variadic, write_string, ) from .YoutubeDL import YoutubeDL +def _exit(status=0, *args): + for msg in args: + sys.stderr.write(msg) + raise SystemExit(status) + + def get_urls(urls, batchfile, verbose): # Batch file verification batch_urls = [] @@ -66,7 +74,7 @@ def get_urls(urls, batchfile, verbose): if verbose: write_string('[debug] Batch file urls: ' + repr(batch_urls) + '\n') except OSError: - sys.exit('ERROR: batch file %s could not be read' % batchfile) + _exit(f'ERROR: batch file {batchfile} could not be read') _enc = preferredencoding() return [ url.strip().decode(_enc, 'ignore') if isinstance(url, bytes) else url.strip() @@ -810,10 +818,10 @@ def _real_main(argv=None): if opts.dump_user_agent: ua = traverse_obj(opts.headers, 'User-Agent', casesense=False, default=std_headers['User-Agent']) write_string(f'{ua}\n', out=sys.stdout) - sys.exit(0) + return if print_extractor_information(opts, all_urls): - sys.exit(0) + return with YoutubeDL(ydl_opts) as ydl: actual_use = all_urls or opts.load_info_filename @@ -827,13 +835,13 @@ def _real_main(argv=None): # If updater returns True, exit. Required for windows if run_update(ydl): if actual_use: - sys.exit('ERROR: The program must exit for the update to complete') - sys.exit() + return 100, 'ERROR: The program must exit for the update to complete' + return # Maybe do nothing if not actual_use: if opts.update_self or opts.rm_cachedir: - sys.exit() + return ydl.warn_if_short_id(sys.argv[1:] if argv is None else argv) parser.error( @@ -842,30 +850,30 @@ def _real_main(argv=None): try: if opts.load_info_filename is not None: - retcode = ydl.download_with_info_file(expand_path(opts.load_info_filename)) + return ydl.download_with_info_file(expand_path(opts.load_info_filename)) else: - retcode = ydl.download(all_urls) + return ydl.download(all_urls) except DownloadCancelled: ydl.to_screen('Aborting remaining downloads') - retcode = 101 - - sys.exit(retcode) + return 101 def main(argv=None): try: - _real_main(argv) + _exit(*variadic(_real_main(argv))) except DownloadError: - sys.exit(1) + _exit(1) except SameFileError as e: - sys.exit(f'ERROR: {e}') + _exit(f'ERROR: {e}') except KeyboardInterrupt: - sys.exit('\nERROR: Interrupted by user') + _exit('\nERROR: Interrupted by user') except BrokenPipeError as e: # https://docs.python.org/3/library/signal.html#note-on-sigpipe devnull = os.open(os.devnull, os.O_WRONLY) os.dup2(devnull, sys.stdout.fileno()) - sys.exit(f'\nERROR: {e}') + _exit(f'\nERROR: {e}') + except optparse.OptParseError as e: + _exit(2, f'\n{e}') from .extractor import gen_extractors, list_extractors diff --git a/yt_dlp/options.py b/yt_dlp/options.py index 2e8d384c0..1efdc8957 100644 --- a/yt_dlp/options.py +++ b/yt_dlp/options.py @@ -1,7 +1,10 @@ +import collections +import contextlib import optparse import os.path import re import shlex +import string import sys from .compat import compat_expanduser, compat_get_terminal_size, compat_getenv @@ -15,6 +18,7 @@ from .postprocessor import ( SponsorBlockPP, ) from .postprocessor.modify_chapters import DEFAULT_SPONSORBLOCK_CHAPTER_TITLE +from .update import detect_variant from .utils import ( OUTTMPL_TYPES, POSTPROCESS_WHEN, @@ -29,15 +33,9 @@ from .version import __version__ def parseOpts(overrideArguments=None, ignore_config_files='if_override'): - parser = create_parser() - root = Config(parser) - + root = Config(create_parser()) if ignore_config_files == 'if_override': ignore_config_files = overrideArguments is not None - if overrideArguments: - root.append_config(overrideArguments, label='Override') - else: - root.append_config(sys.argv[1:], label='Command-line') def _readUserConf(package_name, default=[]): # .config @@ -73,7 +71,7 @@ def parseOpts(overrideArguments=None, ignore_config_files='if_override'): def add_config(label, path, user=False): """ Adds config and returns whether to continue """ - if root.parse_args()[0].ignoreconfig: + if root.parse_known_args()[0].ignoreconfig: return False # Multiple package names can be given here # Eg: ('yt-dlp', 'youtube-dlc', 'youtube-dl') will look for @@ -92,22 +90,44 @@ def parseOpts(overrideArguments=None, ignore_config_files='if_override'): def load_configs(): yield not ignore_config_files yield add_config('Portable', get_executable_path()) - yield add_config('Home', expand_path(root.parse_args()[0].paths.get('home', '')).strip()) + yield add_config('Home', expand_path(root.parse_known_args()[0].paths.get('home', '')).strip()) yield add_config('User', None, user=True) yield add_config('System', '/etc') - if all(load_configs()): - # If ignoreconfig is found inside the system configuration file, - # the user configuration is removed - if root.parse_args()[0].ignoreconfig: - user_conf = next((i for i, conf in enumerate(root.configs) if conf.label == 'User'), None) - if user_conf is not None: - root.configs.pop(user_conf) + opts = optparse.Values({'verbose': True, 'print_help': False}) + try: + if overrideArguments: + root.append_config(overrideArguments, label='Override') + else: + root.append_config(sys.argv[1:], label='Command-line') + + if all(load_configs()): + # If ignoreconfig is found inside the system configuration file, + # the user configuration is removed + if root.parse_known_args()[0].ignoreconfig: + user_conf = next((i for i, conf in enumerate(root.configs) if conf.label == 'User'), None) + if user_conf is not None: + root.configs.pop(user_conf) - opts, args = root.parse_args() - if opts.verbose: - write_string(f'\n{root}'.replace('\n| ', '\n[debug] ')[1:] + '\n') - return parser, opts, args + opts, args = root.parse_args() + except optparse.OptParseError: + with contextlib.suppress(optparse.OptParseError): + opts, _ = root.parse_known_args(strict=False) + raise + except (SystemExit, KeyboardInterrupt): + opts.verbose = False + raise + finally: + verbose = opts.verbose and f'\n{root}'.replace('\n| ', '\n[debug] ')[1:] + if verbose: + write_string(f'{verbose}\n') + if opts.print_help: + if verbose: + write_string('\n') + root.parser.print_help() + if opts.print_help: + sys.exit() + return root.parser, opts, args class _YoutubeDLHelpFormatter(optparse.IndentedHelpFormatter): @@ -133,10 +153,11 @@ class _YoutubeDLHelpFormatter(optparse.IndentedHelpFormatter): class _YoutubeDLOptionParser(optparse.OptionParser): # optparse is deprecated since python 3.2. So assume a stable interface even for private methods + ALIAS_TRIGGER_LIMIT = 100 def __init__(self): super().__init__( - prog='yt-dlp', + prog='yt-dlp' if detect_variant() == 'source' else None, version=__version__, usage='%prog [OPTIONS] URL [URL...]', epilog='See full documentation at https://github.com/yt-dlp/yt-dlp#readme', @@ -144,6 +165,29 @@ class _YoutubeDLOptionParser(optparse.OptionParser): conflict_handler='resolve', ) + _UNKNOWN_OPTION = (optparse.BadOptionError, optparse.AmbiguousOptionError) + _BAD_OPTION = optparse.OptionValueError + + def parse_known_args(self, args=None, values=None, strict=True): + """Same as parse_args, but ignore unknown switches. Similar to argparse.parse_known_args""" + self.rargs, self.largs = self._get_args(args), [] + self.values = values or self.get_default_values() + while self.rargs: + try: + self._process_args(self.largs, self.rargs, self.values) + except optparse.OptParseError as err: + if isinstance(err, self._UNKNOWN_OPTION): + self.largs.append(err.opt_str) + elif strict: + if isinstance(err, self._BAD_OPTION): + self.error(str(err)) + raise + return self.check_values(self.values, self.largs) + + def error(self, msg): + msg = f'{self.get_prog_name()}: error: {msg.strip()}\n' + raise optparse.OptParseError(f'{self.get_usage()}\n{msg}' if self.usage else msg) + def _get_args(self, args): return sys.argv[1:] if args is None else list(args) @@ -223,11 +267,44 @@ def create_parser(): setattr(parser.values, option.dest, out_dict) parser = _YoutubeDLOptionParser() + alias_group = optparse.OptionGroup(parser, 'Aliases') + Formatter = string.Formatter() + + def _create_alias(option, opt_str, value, parser): + aliases, opts = value + try: + nargs = len({i if f == '' else f + for i, (_, f, _, _) in enumerate(Formatter.parse(opts)) if f is not None}) + opts.format(*map(str, range(nargs))) # validate + except Exception as err: + raise optparse.OptionValueError(f'wrong {opt_str} OPTIONS formatting; {err}') + if alias_group not in parser.option_groups: + parser.add_option_group(alias_group) + + aliases = (x if x.startswith('-') else f'--{x}' for x in map(str.strip, aliases.split(','))) + try: + alias_group.add_option( + *aliases, help=opts, nargs=nargs, type='str' if nargs else None, + dest='_triggered_aliases', default=collections.defaultdict(int), + metavar=' '.join(f'ARG{i}' for i in range(nargs)), action='callback', + callback=_alias_callback, callback_kwargs={'opts': opts, 'nargs': nargs}) + except Exception as err: + raise optparse.OptionValueError(f'wrong {opt_str} formatting; {err}') + + def _alias_callback(option, opt_str, value, parser, opts, nargs): + counter = getattr(parser.values, option.dest) + counter[opt_str] += 1 + if counter[opt_str] > parser.ALIAS_TRIGGER_LIMIT: + raise optparse.OptionValueError(f'Alias {opt_str} exceeded invocation limit') + if nargs == 1: + value = [value] + assert (nargs == 0 and value is None) or len(value) == nargs + parser.rargs[:0] = shlex.split( + opts if value is None else opts.format(*map(shlex.quote, value))) general = optparse.OptionGroup(parser, 'General Options') general.add_option( - '-h', '--help', - action='help', + '-h', '--help', dest='print_help', action='store_true', help='Print this help text and exit') general.add_option( '--version', @@ -344,6 +421,18 @@ def create_parser(): 'Options that can help keep compatibility with youtube-dl or youtube-dlc ' 'configurations by reverting some of the changes made in yt-dlp. ' 'See "Differences in default behavior" for details')) + general.add_option( + '--alias', metavar='ALIASES OPTIONS', dest='_', type='str', nargs=2, + action='callback', callback=_create_alias, + help=( + 'Create aliases for an option string. Unless an alias starts with a dash "-", it is prefixed with "--". ' + 'Arguments are parsed according to the Python string formatting mini-language. ' + 'Eg: --alias get-audio,-X "-S=aext:{0},abr -x --audio-format {0}" creates options ' + '"--get-audio" and "-X" that takes an argument (ARG0) and expands to ' + '"-S=aext:ARG0,abr -x --audio-format ARG0". All defined aliases are listed in the --help output. ' + 'Alias options can trigger more aliases; so be carefull to avoid defining recursive options. ' + f'As a safety measure, each alias may be triggered a maximum of {_YoutubeDLOptionParser.ALIAS_TRIGGER_LIMIT} times. ' + 'This option can be used multiple times')) network = optparse.OptionGroup(parser, 'Network Options') network.add_option( diff --git a/yt_dlp/utils.py b/yt_dlp/utils.py index bcdb7d55b..f02f71177 100644 --- a/yt_dlp/utils.py +++ b/yt_dlp/utils.py @@ -5153,11 +5153,12 @@ def parse_http_range(range): class Config: own_args = None + parsed_args = None filename = None __initialized = False def __init__(self, parser, label=None): - self._parser, self.label = parser, label + self.parser, self.label = parser, label self._loaded_paths, self.configs = set(), [] def init(self, args=None, filename=None): @@ -5170,14 +5171,16 @@ class Config: return False self._loaded_paths.add(location) - self.__initialized = True - self.own_args, self.filename = args, filename - for location in self._parser.parse_args(args)[0].config_locations or []: + self.own_args, self.__initialized = args, True + opts, _ = self.parser.parse_known_args(args) + self.parsed_args, self.filename = args, filename + + for location in opts.config_locations or []: location = os.path.join(directory, expand_path(location)) if os.path.isdir(location): location = os.path.join(location, 'yt-dlp.conf') if not os.path.exists(location): - self._parser.error(f'config location {location} does not exist') + self.parser.error(f'config location {location} does not exist') self.append_config(self.read_file(location), location) return True @@ -5223,7 +5226,7 @@ class Config: return opts def append_config(self, *args, label=None): - config = type(self)(self._parser, label) + config = type(self)(self.parser, label) config._loaded_paths = self._loaded_paths if config.init(*args): self.configs.append(config) @@ -5232,10 +5235,13 @@ class Config: def all_args(self): for config in reversed(self.configs): yield from config.all_args - yield from self.own_args or [] + yield from self.parsed_args or [] + + def parse_known_args(self, **kwargs): + return self.parser.parse_known_args(self.all_args, **kwargs) def parse_args(self): - return self._parser.parse_args(self.all_args) + return self.parser.parse_args(self.all_args) class WebSocketsWrapper(): -- cgit v1.2.3 From 23326151c45b632c3d5948bd018e80abb370e676 Mon Sep 17 00:00:00 2001 From: pukkandan Date: Thu, 19 May 2022 20:00:31 +0530 Subject: Add option --retry-sleep (#3059) Closes #2852 --- yt_dlp/YoutubeDL.py | 3 +++ yt_dlp/__init__.py | 23 +++++++++++++++++++++++ yt_dlp/downloader/common.py | 18 ++++++++++++++++-- yt_dlp/downloader/external.py | 1 + yt_dlp/downloader/fragment.py | 5 +---- yt_dlp/options.py | 12 ++++++++++++ 6 files changed, 56 insertions(+), 6 deletions(-) diff --git a/yt_dlp/YoutubeDL.py b/yt_dlp/YoutubeDL.py index ba08f6a7d..749cf9402 100644 --- a/yt_dlp/YoutubeDL.py +++ b/yt_dlp/YoutubeDL.py @@ -453,6 +453,9 @@ class YoutubeDL: Allowed keys are 'download', 'postprocess', 'download-title' (console title) and 'postprocess-title'. The template is mapped on a dictionary with keys 'progress' and 'info' + retry_sleep_functions: Dictionary of functions that takes the number of attempts + as argument and returns the time to sleep in seconds. + Allowed keys are 'http', 'fragment', 'file_access' The following parameters are not used by YoutubeDL itself, they are used by the downloader (see yt_dlp/downloader/common.py): diff --git a/yt_dlp/__init__.py b/yt_dlp/__init__.py index 81b1716df..b2429f5af 100644 --- a/yt_dlp/__init__.py +++ b/yt_dlp/__init__.py @@ -247,6 +247,28 @@ def validate_options(opts): opts.extractor_retries = parse_retries('extractor', opts.extractor_retries) opts.file_access_retries = parse_retries('file access', opts.file_access_retries) + # Retry sleep function + def parse_sleep_func(expr): + NUMBER_RE = r'\d+(?:\.\d+)?' + op, start, limit, step, *_ = tuple(re.fullmatch( + rf'(?:(linear|exp)=)?({NUMBER_RE})(?::({NUMBER_RE}))?(?::({NUMBER_RE}))?', + expr.strip()).groups()) + (None, None) + + if op == 'exp': + return lambda n: min(float(start) * (float(step or 2) ** n), float(limit or 'inf')) + else: + default_step = start if op or limit else 0 + return lambda n: min(float(start) + float(step or default_step) * n, float(limit or 'inf')) + + for key, expr in opts.retry_sleep.items(): + if not expr: + del opts.retry_sleep[key] + continue + try: + opts.retry_sleep[key] = parse_sleep_func(expr) + except AttributeError as e: + raise ValueError(f'invalid {key} retry sleep expression {expr!r}: {e}') + # Bytes def parse_bytes(name, value): if value is None: @@ -694,6 +716,7 @@ def parse_options(argv=None): 'file_access_retries': opts.file_access_retries, 'fragment_retries': opts.fragment_retries, 'extractor_retries': opts.extractor_retries, + 'retry_sleep_functions': opts.retry_sleep, 'skip_unavailable_fragments': opts.skip_unavailable_fragments, 'keep_fragments': opts.keep_fragments, 'concurrent_fragment_downloads': opts.concurrent_fragment_downloads, diff --git a/yt_dlp/downloader/common.py b/yt_dlp/downloader/common.py index 465b5ef99..0b3383071 100644 --- a/yt_dlp/downloader/common.py +++ b/yt_dlp/downloader/common.py @@ -19,6 +19,7 @@ from ..utils import ( encodeFilename, error_to_compat_str, format_bytes, + int_or_none, sanitize_open, shell_quote, timeconvert, @@ -64,6 +65,7 @@ class FileDownloader: useful for bypassing bandwidth throttling imposed by a webserver (experimental) progress_template: See YoutubeDL.py + retry_sleep_functions: See YoutubeDL.py Subclasses of this one must re-define the real_download method. """ @@ -98,6 +100,8 @@ class FileDownloader: def to_screen(self, *args, **kargs): self.ydl.to_screen(*args, quiet=self.params.get('quiet'), **kargs) + __to_screen = to_screen + @property def FD_NAME(self): return re.sub(r'(? fragment_retries: if not skip_unavailable_fragments: self.report_error('Giving up after %s fragment retries' % fragment_retries) diff --git a/yt_dlp/downloader/fragment.py b/yt_dlp/downloader/fragment.py index 4655f067f..410c8c1a4 100644 --- a/yt_dlp/downloader/fragment.py +++ b/yt_dlp/downloader/fragment.py @@ -25,10 +25,6 @@ class HttpQuietDownloader(HttpFD): console_title = to_screen - def report_retry(self, err, count, retries): - super().to_screen( - f'[download] Got server HTTP error: {err}. Retrying (attempt {count} of {self.format_retries(retries)}) ...') - class FragmentFD(FileDownloader): """ @@ -70,6 +66,7 @@ class FragmentFD(FileDownloader): self.to_screen( '\r[download] Got server HTTP error: %s. Retrying fragment %d (attempt %d of %s) ...' % (error_to_compat_str(err), frag_index, count, self.format_retries(retries))) + self.sleep_retry('fragment', count) def report_skip_fragment(self, frag_index, err=None): err = f' {err};' if err else '' diff --git a/yt_dlp/options.py b/yt_dlp/options.py index 1efdc8957..5c97facb7 100644 --- a/yt_dlp/options.py +++ b/yt_dlp/options.py @@ -828,6 +828,18 @@ def create_parser(): '--fragment-retries', dest='fragment_retries', metavar='RETRIES', default=10, help='Number of retries for a fragment (default is %default), or "infinite" (DASH, hlsnative and ISM)') + downloader.add_option( + '--retry-sleep', + dest='retry_sleep', metavar='[TYPE:]EXPR', default={}, type='str', + action='callback', callback=_dict_from_options_callback, + callback_kwargs={ + 'allowed_keys': 'http|fragment|file_access', + 'default_key': 'http', + }, help=( + 'An expression for the time to sleep between retries in seconds (optionally) prefixed ' + 'by the type of retry (http (default), fragment, file_access) to apply the sleep to. ' + 'EXPR can be a number, or of the forms linear=START[:END[:STEP=1]] or exp=START[:END[:BASE=2]]. ' + 'Eg: --retry-sleep linear=1::2 --retry-sleep fragment:exp=1:20')) downloader.add_option( '--skip-unavailable-fragments', '--no-abort-on-unavailable-fragment', action='store_true', dest='skip_unavailable_fragments', default=True, -- cgit v1.2.3 From 2dd5a2e3a1c0fad8441f8e9c7eb77315afcb075b Mon Sep 17 00:00:00 2001 From: pukkandan Date: Thu, 19 May 2022 20:05:17 +0530 Subject: [doc, cleanup] Re-indent "Usage and Options" section --- README.md | 1439 ++++++++++++++++++++++++++--------------------------- yt_dlp/options.py | 66 ++- 2 files changed, 750 insertions(+), 755 deletions(-) diff --git a/README.md b/README.md index d1e44365a..e71a150fd 100644 --- a/README.md +++ b/README.md @@ -320,767 +320,764 @@ You can also fork the project on github and run your fork's [build workflow](.gi ## General Options: - -h, --help Print this help text and exit - --version Print program version and exit - -U, --update Update this program to latest version - -i, --ignore-errors Ignore download and postprocessing errors. - The download will be considered successful - even if the postprocessing fails - --no-abort-on-error Continue with next video on download - errors; e.g. to skip unavailable videos in - a playlist (default) - --abort-on-error Abort downloading of further videos if an - error occurs (Alias: --no-ignore-errors) - --dump-user-agent Display the current user-agent and exit - --list-extractors List all supported extractors and exit - --extractor-descriptions Output descriptions of all supported - extractors and exit - --force-generic-extractor Force extraction to use the generic - extractor - --default-search PREFIX Use this prefix for unqualified URLs. For - example "gvsearch2:" downloads two videos - from google videos for the search term - "large apple". Use the value "auto" to let - yt-dlp guess ("auto_warning" to emit a - warning when guessing). "error" just throws - an error. The default value "fixup_error" - repairs broken URLs, but emits an error if - this is not possible instead of searching - --ignore-config Don't load any more configuration files - except those given by --config-locations. - For backward compatibility, if this option - is found inside the system configuration - file, the user configuration is not loaded. - (Alias: --no-config) - --no-config-locations Do not load any custom configuration files - (default). When given inside a - configuration file, ignore all previous - --config-locations defined in the current - file - --config-locations PATH Location of the main configuration file; - either the path to the config or its - containing directory. Can be used multiple - times and inside other configuration files - --flat-playlist Do not extract the videos of a playlist, - only list them - --no-flat-playlist Extract the videos of a playlist - --live-from-start Download livestreams from the start. - Currently only supported for YouTube - (Experimental) - --no-live-from-start Download livestreams from the current time - (default) - --wait-for-video MIN[-MAX] Wait for scheduled streams to become - available. Pass the minimum number of - seconds (or range) to wait between retries - --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) - --no-colors Do not emit color codes in output - --compat-options OPTS Options that can help keep compatibility - with youtube-dl or youtube-dlc - configurations by reverting some of the - changes made in yt-dlp. See "Differences in - default behavior" for details - --alias ALIASES OPTIONS Create aliases for an option string. Unless - an alias starts with a dash "-", it is - prefixed with "--". Arguments are parsed - according to the Python string formatting - mini-language. Eg: --alias get-audio,-X - "-S=aext:{0},abr -x --audio-format {0}" - creates options "--get-audio" and "-X" that - takes an argument (ARG0) and expands to - "-S=aext:ARG0,abr -x --audio-format ARG0". - All defined aliases are listed in the - --help output. Alias options can trigger - more aliases; so be carefull to avoid - defining recursive options. As a safety - measure, each alias may be triggered a - maximum of 100 times. This option can be - used multiple times + -h, --help Print this help text and exit + --version Print program version and exit + -U, --update Update this program to latest version + -i, --ignore-errors Ignore download and postprocessing errors. + The download will be considered successful + even if the postprocessing fails + --no-abort-on-error Continue with next video on download errors; + e.g. to skip unavailable videos in a + playlist (default) + --abort-on-error Abort downloading of further videos if an + error occurs (Alias: --no-ignore-errors) + --dump-user-agent Display the current user-agent and exit + --list-extractors List all supported extractors and exit + --extractor-descriptions Output descriptions of all supported + extractors and exit + --force-generic-extractor Force extraction to use the generic + extractor + --default-search PREFIX Use this prefix for unqualified URLs. Eg: + "gvsearch2:python" downloads two videos from + google videos for the search term "python". + Use the value "auto" to let yt-dlp guess + ("auto_warning" to emit a warning when + guessing). "error" just throws an error. The + default value "fixup_error" repairs broken + URLs, but emits an error if this is not + possible instead of searching + --ignore-config Don't load any more configuration files + except those given by --config-locations. + For backward compatibility, if this option + is found inside the system configuration + file, the user configuration is not loaded. + (Alias: --no-config) + --no-config-locations Do not load any custom configuration files + (default). When given inside a configuration + file, ignore all previous --config-locations + defined in the current file + --config-locations PATH Location of the main configuration file; + either the path to the config or its + containing directory. Can be used multiple + times and inside other configuration files + --flat-playlist Do not extract the videos of a playlist, + only list them + --no-flat-playlist Extract the videos of a playlist + --live-from-start Download livestreams from the start. + Currently only supported for YouTube + (Experimental) + --no-live-from-start Download livestreams from the current time + (default) + --wait-for-video MIN[-MAX] Wait for scheduled streams to become + available. Pass the minimum number of + seconds (or range) to wait between retries + --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) + --no-colors Do not emit color codes in output + --compat-options OPTS Options that can help keep compatibility + with youtube-dl or youtube-dlc + configurations by reverting some of the + changes made in yt-dlp. See "Differences in + default behavior" for details + --alias ALIASES OPTIONS Create aliases for an option string. Unless + an alias starts with a dash "-", it is + prefixed with "--". Arguments are parsed + according to the Python string formatting + mini-language. Eg: --alias get-audio,-X + "-S=aext:{0},abr -x --audio-format {0}" + creates options "--get-audio" and "-X" that + takes an argument (ARG0) and expands to + "-S=aext:ARG0,abr -x --audio-format ARG0". + All defined aliases are listed in the --help + output. Alias options can trigger more + aliases; so be carefull to avoid defining + recursive options. As a safety measure, each + alias may be triggered a maximum of 100 + times. This option can be used multiple + times ## Network Options: - --proxy URL Use the specified HTTP/HTTPS/SOCKS proxy. - To enable SOCKS proxy, specify a proper - scheme. For example - socks5://user:pass@127.0.0.1:1080/. Pass in - an empty string (--proxy "") for direct - connection - --socket-timeout SECONDS Time to wait before giving up, in seconds - --source-address IP Client-side IP address to bind to - -4, --force-ipv4 Make all connections via IPv4 - -6, --force-ipv6 Make all connections via IPv6 + --proxy URL Use the specified HTTP/HTTPS/SOCKS proxy. To + enable SOCKS proxy, specify a proper scheme. + Eg: socks5://user:pass@127.0.0.1:1080/. Pass + in an empty string (--proxy "") for direct + connection + --socket-timeout SECONDS Time to wait before giving up, in seconds + --source-address IP Client-side IP address to bind to + -4, --force-ipv4 Make all connections via IPv4 + -6, --force-ipv6 Make all connections via IPv6 ## Geo-restriction: - --geo-verification-proxy URL Use this proxy to verify the IP address for - some geo-restricted sites. The default - proxy specified by --proxy (or none, if the - option is not present) is used for the - actual downloading - --geo-bypass Bypass geographic restriction via faking - X-Forwarded-For HTTP header (default) - --no-geo-bypass Do not bypass geographic restriction via - faking X-Forwarded-For HTTP header - --geo-bypass-country CODE Force bypass geographic restriction with - explicitly provided two-letter ISO 3166-2 - country code - --geo-bypass-ip-block IP_BLOCK Force bypass geographic restriction with - explicitly provided IP block in CIDR - notation + --geo-verification-proxy URL Use this proxy to verify the IP address for + some geo-restricted sites. The default proxy + specified by --proxy (or none, if the option + is not present) is used for the actual + downloading + --geo-bypass Bypass geographic restriction via faking + X-Forwarded-For HTTP header (default) + --no-geo-bypass Do not bypass geographic restriction via + faking X-Forwarded-For HTTP header + --geo-bypass-country CODE Force bypass geographic restriction with + explicitly provided two-letter ISO 3166-2 + country code + --geo-bypass-ip-block IP_BLOCK Force bypass geographic restriction with + explicitly provided IP block in CIDR + notation ## 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 - 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 - --min-filesize SIZE Do not download any videos smaller than - SIZE (e.g. 50k or 44.6m) - --max-filesize SIZE Do not download any videos larger than SIZE - (e.g. 50k or 44.6m) - --date DATE Download only videos uploaded on this date. - The date can be "YYYYMMDD" or in the format - [now|today|yesterday][-N[day|week|month|year]]. - Eg: --date today-2weeks - --datebefore DATE Download only videos uploaded on or before - this date. The date formats accepted is the - same as --date - --dateafter DATE Download only videos uploaded on or after - this date. The date formats accepted is the - same as --date - --match-filters FILTER Generic video filter. Any field (see - "OUTPUT TEMPLATE") can be compared with a - number or a string using the operators - defined in "Filtering formats". You can - also simply specify a field to match if the - field is present, use "!field" to check if - the field is not present, and "&" to check - multiple conditions. Use a "\" to escape - "&" or quotes if needed. If used multiple - times, the filter matches if atleast one of - the conditions are met. Eg: --match-filter - !is_live --match-filter "like_count>?100 & - description~='(?i)\bcats \& dogs\b'" - matches only videos that are not live OR - those that have a like count more than 100 - (or the like field is not available) and - also has a description that contains the - phrase "cats & dogs" (ignoring case). Use - "--match-filter -" to interactively ask - whether to download each video - --no-match-filter Do not use generic video filter (default) - --no-playlist Download only the video, if the URL refers - to a video and a playlist - --yes-playlist Download the playlist, if the URL refers to - a video and a playlist - --age-limit YEARS Download only videos suitable for the given - age - --download-archive FILE Download only videos not listed in the - archive file. Record the IDs of all - downloaded videos in it - --no-download-archive Do not use archive file (default) - --max-downloads NUMBER Abort after downloading NUMBER files - --break-on-existing Stop the download process when encountering - a file that is in the archive - --break-on-reject Stop the download process when encountering - a file that has been filtered out - --break-per-input Make --break-on-existing, --break-on-reject - and --max-downloads act only on the current - input URL - --no-break-per-input --break-on-existing and similar options - terminates the entire download queue - --skip-playlist-after-errors N Number of allowed failures until the rest - of the playlist is skipped + --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 + 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 + --min-filesize SIZE Do not download any videos smaller than SIZE + (e.g. 50k or 44.6m) + --max-filesize SIZE Do not download any videos larger than SIZE + (e.g. 50k or 44.6m) + --date DATE Download only videos uploaded on this date. + The date can be "YYYYMMDD" or in the format + [now|today|yesterday][-N[day|week|month|year]]. + Eg: --date today-2weeks + --datebefore DATE Download only videos uploaded on or before + this date. The date formats accepted is the + same as --date + --dateafter DATE Download only videos uploaded on or after + this date. The date formats accepted is the + same as --date + --match-filters FILTER Generic video filter. Any "OUTPUT TEMPLATE" + field can be compared with a number or a + string using the operators defined in + "Filtering formats". You can also simply + specify a field to match if the field is + present, use "!field" to check if the field + is not present, and "&" to check multiple + conditions. Use a "\" to escape "&" or + quotes if needed. If used multiple times, + the filter matches if atleast one of the + conditions are met. Eg: --match-filter + !is_live --match-filter "like_count>?100 & + description~='(?i)\bcats \& dogs\b'" matches + only videos that are not live OR those that + have a like count more than 100 (or the like + field is not available) and also has a + description that contains the phrase "cats & + dogs" (caseless). Use "--match-filter -" to + interactively ask whether to download each + video + --no-match-filter Do not use generic video filter (default) + --no-playlist Download only the video, if the URL refers + to a video and a playlist + --yes-playlist Download the playlist, if the URL refers to + a video and a playlist + --age-limit YEARS Download only videos suitable for the given + age + --download-archive FILE Download only videos not listed in the + archive file. Record the IDs of all + downloaded videos in it + --no-download-archive Do not use archive file (default) + --max-downloads NUMBER Abort after downloading NUMBER files + --break-on-existing Stop the download process when encountering + a file that is in the archive + --break-on-reject Stop the download process when encountering + a file that has been filtered out + --break-per-input Make --break-on-existing, --break-on-reject + and --max-downloads act only on the current + input URL + --no-break-per-input --break-on-existing and similar options + terminates the entire download queue + --skip-playlist-after-errors N Number of allowed failures until the rest of + the playlist is skipped ## Download Options: - -N, --concurrent-fragments N Number of fragments of a dash/hlsnative - video that should be downloaded - concurrently (default is 1) - -r, --limit-rate RATE Maximum download rate in bytes per second - (e.g. 50K or 4.2M) - --throttled-rate RATE Minimum download rate in bytes per second - below which throttling is assumed and the - video data is re-extracted (e.g. 100K) - -R, --retries RETRIES Number of retries (default is 10), or - "infinite" - --file-access-retries RETRIES Number of times to retry on file access - error (default is 3), or "infinite" - --fragment-retries RETRIES Number of retries for a fragment (default - is 10), or "infinite" (DASH, hlsnative and - ISM) - --skip-unavailable-fragments Skip unavailable fragments for DASH, - hlsnative and ISM (default) - (Alias: --no-abort-on-unavailable-fragment) - --abort-on-unavailable-fragment Abort downloading if a fragment is unavailable - (Alias: --no-skip-unavailable-fragments) - --keep-fragments Keep downloaded fragments on disk after - downloading is finished - --no-keep-fragments Delete downloaded fragments after - downloading is finished (default) - --buffer-size SIZE Size of download buffer (e.g. 1024 or 16K) - (default is 1024) - --resize-buffer The buffer size is automatically resized - from an initial value of --buffer-size - (default) - --no-resize-buffer Do not automatically adjust the buffer size - --http-chunk-size SIZE Size of a chunk for chunk-based HTTP - downloading (e.g. 10485760 or 10M) (default - is disabled). May be useful for bypassing - bandwidth throttling imposed by a webserver - (experimental) - --playlist-reverse Download playlist videos in reverse order - --no-playlist-reverse Download playlist videos in default order - (default) - --playlist-random Download playlist videos in random order - --xattr-set-filesize Set file xattribute ytdl.filesize with - expected file size - --hls-use-mpegts Use the mpegts container for HLS videos; - allowing some players to play the video - while downloading, and reducing the chance - of file corruption if download is - interrupted. This is enabled by default for - live streams - --no-hls-use-mpegts Do not use the mpegts container for HLS - videos. This is default when not - downloading live streams - --downloader [PROTO:]NAME Name or path of the external downloader to - use (optionally) prefixed by the protocols - (http, ftp, m3u8, dash, rstp, rtmp, mms) to - use it for. Currently supports native, - aria2c, avconv, axel, curl, ffmpeg, httpie, - wget. You can use this option multiple - times to set different downloaders for - different protocols. For example, - --downloader aria2c --downloader - "dash,m3u8:native" will use aria2c for - http/ftp downloads, and the native - downloader for dash/m3u8 downloads (Alias: - --external-downloader) - --downloader-args NAME:ARGS Give these arguments to the external - downloader. Specify the downloader name and - the arguments separated by a colon ":". For - ffmpeg, arguments can be passed to - different positions using the same syntax - as --postprocessor-args. You can use this - option multiple times to give different - arguments to different downloaders (Alias: - --external-downloader-args) + -N, --concurrent-fragments N Number of fragments of a dash/hlsnative + video that should be downloaded concurrently + (default is 1) + -r, --limit-rate RATE Maximum download rate in bytes per second + (e.g. 50K or 4.2M) + --throttled-rate RATE Minimum download rate in bytes per second + below which throttling is assumed and the + video data is re-extracted (e.g. 100K) + -R, --retries RETRIES Number of retries (default is 10), or + "infinite" + --file-access-retries RETRIES Number of times to retry on file access + error (default is 3), or "infinite" + --fragment-retries RETRIES Number of retries for a fragment (default is + 10), or "infinite" (DASH, hlsnative and ISM) + --retry-sleep [TYPE:]EXPR An expression for the time to sleep between + retries in seconds (optionally) prefixed by + the type of retry (file_access, fragment, + http (default)) to apply the sleep to. EXPR + can be a number, linear=START[:END[:STEP=1]] + or exp=START[:END[:BASE=2]]. This option can + be used multiple times to set the sleep for + the different retry types. Eg: --retry-sleep + linear=1::2 --retry-sleep fragment:exp=1:20 + --skip-unavailable-fragments Skip unavailable fragments for DASH, + hlsnative and ISM downloads (default) + (Alias: --no-abort-on-unavailable-fragment) + --abort-on-unavailable-fragment + Abort download if a fragment is unavailable + (Alias: --no-skip-unavailable-fragments) + --keep-fragments Keep downloaded fragments on disk after + downloading is finished + --no-keep-fragments Delete downloaded fragments after + downloading is finished (default) + --buffer-size SIZE Size of download buffer (e.g. 1024 or 16K) + (default is 1024) + --resize-buffer The buffer size is automatically resized + from an initial value of --buffer-size + (default) + --no-resize-buffer Do not automatically adjust the buffer size + --http-chunk-size SIZE Size of a chunk for chunk-based HTTP + downloading (e.g. 10485760 or 10M) (default + is disabled). May be useful for bypassing + bandwidth throttling imposed by a webserver + (experimental) + --playlist-reverse Download playlist videos in reverse order + --no-playlist-reverse Download playlist videos in default order + (default) + --playlist-random Download playlist videos in random order + --xattr-set-filesize Set file xattribute ytdl.filesize with + expected file size + --hls-use-mpegts Use the mpegts container for HLS videos; + allowing some players to play the video + while downloading, and reducing the chance + of file corruption if download is + interrupted. This is enabled by default for + live streams + --no-hls-use-mpegts Do not use the mpegts container for HLS + videos. This is default when not downloading + live streams + --downloader [PROTO:]NAME Name or path of the external downloader to + use (optionally) prefixed by the protocols + (http, ftp, m3u8, dash, rstp, rtmp, mms) to + use it for. Currently supports native, + aria2c, avconv, axel, curl, ffmpeg, httpie, + wget. You can use this option multiple times + to set different downloaders for different + protocols. For example, --downloader aria2c + --downloader "dash,m3u8:native" will use + aria2c for http/ftp downloads, and the + native downloader for dash/m3u8 downloads + (Alias: --external-downloader) + --downloader-args NAME:ARGS Give these arguments to the external + downloader. Specify the downloader name and + the arguments separated by a colon ":". For + ffmpeg, arguments can be passed to different + positions using the same syntax as + --postprocessor-args. You can use this + option multiple times to give different + arguments to different downloaders (Alias: + --external-downloader-args) ## Filesystem Options: - -a, --batch-file FILE File containing URLs to download ("-" for - stdin), one URL per line. Lines starting - with "#", ";" or "]" are considered as - comments and ignored - --no-batch-file Do not read URLs from batch file (default) - -P, --paths [TYPES:]PATH The paths where the files should be - downloaded. Specify the type of file and - the path separated by a colon ":". All the - same TYPES as --output are supported. - Additionally, you can also provide "home" - (default) and "temp" paths. All - intermediary files are first downloaded to - the temp path and then the final files are - moved over to the home path after download - is finished. This option is ignored if - --output is an absolute path - -o, --output [TYPES:]TEMPLATE Output filename template; see "OUTPUT - TEMPLATE" for details - --output-na-placeholder TEXT Placeholder value for unavailable meta - fields in output filename template - (default: "NA") - --restrict-filenames Restrict filenames to only ASCII - characters, and avoid "&" and spaces in - filenames - --no-restrict-filenames Allow Unicode characters, "&" and spaces in - filenames (default) - --windows-filenames Force filenames to be Windows-compatible - --no-windows-filenames Make filenames Windows-compatible only if - using Windows (default) - --trim-filenames LENGTH Limit the filename length (excluding - extension) to the specified number of - characters - -w, --no-overwrites Do not overwrite any files - --force-overwrites Overwrite all video and metadata files. - This option includes --no-continue - --no-force-overwrites Do not overwrite the video, but overwrite - related files (default) - -c, --continue Resume partially downloaded files/fragments - (default) - --no-continue Do not resume partially downloaded - fragments. If the file is not fragmented, - restart download of the entire file - --part Use .part files instead of writing directly - into output file (default) - --no-part Do not use .part files - write directly - into output file - --mtime Use the Last-modified header to set the - file modification time (default) - --no-mtime Do not use the Last-modified header to set - the file modification time - --write-description Write video description to a .description - file - --no-write-description Do not write video description (default) - --write-info-json Write video metadata to a .info.json file - (this may contain personal information) - --no-write-info-json Do not write video metadata (default) - --write-playlist-metafiles Write playlist metadata in addition to the - video metadata when using --write-info-json, - --write-description etc. (default) - --no-write-playlist-metafiles Do not write playlist metadata when using - --write-info-json, --write-description etc. - --clean-info-json Remove some private fields such as - filenames from the infojson. Note that it - could still contain some personal - information (default) - --no-clean-info-json Write all fields to the infojson - --write-comments Retrieve video comments to be placed in the - infojson. The comments are fetched even - without this option if the extraction is - known to be quick (Alias: --get-comments) - --no-write-comments Do not retrieve video comments unless the - extraction is known to be quick (Alias: - --no-get-comments) - --load-info-json FILE JSON file containing the video information - (created with the "--write-info-json" - option) - --cookies FILE Netscape formatted file to read cookies - from and dump cookie jar in - --no-cookies Do not read/dump cookies from/to file - (default) + -a, --batch-file FILE File containing URLs to download ("-" for + stdin), one URL per line. Lines starting + with "#", ";" or "]" are considered as + comments and ignored + --no-batch-file Do not read URLs from batch file (default) + -P, --paths [TYPES:]PATH The paths where the files should be + downloaded. Specify the type of file and the + path separated by a colon ":". All the same + TYPES as --output are supported. + Additionally, you can also provide "home" + (default) and "temp" paths. All intermediary + files are first downloaded to the temp path + and then the final files are moved over to + the home path after download is finished. + This option is ignored if --output is an + absolute path + -o, --output [TYPES:]TEMPLATE Output filename template; see "OUTPUT + TEMPLATE" for details + --output-na-placeholder TEXT Placeholder for unavailable fields in + "OUTPUT TEMPLATE" (default: "NA") + --restrict-filenames Restrict filenames to only ASCII characters, + and avoid "&" and spaces in filenames + --no-restrict-filenames Allow Unicode characters, "&" and spaces in + filenames (default) + --windows-filenames Force filenames to be Windows-compatible + --no-windows-filenames Make filenames Windows-compatible only if + using Windows (default) + --trim-filenames LENGTH Limit the filename length (excluding + extension) to the specified number of + characters + -w, --no-overwrites Do not overwrite any files + --force-overwrites Overwrite all video and metadata files. This + option includes --no-continue + --no-force-overwrites Do not overwrite the video, but overwrite + related files (default) + -c, --continue Resume partially downloaded files/fragments + (default) + --no-continue Do not resume partially downloaded + fragments. If the file is not fragmented, + restart download of the entire file + --part Use .part files instead of writing directly + into output file (default) + --no-part Do not use .part files - write directly into + output file + --mtime Use the Last-modified header to set the file + modification time (default) + --no-mtime Do not use the Last-modified header to set + the file modification time + --write-description Write video description to a .description + file + --no-write-description Do not write video description (default) + --write-info-json Write video metadata to a .info.json file + (this may contain personal information) + --no-write-info-json Do not write video metadata (default) + --write-playlist-metafiles Write playlist metadata in addition to the + video metadata when using --write-info-json, + --write-description etc. (default) + --no-write-playlist-metafiles Do not write playlist metadata when using + --write-info-json, --write-description etc. + --clean-info-json Remove some private fields such as filenames + from the infojson. Note that it could still + contain some personal information (default) + --no-clean-info-json Write all fields to the infojson + --write-comments Retrieve video comments to be placed in the + infojson. The comments are fetched even + without this option if the extraction is + known to be quick (Alias: --get-comments) + --no-write-comments Do not retrieve video comments unless the + extraction is known to be quick (Alias: + --no-get-comments) + --load-info-json FILE JSON file containing the video information + (created with the "--write-info-json" + option) + --cookies FILE Netscape formatted file to read cookies from + and dump cookie jar in + --no-cookies Do not read/dump cookies from/to file + (default) --cookies-from-browser BROWSER[+KEYRING][:PROFILE] - The name of the browser and (optionally) - the name/path of the profile to load - cookies from, separated by a ":". Currently - supported browsers are: brave, chrome, - chromium, edge, firefox, opera, safari, - vivaldi. By default, the most recently - accessed profile is used. The keyring used - for decrypting Chromium cookies on Linux - can be (optionally) specified after the - browser name separated by a "+". Currently - supported keyrings are: basictext, - gnomekeyring, kwallet - --no-cookies-from-browser Do not load cookies from browser (default) - --cache-dir DIR Location in the filesystem where youtube-dl - can store some downloaded information (such - as client ids and signatures) permanently. - By default $XDG_CACHE_HOME/yt-dlp or - ~/.cache/yt-dlp - --no-cache-dir Disable filesystem caching - --rm-cache-dir Delete all filesystem cache files + The name of the browser and (optionally) the + name/path of the profile to load cookies + from, separated by a ":". Currently + supported browsers are: brave, chrome, + chromium, edge, firefox, opera, safari, + vivaldi. By default, the most recently + accessed profile is used. The keyring used + for decrypting Chromium cookies on Linux can + be (optionally) specified after the browser + name separated by a "+". Currently supported + keyrings are: basictext, gnomekeyring, + kwallet + --no-cookies-from-browser Do not load cookies from browser (default) + --cache-dir DIR Location in the filesystem where youtube-dl + can store some downloaded information (such + as client ids and signatures) permanently. + By default $XDG_CACHE_HOME/yt-dlp or + ~/.cache/yt-dlp + --no-cache-dir Disable filesystem caching + --rm-cache-dir Delete all filesystem cache files ## Thumbnail Options: - --write-thumbnail Write thumbnail image to disk - --no-write-thumbnail Do not write thumbnail image to disk - (default) - --write-all-thumbnails Write all thumbnail image formats to disk - --list-thumbnails List available thumbnails of each video. - Simulate unless --no-simulate is used + --write-thumbnail Write thumbnail image to disk + --no-write-thumbnail Do not write thumbnail image to disk + (default) + --write-all-thumbnails Write all thumbnail image formats to disk + --list-thumbnails List available thumbnails of each video. + Simulate unless --no-simulate is used ## Internet Shortcut Options: - --write-link Write an internet shortcut file, depending - on the current platform (.url, .webloc or - .desktop). The URL may be cached by the OS - --write-url-link Write a .url Windows internet shortcut. The - OS caches the URL based on the file path - --write-webloc-link Write a .webloc macOS internet shortcut - --write-desktop-link Write a .desktop Linux internet shortcut + --write-link Write an internet shortcut file, depending + on the current platform (.url, .webloc or + .desktop). The URL may be cached by the OS + --write-url-link Write a .url Windows internet shortcut. The + OS caches the URL based on the file path + --write-webloc-link Write a .webloc macOS internet shortcut + --write-desktop-link Write a .desktop Linux internet shortcut ## Verbosity and Simulation Options: - -q, --quiet Activate quiet mode. If used with - --verbose, print the log to stderr - --no-warnings Ignore warnings - -s, --simulate Do not download the video and do not write - anything to disk - --no-simulate Download the video even if printing/listing - options are used - --ignore-no-formats-error Ignore "No video formats" error. Useful for - extracting metadata even if the videos are - not actually available for download - (experimental) - --no-ignore-no-formats-error Throw error when no downloadable video - formats are found (default) - --skip-download Do not download the video but write all - related files (Alias: --no-download) - -O, --print [WHEN:]TEMPLATE Field name or output template to print to - screen, optionally prefixed with when to - print it, separated by a ":". Supported - values of "WHEN" are the same as that of - --use-postprocessor, and "video" (default). - Implies --quiet. Implies --simulate unless - --no-simulate or later stages of WHEN are - used. This option can be used multiple - times + -q, --quiet Activate quiet mode. If used with --verbose, + print the log to stderr + --no-warnings Ignore warnings + -s, --simulate Do not download the video and do not write + anything to disk + --no-simulate Download the video even if printing/listing + options are used + --ignore-no-formats-error Ignore "No video formats" error. Useful for + extracting metadata even if the videos are + not actually available for download + (experimental) + --no-ignore-no-formats-error Throw error when no downloadable video + formats are found (default) + --skip-download Do not download the video but write all + related files (Alias: --no-download) + -O, --print [WHEN:]TEMPLATE Field name or output template to print to + screen, optionally prefixed with when to + print it, separated by a ":". Supported + values of "WHEN" are the same as that of + --use-postprocessor, and "video" (default). + Implies --quiet. Implies --simulate unless + --no-simulate or later stages of WHEN are + used. This option can be used multiple times --print-to-file [WHEN:]TEMPLATE FILE - Append given template to the file. The - values of WHEN and TEMPLATE are same as - that of --print. FILE uses the same syntax - as the output template. This option can be - used multiple times - -j, --dump-json Quiet, but print JSON information for each - video. Simulate unless --no-simulate is - used. See "OUTPUT TEMPLATE" for a - description of available keys - -J, --dump-single-json Quiet, but print JSON information for each - url or infojson passed. Simulate unless - --no-simulate is used. If the URL refers to - a playlist, the whole playlist information - is dumped in a single line - --force-write-archive Force download archive entries to be - written as far as no errors occur, even if - -s or another simulation option is used - (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 + Append given template to the file. The + values of WHEN and TEMPLATE are same as that + of --print. FILE uses the same syntax as the + output template. This option can be used + multiple times + -j, --dump-json Quiet, but print JSON information for each + video. Simulate unless --no-simulate is + used. See "OUTPUT TEMPLATE" for a + description of available keys + -J, --dump-single-json Quiet, but print JSON information for each + url or infojson passed. Simulate unless + --no-simulate is used. If the URL refers to + a playlist, the whole playlist information + is dumped in a single line + --force-write-archive Force download archive entries to be written + as far as no errors occur, even if -s or + another simulation option is used (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. E.g.: - --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) - --write-pages Write downloaded intermediary pages to - files in the current directory to debug - problems - --print-traffic Display sent and read HTTP traffic + 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. E.g.: + --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) + --write-pages Write downloaded intermediary pages to files + in the current directory to debug problems + --print-traffic Display sent and read HTTP traffic ## Workarounds: - --encoding ENCODING Force the specified encoding (experimental) - --legacy-server-connect Explicitly allow HTTPS connection to - servers that do not support RFC 5746 secure - renegotiation - --no-check-certificates Suppress HTTPS certificate validation - --prefer-insecure Use an unencrypted connection to retrieve - information about the video (Currently - supported only for YouTube) - --add-header FIELD:VALUE Specify a custom HTTP header and its value, - separated by a colon ":". You can use this - option multiple times - --bidi-workaround Work around terminals that lack - bidirectional text support. Requires bidiv - or fribidi executable in PATH - --sleep-requests SECONDS Number of seconds to sleep between requests - during data extraction - --sleep-interval SECONDS Number of seconds to sleep before each - download. This is the minimum time to sleep - when used along with --max-sleep-interval - (Alias: --min-sleep-interval) - --max-sleep-interval SECONDS Maximum number of seconds to sleep. Can - only be used along with --min-sleep-interval - --sleep-subtitles SECONDS Number of seconds to sleep before each - subtitle download + --encoding ENCODING Force the specified encoding (experimental) + --legacy-server-connect Explicitly allow HTTPS connection to servers + that do not support RFC 5746 secure + renegotiation + --no-check-certificates Suppress HTTPS certificate validation + --prefer-insecure Use an unencrypted connection to retrieve + information about the video (Currently + supported only for YouTube) + --add-header FIELD:VALUE Specify a custom HTTP header and its value, + separated by a colon ":". You can use this + option multiple times + --bidi-workaround Work around terminals that lack + bidirectional text support. Requires bidiv + or fribidi executable in PATH + --sleep-requests SECONDS Number of seconds to sleep between requests + during data extraction + --sleep-interval SECONDS Number of seconds to sleep before each + download. This is the minimum time to sleep + when used along with --max-sleep-interval + (Alias: --min-sleep-interval) + --max-sleep-interval SECONDS Maximum number of seconds to sleep. Can only + be used along with --min-sleep-interval + --sleep-subtitles SECONDS Number of seconds to sleep before each + subtitle download ## Video Format Options: - -f, --format FORMAT Video format code, see "FORMAT SELECTION" - for more details - -S, --format-sort SORTORDER Sort the formats by the fields given, see - "Sorting Formats" for more details - --S-force, --format-sort-force Force user specified sort order to have - precedence over all fields, see "Sorting - Formats" for more details - --no-format-sort-force Some fields have precedence over the user - specified sort order (default), see - "Sorting Formats" for more details - --video-multistreams Allow multiple video streams to be merged - into a single file - --no-video-multistreams Only one video stream is downloaded for - each output file (default) - --audio-multistreams Allow multiple audio streams to be merged - into a single file - --no-audio-multistreams Only one audio stream is downloaded for - each output file (default) - --prefer-free-formats Prefer video formats with free containers - over non-free ones of same quality. Use - with "-S ext" to strictly prefer free - containers irrespective of quality - --no-prefer-free-formats Don't give any special preference to free - containers (default) - --check-formats Make sure formats are selected only from - those that are actually downloadable - --check-all-formats Check all formats for whether they are - actually downloadable - --no-check-formats Do not check that the formats are actually - downloadable - -F, --list-formats List available formats of each video. - Simulate unless --no-simulate is used - --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 + -f, --format FORMAT Video format code, see "FORMAT SELECTION" + for more details + -S, --format-sort SORTORDER Sort the formats by the fields given, see + "Sorting Formats" for more details + --format-sort-force Force user specified sort order to have + precedence over all fields, see "Sorting + Formats" for more details (Alias: --S-force) + --no-format-sort-force Some fields have precedence over the user + specified sort order (default) + --video-multistreams Allow multiple video streams to be merged + into a single file + --no-video-multistreams Only one video stream is downloaded for each + output file (default) + --audio-multistreams Allow multiple audio streams to be merged + into a single file + --no-audio-multistreams Only one audio stream is downloaded for each + output file (default) + --prefer-free-formats Prefer video formats with free containers + over non-free ones of same quality. Use with + "-S ext" to strictly prefer free containers + irrespective of quality + --no-prefer-free-formats Don't give any special preference to free + containers (default) + --check-formats Make sure formats are selected only from + those that are actually downloadable + --check-all-formats Check all formats for whether they are + actually downloadable + --no-check-formats Do not check that the formats are actually + downloadable + -F, --list-formats List available formats of each video. + Simulate unless --no-simulate is used + --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: - --write-subs Write subtitle file - --no-write-subs Do not write subtitle file (default) - --write-auto-subs Write automatically generated subtitle file - (Alias: --write-automatic-subs) - --no-write-auto-subs Do not write auto-generated subtitles - (default) (Alias: --no-write-automatic-subs) - --list-subs List available subtitles of each video. - Simulate unless --no-simulate is used - --sub-format FORMAT Subtitle format, accepts formats - preference, for example: "srt" or - "ass/srt/best" - --sub-langs LANGS Languages of the subtitles to download (can - be regex) or "all" separated by commas. - (Eg: --sub-langs "en.*,ja") You can prefix - the language code with a "-" to exempt it - from the requested languages. (Eg: - --sub-langs all,-live_chat) Use --list-subs - for a list of available language tags + --write-subs Write subtitle file + --no-write-subs Do not write subtitle file (default) + --write-auto-subs Write automatically generated subtitle file + (Alias: --write-automatic-subs) + --no-write-auto-subs Do not write auto-generated subtitles + (default) (Alias: --no-write-automatic-subs) + --list-subs List available subtitles of each video. + Simulate unless --no-simulate is used + --sub-format FORMAT Subtitle format; accepts formats preference, + Eg: "srt" or "ass/srt/best" + --sub-langs LANGS Languages of the subtitles to download (can + be regex) or "all" separated by commas. (Eg: + --sub-langs "en.*,ja") You can prefix the + language code with a "-" to exclude it from + the requested languages. (Eg: --sub-langs + all,-live_chat) Use --list-subs for a list + of available language tags ## Authentication Options: - -u, --username USERNAME Login with this account ID - -p, --password PASSWORD Account password. If this option is left - out, yt-dlp will ask interactively - -2, --twofactor TWOFACTOR Two-factor authentication code - -n, --netrc Use .netrc authentication data - --netrc-location PATH Location of .netrc authentication data; - either the path or its containing - directory. Defaults to ~/.netrc - --video-password PASSWORD Video password (vimeo, youku) - --ap-mso MSO Adobe Pass multiple-system operator (TV - provider) identifier, use --ap-list-mso for - a list of available MSOs - --ap-username USERNAME Multiple-system operator account login - --ap-password PASSWORD Multiple-system operator account password. - If this option is left out, yt-dlp will ask - interactively - --ap-list-mso List all supported multiple-system - operators - --client-certificate CERTFILE Path to client certificate file in PEM - format. May include the private key - --client-certificate-key KEYFILE Path to private key file for client - certificate + -u, --username USERNAME Login with this account ID + -p, --password PASSWORD Account password. If this option is left + out, yt-dlp will ask interactively + -2, --twofactor TWOFACTOR Two-factor authentication code + -n, --netrc Use .netrc authentication data + --netrc-location PATH Location of .netrc authentication data; + either the path or its containing directory. + Defaults to ~/.netrc + --video-password PASSWORD Video password (vimeo, youku) + --ap-mso MSO Adobe Pass multiple-system operator (TV + provider) identifier, use --ap-list-mso for + a list of available MSOs + --ap-username USERNAME Multiple-system operator account login + --ap-password PASSWORD Multiple-system operator account password. + If this option is left out, yt-dlp will ask + interactively + --ap-list-mso List all supported multiple-system operators + --client-certificate CERTFILE Path to client certificate file in PEM + format. May include the private key + --client-certificate-key KEYFILE + Path to private key file for client + certificate --client-certificate-password PASSWORD - Password for client certificate private - key, if encrypted. If not provided and the - key is encrypted, yt-dlp will ask - interactively + Password for client certificate private key, + if encrypted. If not provided, and the key + is encrypted, yt-dlp will ask interactively ## Post-Processing Options: - -x, --extract-audio Convert video files to audio-only files - (requires ffmpeg and ffprobe) - --audio-format FORMAT Specify audio format to convert the audio - to when -x is used. Currently supported - formats are: best (default) or one of aac, - flac, mp3, m4a, opus, vorbis, wav, alac - --audio-quality QUALITY Specify ffmpeg audio quality to use when - converting the audio with -x. Insert a - value between 0 (best) and 10 (worst) for - VBR or a specific bitrate like 128K - (default 5) - --remux-video FORMAT Remux the video into another container if - necessary (currently supported: mp4, mkv, - flv, webm, mov, avi, mka, ogg, aac, flac, - mp3, m4a, opus, vorbis, wav, alac). If - target container does not support the - video/audio codec, remuxing will fail. You - can specify multiple rules; Eg. - "aac>m4a/mov>mp4/mkv" will remux aac to - m4a, mov to mp4 and anything else to mkv. - --recode-video FORMAT Re-encode the video into another format if - re-encoding is necessary. The syntax and - supported formats are the same as --remux-video - --postprocessor-args NAME:ARGS Give these arguments to the postprocessors. - Specify the postprocessor/executable name - and the arguments separated by a colon ":" - to give the argument to the specified - postprocessor/executable. Supported PP are: - Merger, ModifyChapters, SplitChapters, - ExtractAudio, VideoRemuxer, VideoConvertor, - Metadata, EmbedSubtitle, EmbedThumbnail, - SubtitlesConvertor, ThumbnailsConvertor, - FixupStretched, FixupM4a, FixupM3u8, - FixupTimestamp and FixupDuration. The - supported executables are: AtomicParsley, - FFmpeg and FFprobe. You can also specify - "PP+EXE:ARGS" to give the arguments to the - specified executable only when being used - by the specified postprocessor. - Additionally, for ffmpeg/ffprobe, "_i"/"_o" - can be appended to the prefix optionally - followed by a number to pass the argument - before the specified input/output file. Eg: - --ppa "Merger+ffmpeg_i1:-v quiet". You can - use this option multiple times to give - different arguments to different - postprocessors. (Alias: --ppa) - -k, --keep-video Keep the intermediate video file on disk - after post-processing - --no-keep-video Delete the intermediate video file after - post-processing (default) - --post-overwrites Overwrite post-processed files (default) - --no-post-overwrites Do not overwrite post-processed files - --embed-subs Embed subtitles in the video (only for mp4, - webm and mkv videos) - --no-embed-subs Do not embed subtitles (default) - --embed-thumbnail Embed thumbnail in the video as cover art - --no-embed-thumbnail Do not embed thumbnail (default) - --embed-metadata Embed metadata to the video file. Also - embeds chapters/infojson if present unless - --no-embed-chapters/--no-embed-info-json - are used (Alias: --add-metadata) - --no-embed-metadata Do not add metadata to file (default) - (Alias: --no-add-metadata) - --embed-chapters Add chapter markers to the video file - (Alias: --add-chapters) - --no-embed-chapters Do not add chapter markers (default) - (Alias: --no-add-chapters) - --embed-info-json Embed the infojson as an attachment to - mkv/mka video files - --no-embed-info-json Do not embed the infojson as an attachment - to the video file - --parse-metadata FROM:TO Parse additional metadata like title/artist - from other fields; see "MODIFYING METADATA" - for details + -x, --extract-audio Convert video files to audio-only files + (requires ffmpeg and ffprobe) + --audio-format FORMAT Specify audio format to convert the audio to + when -x is used. Currently supported formats + are: best (default) or one of aac, flac, + mp3, m4a, opus, vorbis, wav, alac + --audio-quality QUALITY Specify ffmpeg audio quality to use when + converting the audio with -x. Insert a value + between 0 (best) and 10 (worst) for VBR or a + specific bitrate like 128K (default 5) + --remux-video FORMAT Remux the video into another container if + necessary (currently supported: mp4, mkv, + flv, webm, mov, avi, mka, ogg, aac, flac, + mp3, m4a, opus, vorbis, wav, alac). If + target container does not support the + video/audio codec, remuxing will fail. You + can specify multiple rules; Eg. + "aac>m4a/mov>mp4/mkv" will remux aac to m4a, + mov to mp4 and anything else to mkv. + --recode-video FORMAT Re-encode the video into another format if + necessary. The syntax and supported formats + are the same as --remux-video + --postprocessor-args NAME:ARGS Give these arguments to the postprocessors. + Specify the postprocessor/executable name + and the arguments separated by a colon ":" + to give the argument to the specified + postprocessor/executable. Supported PP are: + Merger, ModifyChapters, SplitChapters, + ExtractAudio, VideoRemuxer, VideoConvertor, + Metadata, EmbedSubtitle, EmbedThumbnail, + SubtitlesConvertor, ThumbnailsConvertor, + FixupStretched, FixupM4a, FixupM3u8, + FixupTimestamp and FixupDuration. The + supported executables are: AtomicParsley, + FFmpeg and FFprobe. You can also specify + "PP+EXE:ARGS" to give the arguments to the + specified executable only when being used by + the specified postprocessor. Additionally, + for ffmpeg/ffprobe, "_i"/"_o" can be + appended to the prefix optionally followed + by a number to pass the argument before the + specified input/output file. Eg: --ppa + "Merger+ffmpeg_i1:-v quiet". You can use + this option multiple times to give different + arguments to different postprocessors. + (Alias: --ppa) + -k, --keep-video Keep the intermediate video file on disk + after post-processing + --no-keep-video Delete the intermediate video file after + post-processing (default) + --post-overwrites Overwrite post-processed files (default) + --no-post-overwrites Do not overwrite post-processed files + --embed-subs Embed subtitles in the video (only for mp4, + webm and mkv videos) + --no-embed-subs Do not embed subtitles (default) + --embed-thumbnail Embed thumbnail in the video as cover art + --no-embed-thumbnail Do not embed thumbnail (default) + --embed-metadata Embed metadata to the video file. Also + embeds chapters/infojson if present unless + --no-embed-chapters/--no-embed-info-json are + used (Alias: --add-metadata) + --no-embed-metadata Do not add metadata to file (default) + (Alias: --no-add-metadata) + --embed-chapters Add chapter markers to the video file + (Alias: --add-chapters) + --no-embed-chapters Do not add chapter markers (default) (Alias: + --no-add-chapters) + --embed-info-json Embed the infojson as an attachment to + mkv/mka video files + --no-embed-info-json Do not embed the infojson as an attachment + to the video file + --parse-metadata FROM:TO Parse additional metadata like title/artist + from other fields; see "MODIFYING METADATA" + for details --replace-in-metadata FIELDS REGEX REPLACE - Replace text in a metadata field using the - given regex. This option can be used - multiple times - --xattrs Write metadata to the video file's xattrs - (using dublin core and xdg standards) - --concat-playlist POLICY Concatenate videos in a playlist. One of - "never", "always", or "multi_video" - (default; only when the videos form a - single show). All the video files must have - same codecs and number of streams to be - concatable. The "pl_video:" prefix can be - used with "--paths" and "--output" to set - the output filename for the concatenated - files. See "OUTPUT TEMPLATE" for details - --fixup POLICY Automatically correct known faults of the - file. One of never (do nothing), warn (only - emit a warning), detect_or_warn (the - default; fix file if we can, warn - otherwise), force (try fixing even if file - already exists) - --ffmpeg-location PATH Location of the ffmpeg binary; either the - path to the binary or its containing - directory - --exec [WHEN:]CMD Execute a command, optionally prefixed with - when to execute it (after_move if - unspecified), separated by a ":". Supported - values of "WHEN" are the same as that of - --use-postprocessor. Same syntax as the - output template can be used to pass any - field as arguments to the command. After - download, an additional field "filepath" - that contains the final path of the - downloaded file is also available, and if - no fields are passed, %(filepath)q is - appended to the end of the command. This - option can be used multiple times - --no-exec Remove any previously defined --exec - --convert-subs FORMAT Convert the subtitles to another format - (currently supported: srt, vtt, ass, lrc) - (Alias: --convert-subtitles) - --convert-thumbnails FORMAT Convert the thumbnails to another format - (currently supported: jpg, png, webp) - --split-chapters Split video into multiple files based on - internal chapters. The "chapter:" prefix - can be used with "--paths" and "--output" - to set the output filename for the split - files. See "OUTPUT TEMPLATE" for details - --no-split-chapters Do not split video based on chapters - (default) - --remove-chapters REGEX Remove chapters whose title matches the - given regular expression. Time ranges - prefixed by a "*" can also be used in place - of chapters to remove the specified range. - Eg: --remove-chapters "*10:15-15:00" - --remove-chapters "intro". This option can - be used multiple times - --no-remove-chapters Do not remove any chapters from the file - (default) - --force-keyframes-at-cuts Force keyframes around the chapters before - removing/splitting them. Requires a - re-encode and thus is very slow, but the - resulting video may have fewer artifacts - around the cuts - --no-force-keyframes-at-cuts Do not force keyframes around the chapters - when cutting/splitting (default) - --use-postprocessor NAME[:ARGS] The (case sensitive) name of plugin - postprocessors to be enabled, and - (optionally) arguments to be passed to it, - separated by a colon ":". ARGS are a - semicolon ";" delimited list of NAME=VALUE. - The "when" argument determines when the - postprocessor is invoked. It can be one of - "pre_process" (after video extraction), - "after_filter" (after video passes filter), - "before_dl" (before each video download), - "post_process" (after each video download; - default), "after_move" (after moving video - file to it's final locations), - "after_video" (after downloading and - processing all formats of a video), or - "playlist" (at end of playlist). This - option can be used multiple times to add - different postprocessors + Replace text in a metadata field using the + given regex. This option can be used + multiple times + --xattrs Write metadata to the video file's xattrs + (using dublin core and xdg standards) + --concat-playlist POLICY Concatenate videos in a playlist. One of + "never", "always", or "multi_video" + (default; only when the videos form a single + show). All the video files must have same + codecs and number of streams to be + concatable. The "pl_video:" prefix can be + used with "--paths" and "--output" to set + the output filename for the concatenated + files. See "OUTPUT TEMPLATE" for details + --fixup POLICY Automatically correct known faults of the + file. One of never (do nothing), warn (only + emit a warning), detect_or_warn (the + default; fix file if we can, warn + otherwise), force (try fixing even if file + already exists) + --ffmpeg-location PATH Location of the ffmpeg binary; either the + path to the binary or its containing + directory + --exec [WHEN:]CMD Execute a command, optionally prefixed with + when to execute it (after_move if + unspecified), separated by a ":". Supported + values of "WHEN" are the same as that of + --use-postprocessor. Same syntax as the + output template can be used to pass any + field as arguments to the command. After + download, an additional field "filepath" + that contains the final path of the + downloaded file is also available, and if no + fields are passed, %(filepath)q is appended + to the end of the command. This option can + be used multiple times + --no-exec Remove any previously defined --exec + --convert-subs FORMAT Convert the subtitles to another format + (currently supported: srt, vtt, ass, lrc) + (Alias: --convert-subtitles) + --convert-thumbnails FORMAT Convert the thumbnails to another format + (currently supported: jpg, png, webp) + --split-chapters Split video into multiple files based on + internal chapters. The "chapter:" prefix can + be used with "--paths" and "--output" to set + the output filename for the split files. See + "OUTPUT TEMPLATE" for details + --no-split-chapters Do not split video based on chapters + (default) + --remove-chapters REGEX Remove chapters whose title matches the + given regular expression. Time ranges + prefixed by a "*" can also be used in place + of chapters to remove the specified range. + Eg: --remove-chapters "*10:15-15:00" + --remove-chapters "intro". This option can + be used multiple times + --no-remove-chapters Do not remove any chapters from the file + (default) + --force-keyframes-at-cuts Force keyframes around chapters when + removing/splitting them. The resulting video + may have fewer artifacts around the cuts, + but is very slow due to needing a re-encode + --no-force-keyframes-at-cuts Do not force keyframes around the chapters + when cutting/splitting (default) + --use-postprocessor NAME[:ARGS] + The (case sensitive) name of plugin + postprocessors to be enabled, and + (optionally) arguments to be passed to it, + separated by a colon ":". ARGS are a + semicolon ";" delimited list of NAME=VALUE. + The "when" argument determines when the + postprocessor is invoked. It can be one of + "pre_process" (after video extraction), + "after_filter" (after video passes filter), + "before_dl" (before each video download), + "post_process" (after each video download; + default), "after_move" (after moving video + file to it's final locations), "after_video" + (after downloading and processing all + formats of a video), or "playlist" (at end + of playlist). This option can be used + multiple times to add different + postprocessors ## SponsorBlock Options: Make chapter entries for, or remove various segments (sponsor, introductions, etc.) from downloaded YouTube videos using the [SponsorBlock API](https://sponsor.ajay.app) - --sponsorblock-mark CATS SponsorBlock categories to create chapters - for, separated by commas. Available - categories are all, default(=all), sponsor, - intro, outro, selfpromo, preview, filler, - interaction, music_offtopic, poi_highlight. - You can prefix the category with a "-" to - exempt it. See [1] for description of the - categories. Eg: --sponsorblock-mark all,-preview - [1] https://wiki.sponsor.ajay.app/w/Segment_Categories - --sponsorblock-remove CATS SponsorBlock categories to be removed from - the video file, separated by commas. If a - category is present in both mark and - remove, remove takes precedence. The syntax - and available categories are the same as - for --sponsorblock-mark except that - "default" refers to "all,-filler" and - poi_highlight is not available + --sponsorblock-mark CATS SponsorBlock categories to create chapters + for, separated by commas. Available + categories are sponsor, intro, outro, + selfpromo, preview, filler, interaction, + music_offtopic, poi_highlight, all and + default (=all). You can prefix the category + with a "-" to exclude it. See [1] for + description of the categories. Eg: + --sponsorblock-mark all,-preview + [1] https://wiki.sponsor.ajay.app/w/Segment_Categories + --sponsorblock-remove CATS SponsorBlock categories to be removed from + the video file, separated by commas. If a + category is present in both mark and remove, + remove takes precedence. The syntax and + available categories are the same as for + --sponsorblock-mark except that "default" + refers to "all,-filler" and poi_highlight is + not available --sponsorblock-chapter-title TEMPLATE - The title template for SponsorBlock - chapters created by --sponsorblock-mark. - The same syntax as the output template is - used, but the only available fields are - start_time, end_time, category, categories, - name, category_names. Defaults to - "[SponsorBlock]: %(category_names)l" - --no-sponsorblock Disable both --sponsorblock-mark and - --sponsorblock-remove - --sponsorblock-api URL SponsorBlock API location, defaults to - https://sponsor.ajay.app + An output template for the title of the + SponsorBlock chapters created by + --sponsorblock-mark. The only available + fields are start_time, end_time, category, + categories, name, category_names. Defaults + to "[SponsorBlock]: %(category_names)l" + --no-sponsorblock Disable both --sponsorblock-mark and + --sponsorblock-remove + --sponsorblock-api URL SponsorBlock API location, defaults to + https://sponsor.ajay.app ## Extractor Options: - --extractor-retries RETRIES Number of retries for known extractor - errors (default is 3), or "infinite" - --allow-dynamic-mpd Process dynamic DASH manifests (default) - (Alias: --no-ignore-dynamic-mpd) - --ignore-dynamic-mpd Do not process dynamic DASH manifests - (Alias: --no-allow-dynamic-mpd) - --hls-split-discontinuity Split HLS playlists to different formats at - discontinuities such as ad breaks - --no-hls-split-discontinuity Do not split HLS playlists to different - formats at discontinuities such as ad - breaks (default) - --extractor-args KEY:ARGS Pass these arguments to the extractor. See - "EXTRACTOR ARGUMENTS" for details. You can - use this option multiple times to give - arguments for different extractors + --extractor-retries RETRIES Number of retries for known extractor errors + (default is 3), or "infinite" + --allow-dynamic-mpd Process dynamic DASH manifests (default) + (Alias: --no-ignore-dynamic-mpd) + --ignore-dynamic-mpd Do not process dynamic DASH manifests + (Alias: --no-allow-dynamic-mpd) + --hls-split-discontinuity Split HLS playlists to different formats at + discontinuities such as ad breaks + --no-hls-split-discontinuity Do not split HLS playlists to different + formats at discontinuities such as ad breaks + (default) + --extractor-args KEY:ARGS Pass these arguments to the extractor. See + "EXTRACTOR ARGUMENTS" for details. You can + use this option multiple times to give + arguments for different extractors # CONFIGURATION diff --git a/yt_dlp/options.py b/yt_dlp/options.py index 5c97facb7..c0718e007 100644 --- a/yt_dlp/options.py +++ b/yt_dlp/options.py @@ -134,10 +134,8 @@ class _YoutubeDLHelpFormatter(optparse.IndentedHelpFormatter): def __init__(self): # No need to wrap help messages if we're on a wide console max_width = compat_get_terminal_size().columns or 80 - # 47% is chosen because that is how README.md is currently formatted - # and moving help text even further to the right is undesirable. - # This can be reduced in the future to get a prettier output - super().__init__(width=max_width, max_help_position=int(0.47 * max_width)) + # The % is chosen to get a pretty output in README.md + super().__init__(width=max_width, max_help_position=int(0.45 * max_width)) @staticmethod def format_option_strings(option): @@ -345,7 +343,12 @@ def create_parser(): general.add_option( '--default-search', dest='default_search', metavar='PREFIX', - help='Use this prefix for unqualified URLs. For example "gvsearch2:" downloads two videos from google videos for the search term "large apple". Use the value "auto" to let yt-dlp guess ("auto_warning" to emit a warning when guessing). "error" just throws an error. The default value "fixup_error" repairs broken URLs, but emits an error if this is not possible instead of searching') + help=( + 'Use this prefix for unqualified URLs. ' + 'Eg: "gvsearch2:python" downloads two videos from google videos for the search term "python". ' + 'Use the value "auto" to let yt-dlp guess ("auto_warning" to emit a warning when guessing). ' + '"error" just throws an error. The default value "fixup_error" repairs broken URLs, ' + 'but emits an error if this is not possible instead of searching')) general.add_option( '--ignore-config', '--no-config', action='store_true', dest='ignoreconfig', @@ -439,10 +442,8 @@ def create_parser(): '--proxy', dest='proxy', default=None, metavar='URL', help=( - 'Use the specified HTTP/HTTPS/SOCKS proxy. To enable ' - 'SOCKS proxy, specify a proper scheme. For example ' - 'socks5://user:pass@127.0.0.1:1080/. Pass in an empty string (--proxy "") ' - 'for direct connection')) + 'Use the specified HTTP/HTTPS/SOCKS proxy. To enable SOCKS proxy, specify a proper scheme. ' + 'Eg: socks5://user:pass@127.0.0.1:1080/. Pass in an empty string (--proxy "") for direct connection')) network.add_option( '--socket-timeout', dest='socket_timeout', type=float, default=None, metavar='SECONDS', @@ -550,7 +551,7 @@ def create_parser(): '--match-filters', metavar='FILTER', dest='match_filter', action='append', help=( - 'Generic video filter. Any field (see "OUTPUT TEMPLATE") can be compared with a ' + 'Generic video filter. Any "OUTPUT TEMPLATE" field can be compared with a ' 'number or a string using the operators defined in "Filtering formats". ' 'You can also simply specify a field to match if the field is present, ' 'use "!field" to check if the field is not present, and "&" to check multiple conditions. ' @@ -559,7 +560,7 @@ def create_parser(): '!is_live --match-filter "like_count>?100 & description~=\'(?i)\\bcats \\& dogs\\b\'" ' 'matches only videos that are not live OR those that have a like count more than 100 ' '(or the like field is not available) and also has a description ' - 'that contains the phrase "cats & dogs" (ignoring case). ' + 'that contains the phrase "cats & dogs" (caseless). ' 'Use "--match-filter -" to interactively ask whether to download each video')) selection.add_option( '--no-match-filter', @@ -671,7 +672,7 @@ def create_parser(): '--client-certificate-password', dest='client_certificate_password', metavar='PASSWORD', help='Password for client certificate private key, if encrypted. ' - 'If not provided and the key is encrypted, yt-dlp will ask interactively') + 'If not provided, and the key is encrypted, yt-dlp will ask interactively') video_format = optparse.OptionGroup(parser, 'Video Format Options') video_format.add_option( @@ -688,13 +689,11 @@ def create_parser(): action='store_true', dest='format_sort_force', metavar='FORMAT', default=False, help=( 'Force user specified sort order to have precedence over all fields, ' - 'see "Sorting Formats" for more details')) + 'see "Sorting Formats" for more details (Alias: --S-force)')) video_format.add_option( '--no-format-sort-force', action='store_false', dest='format_sort_force', metavar='FORMAT', default=False, - help=( - 'Some fields have precedence over the user specified sort order (default), ' - 'see "Sorting Formats" for more details')) + help='Some fields have precedence over the user specified sort order (default)') video_format.add_option( '--video-multistreams', action='store_true', dest='allow_multiple_video_streams', default=None, @@ -793,14 +792,14 @@ def create_parser(): subtitles.add_option( '--sub-format', action='store', dest='subtitlesformat', metavar='FORMAT', default='best', - help='Subtitle format, accepts formats preference, for example: "srt" or "ass/srt/best"') + help='Subtitle format; accepts formats preference, Eg: "srt" or "ass/srt/best"') subtitles.add_option( '--sub-langs', '--srt-langs', action='callback', dest='subtitleslangs', metavar='LANGS', type='str', default=[], callback=_list_from_options_callback, help=( 'Languages of the subtitles to download (can be regex) or "all" separated by commas. (Eg: --sub-langs "en.*,ja") ' - 'You can prefix the language code with a "-" to exempt it from the requested languages. (Eg: --sub-langs all,-live_chat) ' + 'You can prefix the language code with a "-" to exclude it from the requested languages. (Eg: --sub-langs all,-live_chat) ' 'Use --list-subs for a list of available language tags')) downloader = optparse.OptionGroup(parser, 'Download Options') @@ -837,17 +836,18 @@ def create_parser(): 'default_key': 'http', }, help=( 'An expression for the time to sleep between retries in seconds (optionally) prefixed ' - 'by the type of retry (http (default), fragment, file_access) to apply the sleep to. ' - 'EXPR can be a number, or of the forms linear=START[:END[:STEP=1]] or exp=START[:END[:BASE=2]]. ' + 'by the type of retry (file_access, fragment, http (default)) to apply the sleep to. ' + 'EXPR can be a number, linear=START[:END[:STEP=1]] or exp=START[:END[:BASE=2]]. ' + 'This option can be used multiple times to set the sleep for the different retry types. ' 'Eg: --retry-sleep linear=1::2 --retry-sleep fragment:exp=1:20')) downloader.add_option( '--skip-unavailable-fragments', '--no-abort-on-unavailable-fragment', action='store_true', dest='skip_unavailable_fragments', default=True, - help='Skip unavailable fragments for DASH, hlsnative and ISM (default) (Alias: --no-abort-on-unavailable-fragment)') + help='Skip unavailable fragments for DASH, hlsnative and ISM downloads (default) (Alias: --no-abort-on-unavailable-fragment)') downloader.add_option( '--abort-on-unavailable-fragment', '--no-skip-unavailable-fragments', action='store_false', dest='skip_unavailable_fragments', - help='Abort downloading if a fragment is unavailable (Alias: --no-skip-unavailable-fragments)') + help='Abort download if a fragment is unavailable (Alias: --no-skip-unavailable-fragments)') downloader.add_option( '--keep-fragments', action='store_true', dest='keep_fragments', default=False, @@ -1213,7 +1213,7 @@ def create_parser(): filesystem.add_option( '--output-na-placeholder', dest='outtmpl_na_placeholder', metavar='TEXT', default='NA', - help=('Placeholder value for unavailable meta fields in output filename template (default: "%default")')) + help=('Placeholder for unavailable fields in "OUTPUT TEMPLATE" (default: "%default")')) filesystem.add_option( '--autonumber-size', dest='autonumber_size', metavar='NUMBER', type=int, @@ -1436,9 +1436,7 @@ def create_parser(): postproc.add_option( '--recode-video', metavar='FORMAT', dest='recodevideo', default=None, - help=( - 'Re-encode the video into another format if re-encoding is necessary. ' - 'The syntax and supported formats are the same as --remux-video')) + help='Re-encode the video into another format if necessary. The syntax and supported formats are the same as --remux-video') postproc.add_option( '--postprocessor-args', '--ppa', metavar='NAME:ARGS', dest='postprocessor_args', default={}, type='str', @@ -1635,9 +1633,9 @@ def create_parser(): '--force-keyframes-at-cuts', action='store_true', dest='force_keyframes_at_cuts', default=False, help=( - 'Force keyframes around the chapters before removing/splitting them. ' - 'Requires a re-encode and thus is very slow, but the resulting video ' - 'may have fewer artifacts around the cuts')) + 'Force keyframes around chapters when removing/splitting them. ' + 'The resulting video may have fewer artifacts around the cuts, ' + 'but is very slow due to needing a re-encode')) postproc.add_option( '--no-force-keyframes-at-cuts', action='store_false', dest='force_keyframes_at_cuts', @@ -1675,8 +1673,8 @@ def create_parser(): 'aliases': {'default': ['all']} }, help=( 'SponsorBlock categories to create chapters for, separated by commas. ' - f'Available categories are all, default(=all), {", ".join(SponsorBlockPP.CATEGORIES.keys())}. ' - 'You can prefix the category with a "-" to exempt it. See [1] for description of the categories. ' + f'Available categories are {", ".join(SponsorBlockPP.CATEGORIES.keys())}, all and default (=all). ' + 'You can prefix the category with a "-" to exclude it. See [1] for description of the categories. ' 'Eg: --sponsorblock-mark all,-preview [1] https://wiki.sponsor.ajay.app/w/Segment_Categories')) sponsorblock.add_option( '--sponsorblock-remove', metavar='CATS', @@ -1697,9 +1695,9 @@ def create_parser(): '--sponsorblock-chapter-title', metavar='TEMPLATE', default=DEFAULT_SPONSORBLOCK_CHAPTER_TITLE, dest='sponsorblock_chapter_title', help=( - 'The title template for SponsorBlock chapters created by --sponsorblock-mark. ' - 'The same syntax as the output template is used, but the only available fields are ' - 'start_time, end_time, category, categories, name, category_names. Defaults to "%default"')) + 'An output template for the title of the SponsorBlock chapters created by --sponsorblock-mark. ' + 'The only available fields are start_time, end_time, category, categories, name, category_names. ' + 'Defaults to "%default"')) sponsorblock.add_option( '--no-sponsorblock', default=False, action='store_true', dest='no_sponsorblock', -- cgit v1.2.3 From 2f97cc615bdb788bf5c86c1132144ca491b820c3 Mon Sep 17 00:00:00 2001 From: pukkandan Date: Wed, 18 May 2022 14:06:41 +0530 Subject: [utils] `ISO3166Utils`: Add `EU` and `AP` Fixes https://github.com/yt-dlp/yt-dlp/pull/3302#discussion_r875528517 --- yt_dlp/utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/yt_dlp/utils.py b/yt_dlp/utils.py index f02f71177..41157f5de 100644 --- a/yt_dlp/utils.py +++ b/yt_dlp/utils.py @@ -4166,6 +4166,9 @@ class ISO3166Utils: 'YE': 'Yemen', 'ZM': 'Zambia', 'ZW': 'Zimbabwe', + # Not ISO 3166 codes, but used for IP blocks + 'AP': 'Asia/Pacific Region', + 'EU': 'Europe', } @classmethod -- cgit v1.2.3 From 0b9c08b47bb5e95c21b067044ace4e824d19a9c2 Mon Sep 17 00:00:00 2001 From: pukkandan Date: Thu, 19 May 2022 19:36:31 +0530 Subject: [utils] Improve performance using `functools.cache` Closes #3786 --- yt_dlp/compat/functools.py | 12 ++++++++++++ yt_dlp/update.py | 3 ++- yt_dlp/utils.py | 7 +++++-- 3 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 yt_dlp/compat/functools.py diff --git a/yt_dlp/compat/functools.py b/yt_dlp/compat/functools.py new file mode 100644 index 000000000..36c983642 --- /dev/null +++ b/yt_dlp/compat/functools.py @@ -0,0 +1,12 @@ +# flake8: noqa: F405 +from functools import * # noqa: F403 + +from .compat_utils import passthrough_module + +passthrough_module(__name__, 'functools') +del passthrough_module + +try: + cache # >= 3.9 +except NameError: + cache = lru_cache(maxsize=None) diff --git a/yt_dlp/update.py b/yt_dlp/update.py index 8dcf260f5..d627ae269 100644 --- a/yt_dlp/update.py +++ b/yt_dlp/update.py @@ -7,11 +7,12 @@ import sys import traceback from zipimport import zipimporter -from .compat import compat_realpath +from .compat import compat_realpath, functools from .utils import Popen, encode_compat_str, write_string from .version import __version__ +@functools.cache def detect_variant(): if hasattr(sys, 'frozen'): prefix = 'mac' if sys.platform == 'darwin' else 'win' diff --git a/yt_dlp/utils.py b/yt_dlp/utils.py index 41157f5de..0274e330d 100644 --- a/yt_dlp/utils.py +++ b/yt_dlp/utils.py @@ -11,7 +11,6 @@ import datetime import email.header import email.utils import errno -import functools import gzip import hashlib import hmac @@ -39,8 +38,8 @@ import urllib.parse import xml.etree.ElementTree import zlib +from .compat import asyncio, functools # Modules from .compat import ( - asyncio, compat_chr, compat_cookiejar, compat_etree_fromstring, @@ -248,6 +247,7 @@ JSON_LD_RE = r'(?is)]+type=(["\']?)application/ld\+json\1[^>]*>(?P Date: Fri, 20 May 2022 06:01:08 +0530 Subject: [tiktok] Detect embeds Closes #3799 --- yt_dlp/extractor/generic.py | 6 ++++++ yt_dlp/extractor/tiktok.py | 28 ++++++++++++++++++---------- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/yt_dlp/extractor/generic.py b/yt_dlp/extractor/generic.py index b0fc176ef..c7e9ea059 100644 --- a/yt_dlp/extractor/generic.py +++ b/yt_dlp/extractor/generic.py @@ -74,6 +74,7 @@ from .teachable import TeachableIE from .ted import TedEmbedIE from .theplatform import ThePlatformIE from .threeqsdn import ThreeQSDNIE +from .tiktok import TikTokIE from .tnaflix import TNAFlixNetworkEmbedIE from .tube8 import Tube8IE from .tunein import TuneInBaseIE @@ -3756,6 +3757,11 @@ class GenericIE(InfoExtractor): if ruutu_urls: return self.playlist_from_matches(ruutu_urls, video_id, video_title) + # Look for Tiktok embeds + tiktok_urls = TikTokIE._extract_urls(webpage) + if tiktok_urls: + return self.playlist_from_matches(tiktok_urls, video_id, video_title) + # Look for HTML5 media entries = self._parse_html5_media_entries(url, webpage, video_id, m3u8_id='hls') if entries: diff --git a/yt_dlp/extractor/tiktok.py b/yt_dlp/extractor/tiktok.py index 4ba993582..4926096c0 100644 --- a/yt_dlp/extractor/tiktok.py +++ b/yt_dlp/extractor/tiktok.py @@ -1,28 +1,26 @@ import itertools +import json import random +import re import string import time -import json from .common import InfoExtractor -from ..compat import ( - compat_urllib_parse_unquote, - compat_urllib_parse_urlparse -) +from ..compat import compat_urllib_parse_unquote, compat_urllib_parse_urlparse from ..utils import ( ExtractorError, HEADRequest, + LazyList, UnsupportedError, get_first, int_or_none, join_nonempty, - LazyList, + qualities, srt_subtitles_timecode, str_or_none, traverse_obj, try_get, url_or_none, - qualities, ) @@ -36,6 +34,10 @@ class TikTokBaseIE(InfoExtractor): _WEBPAGE_HOST = 'https://www.tiktok.com/' QUALITIES = ('360p', '540p', '720p', '1080p') + @staticmethod + def _create_url(user_id, video_id): + return f'https://www.tiktok.com/@{user_id or "_"}/video/{video_id}' + def _call_api_impl(self, ep, query, manifest_app_version, video_id, fatal=True, note='Downloading API JSON', errnote='Unable to download API page'): self._set_cookie(self._API_HOSTNAME, 'odin_tt', ''.join(random.choice('0123456789abcdef') for _ in range(160))) @@ -361,7 +363,7 @@ class TikTokBaseIE(InfoExtractor): class TikTokIE(TikTokBaseIE): - _VALID_URL = r'https?://www\.tiktok\.com/@[\w\.-]+/video/(?P\d+)' + _VALID_URL = r'https?://www\.tiktok\.com/(?:embed|@(?P[\w\.-]+)/video)/(?P\d+)' _TESTS = [{ 'url': 'https://www.tiktok.com/@leenabhushan/video/6748451240264420610', @@ -466,7 +468,7 @@ class TikTokIE(TikTokBaseIE): 'info_dict': { 'id': '7059698374567611694', 'ext': 'mp4', - 'title': 'tiktok video #7059698374567611694', + 'title': 'TikTok video #7059698374567611694', 'description': '', 'uploader': 'pokemonlife22', 'creator': 'Pokemon', @@ -490,6 +492,11 @@ class TikTokIE(TikTokBaseIE): 'only_matching': True }] + @classmethod + def _extract_urls(cls, webpage): + return [mobj.group('url') for mobj in re.finditer( + rf'<(?:script|iframe)[^>]+\bsrc=(["\'])(?P{cls._VALID_URL})', webpage)] + def _extract_aweme_app(self, aweme_id): try: aweme_detail = self._call_api('aweme/detail', {'aweme_id': aweme_id}, aweme_id, @@ -506,7 +513,8 @@ class TikTokIE(TikTokBaseIE): return self._parse_aweme_video_app(aweme_detail) def _real_extract(self, url): - video_id = self._match_id(url) + video_id, user_id = self._match_valid_url(url).group('id', 'user_id') + url = self._create_url(user_id, video_id) try: return self._extract_aweme_app(video_id) -- cgit v1.2.3 From 53973b4d2cb349d39d6f240911142b330d1dd80d Mon Sep 17 00:00:00 2001 From: pukkandan Date: Fri, 20 May 2022 03:02:25 +0530 Subject: [utils] Fix bug in 0b9c08b47bb5e95c21b067044ace4e824d19a9c2 * Cache of `supports_terminal_sequences` must be reset after enabling VT mode * and move `windows_enable_vt_mode` to utils to avoid cyclic imports --- yt_dlp/YoutubeDL.py | 4 ++-- yt_dlp/compat/__init__.py | 16 ---------------- yt_dlp/compat/_legacy.py | 7 +++++++ yt_dlp/utils.py | 19 ++++++++++++++++++- 4 files changed, 27 insertions(+), 19 deletions(-) diff --git a/yt_dlp/YoutubeDL.py b/yt_dlp/YoutubeDL.py index 749cf9402..3dc11463c 100644 --- a/yt_dlp/YoutubeDL.py +++ b/yt_dlp/YoutubeDL.py @@ -33,7 +33,6 @@ from .compat import ( compat_str, compat_urllib_error, compat_urllib_request, - windows_enable_vt_mode, ) from .cookies import load_cookies from .downloader import FFmpegFD, get_suitable_downloader, shorten_protocol_name @@ -142,6 +141,7 @@ from .utils import ( url_basename, variadic, version_tuple, + windows_enable_vt_mode, write_json_file, write_string, ) @@ -3605,7 +3605,7 @@ class YoutubeDL: def get_encoding(stream): ret = str(getattr(stream, 'encoding', 'missing (%s)' % type(stream).__name__)) if not supports_terminal_sequences(stream): - from .compat import WINDOWS_VT_MODE # Must be imported locally + from .utils import WINDOWS_VT_MODE # Must be imported locally ret += ' (No VT)' if WINDOWS_VT_MODE is False else ' (No ANSI)' return ret diff --git a/yt_dlp/compat/__init__.py b/yt_dlp/compat/__init__.py index a0cd62110..c02e843d4 100644 --- a/yt_dlp/compat/__init__.py +++ b/yt_dlp/compat/__init__.py @@ -1,6 +1,4 @@ -import contextlib import os -import subprocess import sys import warnings import xml.etree.ElementTree as etree @@ -74,17 +72,3 @@ if compat_os_name in ('nt', 'ce'): return userhome + path[i:] else: compat_expanduser = os.path.expanduser - - -WINDOWS_VT_MODE = False if compat_os_name == 'nt' else None - - -def windows_enable_vt_mode(): # TODO: Do this the proper way https://bugs.python.org/issue30075 - if compat_os_name != 'nt': - return - global WINDOWS_VT_MODE - startupinfo = subprocess.STARTUPINFO() - startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW - with contextlib.suppress(Exception): - subprocess.Popen('', shell=True, startupinfo=startupinfo).wait() - WINDOWS_VT_MODE = True diff --git a/yt_dlp/compat/_legacy.py b/yt_dlp/compat/_legacy.py index ce24760e5..c4d95e1fb 100644 --- a/yt_dlp/compat/_legacy.py +++ b/yt_dlp/compat/_legacy.py @@ -55,3 +55,10 @@ compat_xml_parse_error = etree.ParseError compat_xpath = lambda xpath: xpath compat_zip = zip workaround_optparse_bug9161 = lambda: None + + +def __getattr__(name): + if name in ('WINDOWS_VT_MODE', 'windows_enable_vt_mode'): + from .. import utils + return getattr(utils, name) + raise AttributeError(name) diff --git a/yt_dlp/utils.py b/yt_dlp/utils.py index 0274e330d..78789b1c5 100644 --- a/yt_dlp/utils.py +++ b/yt_dlp/utils.py @@ -5094,10 +5094,12 @@ def jwt_decode_hs256(jwt): return payload_data +WINDOWS_VT_MODE = False if compat_os_name == 'nt' else None + + @functools.cache def supports_terminal_sequences(stream): if compat_os_name == 'nt': - from .compat import WINDOWS_VT_MODE # Must be imported locally if not WINDOWS_VT_MODE or get_windows_version() < (10, 0, 10586): return False elif not os.getenv('TERM'): @@ -5108,6 +5110,21 @@ def supports_terminal_sequences(stream): return False +def windows_enable_vt_mode(): # TODO: Do this the proper way https://bugs.python.org/issue30075 + if compat_os_name != 'nt': + return + global WINDOWS_VT_MODE + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + try: + subprocess.Popen('', shell=True, startupinfo=startupinfo).wait() + except Exception: + return + + WINDOWS_VT_MODE = True + supports_terminal_sequences.cache_clear() + + _terminal_sequences_re = re.compile('\033\\[[^m]+m') -- cgit v1.2.3 From e79969b2425e0c52813780f2b2afbccd4b4b0647 Mon Sep 17 00:00:00 2001 From: pukkandan Date: Fri, 20 May 2022 05:55:32 +0530 Subject: Return an error code if update fails Closes #3802 --- yt_dlp/__init__.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/yt_dlp/__init__.py b/yt_dlp/__init__.py index b2429f5af..893b86a3b 100644 --- a/yt_dlp/__init__.py +++ b/yt_dlp/__init__.py @@ -849,22 +849,16 @@ def _real_main(argv=None): with YoutubeDL(ydl_opts) as ydl: actual_use = all_urls or opts.load_info_filename - # Remove cache dir if opts.rm_cachedir: ydl.cache.remove() - # Update version - if opts.update_self: + if opts.update_self and run_update(ydl) and actual_use: # If updater returns True, exit. Required for windows - if run_update(ydl): - if actual_use: - return 100, 'ERROR: The program must exit for the update to complete' - return + return 100, 'ERROR: The program must exit for the update to complete' - # Maybe do nothing if not actual_use: if opts.update_self or opts.rm_cachedir: - return + return ydl._download_retcode ydl.warn_if_short_id(sys.argv[1:] if argv is None else argv) parser.error( -- cgit v1.2.3 From 88d62206b41723ba85174bd4d33469089d23334b Mon Sep 17 00:00:00 2001 From: Jeff Huffman Date: Thu, 19 May 2022 17:37:04 -0700 Subject: [crunchyroll:beta] Fix extractor after API change (#3801) Closes #2052 Authored by: Burve, tejing1 --- yt_dlp/extractor/crunchyroll.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/yt_dlp/extractor/crunchyroll.py b/yt_dlp/extractor/crunchyroll.py index bb1dbbaad..1d186bd38 100644 --- a/yt_dlp/extractor/crunchyroll.py +++ b/yt_dlp/extractor/crunchyroll.py @@ -728,11 +728,12 @@ class CrunchyrollBetaBaseIE(CrunchyrollBaseIE): headers={ 'Authorization': auth_response['token_type'] + ' ' + auth_response['access_token'] }) - bucket = policy_response['cms']['bucket'] + cms = traverse_obj(policy_response, 'cms_beta', 'cms') + bucket = cms['bucket'] params = { - 'Policy': policy_response['cms']['policy'], - 'Signature': policy_response['cms']['signature'], - 'Key-Pair-Id': policy_response['cms']['key_pair_id'] + 'Policy': cms['policy'], + 'Signature': cms['signature'], + 'Key-Pair-Id': cms['key_pair_id'] } locale = traverse_obj(initial_state, ('localization', 'locale')) if locale: -- cgit v1.2.3 From 79c318937bd3d2bdd348b94465101925b146d14d Mon Sep 17 00:00:00 2001 From: Elyse Date: Fri, 20 May 2022 05:17:32 -0500 Subject: [ina] Fix extractor (#3807) Closes #2463 Authored by: elyse0 --- yt_dlp/extractor/ina.py | 84 +++++++++++++++++++------------------------------ 1 file changed, 32 insertions(+), 52 deletions(-) diff --git a/yt_dlp/extractor/ina.py b/yt_dlp/extractor/ina.py index 56038f1ca..9e2c9cf47 100644 --- a/yt_dlp/extractor/ina.py +++ b/yt_dlp/extractor/ina.py @@ -1,23 +1,19 @@ from .common import InfoExtractor -from ..utils import ( - determine_ext, - int_or_none, - strip_or_none, - xpath_attr, - xpath_text, -) +from ..utils import unified_strdate class InaIE(InfoExtractor): - _VALID_URL = r'https?://(?:(?:www|m)\.)?ina\.fr/(?:video|audio)/(?P[A-Z0-9_]+)' + _VALID_URL = r'https?://(?:(?:www|m)\.)?ina\.fr/(?:[^/]+/)?(?:video|audio)/(?P\w+)' _TESTS = [{ - 'url': 'http://www.ina.fr/video/I12055569/francois-hollande-je-crois-que-c-est-clair-video.html', - 'md5': 'a667021bf2b41f8dc6049479d9bb38a3', + 'url': 'https://www.ina.fr/video/I12055569/francois-hollande-je-crois-que-c-est-clair-video.html', + 'md5': 'c5a09e5cb5604ed10709f06e7a377dda', 'info_dict': { 'id': 'I12055569', 'ext': 'mp4', 'title': 'François Hollande "Je crois que c\'est clair"', - 'description': 'md5:3f09eb072a06cb286b8f7e4f77109663', + 'description': 'md5:08201f1c86fb250611f0ba415d21255a', + 'upload_date': '20070712', + 'thumbnail': 'https://cdn-hub.ina.fr/notice/690x517/3c4/I12055569.jpeg', } }, { 'url': 'https://www.ina.fr/video/S806544_001/don-d-organes-des-avancees-mais-d-importants-besoins-video.html', @@ -31,53 +27,37 @@ class InaIE(InfoExtractor): }, { 'url': 'http://m.ina.fr/video/I12055569', 'only_matching': True, + }, { + 'url': 'https://www.ina.fr/ina-eclaire-actu/video/cpb8205116303/les-jeux-electroniques', + 'md5': '4b8284a9a3a184fdc7e744225b8251e7', + 'info_dict': { + 'id': 'CPB8205116303', + 'ext': 'mp4', + 'title': 'Les jeux électroniques', + 'description': 'md5:e09f7683dad1cc60b74950490127d233', + 'upload_date': '19821204', + 'duration': 657, + 'thumbnail': 'https://cdn-hub.ina.fr/notice/690x517/203/CPB8205116303.jpeg', + } }] def _real_extract(self, url): - video_id = self._match_id(url) - info_doc = self._download_xml( - 'http://player.ina.fr/notices/%s.mrss' % video_id, video_id) - item = info_doc.find('channel/item') - title = xpath_text(item, 'title', fatal=True) - media_ns_xpath = lambda x: self._xpath_ns(x, 'http://search.yahoo.com/mrss/') - content = item.find(media_ns_xpath('content')) + video_id = self._match_id(url).upper() + webpage = self._download_webpage(url, video_id) - get_furl = lambda x: xpath_attr(content, media_ns_xpath(x), 'url') - formats = [] - for q, w, h in (('bq', 400, 300), ('mq', 512, 384), ('hq', 768, 576)): - q_url = get_furl(q) - if not q_url: - continue - formats.append({ - 'format_id': q, - 'url': q_url, - 'width': w, - 'height': h, - }) - if not formats: - furl = get_furl('player') or content.attrib['url'] - ext = determine_ext(furl) - formats = [{ - 'url': furl, - 'vcodec': 'none' if ext == 'mp3' else None, - 'ext': ext, - }] + api_url = self._html_search_regex( + r'asset-details-url\s*=\s*["\'](?P[^"\']+)', + webpage, 'api_url').replace(video_id, f'{video_id}.json') - thumbnails = [] - for thumbnail in content.findall(media_ns_xpath('thumbnail')): - thumbnail_url = thumbnail.get('url') - if not thumbnail_url: - continue - thumbnails.append({ - 'url': thumbnail_url, - 'height': int_or_none(thumbnail.get('height')), - 'width': int_or_none(thumbnail.get('width')), - }) + api_response = self._download_json(api_url, video_id) return { 'id': video_id, - 'formats': formats, - 'title': title, - 'description': strip_or_none(xpath_text(item, 'description')), - 'thumbnails': thumbnails, + 'url': api_response['resourceUrl'], + 'ext': {'video': 'mp4', 'audio': 'mp3'}.get(api_response.get('type')), + 'title': api_response.get('title'), + 'description': api_response.get('description'), + 'upload_date': unified_strdate(api_response.get('dateOfBroadcast')), + 'duration': api_response.get('duration'), + 'thumbnail': api_response.get('resourceThumbnail'), } -- cgit v1.2.3 From 854b0d325e36acddef7a798be21a98756d86ca89 Mon Sep 17 00:00:00 2001 From: adamanldo <97370646+adamanldo@users.noreply.github.com> Date: Fri, 20 May 2022 09:19:13 -0400 Subject: [StreamCZ] Fix extractor (#3789) Closes #3579 Authored by: dirkf, adamanldo --- yt_dlp/extractor/streamcz.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/yt_dlp/extractor/streamcz.py b/yt_dlp/extractor/streamcz.py index 85fc3a3c3..849a9882d 100644 --- a/yt_dlp/extractor/streamcz.py +++ b/yt_dlp/extractor/streamcz.py @@ -52,8 +52,8 @@ class StreamCZIE(InfoExtractor): def _extract_formats(self, spl_url, video): for ext, pref, streams in ( - ('ts', -1, traverse_obj(video, ('http_stream', 'qualities'))), - ('mp4', 1, video.get('mp4'))): + ('ts', -1, traverse_obj(video, ('http_stream', 'qualities')) or {}), + ('mp4', 1, video.get('mp4') or {})): for format_id, stream in streams.items(): if not stream.get('url'): continue -- cgit v1.2.3 From 666c36d58dfacc8998952569cc2d9c414957c53d Mon Sep 17 00:00:00 2001 From: pukkandan Date: Fri, 20 May 2022 20:49:30 +0530 Subject: Bugfix for 23326151c45b632c3d5948bd018e80abb370e676 --- yt_dlp/downloader/common.py | 6 +++--- yt_dlp/downloader/fragment.py | 17 +++++------------ 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/yt_dlp/downloader/common.py b/yt_dlp/downloader/common.py index 0b3383071..276675532 100644 --- a/yt_dlp/downloader/common.py +++ b/yt_dlp/downloader/common.py @@ -18,8 +18,8 @@ from ..utils import ( decodeArgument, encodeFilename, error_to_compat_str, + float_or_none, format_bytes, - int_or_none, sanitize_open, shell_quote, timeconvert, @@ -406,9 +406,9 @@ class FileDownloader: def sleep_retry(self, retry_type, count): sleep_func = self.params.get('retry_sleep_functions', {}).get(retry_type) - delay = int_or_none(sleep_func(n=count - 1)) if sleep_func else None + delay = float_or_none(sleep_func(n=count - 1)) if sleep_func else None if delay: - self.__to_screen(f'Sleeping {delay} seconds ...') + self.__to_screen(f'Sleeping {delay:.2f} seconds ...') time.sleep(delay) return sleep_func is not None diff --git a/yt_dlp/downloader/fragment.py b/yt_dlp/downloader/fragment.py index 410c8c1a4..493849001 100644 --- a/yt_dlp/downloader/fragment.py +++ b/yt_dlp/downloader/fragment.py @@ -165,18 +165,11 @@ class FragmentFD(FileDownloader): total_frags_str = 'unknown (live)' self.to_screen(f'[{self.FD_NAME}] Total fragments: {total_frags_str}') self.report_destination(ctx['filename']) - dl = HttpQuietDownloader( - self.ydl, - { - 'continuedl': self.params.get('continuedl', True), - 'quiet': self.params.get('quiet'), - 'noprogress': True, - 'ratelimit': self.params.get('ratelimit'), - 'retries': self.params.get('retries', 0), - 'nopart': self.params.get('nopart', False), - 'test': False, - } - ) + dl = HttpQuietDownloader(self.ydl, { + **self.params, + 'noprogress': True, + 'test': False, + }) tmpfilename = self.temp_name(ctx['filename']) open_mode = 'wb' resume_len = 0 -- cgit v1.2.3 From 2762dbb17e8556140f9fff0c0aa3373c521f5e09 Mon Sep 17 00:00:00 2001 From: pukkandan Date: Fri, 20 May 2022 20:55:21 +0530 Subject: [compat] Add `functools.cached_property` --- yt_dlp/compat/functools.py | 12 ++++++++++++ yt_dlp/downloader/common.py | 3 ++- yt_dlp/downloader/external.py | 3 ++- yt_dlp/extractor/common.py | 4 ++-- yt_dlp/extractor/youtube.py | 6 +++--- 5 files changed, 21 insertions(+), 7 deletions(-) diff --git a/yt_dlp/compat/functools.py b/yt_dlp/compat/functools.py index 36c983642..c3c4d8f48 100644 --- a/yt_dlp/compat/functools.py +++ b/yt_dlp/compat/functools.py @@ -10,3 +10,15 @@ try: cache # >= 3.9 except NameError: cache = lru_cache(maxsize=None) + +try: + cached_property # >= 3.8 +except NameError: + class cached_property: + def __init__(self, func): + update_wrapper(self, func) + self.func = func + + def __get__(self, instance, _): + setattr(instance, self.func.__name__, self.func(instance)) + return getattr(instance, self.func.__name__) diff --git a/yt_dlp/downloader/common.py b/yt_dlp/downloader/common.py index 276675532..d74692130 100644 --- a/yt_dlp/downloader/common.py +++ b/yt_dlp/downloader/common.py @@ -11,6 +11,7 @@ from ..minicurses import ( MultilinePrinter, QuietMultilinePrinter, ) +from ..compat import functools from ..utils import ( NUMBER_RE, LockingUnsupportedError, @@ -102,7 +103,7 @@ class FileDownloader: __to_screen = to_screen - @property + @functools.cached_property def FD_NAME(self): return re.sub(r'(? Date: Fri, 20 May 2022 20:55:21 +0530 Subject: Bugfix for 3a408f9d199127ca2626359e21a866a09ab236b3 --- yt_dlp/compat/functools.py | 2 ++ yt_dlp/downloader/common.py | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/yt_dlp/compat/functools.py b/yt_dlp/compat/functools.py index c3c4d8f48..ec003ea90 100644 --- a/yt_dlp/compat/functools.py +++ b/yt_dlp/compat/functools.py @@ -20,5 +20,7 @@ except NameError: self.func = func def __get__(self, instance, _): + if instance is None: + return self setattr(instance, self.func.__name__, self.func(instance)) return getattr(instance, self.func.__name__) diff --git a/yt_dlp/downloader/common.py b/yt_dlp/downloader/common.py index d74692130..93eb10f76 100644 --- a/yt_dlp/downloader/common.py +++ b/yt_dlp/downloader/common.py @@ -11,11 +11,11 @@ from ..minicurses import ( MultilinePrinter, QuietMultilinePrinter, ) -from ..compat import functools from ..utils import ( NUMBER_RE, LockingUnsupportedError, Namespace, + classproperty, decodeArgument, encodeFilename, error_to_compat_str, @@ -103,9 +103,9 @@ class FileDownloader: __to_screen = to_screen - @functools.cached_property - def FD_NAME(self): - return re.sub(r'(? Date: Fri, 20 May 2022 21:55:26 +0530 Subject: Fix `--simulate --max-downloads` Bug in c3e6ffba536980e5e1af00e0ecb2275621b4db17 Closes #3815 --- yt_dlp/YoutubeDL.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/yt_dlp/YoutubeDL.py b/yt_dlp/YoutubeDL.py index 3dc11463c..037b24d00 100644 --- a/yt_dlp/YoutubeDL.py +++ b/yt_dlp/YoutubeDL.py @@ -2877,8 +2877,13 @@ class YoutubeDL: # Forced printings self.__forced_printings(info_dict, full_filename, incomplete=('format' not in info_dict)) + def check_max_downloads(): + if self._num_downloads >= float(self.params.get('max_downloads') or 'inf'): + raise MaxDownloadsReached() + if self.params.get('simulate'): info_dict['__write_download_archive'] = self.params.get('force_write_download_archive') + check_max_downloads() return if full_filename is None: @@ -3221,10 +3226,7 @@ class YoutubeDL: # Make sure the info_dict was modified in-place assert info_dict is original_infodict - - max_downloads = self.params.get('max_downloads') - if max_downloads is not None and self._num_downloads >= int(max_downloads): - raise MaxDownloadsReached() + check_max_downloads() def __download_wrapper(self, func): @functools.wraps(func) -- cgit v1.2.3 From 415f8d51a8f3565d7a1d4a8188511e7ad68514c7 Mon Sep 17 00:00:00 2001 From: pukkandan Date: Sat, 21 May 2022 02:13:23 +0530 Subject: Ensure pre-processor errors do not block video download Closes #2875 --- yt_dlp/YoutubeDL.py | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/yt_dlp/YoutubeDL.py b/yt_dlp/YoutubeDL.py index 037b24d00..946b48b10 100644 --- a/yt_dlp/YoutubeDL.py +++ b/yt_dlp/YoutubeDL.py @@ -1038,6 +1038,7 @@ class YoutubeDL: def _copy_infodict(info_dict): info_dict = dict(info_dict) info_dict.pop('__postprocessors', None) + info_dict.pop('__pending_error', None) return info_dict def prepare_outtmpl(self, outtmpl, info_dict, sanitize=False): @@ -1529,6 +1530,7 @@ class YoutubeDL: self.add_extra_info(info_copy, extra_info) info_copy, _ = self.pre_process(info_copy) self.__forced_printings(info_copy, self.prepare_filename(info_copy), incomplete=True) + self._raise_pending_errors(info_copy) if self.params.get('force_write_download_archive', False): self.record_download_archive(info_copy) return ie_result @@ -1536,6 +1538,7 @@ class YoutubeDL: if result_type == 'video': self.add_extra_info(ie_result, extra_info) ie_result = self.process_video_result(ie_result, download=download) + self._raise_pending_errors(ie_result) additional_urls = (ie_result or {}).get('additional_urls') if additional_urls: # TODO: Improve MetadataParserPP to allow setting a list @@ -2383,6 +2386,11 @@ class YoutubeDL: if info_dict.get('%s_number' % field) is not None and not info_dict.get(field): info_dict[field] = '%s %d' % (field.capitalize(), info_dict['%s_number' % field]) + def _raise_pending_errors(self, info): + err = info.pop('__pending_error', None) + if err: + self.report_error(err, tb=False) + def process_video_result(self, info_dict, download=True): assert info_dict.get('_type', 'video') == 'video' self._num_videos += 1 @@ -2643,6 +2651,7 @@ class YoutubeDL: self.process_info(new_info) except MaxDownloadsReached: max_downloads_reached = True + self._raise_pending_errors(new_info) # Remove copied info for key, val in tuple(new_info.items()): if info_dict.get(key) == val: @@ -2987,12 +2996,8 @@ class YoutubeDL: info_dict.clear() info_dict.update(new_info) - try: - new_info, files_to_move = self.pre_process(info_dict, 'before_dl', files_to_move) - replace_info_dict(new_info) - except PostProcessingError as err: - self.report_error('Preprocessing: %s' % str(err)) - return + new_info, files_to_move = self.pre_process(info_dict, 'before_dl', files_to_move) + replace_info_dict(new_info) if self.params.get('skip_download'): info_dict['filepath'] = temp_filename @@ -3152,6 +3157,7 @@ class YoutubeDL: self.report_error(f'content too short (expected {err.expected} bytes and served {err.downloaded})') return + self._raise_pending_errors(info_dict) if success and full_filename != '-': def fixup(): @@ -3369,7 +3375,12 @@ class YoutubeDL: def pre_process(self, ie_info, key='pre_process', files_to_move=None): info = dict(ie_info) info['__files_to_move'] = files_to_move or {} - info = self.run_all_pps(key, info) + try: + info = self.run_all_pps(key, info) + except PostProcessingError as err: + msg = f'Preprocessing: {err}' + info.setdefault('__pending_error', msg) + self.report_error(msg, is_error=False) return info, info.pop('__files_to_move', None) def post_process(self, filename, info, files_to_move=None): -- cgit v1.2.3 From c487cf00101525ff836d59a2a42ef63e85ea9556 Mon Sep 17 00:00:00 2001 From: pukkandan Date: Sun, 17 Apr 2022 22:48:50 +0530 Subject: [cleanup] Misc --- Makefile | 12 +++--- devscripts/make_issue_template.py | 1 - devscripts/make_readme.py | 46 ++++++++++++++++----- pytest.ini | 4 -- setup.cfg | 32 ++++++++++++++- setup.py | 2 +- tox.ini | 16 -------- yt_dlp/YoutubeDL.py | 14 +++---- yt_dlp/__init__.py | 1 + yt_dlp/downloader/common.py | 1 + yt_dlp/downloader/dash.py | 2 +- yt_dlp/downloader/external.py | 34 +++++++++------- yt_dlp/downloader/hls.py | 2 +- yt_dlp/downloader/http.py | 16 ++++---- yt_dlp/downloader/niconico.py | 6 +-- yt_dlp/downloader/youtube_live_chat.py | 5 ++- yt_dlp/extractor/common.py | 6 +-- yt_dlp/extractor/testurl.py | 2 +- yt_dlp/update.py | 35 +++++++++------- yt_dlp/utils.py | 73 +++++++++++++++------------------- 20 files changed, 171 insertions(+), 139 deletions(-) delete mode 100644 pytest.ini delete mode 100644 tox.ini diff --git a/Makefile b/Makefile index 0ff5626ad..3e5885c1d 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,9 @@ tar: yt-dlp.tar.gz # Keep this list in sync with MANIFEST.in # intended use: when building a source distribution, # make pypi-files && python setup.py sdist -pypi-files: AUTHORS Changelog.md LICENSE README.md README.txt supportedsites completions yt-dlp.1 devscripts/* test/* +pypi-files: + AUTHORS Changelog.md LICENSE README.md README.txt supportedsites \ + completions yt-dlp.1 requirements.txt devscripts/* test/* .PHONY: all clean install test tar pypi-files completions ot offlinetest codetest supportedsites @@ -91,10 +93,10 @@ yt-dlp: yt_dlp/*.py yt_dlp/*/*.py rm yt-dlp.zip chmod a+x yt-dlp -README.md: yt_dlp/*.py yt_dlp/*/*.py +README.md: yt_dlp/*.py yt_dlp/*/*.py devscripts/make_readme.py COLUMNS=80 $(PYTHON) yt_dlp/__main__.py --ignore-config --help | $(PYTHON) devscripts/make_readme.py -CONTRIBUTING.md: README.md +CONTRIBUTING.md: README.md devscripts/make_contributing.py $(PYTHON) devscripts/make_contributing.py README.md CONTRIBUTING.md issuetemplates: devscripts/make_issue_template.py .github/ISSUE_TEMPLATE_tmpl/1_broken_site.yml .github/ISSUE_TEMPLATE_tmpl/2_site_support_request.yml .github/ISSUE_TEMPLATE_tmpl/3_site_feature_request.yml .github/ISSUE_TEMPLATE_tmpl/4_bug_report.yml .github/ISSUE_TEMPLATE_tmpl/5_feature_request.yml yt_dlp/version.py @@ -111,7 +113,7 @@ supportedsites: README.txt: README.md pandoc -f $(MARKDOWN) -t plain README.md -o README.txt -yt-dlp.1: README.md +yt-dlp.1: README.md devscripts/prepare_manpage.py $(PYTHON) devscripts/prepare_manpage.py yt-dlp.1.temp.md pandoc -s -f $(MARKDOWN) -t man yt-dlp.1.temp.md -o yt-dlp.1 rm -f yt-dlp.1.temp.md @@ -147,7 +149,7 @@ yt-dlp.tar.gz: all CONTRIBUTING.md Collaborators.md CONTRIBUTORS AUTHORS \ Makefile MANIFEST.in yt-dlp.1 README.txt completions \ setup.py setup.cfg yt-dlp yt_dlp requirements.txt \ - devscripts test tox.ini pytest.ini + devscripts test AUTHORS: .mailmap git shortlog -s -n | cut -f2 | sort > AUTHORS diff --git a/devscripts/make_issue_template.py b/devscripts/make_issue_template.py index 811a3e9b5..5a309008e 100644 --- a/devscripts/make_issue_template.py +++ b/devscripts/make_issue_template.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -import io import optparse diff --git a/devscripts/make_readme.py b/devscripts/make_readme.py index fd234bf58..15c4a7c7d 100755 --- a/devscripts/make_readme.py +++ b/devscripts/make_readme.py @@ -2,6 +2,7 @@ # yt-dlp --help | make_readme.py # This must be run in a console of correct width +import functools import re import sys @@ -12,19 +13,44 @@ OPTIONS_END = 'CONFIGURATION' EPILOG_START = 'See full documentation' -helptext = sys.stdin.read() -if isinstance(helptext, bytes): - helptext = helptext.decode() +def take_section(text, start=None, end=None, *, shift=0): + return text[ + text.index(start) + shift if start else None: + text.index(end) + shift if end else None + ] -start, end = helptext.index(f'\n {OPTIONS_START}'), helptext.index(f'\n{EPILOG_START}') -options = re.sub(r'(?m)^ (\w.+)$', r'## \1', helptext[start + 1: end + 1]) + +def apply_patch(text, patch): + return re.sub(*patch, text) + + +options = take_section(sys.stdin.read(), f'\n {OPTIONS_START}', f'\n{EPILOG_START}', shift=1) + +switch_col_width = len(re.search(r'(?m)^\s{5,}', options).group()) +delim = f'\n{" " * switch_col_width}' + +PATCHES = ( + ( # Headings + r'(?m)^ (\w.+\n)( (?=\w))?', + r'## \1' + ), + ( # Do not split URLs + rf'({delim[:-1]})? (?P