vlive.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  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. try_get,
  15. urlencode_postdata,
  16. )
  17. class VLiveBaseIE(NaverBaseIE):
  18. _APP_ID = '8c6cc7b45d2568fb668be6e05b6e5a3b'
  19. class VLiveIE(VLiveBaseIE):
  20. IE_NAME = 'vlive'
  21. _VALID_URL = r'https?://(?:(?:www|m)\.)?vlive\.tv/(?:video|embed)/(?P<id>[0-9]+)'
  22. _NETRC_MACHINE = 'vlive'
  23. _TESTS = [{
  24. 'url': 'http://www.vlive.tv/video/1326',
  25. 'md5': 'cc7314812855ce56de70a06a27314983',
  26. 'info_dict': {
  27. 'id': '1326',
  28. 'ext': 'mp4',
  29. 'title': "Girl's Day's Broadcast",
  30. 'creator': "Girl's Day",
  31. 'view_count': int,
  32. 'uploader_id': 'muploader_a',
  33. },
  34. }, {
  35. 'url': 'http://www.vlive.tv/video/16937',
  36. 'info_dict': {
  37. 'id': '16937',
  38. 'ext': 'mp4',
  39. 'title': '첸백시 걍방',
  40. 'creator': 'EXO',
  41. 'view_count': int,
  42. 'subtitles': 'mincount:12',
  43. 'uploader_id': 'muploader_j',
  44. },
  45. 'params': {
  46. 'skip_download': True,
  47. },
  48. }, {
  49. 'url': 'https://www.vlive.tv/video/129100',
  50. 'md5': 'ca2569453b79d66e5b919e5d308bff6b',
  51. 'info_dict': {
  52. 'id': '129100',
  53. 'ext': 'mp4',
  54. 'title': '[V LIVE] [BTS+] Run BTS! 2019 - EP.71 :: Behind the scene',
  55. 'creator': 'BTS+',
  56. 'view_count': int,
  57. 'subtitles': 'mincount:10',
  58. },
  59. 'skip': 'This video is only available for CH+ subscribers',
  60. }, {
  61. 'url': 'https://www.vlive.tv/embed/1326',
  62. 'only_matching': True,
  63. }]
  64. def _real_initialize(self):
  65. self._login()
  66. def _login(self):
  67. email, password = self._get_login_info()
  68. if None in (email, password):
  69. return
  70. def is_logged_in():
  71. login_info = self._download_json(
  72. 'https://www.vlive.tv/auth/loginInfo', None,
  73. note='Downloading login info',
  74. headers={'Referer': 'https://www.vlive.tv/home'})
  75. return try_get(
  76. login_info, lambda x: x['message']['login'], bool) or False
  77. LOGIN_URL = 'https://www.vlive.tv/auth/email/login'
  78. self._request_webpage(
  79. LOGIN_URL, None, note='Downloading login cookies')
  80. self._download_webpage(
  81. LOGIN_URL, None, note='Logging in',
  82. data=urlencode_postdata({'email': email, 'pwd': password}),
  83. headers={
  84. 'Referer': LOGIN_URL,
  85. 'Content-Type': 'application/x-www-form-urlencoded'
  86. })
  87. if not is_logged_in():
  88. raise ExtractorError('Unable to log in', expected=True)
  89. def _call_api(self, path_template, video_id, fields=None):
  90. query = {'appId': self._APP_ID}
  91. if fields:
  92. query['fields'] = fields
  93. return self._download_json(
  94. 'https://www.vlive.tv/globalv-web/vam-web/' + path_template % video_id, video_id,
  95. 'Downloading %s JSON metadata' % path_template.split('/')[-1].split('-')[0],
  96. headers={'Referer': 'https://www.vlive.tv/'}, query=query)
  97. def _real_extract(self, url):
  98. video_id = self._match_id(url)
  99. try:
  100. post = self._call_api(
  101. 'post/v1.0/officialVideoPost-%s', video_id,
  102. 'author{nickname},channel{channelCode,channelName},officialVideo{commentCount,exposeStatus,likeCount,playCount,playTime,status,title,type,vodId}')
  103. except ExtractorError as e:
  104. if isinstance(e.cause, compat_HTTPError) and e.cause.code == 403:
  105. self.raise_login_required(json.loads(e.cause.read().decode())['message'])
  106. raise
  107. video = post['officialVideo']
  108. def get_common_fields():
  109. channel = post.get('channel') or {}
  110. return {
  111. 'title': video.get('title'),
  112. 'creator': post.get('author', {}).get('nickname'),
  113. 'channel': channel.get('channelName'),
  114. 'channel_id': channel.get('channelCode'),
  115. 'duration': int_or_none(video.get('playTime')),
  116. 'view_count': int_or_none(video.get('playCount')),
  117. 'like_count': int_or_none(video.get('likeCount')),
  118. 'comment_count': int_or_none(video.get('commentCount')),
  119. }
  120. video_type = video.get('type')
  121. if video_type == 'VOD':
  122. inkey = self._call_api('video/v1.0/vod/%s/inkey', video_id)['inkey']
  123. vod_id = video['vodId']
  124. return merge_dicts(
  125. get_common_fields(),
  126. self._extract_video_info(video_id, vod_id, inkey))
  127. elif video_type == 'LIVE':
  128. status = video.get('status')
  129. if status == 'ON_AIR':
  130. stream_url = self._call_api(
  131. 'old/v3/live/%s/playInfo',
  132. video_id)['result']['adaptiveStreamUrl']
  133. formats = self._extract_m3u8_formats(stream_url, video_id, 'mp4')
  134. info = get_common_fields()
  135. info.update({
  136. 'title': self._live_title(video['title']),
  137. 'id': video_id,
  138. 'formats': formats,
  139. 'is_live': True,
  140. })
  141. return info
  142. elif status == 'ENDED':
  143. raise ExtractorError(
  144. 'Uploading for replay. Please wait...', expected=True)
  145. elif status == 'RESERVED':
  146. raise ExtractorError('Coming soon!', expected=True)
  147. elif video.get('exposeStatus') == 'CANCEL':
  148. raise ExtractorError(
  149. 'We are sorry, but the live broadcast has been canceled.',
  150. expected=True)
  151. else:
  152. raise ExtractorError('Unknown status ' + status)
  153. class VLiveChannelIE(VLiveBaseIE):
  154. IE_NAME = 'vlive:channel'
  155. _VALID_URL = r'https?://(?:channels\.vlive\.tv|(?:(?:www|m)\.)?vlive\.tv/channel)/(?P<id>[0-9A-Z]+)'
  156. _TESTS = [{
  157. 'url': 'http://channels.vlive.tv/FCD4B',
  158. 'info_dict': {
  159. 'id': 'FCD4B',
  160. 'title': 'MAMAMOO',
  161. },
  162. 'playlist_mincount': 110
  163. }, {
  164. 'url': 'https://www.vlive.tv/channel/FCD4B',
  165. 'only_matching': True,
  166. }]
  167. def _call_api(self, path, channel_key_suffix, channel_value, note, query):
  168. q = {
  169. 'app_id': self._APP_ID,
  170. 'channel' + channel_key_suffix: channel_value,
  171. }
  172. q.update(query)
  173. return self._download_json(
  174. 'http://api.vfan.vlive.tv/vproxy/channelplus/' + path,
  175. channel_value, note='Downloading ' + note, query=q)['result']
  176. def _real_extract(self, url):
  177. channel_code = self._match_id(url)
  178. channel_seq = self._call_api(
  179. 'decodeChannelCode', 'Code', channel_code,
  180. 'decode channel code', {})['channelSeq']
  181. channel_name = None
  182. entries = []
  183. for page_num in itertools.count(1):
  184. video_list = self._call_api(
  185. 'getChannelVideoList', 'Seq', channel_seq,
  186. 'channel list page #%d' % page_num, {
  187. # Large values of maxNumOfRows (~300 or above) may cause
  188. # empty responses (see [1]), e.g. this happens for [2] that
  189. # has more than 300 videos.
  190. # 1. https://github.com/ytdl-org/youtube-dl/issues/13830
  191. # 2. http://channels.vlive.tv/EDBF.
  192. 'maxNumOfRows': 100,
  193. 'pageNo': page_num
  194. }
  195. )
  196. if not channel_name:
  197. channel_name = try_get(
  198. video_list,
  199. lambda x: x['channelInfo']['channelName'],
  200. compat_str)
  201. videos = try_get(
  202. video_list, lambda x: x['videoList'], list)
  203. if not videos:
  204. break
  205. for video in videos:
  206. video_id = video.get('videoSeq')
  207. if not video_id:
  208. continue
  209. video_id = compat_str(video_id)
  210. entries.append(
  211. self.url_result(
  212. 'http://www.vlive.tv/video/%s' % video_id,
  213. ie=VLiveIE.ie_key(), video_id=video_id))
  214. return self.playlist_result(
  215. entries, channel_code, channel_name)