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
- Open the same URL in a browser (incognito) and see what notice YouPorn shows — usually removal or region restriction; pick another source if so.
- Confirm the request really sends Cookie: age_verified=1; without it the age-verify interstitial can hide the watch container.
- 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.
- 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
- Always send Cookie: age_verified=1 (the extractor does; replicate it in custom clients).
- Treat the scraped reason after 'says:' as the site's own notice and prune dead links from crawl lists.
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
- lynda returned error: %s
- %s said: %s
- PornHub said: %s
- Unable to login: %s
- The video is not available, Facebook said: "%s"
AI-assisted analysis of ytdl-org/youtube-dl@956b8c5855 (2026-08-14).
Data as JSON: /api/errors/4e20d53dbc1c6391.
Report an issue: GitHub.