yt-dlp/yt-dlp · error · ExtractorError

No chapters found for course {course_id}

Error message

No chapters found for course {course_id}

What it means

SafariCourseIE turns an O'Reilly Learning (Safari) course into a playlist by downloading {self._API_BASE}/book/{course_id}/?override_format=... (API base https://learning.oreilly.com/api/v1) and requires a 'chapters' key in that JSON. When the API responds but the key is absent, it raises this expected ExtractorError rather than returning an empty playlist. In practice the id is usually not a course (single chapter/lesson ids, removed or non-entitled courses) or the API response shape changed.

Source

Thrown at yt_dlp/extractor/safari.py:246

    }, {
        'url': 'https://www.oreilly.com/library/view/hadoop-fundamentals-livelessons/9780133392838/',
        'only_matching': True,
    }]

    @classmethod
    def suitable(cls, url):
        return (False if SafariIE.suitable(url) or SafariApiIE.suitable(url)
                else super().suitable(url))

    def _real_extract(self, url):
        course_id = self._match_id(url)

        course_json = self._download_json(
            f'{self._API_BASE}/book/{course_id}/?override_format={self._API_FORMAT}',
            course_id, 'Downloading course JSON')

        if 'chapters' not in course_json:
            raise ExtractorError(
                f'No chapters found for course {course_id}', expected=True)

        entries = [
            self.url_result(chapter, SafariApiIE.ie_key())
            for chapter in course_json['chapters']]

        course_title = course_json['title']

        return self.playlist_result(entries, course_id, course_title)

View on GitHub (pinned to 81ecd58b13)

Solutions

  1. Confirm the URL is a course page (shows a chapter list on learning.oreilly.com), not a single lesson link
  2. Fetch https://learning.oreilly.com/api/v1/book/{course_id}/ with the same session cookies and check whether 'chapters' exists
  3. If the id is a chapter, extract the parent course URL instead and rerun
  4. Update yt-dlp (yt-dlp -U) in case the extractor/API contract changed, and file an upstream issue if the JSON genuinely lacks chapters

Example fix

// before
if 'chapters' not in course_json:
    raise ExtractorError(f'No chapters found for course {course_id}', expected=True)

// after: keep the guard but say why the chapters list is missing
if not course_json.get('chapters'):
    raise ExtractorError(
        f'No chapters found for course {course_id} (keys: {", ".join(course_json)}). '
        'Verify the id is a course and the account has access.', expected=True)
Defensive patterns

Strategy: try-catch

Type guard

def course_json_has_chapters(course_json) -> bool:
    return (
        isinstance(course_json, dict)
        and isinstance(course_json.get('chapters'), list)
        and len(course_json['chapters']) > 0
    )

Try / catch

from yt_dlp import YoutubeDL
from yt_dlp.utils import ExtractorError

with YoutubeDL({'quiet': True}) as ydl:
    try:
        playlist = ydl.extract_info(url, download=False)
    except ExtractorError as e:
        if e.expected and 'No chapters found for course' in str(e):
            log.warning('skipping %s: id is not exposing chapters (not a course / no entitlement)', url)
        else:
            raise

Prevention

When it happens

Trigger: Calling the extractor on a Safari course URL whose /book/{course_id}/ API payload has no 'chapters' key: chapter or lesson ids used as course ids, courses the logged-in account cannot see, or API schema drift after a yt-dlp release.

Common situations: Deep-linking a single chapter and treating it as a course; expired subscription cookies producing limited payloads; typo'd course id; yt-dlp version predating an API change on learning.oreilly.com.

Related errors


AI-assisted analysis of yt-dlp/yt-dlp@81ecd58b13 (2026-08-22). Data as JSON: /api/errors/c46775a33507d716. Report an issue: GitHub.