2 from __future__
import unicode_literals
7 from .naver
import NaverBaseIE
23 class VLiveBaseIE(NaverBaseIE
):
24 _APP_ID
= '8c6cc7b45d2568fb668be6e05b6e5a3b'
27 class VLiveIE(VLiveBaseIE
):
29 _VALID_URL
= r
'https?://(?:(?:www|m)\.)?vlive\.tv/(?:video|embed)/(?P<id>[0-9]+)'
30 _NETRC_MACHINE
= 'vlive'
32 'url': 'http://www.vlive.tv/video/1326',
33 'md5': 'cc7314812855ce56de70a06a27314983',
37 'title': "Girl's Day's Broadcast",
38 'creator': "Girl's Day",
40 'uploader_id': 'muploader_a',
43 'url': 'http://www.vlive.tv/video/16937',
50 'subtitles': 'mincount:12',
51 'uploader_id': 'muploader_j',
54 'skip_download': True,
57 'url': 'https://www.vlive.tv/video/129100',
58 'md5': 'ca2569453b79d66e5b919e5d308bff6b',
62 'title': '[V LIVE] [BTS+] Run BTS! 2019 - EP.71 :: Behind the scene',
65 'subtitles': 'mincount:10',
67 'skip': 'This video is only available for CH+ subscribers',
69 'url': 'https://www.vlive.tv/embed/1326',
70 'only_matching': True,
72 # works only with gcc=KR
73 'url': 'https://www.vlive.tv/video/225019',
74 'only_matching': True,
76 'url': 'https://www.vlive.tv/video/223906',
81 'playlist_mincount': 120
84 def _real_initialize(self
):
88 email
, password
= self
._get
_login
_info
()
89 if None in (email
, password
):
93 login_info
= self
._download
_json
(
94 'https://www.vlive.tv/auth/loginInfo', None,
95 note
='Downloading login info',
96 headers
={'Referer': 'https://www.vlive.tv/home'}
)
98 login_info
, lambda x
: x
['message']['login'], bool) or False
100 LOGIN_URL
= 'https://www.vlive.tv/auth/email/login'
101 self
._request
_webpage
(
102 LOGIN_URL
, None, note
='Downloading login cookies')
104 self
._download
_webpage
(
105 LOGIN_URL
, None, note
='Logging in',
106 data
=urlencode_postdata({'email': email, 'pwd': password}
),
108 'Referer': LOGIN_URL
,
109 'Content-Type': 'application/x-www-form-urlencoded'
112 if not is_logged_in():
113 raise ExtractorError('Unable to log in', expected
=True)
115 def _call_api(self
, path_template
, video_id
, fields
=None, limit
=None):
116 query
= {'appId': self._APP_ID, 'gcc': 'KR', 'platformType': 'PC'}
118 query
['fields'] = fields
120 query
['limit'] = limit
122 return self
._download
_json
(
123 'https://www.vlive.tv/globalv-web/vam-web/' + path_template
% video_id
, video_id
,
124 'Downloading %s JSON metadata' % path_template
.split('/')[-1].split('-')[0],
125 headers
={'Referer': 'https://www.vlive.tv/'}
, query
=query
)
126 except ExtractorError
as e
:
127 if isinstance(e
.cause
, compat_HTTPError
) and e
.cause
.code
== 403:
128 self
.raise_login_required(json
.loads(e
.cause
.read().decode('utf-8'))['message'])
131 def _real_extract(self
, url
):
132 video_id
= self
._match
_id
(url
)
134 post
= self
._call
_api
(
135 'post/v1.0/officialVideoPost-%s', video_id
,
136 'author{nickname},channel{channelCode,channelName},officialVideo{commentCount,exposeStatus,likeCount,playCount,playTime,status,title,type,vodId},playlist{playlistSeq,totalCount,name}')
138 playlist
= post
.get('playlist')
139 if not playlist
or self
.get_param('noplaylist'):
142 'Downloading just video %s because of --no-playlist'
145 video
= post
['officialVideo']
146 return self
._get
_vlive
_info
(post
, video
, video_id
)
148 playlist_name
= playlist
.get('name')
149 playlist_id
= str_or_none(playlist
.get('playlistSeq'))
150 playlist_count
= str_or_none(playlist
.get('totalCount'))
152 playlist
= self
._call
_api
(
153 'playlist/v1.0/playlist-%s/posts', playlist_id
, 'data', limit
=playlist_count
)
156 for video_data
in playlist
['data']:
157 video
= video_data
.get('officialVideo')
158 video_id
= str_or_none(video
.get('videoSeq'))
159 entries
.append(self
._get
_vlive
_info
(video_data
, video
, video_id
))
161 return self
.playlist_result(entries
, playlist_id
, playlist_name
)
163 def _get_vlive_info(self
, post
, video
, video_id
):
164 def get_common_fields():
165 channel
= post
.get('channel') or {}
167 'title': video
.get('title'),
168 'creator': post
.get('author', {}).get('nickname'),
169 'channel': channel
.get('channelName'),
170 'channel_id': channel
.get('channelCode'),
171 'duration': int_or_none(video
.get('playTime')),
172 'view_count': int_or_none(video
.get('playCount')),
173 'like_count': int_or_none(video
.get('likeCount')),
174 'comment_count': int_or_none(video
.get('commentCount')),
177 video_type
= video
.get('type')
178 if video_type
== 'VOD':
179 inkey
= self
._call
_api
('video/v1.0/vod/%s/inkey', video_id
)['inkey']
180 vod_id
= video
['vodId']
181 info_dict
= merge_dicts(
183 self
._extract
_video
_info
(video_id
, vod_id
, inkey
))
184 thumbnail
= video
.get('thumb')
186 if not info_dict
.get('thumbnails') and info_dict
.get('thumbnail'):
187 info_dict
['thumbnails'] = [{'url': info_dict.pop('thumbnail')}
]
188 info_dict
.setdefault('thumbnails', []).append({'url': thumbnail, 'preference': 1}
)
190 elif video_type
== 'LIVE':
191 status
= video
.get('status')
192 if status
== 'ON_AIR':
193 stream_url
= self
._call
_api
(
194 'old/v3/live/%s/playInfo',
195 video_id
)['result']['adaptiveStreamUrl']
196 formats
= self
._extract
_m
3u8_formats
(stream_url
, video_id
, 'mp4')
197 self
._sort
_formats
(formats
)
198 info
= get_common_fields()
200 'title': self
._live
_title
(video
['title']),
206 elif status
== 'ENDED':
207 raise ExtractorError(
208 'Uploading for replay. Please wait...', expected
=True)
209 elif status
== 'RESERVED':
210 raise ExtractorError('Coming soon!', expected
=True)
211 elif video
.get('exposeStatus') == 'CANCEL':
212 raise ExtractorError(
213 'We are sorry, but the live broadcast has been canceled.',
216 raise ExtractorError('Unknown status ' + status
)
219 class VLivePostIE(VLiveIE
):
220 IE_NAME
= 'vlive:post'
221 _VALID_URL
= r
'https?://(?:(?:www|m)\.)?vlive\.tv/post/(?P<id>\d-\d+)'
224 'url': 'https://www.vlive.tv/post/1-20088044',
227 'title': 'Hola estrellitas la tierra les dice hola (si era así no?) Ha...',
228 'description': 'md5:fab8a1e50e6e51608907f46c7fa4b407',
233 'url': 'https://www.vlive.tv/post/1-20087926',
236 'title': 'James Corden: And so, the baby becamos the Papa💜😭💪😭',
240 _FVIDEO_TMPL
= 'fvideo/v1.0/fvideo-%%s/%s'
241 _SOS_TMPL
= _FVIDEO_TMPL
% 'sosPlayInfo'
242 _INKEY_TMPL
= _FVIDEO_TMPL
% 'inKey'
244 def _real_extract(self
, url
):
245 post_id
= self
._match
_id
(url
)
247 post
= self
._call
_api
(
248 'post/v1.0/post-%s', post_id
,
249 'attachments{video},officialVideo{videoSeq},plainBody,title')
251 video_seq
= str_or_none(try_get(
252 post
, lambda x
: x
['officialVideo']['videoSeq']))
254 return self
.url_result(
255 'http://www.vlive.tv/video/' + video_seq
,
256 VLiveIE
.ie_key(), video_seq
)
258 title
= post
['title']
260 for idx
, video
in enumerate(post
['attachments']['video'].values()):
261 video_id
= video
.get('videoId')
264 upload_type
= video
.get('uploadType')
265 upload_info
= video
.get('uploadInfo') or {}
267 if upload_type
== 'SOS':
268 download
= self
._call
_api
(
269 self
._SOS
_TMPL
, video_id
)['videoUrl']['download']
271 for f_id
, f_url
in download
.items():
275 'height': int_or_none(f_id
[:-1]),
277 self
._sort
_formats
(formats
)
281 'thumbnail': upload_info
.get('imageUrl'),
283 elif upload_type
== 'V':
284 vod_id
= upload_info
.get('videoId')
287 inkey
= self
._call
_api
(self
._INKEY
_TMPL
, video_id
)['inKey']
288 entry
= self
._extract
_video
_info
(video_id
, vod_id
, inkey
)
290 entry
['title'] = '%s_part%s' % (title
, idx
)
291 entries
.append(entry
)
292 return self
.playlist_result(
293 entries
, post_id
, title
, strip_or_none(post
.get('plainBody')))
296 class VLiveChannelIE(VLiveBaseIE
):
297 IE_NAME
= 'vlive:channel'
298 _VALID_URL
= r
'https?://(?:channels\.vlive\.tv|(?:(?:www|m)\.)?vlive\.tv/channel)/(?P<id>[0-9A-Z]+)'
300 'url': 'http://channels.vlive.tv/FCD4B',
305 'playlist_mincount': 110
307 'url': 'https://www.vlive.tv/channel/FCD4B',
308 'only_matching': True,
311 def _call_api(self
, path
, channel_key_suffix
, channel_value
, note
, query
):
313 'app_id': self
._APP
_ID
,
314 'channel' + channel_key_suffix
: channel_value
,
317 return self
._download
_json
(
318 'http://api.vfan.vlive.tv/vproxy/channelplus/' + path
,
319 channel_value
, note
='Downloading ' + note
, query
=q
)['result']
321 def _real_extract(self
, url
):
322 channel_code
= self
._match
_id
(url
)
324 channel_seq
= self
._call
_api
(
325 'decodeChannelCode', 'Code', channel_code
,
326 'decode channel code', {})['channelSeq']
331 for page_num
in itertools
.count(1):
332 video_list
= self
._call
_api
(
333 'getChannelVideoList', 'Seq', channel_seq
,
334 'channel list page #%d' % page_num
, {
335 # Large values of maxNumOfRows (~300 or above) may cause
336 # empty responses (see [1]), e.g. this happens for [2] that
337 # has more than 300 videos.
338 # 1. https://github.com/ytdl-org/youtube-dl/issues/13830
339 # 2. http://channels.vlive.tv/EDBF.
346 channel_name
= try_get(
348 lambda x
: x
['channelInfo']['channelName'],
352 video_list
, lambda x
: x
['videoList'], list)
357 video_id
= video
.get('videoSeq')
358 video_type
= video
.get('videoType')
360 if not video_id
or not video_type
:
362 video_id
= compat_str(video_id
)
364 if video_type
in ('PLAYLIST'):
365 first_video_id
= try_get(
367 lambda x
: x
['videoPlaylist']['videoList'][0]['videoSeq'], int)
369 if not first_video_id
:
374 'http://www.vlive.tv/video/%s' % first_video_id
,
375 ie
=VLiveIE
.ie_key(), video_id
=first_video_id
))
379 'http://www.vlive.tv/video/%s' % video_id
,
380 ie
=VLiveIE
.ie_key(), video_id
=video_id
))
382 return self
.playlist_result(
383 entries
, channel_code
, channel_name
)