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.

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