logo

youtube-dl

[mirror] Download/Watch videos from video hostersgit clone https://hacktivis.me/git/mirror/youtube-dl.git

vlive.py (11905B)


  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', 'platformType': 'PC'}
  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('utf-8'))['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. self._sort_formats(formats)
  141. info = get_common_fields()
  142. info.update({
  143. 'title': self._live_title(video['title']),
  144. 'id': video_id,
  145. 'formats': formats,
  146. 'is_live': True,
  147. })
  148. return info
  149. elif status == 'ENDED':
  150. raise ExtractorError(
  151. 'Uploading for replay. Please wait...', expected=True)
  152. elif status == 'RESERVED':
  153. raise ExtractorError('Coming soon!', expected=True)
  154. elif video.get('exposeStatus') == 'CANCEL':
  155. raise ExtractorError(
  156. 'We are sorry, but the live broadcast has been canceled.',
  157. expected=True)
  158. else:
  159. raise ExtractorError('Unknown status ' + status)
  160. class VLivePostIE(VLiveIE):
  161. IE_NAME = 'vlive:post'
  162. _VALID_URL = r'https?://(?:(?:www|m)\.)?vlive\.tv/post/(?P<id>\d-\d+)'
  163. _TESTS = [{
  164. # uploadType = SOS
  165. 'url': 'https://www.vlive.tv/post/1-20088044',
  166. 'info_dict': {
  167. 'id': '1-20088044',
  168. 'title': 'Hola estrellitas la tierra les dice hola (si era así no?) Ha...',
  169. 'description': 'md5:fab8a1e50e6e51608907f46c7fa4b407',
  170. },
  171. 'playlist_count': 3,
  172. }, {
  173. # uploadType = V
  174. 'url': 'https://www.vlive.tv/post/1-20087926',
  175. 'info_dict': {
  176. 'id': '1-20087926',
  177. 'title': 'James Corden: And so, the baby becamos the Papa💜😭💪😭',
  178. },
  179. 'playlist_count': 1,
  180. }]
  181. _FVIDEO_TMPL = 'fvideo/v1.0/fvideo-%%s/%s'
  182. _SOS_TMPL = _FVIDEO_TMPL % 'sosPlayInfo'
  183. _INKEY_TMPL = _FVIDEO_TMPL % 'inKey'
  184. def _real_extract(self, url):
  185. post_id = self._match_id(url)
  186. post = self._call_api(
  187. 'post/v1.0/post-%s', post_id,
  188. 'attachments{video},officialVideo{videoSeq},plainBody,title')
  189. video_seq = str_or_none(try_get(
  190. post, lambda x: x['officialVideo']['videoSeq']))
  191. if video_seq:
  192. return self.url_result(
  193. 'http://www.vlive.tv/video/' + video_seq,
  194. VLiveIE.ie_key(), video_seq)
  195. title = post['title']
  196. entries = []
  197. for idx, video in enumerate(post['attachments']['video'].values()):
  198. video_id = video.get('videoId')
  199. if not video_id:
  200. continue
  201. upload_type = video.get('uploadType')
  202. upload_info = video.get('uploadInfo') or {}
  203. entry = None
  204. if upload_type == 'SOS':
  205. download = self._call_api(
  206. self._SOS_TMPL, video_id)['videoUrl']['download']
  207. formats = []
  208. for f_id, f_url in download.items():
  209. formats.append({
  210. 'format_id': f_id,
  211. 'url': f_url,
  212. 'height': int_or_none(f_id[:-1]),
  213. })
  214. self._sort_formats(formats)
  215. entry = {
  216. 'formats': formats,
  217. 'id': video_id,
  218. 'thumbnail': upload_info.get('imageUrl'),
  219. }
  220. elif upload_type == 'V':
  221. vod_id = upload_info.get('videoId')
  222. if not vod_id:
  223. continue
  224. inkey = self._call_api(self._INKEY_TMPL, video_id)['inKey']
  225. entry = self._extract_video_info(video_id, vod_id, inkey)
  226. if entry:
  227. entry['title'] = '%s_part%s' % (title, idx)
  228. entries.append(entry)
  229. return self.playlist_result(
  230. entries, post_id, title, strip_or_none(post.get('plainBody')))
  231. class VLiveChannelIE(VLiveBaseIE):
  232. IE_NAME = 'vlive:channel'
  233. _VALID_URL = r'https?://(?:channels\.vlive\.tv|(?:(?:www|m)\.)?vlive\.tv/channel)/(?P<id>[0-9A-Z]+)'
  234. _TESTS = [{
  235. 'url': 'http://channels.vlive.tv/FCD4B',
  236. 'info_dict': {
  237. 'id': 'FCD4B',
  238. 'title': 'MAMAMOO',
  239. },
  240. 'playlist_mincount': 110
  241. }, {
  242. 'url': 'https://www.vlive.tv/channel/FCD4B',
  243. 'only_matching': True,
  244. }]
  245. def _call_api(self, path, channel_key_suffix, channel_value, note, query):
  246. q = {
  247. 'app_id': self._APP_ID,
  248. 'channel' + channel_key_suffix: channel_value,
  249. }
  250. q.update(query)
  251. return self._download_json(
  252. 'http://api.vfan.vlive.tv/vproxy/channelplus/' + path,
  253. channel_value, note='Downloading ' + note, query=q)['result']
  254. def _real_extract(self, url):
  255. channel_code = self._match_id(url)
  256. channel_seq = self._call_api(
  257. 'decodeChannelCode', 'Code', channel_code,
  258. 'decode channel code', {})['channelSeq']
  259. channel_name = None
  260. entries = []
  261. for page_num in itertools.count(1):
  262. video_list = self._call_api(
  263. 'getChannelVideoList', 'Seq', channel_seq,
  264. 'channel list page #%d' % page_num, {
  265. # Large values of maxNumOfRows (~300 or above) may cause
  266. # empty responses (see [1]), e.g. this happens for [2] that
  267. # has more than 300 videos.
  268. # 1. https://github.com/ytdl-org/youtube-dl/issues/13830
  269. # 2. http://channels.vlive.tv/EDBF.
  270. 'maxNumOfRows': 100,
  271. 'pageNo': page_num
  272. }
  273. )
  274. if not channel_name:
  275. channel_name = try_get(
  276. video_list,
  277. lambda x: x['channelInfo']['channelName'],
  278. compat_str)
  279. videos = try_get(
  280. video_list, lambda x: x['videoList'], list)
  281. if not videos:
  282. break
  283. for video in videos:
  284. video_id = video.get('videoSeq')
  285. if not video_id:
  286. continue
  287. video_id = compat_str(video_id)
  288. entries.append(
  289. self.url_result(
  290. 'http://www.vlive.tv/video/%s' % video_id,
  291. ie=VLiveIE.ie_key(), video_id=video_id))
  292. return self.playlist_result(
  293. entries, channel_code, channel_name)