ytdl-org/youtube-dl · error · ExtractorError

%s said: %s

Error message

%s said: %s

What it means

Raised by the mojvideo.com extractor when the playerapi.php XML response contains '<error>true</error>'. The extractor then scrapes <errordesc> for the human-readable reason and re-raises it prefixed with the extractor name. Expected=True — a faithful relay of the site's own error channel.

Source

Thrown at youtube_dl/extractor/mojvideo.py:40

            'title': 'V avtu pred mano rdečelaska - Alfi Nipič',
            'thumbnail': r're:^http://.*\.jpg$',
            'duration': 242,
        }
    }

    def _real_extract(self, url):
        mobj = re.match(self._VALID_URL, url)
        video_id = mobj.group('id')
        display_id = mobj.group('display_id')

        # XML is malformed
        playerapi = self._download_webpage(
            'http://www.mojvideo.com/playerapi.php?v=%s&t=1' % video_id, display_id)

        if '<error>true</error>' in playerapi:
            error_desc = self._html_search_regex(
                r'<errordesc>([^<]*)</errordesc>', playerapi, 'error description', fatal=False)
            raise ExtractorError('%s said: %s' % (self.IE_NAME, error_desc), expected=True)

        title = self._html_search_regex(
            r'<title>([^<]+)</title>', playerapi, 'title')
        video_url = self._html_search_regex(
            r'<file>([^<]+)</file>', playerapi, 'video URL')
        thumbnail = self._html_search_regex(
            r'<preview>([^<]+)</preview>', playerapi, 'thumbnail', fatal=False)
        duration = parse_duration(self._html_search_regex(
            r'<duration>([^<]+)</duration>', playerapi, 'duration', fatal=False))

        return {
            'id': video_id,
            'display_id': display_id,
            'url': video_url,
            'title': title,
            'thumbnail': thumbnail,
            'duration': duration,
        }

View on GitHub (pinned to 956b8c5855)

Solutions

  1. Read the errordesc text — it is mojvideo's stated reason.
  2. Verify the video at www.mojvideo.com in a browser.
  3. If the whole service is offline/erroring, source the media elsewhere; no client fix recovers it.
Defensive patterns

Strategy: try-catch

Type guard

def mojvideo_api_error(api_xml):
    return isinstance(api_xml, str) and '<error>true</error>' in api_xml

Try / catch

try:
    info = ydl.extract_info(url, download=False)
except ExtractorError as e:
    if 'mojvideo said:' in str(e):
        mark_permanently_dead(url)  # site's API declared the video gone
    else:
        raise

Prevention

When it happens

Trigger: GET http://www.mojvideo.com/playerapi.php?v={id}&t=1 returns malformed XML containing <error>true</error>; <errordesc> is searched with fatal=False, so a missing errordesc yields None and the message reads 'mojvideo said: None'.

Common situations: Video deleted from mojvideo; private/pending-approval videos; the Slovenian service (largely defunct) returning errors for all ids; API changes renaming errordesc.

Related errors


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