5 from .common
import InfoExtractor
6 from ..networking
.exceptions
import HTTPError
16 class RCTIPlusBaseIE(InfoExtractor
):
17 def _real_initialize(self
):
18 self
._AUTH
_KEY
= self
._download
_json
(
19 'https://api.rctiplus.com/api/v1/visitor?platform=web', # platform can be web, mweb, android, ios
20 None, 'Fetching authorization key')['data']['access_token']
22 def _call_api(self
, url
, video_id
, note
=None):
23 json
= self
._download
_json
(
24 url
, video_id
, note
=note
, headers
={'Authorization': self
._AUTH
_KEY
})
25 if json
.get('status', {}).get('code', 0) != 0:
26 raise ExtractorError(f
'{self.IE_NAME} said: {json["status"]["message_client"]}', cause
=json
)
27 return json
.get('data'), json
.get('meta')
30 class RCTIPlusIE(RCTIPlusBaseIE
):
31 _VALID_URL
= r
'https?://www\.rctiplus\.com/(?:programs/\d+?/.*?/)?(?P<type>episode|clip|extra|live-event|missed-event)/(?P<id>\d+)/(?P<display_id>[^/?#&]+)'
33 'url': 'https://www.rctiplus.com/programs/1259/kiko-untuk-lola/episode/22124/untuk-lola',
34 'md5': '56ed45affad45fa18d5592a1bc199997',
37 'title': 'Untuk Lola',
38 'display_id': 'untuk-lola',
39 'description': 'md5:2b809075c0b1e071e228ad6d13e41deb',
42 'timestamp': 1615978800,
43 'upload_date': '20210317',
44 'series': 'Kiko : Untuk Lola',
52 }, { # Clip; Series title doesn't appear on metadata JSON
53 'url': 'https://www.rctiplus.com/programs/316/cahaya-terindah/clip/3921/make-a-wish',
54 'md5': 'd179b2ff356f0e91a53bcc6a4d8504f0',
57 'title': 'Make A Wish',
58 'display_id': 'make-a-wish',
59 'description': 'Make A Wish',
62 'timestamp': 1571652600,
63 'upload_date': '20191021',
64 'series': 'Cahaya Terindah',
71 'url': 'https://www.rctiplus.com/programs/616/inews-malam/extra/9438/diungkapkan-melalui-surat-terbuka-ceo-ruangguru-belva-devara-mundur-dari-staf-khusus-presiden',
72 'md5': 'c48106afdbce609749f5e0c007d9278a',
75 'title': 'md5:2ede828c0f8bde249e0912be150314ca',
76 'display_id': 'md5:62b8d4e9ff096db527a1ad797e8a9933',
77 'description': 'md5:2ede828c0f8bde249e0912be150314ca',
80 'timestamp': 1587561540,
81 'upload_date': '20200422',
82 'series': 'iNews Malam',
85 }, { # Missed event/replay
86 'url': 'https://www.rctiplus.com/missed-event/2507/mou-signing-ceremony-27-juli-2021-1400-wib',
87 'md5': '649c5f27250faed1452ca8b91e06922d',
90 'title': 'MOU Signing Ceremony | 27 Juli 2021 | 14.00 WIB',
91 'display_id': 'mou-signing-ceremony-27-juli-2021-1400-wib',
93 'timestamp': 1627142400,
94 'upload_date': '20210724',
96 'release_timestamp': 1627369200,
101 }, { # Live event; Cloudfront CDN
102 'url': 'https://www.rctiplus.com/live-event/2530/dai-muda-charging-imun-dengan-iman-4-agustus-2021-1600-wib',
105 'title': 'Dai Muda : Charging Imun dengan Iman | 4 Agustus 2021 | 16.00 WIB',
106 'display_id': 'dai-muda-charging-imun-dengan-iman-4-agustus-2021-1600-wib',
108 'timestamp': 1627898400,
109 'upload_date': '20210802',
110 'release_timestamp': 1628067600,
113 'skip_download': True,
115 'skip': 'This live event has ended.',
116 }, { # TV; live_at is null
117 'url': 'https://www.rctiplus.com/live-event/1/rcti',
121 'display_id': 'rcti',
123 'timestamp': 1546344000,
124 'upload_date': '20190101',
128 'skip_download': True,
131 _CONVIVA_JSON_TEMPLATE
= {
133 'cid': 'ff84ae928c3b33064b76dec08f12500465e59a6f',
142 def _real_extract(self
, url
):
143 match
= self
._match
_valid
_url
(url
).groupdict()
144 video_type
, video_id
, display_id
= match
['type'], match
['id'], match
['display_id']
146 url_api_version
= 'v2' if video_type
== 'missed-event' else 'v1'
147 appier_id
= '23984824_' + str(random
.randint(0, 10000000000)) # Based on the webpage's uuidRandom generator
148 video_json
= self
._call
_api
(
149 f
'https://api.rctiplus.com/api/{url_api_version}/{video_type}/{video_id}/url?appierid={appier_id}', display_id
, 'Downloading video URL JSON')[0]
150 video_url
= video_json
['url']
152 is_upcoming
= try_get(video_json
, lambda x
: x
['current_date'] < x
['live_at'])
153 if is_upcoming
is None:
154 is_upcoming
= try_get(video_json
, lambda x
: x
['current_date'] < x
['start_date'])
156 self
.raise_no_formats(
157 'This event will start at {}.'.format(video_json
['live_label']) if video_json
.get('live_label') else 'This event has not started yet.', expected
=True)
158 if 'akamaized' in video_url
:
159 # For some videos hosted on Akamai's CDN (possibly AES-encrypted ones?), a session needs to at least be made via Conviva's API
160 conviva_json_data
= {
161 **self
._CONVIVA
_JSON
_TEMPLATE
,
163 'sst': int(time
.time()),
165 conviva_json_res
= self
._download
_json
(
166 'https://ff84ae928c3b33064b76dec08f12500465e59a6f.cws.conviva.com/0/wsg', display_id
,
167 'Creating Conviva session', 'Failed to create Conviva session',
168 fatal
=False, data
=json
.dumps(conviva_json_data
).encode())
169 if conviva_json_res
and conviva_json_res
.get('err') != 'ok':
170 self
.report_warning('Conviva said: {}'.format(str(conviva_json_res
.get('err'))))
172 video_meta
, meta_paths
= self
._call
_api
(
173 f
'https://api.rctiplus.com/api/v1/{video_type}/{video_id}', display_id
, 'Downloading video metadata')
175 thumbnails
, image_path
= [], meta_paths
.get('image_path', 'https://rstatic.akamaized.net/media/')
176 if video_meta
.get('portrait_image'):
178 'id': 'portrait_image',
179 'url': '{}{}{}'.format(image_path
, 2000, video_meta
['portrait_image']), # 2000px seems to be the highest resolution that can be given
181 if video_meta
.get('landscape_image'):
183 'id': 'landscape_image',
184 'url': '{}{}{}'.format(image_path
, 2000, video_meta
['landscape_image']),
187 formats
= self
._extract
_m
3u8_formats
(video_url
, display_id
, 'mp4', headers
={'Referer': 'https://www.rctiplus.com/'})
188 except ExtractorError
as e
:
189 if isinstance(e
.cause
, HTTPError
) and e
.cause
.status
== 403:
190 self
.raise_geo_restricted(countries
=['ID'], metadata_available
=True)
194 if 'akamaized' in f
['url'] or 'cloudfront' in f
['url']:
195 f
.setdefault('http_headers', {})['Referer'] = 'https://www.rctiplus.com/' # Referer header is required for akamai/cloudfront CDNs
198 'id': video_meta
.get('product_id') or video_json
.get('product_id'),
199 'title': dict_get(video_meta
, ('title', 'name')) or dict_get(video_json
, ('content_name', 'assets_name')),
200 'display_id': display_id
,
201 'description': video_meta
.get('summary'),
202 'timestamp': video_meta
.get('release_date') or video_json
.get('start_date'),
203 'duration': video_meta
.get('duration'),
204 'categories': [video_meta
['genre']] if video_meta
.get('genre') else None,
205 'average_rating': video_meta
.get('star_rating'),
206 'series': video_meta
.get('program_title') or video_json
.get('program_title'),
207 'season_number': video_meta
.get('season'),
208 'episode_number': video_meta
.get('episode'),
209 'channel': video_json
.get('tv_name'),
210 'channel_id': video_json
.get('tv_id'),
212 'thumbnails': thumbnails
,
213 'is_live': video_type
== 'live-event' and not is_upcoming
,
214 'was_live': video_type
== 'missed-event',
215 'live_status': 'is_upcoming' if is_upcoming
else None,
216 'release_timestamp': video_json
.get('live_at'),
220 class RCTIPlusSeriesIE(RCTIPlusBaseIE
):
221 _VALID_URL
= r
'https?://www\.rctiplus\.com/programs/(?P<id>\d+)/(?P<display_id>[^/?#&]+)(?:/(?P<type>episodes|extras|clips))?'
223 'url': 'https://www.rctiplus.com/programs/829/putri-untuk-pangeran',
224 'playlist_mincount': 1019,
227 'title': 'Putri Untuk Pangeran',
228 'description': 'md5:aca7b54d05bd95a67d4f4613cc1d622d',
230 'cast': ['Verrel Bramasta', 'Ranty Maria', 'Riza Syah', 'Ivan Fadilla', 'Nicole Parham', 'Dll', 'Aviv Elham'],
231 'display_id': 'putri-untuk-pangeran',
235 'url': 'https://www.rctiplus.com/programs/615/inews-pagi',
236 'playlist_mincount': 388,
239 'title': 'iNews Pagi',
240 'description': 'md5:f18ee3d4643cfb41c358e5a9b693ee04',
243 'display_id': 'inews-pagi',
246 _AGE_RATINGS
= { # Based off https://id.wikipedia.org/wiki/Sistem_rating_konten_televisi with additional ratings
252 'R-R/1': 17, # Labelled as 17+ despite being R
257 def suitable(cls
, url
):
258 return False if RCTIPlusIE
.suitable(url
) else super().suitable(url
)
260 def _entries(self
, url
, display_id
=None, note
='Downloading entries JSON', metadata
={}):
263 total_pages
= self
._call
_api
(
264 f
'{url}&length=20&page=0',
265 display_id
, note
)[1]['pagination']['total_page']
266 except ExtractorError
as e
:
267 if 'not found' in str(e
):
273 for page_num
in range(1, total_pages
+ 1):
274 episode_list
= self
._call
_api
(
275 f
'{url}&length=20&page={page_num}',
276 display_id
, f
'{note} page {page_num}')[0] or []
278 for video_json
in episode_list
:
281 'url': video_json
['share_link'],
282 'ie_key': RCTIPlusIE
.ie_key(),
283 'id': video_json
.get('product_id'),
284 'title': video_json
.get('title'),
285 'display_id': video_json
.get('title_code').replace('_', '-'),
286 'description': video_json
.get('summary'),
287 'timestamp': video_json
.get('release_date'),
288 'duration': video_json
.get('duration'),
289 'season_number': video_json
.get('season'),
290 'episode_number': video_json
.get('episode'),
294 def _series_entries(self
, series_id
, display_id
=None, video_type
=None, metadata
={}):
295 if not video_type
or video_type
in 'episodes':
297 seasons_list
= self
._call
_api
(
298 f
'https://api.rctiplus.com/api/v1/program/{series_id}/season',
299 display_id
, 'Downloading seasons list JSON')[0]
300 except ExtractorError
as e
:
301 if 'not found' not in str(e
):
304 for season
in seasons_list
:
305 yield from self
._entries
(
306 f
'https://api.rctiplus.com/api/v2/program/{series_id}/episode?season={season["season"]}',
307 display_id
, f
'Downloading season {season["season"]} episode entries', metadata
)
308 if not video_type
or video_type
in 'extras':
309 yield from self
._entries
(
310 f
'https://api.rctiplus.com/api/v2/program/{series_id}/extra?content_id=0',
311 display_id
, 'Downloading extra entries', metadata
)
312 if not video_type
or video_type
in 'clips':
313 yield from self
._entries
(
314 f
'https://api.rctiplus.com/api/v2/program/{series_id}/clip?content_id=0',
315 display_id
, 'Downloading clip entries', metadata
)
317 def _real_extract(self
, url
):
318 series_id
, display_id
, video_type
= self
._match
_valid
_url
(url
).group('id', 'display_id', 'type')
321 f
'Only {video_type} will be downloaded. '
322 f
'To download everything from the series, remove "/{video_type}" from the URL')
324 series_meta
, meta_paths
= self
._call
_api
(
325 f
'https://api.rctiplus.com/api/v1/program/{series_id}/detail', display_id
, 'Downloading series metadata')
327 'age_limit': try_get(series_meta
, lambda x
: self
._AGE
_RATINGS
[x
['age_restriction'][0]['code']]),
328 'cast': traverse_obj(series_meta
, (('starring', 'creator', 'writer'), ..., 'name'),
329 expected_type
=lambda x
: strip_or_none(x
) or None),
330 'tags': traverse_obj(series_meta
, ('tag', ..., 'name'),
331 expected_type
=lambda x
: strip_or_none(x
) or None),
333 return self
.playlist_result(
334 self
._series
_entries
(series_id
, display_id
, video_type
, metadata
), series_id
,
335 series_meta
.get('title'), series_meta
.get('summary'), display_id
=display_id
, **metadata
)
338 class RCTIPlusTVIE(RCTIPlusBaseIE
):
339 _VALID_URL
= r
'https?://www\.rctiplus\.com/((tv/(?P<tvname>\w+))|(?P<eventname>live-event|missed-event))'
341 'url': 'https://www.rctiplus.com/tv/rcti',
346 'timestamp': 1546344000,
347 'upload_date': '20190101',
350 'skip_download': True,
353 # Returned video will always change
354 'url': 'https://www.rctiplus.com/live-event',
355 'only_matching': True,
357 # Returned video will also always change
358 'url': 'https://www.rctiplus.com/missed-event',
359 'only_matching': True,
363 def suitable(cls
, url
):
364 return False if RCTIPlusIE
.suitable(url
) else super().suitable(url
)
366 def _real_extract(self
, url
):
367 match
= self
._match
_valid
_url
(url
).groupdict()
368 tv_id
= match
.get('tvname') or match
.get('eventname')
369 webpage
= self
._download
_webpage
(url
, tv_id
)
370 video_type
, video_id
= self
._search
_regex
(
371 r
'url\s*:\s*["\']https
://api\
.rctiplus\
.com
/api
/v
./(?P
<type>[^
/]+)/(?P
<id>\d
+)/url
',
372 webpage, 'video link
', group=('type', 'id'))
373 return self.url_result(f'https
://www
.rctiplus
.com
/{video_type}
/{video_id}
/{tv_id}
', 'RCTIPlus
')