ytdl-org/youtube-dl · error · ExtractorError

Failed to find episode data

Error message

Failed to find episode data

What it means

Raised by CallinIE when the parsed Next.js __NEXT_DATA__ JSON has no dict at props.pageProps.episode (traverse_obj returns None). The extractor relies entirely on that embedded structure to get title, m3u8 URL, and show metadata, so without it extraction cannot continue.

Solutions

  1. Confirm the URL is a real episode page (it should play in a plain browser).
  2. Open the page source and search for __NEXT_DATA__; if 'episode' is absent, the data layout changed — switch to yt-dlp or a newer extractor.
  3. If the show link redirects, follow the redirect and use the final URL.
  4. Report the breakage / patch the traverse path to wherever the episode object now lives.

Example fix

# example: make extraction resilient to a moved key
episode = (traverse_obj(next_data, ('props', 'pageProps', 'episode'), expected_type=dict)
           or traverse_obj(next_data, ('props', 'pageProps', 'data', 'episode'), expected_type=dict))
Defensive patterns

Strategy: validation

Validate before calling

import json, re
def has_episode_data(html):
    m = re.search(r'<script[^>]+id="__NEXT_DATA__"[^>]*>([^<]+)</script>', html)
    if not m:
        return False
    data = json.loads(m.group(1))
    return isinstance(data.get('props', {}).get('pageProps', {}).get('episode'), dict)

Type guard

def is_episode_page(next_data):
    ep = next_data.get('props', {}).get('pageProps', {}).get('episode') if isinstance(next_data, dict) else None
    return isinstance(ep, dict) and bool(ep.get('m3u8'))

Try / catch

from youtube_dl.utils import ExtractorError
try:
    ydl.extract_info(url)
except ExtractorError as e:
    if 'Failed to find episode data' in str(e):
        mark_as_not_an_episode(url)  # do not retry
    else:
        raise

Prevention

When it happens

Trigger: Downloading a callin.com URL that is not an episode page (a show/landing/login page), a page rendered differently (SSR structure changed or data moved), or an episode that was removed so pageProps no longer contains 'episode'.

Common situations: Site redesign moving the episode payload to a different key or to client-side fetching; passing a stub or preview URL; episode unlisted/deleted.

Related errors


AI-assisted analysis of ytdl-org/youtube-dl@956b8c5855 (2026-08-14). Data as JSON: /api/errors/0144d910de351118. Report an issue: GitHub.

Appendix: source

Thrown at youtube_dl/extractor/callin.py:53

            'channel_url': 'https://callin.com/show/the-debrief-with-briahna-joy-gray-siiFDzGegm',
        }
    }]

    def _search_nextjs_data(self, webpage, video_id, transform_source=None, fatal=True, **kw):
        return self._parse_json(
            self._search_regex(
                r'(?s)<script[^>]+id=[\'"]__NEXT_DATA__[\'"][^>]*>([^<]+)</script>',
                webpage, 'next.js data', fatal=fatal, **kw),
            video_id, transform_source=transform_source, fatal=fatal)

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

        next_data = self._search_nextjs_data(webpage, video_id)
        episode = traverse_obj(next_data, ('props', 'pageProps', 'episode'), expected_type=dict)
        if not episode:
            raise ExtractorError('Failed to find episode data')

        title = episode.get('title') or self._og_search_title(webpage)
        description = episode.get('description') or self._og_search_description(webpage)

        formats = []
        formats.extend(self._extract_m3u8_formats(
            episode.get('m3u8'), video_id, 'mp4',
            entry_protocol='m3u8_native', fatal=False))
        self._sort_formats(formats)

        channel = try_get(episode, lambda x: x['show']['title'], compat_str)
        channel_url = try_get(episode, lambda x: x['show']['linkObj']['resourceUrl'], compat_str)

        return {
            'id': video_id,
            'title': title,
            'description': description,
            'formats': formats,

View on GitHub (pinned to 956b8c5855)