ytdl-org/youtube-dl · warning · ExtractorError

The given url does not contain a video

Error message

The given url does not contain a video

What it means

Raised by NineGagIE when the post fetched from 9gag's v1/post API has type != 'Animated' — i.e. the URL points to a static image, article, or other non-video post. Only 'Animated' posts carry media streams. Marked expected=True, it cleanly rejects non-video permalinks.

Source

Thrown at youtube_dl/extractor/ninegag.py:47

            'like_count': int,
            'dislike_count': int,
            'comment_count': int,
        }
    }, {
        # HTML escaped title
        'url': 'https://9gag.com/gag/av5nvyb',
        'only_matching': True,
    }]

    def _real_extract(self, url):
        post_id = self._match_id(url)
        post = self._download_json(
            'https://9gag.com/v1/post', post_id, query={
                'id': post_id
            })['data']['post']

        if post.get('type') != 'Animated':
            raise ExtractorError(
                'The given url does not contain a video',
                expected=True)

        title = unescapeHTML(post['title'])

        duration = None
        formats = []
        thumbnails = []
        for key, image in (post.get('images') or {}).items():
            image_url = url_or_none(image.get('url'))
            if not image_url:
                continue
            ext = determine_ext(image_url)
            image_id = key.strip('image')
            common = {
                'url': image_url,
                'width': int_or_none(image.get('width')),
                'height': int_or_none(image.get('height')),

View on GitHub (pinned to 956b8c5855)

Solutions

  1. Filter by post type before downloading: query the same https://9gag.com/v1/post?id=<id> endpoint and keep only type == 'Animated'.
  2. For photo posts, fetch post['images']['image700']['url'] (or similar) directly — no video extractor needed.
  3. Catch this expected ExtractorError in batch scripts and skip non-video items.

Example fix

# before
urls.each { |u| system("youtube-dl #{u}") }

# after (pre-filter via the public JSON API)
import requests, sys
for u in urls:
    pid = u.rstrip('/').rsplit('/', 1)[-1]
    p = requests.get('https://9gag.com/v1/post', params={'id': pid}).json()['data']['post']
    if p.get('type') == 'Animated':
        sys.argv = ['youtube-dl', u]  # download video
# else: fetch image URL from p['images']
Defensive patterns

Strategy: validation

Validate before calling

import requests
p = requests.get('https://9gag.com/v1/post', params={'id': post_id}).json()['data']['post']
assert p.get('type') == 'Animated', f"post is {p.get('type')}, not a video"

Try / catch

except ExtractorError as e:
    if e.expected and 'does not contain a video' in str(e):
        image_url = fetch_image_fallback(post_id)  # photo posts are still fetchable
        if image_url:
            download(image_url)
    else:
        raise

Prevention

When it happens

Trigger: Calling the extractor on a 9gag.com/gag/<id> URL whose post['type'] is 'Photo', 'Article', etc. instead of 'Animated'.

Common situations: Feeding scraped 9gag permalinks indiscriminately (most are images); GIF/photo posts shared as if videos; automation that assumes every gag is downloadable video.

Related errors


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