]>
Commit | Line | Data |
---|---|---|
1 | import random | |
2 | import re | |
3 | import string | |
4 | import time | |
5 | ||
6 | from .common import InfoExtractor | |
7 | from ..utils import ( | |
8 | ExtractorError, | |
9 | clean_html, | |
10 | get_element_by_class, | |
11 | js_to_json, | |
12 | str_or_none, | |
13 | strip_jsonp, | |
14 | ) | |
15 | ||
16 | ||
17 | class YoukuIE(InfoExtractor): | |
18 | IE_NAME = 'youku' | |
19 | IE_DESC = '优酷' | |
20 | _VALID_URL = r'''(?x) | |
21 | (?: | |
22 | https?://( | |
23 | (?:v|play(?:er)?)\.(?:youku|tudou)\.com/(?:v_show/id_|player\.php/sid/)| | |
24 | video\.tudou\.com/v/)| | |
25 | youku:) | |
26 | (?P<id>[A-Za-z0-9]+)(?:\.html|/v\.swf|) | |
27 | ''' | |
28 | ||
29 | _TESTS = [{ | |
30 | 'url': 'http://player.youku.com/player.php/sid/XNDgyMDQ2NTQw/v.swf', | |
31 | 'only_matching': True, | |
32 | }, { | |
33 | 'url': 'http://v.youku.com/v_show/id_XNjA1NzA2Njgw.html', | |
34 | 'note': 'Video protected with password', | |
35 | 'info_dict': { | |
36 | 'id': 'XNjA1NzA2Njgw', | |
37 | 'ext': 'mp4', | |
38 | 'title': '邢義田复旦讲座之想象中的胡人—从“左衽孔子”说起', | |
39 | 'duration': 7264.5, | |
40 | 'thumbnail': r're:^https?://.*', | |
41 | 'uploader': 'FoxJin1006', | |
42 | 'uploader_id': '322014285', | |
43 | 'uploader_url': 'http://i.youku.com/u/UMTI4ODA1NzE0MA==', | |
44 | 'tags': list, | |
45 | }, | |
46 | 'params': { | |
47 | 'videopassword': '100600', | |
48 | }, | |
49 | 'skip': '404', | |
50 | }, { | |
51 | # /play/get.json contains streams with "channel_type":"tail" | |
52 | 'url': 'http://v.youku.com/v_show/id_XOTUxMzg4NDMy.html', | |
53 | 'info_dict': { | |
54 | 'id': 'XOTUxMzg4NDMy', | |
55 | 'ext': 'mp4', | |
56 | 'title': '我的世界☆明月庄主☆车震猎杀☆杀人艺术Minecraft', | |
57 | 'duration': 702.08, | |
58 | 'thumbnail': r're:^https?://.*', | |
59 | 'uploader': '明月庄主moon', | |
60 | 'uploader_id': '38465621', | |
61 | 'uploader_url': 'https://www.youku.com/profile/index/?uid=UMTUzODYyNDg0', | |
62 | 'tags': list, | |
63 | }, | |
64 | }, { | |
65 | 'url': 'https://v.youku.com/v_show/id_XNTA2NTA0MjA1Mg==.html', | |
66 | 'info_dict': { | |
67 | 'id': 'XNTA2NTA0MjA1Mg', | |
68 | 'ext': 'mp4', | |
69 | 'title': 'Minecraft我的世界:建造超大巨型航空飞机,菜鸟vs高手vs黑客', | |
70 | 'duration': 542.13, | |
71 | 'thumbnail': r're:^https?://.*', | |
72 | 'uploader': '波哥游戏解说', | |
73 | 'uploader_id': '156688084', | |
74 | 'uploader_url': 'https://www.youku.com/profile/index/?uid=UNjI2NzUyMzM2', | |
75 | 'tags': list, | |
76 | }, | |
77 | }, { | |
78 | 'url': 'https://v.youku.com/v_show/id_XNTE1MzczOTg4MA==.html', | |
79 | 'info_dict': { | |
80 | 'id': 'XNTE1MzczOTg4MA', | |
81 | 'ext': 'mp4', | |
82 | 'title': '国产超A特工片', | |
83 | 'duration': 362.97, | |
84 | 'thumbnail': r're:^https?://.*', | |
85 | 'uploader': '陈晓娟说历史', | |
86 | 'uploader_id': '1640913339', | |
87 | 'uploader_url': 'https://www.youku.com/profile/index/?uid=UNjU2MzY1MzM1Ng==', | |
88 | 'tags': list, | |
89 | }, | |
90 | }, { | |
91 | 'url': 'https://play.tudou.com/v_show/id_XNjAxNjI2OTU3Ng==.html?', | |
92 | 'info_dict': { | |
93 | 'id': 'XNjAxNjI2OTU3Ng', | |
94 | 'ext': 'mp4', | |
95 | 'title': '阿斯塔意识到哈里杀了人,自己被骗了', | |
96 | 'thumbnail': 'https://m.ykimg.com/0541010164F732752794D4D7B70331D1', | |
97 | 'uploader_id': '88758207', | |
98 | 'tags': [], | |
99 | 'uploader_url': 'https://www.youku.com/profile/index/?uid=UMzU1MDMyODI4', | |
100 | 'uploader': '英美剧场', | |
101 | 'duration': 72.91, | |
102 | }, | |
103 | }] | |
104 | ||
105 | @staticmethod | |
106 | def get_ysuid(): | |
107 | return '%d%s' % (int(time.time()), ''.join( | |
108 | random.choices(string.ascii_letters, k=3))) | |
109 | ||
110 | def get_format_name(self, fm): | |
111 | _dict = { | |
112 | '3gp': 'h6', | |
113 | '3gphd': 'h5', | |
114 | 'flv': 'h4', | |
115 | 'flvhd': 'h4', | |
116 | 'mp4': 'h3', | |
117 | 'mp4hd': 'h3', | |
118 | 'mp4hd2': 'h4', | |
119 | 'mp4hd3': 'h4', | |
120 | 'hd2': 'h2', | |
121 | 'hd3': 'h1', | |
122 | } | |
123 | return _dict.get(fm) | |
124 | ||
125 | def _real_extract(self, url): | |
126 | video_id = self._match_id(url) | |
127 | ||
128 | self._set_cookie('youku.com', '__ysuid', self.get_ysuid()) | |
129 | self._set_cookie('youku.com', 'xreferrer', 'http://www.youku.com') | |
130 | ||
131 | _, urlh = self._download_webpage_handle( | |
132 | 'https://log.mmstat.com/eg.js', video_id, 'Retrieving cna info') | |
133 | # The etag header is '"foobar"'; let's remove the double quotes | |
134 | cna = urlh.headers['etag'][1:-1] | |
135 | ||
136 | # request basic data | |
137 | basic_data_params = { | |
138 | 'vid': video_id, | |
139 | 'ccode': '0524', | |
140 | 'client_ip': '192.168.1.1', | |
141 | 'utid': cna, | |
142 | 'client_ts': time.time() / 1000, | |
143 | } | |
144 | ||
145 | video_password = self.get_param('videopassword') | |
146 | if video_password: | |
147 | basic_data_params['password'] = video_password | |
148 | ||
149 | headers = { | |
150 | 'Referer': url, | |
151 | } | |
152 | headers.update(self.geo_verification_headers()) | |
153 | data = self._download_json( | |
154 | 'https://ups.youku.com/ups/get.json', video_id, | |
155 | 'Downloading JSON metadata', | |
156 | query=basic_data_params, headers=headers)['data'] | |
157 | ||
158 | error = data.get('error') | |
159 | if error: | |
160 | error_note = error.get('note') | |
161 | if error_note is not None and '因版权原因无法观看此视频' in error_note: | |
162 | raise ExtractorError( | |
163 | 'Youku said: Sorry, this video is available in China only', expected=True) | |
164 | elif error_note and '该视频被设为私密' in error_note: | |
165 | raise ExtractorError( | |
166 | 'Youku said: Sorry, this video is private', expected=True) | |
167 | else: | |
168 | msg = 'Youku server reported error %i' % error.get('code') | |
169 | if error_note is not None: | |
170 | msg += ': ' + clean_html(error_note) | |
171 | raise ExtractorError(msg) | |
172 | ||
173 | # get video title | |
174 | video_data = data['video'] | |
175 | title = video_data['title'] | |
176 | ||
177 | formats = [{ | |
178 | 'url': stream['m3u8_url'], | |
179 | 'format_id': self.get_format_name(stream.get('stream_type')), | |
180 | 'ext': 'mp4', | |
181 | 'protocol': 'm3u8_native', | |
182 | 'filesize': int(stream.get('size')), | |
183 | 'width': stream.get('width'), | |
184 | 'height': stream.get('height'), | |
185 | } for stream in data['stream'] if stream.get('channel_type') != 'tail'] | |
186 | ||
187 | return { | |
188 | 'id': video_id, | |
189 | 'title': title, | |
190 | 'formats': formats, | |
191 | 'duration': video_data.get('seconds'), | |
192 | 'thumbnail': video_data.get('logo'), | |
193 | 'uploader': video_data.get('username'), | |
194 | 'uploader_id': str_or_none(video_data.get('userid')), | |
195 | 'uploader_url': data.get('uploader', {}).get('homepage'), | |
196 | 'tags': video_data.get('tags'), | |
197 | } | |
198 | ||
199 | ||
200 | class YoukuShowIE(InfoExtractor): | |
201 | _VALID_URL = r'https?://list\.youku\.com/show/id_(?P<id>[0-9a-z]+)\.html' | |
202 | IE_NAME = 'youku:show' | |
203 | ||
204 | _TESTS = [{ | |
205 | 'url': 'http://list.youku.com/show/id_zc7c670be07ff11e48b3f.html', | |
206 | 'info_dict': { | |
207 | 'id': 'zc7c670be07ff11e48b3f', | |
208 | 'title': '花千骨 DVD版', | |
209 | 'description': 'md5:a1ae6f5618571bbeb5c9821f9c81b558', | |
210 | }, | |
211 | 'playlist_count': 50, | |
212 | }, { | |
213 | # Episode number not starting from 1 | |
214 | 'url': 'http://list.youku.com/show/id_zefbfbd70efbfbd780bef.html', | |
215 | 'info_dict': { | |
216 | 'id': 'zefbfbd70efbfbd780bef', | |
217 | 'title': '超级飞侠3', | |
218 | 'description': 'md5:275715156abebe5ccc2a1992e9d56b98', | |
219 | }, | |
220 | 'playlist_count': 24, | |
221 | }, { | |
222 | # Ongoing playlist. The initial page is the last one | |
223 | 'url': 'http://list.youku.com/show/id_za7c275ecd7b411e1a19e.html', | |
224 | 'only_matching': True, | |
225 | }, { | |
226 | # No data-id value. | |
227 | 'url': 'http://list.youku.com/show/id_zefbfbd61237fefbfbdef.html', | |
228 | 'only_matching': True, | |
229 | }, { | |
230 | # Wrong number of reload_id. | |
231 | 'url': 'http://list.youku.com/show/id_z20eb4acaf5c211e3b2ad.html', | |
232 | 'only_matching': True, | |
233 | }] | |
234 | ||
235 | def _extract_entries(self, playlist_data_url, show_id, note, query): | |
236 | query['callback'] = 'cb' | |
237 | playlist_data = self._download_json( | |
238 | playlist_data_url, show_id, query=query, note=note, | |
239 | transform_source=lambda s: js_to_json(strip_jsonp(s))).get('html') | |
240 | if playlist_data is None: | |
241 | return [None, None] | |
242 | drama_list = (get_element_by_class('p-drama-grid', playlist_data) | |
243 | or get_element_by_class('p-drama-half-row', playlist_data)) | |
244 | if drama_list is None: | |
245 | raise ExtractorError('No episodes found') | |
246 | video_urls = re.findall(r'<a[^>]+href="([^"]+)"', drama_list) | |
247 | return playlist_data, [ | |
248 | self.url_result(self._proto_relative_url(video_url, 'http:'), YoukuIE.ie_key()) | |
249 | for video_url in video_urls] | |
250 | ||
251 | def _real_extract(self, url): | |
252 | show_id = self._match_id(url) | |
253 | webpage = self._download_webpage(url, show_id) | |
254 | ||
255 | entries = [] | |
256 | page_config = self._parse_json(self._search_regex( | |
257 | r'var\s+PageConfig\s*=\s*({.+});', webpage, 'page config'), | |
258 | show_id, transform_source=js_to_json) | |
259 | first_page, initial_entries = self._extract_entries( | |
260 | 'http://list.youku.com/show/module', show_id, | |
261 | note='Downloading initial playlist data page', | |
262 | query={ | |
263 | 'id': page_config['showid'], | |
264 | 'tab': 'showInfo', | |
265 | }) | |
266 | first_page_reload_id = self._html_search_regex( | |
267 | r'<div[^>]+id="(reload_\d+)', first_page, 'first page reload id') | |
268 | # The first reload_id has the same items as first_page | |
269 | reload_ids = re.findall('<li[^>]+data-id="([^"]+)">', first_page) | |
270 | entries.extend(initial_entries) | |
271 | for idx, reload_id in enumerate(reload_ids): | |
272 | if reload_id == first_page_reload_id: | |
273 | continue | |
274 | _, new_entries = self._extract_entries( | |
275 | 'http://list.youku.com/show/episode', show_id, | |
276 | note='Downloading playlist data page %d' % (idx + 1), | |
277 | query={ | |
278 | 'id': page_config['showid'], | |
279 | 'stage': reload_id, | |
280 | }) | |
281 | if new_entries is not None: | |
282 | entries.extend(new_entries) | |
283 | desc = self._html_search_meta('description', webpage, fatal=False) | |
284 | playlist_title = desc.split(',')[0] if desc else None | |
285 | detail_li = get_element_by_class('p-intro', webpage) | |
286 | playlist_description = get_element_by_class( | |
287 | 'intro-more', detail_li) if detail_li else None | |
288 | ||
289 | return self.playlist_result( | |
290 | entries, show_id, playlist_title, playlist_description) |