diff --git a/README.md b/README.md index a306b199e9..4f731785d3 100644 --- a/README.md +++ b/README.md @@ -1193,7 +1193,7 @@ # OUTPUT TEMPLATE 1. **More Conversions**: In addition to the normal format types `diouxXeEfFgGcrs`, yt-dlp additionally supports converting to `B` = **B**ytes, `j` = **j**son (flag `#` for pretty-printing, `+` for Unicode), `h` = HTML escaping, `l` = a comma separated **l**ist (flag `#` for `\n` newline-separated), `q` = a string **q**uoted for the terminal (flag `#` to split a list into different arguments), `D` = add **D**ecimal suffixes (e.g. 10M) (flag `#` to use 1024 as factor), and `S` = **S**anitize as filename (flag `#` for restricted) -1. **Unicode normalization**: The format type `U` can be used for NFC [unicode normalization](https://docs.python.org/3/library/unicodedata.html#unicodedata.normalize). The alternate form flag (`#`) changes the normalization to NFD and the conversion flag `+` can be used for NFKC/NFKD compatibility equivalence normalization. E.g. `%(title)+.100U` is NFKC +1. **Unicode normalization**: The format type `U` can be used for NFC [Unicode normalization](https://docs.python.org/3/library/unicodedata.html#unicodedata.normalize). The alternate form flag (`#`) changes the normalization to NFD and the conversion flag `+` can be used for NFKC/NFKD compatibility equivalence normalization. E.g. `%(title)+.100U` is NFKC To summarize, the general syntax for a field is: ``` diff --git a/yt_dlp/YoutubeDL.py b/yt_dlp/YoutubeDL.py index 13725cddc3..42780e7941 100644 --- a/yt_dlp/YoutubeDL.py +++ b/yt_dlp/YoutubeDL.py @@ -548,7 +548,7 @@ class YoutubeDL: # NB: Keep in sync with the docstring of extractor/common.py 'url', 'manifest_url', 'manifest_stream_number', 'ext', 'format', 'format_id', 'format_note', 'width', 'height', 'resolution', 'dynamic_range', 'tbr', 'abr', 'acodec', 'asr', 'audio_channels', - 'vbr', 'fps', 'vcodec', 'container', 'filesize', 'filesize_approx', + 'vbr', 'fps', 'vcodec', 'container', 'filesize', 'filesize_approx', 'rows', 'columns', 'player_url', 'protocol', 'fragment_base_url', 'fragments', 'is_from_start', 'preference', 'language', 'language_preference', 'quality', 'source_preference', 'http_headers', 'stretched_ratio', 'no_resume', 'has_drm', 'downloader_options', @@ -3586,7 +3586,7 @@ def render_formats_table(self, info_dict): format_field(f, 'ext'), self.format_resolution(f), self._format_note(f) - ] for f in formats if f.get('preference') is None or f['preference'] >= -1000] + ] for f in formats if (f.get('preference') or 0) >= -1000] return render_table(['format code', 'extension', 'resolution', 'note'], table, extra_gap=1) def simplified_codec(f, field): diff --git a/yt_dlp/__init__.py b/yt_dlp/__init__.py index 9382ff43ba..726fb0685c 100644 --- a/yt_dlp/__init__.py +++ b/yt_dlp/__init__.py @@ -962,6 +962,8 @@ def _real_main(argv=None): def main(argv=None): + global _IN_CLI + _IN_CLI = True try: _exit(*variadic(_real_main(argv))) except DownloadError: diff --git a/yt_dlp/__main__.py b/yt_dlp/__main__.py index 895918c272..ff5d71d3c9 100644 --- a/yt_dlp/__main__.py +++ b/yt_dlp/__main__.py @@ -14,5 +14,4 @@ import yt_dlp if __name__ == '__main__': - yt_dlp._IN_CLI = True yt_dlp.main() diff --git a/yt_dlp/downloader/common.py b/yt_dlp/downloader/common.py index 221b3827c7..8d110c3747 100644 --- a/yt_dlp/downloader/common.py +++ b/yt_dlp/downloader/common.py @@ -333,7 +333,7 @@ def with_fields(*tups, default=''): return tmpl return default - _formats_bytes = lambda k: f'{format_bytes(s.get(k)):>10s}' + _format_bytes = lambda k: f'{format_bytes(s.get(k)):>10s}' if s['status'] == 'finished': if self.params.get('noprogress'): @@ -342,7 +342,7 @@ def with_fields(*tups, default=''): s.update({ 'speed': speed, '_speed_str': self.format_speed(speed).strip(), - '_total_bytes_str': _formats_bytes('total_bytes'), + '_total_bytes_str': _format_bytes('total_bytes'), '_elapsed_str': self.format_seconds(s.get('elapsed')), '_percent_str': self.format_percent(100), }) @@ -363,9 +363,9 @@ def with_fields(*tups, default=''): lambda: 100 * s['downloaded_bytes'] / s['total_bytes'], lambda: 100 * s['downloaded_bytes'] / s['total_bytes_estimate'], lambda: s['downloaded_bytes'] == 0 and 0)), - '_total_bytes_str': _formats_bytes('total_bytes'), - '_total_bytes_estimate_str': _formats_bytes('total_bytes_estimate'), - '_downloaded_bytes_str': _formats_bytes('downloaded_bytes'), + '_total_bytes_str': _format_bytes('total_bytes'), + '_total_bytes_estimate_str': _format_bytes('total_bytes_estimate'), + '_downloaded_bytes_str': _format_bytes('downloaded_bytes'), '_elapsed_str': self.format_seconds(s.get('elapsed')), }) diff --git a/yt_dlp/extractor/common.py b/yt_dlp/extractor/common.py index ab8def57da..ec3fb58e56 100644 --- a/yt_dlp/extractor/common.py +++ b/yt_dlp/extractor/common.py @@ -1108,7 +1108,9 @@ def get_param(self, name, default=None, *args, **kwargs): return self._downloader.params.get(name, default, *args, **kwargs) return default - def report_drm(self, video_id, partial=False): + def report_drm(self, video_id, partial=NO_DEFAULT): + if partial is not NO_DEFAULT: + self._downloader.deprecation_warning('InfoExtractor.report_drm no longer accepts the argument partial') self.raise_no_formats('This video is DRM protected', expected=True, video_id=video_id) def report_extraction(self, id_or_name): diff --git a/yt_dlp/extractor/generic.py b/yt_dlp/extractor/generic.py index b7a5ffb5b1..5abde33a91 100644 --- a/yt_dlp/extractor/generic.py +++ b/yt_dlp/extractor/generic.py @@ -32,6 +32,7 @@ unified_timestamp, unsmuggle_url, url_or_none, + variadic, xpath_attr, xpath_text, xpath_with_ns, @@ -2820,11 +2821,8 @@ def _extract_embeds(self, url, webpage, *, urlh=None, info_dict={}): webpage) if mobj is not None: varname = mobj.group(1) - sources = self._parse_json( - mobj.group(2), video_id, transform_source=js_to_json, - fatal=False) or [] - if not isinstance(sources, list): - sources = [sources] + sources = variadic(self._parse_json( + mobj.group(2), video_id, transform_source=js_to_json, fatal=False) or []) formats = [] subtitles = {} for source in sources: diff --git a/yt_dlp/extractor/prankcast.py b/yt_dlp/extractor/prankcast.py index 7446caf3c0..0eb5f98d19 100644 --- a/yt_dlp/extractor/prankcast.py +++ b/yt_dlp/extractor/prankcast.py @@ -21,6 +21,23 @@ class PrankCastIE(InfoExtractor): 'tags': ['prank call', 'prank'], 'upload_date': '20220825' } + }, { + 'url': 'https://prankcast.com/phonelosers/showreel/2048-NOT-COOL', + 'info_dict': { + 'id': '2048', + 'ext': 'mp3', + 'title': 'NOT COOL', + 'display_id': 'NOT-COOL', + 'timestamp': 1665028364, + 'uploader': 'phonelosers', + 'channel_id': 6, + 'duration': 4044, + 'cast': ['phonelosers'], + 'description': '', + 'categories': ['prank'], + 'tags': ['prank call', 'prank'], + 'upload_date': '20221006' + } }] def _real_extract(self, url): diff --git a/yt_dlp/extractor/tv24ua.py b/yt_dlp/extractor/tv24ua.py index 553a70b6b2..2f2571df76 100644 --- a/yt_dlp/extractor/tv24ua.py +++ b/yt_dlp/extractor/tv24ua.py @@ -1,12 +1,7 @@ import re from .common import InfoExtractor -from ..utils import ( - determine_ext, - js_to_json, - mimetype2ext, - traverse_obj, -) +from ..utils import determine_ext, js_to_json, mimetype2ext, traverse_obj class TV24UAVideoIE(InfoExtractor): diff --git a/yt_dlp/extractor/youtube.py b/yt_dlp/extractor/youtube.py index 857c9670c5..a12e5b03e7 100644 --- a/yt_dlp/extractor/youtube.py +++ b/yt_dlp/extractor/youtube.py @@ -1721,7 +1721,8 @@ class YoutubeIE(YoutubeBaseInfoExtractor): 'live_status': 'not_live', 'playable_in_embed': True, 'comment_count': int, - 'channel_follower_count': int + 'channel_follower_count': int, + 'chapters': list, }, 'params': { 'skip_download': True, @@ -1754,7 +1755,8 @@ class YoutubeIE(YoutubeBaseInfoExtractor): 'live_status': 'not_live', 'channel_url': 'https://www.youtube.com/channel/UCH1dpzjCEiGAt8CXkryhkZg', 'comment_count': int, - 'channel_follower_count': int + 'channel_follower_count': int, + 'chapters': list, }, 'params': { 'skip_download': True, @@ -2019,7 +2021,8 @@ class YoutubeIE(YoutubeBaseInfoExtractor): 'duration': 522, 'channel': 'kudvenkat', 'comment_count': int, - 'channel_follower_count': int + 'channel_follower_count': int, + 'chapters': list, }, 'params': { 'skip_download': True, @@ -2169,7 +2172,8 @@ class YoutubeIE(YoutubeBaseInfoExtractor): 'like_count': int, 'live_status': 'not_live', 'playable_in_embed': True, - 'channel_follower_count': int + 'channel_follower_count': int, + 'chapters': list, }, 'params': { 'format': '17', # 3gp format available on android @@ -2213,7 +2217,8 @@ class YoutubeIE(YoutubeBaseInfoExtractor): 'duration': 248, 'categories': ['Education'], 'age_limit': 0, - 'channel_follower_count': int + 'channel_follower_count': int, + 'chapters': list, }, 'params': {'format': 'mhtml', 'skip_download': True} }, { # Ensure video upload_date is in UTC timezone (video was uploaded 1641170939) diff --git a/yt_dlp/postprocessor/sponsorblock.py b/yt_dlp/postprocessor/sponsorblock.py index 188eb059aa..6ba87cd672 100644 --- a/yt_dlp/postprocessor/sponsorblock.py +++ b/yt_dlp/postprocessor/sponsorblock.py @@ -85,7 +85,7 @@ def to_chapter(s): sponsor_chapters = [to_chapter(s) for s in duration_match] if not sponsor_chapters: - self.to_screen('No segments were found in the SponsorBlock database') + self.to_screen('No matching segments were found in the SponsorBlock database') else: self.to_screen(f'Found {len(sponsor_chapters)} segments in the SponsorBlock database') return sponsor_chapters diff --git a/yt_dlp/utils.py b/yt_dlp/utils.py index adb7c0e8c5..1e2342f3e9 100644 --- a/yt_dlp/utils.py +++ b/yt_dlp/utils.py @@ -5724,7 +5724,7 @@ def parse_args(self): return self.parser.parse_args(self.all_args) -class WebSocketsWrapper(): +class WebSocketsWrapper: """Wraps websockets module to use in non-async scopes""" pool = None @@ -5808,11 +5808,9 @@ def cached_method(f): def wrapper(self, *args, **kwargs): bound_args = signature.bind(self, *args, **kwargs) bound_args.apply_defaults() - key = tuple(bound_args.arguments.values()) + key = tuple(bound_args.arguments.values())[1:] - if not hasattr(self, '__cached_method__cache'): - self.__cached_method__cache = {} - cache = self.__cached_method__cache.setdefault(f.__name__, {}) + cache = vars(self).setdefault('__cached_method__cache', {}).setdefault(f.__name__, {}) if key not in cache: cache[key] = f(self, *args, **kwargs) return cache[key]