318 lines
13 KiB
Python
318 lines
13 KiB
Python
import json
|
|
import random
|
|
import sys
|
|
from base64 import b64encode, b64decode
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
# 引入 RSA 加解密所需模块
|
|
from Crypto.PublicKey import RSA
|
|
from Crypto.Cipher import PKCS1_v1_5
|
|
sys.path.append('..')
|
|
from base.spider import Spider
|
|
class Spider(Spider):
|
|
def init(self, extend=""):
|
|
did = self.getdid()
|
|
self.headers.update({'deviceId': did})
|
|
token = self.gettk()
|
|
self.headers.update({'token': token})
|
|
def getName(self):
|
|
pass
|
|
def isVideoFormat(self, url):
|
|
pass
|
|
def manualVideoCheck(self):
|
|
pass
|
|
def destroy(self):
|
|
pass
|
|
# 1. 修改为主机域名
|
|
host = 'http://qkys.qukanwh.com'
|
|
|
|
# 2. 同步原脚本的配置请求头
|
|
headers = {
|
|
'HOST': 'qkys.qukanwh.com',
|
|
'User-Agent': 'okhttp/4.12.0',
|
|
'client': 'app',
|
|
'deviceType': 'Android',
|
|
'Referer': ''
|
|
}
|
|
# 3. 导入原脚本中的 RSA 密钥对与配置
|
|
publicKey_str = "-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCoYt0BP77U+DM08BiI/QbSRIfxijXo85BTPqIM1Ow8BNwhLETzRIZ+dEwdWDbydG/PspgBAfRpGaYVdJYtvaC2JnoO8+Ik6qMWojfEJxSFLa0Pb0A892tun4gsxoEMjcreZ+YGyaBxAfqX0BSMfdrOgIYaZQjYrw9TRLlUT31QoQIDAQAB\n-----END PUBLIC KEY-----"
|
|
privateKey_str = "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCquQQ5r6+yJI8CDFkXRp8vUsdD45ov8EP12ooLs56ca2DQXaSNGS9910bAPVA9chkp0mKIvKqjAsHz5Tl9EeNPblarGEeJUIxpxZtiSqNTpvtiD/TjhpzuHYic7RAfQ/h7p/ypE8ymU42pYjsB5t26Mv6XgkLV+jzrSf73HlCuS0iMyLmt6zz3Mw9izM13EpB8iFLtfbbYymycKTx4RAmPQLwhNGex/AlUIYxXP4R2yyaa4W6mEtc6aME2QuzJFxPgP3HJ9NBx/LWVn4skxWjZ7zg+VRQRHnjyVaSLu3Z5gN5ITWCyE32qaHJa6WBahZj5jWhRyAG1bQ+xKJa8lBL5AgMBAAECggEAUwv9SjJ0PSwbhNuM2w23kcWquROWhYtTA91zGY4esehqB/IFgb2mpIh8Gje5OKqwIu/8jpd4SiOlRYdUF8sD0DfUYRZGdj2AkFNX6tBz8tVfo6wvbB6naA1lzzBij1L5JO3qsjS3cJFkb+kg2yP66AC2Z+0tpfk8eRhdtshAZwfcd1DEGt1uAvYL1eaUK9HRvpt9lPeGcHERDl2hBd4uyaF0K1O+zF9y59nYbTySWPxRZq3sFEE85xRMlstD7YZi7W2gKvMFRD4/FKmrZ3m7aKJRITtyKOyyPcYmepNv3Qv7kk59Pg38n2WWQ0Ra/bCH3E48YNCnQvZMpitkTfJhoQKBgQDbnROOYTP8OTJ6f/qhoGjxeO3x1VOaOp8l0x7b0SCfoqNGS0Cyiqj72BmJtPMPqSTjn6MmNzqbg1KOdhXyzNozs+i5ccW1M56j96mr5I/Z0FpE3oyIHNfDDBlf9M8YQqEF9oYxniYYft9oapO7cRQkHER6qpvnHTavwlv4m78CXwKBgQDHAjs2YlpKDdI1lcbZJCc7TwtH+Pd2bUki8YXafWNcPhITQHbOZjr310eK1QJC6GJncjkOqbX7yv3ivvTO35FZTQhuA1xEG1P00FG8bE0tHYPIwQHi9y0eA5cieMdo8E6XYria1mw/3fqSQEsfZyJlR32JQIoGAipM8iO1X2nZpwKBgDkMFIhnt5lNQk+P7wsNIDWZtDWdtJnboHuy29E+Abt2A/O+mI/IdRz2hau/1WO8DFkUnszOi+rZshhPlGP90rCbi1igtTrcrdjp/KkqNjPea5R4OwkgdOu1uOG0NheXNzzVTQaWjk7Opjn5dWa7eP/oV+GFb/oZHJuLYVizHGsBAoGADA7rjZEKDYCm4w5PPSr+oY5ZjaPdQrS+gLqHtMRyN82fBMGcMUdqfUfzEstzVqCEDeaS5HuOBlK3bXzKkppjUTjksN3NQmcxgBz7RuJ9DqXCLXDcb2cwuafYCYOt+YLOEEgwDVm+t2P44dG5e46hO+fICH/7nP+WlpD5buz4GfMCgYB57r3g/6hi9WUDnfc7ZAzWMqR0EhJVYKYy+KFEtdIPzhkkIHq5RASe88E9kzoGoZFdb3tIjvGZWcHerirrqWkMsuQtP/Qi0zjieid5tAPj+r4kbiCVTw0E0jnmPBzGInQi7lpeTTKnG1fbyS5lBS+WmHfIuzpECgCkxhaT+LJJkg==\n-----END PRIVATE KEY-----"
|
|
# RSA 公钥加密实现
|
|
def rsa_encrypt(self, text):
|
|
try:
|
|
key = RSA.import_key(self.publicKey_str)
|
|
cipher = PKCS1_v1_5.new(key)
|
|
cipher_text = cipher.encrypt(text.encode('utf-8'))
|
|
return b64encode(cipher_text).decode('utf-8')
|
|
except Exception as e:
|
|
print(f"RSA加密失败: {e}")
|
|
return ""
|
|
|
|
# RSA 私钥解密实现
|
|
def rsa_decrypt(self, text):
|
|
try:
|
|
key = RSA.import_key(self.privateKey_str)
|
|
cipher = PKCS1_v1_5.new(key)
|
|
raw_bytes = b64decode(text.encode('utf-8'))
|
|
|
|
decrypted = b""
|
|
offset = 0
|
|
while offset < len(raw_bytes):
|
|
chunk = raw_bytes[offset:offset + 256]
|
|
decrypted += cipher.decrypt(chunk, None)
|
|
offset += 256
|
|
return decrypted.decode('utf-8')
|
|
except Exception as e:
|
|
print(f"RSA解密失败: {e}")
|
|
return ""
|
|
|
|
def homeContent(self, filter):
|
|
data = self.post(f"{self.host}/api/v1/app/screen/screenType", headers=self.headers).json()
|
|
result = {}
|
|
cate = {
|
|
"类型": "type",
|
|
"地区": "area",
|
|
"年份": "year"
|
|
}
|
|
sort = {
|
|
'key': 'sort',
|
|
'name': '排序',
|
|
'value': [{'n': '最新', 'v': 'NEWEST'}, {'n': '热门', 'v': 'HOT'}, {'n': '收藏', 'v': 'COLLECT'}]
|
|
}
|
|
classes = []
|
|
filters = {}
|
|
for k in data.get('data', []):
|
|
classes.append({
|
|
'type_name': k['name'],
|
|
'type_id': str(k['id'])
|
|
})
|
|
filters[str(k['id'])] = []
|
|
for v in k.get('children', []):
|
|
if v['name'] in cate:
|
|
filters[str(k['id'])].append({
|
|
'name': v['name'],
|
|
'key': cate[v['name']],
|
|
'value': [{'n': i['name'], 'v': i['name']} for i in v.get('children', [])]
|
|
})
|
|
filters[str(k['id'])].append(sort)
|
|
result['class'] = classes
|
|
result['filters'] = filters
|
|
return result
|
|
|
|
def homeVideoContent(self):
|
|
jdata = {
|
|
"condition": {
|
|
"sreecnTypeEnum": "NEWEST"
|
|
},
|
|
"pageNum": 1,
|
|
"pageSize": 40
|
|
}
|
|
data = self.post(f"{self.host}/api/v1/app/screen/screenMovie", headers=self.headers, json=jdata).json()
|
|
return {'list': self.getlist(data.get('data', {}).get('records', []))}
|
|
|
|
def categoryContent(self, tid, pg, filter, extend):
|
|
# 保持最纯粹的条件字段,移除任何空字符串占位
|
|
condition = {
|
|
'sreecnTypeEnum': 'NEWEST',
|
|
'typeId': int(tid) if str(tid).isdigit() else tid
|
|
}
|
|
|
|
if extend:
|
|
if 'sort' in extend:
|
|
condition['sreecnTypeEnum'] = extend.pop('sort')
|
|
condition.update(extend)
|
|
|
|
jdata = {
|
|
'condition': condition,
|
|
'pageNum': int(pg),
|
|
'pageSize': 40,
|
|
}
|
|
|
|
try:
|
|
data = self.post(f"{self.host}/api/v1/app/screen/screenMovie", headers=self.headers, json=jdata).json()
|
|
result = {}
|
|
if data and data.get('data') and 'records' in data['data']:
|
|
result['list'] = self.getlist(data['data']['records'])
|
|
else:
|
|
result['list'] = []
|
|
result['page'] = pg
|
|
result['pagecount'] = 9999
|
|
result['limit'] = 40
|
|
result['total'] = 999999
|
|
return result
|
|
except Exception as e:
|
|
print(f"分类获取错误: {e}")
|
|
return {'list': [], 'page': pg}
|
|
|
|
def detailContent(self, ids):
|
|
ids = ids[0].split('@@')
|
|
jdata = {"id": int(ids[0]), "typeId": ids[-1]}
|
|
v = self.post(f"{self.host}/api/v1/app/play/movieDesc", headers=self.headers, json=jdata).json()
|
|
v = v.get('data', {})
|
|
vod = {
|
|
'type_name': v.get('typeId', ''),
|
|
'vod_year': v.get('year', ''),
|
|
'vod_area': v.get('area', ''),
|
|
'vod_actor': v.get('star', ''),
|
|
'vod_director': v.get('director', ''),
|
|
'vod_content': v.get('introduce', ''),
|
|
'vod_play_from': '',
|
|
'vod_play_url': ''
|
|
}
|
|
|
|
play_params = {
|
|
"id": int(ids[0]),
|
|
"source": 0,
|
|
"typeId": ids[-1]
|
|
}
|
|
encrypt_payload = {"key": self.rsa_encrypt(json.dumps(play_params))}
|
|
|
|
c_res = self.post(f"{self.host}/api/v1/app/play/movieDetails", headers=self.headers, json=encrypt_payload).json()
|
|
decrypted_play_str = self.rsa_decrypt(c_res.get('data', ''))
|
|
if not decrypted_play_str:
|
|
return {'list': [vod]}
|
|
|
|
decrypted_play_data = json.loads(decrypted_play_str)
|
|
l = decrypted_play_data.get('moviePlayerList', [])
|
|
if not l:
|
|
return {'list': [vod]}
|
|
|
|
n = {str(i['id']): i['moviePlayerName'] for i in l}
|
|
|
|
m = play_params.copy()
|
|
m.update({'playerId': l[0]['id']})
|
|
|
|
first_source_payload = {"key": self.rsa_encrypt(json.dumps(m))}
|
|
first_res = self.post(f"{self.host}/api/v1/app/play/movieDetails", headers=self.headers, json=first_source_payload).json()
|
|
|
|
decrypted_first_str = self.rsa_decrypt(first_res.get('data', ''))
|
|
if decrypted_first_str:
|
|
decrypted_first_episode = json.loads(decrypted_first_str)
|
|
pd = self.getv(m, decrypted_first_episode.get('episodeList', []))
|
|
else:
|
|
pd = {}
|
|
|
|
if len(l) > 1:
|
|
with ThreadPoolExecutor(max_workers=len(l)-1) as executor:
|
|
future_to_player = {executor.submit(self.getd, play_params, player): player for player in l[1:]}
|
|
for future in future_to_player:
|
|
try:
|
|
o, p = future.result()
|
|
if p:
|
|
pd.update(self.getv(o, p))
|
|
except Exception as e:
|
|
print(f"多线路请求失败: {e}")
|
|
w, e = [], []
|
|
for i, x in pd.items():
|
|
if x:
|
|
w.append(n.get(i, '未知线路'))
|
|
e.append(x)
|
|
vod['vod_play_from'] = '$$$'.join(w)
|
|
vod['vod_play_url'] = '$$$'.join(e)
|
|
return {'list': [vod]}
|
|
|
|
def searchContent(self, key, quick, pg="1"):
|
|
jdata = {
|
|
"condition": {
|
|
"value": str(key)
|
|
},
|
|
"pageNum": int(pg),
|
|
"pageSize": 40
|
|
}
|
|
try:
|
|
data = self.post(f"{self.host}/api/v1/app/search/searchMovie", headers=self.headers, json=jdata).json()
|
|
return {'list': self.getlist(data.get('data', {}).get('records', [])), 'page': pg}
|
|
except Exception as e:
|
|
print(f"搜索请求失败: {e}")
|
|
return {'list': [], 'page': pg}
|
|
|
|
def playerContent(self, flag, id, vipFlags):
|
|
raw_id_str = self.d64(id)
|
|
if not raw_id_str:
|
|
return {'parse': 0, 'url': ''}
|
|
jdata = json.loads(raw_id_str)
|
|
encrypt_payload = {"key": self.rsa_encrypt(json.dumps(jdata))}
|
|
data = self.post(f"{self.host}/api/v1/app/play/movieDetails", headers=self.headers, json=encrypt_payload).json()
|
|
|
|
try:
|
|
decrypted_url_data = json.loads(self.rsa_decrypt(data.get('data', '')))
|
|
playerUrl = decrypted_url_data.get('url', '')
|
|
if not playerUrl:
|
|
return {'parse': 0, 'url': ''}
|
|
|
|
params = {'playerUrl': playerUrl, 'playerId': jdata['playerId']}
|
|
pd = self.fetch(f"{self.host}/api/v1/app/play/analysisMovieUrl", headers=self.headers, params=params).json()
|
|
url, p = pd.get('data', ''), 0
|
|
except Exception as e:
|
|
print(f"解析流媒体直链失败: {e}")
|
|
url, p = "", 0
|
|
return {'parse': p, 'url': url, 'header': {'User-Agent': 'okhttp/4.12.0'}, 'danmaku': 'http://127.0.0.1:9978/proxy?do=diydanmu'}
|
|
|
|
def localProxy(self, param):
|
|
pass
|
|
|
|
def liveContent(self, url):
|
|
pass
|
|
|
|
def gettk(self):
|
|
self.headers.update({'deviceId': self.getdid()})
|
|
try:
|
|
data = self.fetch(f"{self.host}/api/v1/app/user/visitorInfo", headers=self.headers).json()
|
|
return data.get('data', {}).get('token', '')
|
|
except:
|
|
return ""
|
|
|
|
def getdid(self):
|
|
did = self.getCache('ldid')
|
|
if not did:
|
|
hex_chars = '0123456789abcdef'
|
|
did = ''.join(random.choice(hex_chars) for _ in range(16))
|
|
self.setCache('ldid', did)
|
|
return did
|
|
|
|
def getd(self, jdata, player):
|
|
x = jdata.copy()
|
|
x.update({'playerId': player['id']})
|
|
encrypt_payload = {"key": self.rsa_encrypt(json.dumps(x))}
|
|
response = self.post(f"{self.host}/api/v1/app/play/movieDetails", headers=self.headers, json=encrypt_payload).json()
|
|
decrypted_str = self.rsa_decrypt(response.get('data', ''))
|
|
if decrypted_str:
|
|
decrypted_episode = json.loads(decrypted_str)
|
|
return x, decrypted_episode.get('episodeList', [])
|
|
return x, []
|
|
|
|
def getv(self, d, c):
|
|
f = {str(d['playerId']): ''}
|
|
g = []
|
|
for i in c:
|
|
j = d.copy()
|
|
j.update({'episodeId': i['id']})
|
|
g.append(f"{i['episode']}${self.e64(json.dumps(j))}")
|
|
f[str(d['playerId'])] = '#'.join(g)
|
|
return f
|
|
|
|
def getlist(self, data):
|
|
videos = []
|
|
for i in data:
|
|
if not i.get('id'):
|
|
continue
|
|
videos.append({
|
|
'vod_id': f"{i['id']}@@{i.get('typeId', '')}",
|
|
'vod_name': i.get('name', ''),
|
|
'vod_pic': i.get('cover', ''),
|
|
'vod_year': i.get('year', ''),
|
|
'vod_remarks': i.get('totalEpisode', '')
|
|
})
|
|
return videos
|
|
|
|
def e64(self, text):
|
|
try:
|
|
return b64encode(text.encode('utf-8')).decode('utf-8')
|
|
except:
|
|
return ""
|
|
|
|
def d64(self, encoded_text):
|
|
try:
|
|
return b64decode(encoded_text.encode('utf-8')).decode('utf-8')
|
|
except:
|
|
return ""
|