diff options
Diffstat (limited to 'yt_dlp/__init__.py')
-rw-r--r-- | yt_dlp/__init__.py | 169 |
1 files changed, 63 insertions, 106 deletions
diff --git a/yt_dlp/__init__.py b/yt_dlp/__init__.py index f1a347514..0c68f8571 100644 --- a/yt_dlp/__init__.py +++ b/yt_dlp/__init__.py @@ -1,12 +1,8 @@ -try: - import contextvars # noqa: F401 -except Exception: - raise Exception( - f'You are using an unsupported version of Python. Only Python versions 3.7 and above are supported by yt-dlp') # noqa: F541 +#!/usr/bin/python +f'You are using an unsupported version of Python. Only Python versions 3.6 and above are supported by yt-dlp' # noqa: F541 -__license__ = 'Public Domain' +__license__ = 'CC0-1.0' -import collections import getpass import itertools import optparse @@ -16,14 +12,14 @@ import sys from .compat import compat_shlex_quote from .cookies import SUPPORTED_BROWSERS, SUPPORTED_KEYRINGS +from .downloader import FileDownloader from .downloader.external import get_external_downloader from .extractor import list_extractor_classes from .extractor.adobepass import MSO_INFO +from .extractor.common import InfoExtractor from .options import parseOpts from .postprocessor import ( FFmpegExtractAudioPP, - FFmpegMergerPP, - FFmpegPostProcessor, FFmpegSubtitlesConvertorPP, FFmpegThumbnailsConvertorPP, FFmpegVideoConvertorPP, @@ -38,7 +34,6 @@ from .utils import ( DateRange, DownloadCancelled, DownloadError, - FormatSorter, GeoUtils, PlaylistEntries, SameFileError, @@ -49,7 +44,6 @@ from .utils import ( format_field, int_or_none, match_filter_func, - parse_bytes, parse_duration, preferredencoding, read_batch_urls, @@ -63,8 +57,6 @@ from .utils import ( ) from .YoutubeDL import YoutubeDL -_IN_CLI = False - def _exit(status=0, *args): for msg in args: @@ -97,27 +89,28 @@ def print_extractor_information(opts, urls): out = '' if opts.list_extractors: - urls = dict.fromkeys(urls, False) - for ie in list_extractor_classes(opts.age_limit): - out += ie.IE_NAME + (' (CURRENTLY BROKEN)' if not ie.working() else '') + '\n' - if ie == GenericIE: - matched_urls = [url for url, matched in urls.items() if not matched] - else: - matched_urls = tuple(filter(ie.suitable, urls.keys())) - urls.update(dict.fromkeys(matched_urls, True)) - out += ''.join(f' {url}\n' for url in matched_urls) + for ie in list_extractors(opts.age_limit): + write_string(ie.IE_NAME + (' (CURRENTLY BROKEN)' if not ie.working() else '') + '\n', out=sys.stdout) + matchedUrls = [url for url in urls if ie.suitable(url)] + for mu in matchedUrls: + write_string(' ' + mu + '\n', out=sys.stdout) elif opts.list_extractor_descriptions: - _SEARCHES = ('cute kittens', 'slithering pythons', 'falling cat', 'angry poodle', 'purple fish', 'running tortoise', 'sleeping bunny', 'burping cow') - out = '\n'.join( - ie.description(markdown=False, search_examples=_SEARCHES) - for ie in list_extractor_classes(opts.age_limit) if ie.working() and ie.IE_DESC is not False) + for ie in list_extractors(opts.age_limit): + if not ie.working(): + continue + if ie.IE_DESC is False: + continue + desc = ie.IE_DESC or ie.IE_NAME + if getattr(ie, 'SEARCH_KEY', None) is not None: + _SEARCHES = ('cute kittens', 'slithering pythons', 'falling cat', 'angry poodle', 'purple fish', 'running tortoise', 'sleeping bunny', 'burping cow') + _COUNTS = ('', '5', '10', 'all') + desc += f'; "{ie.SEARCH_KEY}:" prefix (Example: "{ie.SEARCH_KEY}{random.choice(_COUNTS)}:{random.choice(_SEARCHES)}")' + write_string(desc + '\n', out=sys.stdout) elif opts.ap_list_mso: - out = 'Supported TV Providers:\n%s\n' % render_table( - ['mso', 'mso name'], - [[mso_id, mso_info['name']] for mso_id, mso_info in MSO_INFO.items()]) + table = [[mso_id, mso_info['name']] for mso_id, mso_info in MSO_INFO.items()] + write_string('Supported TV Providers:\n' + render_table(['mso', 'mso name'], table) + '\n', out=sys.stdout) else: return False - write_string(out, out=sys.stdout) return True @@ -152,7 +145,7 @@ def set_compat_opts(opts): else: opts.embed_infojson = False if 'format-sort' in opts.compat_opts: - opts.format_sort.extend(FormatSorter.ytdl_default) + opts.format_sort.extend(InfoExtractor.FormatSort.ytdl_default) _video_multistreams_set = set_default_compat('multistreams', 'allow_multiple_video_streams', False, remove_compat=False) _audio_multistreams_set = set_default_compat('multistreams', 'allow_multiple_audio_streams', False, remove_compat=False) if _video_multistreams_set is False and _audio_multistreams_set is False: @@ -227,11 +220,9 @@ def validate_options(opts): # Format sort for f in opts.format_sort: - validate_regex('format sorting', f, FormatSorter.regex) + validate_regex('format sorting', f, InfoExtractor.FormatSort.regex) # Postprocessor formats - validate_regex('merge output format', opts.merge_output_format, - r'({0})(/({0}))*'.format('|'.join(map(re.escape, FFmpegMergerPP.SUPPORTED_EXTS)))) validate_regex('audio format', opts.audioformat, FFmpegExtractAudioPP.FORMAT_RE) validate_in('subtitle format', opts.convertsubtitles, FFmpegSubtitlesConvertorPP.SUPPORTED_EXTS) validate_regex('thumbnail format', opts.convertthumbnails, FFmpegThumbnailsConvertorPP.FORMAT_RE) @@ -281,19 +272,19 @@ def validate_options(opts): raise ValueError(f'invalid {key} retry sleep expression {expr!r}') # Bytes - def validate_bytes(name, value): + def parse_bytes(name, value): if value is None: return None - numeric_limit = parse_bytes(value) + numeric_limit = FileDownloader.parse_bytes(value) validate(numeric_limit is not None, 'rate limit', value) return numeric_limit - opts.ratelimit = validate_bytes('rate limit', opts.ratelimit) - opts.throttledratelimit = validate_bytes('throttled rate limit', opts.throttledratelimit) - opts.min_filesize = validate_bytes('min filesize', opts.min_filesize) - opts.max_filesize = validate_bytes('max filesize', opts.max_filesize) - opts.buffersize = validate_bytes('buffer size', opts.buffersize) - opts.http_chunk_size = validate_bytes('http chunk size', opts.http_chunk_size) + opts.ratelimit = parse_bytes('rate limit', opts.ratelimit) + opts.throttledratelimit = parse_bytes('throttled rate limit', opts.throttledratelimit) + opts.min_filesize = parse_bytes('min filesize', opts.min_filesize) + opts.max_filesize = parse_bytes('max filesize', opts.max_filesize) + opts.buffersize = parse_bytes('buffer size', opts.buffersize) + opts.http_chunk_size = parse_bytes('http chunk size', opts.http_chunk_size) # Output templates def validate_outtmpl(tmpl, msg): @@ -326,15 +317,14 @@ def validate_options(opts): def parse_chapters(name, value): chapters, ranges = [], [] - parse_timestamp = lambda x: float('inf') if x in ('inf', 'infinite') else parse_duration(x) for regex in value or []: if regex.startswith('*'): - for range_ in map(str.strip, regex[1:].split(',')): - mobj = range_ != '-' and re.fullmatch(r'([^-]+)?\s*-\s*([^-]+)?', range_) - dur = mobj and (parse_timestamp(mobj.group(1) or '0'), parse_timestamp(mobj.group(2) or 'inf')) - if None in (dur or [None]): + for range in regex[1:].split(','): + dur = tuple(map(parse_duration, range.strip().split('-'))) + if len(dur) == 2 and all(t is not None for t in dur): + ranges.append(dur) + else: raise ValueError(f'invalid {name} time range "{regex}". Must be of the form *start-end') - ranges.append(dur) continue try: chapters.append(re.compile(regex)) @@ -347,16 +337,10 @@ def validate_options(opts): # Cookies from browser if opts.cookiesfrombrowser: - container = None - mobj = re.fullmatch(r'''(?x) - (?P<name>[^+:]+) - (?:\s*\+\s*(?P<keyring>[^:]+))? - (?:\s*:\s*(?P<profile>.+?))? - (?:\s*::\s*(?P<container>.+))? - ''', opts.cookiesfrombrowser) + mobj = re.match(r'(?P<name>[^+:]+)(\s*\+\s*(?P<keyring>[^:]+))?(\s*:(?P<profile>.+))?', opts.cookiesfrombrowser) if mobj is None: raise ValueError(f'invalid cookies from browser arguments: {opts.cookiesfrombrowser}') - browser_name, keyring, profile, container = mobj.group('name', 'keyring', 'profile', 'container') + browser_name, keyring, profile = mobj.group('name', 'keyring', 'profile') browser_name = browser_name.lower() if browser_name not in SUPPORTED_BROWSERS: raise ValueError(f'unsupported browser specified for cookies: "{browser_name}". ' @@ -366,7 +350,7 @@ def validate_options(opts): if keyring not in SUPPORTED_KEYRINGS: raise ValueError(f'unsupported keyring specified for cookies: "{keyring}". ' f'Supported keyrings are: {", ".join(sorted(SUPPORTED_KEYRINGS))}') - opts.cookiesfrombrowser = (browser_name, profile, keyring, container) + opts.cookiesfrombrowser = (browser_name, profile, keyring) # MetadataParser def metadataparser_actions(f): @@ -411,9 +395,6 @@ def validate_options(opts): if opts.download_archive is not None: opts.download_archive = expand_path(opts.download_archive) - if opts.ffmpeg_location is not None: - opts.ffmpeg_location = expand_path(opts.ffmpeg_location) - if opts.user_agent is not None: opts.headers.setdefault('User-Agent', opts.user_agent) if opts.referer is not None: @@ -478,24 +459,22 @@ def validate_options(opts): report_conflict('--playlist-random', 'playlist_random', '--lazy-playlist', 'lazy_playlist') report_conflict('--dateafter', 'dateafter', '--date', 'date', default=None) report_conflict('--datebefore', 'datebefore', '--date', 'date', default=None) - report_conflict('--exec-before-download', 'exec_before_dl_cmd', - '"--exec before_dl:"', 'exec_cmd', val2=opts.exec_cmd.get('before_dl')) + report_conflict('--exec-before-download', 'exec_before_dl_cmd', '"--exec before_dl:"', 'exec_cmd', opts.exec_cmd.get('before_dl')) report_conflict('--id', 'useid', '--output', 'outtmpl', val2=opts.outtmpl.get('default')) report_conflict('--remux-video', 'remuxvideo', '--recode-video', 'recodevideo') report_conflict('--sponskrub', 'sponskrub', '--remove-chapters', 'remove_chapters') report_conflict('--sponskrub', 'sponskrub', '--sponsorblock-mark', 'sponsorblock_mark') report_conflict('--sponskrub', 'sponskrub', '--sponsorblock-remove', 'sponsorblock_remove') - report_conflict('--sponskrub-cut', 'sponskrub_cut', '--split-chapter', 'split_chapters', - val1=opts.sponskrub and opts.sponskrub_cut) + report_conflict('--sponskrub-cut', 'sponskrub_cut', '--split-chapter', 'split_chapters', val1=opts.sponskrub and opts.sponskrub_cut) # Conflicts with --allow-unplayable-formats - report_conflict('--embed-metadata', 'addmetadata') + report_conflict('--add-metadata', 'addmetadata') report_conflict('--embed-chapters', 'addchapters') report_conflict('--embed-info-json', 'embed_infojson') report_conflict('--embed-subs', 'embedsubtitles') report_conflict('--embed-thumbnail', 'embedthumbnail') report_conflict('--extract-audio', 'extractaudio') - report_conflict('--fixup', 'fixup', val1=opts.fixup not in (None, 'never', 'ignore'), default='never') + report_conflict('--fixup', 'fixup', val1=(opts.fixup or '').lower() in ('', 'never', 'ignore'), default='never') report_conflict('--recode-video', 'recodevideo') report_conflict('--remove-chapters', 'remove_chapters', default=[]) report_conflict('--remux-video', 'remuxvideo') @@ -537,7 +516,7 @@ def validate_options(opts): # Do not unnecessarily download audio opts.format = 'bestaudio/best' - if opts.getcomments and opts.writeinfojson is None and not opts.embed_infojson: + if opts.getcomments and opts.writeinfojson is None: # If JSON is not printed anywhere, but comments are requested, save it to file if not opts.dumpjson or opts.print_json or opts.dump_single_json: opts.writeinfojson = True @@ -686,11 +665,8 @@ def get_postprocessors(opts): } -ParsedOptions = collections.namedtuple('ParsedOptions', ('parser', 'options', 'urls', 'ydl_opts')) - - def parse_options(argv=None): - """@returns ParsedOptions(parser, opts, urls, ydl_opts)""" + """ @returns (parser, opts, urls, ydl_opts) """ parser, opts, urls = parseOpts(argv) urls = get_urls(urls, opts.batchfile, opts.verbose) @@ -702,26 +678,11 @@ def parse_options(argv=None): postprocessors = list(get_postprocessors(opts)) - print_only = bool(opts.forceprint) and all(k not in opts.forceprint for k in POSTPROCESS_WHEN[2:]) - any_getting = any(getattr(opts, k) for k in ( - 'dumpjson', 'dump_single_json', 'getdescription', 'getduration', 'getfilename', - 'getformat', 'getid', 'getthumbnail', 'gettitle', 'geturl' - )) - - playlist_pps = [pp for pp in postprocessors if pp.get('when') == 'playlist'] - write_playlist_infojson = (opts.writeinfojson and not opts.clean_infojson - and opts.allow_playlist_files and opts.outtmpl.get('pl_infojson') != '') - if not any(( - opts.extract_flat, - opts.dump_single_json, - opts.forceprint.get('playlist'), - opts.print_to_file.get('playlist'), - write_playlist_infojson, - )): - if not playlist_pps: - opts.extract_flat = 'discard' - elif playlist_pps == [{'key': 'FFmpegConcat', 'only_multi_video': True, 'when': 'playlist'}]: - opts.extract_flat = 'discard_in_playlist' + any_getting = (any(opts.forceprint.values()) or opts.dumpjson or opts.dump_single_json + or opts.geturl or opts.gettitle or opts.getid or opts.getthumbnail + or opts.getdescription or opts.getfilename or opts.getformat or opts.getduration) + + any_printing = opts.print_json final_ext = ( opts.recodevideo if opts.recodevideo in FFmpegVideoConvertorPP.SUPPORTED_EXTS @@ -729,7 +690,7 @@ def parse_options(argv=None): else opts.audioformat if (opts.extractaudio and opts.audioformat in FFmpegExtractAudioPP.SUPPORTED_EXTS) else None) - return ParsedOptions(parser, opts, urls, { + return parser, opts, urls, { 'usenetrc': opts.usenetrc, 'netrc_location': opts.netrc_location, 'username': opts.username, @@ -739,10 +700,7 @@ def parse_options(argv=None): 'ap_mso': opts.ap_mso, 'ap_username': opts.ap_username, 'ap_password': opts.ap_password, - 'client_certificate': opts.client_certificate, - 'client_certificate_key': opts.client_certificate_key, - 'client_certificate_password': opts.client_certificate_password, - 'quiet': opts.quiet or any_getting or opts.print_json or bool(opts.forceprint), + 'quiet': (opts.quiet or any_getting or any_printing), 'no_warnings': opts.no_warnings, 'forceurl': opts.geturl, 'forcetitle': opts.gettitle, @@ -757,7 +715,7 @@ def parse_options(argv=None): 'forcejson': opts.dumpjson or opts.print_json, 'dump_single_json': opts.dump_single_json, 'force_write_download_archive': opts.force_write_download_archive, - 'simulate': (print_only or any_getting or None) if opts.simulate is None else opts.simulate, + 'simulate': (any_getting or None) if opts.simulate is None else opts.simulate, 'skip_download': opts.skip_download, 'format': opts.format, 'allow_unplayable_formats': opts.allow_unplayable_formats, @@ -778,7 +736,6 @@ def parse_options(argv=None): 'windowsfilenames': opts.windowsfilenames, 'ignoreerrors': opts.ignoreerrors, 'force_generic_extractor': opts.force_generic_extractor, - 'allowed_extractors': opts.allowed_extractors or ['default'], 'ratelimit': opts.ratelimit, 'throttledratelimit': opts.throttledratelimit, 'overwrites': opts.overwrites, @@ -903,10 +860,17 @@ def parse_options(argv=None): '_warnings': warnings, '_deprecation_warnings': deprecation_warnings, 'compat_opts': opts.compat_opts, - }) + } def _real_main(argv=None): + # Compatibility fixes for Windows + if sys.platform == 'win32': + # https://github.com/ytdl-org/youtube-dl/issues/820 + codecs.register(lambda name: codecs.lookup('utf-8') if name == 'cp65001' else None) + + workaround_optparse_bug9161() + setproctitle('yt-dlp') parser, opts, all_urls, ydl_opts = parse_options(argv) @@ -920,11 +884,6 @@ def _real_main(argv=None): if print_extractor_information(opts, all_urls): return - # We may need ffmpeg_location without having access to the YoutubeDL instance - # See https://github.com/yt-dlp/yt-dlp/issues/2191 - if opts.ffmpeg_location: - FFmpegPostProcessor._ffmpeg_location.set(opts.ffmpeg_location) - with YoutubeDL(ydl_opts) as ydl: pre_process = opts.update_self or opts.rm_cachedir actual_use = all_urls or opts.load_info_filename @@ -962,8 +921,6 @@ def _real_main(argv=None): def main(argv=None): - global _IN_CLI - _IN_CLI = True try: _exit(*variadic(_real_main(argv))) except DownloadError: |