mildom.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. import functools
  2. import json
  3. import uuid
  4. from .common import InfoExtractor
  5. from ..utils import (
  6. ExtractorError,
  7. OnDemandPagedList,
  8. determine_ext,
  9. dict_get,
  10. float_or_none,
  11. traverse_obj,
  12. )
  13. class MildomBaseIE(InfoExtractor):
  14. _GUEST_ID = None
  15. def _call_api(self, url, video_id, query=None, note='Downloading JSON metadata', body=None):
  16. if not self._GUEST_ID:
  17. self._GUEST_ID = f'pc-gp-{uuid.uuid4()}'
  18. content = self._download_json(
  19. url, video_id, note=note, data=json.dumps(body).encode() if body else None,
  20. headers={'Content-Type': 'application/json'} if body else {},
  21. query={
  22. '__guest_id': self._GUEST_ID,
  23. '__platform': 'web',
  24. **(query or {}),
  25. })
  26. if content['code'] != 0:
  27. raise ExtractorError(
  28. f'Mildom says: {content["message"]} (code {content["code"]})',
  29. expected=True)
  30. return content['body']
  31. class MildomIE(MildomBaseIE):
  32. IE_NAME = 'mildom'
  33. IE_DESC = 'Record ongoing live by specific user in Mildom'
  34. _VALID_URL = r'https?://(?:(?:www|m)\.)mildom\.com/(?P<id>\d+)'
  35. def _real_extract(self, url):
  36. video_id = self._match_id(url)
  37. webpage = self._download_webpage(f'https://www.mildom.com/{video_id}', video_id)
  38. enterstudio = self._call_api(
  39. 'https://cloudac.mildom.com/nonolive/gappserv/live/enterstudio', video_id,
  40. note='Downloading live metadata', query={'user_id': video_id})
  41. result_video_id = enterstudio.get('log_id', video_id)
  42. servers = self._call_api(
  43. 'https://cloudac.mildom.com/nonolive/gappserv/live/liveserver', result_video_id,
  44. note='Downloading live server list', query={
  45. 'user_id': video_id,
  46. 'live_server_type': 'hls',
  47. })
  48. playback_token = self._call_api(
  49. 'https://cloudac.mildom.com/nonolive/gappserv/live/token', result_video_id,
  50. note='Obtaining live playback token', body={'host_id': video_id, 'type': 'hls'})
  51. playback_token = traverse_obj(playback_token, ('data', ..., 'token'), get_all=False)
  52. if not playback_token:
  53. raise ExtractorError('Failed to obtain live playback token')
  54. formats = self._extract_m3u8_formats(
  55. f'{servers["stream_server"]}/{video_id}_master.m3u8?{playback_token}',
  56. result_video_id, 'mp4', headers={
  57. 'Referer': 'https://www.mildom.com/',
  58. 'Origin': 'https://www.mildom.com',
  59. })
  60. for fmt in formats:
  61. fmt.setdefault('http_headers', {})['Referer'] = 'https://www.mildom.com/'
  62. return {
  63. 'id': result_video_id,
  64. 'title': self._html_search_meta('twitter:description', webpage, default=None) or traverse_obj(enterstudio, 'anchor_intro'),
  65. 'description': traverse_obj(enterstudio, 'intro', 'live_intro', expected_type=str),
  66. 'timestamp': float_or_none(enterstudio.get('live_start_ms'), scale=1000),
  67. 'uploader': self._html_search_meta('twitter:title', webpage, default=None) or traverse_obj(enterstudio, 'loginname'),
  68. 'uploader_id': video_id,
  69. 'formats': formats,
  70. 'is_live': True,
  71. }
  72. class MildomVodIE(MildomBaseIE):
  73. IE_NAME = 'mildom:vod'
  74. IE_DESC = 'VOD in Mildom'
  75. _VALID_URL = r'https?://(?:(?:www|m)\.)mildom\.com/playback/(?P<user_id>\d+)/(?P<id>(?P=user_id)-[a-zA-Z0-9]+-?[0-9]*)'
  76. _TESTS = [{
  77. 'url': 'https://www.mildom.com/playback/10882672/10882672-1597662269',
  78. 'info_dict': {
  79. 'id': '10882672-1597662269',
  80. 'ext': 'mp4',
  81. 'title': '始めてのミルダム配信じゃぃ!',
  82. 'thumbnail': r're:^https?://.*\.(png|jpg)$',
  83. 'upload_date': '20200817',
  84. 'duration': 4138.37,
  85. 'description': 'ゲームをしたくて!',
  86. 'timestamp': 1597662269.0,
  87. 'uploader_id': '10882672',
  88. 'uploader': 'kson組長(けいそん)',
  89. },
  90. }, {
  91. 'url': 'https://www.mildom.com/playback/10882672/10882672-1597758589870-477',
  92. 'info_dict': {
  93. 'id': '10882672-1597758589870-477',
  94. 'ext': 'mp4',
  95. 'title': '【kson】感染メイズ!麻酔銃で無双する',
  96. 'thumbnail': r're:^https?://.*\.(png|jpg)$',
  97. 'timestamp': 1597759093.0,
  98. 'uploader': 'kson組長(けいそん)',
  99. 'duration': 4302.58,
  100. 'uploader_id': '10882672',
  101. 'description': 'このステージ絶対乗り越えたい',
  102. 'upload_date': '20200818',
  103. },
  104. }, {
  105. 'url': 'https://www.mildom.com/playback/10882672/10882672-buha9td2lrn97fk2jme0',
  106. 'info_dict': {
  107. 'id': '10882672-buha9td2lrn97fk2jme0',
  108. 'ext': 'mp4',
  109. 'title': '【kson組長】CART RACER!!!',
  110. 'thumbnail': r're:^https?://.*\.(png|jpg)$',
  111. 'uploader_id': '10882672',
  112. 'uploader': 'kson組長(けいそん)',
  113. 'upload_date': '20201104',
  114. 'timestamp': 1604494797.0,
  115. 'duration': 4657.25,
  116. 'description': 'WTF',
  117. },
  118. }]
  119. def _real_extract(self, url):
  120. user_id, video_id = self._match_valid_url(url).group('user_id', 'id')
  121. webpage = self._download_webpage(f'https://www.mildom.com/playback/{user_id}/{video_id}', video_id)
  122. autoplay = self._call_api(
  123. 'https://cloudac.mildom.com/nonolive/videocontent/playback/getPlaybackDetail', video_id,
  124. note='Downloading playback metadata', query={
  125. 'v_id': video_id,
  126. })['playback']
  127. formats = [{
  128. 'url': autoplay['audio_url'],
  129. 'format_id': 'audio',
  130. 'protocol': 'm3u8_native',
  131. 'vcodec': 'none',
  132. 'acodec': 'aac',
  133. 'ext': 'm4a',
  134. }]
  135. for fmt in autoplay['video_link']:
  136. formats.append({
  137. 'format_id': 'video-{}'.format(fmt['name']),
  138. 'url': fmt['url'],
  139. 'protocol': 'm3u8_native',
  140. 'width': fmt['level'] * autoplay['video_width'] // autoplay['video_height'],
  141. 'height': fmt['level'],
  142. 'vcodec': 'h264',
  143. 'acodec': 'aac',
  144. 'ext': 'mp4',
  145. })
  146. return {
  147. 'id': video_id,
  148. 'title': self._html_search_meta(('og:description', 'description'), webpage, default=None) or autoplay.get('title'),
  149. 'description': traverse_obj(autoplay, 'video_intro'),
  150. 'timestamp': float_or_none(autoplay.get('publish_time'), scale=1000),
  151. 'duration': float_or_none(autoplay.get('video_length'), scale=1000),
  152. 'thumbnail': dict_get(autoplay, ('upload_pic', 'video_pic')),
  153. 'uploader': traverse_obj(autoplay, ('author_info', 'login_name')),
  154. 'uploader_id': user_id,
  155. 'formats': formats,
  156. }
  157. class MildomClipIE(MildomBaseIE):
  158. IE_NAME = 'mildom:clip'
  159. IE_DESC = 'Clip in Mildom'
  160. _VALID_URL = r'https?://(?:(?:www|m)\.)mildom\.com/clip/(?P<id>(?P<user_id>\d+)-[a-zA-Z0-9]+)'
  161. _TESTS = [{
  162. 'url': 'https://www.mildom.com/clip/10042245-63921673e7b147ebb0806d42b5ba5ce9',
  163. 'info_dict': {
  164. 'id': '10042245-63921673e7b147ebb0806d42b5ba5ce9',
  165. 'title': '全然違ったよ',
  166. 'timestamp': 1619181890,
  167. 'duration': 59,
  168. 'thumbnail': r're:https?://.+',
  169. 'uploader': 'ざきんぽ',
  170. 'uploader_id': '10042245',
  171. },
  172. }, {
  173. 'url': 'https://www.mildom.com/clip/10111524-ebf4036e5aa8411c99fb3a1ae0902864',
  174. 'info_dict': {
  175. 'id': '10111524-ebf4036e5aa8411c99fb3a1ae0902864',
  176. 'title': 'かっこいい',
  177. 'timestamp': 1621094003,
  178. 'duration': 59,
  179. 'thumbnail': r're:https?://.+',
  180. 'uploader': '(ルーキー',
  181. 'uploader_id': '10111524',
  182. },
  183. }, {
  184. 'url': 'https://www.mildom.com/clip/10660174-2c539e6e277c4aaeb4b1fbe8d22cb902',
  185. 'info_dict': {
  186. 'id': '10660174-2c539e6e277c4aaeb4b1fbe8d22cb902',
  187. 'title': 'あ',
  188. 'timestamp': 1614769431,
  189. 'duration': 31,
  190. 'thumbnail': r're:https?://.+',
  191. 'uploader': 'ドルゴルスレンギーン=ダグワドルジ',
  192. 'uploader_id': '10660174',
  193. },
  194. }]
  195. def _real_extract(self, url):
  196. user_id, video_id = self._match_valid_url(url).group('user_id', 'id')
  197. webpage = self._download_webpage(f'https://www.mildom.com/clip/{video_id}', video_id)
  198. clip_detail = self._call_api(
  199. 'https://cloudac-cf-jp.mildom.com/nonolive/videocontent/clip/detail', video_id,
  200. note='Downloading playback metadata', query={
  201. 'clip_id': video_id,
  202. })
  203. return {
  204. 'id': video_id,
  205. 'title': self._html_search_meta(
  206. ('og:description', 'description'), webpage, default=None) or clip_detail.get('title'),
  207. 'timestamp': float_or_none(clip_detail.get('create_time')),
  208. 'duration': float_or_none(clip_detail.get('length')),
  209. 'thumbnail': clip_detail.get('cover'),
  210. 'uploader': traverse_obj(clip_detail, ('user_info', 'loginname')),
  211. 'uploader_id': user_id,
  212. 'url': clip_detail['url'],
  213. 'ext': determine_ext(clip_detail.get('url'), 'mp4'),
  214. }
  215. class MildomUserVodIE(MildomBaseIE):
  216. IE_NAME = 'mildom:user:vod'
  217. IE_DESC = 'Download all VODs from specific user in Mildom'
  218. _VALID_URL = r'https?://(?:(?:www|m)\.)mildom\.com/profile/(?P<id>\d+)'
  219. _TESTS = [{
  220. 'url': 'https://www.mildom.com/profile/10093333',
  221. 'info_dict': {
  222. 'id': '10093333',
  223. 'title': 'Uploads from ねこばたけ',
  224. },
  225. 'playlist_mincount': 732,
  226. }, {
  227. 'url': 'https://www.mildom.com/profile/10882672',
  228. 'info_dict': {
  229. 'id': '10882672',
  230. 'title': 'Uploads from kson組長(けいそん)',
  231. },
  232. 'playlist_mincount': 201,
  233. }]
  234. def _fetch_page(self, user_id, page):
  235. page += 1
  236. reply = self._call_api(
  237. 'https://cloudac.mildom.com/nonolive/videocontent/profile/playbackList',
  238. user_id, note=f'Downloading page {page}', query={
  239. 'user_id': user_id,
  240. 'page': page,
  241. 'limit': '30',
  242. })
  243. if not reply:
  244. return
  245. for x in reply:
  246. v_id = x.get('v_id')
  247. if not v_id:
  248. continue
  249. yield self.url_result(f'https://www.mildom.com/playback/{user_id}/{v_id}')
  250. def _real_extract(self, url):
  251. user_id = self._match_id(url)
  252. self.to_screen(f'This will download all VODs belonging to user. To download ongoing live video, use "https://www.mildom.com/{user_id}" instead')
  253. profile = self._call_api(
  254. 'https://cloudac.mildom.com/nonolive/gappserv/user/profileV2', user_id,
  255. query={'user_id': user_id}, note='Downloading user profile')['user_info']
  256. return self.playlist_result(
  257. OnDemandPagedList(functools.partial(self._fetch_page, user_id), 30),
  258. user_id, f'Uploads from {profile["loginname"]}')