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',
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>[^/?#&]+)'
228 'url': 'https://www.rctiplus.com/programs/540/upin-ipin',
229 'playlist_mincount': 417,
232 'title': 'Upin & Ipin',
233 'description': 'md5:22cc912381f389664416844e1ec4f86b',
236 'url': 'https://www.rctiplus.com/programs/540/upin-ipin/episodes?utm_source=Rplusdweb&utm_medium=share_copy&utm_campaign=programsupin-ipin',
237 'only_matching': True,
239 _AGE_RATINGS
= { # Based off https://id.wikipedia.org/wiki/Sistem_rating_konten_televisi with additional ratings
245 'R-R/1': 17, # Labelled as 17+ despite being R
250 def suitable(cls
, url
):
251 return False if RCTIPlusIE
.suitable(url
) else super(RCTIPlusSeriesIE
, cls
).suitable(url
)
253 def _entries(self
, url
, display_id
=None, note
='Downloading entries JSON', metadata
={}):
256 total_pages
= self
._call
_api
(
257 '%s&length=20&page=0' % url
,
258 display_id
, note
)[1]['pagination']['total_page']
259 except ExtractorError
as e
:
260 if 'not found' in str(e
):
266 for page_num
in range(1, total_pages
+ 1):
267 episode_list
= self
._call
_api
(
268 '%s&length=20&page=%s' % (url
, page_num
),
269 display_id
, '%s page %s' % (note
, page_num
))[0] or []
271 for video_json
in episode_list
:
272 link
= video_json
['share_link']
273 url_res
= self
.url_result(link
, 'RCTIPlus', video_json
.get('product_id'), video_json
.get('title'))
274 url_res
.update(metadata
)
277 def _real_extract(self
, url
):
278 series_id
, display_id
= self
._match
_valid
_url
(url
).groups()
280 series_meta
, meta_paths
= self
._call
_api
(
281 'https://api.rctiplus.com/api/v1/program/%s/detail' % series_id
, display_id
, 'Downloading series metadata')
283 'age_limit': try_get(series_meta
, lambda x
: self
._AGE
_RATINGS
[x
['age_restriction'][0]['code']])
287 for star
in series_meta
.get('starring', []):
288 cast
.append(strip_or_none(star
.get('name')))
289 for star
in series_meta
.get('creator', []):
290 cast
.append(strip_or_none(star
.get('name')))
291 for star
in series_meta
.get('writer', []):
292 cast
.append(strip_or_none(star
.get('name')))
293 metadata
['cast'] = cast
296 for tag
in series_meta
.get('tag', []):
297 tags
.append(strip_or_none(tag
.get('name')))
298 metadata
['tag'] = tags
301 seasons_list
= self
._call
_api
(
302 'https://api.rctiplus.com/api/v1/program/%s/season' % series_id
, display_id
, 'Downloading seasons list JSON')[0]
303 for season
in seasons_list
:
304 entries
.append(self
._entries
('https://api.rctiplus.com/api/v2/program/%s/episode?season=%s' % (series_id
, season
['season']),
305 display_id
, 'Downloading season %s episode entries' % season
['season'], metadata
))
307 entries
.append(self
._entries
('https://api.rctiplus.com/api/v2/program/%s/clip?content_id=0' % series_id
,
308 display_id
, 'Downloading clip entries', metadata
))
309 entries
.append(self
._entries
('https://api.rctiplus.com/api/v2/program/%s/extra?content_id=0' % series_id
,
310 display_id
, 'Downloading extra entries', metadata
))
312 return self
.playlist_result(itertools
.chain(*entries
), series_id
, series_meta
.get('title'), series_meta
.get('summary'), **metadata
)
315 class RCTIPlusTVIE(RCTIPlusBaseIE
):
316 _VALID_URL
= r
'https://www\.rctiplus\.com/((tv/(?P<tvname>\w+))|(?P<eventname>live-event|missed-event))'
318 'url': 'https://www.rctiplus.com/tv/rcti',
323 'timestamp': 1546344000,
324 'upload_date': '20190101',
327 'skip_download': True,
330 # Returned video will always change
331 'url': 'https://www.rctiplus.com/live-event',
332 'only_matching': True,
334 # Returned video will also always change
335 'url': 'https://www.rctiplus.com/missed-event',
336 'only_matching': True,
340 def suitable(cls
, url
):
341 return False if RCTIPlusIE
.suitable(url
) else super(RCTIPlusTVIE
, cls
).suitable(url
)
343 def _real_extract(self
, url
):
344 match
= self
._match
_valid
_url
(url
).groupdict()
345 tv_id
= match
.get('tvname') or match
.get('eventname')
346 webpage
= self
._download
_webpage
(url
, tv_id
)
347 video_type
, video_id
= self
._search
_regex
(
348 r
'url\s*:\s*["\']https
://api\
.rctiplus\
.com
/api
/v
./(?P
<type>[^
/]+)/(?P
<id>\d
+)/url
', webpage, 'video link
', group=('type', 'id'))
349 return self.url_result(f'https
://www
.rctiplus
.com
/{video_type}
/{video_id}
/{tv_id}
', 'RCTIPlus
')