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.

322 lines
11 KiB

11 years ago
11 years ago
11 years ago
11 years ago
10 years ago
10 years ago
  1. # coding: utf-8
  2. from __future__ import unicode_literals
  3. import json
  4. import time
  5. import hmac
  6. import hashlib
  7. import itertools
  8. from ..utils import (
  9. ExtractorError,
  10. int_or_none,
  11. parse_age_limit,
  12. parse_iso8601,
  13. )
  14. from ..compat import compat_urllib_request
  15. from .common import InfoExtractor
  16. class VikiBaseIE(InfoExtractor):
  17. _VALID_URL_BASE = r'https?://(?:www\.)?viki\.(?:com|net|mx|jp|fr)/'
  18. _API_QUERY_TEMPLATE = '/v4/%sapp=%s&t=%s&site=www.viki.com'
  19. _API_URL_TEMPLATE = 'http://api.viki.io%s&sig=%s'
  20. _APP = '65535a'
  21. _APP_VERSION = '2.2.5.1428709186'
  22. _APP_SECRET = '-$iJ}@p7!G@SyU/je1bEyWg}upLu-6V6-Lg9VD(]siH,r.,m-r|ulZ,U4LC/SeR)'
  23. _NETRC_MACHINE = 'viki'
  24. _token = None
  25. def _prepare_call(self, path, timestamp=None, post_data=None):
  26. path += '?' if '?' not in path else '&'
  27. if not timestamp:
  28. timestamp = int(time.time())
  29. query = self._API_QUERY_TEMPLATE % (path, self._APP, timestamp)
  30. if self._token:
  31. query += '&token=%s' % self._token
  32. sig = hmac.new(
  33. self._APP_SECRET.encode('ascii'),
  34. query.encode('ascii'),
  35. hashlib.sha1
  36. ).hexdigest()
  37. url = self._API_URL_TEMPLATE % (query, sig)
  38. return compat_urllib_request.Request(
  39. url, json.dumps(post_data).encode('utf-8')) if post_data else url
  40. def _call_api(self, path, video_id, note, timestamp=None, post_data=None):
  41. resp = self._download_json(
  42. self._prepare_call(path, timestamp, post_data), video_id, note)
  43. error = resp.get('error')
  44. if error:
  45. if error == 'invalid timestamp':
  46. resp = self._download_json(
  47. self._prepare_call(path, int(resp['current_timestamp']), post_data),
  48. video_id, '%s (retry)' % note)
  49. error = resp.get('error')
  50. if error:
  51. self._raise_error(resp['error'])
  52. return resp
  53. def _raise_error(self, error):
  54. raise ExtractorError(
  55. '%s returned error: %s' % (self.IE_NAME, error),
  56. expected=True)
  57. def _real_initialize(self):
  58. self._login()
  59. def _login(self):
  60. (username, password) = self._get_login_info()
  61. if username is None:
  62. return
  63. login_form = {
  64. 'login_id': username,
  65. 'password': password,
  66. }
  67. login = self._call_api(
  68. 'sessions.json', None,
  69. 'Logging in as %s' % username, post_data=login_form)
  70. self._token = login.get('token')
  71. if not self._token:
  72. self.report_warning('Unable to get session token, login has probably failed')
  73. class VikiIE(VikiBaseIE):
  74. IE_NAME = 'viki'
  75. _VALID_URL = r'%s(?:videos|player)/(?P<id>[0-9]+v)' % VikiBaseIE._VALID_URL_BASE
  76. _TESTS = [{
  77. 'url': 'http://www.viki.com/videos/1023585v-heirs-episode-14',
  78. 'info_dict': {
  79. 'id': '1023585v',
  80. 'ext': 'mp4',
  81. 'title': 'Heirs Episode 14',
  82. 'uploader': 'SBS',
  83. 'description': 'md5:c4b17b9626dd4b143dcc4d855ba3474e',
  84. 'upload_date': '20131121',
  85. 'age_limit': 13,
  86. },
  87. 'skip': 'Blocked in the US',
  88. }, {
  89. # clip
  90. 'url': 'http://www.viki.com/videos/1067139v-the-avengers-age-of-ultron-press-conference',
  91. 'md5': '86c0b5dbd4d83a6611a79987cc7a1989',
  92. 'info_dict': {
  93. 'id': '1067139v',
  94. 'ext': 'mp4',
  95. 'title': "'The Avengers: Age of Ultron' Press Conference",
  96. 'description': 'md5:d70b2f9428f5488321bfe1db10d612ea',
  97. 'duration': 352,
  98. 'timestamp': 1430380829,
  99. 'upload_date': '20150430',
  100. 'uploader': 'Arirang TV',
  101. 'like_count': int,
  102. 'age_limit': 0,
  103. }
  104. }, {
  105. 'url': 'http://www.viki.com/videos/1048879v-ankhon-dekhi',
  106. 'info_dict': {
  107. 'id': '1048879v',
  108. 'ext': 'mp4',
  109. 'title': 'Ankhon Dekhi',
  110. 'duration': 6512,
  111. 'timestamp': 1408532356,
  112. 'upload_date': '20140820',
  113. 'uploader': 'Spuul',
  114. 'like_count': int,
  115. 'age_limit': 13,
  116. },
  117. 'params': {
  118. # m3u8 download
  119. 'skip_download': True,
  120. }
  121. }, {
  122. # episode
  123. 'url': 'http://www.viki.com/videos/44699v-boys-over-flowers-episode-1',
  124. 'md5': '190f3ef426005ba3a080a63325955bc3',
  125. 'info_dict': {
  126. 'id': '44699v',
  127. 'ext': 'mp4',
  128. 'title': 'Boys Over Flowers - Episode 1',
  129. 'description': 'md5:52617e4f729c7d03bfd4bcbbb6e946f2',
  130. 'duration': 4155,
  131. 'timestamp': 1270496524,
  132. 'upload_date': '20100405',
  133. 'uploader': 'group8',
  134. 'like_count': int,
  135. 'age_limit': 13,
  136. }
  137. }, {
  138. # youtube external
  139. 'url': 'http://www.viki.com/videos/50562v-poor-nastya-complete-episode-1',
  140. 'md5': '216d1afdc0c64d1febc1e9f2bd4b864b',
  141. 'info_dict': {
  142. 'id': '50562v',
  143. 'ext': 'mp4',
  144. 'title': 'Poor Nastya [COMPLETE] - Episode 1',
  145. 'description': '',
  146. 'duration': 607,
  147. 'timestamp': 1274949505,
  148. 'upload_date': '20101213',
  149. 'uploader': 'ad14065n',
  150. 'uploader_id': 'ad14065n',
  151. 'like_count': int,
  152. 'age_limit': 13,
  153. }
  154. }, {
  155. 'url': 'http://www.viki.com/player/44699v',
  156. 'only_matching': True,
  157. }]
  158. def _real_extract(self, url):
  159. video_id = self._match_id(url)
  160. video = self._call_api(
  161. 'videos/%s.json' % video_id, video_id, 'Downloading video JSON')
  162. title = None
  163. titles = video.get('titles')
  164. if titles:
  165. title = titles.get('en') or titles[titles.keys()[0]]
  166. if not title:
  167. title = 'Episode %d' % video.get('number') if video.get('type') == 'episode' else video.get('id') or video_id
  168. container_titles = video.get('container', {}).get('titles')
  169. if container_titles:
  170. container_title = container_titles.get('en') or container_titles[container_titles.keys()[0]]
  171. title = '%s - %s' % (container_title, title)
  172. descriptions = video.get('descriptions')
  173. description = descriptions.get('en') or descriptions[titles.keys()[0]] if descriptions else None
  174. duration = int_or_none(video.get('duration'))
  175. timestamp = parse_iso8601(video.get('created_at'))
  176. uploader = video.get('author')
  177. like_count = int_or_none(video.get('likes', {}).get('count'))
  178. age_limit = parse_age_limit(video.get('rating'))
  179. thumbnails = []
  180. for thumbnail_id, thumbnail in video.get('images', {}).items():
  181. thumbnails.append({
  182. 'id': thumbnail_id,
  183. 'url': thumbnail.get('url'),
  184. })
  185. subtitles = {}
  186. for subtitle_lang, _ in video.get('subtitle_completions', {}).items():
  187. subtitles[subtitle_lang] = [{
  188. 'ext': subtitles_format,
  189. 'url': self._prepare_call(
  190. 'videos/%s/subtitles/%s.%s' % (video_id, subtitle_lang, subtitles_format)),
  191. } for subtitles_format in ('srt', 'vtt')]
  192. result = {
  193. 'id': video_id,
  194. 'title': title,
  195. 'description': description,
  196. 'duration': duration,
  197. 'timestamp': timestamp,
  198. 'uploader': uploader,
  199. 'like_count': like_count,
  200. 'age_limit': age_limit,
  201. 'thumbnails': thumbnails,
  202. 'subtitles': subtitles,
  203. }
  204. streams = self._call_api(
  205. 'videos/%s/streams.json' % video_id, video_id,
  206. 'Downloading video streams JSON')
  207. if 'external' in streams:
  208. result.update({
  209. '_type': 'url_transparent',
  210. 'url': streams['external']['url'],
  211. })
  212. return result
  213. formats = []
  214. for format_id, stream_dict in streams.items():
  215. height = self._search_regex(
  216. r'^(\d+)[pP]$', format_id, 'height', default=None)
  217. for protocol, format_dict in stream_dict.items():
  218. if format_id == 'm3u8':
  219. formats = self._extract_m3u8_formats(
  220. format_dict['url'], video_id, 'mp4', m3u8_id='m3u8-%s' % protocol)
  221. else:
  222. formats.append({
  223. 'url': format_dict['url'],
  224. 'format_id': '%s-%s' % (format_id, protocol),
  225. 'height': height,
  226. })
  227. self._sort_formats(formats)
  228. result['formats'] = formats
  229. return result
  230. class VikiChannelIE(VikiBaseIE):
  231. IE_NAME = 'viki:channel'
  232. _VALID_URL = r'%s(?:tv|news|movies|artists)/(?P<id>[0-9]+c)' % VikiBaseIE._VALID_URL_BASE
  233. _TESTS = [{
  234. 'url': 'http://www.viki.com/tv/50c-boys-over-flowers',
  235. 'info_dict': {
  236. 'id': '50c',
  237. 'title': 'Boys Over Flowers',
  238. 'description': 'md5:ecd3cff47967fe193cff37c0bec52790',
  239. },
  240. 'playlist_count': 70,
  241. }, {
  242. 'url': 'http://www.viki.com/tv/1354c-poor-nastya-complete',
  243. 'info_dict': {
  244. 'id': '1354c',
  245. 'title': 'Poor Nastya [COMPLETE]',
  246. 'description': 'md5:05bf5471385aa8b21c18ad450e350525',
  247. },
  248. 'playlist_count': 127,
  249. }, {
  250. 'url': 'http://www.viki.com/news/24569c-showbiz-korea',
  251. 'only_matching': True,
  252. }, {
  253. 'url': 'http://www.viki.com/movies/22047c-pride-and-prejudice-2005',
  254. 'only_matching': True,
  255. }, {
  256. 'url': 'http://www.viki.com/artists/2141c-shinee',
  257. 'only_matching': True,
  258. }]
  259. _PER_PAGE = 25
  260. def _real_extract(self, url):
  261. channel_id = self._match_id(url)
  262. channel = self._call_api(
  263. 'containers/%s.json' % channel_id, channel_id,
  264. 'Downloading channel JSON')
  265. titles = channel['titles']
  266. title = titles.get('en') or titles[titles.keys()[0]]
  267. descriptions = channel['descriptions']
  268. description = descriptions.get('en') or descriptions[descriptions.keys()[0]]
  269. entries = []
  270. for video_type in ('episodes', 'clips', 'movies'):
  271. for page_num in itertools.count(1):
  272. page = self._call_api(
  273. 'containers/%s/%s.json?per_page=%d&sort=number&direction=asc&with_paging=true&page=%d'
  274. % (channel_id, video_type, self._PER_PAGE, page_num), channel_id,
  275. 'Downloading %s JSON page #%d' % (video_type, page_num))
  276. for video in page['response']:
  277. video_id = video['id']
  278. entries.append(self.url_result(
  279. 'http://www.viki.com/videos/%s' % video_id, 'Viki'))
  280. if not page['pagination']['next']:
  281. break
  282. return self.playlist_result(entries, channel_id, title, description)