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.

275 lines
10 KiB

  1. # coding: utf-8
  2. from __future__ import unicode_literals
  3. import json
  4. import datetime
  5. from .common import InfoExtractor
  6. from ..compat import (
  7. compat_parse_qs,
  8. compat_urlparse,
  9. )
  10. from ..utils import (
  11. determine_ext,
  12. ExtractorError,
  13. int_or_none,
  14. parse_duration,
  15. parse_iso8601,
  16. urlencode_postdata,
  17. xpath_text,
  18. )
  19. class NiconicoIE(InfoExtractor):
  20. IE_NAME = 'niconico'
  21. IE_DESC = 'ニコニコ動画'
  22. _TESTS = [{
  23. 'url': 'http://www.nicovideo.jp/watch/sm22312215',
  24. 'md5': 'd1a75c0823e2f629128c43e1212760f9',
  25. 'info_dict': {
  26. 'id': 'sm22312215',
  27. 'ext': 'mp4',
  28. 'title': 'Big Buck Bunny',
  29. 'uploader': 'takuya0301',
  30. 'uploader_id': '2698420',
  31. 'upload_date': '20131123',
  32. 'timestamp': 1385182762,
  33. 'description': '(c) copyright 2008, Blender Foundation / www.bigbuckbunny.org',
  34. 'duration': 33,
  35. },
  36. 'skip': 'Requires an account',
  37. }, {
  38. # File downloaded with and without credentials are different, so omit
  39. # the md5 field
  40. 'url': 'http://www.nicovideo.jp/watch/nm14296458',
  41. 'info_dict': {
  42. 'id': 'nm14296458',
  43. 'ext': 'swf',
  44. 'title': '【鏡音リン】Dance on media【オリジナル】take2!',
  45. 'description': 'md5:689f066d74610b3b22e0f1739add0f58',
  46. 'uploader': 'りょうた',
  47. 'uploader_id': '18822557',
  48. 'upload_date': '20110429',
  49. 'timestamp': 1304065916,
  50. 'duration': 209,
  51. },
  52. 'skip': 'Requires an account',
  53. }, {
  54. # 'video exists but is marked as "deleted"
  55. # md5 is unstable
  56. 'url': 'http://www.nicovideo.jp/watch/sm10000',
  57. 'info_dict': {
  58. 'id': 'sm10000',
  59. 'ext': 'unknown_video',
  60. 'description': 'deleted',
  61. 'title': 'ドラえもんエターナル第3話「決戦第3新東京市」<前編>',
  62. 'upload_date': '20071224',
  63. 'timestamp': int, # timestamp field has different value if logged in
  64. 'duration': 304,
  65. },
  66. 'skip': 'Requires an account',
  67. }, {
  68. 'url': 'http://www.nicovideo.jp/watch/so22543406',
  69. 'info_dict': {
  70. 'id': '1388129933',
  71. 'ext': 'mp4',
  72. 'title': '【第1回】RADIOアニメロミックス ラブライブ!~のぞえりRadio Garden~',
  73. 'description': 'md5:b27d224bb0ff53d3c8269e9f8b561cf1',
  74. 'timestamp': 1388851200,
  75. 'upload_date': '20140104',
  76. 'uploader': 'アニメロチャンネル',
  77. 'uploader_id': '312',
  78. },
  79. 'skip': 'The viewing period of the video you were searching for has expired.',
  80. }, {
  81. 'url': 'http://sp.nicovideo.jp/watch/sm28964488?ss_pos=1&cp_in=wt_tg',
  82. 'only_matching': True,
  83. }]
  84. _VALID_URL = r'https?://(?:www\.|secure\.|sp\.)?nicovideo\.jp/watch/(?P<id>(?:[a-z]{2})?[0-9]+)'
  85. _NETRC_MACHINE = 'niconico'
  86. def _real_initialize(self):
  87. self._login()
  88. def _login(self):
  89. (username, password) = self._get_login_info()
  90. # No authentication to be performed
  91. if not username:
  92. return True
  93. # Log in
  94. login_ok = True
  95. login_form_strs = {
  96. 'mail_tel': username,
  97. 'password': password,
  98. }
  99. urlh = self._request_webpage(
  100. 'https://account.nicovideo.jp/api/v1/login', None,
  101. note='Logging in', errnote='Unable to log in',
  102. data=urlencode_postdata(login_form_strs))
  103. if urlh is False:
  104. login_ok = False
  105. else:
  106. parts = compat_urlparse.urlparse(urlh.geturl())
  107. if compat_parse_qs(parts.query).get('message', [None])[0] == 'cant_login':
  108. login_ok = False
  109. if not login_ok:
  110. self._downloader.report_warning('unable to log in: bad username or password')
  111. return login_ok
  112. def _real_extract(self, url):
  113. video_id = self._match_id(url)
  114. # Get video webpage. We are not actually interested in it for normal
  115. # cases, but need the cookies in order to be able to download the
  116. # info webpage
  117. webpage, handle = self._download_webpage_handle(
  118. 'http://www.nicovideo.jp/watch/' + video_id, video_id)
  119. if video_id.startswith('so'):
  120. video_id = self._match_id(handle.geturl())
  121. video_info = self._download_xml(
  122. 'http://ext.nicovideo.jp/api/getthumbinfo/' + video_id, video_id,
  123. note='Downloading video info page')
  124. # Get flv info
  125. flv_info_webpage = self._download_webpage(
  126. 'http://flapi.nicovideo.jp/api/getflv/' + video_id + '?as3=1',
  127. video_id, 'Downloading flv info')
  128. flv_info = compat_urlparse.parse_qs(flv_info_webpage)
  129. if 'url' not in flv_info:
  130. if 'deleted' in flv_info:
  131. raise ExtractorError('The video has been deleted.',
  132. expected=True)
  133. elif 'closed' in flv_info:
  134. raise ExtractorError('Niconico videos now require logging in',
  135. expected=True)
  136. else:
  137. raise ExtractorError('Unable to find video URL')
  138. video_real_url = flv_info['url'][0]
  139. # Start extracting information
  140. title = xpath_text(video_info, './/title')
  141. if not title:
  142. title = self._og_search_title(webpage, default=None)
  143. if not title:
  144. title = self._html_search_regex(
  145. r'<span[^>]+class="videoHeaderTitle"[^>]*>([^<]+)</span>',
  146. webpage, 'video title')
  147. watch_api_data_string = self._html_search_regex(
  148. r'<div[^>]+id="watchAPIDataContainer"[^>]+>([^<]+)</div>',
  149. webpage, 'watch api data', default=None)
  150. watch_api_data = self._parse_json(watch_api_data_string, video_id) if watch_api_data_string else {}
  151. video_detail = watch_api_data.get('videoDetail', {})
  152. extension = xpath_text(video_info, './/movie_type')
  153. if not extension:
  154. extension = determine_ext(video_real_url)
  155. thumbnail = (
  156. xpath_text(video_info, './/thumbnail_url') or
  157. self._html_search_meta('image', webpage, 'thumbnail', default=None) or
  158. video_detail.get('thumbnail'))
  159. description = xpath_text(video_info, './/description')
  160. timestamp = parse_iso8601(xpath_text(video_info, './/first_retrieve'))
  161. if not timestamp:
  162. match = self._html_search_meta('datePublished', webpage, 'date published', default=None)
  163. if match:
  164. timestamp = parse_iso8601(match.replace('+', ':00+'))
  165. if not timestamp and video_detail.get('postedAt'):
  166. timestamp = parse_iso8601(
  167. video_detail['postedAt'].replace('/', '-'),
  168. delimiter=' ', timezone=datetime.timedelta(hours=9))
  169. view_count = int_or_none(xpath_text(video_info, './/view_counter'))
  170. if not view_count:
  171. match = self._html_search_regex(
  172. r'>Views: <strong[^>]*>([^<]+)</strong>',
  173. webpage, 'view count', default=None)
  174. if match:
  175. view_count = int_or_none(match.replace(',', ''))
  176. view_count = view_count or video_detail.get('viewCount')
  177. comment_count = int_or_none(xpath_text(video_info, './/comment_num'))
  178. if not comment_count:
  179. match = self._html_search_regex(
  180. r'>Comments: <strong[^>]*>([^<]+)</strong>',
  181. webpage, 'comment count', default=None)
  182. if match:
  183. comment_count = int_or_none(match.replace(',', ''))
  184. comment_count = comment_count or video_detail.get('commentCount')
  185. duration = (parse_duration(
  186. xpath_text(video_info, './/length') or
  187. self._html_search_meta(
  188. 'video:duration', webpage, 'video duration', default=None)) or
  189. video_detail.get('length'))
  190. webpage_url = xpath_text(video_info, './/watch_url') or url
  191. if video_info.find('.//ch_id') is not None:
  192. uploader_id = video_info.find('.//ch_id').text
  193. uploader = video_info.find('.//ch_name').text
  194. elif video_info.find('.//user_id') is not None:
  195. uploader_id = video_info.find('.//user_id').text
  196. uploader = video_info.find('.//user_nickname').text
  197. else:
  198. uploader_id = uploader = None
  199. return {
  200. 'id': video_id,
  201. 'url': video_real_url,
  202. 'title': title,
  203. 'ext': extension,
  204. 'format_id': 'economy' if video_real_url.endswith('low') else 'normal',
  205. 'thumbnail': thumbnail,
  206. 'description': description,
  207. 'uploader': uploader,
  208. 'timestamp': timestamp,
  209. 'uploader_id': uploader_id,
  210. 'view_count': view_count,
  211. 'comment_count': comment_count,
  212. 'duration': duration,
  213. 'webpage_url': webpage_url,
  214. }
  215. class NiconicoPlaylistIE(InfoExtractor):
  216. _VALID_URL = r'https?://(?:www\.)?nicovideo\.jp/mylist/(?P<id>\d+)'
  217. _TEST = {
  218. 'url': 'http://www.nicovideo.jp/mylist/27411728',
  219. 'info_dict': {
  220. 'id': '27411728',
  221. 'title': 'AKB48のオールナイトニッポン',
  222. },
  223. 'playlist_mincount': 225,
  224. }
  225. def _real_extract(self, url):
  226. list_id = self._match_id(url)
  227. webpage = self._download_webpage(url, list_id)
  228. entries_json = self._search_regex(r'Mylist\.preload\(\d+, (\[.*\])\);',
  229. webpage, 'entries')
  230. entries = json.loads(entries_json)
  231. entries = [{
  232. '_type': 'url',
  233. 'ie_key': NiconicoIE.ie_key(),
  234. 'url': ('http://www.nicovideo.jp/watch/%s' %
  235. entry['item_data']['video_id']),
  236. } for entry in entries]
  237. return {
  238. '_type': 'playlist',
  239. 'title': self._search_regex(r'\s+name: "(.*?)"', webpage, 'title'),
  240. 'id': list_id,
  241. 'entries': entries,
  242. }