2 from __future__
import unicode_literals
9 from .common
import InfoExtractor
10 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('%s said: %s' % (self
.IE_NAME
, 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',
89 'format': 'bestvideo',
91 }, { # Missed event/replay
92 'url': 'https://www.rctiplus.com/missed-event/2507/mou-signing-ceremony-27-juli-2021-1400-wib',
93 'md5': '649c5f27250faed1452ca8b91e06922d',
96 'title': 'MOU Signing Ceremony | 27 Juli 2021 | 14.00 WIB',
97 'display_id': 'mou-signing-ceremony-27-juli-2021-1400-wib',
99 'timestamp': 1627142400,
100 'upload_date': '20210724',
102 'release_timestamp': 1627369200,
107 }, { # Live event; Cloudfront CDN
108 'url': 'https://www.rctiplus.com/live-event/2530/dai-muda-charging-imun-dengan-iman-4-agustus-2021-1600-wib',
111 'title': 'Dai Muda : Charging Imun dengan Iman | 4 Agustus 2021 | 16.00 WIB',
112 'display_id': 'dai-muda-charging-imun-dengan-iman-4-agustus-2021-1600-wib',
114 'timestamp': 1627898400,
115 'upload_date': '20210802',
116 'release_timestamp': 1628067600,
119 'skip_download': True,
121 'skip': 'This live event has ended.',
122 }, { # TV; live_at is null
123 'url': 'https://www.rctiplus.com/live-event/1/rcti',
127 'display_id': 'rcti',
129 'timestamp': 1546344000,
130 'upload_date': '20190101',
134 'skip_download': True,
135 'format': 'bestvideo',
138 _CONVIVA_JSON_TEMPLATE
= {
140 'cid': 'ff84ae928c3b33064b76dec08f12500465e59a6f',
149 def _real_extract(self
, url
):
150 match
= self
._match
_valid
_url
(url
).groupdict()
151 video_type
, video_id
, display_id
= match
['type'], match
['id'], match
['display_id']
153 url_api_version
= 'v2' if video_type
== 'missed-event' else 'v1'
154 appier_id
= '23984824_' + str(random
.randint(0, 10000000000)) # Based on the webpage's uuidRandom generator
155 video_json
= self
._call
_api
(
156 f
'https://api.rctiplus.com/api/{url_api_version}/{video_type}/{video_id}/url?appierid={appier_id}', display_id
, 'Downloading video URL JSON')[0]
157 video_url
= video_json
['url']
159 is_upcoming
= try_get(video_json
, lambda x
: x
['current_date'] < x
['live_at'])
160 if is_upcoming
is None:
161 is_upcoming
= try_get(video_json
, lambda x
: x
['current_date'] < x
['start_date'])
163 self
.raise_no_formats(
164 '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)
165 if 'akamaized' in video_url
:
166 # For some videos hosted on Akamai's CDN (possibly AES-encrypted ones?), a session needs to at least be made via Conviva's API
167 conviva_json_data
= {
168 **self
._CONVIVA
_JSON
_TEMPLATE
,
170 'sst': int(time
.time())
172 conviva_json_res
= self
._download
_json
(
173 'https://ff84ae928c3b33064b76dec08f12500465e59a6f.cws.conviva.com/0/wsg', display_id
,
174 'Creating Conviva session', 'Failed to create Conviva session',
175 fatal
=False, data
=json
.dumps(conviva_json_data
).encode('utf-8'))
176 if conviva_json_res
and conviva_json_res
.get('err') != 'ok':
177 self
.report_warning('Conviva said: %s' % str(conviva_json_res
.get('err')))
179 video_meta
, meta_paths
= self
._call
_api
(
180 'https://api.rctiplus.com/api/v1/%s/%s' % (video_type
, video_id
), display_id
, 'Downloading video metadata')
182 thumbnails
, image_path
= [], meta_paths
.get('image_path', 'https://rstatic.akamaized.net/media/')
183 if video_meta
.get('portrait_image'):
185 'id': 'portrait_image',
186 'url': '%s%d%s' % (image_path
, 2000, video_meta
['portrait_image']) # 2000px seems to be the highest resolution that can be given
188 if video_meta
.get('landscape_image'):
190 'id': 'landscape_image',
191 'url': '%s%d%s' % (image_path
, 2000, video_meta
['landscape_image'])
194 formats
= self
._extract
_m
3u8_formats
(video_url
, display_id
, 'mp4', headers
={'Referer': 'https://www.rctiplus.com/'}
)
195 except ExtractorError
as e
:
196 if isinstance(e
.cause
, compat_HTTPError
) and e
.cause
.code
== 403:
197 self
.raise_geo_restricted(countries
=['ID'], metadata_available
=True)
201 if 'akamaized' in f
['url'] or 'cloudfront' in f
['url']:
202 f
.setdefault('http_headers', {})['Referer'] = 'https://www.rctiplus.com/' # Referer header is required for akamai/cloudfront CDNs
204 self
._sort
_formats
(formats
)
207 'id': video_meta
.get('product_id') or video_json
.get('product_id'),
208 'title': dict_get(video_meta
, ('title', 'name')) or dict_get(video_json
, ('content_name', 'assets_name')),
209 'display_id': display_id
,
210 'description': video_meta
.get('summary'),
211 'timestamp': video_meta
.get('release_date') or video_json
.get('start_date'),
212 'duration': video_meta
.get('duration'),
213 'categories': [video_meta
['genre']] if video_meta
.get('genre') else None,
214 'average_rating': video_meta
.get('star_rating'),
215 'series': video_meta
.get('program_title') or video_json
.get('program_title'),
216 'season_number': video_meta
.get('season'),
217 'episode_number': video_meta
.get('episode'),
218 'channel': video_json
.get('tv_name'),
219 'channel_id': video_json
.get('tv_id'),
221 'thumbnails': thumbnails
,
222 'is_live': video_type
== 'live-event' and not is_upcoming
,
223 'was_live': video_type
== 'missed-event',
224 'live_status': 'is_upcoming' if is_upcoming
else None,
225 'release_timestamp': video_json
.get('live_at'),
229 class RCTIPlusSeriesIE(RCTIPlusBaseIE
):
230 _VALID_URL
= r
'https://www\.rctiplus\.com/programs/(?P<id>\d+)/(?P<display_id>[^/?#&]+)'
232 'url': 'https://www.rctiplus.com/programs/540/upin-ipin',
233 'playlist_mincount': 417,
236 'title': 'Upin & Ipin',
237 'description': 'md5:22cc912381f389664416844e1ec4f86b',
240 'url': 'https://www.rctiplus.com/programs/540/upin-ipin/episodes?utm_source=Rplusdweb&utm_medium=share_copy&utm_campaign=programsupin-ipin',
241 'only_matching': True,
243 _AGE_RATINGS
= { # Based off https://id.wikipedia.org/wiki/Sistem_rating_konten_televisi with additional ratings
249 'R-R/1': 17, # Labelled as 17+ despite being R
254 def suitable(cls
, url
):
255 return False if RCTIPlusIE
.suitable(url
) else super(RCTIPlusSeriesIE
, cls
).suitable(url
)
257 def _entries(self
, url
, display_id
=None, note
='Downloading entries JSON', metadata
={}):
260 total_pages
= self
._call
_api
(
261 '%s&length=20&page=0' % url
,
262 display_id
, note
)[1]['pagination']['total_page']
263 except ExtractorError
as e
:
264 if 'not found' in str(e
):
270 for page_num
in range(1, total_pages
+ 1):
271 episode_list
= self
._call
_api
(
272 '%s&length=20&page=%s' % (url
, page_num
),
273 display_id
, '%s page %s' % (note
, page_num
))[0] or []
275 for video_json
in episode_list
:
276 link
= video_json
['share_link']
277 url_res
= self
.url_result(link
, 'RCTIPlus', video_json
.get('product_id'), video_json
.get('title'))
278 url_res
.update(metadata
)
281 def _real_extract(self
, url
):
282 series_id
, display_id
= self
._match
_valid
_url
(url
).groups()
284 series_meta
, meta_paths
= self
._call
_api
(
285 'https://api.rctiplus.com/api/v1/program/%s/detail' % series_id
, display_id
, 'Downloading series metadata')
287 'age_limit': try_get(series_meta
, lambda x
: self
._AGE
_RATINGS
[x
['age_restriction'][0]['code']])
291 for star
in series_meta
.get('starring', []):
292 cast
.append(strip_or_none(star
.get('name')))
293 for star
in series_meta
.get('creator', []):
294 cast
.append(strip_or_none(star
.get('name')))
295 for star
in series_meta
.get('writer', []):
296 cast
.append(strip_or_none(star
.get('name')))
297 metadata
['cast'] = cast
300 for tag
in series_meta
.get('tag', []):
301 tags
.append(strip_or_none(tag
.get('name')))
302 metadata
['tag'] = tags
305 seasons_list
= self
._call
_api
(
306 'https://api.rctiplus.com/api/v1/program/%s/season' % series_id
, display_id
, 'Downloading seasons list JSON')[0]
307 for season
in seasons_list
:
308 entries
.append(self
._entries
('https://api.rctiplus.com/api/v2/program/%s/episode?season=%s' % (series_id
, season
['season']),
309 display_id
, 'Downloading season %s episode entries' % season
['season'], metadata
))
311 entries
.append(self
._entries
('https://api.rctiplus.com/api/v2/program/%s/clip?content_id=0' % series_id
,
312 display_id
, 'Downloading clip entries', metadata
))
313 entries
.append(self
._entries
('https://api.rctiplus.com/api/v2/program/%s/extra?content_id=0' % series_id
,
314 display_id
, 'Downloading extra entries', metadata
))
316 return self
.playlist_result(itertools
.chain(*entries
), series_id
, series_meta
.get('title'), series_meta
.get('summary'), **metadata
)
319 class RCTIPlusTVIE(RCTIPlusBaseIE
):
320 _VALID_URL
= r
'https://www\.rctiplus\.com/((tv/(?P<tvname>\w+))|(?P<eventname>live-event|missed-event))'
322 'url': 'https://www.rctiplus.com/tv/rcti',
327 'timestamp': 1546344000,
328 'upload_date': '20190101',
331 'skip_download': True,
332 'format': 'bestvideo',
335 # Returned video will always change
336 'url': 'https://www.rctiplus.com/live-event',
337 'only_matching': True,
339 # Returned video will also always change
340 'url': 'https://www.rctiplus.com/missed-event',
341 'only_matching': True,
345 def suitable(cls
, url
):
346 return False if RCTIPlusIE
.suitable(url
) else super(RCTIPlusTVIE
, cls
).suitable(url
)
348 def _real_extract(self
, url
):
349 match
= self
._match
_valid
_url
(url
).groupdict()
350 tv_id
= match
.get('tvname') or match
.get('eventname')
351 webpage
= self
._download
_webpage
(url
, tv_id
)
352 video_type
, video_id
= self
._search
_regex
(
353 r
'url\s*:\s*["\']https
://api\
.rctiplus\
.com
/api
/v
./(?P
<type>[^
/]+)/(?P
<id>\d
+)/url
', webpage, 'video link
', group=('type', 'id'))
354 return self.url_result(f'https
://www
.rctiplus
.com
/{video_type}
/{video_id}
/{tv_id}
', 'RCTIPlus
')