5 from .common
import InfoExtractor
6 from ..compat
import compat_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 %s.' % 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('utf-8'))
169 if conviva_json_res
and conviva_json_res
.get('err') != 'ok':
170 self
.report_warning('Conviva said: %s' % str(conviva_json_res
.get('err')))
172 video_meta
, meta_paths
= self
._call
_api
(
173 'https://api.rctiplus.com/api/v1/%s/%s' % (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': '%s%d%s' % (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': '%s%d%s' % (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
, compat_HTTPError
) and e
.cause
.code
== 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
197 self
._sort
_formats
(formats
)
200 'id': video_meta
.get('product_id') or video_json
.get('product_id'),
201 'title': dict_get(video_meta
, ('title', 'name')) or dict_get(video_json
, ('content_name', 'assets_name')),
202 'display_id': display_id
,
203 'description': video_meta
.get('summary'),
204 'timestamp': video_meta
.get('release_date') or video_json
.get('start_date'),
205 'duration': video_meta
.get('duration'),
206 'categories': [video_meta
['genre']] if video_meta
.get('genre') else None,
207 'average_rating': video_meta
.get('star_rating'),
208 'series': video_meta
.get('program_title') or video_json
.get('program_title'),
209 'season_number': video_meta
.get('season'),
210 'episode_number': video_meta
.get('episode'),
211 'channel': video_json
.get('tv_name'),
212 'channel_id': video_json
.get('tv_id'),
214 'thumbnails': thumbnails
,
215 'is_live': video_type
== 'live-event' and not is_upcoming
,
216 'was_live': video_type
== 'missed-event',
217 'live_status': 'is_upcoming' if is_upcoming
else None,
218 'release_timestamp': video_json
.get('live_at'),
222 class RCTIPlusSeriesIE(RCTIPlusBaseIE
):
223 _VALID_URL
= r
'https://www\.rctiplus\.com/programs/(?P<id>\d+)/(?P<display_id>[^/?#&]+)(?:/(?P<type>episodes|extras|clips))?'
225 'url': 'https://www.rctiplus.com/programs/829/putri-untuk-pangeran',
226 'playlist_mincount': 1019,
229 'title': 'Putri Untuk Pangeran',
230 'description': 'md5:aca7b54d05bd95a67d4f4613cc1d622d',
232 'cast': ['Verrel Bramasta', 'Ranty Maria', 'Riza Syah', 'Ivan Fadilla', 'Nicole Parham', 'Dll', 'Aviv Elham'],
233 'display_id': 'putri-untuk-pangeran',
237 'url': 'https://www.rctiplus.com/programs/615/inews-pagi',
238 'playlist_mincount': 388,
241 'title': 'iNews Pagi',
242 'description': 'md5:f18ee3d4643cfb41c358e5a9b693ee04',
245 'display_id': 'inews-pagi',
248 _AGE_RATINGS
= { # Based off https://id.wikipedia.org/wiki/Sistem_rating_konten_televisi with additional ratings
254 'R-R/1': 17, # Labelled as 17+ despite being R
259 def suitable(cls
, url
):
260 return False if RCTIPlusIE
.suitable(url
) else super(RCTIPlusSeriesIE
, cls
).suitable(url
)
262 def _entries(self
, url
, display_id
=None, note
='Downloading entries JSON', metadata
={}):
265 total_pages
= self
._call
_api
(
266 '%s&length=20&page=0' % url
,
267 display_id
, note
)[1]['pagination']['total_page']
268 except ExtractorError
as e
:
269 if 'not found' in str(e
):
275 for page_num
in range(1, total_pages
+ 1):
276 episode_list
= self
._call
_api
(
277 '%s&length=20&page=%s' % (url
, page_num
),
278 display_id
, '%s page %s' % (note
, page_num
))[0] or []
280 for video_json
in episode_list
:
283 'url': video_json
['share_link'],
284 'ie_key': RCTIPlusIE
.ie_key(),
285 'id': video_json
.get('product_id'),
286 'title': video_json
.get('title'),
287 'display_id': video_json
.get('title_code').replace('_', '-'),
288 'description': video_json
.get('summary'),
289 'timestamp': video_json
.get('release_date'),
290 'duration': video_json
.get('duration'),
291 'season_number': video_json
.get('season'),
292 'episode_number': video_json
.get('episode'),
296 def _series_entries(self
, series_id
, display_id
=None, video_type
=None, metadata
={}):
297 if not video_type
or video_type
in 'episodes':
299 seasons_list
= self
._call
_api
(
300 f
'https://api.rctiplus.com/api/v1/program/{series_id}/season',
301 display_id
, 'Downloading seasons list JSON')[0]
302 except ExtractorError
as e
:
303 if 'not found' not in str(e
):
306 for season
in seasons_list
:
307 yield from self
._entries
(
308 f
'https://api.rctiplus.com/api/v2/program/{series_id}/episode?season={season["season"]}',
309 display_id
, f
'Downloading season {season["season"]} episode entries', metadata
)
310 if not video_type
or video_type
in 'extras':
311 yield from self
._entries
(
312 f
'https://api.rctiplus.com/api/v2/program/{series_id}/extra?content_id=0',
313 display_id
, 'Downloading extra entries', metadata
)
314 if not video_type
or video_type
in 'clips':
315 yield from self
._entries
(
316 f
'https://api.rctiplus.com/api/v2/program/{series_id}/clip?content_id=0',
317 display_id
, 'Downloading clip entries', metadata
)
319 def _real_extract(self
, url
):
320 series_id
, display_id
, video_type
= self
._match
_valid
_url
(url
).group('id', 'display_id', 'type')
323 f
'Only {video_type} will be downloaded. '
324 f
'To download everything from the series, remove "/{video_type}" from the URL')
326 series_meta
, meta_paths
= self
._call
_api
(
327 f
'https://api.rctiplus.com/api/v1/program/{series_id}/detail', display_id
, 'Downloading series metadata')
329 'age_limit': try_get(series_meta
, lambda x
: self
._AGE
_RATINGS
[x
['age_restriction'][0]['code']]),
330 'cast': traverse_obj(series_meta
, (('starring', 'creator', 'writer'), ..., 'name'),
331 expected_type
=lambda x
: strip_or_none(x
) or None),
332 'tag': traverse_obj(series_meta
, ('tag', ..., 'name'),
333 expected_type
=lambda x
: strip_or_none(x
) or None),
335 return self
.playlist_result(
336 self
._series
_entries
(series_id
, display_id
, video_type
, metadata
), series_id
,
337 series_meta
.get('title'), series_meta
.get('summary'), display_id
=display_id
, **metadata
)
340 class RCTIPlusTVIE(RCTIPlusBaseIE
):
341 _VALID_URL
= r
'https://www\.rctiplus\.com/((tv/(?P<tvname>\w+))|(?P<eventname>live-event|missed-event))'
343 'url': 'https://www.rctiplus.com/tv/rcti',
348 'timestamp': 1546344000,
349 'upload_date': '20190101',
352 'skip_download': True,
355 # Returned video will always change
356 'url': 'https://www.rctiplus.com/live-event',
357 'only_matching': True,
359 # Returned video will also always change
360 'url': 'https://www.rctiplus.com/missed-event',
361 'only_matching': True,
365 def suitable(cls
, url
):
366 return False if RCTIPlusIE
.suitable(url
) else super(RCTIPlusTVIE
, cls
).suitable(url
)
368 def _real_extract(self
, url
):
369 match
= self
._match
_valid
_url
(url
).groupdict()
370 tv_id
= match
.get('tvname') or match
.get('eventname')
371 webpage
= self
._download
_webpage
(url
, tv_id
)
372 video_type
, video_id
= self
._search
_regex
(
373 r
'url\s*:\s*["\']https
://api\
.rctiplus\
.com
/api
/v
./(?P
<type>[^
/]+)/(?P
<id>\d
+)/url
',
374 webpage, 'video link
', group=('type', 'id'))
375 return self.url_result(f'https
://www
.rctiplus
.com
/{video_type}
/{video_id}
/{tv_id}
', 'RCTIPlus
')