Files
sg/py/央视网.py
2026-08-14 11:26:45 +02:00

649 lines
27 KiB
Python

import sys
import re
import json
import time
import random
import struct
import requests
from base64 import b64encode, b64decode
from urllib.parse import urlencode
from urllib3 import disable_warnings
from base.spider import Spider
disable_warnings()
class Spider(Spider):
# ==================== 加密常量 ====================
DELTA = 0x9e3779b9
ROUNDS = 16
LOG_ROUNDS = 4
SALT_LEN = 2
ZERO_LEN = 7
TEA_CKEY = bytes.fromhex('59b2f7cf725ef43c34fdd7c123411ed3')
GUARD_TEA_KEY = bytes.fromhex('110DBEC10C23E7D2E56A1CAD6914EF1B')
XOR_KEY = [0x84, 0x2E, 0xED, 0x08, 0xF0, 0x66, 0xE6, 0xEA,
0x48, 0xB4, 0xCA, 0xA9, 0x91, 0xED, 0x6F, 0xF3]
GUARD_XOR_KEY = [0xB3, 0xC9, 0x53, 0xA0, 0x69, 0x13, 0xAD, 0x4D]
STANDARD_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='
CUSTOM_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-='
# ==================== 频道图标 ====================
# 使用 wanglindl/TVlogo 小尺寸台标源(jsdelivr CDN,已验证可用)
# 图标尺寸较小,适合TVBox/直播列表显示
LOGOS = {
'cctv1': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV1.png',
'cctv2': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV2.png',
'cctv3': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV3.png',
'cctv4': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV4.png',
'cctv5': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV5.png',
'cctv5p': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV5plus.png',
'cctv6': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV6.png',
'cctv7': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV7.png',
'cctv8': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV8.png',
'cctv9': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV9.png',
'cctv10': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV10.png',
'cctv11': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV11.png',
'cctv12': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV12.png',
'cctv13': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV13.png',
'cctv14': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV14.png',
'cctv15': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV15.png',
'cctv16': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV16.png',
'cctv164k': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV16.png',
'cctv17': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV17.png',
'cctv4k': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV4K.png',
'cctv8k': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV8K.png',
'cgtn': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CGTN.png',
'cgtnfy': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CGTNfy.png',
'cgtney': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CGTNey.png',
'cgtnalby': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CGTNalby.png',
'cgtnxby': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CGTNxbyy.png',
'cgtnwyjl': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CGTNjilu.png',
'cctvfyjc': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTVfyjc.png',
'cctvdyjc': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTVdyjc.png',
'cctvhjjc': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTVhjjc.png',
'cctvsjdl': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTVsjdl.png',
'cctvfyyy': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTVfyyy.png',
'cctvbqkj': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTVbqkj.png',
'cctvfyzq': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTVfyzq.png',
'cctvgeqwq': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTVgefwq.png',
'cctvnxss': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTVnxss.png',
'cctvyswhjp': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTVyswhjp.png',
'cctvystq': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTVystq.png',
'cctvdszn': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTVdszn.png',
'cctvwsjk': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTVwsjk.png',
'bjws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Beijing.png',
'jsws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Jiangsu.png',
'dfws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Dongfang.png',
'zjws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Zhejiang.png',
'hnws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Hunan.png',
'hbws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Hubei.png',
'gdws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Guangdong.png',
'gxws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Guangxi.png',
'hljws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Heilongjiang.png',
'hnws2': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Hainan.png',
'cqws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Chongqing.png',
'szws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Shenzhen.png',
'scws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Sichuan.png',
'henanws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Henan.png',
'fjdnhz': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Dongnan.png',
'gzhws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Guizhou.png',
'jxws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Jiangxi.png',
'lnws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Liaoning.png',
'ahws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Anhui.png',
'hbws2': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Hebei.png',
'sdws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Shandong.png',
'tjws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Tianjin.png',
'jlws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Jilin.png',
'shanxiws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Shanxi.png',
'nxws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Ningxia.png',
'nmgws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Neimeng.png',
'ynws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Yunnan.png',
'shanxiws2': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Shanxi_.png',
'qhws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Qinghai.png',
'xzws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Xizang.png',
'xjws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Xinjiang.png',
'cetv1': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CETV1.png',
'gxpd': '',
}
# ==================== 频道数据 ====================
# {key: [cnlid, livepid, defn, display_name]}
CHANNELS = {
'cctv1': ['2024078201', '600001859', 'fhd', 'CCTV-1'],
'cctv2': ['2024075401', '600001800', 'fhd', 'CCTV-2'],
'cctv3': ['2024068501', '600001801', 'fhd', 'CCTV-3'],
'cctv4': ['2029797101', '600001814', 'fhd', 'CCTV-4'],
'cctv5': ['2024078401', '600001818', 'fhd', 'CCTV-5'],
'cctv5p': ['2024078001', '600001817', 'fhd', 'CCTV-5+'],
'cctv6': ['2013693901', '600108442', 'fhd', 'CCTV-6'],
'cctv7': ['2024072001', '600004092', 'fhd', 'CCTV-7'],
'cctv8': ['2029793001', '600001803', 'fhd', 'CCTV-8'],
'cctv9': ['2024078601', '600004078', 'fhd', 'CCTV-9'],
'cctv10': ['2024078701', '600001805', 'fhd', 'CCTV-10'],
'cctv11': ['2027248701', '600001806', 'fhd', 'CCTV-11'],
'cctv12': ['2027248801', '600001807', 'fhd', 'CCTV-12'],
'cctv13': ['2029797201', '600001811', 'fhd', 'CCTV-13'],
'cctv14': ['2027248901', '600001809', 'fhd', 'CCTV-14'],
'cctv15': ['2027249001', '600001815', 'fhd', 'CCTV-15'],
'cctv16': ['2027249101', '600098637', 'fhd', 'CCTV-16'],
'cctv164k': ['2027249301', '600099502', 'fhd', 'CCTV-16(4K)'],
'cctv17': ['2027249401', '600001810', 'fhd', 'CCTV-17'],
'cctv4k': ['2029810301', '600002264', 'fhd', 'CCTV-4K'],
'cctv8k': ['2026774101', '600156816', 'fhd', 'CCTV-8K'],
'cgtn': ['2024181701', '600014550', 'fhd', 'CGTN'],
'cgtnfy': ['2024181801', '600084704', 'fhd', 'CGTN法语'],
'cgtney': ['2024181901', '600084758', 'fhd', 'CGTN俄语'],
'cgtnalby': ['2024182001', '600084782', 'fhd', 'CGTN阿拉伯语'],
'cgtnxby': ['2024182101', '600084744', 'fhd', 'CGTN西班牙语'],
'cgtnwyjl': ['2024182301', '600084781', 'fhd', 'CGTN外语纪录'],
'cctvfyjc': ['2025637103', '600099658', 'shd', '风云剧场'],
'cctvdyjc': ['2026874203', '600099655', 'shd', '第一剧场'],
'cctvhjjc': ['2026874303', '600099620', 'shd', '怀旧剧场'],
'cctvsjdl': ['2026874403', '600099637', 'shd', '世界地理'],
'cctvfyyy': ['2026874503', '600099660', 'shd', '风云音乐'],
'cctvbqkj': ['2026874603', '600099649', 'shd', '兵器科技'],
'cctvfyzq': ['2026966203', '600099636', 'shd', '风云足球'],
'cctvgeqwq': ['2026874703', '600099659', 'shd', '高尔夫·网球'],
'cctvnxss': ['2026874803', '600099650', 'shd', '女性时尚'],
'cctvyswhjp': ['2026874903', '600099653', 'shd', '央视文化精品'],
'cctvystq': ['2026875003', '600099652', 'shd', '央视台球'],
'cctvdszn': ['2026875103', '600099656', 'shd', '电视指南'],
'cctvwsjk': ['2025637003', '600099651', 'shd', '卫生健康'],
'bjws': ['2024052703', '600002309', 'fhd', '北京卫视'],
'jsws': ['2024171103', '600002521', 'fhd', '江苏卫视'],
'dfws': ['2024054503', '600002483', 'fhd', '东方卫视'],
'zjws': ['2024054703', '600002520', 'fhd', '浙江卫视'],
'hnws': ['2024054803', '600002475', 'fhd', '湖南卫视'],
'hbws': ['2024171203', '600002508', 'fhd', '湖北卫视'],
'gdws': ['2024060903', '600002485', 'fhd', '广东卫视'],
'gxws': ['2024060703', '600002509', 'fhd', '广西卫视'],
'hljws': ['2029797003', '600002498', 'fhd', '黑龙江卫视'],
'hnws2': ['2024055603', '600002506', 'fhd', '海南卫视'],
'cqws': ['2024061103', '600002531', 'fhd', '重庆卫视'],
'szws': ['2024061303', '600002481', 'fhd', '深圳卫视'],
'scws': ['2024061403', '600002516', 'fhd', '四川卫视'],
'henanws': ['2029797303', '600002525', 'fhd', '河南卫视'],
'fjdnhz': ['2024061503', '600002484', 'fhd', '福建东南卫视'],
'gzhws': ['2024061603', '600002490', 'fhd', '贵州卫视'],
'jxws': ['2024061703', '600002503', 'fhd', '江西卫视'],
'lnws': ['2024171303', '600002505', 'fhd', '辽宁卫视'],
'ahws': ['2024171403', '600002532', 'fhd', '安徽卫视'],
'hbws2': ['2024171503', '600002493', 'fhd', '河北卫视'],
'sdws': ['2029787903', '600002513', 'fhd', '山东卫视'],
'tjws': ['2019927003', '600152137', 'fhd', '天津卫视'],
'jlws': ['2025561503', '600190405', 'fhd', '吉林卫视'],
'shanxiws': ['2029795103', '600190400', 'fhd', '陕西卫视'],
'nxws': ['2025608503', '600190737', 'fhd', '宁夏卫视'],
'nmgws': ['2025561203', '600190401', 'fhd', '内蒙古卫视'],
'ynws': ['2025561303', '600190402', 'fhd', '云南卫视'],
'shanxiws2': ['2025560803', '600190407', 'fhd', '山西卫视'],
'qhws': ['2025559103', '600190406', 'fhd', '青海卫视'],
'xzws': ['2025558003', '600190403', 'fhd', '西藏卫视'],
'xjws': ['2019927403', '600152138', 'fhd', '新疆卫视'],
'cetv1': ['2022823801', '600171827', 'fhd', '中国教育电视台'],
'gxpd': ['2029360403', '600213139', 'fhd', '国学频道'],
}
# ==================== 分类定义 ====================
CATS = [
{
'name': '央视',
'ids': [
'cctv1', 'cctv2', 'cctv3', 'cctv4', 'cctv5', 'cctv5p',
'cctv6', 'cctv7', 'cctv8', 'cctv9', 'cctv10', 'cctv11',
'cctv12', 'cctv13', 'cctv14', 'cctv15', 'cctv16', 'cctv164k',
'cctv17', 'cctv4k', 'cctv8k',
]
},
{
'name': 'CGTN',
'ids': ['cgtn', 'cgtnfy', 'cgtney', 'cgtnalby', 'cgtnxby', 'cgtnwyjl']
},
{
'name': '央视付费',
'ids': [
'cctvfyjc', 'cctvdyjc', 'cctvhjjc', 'cctvsjdl', 'cctvfyyy',
'cctvbqkj', 'cctvfyzq', 'cctvgeqwq', 'cctvnxss', 'cctvyswhjp',
'cctvystq', 'cctvdszn', 'cctvwsjk',
]
},
{
'name': '卫视',
'ids': [
'bjws', 'jsws', 'dfws', 'zjws', 'hnws', 'hbws', 'gdws',
'gxws', 'hljws', 'hnws2', 'cqws', 'szws', 'scws', 'henanws',
'fjdnhz', 'gzhws', 'jxws', 'lnws', 'ahws', 'hbws2', 'sdws',
'tjws', 'jlws', 'shanxiws', 'nxws', 'nmgws', 'ynws',
'shanxiws2', 'qhws', 'xzws', 'xjws',
]
},
{
'name': '其他',
'ids': ['cetv1', 'gxpd']
},
]
# ==================== 初始化 ====================
def init(self, extend=""):
self.guid = self._gen_guid()
self.session = requests.Session()
self.session.verify = False
self.session.headers.update({
'User-Agent': 'qqlive',
'Connection': 'Keep-Alive',
'Accept': 'application/json',
})
def getName(self):
return "央视频"
# ==================== 蜘蛛壳接口 ====================
def homeContent(self, filter):
classes = [
{'type_id': str(i), 'type_name': c['name']}
for i, c in enumerate(self.CATS)
]
return {'class': classes}
def categoryContent(self, tid, pg, filter, extend):
cat = self.CATS[int(tid)]
vod_list = []
for cid in cat['ids']:
ch = self.CHANNELS.get(cid)
if ch:
vod_list.append({
'vod_id': cid,
'vod_name': ch[3],
'vod_pic': self.LOGOS.get(cid, ''),
'vod_remarks': ch[2].upper(),
})
return {
'page': 1,
'pagecount': 1,
'limit': len(vod_list),
'total': len(vod_list),
'list': vod_list,
}
def detailContent(self, ids):
cid = ids[0]
ch = self.CHANNELS.get(cid)
if not ch:
return {'list': []}
vod = {
'vod_id': cid,
'vod_name': ch[3],
'vod_pic': self.LOGOS.get(cid, ''),
'vod_play_from': '央视频',
'vod_play_url': f'直播${cid}',
'vod_content': f'{ch[3]} 高清直播',
}
return {'list': [vod]}
def playerContent(self, flag, id, vipFlags):
ch = self.CHANNELS.get(id)
if not ch:
return {'parse': 0, 'url': '', 'header': ''}
play_url = self._get_play_url(ch[0], ch[1], ch[2])
if play_url:
return {'parse': 0, 'url': play_url, 'header': ''}
return {'parse': 0, 'url': '', 'header': ''}
# ==================== 播放地址获取 ====================
def _get_play_url(self, cnlid, livepid, defn):
try:
self.guid = self._gen_guid()
ck = self._gen_ckey(cnlid)
flowid = self._gen_flowid()
params = {
"atime": "120",
"livepid": livepid,
"cnlid": cnlid,
"appVer": "V8.22.1035.3031",
"app_version": "300090",
"caplv": "1",
"cmd": "2",
"defn": defn,
"device": "iPhone",
"encryptVer": "4.2",
"getpreviewinfo": "0",
"hevclv": "33",
"lang": "zh-Hans_JP",
"livequeue": "0",
"logintype": "1",
"nettype": "1",
"newnettype": "1",
"newplatform": "4330403",
"platform": "4330403",
"sdtfrom": "v3021",
"spacode": "23",
"spaudio": "1",
"spdemuxer": "6",
"spdrm": "2",
"spdynamicrange": "7",
"spflv": "1",
"spflvaudio": "1",
"sphdrfps": "60",
"sphttps": "0",
"spvcode": "MSgzMDoyMTYwLDYwOjIxNjB8MzA6MjE2MCw2MDoyMTYwKTsyKDMwOjIxNjAsNjA6MjE2MHwzMDoyMTYwLDYwOjIxNjAp",
"spvideo": "4",
"stream": "1",
"system": "1",
"sysver": "ios18.2.1",
"uhd_flag": "4",
"cKey": ck['ckey'],
"guid": self.guid,
"fntick": ck['params']['Timestamp'],
"flowid": flowid,
"playbacktime": "0",
}
resp = self.session.get(
"https://bkliveinfo.ysp.cctv.cn",
params=params,
timeout=15,
)
data = resp.json()
if data.get('iretcode') == 0 and data.get('playurl'):
return data['playurl']
except Exception as e:
print(f"[央视频] 获取播放地址失败: {e}")
return None
# ==================== GUID / FlowID ====================
def _gen_guid(self):
return '{:08x}{:04x}{:04x}{:04x}{:012x}'.format(
random.randint(0, 0xffffffff),
random.randint(0, 0xffff),
random.randint(0, 0xffff),
random.randint(0, 0xffff),
random.randint(0, 0xffffffffffff),
)
def _gen_flowid(self):
p = [
random.randint(0, 0xffff), random.randint(0, 0xffff),
random.randint(0, 0xffff),
random.randint(0, 0x0fff) | 0x4000,
random.randint(0, 0x3fff) | 0x8000,
random.randint(0, 0xffff), random.randint(0, 0xffff),
random.randint(0, 0xffff),
]
return '{:04X}{:04X}-{:04X}-{:04X}-{:04X}-{:04X}{:04X}{:04X}_4330403'.format(*p)
# ==================== 签名 ====================
def _calc_sig(self, buf):
s = 0
for b in buf:
s = (0x83 * s + (b & 0xFF)) & 0x7FFFFFFF
return s
# ==================== 自定义 Base64 ====================
def _b64enc(self, data):
enc = b64encode(data).decode()
return enc.translate(str.maketrans(self.STANDARD_ALPHABET, self.CUSTOM_ALPHABET)).rstrip('=')
def _b64dec(self, text):
text = text.rstrip('=')
if len(text) % 4:
text += '=' * (4 - len(text) % 4)
return b64decode(text.translate(str.maketrans(self.CUSTOM_ALPHABET, self.STANDARD_ALPHABET)))
# ==================== XOR ====================
def _xor(self, arr):
return [arr[i] ^ self.XOR_KEY[i & 0xF] for i in range(len(arr))]
# ==================== TEA ====================
def _tea_enc(self, data, key):
if len(data) < 8:
data = data.ljust(8, b'\0')
y, z = struct.unpack('>II', data[:8])
k = struct.unpack('>4I', key[:16])
s = 0
for _ in range(self.ROUNDS):
s = (s + self.DELTA) & 0xFFFFFFFF
y = (y + (((z << 4) + k[0]) ^ (z + s) ^ ((z >> 5) + k[1]))) & 0xFFFFFFFF
z = (z + (((y << 4) + k[2]) ^ (y + s) ^ ((y >> 5) + k[3]))) & 0xFFFFFFFF
return struct.pack('>II', y, z)
def _tea_dec(self, data, key):
y, z = struct.unpack('>II', data[:8])
k = struct.unpack('>4I', key[:16])
s = (self.DELTA << self.LOG_ROUNDS) & 0xFFFFFFFF
for _ in range(self.ROUNDS):
z = (z - (((y << 4) + k[2]) ^ (y + s) ^ ((y >> 5) + k[3]))) & 0xFFFFFFFF
y = (y - (((z << 4) + k[0]) ^ (z + s) ^ ((z >> 5) + k[1]))) & 0xFFFFFFFF
s = (s - self.DELTA) & 0xFFFFFFFF
return struct.pack('>II', y, z)
# ==================== CBC 加密 ====================
def _cbc_enc(self, p_in, n_len, p_key):
pad_salt_zero = n_len + 1 + self.SALT_LEN + self.ZERO_LEN
n_pad = pad_salt_zero % 8
if n_pad:
n_pad = 8 - n_pad
out = b''
src = [0] * 8
src[0] = (random.randint(0, 255) & 0xF8) | n_pad
si = 1
while n_pad:
src[si] = random.randint(0, 255)
si += 1
n_pad -= 1
iv_p = [0] * 8
iv_c = [0] * 8
# salt
i = 0
while i < self.SALT_LEN:
if si < 8:
src[si] = random.randint(0, 255)
si += 1
i += 1
if si == 8:
for j in range(8):
src[j] ^= iv_c[j]
tb = list(self._tea_enc(bytes(src), p_key))
for j in range(8):
tb[j] ^= iv_p[j]
iv_p = list(src)
iv_c = list(tb)
out += bytes(tb)
si = 0
# body
pi = 0
while n_len:
if si < 8:
src[si] = p_in[pi]
pi += 1
si += 1
n_len -= 1
if si == 8:
for j in range(8):
src[j] ^= iv_c[j]
tb = list(self._tea_enc(bytes(src), p_key))
for j in range(8):
tb[j] ^= iv_p[j]
iv_p = list(src)
iv_c = list(tb)
out += bytes(tb)
si = 0
# zero
i = 0
while i < self.ZERO_LEN:
if si < 8:
src[si] = 0
si += 1
i += 1
if si == 8:
for j in range(8):
src[j] ^= iv_c[j]
tb = list(self._tea_enc(bytes(src), p_key))
for j in range(8):
tb[j] ^= iv_p[j]
iv_p = list(src)
iv_c = list(tb)
out += bytes(tb)
si = 0
# last
if si > 0:
for j in range(si, 8):
src[j] = 0
for j in range(8):
src[j] ^= iv_c[j]
tb = list(self._tea_enc(bytes(src), p_key))
for j in range(8):
tb[j] ^= iv_p[j]
out += bytes(tb)
return out
# ==================== CBC 解密 ====================
def _cbc_dec(self, p_in, n_len, p_key):
if n_len % 8 != 0 or n_len < 16:
return None
dest = list(self._tea_dec(p_in[:8], p_key))
n_pad = dest[0] & 0x07
out_len = n_len - 1 - n_pad - self.SALT_LEN - self.ZERO_LEN
if out_len < 0:
return None
iv_pre = [0] * 8
iv_cur = list(p_in[:8])
off = 8
di = 1 + n_pad
# skip salt
sc = 1
while sc <= self.SALT_LEN:
if di < 8:
di += 1
sc += 1
elif di == 8:
iv_pre = list(iv_cur)
if off + 8 > n_len:
return None
iv_cur = list(p_in[off:off + 8])
for j in range(8):
dest[j] ^= iv_cur[j]
dest = list(self._tea_dec(bytes(dest), p_key))
off += 8
di = 0
# copy plain
plain = []
rem = out_len
while rem > 0:
if di < 8:
plain.append(dest[di] ^ iv_pre[di])
di += 1
rem -= 1
elif di == 8:
iv_pre = list(iv_cur)
if off + 8 > n_len:
return None
iv_cur = list(p_in[off:off + 8])
for j in range(8):
dest[j] ^= iv_cur[j]
dest = list(self._tea_dec(bytes(dest), p_key))
off += 8
di = 0
return bytes(plain)
# ==================== Guard Time ====================
def _last5(self, v):
v = str(v)
return v[-5:] if len(v) >= 5 else ''
def _gen_guard_time(self, ts, guid):
body = struct.pack('>I', ts)
for part in [self._last5(guid), self._last5('null'), self._last5('null'), '-1']:
pb = part.encode()
body += struct.pack('>H', len(pb)) + pb
plain = struct.pack('>H', len(body)) + body
chk = self._calc_sig(list(plain))
enc = self._cbc_enc(plain, len(plain), self.GUARD_TEA_KEY) + struct.pack('>I', chk)
el = list(enc)
for i in range(len(el)):
el[i] ^= self.GUARD_XOR_KEY[i & 7]
return bytes(el).hex().upper()
# ==================== CKey ====================
def _encrypt_ckey(self, data):
chk = self._calc_sig(list(data))
enc = self._cbc_enc(data, len(data), self.TEA_CKEY) + struct.pack('>I', chk)
return '--01' + self._b64enc(bytes(self._xor(list(enc))))
def _build_pkt(self, params):
d = b''
d += bytes.fromhex('0000004200000004000004d2') # 12-byte header
d += struct.pack('>I', params['Platform'])
d += struct.pack('>I', 0) # sig placeholder
d += struct.pack('>I', params['Timestamp'])
for k in ['Sdtfrom', 'randFlag', 'appVer', 'vid', 'guid']:
v = params[k].encode()
d += struct.pack('>H', len(v)) + v
d += struct.pack('>I', 1) # part1
d += struct.pack('>I', 1) # isDlna
for v in [b'2622783A', b'nil']:
d += struct.pack('>H', len(v)) + v
uuid4 = params['uuid4'].encode()
d += struct.pack('>H', len(uuid4)) + uuid4
d += struct.pack('>H', 3) + b'nil' # bundleID1
for v in [b'v0.1.000', b'com.cctv.yangshipin.app.iphone',
b'4330403', b'ex_json_bus', b'ex_json_vs']:
d += struct.pack('>H', len(v)) + v
cgt = params['ck_guard_time'].encode()
d += struct.pack('>H', len(cgt)) + cgt
buf = struct.pack('>H', len(d)) + d
sig = self._calc_sig(list(buf))
return buf[:18] + struct.pack('>I', sig) + buf[22:]
def _gen_ckey(self, cnlid):
ts = int(time.time())
cgt = self._gen_guard_time(ts, self.guid)
params = {
'Platform': 4330403,
'Timestamp': ts,
'Sdtfrom': 'dcgh',
'vid': cnlid,
'guid': self.guid,
'appVer': 'V8.22.1035.3031',
'randFlag': '_zj1A5Gh6QYcxWjIUGos2w==',
'uuid4': '57eab0c4-2c58-44c6-8ae9-dd2757525dc5',
'ck_guard_time': cgt,
}
pkt = self._build_pkt(params)
return {'ckey': self._encrypt_ckey(pkt), 'params': params}