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

Code before:
            'ext': 'mp4',
            'title': 'Clase inédita',
            'duration': 335.817,
            'thumbnail': r're:https://img2\.rtve\.es/v/.*\.png',
            'series': 'El coro de la cárcel',
        },
        'params': {
            'skip_download': True,
        },
    }

    def _real_extract(self, url):
        page_id = self._match_id(url)
        webpage = self._download_webpage(url, page_id)

        play_url = self._html_search_meta('contentUrl', webpage)
        if play_url is None:
            raise ExtractorError('The webpage doesn\'t contain any video', expected=True)

        return self.url_result(play_url, ie=RTVEALaCartaIE.ie_key())


class RTVEProgramIE(RTVEBaseIE):
    IE_NAME = 'rtve.es:program'
    IE_DESC = 'RTVE.es program'
    _VALID_URL = r'https?://(?:www\.)?rtve\.es/play/videos/(?P<program_slug>[a-z\d_-]+)/?$'

    def _real_extract(self, url):
        entries = []

        program_slug = self._match_valid_url(url).group('program_slug')
        program_page = self._download_webpage(url, program_slug, f'Downloading {program_slug} web page')

        playlist_title = self._html_extract_title(program_page)
        program_id = self._html_search_meta('DC.identifier', program_page)

        if program_id:
            total_page_count = 1
            current_page = 1
            while current_page <= total_page_count:
                params = {
                    'type': 39816,
                    'page': current_page,
                    'size': 60,
                }
                videos_data = self._download_json(
                    f'https://www.rtve.es/api/programas/{program_id}/videos?{urllib.parse.urlencode(params)}',
                    program_slug,
                )
                current_page += 1
                total_page_count = traverse_obj(videos_data, ('page', 'totalPages'), default=1)

Review comment (suggestion):
```suggestion
    _VALID_URL = r'https?://(?:www\.)?rtve\.es/play/videos/(?P<id>[\w-]+)/?(?:[?#]|$)'
```

Code after:
        'info_dict': {
            'id': '572515',
            'ext': 'mp4',
            'title': 'Clase inédita',
            'duration': 335.817,
            'thumbnail': r're:https://img2\.rtve\.es/v/.*\.png',
            'series': 'El coro de la cárcel',
        },
        'params': {
            'skip_download': True,
        },
    }

    def _real_extract(self, url):
        page_id = self._match_id(url)
        webpage = self._download_webpage(url, page_id)

        play_url = self._html_search_meta('contentUrl', webpage)
        if play_url is None:
            raise ExtractorError('The webpage doesn\'t contain any video', expected=True)

        return self.url_result(play_url, ie=RTVEALaCartaIE.ie_key())


class RTVEProgramIE(RTVEBaseIE):
    IE_NAME = 'rtve.es:program'
    IE_DESC = 'RTVE.es programs'
    _VALID_URL = r'https?://(?:www\.)?rtve\.es/play/videos/(?P<id>[\w-]+)/?(?:[?#]|$)'
    _TESTS = [{
        'url': 'https://www.rtve.es/play/videos/saber-vivir/',
        'info_dict': {
            'id': '111570',
            'title': 'Saber vivir - Programa de ciencia y futuro en RTVE Play',
        },
        'playlist_mincount': 400,
    }]
    _PAGE_SIZE = 60

    def _fetch_page(self, program_id, page_num):
        return self._download_json(
            f'https://www.rtve.es/api/programas/{program_id}/videos',
            program_id, note=f'Downloading page {page_num}',
            query={
                'type': 39816,
                'page': page_num,
                'size': 60,
            })

    def _entries(self, page_data):
        for video in traverse_obj(page_data, ('page', 'items', lambda _, v: url_or_none(v['htmlUrl']))):
            yield self.url_result(