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

Code before:
            r'\w+\s*:\s*', s, 'next js data', None, contains_pattern=r'\[(?s:.+)\]', default=None)

    def _old_extract_common_data(self, webpage, display_id, video_type):
        self.write_debug('Falling back to the old method for extracting common data')

        page_data = self._parse_json(self._search_regex(
            r'window\.__data\s*=\s*({.*?});', webpage,
            'page data', default='{}'), display_id, fatal=False)
        if not page_data:
            page_data = self._parse_json(self._parse_json(self._search_regex(
                r'window\.__data\s*=\s*JSON\.parse\s*\(\s*(".+?")\s*\)\s*;',
                webpage, 'page data'), display_id), display_id)

        for kind in ('episode', 'clip'):
            current_key = page_data.get(kind, {}).get(
                f'current{kind.capitalize()}Key')
            if not current_key:
                continue
            cache = page_data.get(kind, {}).get(f'{kind}Cache', {})
            if not cache:
                continue
            return {
                video_type: (cache.get(current_key) or next(iter(cache.values())))[kind],
                'season': (cache.get(current_key) or next(iter(cache.values()))).get('season', None),
            }

    def _real_extract(self, url):
        display_id, video_type = self._match_valid_url(url).group('id', 'type')
        webpage = self._download_webpage(url, display_id)

        common_data = traverse_obj(
            re.findall(r'<script[^>]*>\s*self\.__next_f\.push\(\s*(\[.+?\])\s*\);?\s*</script>', webpage),
            (..., {json.loads}, ..., {self._find_json},
             lambda _, v: v['payload'][video_type]['slug'] == display_id,
             'payload', any)) or self._old_extract_common_data(webpage, display_id, video_type)
        if not common_data:
            raise ExtractorError('Unable to extract video data')

        if traverse_obj(common_data, (video_type, 'video', 'drm', {bool})):
            self.report_drm(display_id)
        brightcove_id = try_get(
            common_data, lambda x: x[video_type]['video']['brightcoveId'], str) or 'ref:{}'.format(common_data[video_type]['video']['referenceId'])
        video_id = str_or_none(try_get(common_data, lambda x: x['episode']['video']['id'])) or brightcove_id

        title = try_get(common_data, lambda x: x[video_type]['name'], str)
        season_number = try_get(common_data, lambda x: x['season']['seasonNumber'], int)
        episode_number = try_get(common_data, lambda x: x['episode']['episodeNumber'], int)
        timestamp = unified_timestamp(try_get(common_data, lambda x: x['episode']['airDate'], str))
        release_date = unified_strdate(try_get(common_data, lambda x: x[video_type]['availability'], str))
        thumbnails_data = try_get(common_data, lambda x: x['episode']['image']['sizes'], dict) or {}
        thumbnails = [{

Review comment (refactor):
If this is going to be the standard way of parsing next_f script calls, this should be refactored with goplay and francetv combining traverse_obj into .extractor.common before racking up more technical debt. 

Code after:
        'info_dict': {
            'id': '6367798770112',
            'ext': 'mp4',
            'title': 'Episode 1',
            'description': r're:The cultural sensation of Married At First Sight returns with our first weddings! .{90}$',
            'uploader_id': '4460760524001',
            'duration': 5415.079,
            'thumbnail': r're:https?://.+/1920x0/.+\.png',
            'tags': ['episode'],
            'season': 'Season 12',
            'season_number': 12,
            'episode': 'Episode 1',
            'episode_number': 1,
            'series': 'Married at First Sight',
            'timestamp': 1737973800,
            'upload_date': '20250127',
            'release_timestamp': 1737973800,
            'release_date': '20250127',
        },
        'params': {
            'skip_download': 'HLS/DASH fragments and mp4 URLs are geo-restricted; only available in AU',
        },
    }]
    BRIGHTCOVE_URL_TEMPLATE = 'http://players.brightcove.net/4460760524001/default_default/index.html?videoId={}'

    # XXX: For parsing next.js v15+ data; see also yt_dlp.extractor.francetv and yt_dlp.extractor.goplay
    def _find_json(self, s):
        return self._search_json(
            r'\w+\s*:\s*', s, 'next js data', None, contains_pattern=r'\[(?s:.+)\]', default=None)

    def _real_extract(self, url):
        display_id, video_type = self._match_valid_url(url).group('id', 'type')
        webpage = self._download_webpage(url, display_id)

        common_data = traverse_obj(
            re.findall(r'<script[^>]*>\s*self\.__next_f\.push\(\s*(\[.+?\])\s*\);?\s*</script>', webpage),
            (..., {json.loads}, ..., {self._find_json},
             lambda _, v: v['payload'][video_type]['slug'] == display_id,
             'payload', any, {require('video data')}))

        if traverse_obj(common_data, (video_type, 'video', 'drm', {bool})):
            self.report_drm(display_id)
        brightcove_id = traverse_obj(common_data, (
            video_type, 'video', (
                ('brightcoveId', {str}),
                ('referenceId', {str}, {lambda x: f'ref:{x}' if x else None}),
            ), any, {require('brightcove ID')}))

        return {
            '_type': 'url_transparent',
            'ie_key': BrightcoveNewIE.ie_key(),