1# coding: utf-8 2from __future__ import unicode_literals 3 4from .common import InfoExtractor 5from ..compat import compat_str 6from ..utils import ( 7 determine_ext, 8 int_or_none, 9 try_get, 10 unified_timestamp, 11 url_or_none, 12) 13 14 15class EggheadBaseIE(InfoExtractor): 16 def _call_api(self, path, video_id, resource, fatal=True): 17 return self._download_json( 18 'https://app.egghead.io/api/v1/' + path, 19 video_id, 'Downloading %s JSON' % resource, fatal=fatal) 20 21 22class EggheadCourseIE(EggheadBaseIE): 23 IE_DESC = 'egghead.io course' 24 IE_NAME = 'egghead:course' 25 _VALID_URL = r'https://(?:app\.)?egghead\.io/(?:course|playlist)s/(?P<id>[^/?#&]+)' 26 _TESTS = [{ 27 'url': 'https://egghead.io/courses/professor-frisby-introduces-composable-functional-javascript', 28 'playlist_count': 29, 29 'info_dict': { 30 'id': '432655', 31 'title': 'Professor Frisby Introduces Composable Functional JavaScript', 32 'description': 're:(?s)^This course teaches the ubiquitous.*You\'ll start composing functionality before you know it.$', 33 }, 34 }, { 35 'url': 'https://app.egghead.io/playlists/professor-frisby-introduces-composable-functional-javascript', 36 'only_matching': True, 37 }] 38 39 def _real_extract(self, url): 40 playlist_id = self._match_id(url) 41 series_path = 'series/' + playlist_id 42 lessons = self._call_api( 43 series_path + '/lessons', playlist_id, 'course lessons') 44 45 entries = [] 46 for lesson in lessons: 47 lesson_url = url_or_none(lesson.get('http_url')) 48 if not lesson_url: 49 continue 50 lesson_id = lesson.get('id') 51 if lesson_id: 52 lesson_id = compat_str(lesson_id) 53 entries.append(self.url_result( 54 lesson_url, ie=EggheadLessonIE.ie_key(), video_id=lesson_id)) 55 56 course = self._call_api( 57 series_path, playlist_id, 'course', False) or {} 58 59 playlist_id = course.get('id') 60 if playlist_id: 61 playlist_id = compat_str(playlist_id) 62 63 return self.playlist_result( 64 entries, playlist_id, course.get('title'), 65 course.get('description')) 66 67 68class EggheadLessonIE(EggheadBaseIE): 69 IE_DESC = 'egghead.io lesson' 70 IE_NAME = 'egghead:lesson' 71 _VALID_URL = r'https://(?:app\.)?egghead\.io/(?:api/v1/)?lessons/(?P<id>[^/?#&]+)' 72 _TESTS = [{ 73 'url': 'https://egghead.io/lessons/javascript-linear-data-flow-with-container-style-types-box', 74 'info_dict': { 75 'id': '1196', 76 'display_id': 'javascript-linear-data-flow-with-container-style-types-box', 77 'ext': 'mp4', 78 'title': 'Create linear data flow with container style types (Box)', 79 'description': 'md5:9aa2cdb6f9878ed4c39ec09e85a8150e', 80 'thumbnail': r're:^https?:.*\.jpg$', 81 'timestamp': 1481296768, 82 'upload_date': '20161209', 83 'duration': 304, 84 'view_count': 0, 85 'tags': 'count:2', 86 }, 87 'params': { 88 'skip_download': True, 89 'format': 'bestvideo', 90 }, 91 }, { 92 'url': 'https://egghead.io/api/v1/lessons/react-add-redux-to-a-react-application', 93 'only_matching': True, 94 }, { 95 'url': 'https://app.egghead.io/lessons/javascript-linear-data-flow-with-container-style-types-box', 96 'only_matching': True, 97 }] 98 99 def _real_extract(self, url): 100 display_id = self._match_id(url) 101 102 lesson = self._call_api( 103 'lessons/' + display_id, display_id, 'lesson') 104 105 lesson_id = compat_str(lesson['id']) 106 title = lesson['title'] 107 108 formats = [] 109 for _, format_url in lesson['media_urls'].items(): 110 format_url = url_or_none(format_url) 111 if not format_url: 112 continue 113 ext = determine_ext(format_url) 114 if ext == 'm3u8': 115 formats.extend(self._extract_m3u8_formats( 116 format_url, lesson_id, 'mp4', entry_protocol='m3u8', 117 m3u8_id='hls', fatal=False)) 118 elif ext == 'mpd': 119 formats.extend(self._extract_mpd_formats( 120 format_url, lesson_id, mpd_id='dash', fatal=False)) 121 else: 122 formats.append({ 123 'url': format_url, 124 }) 125 self._sort_formats(formats) 126 127 return { 128 'id': lesson_id, 129 'display_id': display_id, 130 'title': title, 131 'description': lesson.get('summary'), 132 'thumbnail': lesson.get('thumb_nail'), 133 'timestamp': unified_timestamp(lesson.get('published_at')), 134 'duration': int_or_none(lesson.get('duration')), 135 'view_count': int_or_none(lesson.get('plays_count')), 136 'tags': try_get(lesson, lambda x: x['tag_list'], list), 137 'series': try_get( 138 lesson, lambda x: x['series']['title'], compat_str), 139 'formats': formats, 140 } 141