diff --git a/yt_dlp/YoutubeDL.py b/yt_dlp/YoutubeDL.py index c2b306d70..872e0bdc3 100644 --- a/yt_dlp/YoutubeDL.py +++ b/yt_dlp/YoutubeDL.py @@ -115,6 +115,7 @@ from .utils import ( network_exceptions, number_of_digits, orderedSet, + orderedSet_from_options, parse_filesize, preferredencoding, prepend_extension, @@ -2737,27 +2738,11 @@ class YoutubeDL: if self.params.get('allsubtitles', False): requested_langs = all_sub_langs elif self.params.get('subtitleslangs', False): - # A list is used so that the order of languages will be the same as - # given in subtitleslangs. See https://github.com/yt-dlp/yt-dlp/issues/1041 - requested_langs = [] - for lang_re in self.params.get('subtitleslangs'): - discard = lang_re[0] == '-' - if discard: - lang_re = lang_re[1:] - if lang_re == 'all': - if discard: - requested_langs = [] - else: - requested_langs.extend(all_sub_langs) - continue - current_langs = filter(re.compile(lang_re + '$').match, all_sub_langs) - if discard: - for lang in current_langs: - while lang in requested_langs: - requested_langs.remove(lang) - else: - requested_langs.extend(current_langs) - requested_langs = orderedSet(requested_langs) + try: + requested_langs = orderedSet_from_options( + self.params.get('subtitleslangs'), {'all': all_sub_langs}, use_regex=True) + except re.error as e: + raise ValueError(f'Wrong regex for subtitlelangs: {e.pattern}') elif normal_sub_langs: requested_langs = ['en'] if 'en' in normal_sub_langs else normal_sub_langs[:1] else: diff --git a/yt_dlp/options.py b/yt_dlp/options.py index 6373ff8c0..0cddb7fd5 100644 --- a/yt_dlp/options.py +++ b/yt_dlp/options.py @@ -29,6 +29,7 @@ from .utils import ( format_field, get_executable_path, join_nonempty, + orderedSet_from_options, remove_end, write_string, ) @@ -232,30 +233,16 @@ def create_parser(): current + value if append is True else value + current) def _set_from_options_callback( - option, opt_str, value, parser, delim=',', allowed_values=None, aliases={}, + option, opt_str, value, parser, allowed_values, delim=',', aliases={}, process=lambda x: x.lower().strip()): - current = set(getattr(parser.values, option.dest)) - values = [process(value)] if delim is None else list(map(process, value.split(delim)[::-1])) - while values: - actual_val = val = values.pop() - if not val: - raise optparse.OptionValueError(f'Invalid {option.metavar} for {opt_str}: {value}') - if val == 'all': - current.update(allowed_values) - elif val == '-all': - current = set() - elif val in aliases: - values.extend(aliases[val]) - else: - if val[0] == '-': - val = val[1:] - current.discard(val) - else: - current.update([val]) - if allowed_values is not None and val not in allowed_values: - raise optparse.OptionValueError(f'wrong {option.metavar} for {opt_str}: {actual_val}') + values = [process(value)] if delim is None else map(process, value.split(delim)) + try: + requested = orderedSet_from_options(values, collections.ChainMap(aliases, {'all': allowed_values}), + start=getattr(parser.values, option.dest)) + except ValueError as e: + raise optparse.OptionValueError(f'wrong {option.metavar} for {opt_str}: {e.args[0]}') - setattr(parser.values, option.dest, current) + setattr(parser.values, option.dest, set(requested)) def _dict_from_options_callback( option, opt_str, value, parser, @@ -447,8 +434,8 @@ def create_parser(): 'no-youtube-channel-redirect', 'no-youtube-unavailable-videos', 'no-attach-info-json', 'embed-metadata', 'embed-thumbnail-atomicparsley', 'seperate-video-versions', 'no-clean-infojson', 'no-keep-subs', 'no-certifi', }, 'aliases': { - 'youtube-dl': ['-multistreams', 'all'], - 'youtube-dlc': ['-no-youtube-channel-redirect', '-no-live-chat', 'all'], + 'youtube-dl': ['all', '-multistreams'], + 'youtube-dlc': ['all', '-no-youtube-channel-redirect', '-no-live-chat'], } }, help=( 'Options that can help keep compatibility with youtube-dl or youtube-dlc ' diff --git a/yt_dlp/utils.py b/yt_dlp/utils.py index 13768d846..957c7eaa7 100644 --- a/yt_dlp/utils.py +++ b/yt_dlp/utils.py @@ -5785,6 +5785,36 @@ def truncate_string(s, left, right=0): return f'{s[:left-3]}...{s[-right:]}' +def orderedSet_from_options(options, alias_dict, *, use_regex=False, start=None): + assert 'all' in alias_dict, '"all" alias is required' + requested = list(start or []) + for val in options: + discard = val.startswith('-') + if discard: + val = val[1:] + + if val in alias_dict: + val = alias_dict[val] if not discard else [ + i[1:] if i.startswith('-') else f'-{i}' for i in alias_dict[val]] + # NB: Do not allow regex in aliases for performance + requested = orderedSet_from_options(val, alias_dict, start=requested) + continue + + current = (filter(re.compile(val, re.I).fullmatch, alias_dict['all']) if use_regex + else [val] if val in alias_dict['all'] else None) + if current is None: + raise ValueError(val) + + if discard: + for item in current: + while item in requested: + requested.remove(item) + else: + requested.extend(current) + + return orderedSet(requested) + + # Deprecated has_certifi = bool(certifi) has_websockets = bool(websockets)