2 from __future__
import unicode_literals
8 from .common
import InfoExtractor
9 from ..compat
import compat_HTTPError
19 class RCTIPlusBaseIE(InfoExtractor
):
20 def _real_initialize(self
):
21 self
._AUTH
_KEY
= self
._download
_json
(
22 'https://api.rctiplus.com/api/v1/visitor?platform=web', # platform can be web, mweb, android, ios
23 None, 'Fetching authorization key')['data']['access_token']
25 def _call_api(self
, url
, video_id
, note
=None):
26 json
= self
._download
_json
(
27 url
, video_id
, note
=note
, headers
={'Authorization': self._AUTH_KEY}
)
28 if json
.get('status', {}).get('code', 0) != 0:
29 raise ExtractorError(f
'{self.IE_NAME} said: {json["status"]["message_client"]}', cause
=json
)
30 return json
.get('data'), json
.get('meta')
33 class RCTIPlusIE(RCTIPlusBaseIE
):
34 _VALID_URL
= r
'https://www\.rctiplus\.com/(?:programs/\d+?/.*?/)?(?P<type>episode|clip|extra|live-event|missed-event)/(?P<id>\d+)/(?P<display_id>[^/?#&]+)'
36 'url': 'https://www.rctiplus.com/programs/1259/kiko-untuk-lola/episode/22124/untuk-lola',
37 'md5': '56ed45affad45fa18d5592a1bc199997',
40 'title': 'Untuk Lola',
41 'display_id': 'untuk-lola',
42 'description': 'md5:2b809075c0b1e071e228ad6d13e41deb',
45 'timestamp': 1615978800,
46 'upload_date': '20210317',
47 'series': 'Kiko : Untuk Lola',
55 }, { # Clip; Series title doesn't appear on metadata JSON
56 'url': 'https://www.rctiplus.com/programs/316/cahaya-terindah/clip/3921/make-a-wish',
57 'md5': 'd179b2ff356f0e91a53bcc6a4d8504f0',
60 'title': 'Make A Wish',
61 'display_id': 'make-a-wish',
62 'description': 'Make A Wish',
65 'timestamp': 1571652600,
66 'upload_date': '20191021',
67 'series': 'Cahaya Terindah',
74 'url': 'https://www.rctiplus.com/programs/616/inews-malam/extra/9438/diungkapkan-melalui-surat-terbuka-ceo-ruangguru-belva-devara-mundur-dari-staf-khusus-presiden',
75 'md5': 'c48106afdbce609749f5e0c007d9278a',
78 'title': 'md5:2ede828c0f8bde249e0912be150314ca',
79 'display_id': 'md5:62b8d4e9ff096db527a1ad797e8a9933',
80 'description': 'md5:2ede828c0f8bde249e0912be150314ca',
83 'timestamp': 1587561540,
84 'upload_date': '20200422',
85 'series': 'iNews Malam',
88 }, { # Missed event/replay
89 'url': 'https://www.rctiplus.com/missed-event/2507/mou-signing-ceremony-27-juli-2021-1400-wib',
90 'md5': '649c5f27250faed1452ca8b91e06922d',
93 'title': 'MOU Signing Ceremony | 27 Juli 2021 | 14.00 WIB',
94 'display_id': 'mou-signing-ceremony-27-juli-2021-1400-wib',
96 'timestamp': 1627142400,
97 'upload_date': '20210724',
99 'release_timestamp': 1627369200,
104 }, { # Live event; Cloudfront CDN
105 'url': 'https://www.rctiplus.com/live-event/2530/dai-muda-charging-imun-dengan-iman-4-agustus-2021-1600-wib',
108 'title': 'Dai Muda : Charging Imun dengan Iman | 4 Agustus 2021 | 16.00 WIB',
109 'display_id': 'dai-muda-charging-imun-dengan-iman-4-agustus-2021-1600-wib',
111 'timestamp': 1627898400,
112 'upload_date': '20210802',
113 'release_timestamp': 1628067600,
116 'skip_download': True,
118 'skip': 'This live event has ended.',
119 }, { # TV; live_at is null
120 'url': 'https://www.rctiplus.com/live-event/1/rcti',
124 'display_id': 'rcti',
126 'timestamp': 1546344000,
127 'upload_date': '20190101',
131 'skip_download': True,
134 _CONVIVA_JSON_TEMPLATE
= {
136 'cid': 'ff84ae928c3b33064b76dec08f12500465e59a6f',
145 def _real_extract(self
, url
):
146 match
= self
._match
_valid
_url
(url
).groupdict()
147 video_type
, video_id
, display_id
= match
['type'], match
['id'], match
['display_id']
149 url_api_version
= 'v2' if video_type
== 'missed-event' else 'v1'
150 appier_id
= '23984824_' + str(random
.randint(0, 10000000000)) # Based on the webpage's uuidRandom generator
151 video_json
= self
._call
_api
(
152 f
'https://api.rctiplus.com/api/{url_api_version}/{video_type}/{video_id}/url?appierid={appier_id}', display_id
, 'Downloading video URL JSON')[0]
153 video_url
= video_json
['url']
155 is_upcoming
= try_get(video_json
, lambda x
: x
['current_date'] < x
['live_at'])
156 if is_upcoming
is None:
157 is_upcoming
= try_get(video_json
, lambda x
: x
['current_date'] < x
['start_date'])
159 self
.raise_no_formats(
160 'This event will start at %s.' % video_json
['live_label'] if video_json
.get('live_label') else 'This event has not started yet.', expected
=True)
161 if 'akamaized' in video_url
:
162 # For some videos hosted on Akamai's CDN (possibly AES-encrypted ones?), a session needs to at least be made via Conviva's API
163 conviva_json_data
= {
164 **self
._CONVIVA
_JSON
_TEMPLATE
,
166 'sst': int(time
.time())
168 conviva_json_res
= self
._download
_json
(
169 'https://ff84ae928c3b33064b76dec08f12500465e59a6f.cws.conviva.com/0/wsg', display_id
,
170 'Creating Conviva session', 'Failed to create Conviva session',
171 fatal
=False, data
=json
.dumps(conviva_json_data
).encode('utf-8'))
172 if conviva_json_res
and conviva_json_res
.get('err') != 'ok':
173 self
.report_warning('Conviva said: %s' % str(conviva_json_res
.get('err')))
175 video_meta
, meta_paths
= self
._call
_api
(
176 'https://api.rctiplus.com/api/v1/%s/%s' % (video_type
, video_id
), display_id
, 'Downloading video metadata')
178 thumbnails
, image_path
= [], meta_paths
.get('image_path', 'https://rstatic.akamaized.net/media/')
179 if video_meta
.get('portrait_image'):
181 'id': 'portrait_image',
182 'url': '%s%d%s' % (image_path
, 2000, video_meta
['portrait_image']) # 2000px seems to be the highest resolution that can be given
184 if video_meta
.get('landscape_image'):
186 'id': 'landscape_image',
187 'url': '%s%d%s' % (image_path
, 2000, video_meta
['landscape_image'])
190 formats
= self
._extract
_m
3u8_formats
(video_url
, display_id
, 'mp4', headers
={'Referer': 'https://www.rctiplus.com/'}
)
191 except ExtractorError
as e
:
192 if isinstance(e
.cause
, compat_HTTPError
) and e
.cause
.code
== 403:
193 self
.raise_geo_restricted(countries
=['ID'], metadata_available
=True)
197 if 'akamaized' in f
['url'] or 'cloudfront' in f
['url']:
198 f
.setdefault('http_headers', {})['Referer'] = 'https://www.rctiplus.com/' # Referer header is required for akamai/cloudfront CDNs
200 self
._sort
_formats
(formats
)
203 'id': video_meta
.get('product_id') or video_json
.get('product_id'),
204 'title': dict_get(video_meta
, ('title', 'name')) or dict_get(video_json
, ('content_name', 'assets_name')),
205 'display_id': display_id
,
206 'description': video_meta
.get('summary'),
207 'timestamp': video_meta
.get('release_date') or video_json
.get('start_date'),
208 'duration': video_meta
.get('duration'),
209 'categories': [video_meta
['genre']] if video_meta
.get('genre') else None,
210 'average_rating': video_meta
.get('star_rating'),
211 'series': video_meta
.get('program_title') or video_json
.get('program_title'),
212 'season_number': video_meta
.get('season'),
213 'episode_number': video_meta
.get('episode'),
214 'channel': video_json
.get('tv_name'),
215 'channel_id': video_json
.get('tv_id'),
217 'thumbnails': thumbnails
,
218 'is_live': video_type
== 'live-event' and not is_upcoming
,
219 'was_live': video_type
== 'missed-event',
220 'live_status': 'is_upcoming' if is_upcoming
else None,
221 'release_timestamp': video_json
.get('live_at'),
225 class RCTIPlusSeriesIE(RCTIPlusBaseIE
):
226 _VALID_URL
= r
'https://www\.rctiplus\.com/programs/(?P<id>\d+)/(?P<display_id>[^/?#&]+)(?:/(?P<type>episodes|extras|clips))?'
228 'url': 'https://www.rctiplus.com/programs/829/putri-untuk-pangeran',
229 'playlist_mincount': 1019,
232 'title': 'Putri Untuk Pangeran',
233 'description': 'md5:aca7b54d05bd95a67d4f4613cc1d622d',
235 'cast': ['Verrel Bramasta', 'Ranty Maria', 'Riza Syah', 'Ivan Fadilla', 'Nicole Parham', 'Dll', 'Aviv Elham'],
236 'display_id': 'putri-untuk-pangeran',
240 'url': 'https://www.rctiplus.com/programs/615/inews-pagi',
241 'playlist_mincount': 388,
244 'title': 'iNews Pagi',
245 'description': 'md5:f18ee3d4643cfb41c358e5a9b693ee04',
248 'display_id': 'inews-pagi',
251 _AGE_RATINGS
= { # Based off https://id.wikipedia.org/wiki/Sistem_rating_konten_televisi with additional ratings
257 'R-R/1': 17, # Labelled as 17+ despite being R
262 def suitable(cls
, url
):
263 return False if RCTIPlusIE
.suitable(url
) else super(RCTIPlusSeriesIE
, cls
).suitable(url
)
265 def _entries(self
, url
, display_id
=None, note
='Downloading entries JSON', metadata
={}):
268 total_pages
= self
._call
_api
(
269 '%s&length=20&page=0' % url
,
270 display_id
, note
)[1]['pagination']['total_page']
271 except ExtractorError
as e
:
272 if 'not found' in str(e
):
278 for page_num
in range(1, total_pages
+ 1):
279 episode_list
= self
._call
_api
(
280 '%s&length=20&page=%s' % (url
, page_num
),
281 display_id
, '%s page %s' % (note
, page_num
))[0] or []
283 for video_json
in episode_list
:
286 'url': video_json
['share_link'],
287 'ie_key': RCTIPlusIE
.ie_key(),
288 'id': video_json
.get('product_id'),
289 'title': video_json
.get('title'),
290 'display_id': video_json
.get('title_code').replace('_', '-'),
291 'description': video_json
.get('summary'),
292 'timestamp': video_json
.get('release_date'),
293 'duration': video_json
.get('duration'),
294 'season_number': video_json
.get('season'),
295 'episode_number': video_json
.get('episode'),
299 def _series_entries(self
, series_id
, display_id
=None, video_type
=None, metadata
={}):
300 if not video_type
or video_type
in 'episodes':
302 seasons_list
= self
._call
_api
(
303 f
'https://api.rctiplus.com/api/v1/program/{series_id}/season',
304 display_id
, 'Downloading seasons list JSON')[0]
305 except ExtractorError
as e
:
306 if 'not found' not in str(e
):
309 for season
in seasons_list
:
310 yield from self
._entries
(
311 f
'https://api.rctiplus.com/api/v2/program/{series_id}/episode?season={season["season"]}',
312 display_id
, f
'Downloading season {season["season"]} episode entries', metadata
)
313 if not video_type
or video_type
in 'extras':
314 yield from self
._entries
(
315 f
'https://api.rctiplus.com/api/v2/program/{series_id}/extra?content_id=0',
316 display_id
, 'Downloading extra entries', metadata
)
317 if not video_type
or video_type
in 'clips':
318 yield from self
._entries
(
319 f
'https://api.rctiplus.com/api/v2/program/{series_id}/clip?content_id=0',
320 display_id
, 'Downloading clip entries', metadata
)
322 def _real_extract(self
, url
):
323 series_id
, display_id
, video_type
= self
._match
_valid
_url
(url
).group('id', 'display_id', 'type')
326 f
'Only {video_type} will be downloaded. '
327 f
'To download everything from the series, remove "/{video_type}" from the URL')
329 series_meta
, meta_paths
= self
._call
_api
(
330 f
'https://api.rctiplus.com/api/v1/program/{series_id}/detail', display_id
, 'Downloading series metadata')
332 'age_limit': try_get(series_meta
, lambda x
: self
._AGE
_RATINGS
[x
['age_restriction'][0]['code']]),
333 'cast': traverse_obj(series_meta
, (('starring', 'creator', 'writer'), ..., 'name'),
334 expected_type
=lambda x
: strip_or_none(x
) or None),
335 'tag': traverse_obj(series_meta
, ('tag', ..., 'name'),
336 expected_type
=lambda x
: strip_or_none(x
) or None),
338 return self
.playlist_result(
339 self
._series
_entries
(series_id
, display_id
, video_type
, metadata
), series_id
,
340 series_meta
.get('title'), series_meta
.get('summary'), display_id
=display_id
, **metadata
)
343 class RCTIPlusTVIE(RCTIPlusBaseIE
):
344 _VALID_URL
= r
'https://www\.rctiplus\.com/((tv/(?P<tvname>\w+))|(?P<eventname>live-event|missed-event))'
346 'url': 'https://www.rctiplus.com/tv/rcti',
351 'timestamp': 1546344000,
352 'upload_date': '20190101',
355 'skip_download': True,
358 # Returned video will always change
359 'url': 'https://www.rctiplus.com/live-event',
360 'only_matching': True,
362 # Returned video will also always change
363 'url': 'https://www.rctiplus.com/missed-event',
364 'only_matching': True,
368 def suitable(cls
, url
):
369 return False if RCTIPlusIE
.suitable(url
) else super(RCTIPlusTVIE
, cls
).suitable(url
)
371 def _real_extract(self
, url
):
372 match
= self
._match
_valid
_url
(url
).groupdict()
373 tv_id
= match
.get('tvname') or match
.get('eventname')
374 webpage
= self
._download
_webpage
(url
, tv_id
)
375 video_type
, video_id
= self
._search
_regex
(
376 r
'url\s*:\s*["\']https
://api\
.rctiplus\
.com
/api
/v
./(?P
<type>[^
/]+)/(?P
<id>\d
+)/url
',
377 webpage, 'video link
', group=('type', 'id'))
378 return self.url_result(f'https
://www
.rctiplus
.com
/{video_type}
/{video_id}
/{tv_id}
', 'RCTIPlus
')