ytdl-org/youtube-dl · error · ExtractorError
%s said: %s
Error message
%s said: %s
What it means
PornHdIE raises ExtractorError('%s said: %s' % (IE_NAME, message), expected=True) when the video page yields neither a 'sources' JSON object nor any parseable HTML5 <video> entries, and the page instead contains an element (div or p) with class 'no-video'. The message interpolated into the error is the text scraped from that no-video block, so the actual wording comes from the site (typically 'video has been removed' style notices).
Source
Thrown at youtube_dl/extractor/pornhd.py:74
title = self._html_search_regex(
[r'<span[^>]+class=["\']video-name["\'][^>]*>([^<]+)',
r'<title>(.+?) - .*?[Pp]ornHD.*?</title>'], webpage, 'title')
sources = self._parse_json(js_to_json(self._search_regex(
r"(?s)sources'?\s*[:=]\s*(\{.+?\})",
webpage, 'sources', default='{}')), video_id)
info = {}
if not sources:
entries = self._parse_html5_media_entries(url, webpage, video_id)
if entries:
info = entries[0]
if not sources and not info:
message = self._html_search_regex(
r'(?s)<(div|p)[^>]+class="no-video"[^>]*>(?P<value>.+?)</\1',
webpage, 'error message', group='value')
raise ExtractorError('%s said: %s' % (self.IE_NAME, message), expected=True)
formats = []
for format_id, video_url in sources.items():
video_url = urljoin(url, video_url)
if not video_url:
continue
height = int_or_none(self._search_regex(
r'^(\d+)[pP]', format_id, 'height', default=None))
formats.append({
'url': video_url,
'ext': determine_ext(video_url, 'mp4'),
'format_id': format_id,
'height': height,
})
if formats:
info['formats'] = formats
self._sort_formats(info['formats'])
View on GitHub (pinned to 956b8c5855)
Solutions
- Open the URL in a browser to read the actual no-video message (removed, private, region-locked) and act accordingly.
- If region-blocked, route through an egress point where the video is available.
- Remove the dead entry from your queue/playlist and retry the remainder of the batch.
- Update youtube-dl — if the site changed its markup, a newer extractor revision may parse sources correctly again.
Defensive patterns
Strategy: try-catch
Try / catch
try:
info = ydl.extract_info(url)
except ExtractorError as e:
if 'PornHD said:' in str(e):
mark_dead(url) # content removed; drop from queue Prevention
- Treat any '... said:' expected error as terminal for that URL — do not retry.
- Scrub playlists of removed videos periodically.
- Log the site message; it distinguishes removal from geo-blocking.
When it happens
Trigger: Extracting a pornhd.com video URL whose content was taken down or is region-blocked: sources JSON is empty/absent, _parse_html5_media_entries returns nothing, and the regex '(?s)<(div|p)[^>]+class="no-video"...' matches. If the no-video block itself is missing, _html_search_regex raises a different 'unable to extract error message' error instead.
Common situations: Dead/removed video links from old playlists; DMCA takedowns; geo-restrictions rendering the player empty; site template changes that rename the no-video class (then the inner regex fails first).
Related errors
- PornHub said: %s
- %s says: %s (IE_NAME, msg) or 'Video unavailable: no reason
- Unable to login: %s
- %s said: %s
- Unable to login: %s
AI-assisted analysis of ytdl-org/youtube-dl@956b8c5855 (2026-08-14).
Data as JSON: /api/errors/941e3f25ae4429d0.
Report an issue: GitHub.