4 from .common
import InfoExtractor
5 from ..utils
import ExtractorError
, int_or_none
, urlencode_postdata
8 class CuriosityStreamBaseIE(InfoExtractor
):
9 _NETRC_MACHINE
= 'curiositystream'
12 def _handle_errors(self
, result
):
13 error
= result
.get('error', {}).get('message')
15 if isinstance(error
, dict):
16 error
= ', '.join(error
.values())
18 f
'{self.IE_NAME} said: {error}', expected
=True)
20 def _call_api(self
, path
, video_id
, query
=None):
22 if not self
._auth
_token
:
23 auth_cookie
= self
._get
_cookies
('https://curiositystream.com').get('auth_token')
25 self
.write_debug('Obtained auth_token cookie')
26 self
._auth
_token
= urllib
.parse
.unquote(auth_cookie
.value
)
28 headers
['X-Auth-Token'] = self
._auth
_token
29 result
= self
._download
_json
(
30 self
._API
_BASE
_URL
+ path
, video_id
, headers
=headers
, query
=query
)
31 self
._handle
_errors
(result
)
34 def _perform_login(self
, username
, password
):
35 result
= self
._download
_json
(
36 'https://api.curiositystream.com/v1/login', None,
37 note
='Logging in', data
=urlencode_postdata({
41 self
._handle
_errors
(result
)
42 CuriosityStreamBaseIE
._auth
_token
= result
['message']['auth_token']
45 class CuriosityStreamIE(CuriosityStreamBaseIE
):
46 IE_NAME
= 'curiositystream'
47 _VALID_URL
= r
'https?://(?:app\.)?curiositystream\.com/video/(?P<id>\d+)'
49 'url': 'http://app.curiositystream.com/video/2',
53 'title': 'How Did You Develop The Internet?',
54 'description': 'Vint Cerf, Google\'s Chief Internet Evangelist, describes how he and Bob Kahn created the internet.',
55 'channel': 'Curiosity Stream',
56 'categories': ['Technology', 'Interview'],
57 'average_rating': float,
59 'thumbnail': r
're:https://img.curiositystream.com/.+\.jpg',
65 'skip_download': True,
69 _API_BASE_URL
= 'https://api.curiositystream.com/v1/media/'
71 def _real_extract(self
, url
):
72 video_id
= self
._match
_id
(url
)
75 for encoding_format
in ('m3u8', 'mpd'):
76 media
= self
._call
_api
(video_id
, video_id
, query
={
77 'encodingsNew': 'true',
78 'encodingsFormat': encoding_format
,
80 for encoding
in media
.get('encodings', []):
81 playlist_url
= encoding
.get('master_playlist_url')
82 if encoding_format
== 'm3u8':
83 # use `m3u8` entry_protocol until EXT-X-MAP is properly supported by `m3u8_native` entry_protocol
84 formats
.extend(self
._extract
_m
3u8_formats
(
85 playlist_url
, video_id
, 'mp4',
86 m3u8_id
='hls', fatal
=False))
87 elif encoding_format
== 'mpd':
88 formats
.extend(self
._extract
_mpd
_formats
(
89 playlist_url
, video_id
, mpd_id
='dash', fatal
=False))
90 encoding_url
= encoding
.get('url')
91 file_url
= encoding
.get('file_url')
92 if not encoding_url
and not file_url
:
95 'width': int_or_none(encoding
.get('width')),
96 'height': int_or_none(encoding
.get('height')),
97 'vbr': int_or_none(encoding
.get('video_bitrate')),
98 'abr': int_or_none(encoding
.get('audio_bitrate')),
99 'filesize': int_or_none(encoding
.get('size_in_bytes')),
100 'vcodec': encoding
.get('video_codec'),
101 'acodec': encoding
.get('audio_codec'),
102 'container': encoding
.get('container_type'),
104 for f_url
in (encoding_url
, file_url
):
108 rtmp
= re
.search(r
'^(?P<url>rtmpe?://(?P<host>[^/]+)/(?P<app>.+))/(?P<playpath>mp[34]:.+)$', f_url
)
111 'url': rtmp
.group('url'),
112 'play_path': rtmp
.group('playpath'),
113 'app': rtmp
.group('app'),
124 title
= media
['title']
127 for closed_caption
in media
.get('closed_captions', []):
128 sub_url
= closed_caption
.get('file')
131 lang
= closed_caption
.get('code') or closed_caption
.get('language') or 'en'
132 subtitles
.setdefault(lang
, []).append({
140 'description': media
.get('description'),
141 'thumbnail': media
.get('image_large') or media
.get('image_medium') or media
.get('image_small'),
142 'duration': int_or_none(media
.get('duration')),
143 'tags': media
.get('tags'),
144 'subtitles': subtitles
,
145 'channel': media
.get('producer'),
146 'categories': [media
.get('primary_category'), media
.get('type')],
147 'average_rating': media
.get('rating_percentage'),
148 'series_id': str(media
.get('collection_id') or '') or None,
152 class CuriosityStreamCollectionBaseIE(CuriosityStreamBaseIE
):
154 def _real_extract(self
, url
):
155 collection_id
= self
._match
_id
(url
)
156 collection
= self
._call
_api
(collection_id
, collection_id
)
158 for media
in collection
.get('media', []):
159 media_id
= str(media
.get('id'))
160 media_type
, ie
= ('series', CuriosityStreamSeriesIE
) if media
.get('is_collection') else ('video', CuriosityStreamIE
)
161 entries
.append(self
.url_result(
162 f
'https://curiositystream.com/{media_type}/{media_id}',
163 ie
=ie
.ie_key(), video_id
=media_id
))
164 return self
.playlist_result(
165 entries
, collection_id
,
166 collection
.get('title'), collection
.get('description'))
169 class CuriosityStreamCollectionsIE(CuriosityStreamCollectionBaseIE
):
170 IE_NAME
= 'curiositystream:collections'
171 _VALID_URL
= r
'https?://(?:app\.)?curiositystream\.com/collections/(?P<id>\d+)'
172 _API_BASE_URL
= 'https://api.curiositystream.com/v2/collections/'
174 'url': 'https://curiositystream.com/collections/86',
177 'title': 'Staff Picks',
178 'description': 'Wondering where to start? Here are a few of our favorite series and films... from our couch to yours.',
180 'playlist_mincount': 7,
182 'url': 'https://curiositystream.com/collections/36',
183 'only_matching': True,
187 class CuriosityStreamSeriesIE(CuriosityStreamCollectionBaseIE
):
188 IE_NAME
= 'curiositystream:series'
189 _VALID_URL
= r
'https?://(?:app\.)?curiositystream\.com/(?:series|collection)/(?P<id>\d+)'
190 _API_BASE_URL
= 'https://api.curiositystream.com/v2/series/'
192 'url': 'https://curiositystream.com/series/2',
195 'title': 'Curious Minds: The Internet',
196 'description': 'How is the internet shaping our lives in the 21st Century?',
198 'playlist_mincount': 16,
200 'url': 'https://curiositystream.com/collection/2',
201 'only_matching': True,