Repository: yt-dlp/yt-dlp
File: yt_dlp/extractor/cybrary.py (Python)

Code before:
        'md5': '62f26547dccc59c44363e2a13d4ad08d',
        'info_dict': {
            'id': '445638073',
            'ext': 'mp4',
            'title': 'Azure Virtual Network IP Addressing',
            'thumbnail': 'https://i.vimeocdn.com/video/936667051-1647ace66c627d4a2382185e0dae8deb830309bfddd53f8b2367b2f91e92ed0e-d_1280',
            'series_id': '52733',
            'uploader_url': 'https://vimeo.com/user30867300',
            'duration': 426,
            'uploader_id': 'user30867300',
            'series': 'AZ-500: Microsoft Azure Security Technologies',
            'uploader': 'Cybrary',
            'chapter': 'Implement Network Security',
            'chapter_id': '52693'
        },
        'expected_warnings': ['No authenticators for vimeo']
    }]

    def _real_extract(self, url):
        activity_id, enrollment_id = self._match_valid_url(url).group('id', 'enrollment')
        course = self._call_api('enrollment', enrollment_id)['content']
        activity = next((activity for activity in (traverse_obj(course, ('learning_modules', ..., 'activities', ...)) or [])
                        if int(activity_id) == activity.get('id')), None)

        if activity.get('type') not in ['Video Activity', 'Lesson Activity']:
            raise ExtractorError('The activity is not a video')

        module = next((m for m in traverse_obj(course, ('learning_modules')) or []
                      if int(activity_id) in traverse_obj(m, ('activities', ..., 'id') or [])), None)

        vimeo_id = self._get_vimeo_id(activity_id)

        return {
            '_type': 'url_transparent',
            'series': traverse_obj(course, ('content_description', 'title')),
            'series_id': str_or_none(traverse_obj(course, ('content_description', 'id'))),
            'id': vimeo_id,
            'chapter': module.get('title'),
            'chapter_id': str_or_none(module.get('id')),
            'title': activity.get('title'),
            'url': smuggle_url(f'https://player.vimeo.com/video/{vimeo_id}', {'http_headers': {'Referer': 'https://api.cybrary.it'}})
        }


class CybraryCourseIE(CybraryBaseIE):
    _VALID_URL = r'https://app.cybrary.it/browse/course/(?P<id>[\w-]+)/?(?:$|[#?])'
    _TESTS = [{
        'url': 'https://app.cybrary.it/browse/course/az-500-microsoft-azure-security-technologies',
        'info_dict': {
            'id': 898,
            'title': 'AZ-500: Microsoft Azure Security Technologies',

Review comment (refactor):
```suggestion
            raise ExtractorError('The activity is not a video', expected=True)
```

Code after:
            'id': '445638073',
            'ext': 'mp4',
            'title': 'Azure Virtual Network IP Addressing',
            'thumbnail': 'https://i.vimeocdn.com/video/936667051-1647ace66c627d4a2382185e0dae8deb830309bfddd53f8b2367b2f91e92ed0e-d_1280',
            'series_id': '52733',
            'uploader_url': 'https://vimeo.com/user30867300',
            'duration': 426,
            'uploader_id': 'user30867300',
            'series': 'AZ-500: Microsoft Azure Security Technologies',
            'uploader': 'Cybrary',
            'chapter': 'Implement Network Security',
            'chapter_id': '52693'
        },
        'expected_warnings': ['No authenticators for vimeo']
    }]

    def _real_extract(self, url):
        activity_id, enrollment_id = self._match_valid_url(url).group('id', 'enrollment')
        course = self._call_api('enrollment', enrollment_id)['content']
        activity = traverse_obj(course, ('learning_modules', ..., 'activities', lambda _, v: int(activity_id) == v['id']), get_all=False)

        if activity.get('type') not in ['Video Activity', 'Lesson Activity']:
            raise ExtractorError('The activity is not a video', expected=True)

        module = next((m for m in course.get('learning_modules') or []
                      if int(activity_id) in traverse_obj(m, ('activities', ..., 'id') or [])), None)

        vimeo_id = self._get_vimeo_id(activity_id)

        return {
            '_type': 'url_transparent',
            'series': traverse_obj(course, ('content_description', 'title')),
            'series_id': str_or_none(traverse_obj(course, ('content_description', 'id'))),
            'id': vimeo_id,
            'chapter': module.get('title'),
            'chapter_id': str_or_none(module.get('id')),
            'title': activity.get('title'),
            'url': smuggle_url(f'https://player.vimeo.com/video/{vimeo_id}', {'http_headers': {'Referer': 'https://api.cybrary.it'}})
        }


class CybraryCourseIE(CybraryBaseIE):
    _VALID_URL = r'https://app.cybrary.it/browse/course/(?P<id>[\w-]+)/?(?:$|[#?])'
    _TESTS = [{
        'url': 'https://app.cybrary.it/browse/course/az-500-microsoft-azure-security-technologies',
        'info_dict': {
            'id': 898,
            'title': 'AZ-500: Microsoft Azure Security Technologies',
            'description': 'md5:69549d379c0fc1dec92926d4e8b6fbd4'
        },
        'playlist_count': 59