From f2532b1a940a32c4b0dce2a33735d8bcaadff563 Mon Sep 17 00:00:00 2001 From: ivan Date: Wed, 15 Feb 2017 16:13:06 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BB=88=E4=BA=8E=E8=A7=A3=E5=86=B3=E4=BA=86?= =?UTF-8?q?=E8=85=BE=E8=AE=AF=E8=A7=86=E9=A2=91=EF=BC=8C=E4=B8=8D=E4=BB=85?= =?UTF-8?q?=E4=B8=8D=E4=BC=9A=E6=85=A2=EF=BC=8C=E4=B9=9F=E8=A7=A3=E5=86=B3?= =?UTF-8?q?=E4=BA=86=E4=B9=8B=E5=89=8D=E5=8F=AA=E8=83=BD=E4=B8=8B=E8=BD=BD?= =?UTF-8?q?=E5=88=86=E6=AE=B5=E7=9A=84=E9=97=AE=E9=A2=98=EF=BC=8C=E8=BF=98?= =?UTF-8?q?=E5=8F=AF=E4=BB=A5=E7=94=A8mpv=20merge-files=E6=92=AD=E6=94=BE?= =?UTF-8?q?=E8=A7=86=E9=A2=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/you_get/extractors/qq.py | 257 ++++++++++++++++++++++++++--------- 1 file changed, 194 insertions(+), 63 deletions(-) diff --git a/src/you_get/extractors/qq.py b/src/you_get/extractors/qq.py index f2c3d9ec..e4608d3b 100644 --- a/src/you_get/extractors/qq.py +++ b/src/you_get/extractors/qq.py @@ -1,73 +1,203 @@ #!/usr/bin/env python - __all__ = ['qq_download'] from ..common import * -from .qie import download as qieDownload +from .qie import download as qieDownload, VideoExtractor from urllib.parse import urlparse,parse_qs +import struct +import base64 +import random +import time -def qq_download_by_vid(vid, title, output_dir='.', merge=True, info_only=False): - info_api = 'http://vv.video.qq.com/getinfo?otype=json&appver=3%2E2%2E19%2E333&platform=11&defnpayver=1&vid=' + vid - info = get_html(info_api) - video_json = json.loads(match1(info, r'QZOutputJson=(.*)')[:-1]) - parts_vid = video_json['vl']['vi'][0]['vid'] - parts_ti = video_json['vl']['vi'][0]['ti'] - parts_prefix = video_json['vl']['vi'][0]['ul']['ui'][0]['url'] - parts_formats = video_json['fl']['fi'] - # find best quality - # only looking for fhd(1080p) and shd(720p) here. - # 480p usually come with a single file, will be downloaded as fallback. - best_quality = '' - for part_format in parts_formats: - if part_format['name'] == 'fhd': - best_quality = 'fhd' - break +DELTA = 0x9e3779b9 +ROUNDS = 16 - if part_format['name'] == 'shd': - best_quality = 'shd' +SALT_LEN = 2 +ZERO_LEN = 7 - for part_format in parts_formats: - if (not best_quality == '') and (not part_format['name'] == best_quality): - continue - part_format_id = part_format['id'] - part_format_sl = part_format['sl'] - if part_format_sl == 0: - part_urls= [] - total_size = 0 - try: - # For fhd(1080p), every part is about 100M and 6 minutes - # try 100 parts here limited download longest single video of 10 hours. - for part in range(1,100): - filename = vid + '.p' + str(part_format_id % 1000) + '.' + str(part) + '.mp4' - key_api = "http://vv.video.qq.com/getkey?otype=json&platform=11&format=%s&vid=%s&filename=%s" % (part_format_id, parts_vid, filename) - #print(filename) - #print(key_api) - part_info = get_html(key_api) - key_json = json.loads(match1(part_info, r'QZOutputJson=(.*)')[:-1]) - #print(key_json) - vkey = key_json['key'] - url = '%s/%s?vkey=%s' % (parts_prefix, filename, vkey) - part_urls.append(url) - _, ext, size = url_info(url, faker=True) - total_size += size - except: - pass - print_info(site_info, parts_ti, ext, total_size) - if not info_only: - download_urls(part_urls, parts_ti, ext, total_size, output_dir=output_dir, merge=merge) - else: - fvkey = video_json['vl']['vi'][0]['fvkey'] - mp4 = video_json['vl']['vi'][0]['cl'].get('ci', None) - if mp4: - mp4 = mp4[0]['keyid'].replace('.10', '.p') + '.mp4' - else: - mp4 = video_json['vl']['vi'][0]['fn'] - url = '%s/%s?vkey=%s' % ( parts_prefix, mp4, fvkey ) - _, ext, size = url_info(url, faker=True) +SEED = 0xdead - print_info(site_info, title, ext, size) - if not info_only: - download_urls([url], title, ext, size, output_dir=output_dir, merge=merge) +def rand(): + global SEED + if SEED == 0: + SEED = 123459876 + k1 = 0xffffffff & (-2836 * (SEED // 127773)) + k2 = 0xffffffff & (16807 * (SEED % 127773)) + SEED = 0xffffffff & (k1 + k2) + if SEED < 0: + SEED = SEED + 2147483647 + return SEED + +def pack(data): + target = [] + for i in data: + target.extend(struct.pack('>I', i)) + return target + +def unpack(data): + data = bytes(data) + target = [] + for i in range(0, len(data), 4): + target.extend(struct.unpack('>I', data[i:i+4])) + return target + +def tea_encrypt(v, key): + s = 0 + key = unpack(key) + v = unpack(v) + for i in range(ROUNDS): + s += DELTA + s &= 0xffffffff + v[0] += (v[1]+s) ^ ((v[1]>>5)+key[1]) ^ ((v[1]<<4)+key[0]) + v[0] &= 0xffffffff + v[1] += (v[0]+s) ^ ((v[0]>>5)+key[3]) ^ ((v[0]<<4)+key[2]) + v[1] &= 0xffffffff + return pack(v) + +def oi_symmetry_encrypt2(raw_data, key): + pad_salt_body_zero_len = 1 + SALT_LEN + len(raw_data) + ZERO_LEN + pad_len = pad_salt_body_zero_len % 8 + if pad_len: + pad_len = 8 - pad_len + data = [] + data.append(rand() & 0xf8 | pad_len) + while pad_len + SALT_LEN: + data.append(rand() & 0xff) + pad_len = pad_len - 1 + data.extend(raw_data) + data.extend([0x00] * ZERO_LEN) + + temp = [0x00] * 8 + enc = tea_encrypt(data[:8], key) + for i in range(8, len(data), 8): + d1 = data[i:] + for j in range(8): + d1[j] = d1[j] ^ enc[i-8+j] + d1 = tea_encrypt(d1, key) + for j in range(8): + d1[j] = d1[j] ^ data[i-8+j] ^ temp[j] + enc.append(d1[j]) + temp[j] = enc[i-8+j] + return enc + + +KEY = [ + 0xfa, 0x82, 0xde, 0xb5, 0x2d, 0x4b, 0xba, 0x31, + 0x39, 0x6, 0x33, 0xee, 0xfb, 0xbf, 0xf3, 0xb6 +] + +def packstr(data): + l = len(data) + t = [] + t.append((l&0xFF00) >> 8) + t.append(l&0xFF) + t.extend([ord(c) for c in data]) + return t + +def strsum(data): + s = 0 + for c in data: + s = s*131 + c + return 0x7fffffff & s + +def echo_ckeyv3(vid, guid='', t=None, player_version='3.2.38.401', platform=10902, stdfrom='bcng'): + data = [] + data.extend(pack([21507, 3168485562])) + data.extend(pack([platform])) + + if not t: + t = time.time() + seconds = int(t) + microseconds = int(1000000*(t - int(t))) + data.extend(pack([microseconds, seconds])) + data.extend(packstr(stdfrom)) + + r = random.random() + data.extend(packstr('%.16f' % r)) + data.extend(packstr(player_version)) + data.extend(packstr(vid)) + data.extend(packstr('2%s' % guid)) + data.extend(packstr('4null')) + data.extend(packstr('4null')) + data.extend([0x00, 0x00, 0x00, 0x01]) + data.extend([0x00, 0x00, 0x00, 0x00]) + + l = len(data) + data.insert(0, l&0xFF) + data.insert(0, (l&0xFF00) >> 8) + + enc = oi_symmetry_encrypt2(data, KEY) + + pad = [0x00, 0x00, 0x00, 0x00, 0xff&rand(), 0xff&rand(), 0xff&rand(), 0xff&rand()] + pad[0] = pad[4] ^ 71 & 0xFF + pad[1] = pad[5] ^ -121 & 0xFF + pad[2] = pad[6] ^ -84 & 0xFF + pad[3] = pad[7] ^ -86 & 0xFF + + pad.extend(enc) + pad.extend(pack([strsum(data)])) + + result = base64.b64encode(bytes(pad), b'_-').decode('utf-8').replace('=', '') + return result + + +class QQ(VideoExtractor): + name = "腾讯视频(qq)" + + stream_types = [ + {'id': 'fhd'}, + {'id': 'shd'}, + {'id': 'hd'}, + {'id': 'sd'}, + ] + + def _getvkey(self, vid, format, idx): + import uuid + appver = '3.2.38.401' + guid = uuid.uuid4().hex + platform = 11 + cKey = echo_ckeyv3(vid=vid, guid=guid, player_version=appver, platform=platform) + key_api = 'http://vv.video.qq.com/getvkey?vid={vid}&appver={appver}&platform={platform}&otype=json&filename={vid}.p{format1000}.{idx}.mp4&format={format}&cKey={cKey}&guid={guid}&charge=0&encryptVer=5.4&lnk={vid}'.format(vid=vid, appver=appver, format1000=format%1000, format=format, cKey=cKey, guid=guid, platform=platform, idx=idx) + part_info = get_html(key_api) + key_json = json.loads(match1(part_info, r'QZOutputJson=(.*)')[:-1]) + return key_json['key'] + + def prepare(self, **kwargs): + # vid = kwargs['vid'] if 'vid' in kwargs else None + # vid = vid if vid is not None else self.vid + vid = ('vid' in kwargs and kwargs['vid']) or self.vid + if vid is None: + return + + info_api = 'http://vv.video.qq.com/getinfo?otype=json&appver=3%2E2%2E19%2E333&platform=11&defnpayver=1&vid=' + vid + info = get_html(info_api) + video_json = json.loads(match1(info, r'QZOutputJson=(.*)')[:-1]) + vi0 = video_json['vl']['vi'][0] + self.title = vi0['ti'] + url_prefix = vi0['ul']['ui'][0]['url'] + url_prefix = urlparse(url_prefix)._replace(netloc='lmbsy.qq.com').geturl() # fast + # url_prefix = urlparse(url_prefix)._replace(netloc='ugcbsy.qq.com').geturl() # fast + # url_prefix = urlparse(url_prefix)._replace(netloc='vmind.qqvideo.tc.qq.com').geturl() # fast + fi = video_json['fl']['fi'] # streams + self.streams = {} + for f in fi: + urls = ['{prefix}/{vid}.p{format1000}.{idx}.mp4?vkey={vkey}'.format(prefix=url_prefix, vid=vid, format1000=f['id']%1000, idx=idx, vkey=self._getvkey(vid, f['id'], idx)) for idx in range(1, vi0['cl']['fc']+1)] + + # print(vi0['ul']['ui']) + # vkey = self._getvkey(vid, f['id'], 1) + # for ui in vi0['ul']['ui']: + # test = '{prefix}/{vid}.p{format1000}.1.mp4?vkey={vkey}'.format(prefix=ui['url'], vid=vid, format1000=f['id']%1000, vkey=vkey) + # print(test) + + self.streams[f['name']] = { + 'video_profile': f['cname'], + 'size': f['fs'], + 'container': 'mp4', + 'src': urls, + } + + +site = QQ() def qq_download(url, output_dir='.', merge=True, info_only=False, **kwargs): @@ -80,7 +210,7 @@ def qq_download(url, output_dir='.', merge=True, info_only=False, **kwargs): content = get_html(url) vids = matchall(content, [r'\bvid=(\w+)']) for vid in vids: - qq_download_by_vid(vid, vid, output_dir, merge, info_only) + site.download_by_vid(vid=vid, output_dir=output_dir, merge=merge, info_only=info_only, **kwargs) return #do redirect @@ -108,8 +238,9 @@ def qq_download(url, output_dir='.', merge=True, info_only=False, **kwargs): title = match1(content, r'"title":"([^"]+)"') if not title else title title = vid if not title else title #general fallback - qq_download_by_vid(vid, title, output_dir, merge, info_only) + site.download_by_vid(vid=vid, output_dir=output_dir, merge=merge, info_only=info_only, **kwargs) +qq_download_by_vid=site.download_by_vid site_info = "QQ.com" download = qq_download download_playlist = playlist_not_supported('qq')