]>
Commit | Line | Data |
---|---|---|
ddbd9035 | 1 | # coding: utf-8 |
8a32b82e PH |
2 | from __future__ import unicode_literals |
3 | ||
f9355dc9 | 4 | import base64 |
9c286cfa PH |
5 | |
6 | from .common import InfoExtractor | |
c203be3f YCH |
7 | from ..compat import ( |
8 | compat_urllib_parse, | |
9 | compat_ord, | |
5c2266df S |
10 | ) |
11 | from ..utils import ( | |
12 | ExtractorError, | |
13 | sanitized_Request, | |
c203be3f | 14 | ) |
1498940b | 15 | |
aed473cc | 16 | |
9c286cfa | 17 | class YoukuIE(InfoExtractor): |
f9355dc9 | 18 | IE_NAME = 'youku' |
246995db | 19 | IE_DESC = '优酷' |
8a32b82e PH |
20 | _VALID_URL = r'''(?x) |
21 | (?: | |
22 | http://(?:v|player)\.youku\.com/(?:v_show/id_|player\.php/sid/)| | |
23 | youku:) | |
24 | (?P<id>[A-Za-z0-9]+)(?:\.html|/v\.swf|) | |
25 | ''' | |
f9355dc9 | 26 | |
ee697992 | 27 | _TESTS = [{ |
c683454e | 28 | # MD5 is unstable |
aed473cc | 29 | 'url': 'http://v.youku.com/v_show/id_XMTc1ODE5Njcy.html', |
aed473cc | 30 | 'info_dict': { |
f1e66cb2 | 31 | 'id': 'XMTc1ODE5Njcy_part1', |
aed473cc YCH |
32 | 'title': '★Smile﹗♡ Git Fresh -Booty Music舞蹈.', |
33 | 'ext': 'flv' | |
34 | } | |
ee697992 YCH |
35 | }, { |
36 | 'url': 'http://player.youku.com/player.php/sid/XNDgyMDQ2NTQw/v.swf', | |
37 | 'only_matching': True, | |
f1e66cb2 YCH |
38 | }, { |
39 | 'url': 'http://v.youku.com/v_show/id_XODgxNjg1Mzk2_ev_1.html', | |
40 | 'info_dict': { | |
41 | 'id': 'XODgxNjg1Mzk2', | |
42 | 'title': '武媚娘传奇 85', | |
43 | }, | |
44 | 'playlist_count': 11, | |
4d77550c | 45 | 'skip': 'Available in China only', |
5228b756 YCH |
46 | }, { |
47 | 'url': 'http://v.youku.com/v_show/id_XMTI1OTczNDM5Mg==.html', | |
48 | 'info_dict': { | |
49 | 'id': 'XMTI1OTczNDM5Mg', | |
50 | 'title': '花千骨 04', | |
51 | }, | |
52 | 'playlist_count': 13, | |
33eae08f P |
53 | }, { |
54 | 'url': 'http://v.youku.com/v_show/id_XNjA1NzA2Njgw.html', | |
55 | 'note': 'Video protected with password', | |
56 | 'info_dict': { | |
57 | 'id': 'XNjA1NzA2Njgw', | |
5ddc127d | 58 | 'title': '邢義田复旦讲座之想象中的胡人—从“左衽孔子”说起', |
33eae08f | 59 | }, |
cd5d7542 | 60 | 'playlist_count': 19, |
33eae08f P |
61 | 'params': { |
62 | 'videopassword': '100600', | |
63 | }, | |
ee697992 | 64 | }] |
67f51b3d | 65 | |
7e37c394 | 66 | def construct_video_urls(self, data): |
f9355dc9 P |
67 | # get sid, token |
68 | def yk_t(s1, s2): | |
69 | ls = list(range(256)) | |
70 | t = 0 | |
71 | for i in range(256): | |
c203be3f | 72 | t = (t + ls[i] + compat_ord(s1[i % len(s1)])) % 256 |
f9355dc9 | 73 | ls[i], ls[t] = ls[t], ls[i] |
c203be3f | 74 | s = bytearray() |
ca452466 | 75 | x, y = 0, 0 |
f9355dc9 P |
76 | for i in range(len(s2)): |
77 | y = (y + 1) % 256 | |
78 | x = (x + ls[y]) % 256 | |
79 | ls[x], ls[y] = ls[y], ls[x] | |
c203be3f YCH |
80 | s.append(compat_ord(s2[i]) ^ ls[(ls[x] + ls[y]) % 256]) |
81 | return bytes(s) | |
f9355dc9 P |
82 | |
83 | sid, token = yk_t( | |
7e37c394 | 84 | b'becaf9be', base64.b64decode(data['security']['encrypt_string'].encode('ascii')) |
c203be3f | 85 | ).decode('ascii').split('_') |
f9355dc9 P |
86 | |
87 | # get oip | |
7e37c394 | 88 | oip = data['security']['ip'] |
f9355dc9 | 89 | |
f9355dc9 | 90 | fileid_dict = {} |
7e37c394 | 91 | for stream in data['stream']: |
fdf01663 | 92 | format = stream.get('stream_type') |
fdf01663 C |
93 | fileid = stream['stream_fileid'] |
94 | fileid_dict[format] = fileid | |
f9355dc9 P |
95 | |
96 | def get_fileid(format, n): | |
5333842a C |
97 | number = hex(int(str(n), 10))[2:].upper() |
98 | if len(number) == 1: | |
99 | number = '0' + number | |
100 | streamfileids = fileid_dict[format] | |
101 | fileid = streamfileids[0:8] + number + streamfileids[10:] | |
f9355dc9 P |
102 | return fileid |
103 | ||
104 | # get ep | |
105 | def generate_ep(format, n): | |
106 | fileid = get_fileid(format, n) | |
107 | ep_t = yk_t( | |
c203be3f YCH |
108 | b'bf7e5f01', |
109 | ('%s_%s_%s' % (sid, fileid, token)).encode('ascii') | |
ca452466 | 110 | ) |
c203be3f | 111 | ep = base64.b64encode(ep_t).decode('ascii') |
f9355dc9 P |
112 | return ep |
113 | ||
114 | # generate video_urls | |
115 | video_urls_dict = {} | |
7e37c394 | 116 | for stream in data['stream']: |
fdf01663 | 117 | format = stream.get('stream_type') |
f9355dc9 | 118 | video_urls = [] |
fdf01663 | 119 | for dt in stream['segs']: |
98c3806b | 120 | n = str(stream['segs'].index(dt)) |
1498940b | 121 | param = { |
fdf01663 | 122 | 'K': dt['key'], |
1498940b P |
123 | 'hd': self.get_hd(format), |
124 | 'myp': 0, | |
1498940b P |
125 | 'ypp': 0, |
126 | 'ctype': 12, | |
127 | 'ev': 1, | |
128 | 'token': token, | |
129 | 'oip': oip, | |
130 | 'ep': generate_ep(format, n) | |
131 | } | |
f9355dc9 P |
132 | video_url = \ |
133 | 'http://k.youku.com/player/getFlvPath/' + \ | |
134 | 'sid/' + sid + \ | |
f133fd32 | 135 | '_00' + \ |
f9355dc9 | 136 | '/st/' + self.parse_ext_l(format) + \ |
aed473cc | 137 | '/fileid/' + get_fileid(format, n) + '?' + \ |
1498940b | 138 | compat_urllib_parse.urlencode(param) |
f9355dc9 P |
139 | video_urls.append(video_url) |
140 | video_urls_dict[format] = video_urls | |
141 | ||
142 | return video_urls_dict | |
143 | ||
144 | def get_hd(self, fm): | |
145 | hd_id_dict = { | |
aed473cc | 146 | '3gp': '0', |
fdf01663 | 147 | '3gphd': '1', |
dbb7d7e2 | 148 | 'flv': '0', |
8696a7fd | 149 | 'flvhd': '0', |
dbb7d7e2 | 150 | 'mp4': '1', |
8696a7fd | 151 | 'mp4hd': '1', |
dbb7d7e2 | 152 | 'mp4hd2': '1', |
deb1e8d2 | 153 | 'mp4hd3': '1', |
dbb7d7e2 YCH |
154 | 'hd2': '2', |
155 | 'hd3': '3', | |
f9355dc9 P |
156 | } |
157 | return hd_id_dict[fm] | |
158 | ||
159 | def parse_ext_l(self, fm): | |
160 | ext_dict = { | |
dbb7d7e2 YCH |
161 | '3gp': 'flv', |
162 | '3gphd': 'mp4', | |
aed473cc | 163 | 'flv': 'flv', |
dbb7d7e2 | 164 | 'flvhd': 'flv', |
aed473cc | 165 | 'mp4': 'mp4', |
98c3806b | 166 | 'mp4hd': 'mp4', |
8696a7fd C |
167 | 'mp4hd2': 'flv', |
168 | 'mp4hd3': 'flv', | |
aed473cc YCH |
169 | 'hd2': 'flv', |
170 | 'hd3': 'flv', | |
f9355dc9 P |
171 | } |
172 | return ext_dict[fm] | |
9c286cfa | 173 | |
08f7db20 P |
174 | def get_format_name(self, fm): |
175 | _dict = { | |
aed473cc YCH |
176 | '3gp': 'h6', |
177 | '3gphd': 'h5', | |
178 | 'flv': 'h4', | |
dbb7d7e2 | 179 | 'flvhd': 'h4', |
aed473cc | 180 | 'mp4': 'h3', |
8696a7fd | 181 | 'mp4hd': 'h3', |
dbb7d7e2 | 182 | 'mp4hd2': 'h4', |
8696a7fd | 183 | 'mp4hd3': 'h4', |
dbb7d7e2 YCH |
184 | 'hd2': 'h2', |
185 | 'hd3': 'h1', | |
08f7db20 P |
186 | } |
187 | return _dict[fm] | |
188 | ||
9c286cfa | 189 | def _real_extract(self, url): |
9383e66f | 190 | video_id = self._match_id(url) |
9c286cfa | 191 | |
5228b756 | 192 | def retrieve_data(req_url, note): |
51094b1b | 193 | headers = { |
f133fd32 YCH |
194 | 'Referer': req_url, |
195 | } | |
196 | self._set_cookie('youku.com', 'xreferrer', 'http://www.youku.com') | |
197 | req = sanitized_Request(req_url, headers=headers) | |
9c286cfa | 198 | |
5228b756 YCH |
199 | cn_verification_proxy = self._downloader.params.get('cn_verification_proxy') |
200 | if cn_verification_proxy: | |
201 | req.add_header('Ytdl-request-proxy', cn_verification_proxy) | |
202 | ||
203 | raw_data = self._download_json(req, video_id, note=note) | |
51094b1b | 204 | |
fdf01663 | 205 | return raw_data['data'] |
51094b1b | 206 | |
33eae08f P |
207 | video_password = self._downloader.params.get('videopassword', None) |
208 | ||
5228b756 | 209 | # request basic data |
51094b1b | 210 | basic_data_url = "http://play.youku.com/play/get.json?vid=%s&ct=12" % video_id |
33eae08f | 211 | if video_password: |
14c17caf | 212 | basic_data_url += '&pwd=%s' % video_password |
cb3d2eb9 | 213 | |
ade23409 | 214 | data = retrieve_data(basic_data_url, 'Downloading JSON metadata') |
8a32b82e | 215 | |
7e37c394 | 216 | error = data.get('error') |
14c17caf C |
217 | if error: |
218 | error_note = error.get('note') | |
219 | if error_note is not None and '因版权原因无法观看此视频' in error_note: | |
04e75966 YCH |
220 | raise ExtractorError( |
221 | 'Youku said: Sorry, this video is available in China only', expected=True) | |
222 | else: | |
14c17caf | 223 | msg = 'Youku server reported error %i' % error.get('code') |
35e22b6b | 224 | if error_note is not None: |
14c17caf | 225 | msg += ': ' + error_note |
04e75966 | 226 | raise ExtractorError(msg) |
f9355dc9 | 227 | |
f133fd32 | 228 | # get video title |
7e37c394 | 229 | title = data['video']['title'] |
f9355dc9 P |
230 | |
231 | # generate video_urls_dict | |
7e37c394 | 232 | video_urls_dict = self.construct_video_urls(data) |
f9355dc9 P |
233 | |
234 | # construct info | |
f3aecb27 JMF |
235 | entries = [{ |
236 | 'id': '%s_part%d' % (video_id, i + 1), | |
237 | 'title': title, | |
238 | 'formats': [], | |
239 | # some formats are not available for all parts, we have to detect | |
240 | # which one has all | |
7e37c394 C |
241 | } for i in range(max(len(v.get('segs')) for v in data['stream']))] |
242 | for stream in data['stream']: | |
fdf01663 | 243 | fm = stream.get('stream_type') |
f9355dc9 | 244 | video_urls = video_urls_dict[fm] |
fdf01663 | 245 | for video_url, seg, entry in zip(video_urls, stream['segs'], entries): |
f3aecb27 JMF |
246 | entry['formats'].append({ |
247 | 'url': video_url, | |
a155b7e7 YCH |
248 | 'format_id': self.get_format_name(fm), |
249 | 'ext': self.parse_ext_l(fm), | |
f3aecb27 | 250 | 'filesize': int(seg['size']), |
a155b7e7 | 251 | }) |
f9355dc9 | 252 | |
f1e66cb2 YCH |
253 | return { |
254 | '_type': 'multi_video', | |
255 | 'id': video_id, | |
256 | 'title': title, | |
257 | 'entries': entries, | |
258 | } |