weibo.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. import itertools
  2. import json
  3. import random
  4. import urllib.parse
  5. from .common import InfoExtractor
  6. from ..utils import (
  7. int_or_none,
  8. make_archive_id,
  9. mimetype2ext,
  10. parse_resolution,
  11. str_or_none,
  12. strip_jsonp,
  13. traverse_obj,
  14. url_or_none,
  15. urlencode_postdata,
  16. urljoin,
  17. )
  18. class WeiboBaseIE(InfoExtractor):
  19. def _update_visitor_cookies(self, visitor_url, video_id):
  20. headers = {'Referer': visitor_url}
  21. chrome_ver = self._search_regex(
  22. r'Chrome/(\d+)', self.get_param('http_headers')['User-Agent'], 'user agent version', default='90')
  23. visitor_data = self._download_json(
  24. 'https://passport.weibo.com/visitor/genvisitor', video_id,
  25. note='Generating first-visit guest request',
  26. headers=headers, transform_source=strip_jsonp,
  27. data=urlencode_postdata({
  28. 'cb': 'gen_callback',
  29. 'fp': json.dumps({
  30. 'os': '1',
  31. 'browser': f'Chrome{chrome_ver},0,0,0',
  32. 'fonts': 'undefined',
  33. 'screenInfo': '1920*1080*24',
  34. 'plugins': '',
  35. }, separators=(',', ':'))}))['data']
  36. self._download_webpage(
  37. 'https://passport.weibo.com/visitor/visitor', video_id,
  38. note='Running first-visit callback to get guest cookies',
  39. headers=headers, query={
  40. 'a': 'incarnate',
  41. 't': visitor_data['tid'],
  42. 'w': 3 if visitor_data.get('new_tid') else 2,
  43. 'c': f'{visitor_data.get("confidence", 100):03d}',
  44. 'gc': '',
  45. 'cb': 'cross_domain',
  46. 'from': 'weibo',
  47. '_rand': random.random(),
  48. })
  49. def _weibo_download_json(self, url, video_id, *args, fatal=True, note='Downloading JSON metadata', **kwargs):
  50. # XXX: Always fatal; _download_webpage_handle only returns False (not a tuple) on error
  51. webpage, urlh = self._download_webpage_handle(url, video_id, *args, fatal=fatal, note=note, **kwargs)
  52. if urllib.parse.urlparse(urlh.url).netloc == 'passport.weibo.com':
  53. self._update_visitor_cookies(urlh.url, video_id)
  54. webpage = self._download_webpage(url, video_id, *args, fatal=fatal, note=note, **kwargs)
  55. return self._parse_json(webpage, video_id, fatal=fatal)
  56. def _extract_formats(self, video_info):
  57. media_info = traverse_obj(video_info, ('page_info', 'media_info'))
  58. formats = traverse_obj(media_info, (
  59. 'playback_list', lambda _, v: url_or_none(v['play_info']['url']), 'play_info', {
  60. 'url': 'url',
  61. 'format': ('quality_desc', {str}),
  62. 'format_id': ('label', {str}),
  63. 'ext': ('mime', {mimetype2ext}),
  64. 'tbr': ('bitrate', {int_or_none}, {lambda x: x or None}),
  65. 'vcodec': ('video_codecs', {str}),
  66. 'fps': ('fps', {int_or_none}),
  67. 'width': ('width', {int_or_none}),
  68. 'height': ('height', {int_or_none}),
  69. 'filesize': ('size', {int_or_none}),
  70. 'acodec': ('audio_codecs', {str}),
  71. 'asr': ('audio_sample_rate', {int_or_none}),
  72. 'audio_channels': ('audio_channels', {int_or_none}),
  73. }))
  74. if not formats: # fallback, should be barely used
  75. for url in set(traverse_obj(media_info, (..., {url_or_none}))):
  76. if 'label=' in url: # filter out non-video urls
  77. format_id, resolution = self._search_regex(
  78. r'label=(\w+)&template=(\d+x\d+)', url, 'format info',
  79. group=(1, 2), default=(None, None))
  80. formats.append({
  81. 'url': url,
  82. 'format_id': format_id,
  83. **parse_resolution(resolution),
  84. **traverse_obj(media_info, (
  85. 'video_details', lambda _, v: v['label'].startswith(format_id), {
  86. 'size': ('size', {int_or_none}),
  87. 'tbr': ('bitrate', {int_or_none}),
  88. },
  89. ), get_all=False),
  90. })
  91. return formats
  92. def _parse_video_info(self, video_info, video_id=None):
  93. return {
  94. 'id': video_id,
  95. 'extractor_key': WeiboIE.ie_key(),
  96. 'extractor': WeiboIE.IE_NAME,
  97. 'formats': self._extract_formats(video_info),
  98. 'http_headers': {'Referer': 'https://weibo.com/'},
  99. '_old_archive_ids': [make_archive_id('WeiboMobile', video_id)],
  100. **traverse_obj(video_info, {
  101. 'id': (('id', 'id_str', 'mid'), {str_or_none}),
  102. 'display_id': ('mblogid', {str_or_none}),
  103. 'title': ('page_info', 'media_info', ('video_title', 'kol_title', 'name'), {str}, {lambda x: x or None}),
  104. 'description': ('text_raw', {str}),
  105. 'duration': ('page_info', 'media_info', 'duration', {int_or_none}),
  106. 'timestamp': ('page_info', 'media_info', 'video_publish_time', {int_or_none}),
  107. 'thumbnail': ('page_info', 'page_pic', {url_or_none}),
  108. 'uploader': ('user', 'screen_name', {str}),
  109. 'uploader_id': ('user', ('id', 'id_str'), {str_or_none}),
  110. 'uploader_url': ('user', 'profile_url', {lambda x: urljoin('https://weibo.com/', x)}),
  111. 'view_count': ('page_info', 'media_info', 'online_users_number', {int_or_none}),
  112. 'like_count': ('attitudes_count', {int_or_none}),
  113. 'repost_count': ('reposts_count', {int_or_none}),
  114. }, get_all=False),
  115. 'tags': traverse_obj(video_info, ('topic_struct', ..., 'topic_title', {str})) or None,
  116. }
  117. class WeiboIE(WeiboBaseIE):
  118. _VALID_URL = r'https?://(?:m\.weibo\.cn/status|(?:www\.)?weibo\.com/\d+)/(?P<id>[a-zA-Z0-9]+)'
  119. _TESTS = [{
  120. 'url': 'https://weibo.com/7827771738/N4xlMvjhI',
  121. 'info_dict': {
  122. 'id': '4910815147462302',
  123. 'ext': 'mp4',
  124. 'display_id': 'N4xlMvjhI',
  125. 'title': '【睡前消息暑假版第一期:拉泰国一把 对中国有好处】',
  126. 'description': 'md5:e2637a7673980d68694ea7c43cf12a5f',
  127. 'duration': 918,
  128. 'timestamp': 1686312819,
  129. 'upload_date': '20230609',
  130. 'thumbnail': r're:https://.*\.jpg',
  131. 'uploader': '睡前视频基地',
  132. 'uploader_id': '7827771738',
  133. 'uploader_url': 'https://weibo.com/u/7827771738',
  134. 'view_count': int,
  135. 'like_count': int,
  136. 'repost_count': int,
  137. 'tags': ['泰国大选远进党获胜', '睡前消息', '暑期版'],
  138. },
  139. }, {
  140. 'url': 'https://m.weibo.cn/status/4189191225395228',
  141. 'info_dict': {
  142. 'id': '4189191225395228',
  143. 'ext': 'mp4',
  144. 'display_id': 'FBqgOmDxO',
  145. 'title': '柴犬柴犬的秒拍视频',
  146. 'description': 'md5:80f461ab5cdae6bbdb70efbf5a1db24f',
  147. 'duration': 53,
  148. 'timestamp': 1514264429,
  149. 'upload_date': '20171226',
  150. 'thumbnail': r're:https://.*\.jpg',
  151. 'uploader': '柴犬柴犬',
  152. 'uploader_id': '5926682210',
  153. 'uploader_url': 'https://weibo.com/u/5926682210',
  154. 'view_count': int,
  155. 'like_count': int,
  156. 'repost_count': int,
  157. },
  158. }, {
  159. 'url': 'https://weibo.com/0/4224132150961381',
  160. 'note': 'no playback_list example',
  161. 'only_matching': True,
  162. }]
  163. def _real_extract(self, url):
  164. video_id = self._match_id(url)
  165. return self._parse_video_info(self._weibo_download_json(
  166. f'https://weibo.com/ajax/statuses/show?id={video_id}', video_id))
  167. class WeiboVideoIE(WeiboBaseIE):
  168. _VALID_URL = r'https?://(?:www\.)?weibo\.com/tv/show/(?P<id>\d+:\d+)'
  169. _TESTS = [{
  170. 'url': 'https://weibo.com/tv/show/1034:4797699866951785?from=old_pc_videoshow',
  171. 'info_dict': {
  172. 'id': '4797700463137878',
  173. 'ext': 'mp4',
  174. 'display_id': 'LEZDodaiW',
  175. 'title': '呃,稍微了解了一下靡烟miya,感觉这东西也太二了',
  176. 'description': '呃,稍微了解了一下靡烟miya,感觉这东西也太二了 http://t.cn/A6aerGsM \u200b\u200b\u200b',
  177. 'duration': 76,
  178. 'timestamp': 1659344278,
  179. 'upload_date': '20220801',
  180. 'thumbnail': r're:https://.*\.jpg',
  181. 'uploader': '君子爱财陈平安',
  182. 'uploader_id': '3905382233',
  183. 'uploader_url': 'https://weibo.com/u/3905382233',
  184. 'view_count': int,
  185. 'like_count': int,
  186. 'repost_count': int,
  187. },
  188. }]
  189. def _real_extract(self, url):
  190. video_id = self._match_id(url)
  191. post_data = f'data={{"Component_Play_Playinfo":{{"oid":"{video_id}"}}}}'.encode()
  192. video_info = self._weibo_download_json(
  193. f'https://weibo.com/tv/api/component?page=%2Ftv%2Fshow%2F{video_id.replace(":", "%3A")}',
  194. video_id, headers={'Referer': url}, data=post_data)['data']['Component_Play_Playinfo']
  195. return self.url_result(f'https://weibo.com/0/{video_info["mid"]}', WeiboIE)
  196. class WeiboUserIE(WeiboBaseIE):
  197. _VALID_URL = r'https?://(?:www\.)?weibo\.com/u/(?P<id>\d+)'
  198. _TESTS = [{
  199. 'url': 'https://weibo.com/u/2066652961?tabtype=video',
  200. 'info_dict': {
  201. 'id': '2066652961',
  202. 'title': '萧影殿下的视频',
  203. 'description': '萧影殿下的全部视频',
  204. 'uploader': '萧影殿下',
  205. },
  206. 'playlist_mincount': 195,
  207. }]
  208. def _fetch_page(self, uid, cursor=0, page=1):
  209. return self._weibo_download_json(
  210. 'https://weibo.com/ajax/profile/getWaterFallContent',
  211. uid, note=f'Downloading videos page {page}',
  212. query={'uid': uid, 'cursor': cursor})['data']
  213. def _entries(self, uid, first_page):
  214. cursor = 0
  215. for page in itertools.count(1):
  216. response = first_page if page == 1 else self._fetch_page(uid, cursor, page)
  217. for video_info in traverse_obj(response, ('list', ..., {dict})):
  218. yield self._parse_video_info(video_info)
  219. cursor = response.get('next_cursor')
  220. if (int_or_none(cursor) or -1) < 0:
  221. break
  222. def _real_extract(self, url):
  223. uid = self._match_id(url)
  224. first_page = self._fetch_page(uid)
  225. uploader = traverse_obj(first_page, ('list', ..., 'user', 'screen_name', {str}), get_all=False)
  226. metainfo = {
  227. 'title': f'{uploader}的视频',
  228. 'description': f'{uploader}的全部视频',
  229. 'uploader': uploader,
  230. } if uploader else {}
  231. return self.playlist_result(self._entries(uid, first_page), uid, **metainfo)