You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

349 lines
12 KiB

  1. # coding: utf-8
  2. from __future__ import unicode_literals
  3. import itertools
  4. import json
  5. from .naver import NaverBaseIE
  6. from ..compat import (
  7. compat_HTTPError,
  8. compat_str,
  9. )
  10. from ..utils import (
  11. ExtractorError,
  12. int_or_none,
  13. merge_dicts,
  14. str_or_none,
  15. strip_or_none,
  16. try_get,
  17. urlencode_postdata,
  18. )
  19. class VLiveBaseIE(NaverBaseIE):
  20. _APP_ID = '8c6cc7b45d2568fb668be6e05b6e5a3b'
  21. class VLiveIE(VLiveBaseIE):
  22. IE_NAME = 'vlive'
  23. _VALID_URL = r'https?://(?:(?:www|m)\.)?vlive\.tv/(?:video|embed)/(?P<id>[0-9]+)'
  24. _NETRC_MACHINE = 'vlive'
  25. _TESTS = [{
  26. 'url': 'http://www.vlive.tv/video/1326',
  27. 'md5': 'cc7314812855ce56de70a06a27314983',
  28. 'info_dict': {
  29. 'id': '1326',
  30. 'ext': 'mp4',
  31. 'title': "Girl's Day's Broadcast",
  32. 'creator': "Girl's Day",
  33. 'view_count': int,
  34. 'uploader_id': 'muploader_a',
  35. },
  36. }, {
  37. 'url': 'http://www.vlive.tv/video/16937',
  38. 'info_dict': {
  39. 'id': '16937',
  40. 'ext': 'mp4',
  41. 'title': '첸백시 걍방',
  42. 'creator': 'EXO',
  43. 'view_count': int,
  44. 'subtitles': 'mincount:12',
  45. 'uploader_id': 'muploader_j',
  46. },
  47. 'params': {
  48. 'skip_download': True,
  49. },
  50. }, {
  51. 'url': 'https://www.vlive.tv/video/129100',
  52. 'md5': 'ca2569453b79d66e5b919e5d308bff6b',
  53. 'info_dict': {
  54. 'id': '129100',
  55. 'ext': 'mp4',
  56. 'title': '[V LIVE] [BTS+] Run BTS! 2019 - EP.71 :: Behind the scene',
  57. 'creator': 'BTS+',
  58. 'view_count': int,
  59. 'subtitles': 'mincount:10',
  60. },
  61. 'skip': 'This video is only available for CH+ subscribers',
  62. }, {
  63. 'url': 'https://www.vlive.tv/embed/1326',
  64. 'only_matching': True,
  65. }, {
  66. # works only with gcc=KR
  67. 'url': 'https://www.vlive.tv/video/225019',
  68. 'only_matching': True,
  69. }]
  70. def _real_initialize(self):
  71. self._login()
  72. def _login(self):
  73. email, password = self._get_login_info()
  74. if None in (email, password):
  75. return
  76. def is_logged_in():
  77. login_info = self._download_json(
  78. 'https://www.vlive.tv/auth/loginInfo', None,
  79. note='Downloading login info',
  80. headers={'Referer': 'https://www.vlive.tv/home'})
  81. return try_get(
  82. login_info, lambda x: x['message']['login'], bool) or False
  83. LOGIN_URL = 'https://www.vlive.tv/auth/email/login'
  84. self._request_webpage(
  85. LOGIN_URL, None, note='Downloading login cookies')
  86. self._download_webpage(
  87. LOGIN_URL, None, note='Logging in',
  88. data=urlencode_postdata({'email': email, 'pwd': password}),
  89. headers={
  90. 'Referer': LOGIN_URL,
  91. 'Content-Type': 'application/x-www-form-urlencoded'
  92. })
  93. if not is_logged_in():
  94. raise ExtractorError('Unable to log in', expected=True)
  95. def _call_api(self, path_template, video_id, fields=None):
  96. query = {'appId': self._APP_ID, 'gcc': 'KR'}
  97. if fields:
  98. query['fields'] = fields
  99. try:
  100. return self._download_json(
  101. 'https://www.vlive.tv/globalv-web/vam-web/' + path_template % video_id, video_id,
  102. 'Downloading %s JSON metadata' % path_template.split('/')[-1].split('-')[0],
  103. headers={'Referer': 'https://www.vlive.tv/'}, query=query)
  104. except ExtractorError as e:
  105. if isinstance(e.cause, compat_HTTPError) and e.cause.code == 403:
  106. self.raise_login_required(json.loads(e.cause.read().decode())['message'])
  107. raise
  108. def _real_extract(self, url):
  109. video_id = self._match_id(url)
  110. post = self._call_api(
  111. 'post/v1.0/officialVideoPost-%s', video_id,
  112. 'author{nickname},channel{channelCode,channelName},officialVideo{commentCount,exposeStatus,likeCount,playCount,playTime,status,title,type,vodId}')
  113. video = post['officialVideo']
  114. def get_common_fields():
  115. channel = post.get('channel') or {}
  116. return {
  117. 'title': video.get('title'),
  118. 'creator': post.get('author', {}).get('nickname'),
  119. 'channel': channel.get('channelName'),
  120. 'channel_id': channel.get('channelCode'),
  121. 'duration': int_or_none(video.get('playTime')),
  122. 'view_count': int_or_none(video.get('playCount')),
  123. 'like_count': int_or_none(video.get('likeCount')),
  124. 'comment_count': int_or_none(video.get('commentCount')),
  125. }
  126. video_type = video.get('type')
  127. if video_type == 'VOD':
  128. inkey = self._call_api('video/v1.0/vod/%s/inkey', video_id)['inkey']
  129. vod_id = video['vodId']
  130. return merge_dicts(
  131. get_common_fields(),
  132. self._extract_video_info(video_id, vod_id, inkey))
  133. elif video_type == 'LIVE':
  134. status = video.get('status')
  135. if status == 'ON_AIR':
  136. stream_url = self._call_api(
  137. 'old/v3/live/%s/playInfo',
  138. video_id)['result']['adaptiveStreamUrl']
  139. formats = self._extract_m3u8_formats(stream_url, video_id, 'mp4')
  140. info = get_common_fields()
  141. info.update({
  142. 'title': self._live_title(video['title']),
  143. 'id': video_id,
  144. 'formats': formats,
  145. 'is_live': True,
  146. })
  147. return info
  148. elif status == 'ENDED':
  149. raise ExtractorError(
  150. 'Uploading for replay. Please wait...', expected=True)
  151. elif status == 'RESERVED':
  152. raise ExtractorError('Coming soon!', expected=True)
  153. elif video.get('exposeStatus') == 'CANCEL':
  154. raise ExtractorError(
  155. 'We are sorry, but the live broadcast has been canceled.',
  156. expected=True)
  157. else:
  158. raise ExtractorError('Unknown status ' + status)
  159. class VLivePostIE(VLiveIE):
  160. IE_NAME = 'vlive:post'
  161. _VALID_URL = r'https?://(?:(?:www|m)\.)?vlive\.tv/post/(?P<id>\d-\d+)'
  162. _TESTS = [{
  163. # uploadType = SOS
  164. 'url': 'https://www.vlive.tv/post/1-20088044',
  165. 'info_dict': {
  166. 'id': '1-20088044',
  167. 'title': 'Hola estrellitas la tierra les dice hola (si era así no?) Ha...',
  168. 'description': 'md5:fab8a1e50e6e51608907f46c7fa4b407',
  169. },
  170. 'playlist_count': 3,
  171. }, {
  172. # uploadType = V
  173. 'url': 'https://www.vlive.tv/post/1-20087926',
  174. 'info_dict': {
  175. 'id': '1-20087926',
  176. 'title': 'James Corden: And so, the baby becamos the Papa💜😭💪😭',
  177. },
  178. 'playlist_count': 1,
  179. }]
  180. _FVIDEO_TMPL = 'fvideo/v1.0/fvideo-%%s/%s'
  181. _SOS_TMPL = _FVIDEO_TMPL % 'sosPlayInfo'
  182. _INKEY_TMPL = _FVIDEO_TMPL % 'inKey'
  183. def _real_extract(self, url):
  184. post_id = self._match_id(url)
  185. post = self._call_api(
  186. 'post/v1.0/post-%s', post_id,
  187. 'attachments{video},officialVideo{videoSeq},plainBody,title')
  188. video_seq = str_or_none(try_get(
  189. post, lambda x: x['officialVideo']['videoSeq']))
  190. if video_seq:
  191. return self.url_result(
  192. 'http://www.vlive.tv/video/' + video_seq,
  193. VLiveIE.ie_key(), video_seq)
  194. title = post['title']
  195. entries = []
  196. for idx, video in enumerate(post['attachments']['video'].values()):
  197. video_id = video.get('videoId')
  198. if not video_id:
  199. continue
  200. upload_type = video.get('uploadType')
  201. upload_info = video.get('uploadInfo') or {}
  202. entry = None
  203. if upload_type == 'SOS':
  204. download = self._call_api(
  205. self._SOS_TMPL, video_id)['videoUrl']['download']
  206. formats = []
  207. for f_id, f_url in download.items():
  208. formats.append({
  209. 'format_id': f_id,
  210. 'url': f_url,
  211. 'height': int_or_none(f_id[:-1]),
  212. })
  213. self._sort_formats(formats)
  214. entry = {
  215. 'formats': formats,
  216. 'id': video_id,
  217. 'thumbnail': upload_info.get('imageUrl'),
  218. }
  219. elif upload_type == 'V':
  220. vod_id = upload_info.get('videoId')
  221. if not vod_id:
  222. continue
  223. inkey = self._call_api(self._INKEY_TMPL, video_id)['inKey']
  224. entry = self._extract_video_info(video_id, vod_id, inkey)
  225. if entry:
  226. entry['title'] = '%s_part%s' % (title, idx)
  227. entries.append(entry)
  228. return self.playlist_result(
  229. entries, post_id, title, strip_or_none(post.get('plainBody')))
  230. class VLiveChannelIE(VLiveBaseIE):
  231. IE_NAME = 'vlive:channel'
  232. _VALID_URL = r'https?://(?:channels\.vlive\.tv|(?:(?:www|m)\.)?vlive\.tv/channel)/(?P<id>[0-9A-Z]+)'
  233. _TESTS = [{
  234. 'url': 'http://channels.vlive.tv/FCD4B',
  235. 'info_dict': {
  236. 'id': 'FCD4B',
  237. 'title': 'MAMAMOO',
  238. },
  239. 'playlist_mincount': 110
  240. }, {
  241. 'url': 'https://www.vlive.tv/channel/FCD4B',
  242. 'only_matching': True,
  243. }]
  244. def _call_api(self, path, channel_key_suffix, channel_value, note, query):
  245. q = {
  246. 'app_id': self._APP_ID,
  247. 'channel' + channel_key_suffix: channel_value,
  248. }
  249. q.update(query)
  250. return self._download_json(
  251. 'http://api.vfan.vlive.tv/vproxy/channelplus/' + path,
  252. channel_value, note='Downloading ' + note, query=q)['result']
  253. def _real_extract(self, url):
  254. channel_code = self._match_id(url)
  255. channel_seq = self._call_api(
  256. 'decodeChannelCode', 'Code', channel_code,
  257. 'decode channel code', {})['channelSeq']
  258. channel_name = None
  259. entries = []
  260. for page_num in itertools.count(1):
  261. video_list = self._call_api(
  262. 'getChannelVideoList', 'Seq', channel_seq,
  263. 'channel list page #%d' % page_num, {
  264. # Large values of maxNumOfRows (~300 or above) may cause
  265. # empty responses (see [1]), e.g. this happens for [2] that
  266. # has more than 300 videos.
  267. # 1. https://github.com/ytdl-org/youtube-dl/issues/13830
  268. # 2. http://channels.vlive.tv/EDBF.
  269. 'maxNumOfRows': 100,
  270. 'pageNo': page_num
  271. }
  272. )
  273. if not channel_name:
  274. channel_name = try_get(
  275. video_list,
  276. lambda x: x['channelInfo']['channelName'],
  277. compat_str)
  278. videos = try_get(
  279. video_list, lambda x: x['videoList'], list)
  280. if not videos:
  281. break
  282. for video in videos:
  283. video_id = video.get('videoSeq')
  284. video_type = video.get('videoType')
  285. if not video_id or not video_type:
  286. continue
  287. video_id = compat_str(video_id)
  288. if video_type in ('PLAYLIST'):
  289. playlist_videos = try_get(
  290. video,
  291. lambda x: x['videoPlaylist']['videoList'], list)
  292. if not playlist_videos:
  293. continue
  294. for playlist_video in playlist_videos:
  295. playlist_video_id = playlist_video.get('videoSeq')
  296. if not playlist_video_id:
  297. continue
  298. playlist_video_id = compat_str(playlist_video_id)
  299. entries.append(
  300. self.url_result(
  301. 'http://www.vlive.tv/video/%s' % playlist_video_id,
  302. ie=VLiveIE.ie_key(), video_id=playlist_video_id))
  303. else:
  304. entries.append(
  305. self.url_result(
  306. 'http://www.vlive.tv/video/%s' % video_id,
  307. ie=VLiveIE.ie_key(), video_id=video_id))
  308. return self.playlist_result(
  309. entries, channel_code, channel_name)