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

Code before:
    traverse_obj,
    urlencode_postdata,
)


class FreeTvBaseIE(InfoExtractor):
    def _get_api_response(self, content_id, postdata, resource_type):
        response = self._download_json(
            'https://www.freetv.com/wordpress/wp-admin/admin-ajax.php',
            content_id, data=urlencode_postdata(postdata), fatal=True,
            note=f'Downloading {content_id} {resource_type} JSON')

        if response.get('data') is False:
            raise ExtractorError(f"Response doesn't contain {resource_type} data")

        return response

    def _extract_video(self, content_id, action='olyott_video_play'):
        api_response = self._get_api_response(
            content_id, resource_type='video',
            postdata={
                'action': action,
                'contentID': content_id,
            })

        video_id = traverse_obj(api_response, ('data', 'displayMeta', 'contentID'), expected_type=str)
        video_url = traverse_obj(api_response, ('data', 'displayMeta', 'streamURLVideo'), expected_type=str)

        if determine_ext(video_url) != 'm3u8':
            raise ExtractorError('Manifest was not found', video_id=video_id)

        formats, subtitles = self._extract_m3u8_formats_and_subtitles(video_url, video_id, 'mp4')
        self._sort_formats(formats)

        return {
            'id': video_id,
            'title': traverse_obj(api_response, ('data', 'displayMeta', 'title')),
            'description': traverse_obj(api_response, ('data', 'displayMeta', 'desc')),
            'formats': formats,
            'subtitles': subtitles,
        }

    def _extract_series(self, display_id, season_ids, series_title, series_description,
                        action='olyott_get_dynamic_series_content'):
        entries = []
        for season_id in season_ids:
            api_response = self._get_api_response(
                season_id, resource_type='series',
                postdata={
                    'action': action,
                    'contentID': season_id,

Review comment (suggestion):
This should be fatal
```suggestion
        video_id = api_response['data']['displayMeta']['contentID']
```

Code after:
)


class FreeTvBaseIE(InfoExtractor):
    def _get_api_response(self, content_id, resource_type, postdata):
        return self._download_json(
            'https://www.freetv.com/wordpress/wp-admin/admin-ajax.php',
            content_id, data=urlencode_postdata(postdata),
            note=f'Downloading {content_id} {resource_type} JSON')['data']


class FreeTvMoviesIE(FreeTvBaseIE):
    _VALID_URL = r'https?://(?:www\.)?freetv\.com/peliculas/(?P<id>[^/]+)'
    _TESTS = [{
        'url': 'https://www.freetv.com/peliculas/atrapame-si-puedes/',
        'md5': 'dc62d5abf0514726640077cd1591aa92',
        'info_dict': {
            'id': '428021',
            'title': 'Atrápame Si Puedes',
            'description': 'md5:ca63bc00898aeb2f64ec87c6d3a5b982',
            'ext': 'mp4',
        }
    }, {
        'url': 'https://www.freetv.com/peliculas/monstruoso/',
        'md5': '509c15c68de41cb708d1f92d071f20aa',
        'info_dict': {
            'id': '377652',
            'title': 'Monstruoso',
            'description': 'md5:333fc19ee327b457b980e54a911ea4a3',
            'ext': 'mp4',
        }
    }]

    def _extract_video(self, content_id, action='olyott_video_play'):
        api_response = self._get_api_response(content_id, 'video', {
            'action': action,
            'contentID': content_id,
        })

        video_id, video_url = api_response['displayMeta']['contentID'], api_response['displayMeta']['streamURLVideo']
        formats, subtitles = self._extract_m3u8_formats_and_subtitles(video_url, video_id, 'mp4')
        self._sort_formats(formats)

        return {
            'id': video_id,
            'title': traverse_obj(api_response, ('displayMeta', 'title')),
            'description': traverse_obj(api_response, ('displayMeta', 'desc')),
            'formats': formats,
            'subtitles': subtitles,
        }