Repository: yt-dlp/yt-dlp
File: yt_dlp/extractor/common.py (Python)

Code before:
        if mobj:
            if group is None:
                # return the first matching group
                return next(g for g in mobj.groups() if g is not None)
            elif isinstance(group, (list, tuple)):
                return tuple(mobj.group(g) for g in group)
            else:
                return mobj.group(group)
        elif default is not NO_DEFAULT:
            return default
        elif fatal:
            raise RegexNotFoundError('Unable to extract %s' % _name)
        else:
            self.report_warning('unable to extract %s' % _name + bug_reports_message())
            return None

    def _search_json(self, start_pattern, string, name, video_id, *, end_pattern='',
                     contains_pattern='.+', fatal=True, default=NO_DEFAULT, **kwargs):
        """Searches string for the JSON object specified by start_pattern"""
        # NB: end_pattern is only used to reduce the size of the initial match
        return self._parse_json(
            self._search_regex(
                rf'{start_pattern}\s*(?P<json>{{{contains_pattern}}})\s*{end_pattern}',
                string, name, group='json', fatal=fatal,
                default='{}' if default is not NO_DEFAULT else NO_DEFAULT),
            video_id, fatal=fatal, ignore_extra=True, **kwargs) or {} if default is NO_DEFAULT else default

    def _html_search_regex(self, pattern, string, name, default=NO_DEFAULT, fatal=True, flags=0, group=None):
        """
        Like _search_regex, but strips HTML tags and unescapes entities.
        """
        res = self._search_regex(pattern, string, name, default, fatal, flags, group)
        if res:
            return clean_html(res).strip()
        else:
            return res

    def _get_netrc_login_info(self, netrc_machine=None):
        username = None
        password = None
        netrc_machine = netrc_machine or self._NETRC_MACHINE

        if self.get_param('usenetrc', False):
            try:
                netrc_file = compat_expanduser(self.get_param('netrc_location') or '~')
                if os.path.isdir(netrc_file):
                    netrc_file = os.path.join(netrc_file, '.netrc')
                info = netrc.netrc(file=netrc_file).authenticators(netrc_machine)
                if info is not None:
                    username = info[0]
                    password = info[2]

Review comment (bug):
I don't think this behaves correctly. Pls double check

What we want (when regex fails):
* `fatal=True` => throw regex error
* `fatal=False` => print regex warning, but not json warning and return `{}`
* `default=xxx` => print neither warning and return `xxx` (not `json.loads(xxx)`)

(when json.loads fails):
* `fatal=True` => throw json error
* `fatal=False` => print json warning, and return `{}`
* `default=xxx` => print neither warning and return `xxx` (not `json.loads(xxx)`)

Note that `fatal` should be ignored when `default` is given

(split into 2 statements if needed)

You can merge once you confirm this behavior

Code after:
        if mobj:
            if group is None:
                # return the first matching group
                return next(g for g in mobj.groups() if g is not None)
            elif isinstance(group, (list, tuple)):
                return tuple(mobj.group(g) for g in group)
            else:
                return mobj.group(group)
        elif default is not NO_DEFAULT:
            return default
        elif fatal:
            raise RegexNotFoundError('Unable to extract %s' % _name)
        else:
            self.report_warning('unable to extract %s' % _name + bug_reports_message())
            return None

    def _search_json(self, start_pattern, string, name, video_id, *, end_pattern='',
                     contains_pattern='(?s:.+)', fatal=True, default=NO_DEFAULT, **kwargs):
        """Searches string for the JSON object specified by start_pattern"""
        # NB: end_pattern is only used to reduce the size of the initial match
        if default is NO_DEFAULT:
            default, has_default = {}, False
        else:
            fatal, has_default = False, True

        json_string = self._search_regex(
            rf'{start_pattern}\s*(?P<json>{{\s*{contains_pattern}\s*}})\s*{end_pattern}',
            string, name, group='json', fatal=fatal, default=None if has_default else NO_DEFAULT)
        if not json_string:
            return default

        _name = self._downloader._format_err(name, self._downloader.Styles.EMPHASIS)
        try:
            return self._parse_json(json_string, video_id, ignore_extra=True, **kwargs)
        except ExtractorError as e:
            if fatal:
                raise ExtractorError(
                    f'Unable to extract {_name} - Failed to parse JSON', cause=e.cause, video_id=video_id)
            elif not has_default:
                self.report_warning(
                    f'Unable to extract {_name} - Failed to parse JSON: {e}', video_id=video_id)
        return default

    def _html_search_regex(self, pattern, string, name, default=NO_DEFAULT, fatal=True, flags=0, group=None):
        """
        Like _search_regex, but strips HTML tags and unescapes entities.
        """
        res = self._search_regex(pattern, string, name, default, fatal, flags, group)
        if res:
            return clean_html(res).strip()
        else: