ytdl-org/youtube-dl · error · ExtractorError

%s says: %s (IE_NAME, msg) or 'Video unavailable: no reason

Error message

%s says: %s (IE_NAME, msg) or 'Video unavailable: no reason found'

What it means

Raised by YouPornIE._real_extract when the downloaded watch page contains no element with id 'watch-container' — i.e. the video is not watchable. The code then scrapes the #mainContent text, splits on runs of 4 spaces, and reports the first chunk as the site's reason (e.g. 'Video has been removed'), or the generic 'Video unavailable: no reason found' if nothing is scraped. expected=True frames it as a site condition.

Source

Thrown at youtube_dl/extractor/youporn.py:130

                    yield m.group('url')

        return list(yield_urls())

    def _real_extract(self, url):
        # A different video ID (data-video-id) is hidden in the page but
        # never seems to be used
        video_id, display_id = self._match_valid_url(url).group('id', 'display_id')
        url = 'http://www.youporn.com/watch/%s' % (video_id,)
        webpage = self._download_webpage(
            url, video_id, headers={'Cookie': 'age_verified=1'})

        watchable = self._search_regex(
            r'''(<div\s[^>]*\bid\s*=\s*('|")?watch-container(?(2)\2|(?!-)\b)[^>]*>)''',
            webpage, 'watchability', default=None)
        if not watchable:
            msg = re.split(r'\s{4}', clean_html(get_element_by_id(
                'mainContent', webpage)) or '')[0]
            raise ExtractorError(
                ('%s says: %s' % (self.IE_NAME, msg))
                if msg else 'Video unavailable: no reason found',
                expected=True)
        # internal ID ?
        # video_id = extract_attributes(watchable).get('data-video-id')

        playervars = self._search_json(
            r'\bplayervars\s*:', webpage, 'playervars', video_id)

        def get_fmt(x):
            v_url = url_or_none(x.get('videoUrl'))
            if v_url:
                x['videoUrl'] = v_url
                return (x['format'], x)

        defs_by_format = dict(traverse_obj(playervars, (
            'mediaDefinitions', lambda _, v: v.get('format'), T(get_fmt))))

View on GitHub (pinned to 956b8c5855)

Solutions

  1. Open the same URL in a browser (incognito) and see what notice YouPorn shows — usually removal or region restriction; pick another source if so.
  2. Confirm the request really sends Cookie: age_verified=1; without it the age-verify interstitial can hide the watch container.
  3. If the video plays in a browser, dump the webpage and check whether the watch-container id was renamed; update the watchable regex or the get_element_by_id('mainContent') scraping accordingly.
  4. For crawls, treat this expected error as a skip signal for dead entries rather than a hard failure.
Defensive patterns

Strategy: try-catch

Validate before calling

def youporn_watchable(html: str) -> bool:
    return 'id="watch-container"' in html or "id='watch-container'" in html

Try / catch

try:
    info = ydl.extract_info(url)
except ExtractorError as e:
    if str(e).startswith('YouPorn says:') or 'Video unavailable' in str(e):
        mark_dead_and_skip(url)  # removal/geo/age-gate; read the embedded reason
    else:
        raise

Prevention

When it happens

Trigger: Extracting a youporn.com/watch/<id> URL (with Cookie age_verified=1) where the page lacks the watch-container div: removed videos, geo-blocked pages, or the generic 'video unavailable' template. The regex uses a backreference-conditional pattern so a renamed container id also fails to match.

Common situations: Dead/removed video links; region blocks serving a notice page instead of the player; age-gate behavior changing so the age_verified=1 cookie no longer unlocks the real page; template changes altering the container id or the 4-space-separated message layout in #mainContent.

Related errors


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