{"record":{"id":"c46775a33507d716","repo":"yt-dlp/yt-dlp","slug":"no-chapters-found-for-course-course-id","errorCode":null,"errorMessage":"No chapters found for course {course_id}","messagePattern":"No chapters found for course (.+?)","errorType":"exception","errorClass":"ExtractorError","httpStatus":null,"severity":"error","filePath":"yt_dlp/extractor/safari.py","lineNumber":246,"sourceCode":"    }, {\n        'url': 'https://www.oreilly.com/library/view/hadoop-fundamentals-livelessons/9780133392838/',\n        'only_matching': True,\n    }]\n\n    @classmethod\n    def suitable(cls, url):\n        return (False if SafariIE.suitable(url) or SafariApiIE.suitable(url)\n                else super().suitable(url))\n\n    def _real_extract(self, url):\n        course_id = self._match_id(url)\n\n        course_json = self._download_json(\n            f'{self._API_BASE}/book/{course_id}/?override_format={self._API_FORMAT}',\n            course_id, 'Downloading course JSON')\n\n        if 'chapters' not in course_json:\n            raise ExtractorError(\n                f'No chapters found for course {course_id}', expected=True)\n\n        entries = [\n            self.url_result(chapter, SafariApiIE.ie_key())\n            for chapter in course_json['chapters']]\n\n        course_title = course_json['title']\n\n        return self.playlist_result(entries, course_id, course_title)\n","sourceCodeStart":228,"sourceCodeEnd":256,"githubUrl":"https://github.com/yt-dlp/yt-dlp/blob/81ecd58b1394793e6da9998cc19fdb45657f1685/yt_dlp/extractor/safari.py#L228-L256","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Confirm the URL is a course page (shows a chapter list on learning.oreilly.com), not a single lesson link","Fetch https://learning.oreilly.com/api/v1/book/{course_id}/ with the same session cookies and check whether 'chapters' exists","If the id is a chapter, extract the parent course URL instead and rerun","Update yt-dlp (yt-dlp -U) in case the extractor/API contract changed, and file an upstream issue if the JSON genuinely lacks chapters"],"exampleFix":"// before\nif 'chapters' not in course_json:\n    raise ExtractorError(f'No chapters found for course {course_id}', expected=True)\n\n// after: keep the guard but say why the chapters list is missing\nif not course_json.get('chapters'):\n    raise ExtractorError(\n        f'No chapters found for course {course_id} (keys: {\", \".join(course_json)}). '\n        'Verify the id is a course and the account has access.', expected=True)","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"def course_json_has_chapters(course_json) -> bool:\n    return (\n        isinstance(course_json, dict)\n        and isinstance(course_json.get('chapters'), list)\n        and len(course_json['chapters']) > 0\n    )","tryCatchPattern":"from yt_dlp import YoutubeDL\nfrom yt_dlp.utils import ExtractorError\n\nwith YoutubeDL({'quiet': True}) as ydl:\n    try:\n        playlist = ydl.extract_info(url, download=False)\n    except ExtractorError as e:\n        if e.expected and 'No chapters found for course' in str(e):\n            log.warning('skipping %s: id is not exposing chapters (not a course / no entitlement)', url)\n        else:\n            raise","preventionTips":["Queue course landing-page URLs, not links to individual chapters","Validate course ids against the learning.oreilly.com API before batch processing","Keep yt-dlp updated so API-contract fixes are picked up"],"tags":["api-contract","json","playlist","oreilly-safari"],"backgroundTag":"api-response-schema-mismatch","analyzedSha":"81ecd58b1394793e6da9998cc19fdb45657f1685","analyzedAt":"2026-08-22T12:21:25.439Z","schemaVersion":2},"datasetVersion":"2026-08-22T14:17:55.899Z"}