Sync all projects

This commit is contained in:
github-actions[bot]
2026-07-16 02:35:49 +00:00
parent 4a1ea07f2e
commit be14e359f7
25 changed files with 10344 additions and 5711 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
+314
View File
@@ -0,0 +1,314 @@
# coding=utf-8
# !/usr/bin/python
"""
作者 丢丢喵 🚓 内容均从互联网收集而来 仅供交流学习使用 版权归原创者所有 如侵犯了您的权益 请通知作者 将及时删除侵权内容
====================Diudiumiao====================
"""
from Crypto.Util.Padding import unpad
from Crypto.Util.Padding import pad
from urllib.parse import unquote
from Crypto.Cipher import ARC4
from urllib.parse import quote
from base.spider import Spider
from Crypto.Cipher import AES
from datetime import datetime
from bs4 import BeautifulSoup
from base64 import b64decode
import urllib.request
import urllib.parse
import datetime
import binascii
import requests
import base64
import json
import time
import sys
import re
import os
sys.path.append('..')
xurl = "https://search.bilibili.com"
xurl1 = "https://api.live.bilibili.com"
headerx = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0'
}
class Spider(Spider):
global xurl
global xurl1
global headerx
def getName(self):
return "首页"
def init(self, extend):
pass
def isVideoFormat(self, url):
pass
def manualVideoCheck(self):
pass
def extract_middle_text(self, text, start_str, end_str, pl, start_index1: str = '', end_index2: str = ''):
if pl == 3:
plx = []
while True:
start_index = text.find(start_str)
if start_index == -1:
break
end_index = text.find(end_str, start_index + len(start_str))
if end_index == -1:
break
middle_text = text[start_index + len(start_str):end_index]
plx.append(middle_text)
text = text.replace(start_str + middle_text + end_str, '')
if len(plx) > 0:
purl = ''
for i in range(len(plx)):
matches = re.findall(start_index1, plx[i])
output = ""
for match in matches:
match3 = re.search(r'(?:^|[^0-9])(\d+)(?:[^0-9]|$)', match[1])
if match3:
number = match3.group(1)
else:
number = 0
if 'http' not in match[0]:
output += f"#{match[1]}${number}{xurl}{match[0]}"
else:
output += f"#{match[1]}${number}{match[0]}"
output = output[1:]
purl = purl + output + "$$$"
purl = purl[:-3]
return purl
else:
return ""
else:
start_index = text.find(start_str)
if start_index == -1:
return ""
end_index = text.find(end_str, start_index + len(start_str))
if end_index == -1:
return ""
if pl == 0:
middle_text = text[start_index + len(start_str):end_index]
return middle_text.replace("\\", "")
if pl == 1:
middle_text = text[start_index + len(start_str):end_index]
matches = re.findall(start_index1, middle_text)
if matches:
jg = ' '.join(matches)
return jg
if pl == 2:
middle_text = text[start_index + len(start_str):end_index]
matches = re.findall(start_index1, middle_text)
if matches:
new_list = [f'{item}' for item in matches]
jg = '$$$'.join(new_list)
return jg
def homeContent(self, filter):
result = {}
result = {"class": [{"type_id": "", "type_name": "舞蹈"},
{"type_id": "音乐", "type_name": "音乐"},
{"type_id": "手游", "type_name": "手游"},
{"type_id": "网游", "type_name": "网游"},
{"type_id": "单机游戏", "type_name": "单机游戏"},
{"type_id": "虚拟主播", "type_name": "虚拟主播"},
{"type_id": "电台", "type_name": "电台"},
{"type_id": "体育", "type_name": "体育"},
{"type_id": "聊天", "type_name": "聊天"},
{"type_id": "娱乐", "type_name": "娱乐"},
{"type_id": "电影", "type_name": "影视"},
{"type_id": "新闻", "type_name": "新闻"}]
}
return result
def homeVideoContent(self):
pass
def categoryContent(self, cid, pg, filter, ext):
result = {}
videos = []
if pg:
page = int(pg)
else:
page = 1
url = f'{xurl}/live?keyword={cid}&page={str(page)}'
detail = requests.get(url=url, headers=headerx)
detail.encoding = "utf-8"
res = detail.text
doc = BeautifulSoup(res, "lxml")
soups = doc.find_all('div', class_="video-list-item")
for vod in soups:
names = vod.find('h3', class_="bili-live-card__info--tit")
name = names.text.strip().replace('直播中', '')
id = names.find('a')['href']
id = self.extract_middle_text(id, 'bilibili.com/', '?', 0)
pic = vod.find('img')['src']
if 'http' not in pic:
pic = "https:" + pic
remarks = vod.find('a', class_="bili-live-card__info--uname")
remark = remarks.text.strip()
video = {
"vod_id": id,
"vod_name": name,
"vod_pic": pic,
"vod_remarks": remark
}
videos.append(video)
result = {'list': videos}
result['page'] = pg
result['pagecount'] = 9999
result['limit'] = 90
result['total'] = 999999
return result
def detailContent(self, ids):
did = ids[0]
result = {}
videos = []
xianlu = ''
bofang = ''
url = f'{xurl1}/xlive/web-room/v2/index/getRoomPlayInfo?room_id={did}&platform=web&protocol=0,1&format=0,1,2&codec=0,1'
detail = requests.get(url=url, headers=headerx)
detail.encoding = "utf-8"
data = detail.json()
content = '欢迎观看哔哩直播'
setup = data['data']['playurl_info']['playurl']['stream']
nam = 0
for vod in setup:
try:
host = vod['format'][nam]['codec'][0]['url_info'][1]['host']
except (KeyError, IndexError):
continue
base = vod['format'][nam]['codec'][0]['base_url']
extra = vod['format'][nam]['codec'][0]['url_info'][1]['extra']
id = host + base + extra
nam = nam + 1
namc = f"{nam}号线路"
bofang = bofang + namc + '$' + id + '#'
bofang = bofang[:-1]
xianlu = '哔哩专线'
videos.append({
"vod_id": did,
"vod_content": content,
"vod_play_from": xianlu,
"vod_play_url": bofang
})
result['list'] = videos
return result
def playerContent(self, flag, id, vipFlags):
result = {}
result["parse"] = 0
result["playUrl"] = ''
result["url"] = id
result["header"] = headerx
return result
def searchContentPage(self, key, quick, pg):
result = {}
videos = []
if pg:
page = int(pg)
else:
page = 1
url = f'{xurl}/live?keyword={key}&page={str(page)}'
detail = requests.get(url=url, headers=headerx)
detail.encoding = "utf-8"
res = detail.text
doc = BeautifulSoup(res, "lxml")
soups = doc.find_all('div', class_="video-list-item")
for vod in soups:
names = vod.find('h3', class_="bili-live-card__info--tit")
name = names.text.strip().replace('直播中', '')
id = names.find('a')['href']
id = self.extract_middle_text(id, 'bilibili.com/', '?', 0)
pic = vod.find('img')['src']
if 'http' not in pic:
pic = "https:" + pic
remarks = vod.find('a', class_="bili-live-card__info--uname")
remark = remarks.text.strip()
video = {
"vod_id": id,
"vod_name": name,
"vod_pic": pic,
"vod_remarks": remark
}
videos.append(video)
result['list'] = videos
result['page'] = pg
result['pagecount'] = 9999
result['limit'] = 90
result['total'] = 999999
return result
def searchContent(self, key, quick, pg="1"):
return self.searchContentPage(key, quick, '1')
def localProxy(self, params):
if params['type'] == "m3u8":
return self.proxyM3u8(params)
elif params['type'] == "media":
return self.proxyMedia(params)
elif params['type'] == "ts":
return self.proxyTs(params)
return None
+496
View File
@@ -0,0 +1,496 @@
# coding = utf-8
#!/usr/bin/python
import re
import sys
import json
import time
import base64
import hashlib
import random
import string
import urllib.parse
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5
from base.spider import Spider
sys.path.append('..')
class Spider(Spider):
def __init__(self):
self.name = "瓜子"
self.hosts = [
'https://apinew.uozvr.com',
'https://api.w32z7vtd.com',
'https://api.6a7nnf7.com',
'https://api.umygrx3.com',
'https://api.rmedphk.com'
]
self.host_index = 0
self.host = self.hosts[self.host_index]
# AES 固定密钥(与Java版一致)
self.AES_KEY = 'OITxa5OqAYjhswxx'
self.AES_IV = 'rCMNwZASNBKZ8mXV'
# RSA 公钥/私钥
self.RSA_PUBLIC_KEY = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDUM5+/y8sPsWkd1/RQS64X259EUwxFXFE5HlA65MqrxnPs0JqoSRojSDy5QhwvROlaD6TwRQHKMY2OAZ6SnQeUJsChTEFIR9qUkwrs3/MVUMxjsv6JS6Oe/juclyJGTgVmDhB55EafXsD0SQYVj/QXXsxR6ewR5E2kL52yAAD4yQIDAQAB"
self.RSA_PRIVATE_KEY = """-----BEGIN RSA PRIVATE KEY-----
MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGAe6hKrWLi1zQmjTT1
ozbE4QdFeJGNxubxld6GrFGximxfMsMB6BpJhpcTouAqywAFppiKetUBBbXwYsYU
1wNr648XVmPmCMCy4rY8vdliFnbMUj086DU6Z+/oXBdWU3/b1G0DN3E9wULRSwcK
ZT3wj/cCI1vsCm3gj2R5SqkA9Y0CAwEAAQKBgAJH+4CxV0/zBVcLiBCHvSANm0l7
HetybTh/j2p0Y1sTXro4ALwAaCTUeqdBjWiLSo9lNwDHFyq8zX90+gNxa7c5EqcW
V9FmlVXr8VhfBzcZo1nXeNdXFT7tQ2yah/odtdcx+vRMSGJd1t/5k5bDd9wAvYdI
DblMAg+wiKKZ5KcdAkEA1cCakEN4NexkF5tHPRrR6XOY/XHfkqXxEhMqmNbB9U34
saTJnLWIHC8IXys6Qmzz30TtzCjuOqKRRy+FMM4TdwJBAJQZFPjsGC+RqcG5UvVM
iMPhnwe/bXEehShK86yJK/g/UiKrO87h3aEu5gcJqBygTq3BBBoH2md3pr/W+hUM
WBsCQQChfhTIrdDinKi6lRxrdBnn0Ohjg2cwuqK5zzU9p/N+S9x7Ck8wUI53DKm8
jUJE8WAG7WLj/oCOWEh+ic6NIwTdAkEAj0X8nhx6AXsgCYRql1klbqtVmL8+95KZ
K7PnLWG/IfjQUy3pPGoSaZ7fdquG8bq8oyf5+dzjE/oTXcByS+6XRQJAP/5ciy1b
L3NhUhsaOVy55MHXnPjdcTX0FaLi+ybXZIfIQ2P4rb19mVq1feMbCXhz+L1rG8oa
t5lYKfpe8k83ZA==
-----END RSA PRIVATE KEY-----"""
self.DEVICE_OLD_KEY = "aLFBMWpxBrIDAD1Si/KVvm41"
# 设备信息(随机生成)
self.deviceId = str(864150060000000 + random.randint(0, 9999))
self.deviceKey = ''.join(random.choices('0123456789ABCDEF', k=40)) # 20字节hex大写
self.token = ""
self.token_id = ""
self.registered = False
self.header = {
'User-Agent': 'Lavf/57.83.100',
'code': 'GZ0369',
'deviceId': self.deviceId,
'lang': 'zh_cn',
'Cache-Control': 'no-cache',
'Content-Type': 'application/x-www-form-urlencoded',
'Version': '2604028',
'PackageName': 'com.ae06aebdbb.y286327f5a.ofe849883320260517',
'Ver': '3.0.3.2',
'api-ver': '3.0.3.2',
'Referer': self.host
}
self.cache = {}
self.cache_timeout = 300
# 初始化token
self.init_token()
def getName(self):
return self.name
def init(self, extend=''):
pass
# ---------- 设备注册与认证 ----------
def init_token(self):
"""初始化token:注册设备 -> 刷新"""
print("===== 初始化设备认证 =====")
try:
if not self.registered:
self.sign_up()
# 刷新获取最终token
self.refresh_token()
except Exception as e:
print(f"初始化token失败: {e}")
# 兜底使用原有硬编码(几乎没用)
self.token = '024212ef0975c5306a1434e113a46463.bc77313e11a248558a6ca244ca980944ec3421fa480c50e0229ad91f1cb15aea582603202cd71796885c9e5163e500f1b72f737059aff1ddb8beea47c5a331d6760540345b7f88b2302a0e6e09589f9dcf3ff9175d8c905f990203f5fc04748008ea7a366571cbf5b09509a873dcfba3cf1d5590385f5f7ef6e01d1850974aa220eb5178c89e61c24411af9b9a19435e.06fde789ece48d9b33c5dc857e04e9b5838f08264d928b87237d3476c4484b46'
def sign_up(self):
"""注册设备"""
print("注册新设备...")
params = {
"new_key": self.deviceKey,
"old_key": self.DEVICE_OLD_KEY,
"phone_type": 1,
"code": ""
}
result = self._auth_request('/App/Authentication/Device/signUp', params)
self._apply_auth(result)
self.registered = True
def sign_in(self):
"""登录设备"""
print("设备登录...")
params = {
"new_key": self.deviceKey,
"old_key": self.DEVICE_OLD_KEY
}
result = self._auth_request('/App/Authentication/Device/signIn', params)
self._apply_auth(result)
def _apply_auth(self, result):
"""从认证响应中提取token"""
new_token = result.get('token', '')
if not new_token:
raise Exception("认证失败,无token返回: {}".format(result))
self.token = new_token
new_token_id = result.get('app_user_id', '')
if new_token_id:
self.token_id = new_token_id
print(f"获取token成功, token前缀: {self.token[:30]}...")
def refresh_token(self):
"""刷新token"""
print("刷新token...")
result = self._auth_request('/App/Authentication/Authenticator/refresh', {})
self._apply_auth(result)
def _auth_request(self, path, params):
"""认证类请求(不需要ensure_token"""
return self._send_encrypted_request(params, path, is_auth=True)
# ---------- 业务请求核心(修复加密与签名) ----------
def ensure_token(self):
"""确保token有效,如未就绪则重新获取"""
if not self.token or not self.token_id:
if self.registered:
self.sign_in()
else:
self.sign_up()
self.refresh_token()
def _send_encrypted_request(self, data, path, is_auth=False):
"""
发送加密请求,返回解密后的字典
:param data: 业务参数字典
:param path: 请求路径
:param is_auth: 是否为认证类请求(signUp/signIn/refresh),此时不使用ensure_token
"""
try:
if not is_auth:
self.ensure_token()
# 1. 将参数转为JSON并AES加密
json_params = json.dumps(data)
encrypted = self.aes_encrypt(json_params, self.AES_KEY, self.AES_IV)
request_key = encrypted.upper() # Java中是bytesToHex(encrypted).toUpperCase()
# 2. 生成keys (RSA加密 iv/key JSON)
key_json = json.dumps({"iv": self.AES_IV, "key": self.AES_KEY})
keys = self.rsa_encrypt(key_json, self.RSA_PUBLIC_KEY)
# 3. 生成签名
t = str(int(time.time()))
sign_str = f"token_id=,token={self.token},phone_type=1,request_key={request_key},app_id=1,time={t},keys={keys}*&zvdvdvddbfikkkumtmdwqppp?|4Y!s!2br"
signature = self.get_md5(sign_str) # 已改为大写
# 4. 构建请求体
body = {
'token': self.token,
'token_id': '',
'phone_type': '1',
'time': t,
'phone_model': 'xiaomi-25031', # 与Java版保持一致
'keys': keys,
'request_key': request_key,
'signature': signature,
'app_id': '1',
'ad_version': '1'
}
# 5. 发送请求
url = f"{self.host}{path}"
response = self.post(url, headers=self.header, data=body, timeout=10)
if response.status_code != 200:
raise Exception(f"HTTP {response.status_code}")
resp_json = response.json()
# 检查业务code(若不为200可能token过期)
if 'code' in resp_json and resp_json['code'] != 200:
print(f"业务错误码: {resp_json['code']}, 信息: {resp_json}")
# 如果不是认证请求,尝试重新获取token后重试一次(这里简单处理,外层get_data已有重试)
raise Exception("业务错误")
data_section = resp_json.get('data')
if not data_section:
raise Exception("响应缺少data字段")
encrypted_response = data_section.get('response_key', '')
encrypted_keys = data_section.get('keys', '')
# 6. 解密响应
decrypted_keys_json = self.rsa_decrypt(encrypted_keys, self.RSA_PRIVATE_KEY)
key_info = json.loads(decrypted_keys_json)
resp_key = key_info['key']
resp_iv = key_info['iv']
decrypted_data = self.aes_decrypt(encrypted_response, resp_key, resp_iv)
return json.loads(decrypted_data)
except Exception as e:
print(f"请求失败 [{path}]: {e}")
return None
def get_data(self, data, path, use_cache=True):
"""带重试和域名轮询的数据获取(保持原框架)"""
try:
cache_key = f"{path}_{hash(str(data))}" if use_cache else None
if use_cache and cache_key in self.cache:
cached_data, timestamp = self.cache[cache_key]
if time.time() - timestamp < self.cache_timeout:
return cached_data
for attempt in range(3):
tried = 0
while tried < len(self.hosts):
self.host = self.hosts[self.host_index]
self.header['Referer'] = self.host
result = self._send_encrypted_request(data, path)
if result is not None:
print(f"请求成功: {path}, 域名: {self.host}")
if use_cache and cache_key:
self.cache[cache_key] = (result, time.time())
return result
# 切换到下一个域名
self.host_index = (self.host_index + 1) % len(self.hosts)
tried += 1
# 所有域名失败,尝试重新认证并重试
if attempt < 2:
print("所有域名失败,尝试重新认证...")
try:
self.ensure_token()
except:
pass
self.host_index = 0
else:
break
return None
except Exception as e:
print(f"get_data异常: {e}")
return None
# ---------- 加解密工具 ----------
def aes_encrypt(self, text, key, iv):
try:
key_bytes = key.encode('utf-8')
iv_bytes = iv.encode('utf-8')
cipher = AES.new(key_bytes, AES.MODE_CBC, iv_bytes)
encrypted = cipher.encrypt(pad(text.encode('utf-8'), AES.block_size))
return encrypted.hex().upper()
except Exception as e:
print(f"AES加密失败: {e}")
return ""
def aes_decrypt(self, text, key, iv):
try:
key_bytes = key.encode('utf-8')
iv_bytes = iv.encode('utf-8')
cipher = AES.new(key_bytes, AES.MODE_CBC, iv_bytes)
encrypted_bytes = bytes.fromhex(text)
decrypted = unpad(cipher.decrypt(encrypted_bytes), AES.block_size)
return decrypted.decode('utf-8')
except Exception as e:
print(f"AES解密失败: {e}")
return ""
def rsa_encrypt(self, text, public_key_str):
"""RSA公钥加密(PKCS1v1.5"""
try:
key = RSA.import_key("-----BEGIN PUBLIC KEY-----\n" + public_key_str + "\n-----END PUBLIC KEY-----")
cipher = PKCS1_v1_5.new(key)
encrypted = cipher.encrypt(text.encode('utf-8'))
return base64.b64encode(encrypted).decode('utf-8')
except Exception as e:
print(f"RSA加密失败: {e}")
return ""
def rsa_decrypt(self, encrypted_data, private_key_str):
"""RSA私钥解密"""
try:
encrypted_bytes = base64.b64decode(encrypted_data)
rsa_key = RSA.import_key(private_key_str)
cipher = PKCS1_v1_5.new(rsa_key)
decrypted = cipher.decrypt(encrypted_bytes, None)
return decrypted.decode('utf-8') if decrypted else ""
except Exception as e:
print(f"RSA解密失败: {e}")
return ""
def get_md5(self, text):
return hashlib.md5(text.encode()).hexdigest().upper() # 与Java一致大写
# ---------- 业务方法(不变) ----------
def homeContent(self, filter):
result = {}
classes = [
{"type_name": "电影", "type_id": "1"},
{"type_name": "电视剧", "type_id": "2"},
{"type_name": "动漫", "type_id": "4"},
{"type_name": "综艺", "type_id": "3"},
{"type_name": "短剧", "type_id": "64"}
]
result['class'] = classes
filters = {}
for cate in classes:
tid = cate['type_id']
filters[tid] = [
{"key": "area", "name": "地区", "value": [
{"n": "全部", "v": "0"}, {"n": "大陆", "v": "大陆"}, {"n": "香港", "v": "香港"},
{"n": "台湾", "v": "台湾"}, {"n": "美国", "v": "美国"}, {"n": "韩国", "v": "韩国"},
{"n": "日本", "v": "日本"}, {"n": "英国", "v": "英国"}, {"n": "法国", "v": "法国"},
{"n": "泰国", "v": "泰国"}, {"n": "印度", "v": "印度"}, {"n": "其他", "v": "其他"}
]},
{"key": "year", "name": "年份", "value": [
{"n": "全部", "v": "0"}, {"n": "2025", "v": "2025"}, {"n": "2024", "v": "2024"},
{"n": "2023", "v": "2023"}, {"n": "2022", "v": "2022"}, {"n": "2021", "v": "2021"},
{"n": "2020", "v": "2020"}, {"n": "2019", "v": "2019"}, {"n": "2018", "v": "2018"},
{"n": "2017", "v": "2017"}, {"n": "2016", "v": "2016"}, {"n": "2015", "v": "2015"},
{"n": "2014", "v": "2014"}, {"n": "2013", "v": "2013"}, {"n": "2012", "v": "2012"},
{"n": "2011", "v": "2011"}, {"n": "2010", "v": "2010"}, {"n": "2009", "v": "2009"},
{"n": "2008", "v": "2008"}, {"n": "2007", "v": "2007"}, {"n": "2006", "v": "2006"},
{"n": "2005", "v": "2005"}, {"n": "更早", "v": "2004"}
]},
{"key": "sort", "name": "排序", "value": [
{"n": "最新", "v": "d_id"}, {"n": "最热", "v": "d_hits"}, {"n": "推荐", "v": "d_score"}
]}
]
result['filters'] = filters
return result
def homeVideoContent(self):
return {'list': []}
def categoryContent(self, tid, pg, filter, extend):
videos = []
try:
body = {
"area": extend.get('area', '0'),
"year": extend.get('year', '0'),
"pageSize": "30",
"sort": extend.get('sort', 'd_id'),
"page": str(pg),
"tid": tid
}
cache_key = f"category_{tid}_{pg}_{hash(str(body))}"
data = self.get_cached_data(cache_key, body, '/App/IndexList/indexList')
if data and 'list' in data:
for item in data['list']:
vod_continu = item.get('vod_continu', 0)
remarks = '电影' if vod_continu == 0 else f'更新至{vod_continu}'
video = {
"vod_id": f"{item.get('vod_id', '')}/{vod_continu}",
"vod_name": item.get('vod_name', ''),
"vod_pic": item.get('vod_pic', ''),
"vod_remarks": remarks
}
videos.append(video)
except Exception as e:
print(f"获取分类内容失败: {e}")
return {'list': videos, 'page': int(pg), 'pagecount': 9999, 'limit': 30, 'total': 999999}
def detailContent(self, ids):
try:
vod_id = ids[0].split('/')[0]
t = str(int(time.time()))
body1 = {"token_id": self.token_id, "vod_id": vod_id, "mobile_time": t, "token": self.token}
qdata = self.get_data(body1, '/App/IndexPlay/playInfo')
body2 = {"vurl_cloud_id": "2", "vod_d_id": vod_id}
jdata = self.get_data(body2, '/App/Resource/Vurl/show')
if not qdata or 'vodInfo' not in qdata:
return {'list': []}
vod = qdata['vodInfo']
video_detail = {
"vod_id": vod_id,
"vod_name": vod.get('vod_name', ''),
"vod_pic": vod.get('vod_pic', ''),
"vod_year": vod.get('vod_year', ''),
"vod_area": vod.get('vod_area', ''),
"vod_actor": vod.get('vod_actor', ''),
"vod_director": vod.get('vod_director', ''),
"vod_content": vod.get('vod_use_content', '').strip(),
"vod_play_from": "瓜子影视"
}
play_list = []
if jdata and 'list' in jdata:
for index, item in enumerate(jdata['list']):
if 'play' in item:
n, p = [], []
for key, value in item['play'].items():
if 'param' in value and value['param']:
n.append(key)
p.append(value['param'])
if p:
play_name = str(index + 1) if len(jdata['list']) != 1 else vod.get('vod_name', '')
play_url = f"{p[-1]}||{'@'.join(n)}"
play_list.append(f"{play_name}${play_url}")
video_detail["vod_play_url"] = "#".join(play_list)
return {'list': [video_detail]}
except Exception as e:
print(f"获取详情失败: {e}")
return {'list': []}
def searchContent(self, key, quick, pg=1):
videos = []
try:
body = {"keywords": key, "order_val": "1", "page": str(pg)}
data = self.get_data(body, '/App/Index/findMoreVod', use_cache=False)
if data and 'list' in data:
for item in data['list']:
vod_continu = item.get('vod_continu', 0)
remarks = '电影' if vod_continu == 0 else f'更新至{vod_continu}'
videos.append({
"vod_id": f"{item.get('vod_id', '')}/{vod_continu}",
"vod_name": item.get('vod_name', ''),
"vod_pic": item.get('vod_pic', ''),
"vod_remarks": remarks
})
except Exception as e:
print(f"搜索失败: {e}")
return {'list': videos, 'page': int(pg), 'pagecount': 9999, 'limit': 30, 'total': 999999}
def playerContent(self, flag, id, vipFlags):
try:
parts = id.split('||')
if len(parts) < 2:
return {"parse": 0, "playUrl": "", "url": ""}
param_str = parts[0]
resolutions = parts[1].split('@') if len(parts) > 1 else []
params = {}
for pair in param_str.split('&'):
if '=' in pair:
key, value = pair.split('=', 1)
params[key] = value
if resolutions:
resolutions.sort(key=lambda x: int(x) if x.isdigit() else 0, reverse=True)
params['resolution'] = resolutions[0]
data = self.get_data(params, '/App/Resource/VurlDetail/showOne', use_cache=False)
if data and 'url' in data:
return {"parse": 0, "playUrl": "", "url": data['url'],
"header": json.dumps({"User-Agent": "Lavf/57.83.100", "Referer": "http://WJiZxLXA2.com/"}), 'danmaku': 'http://127.0.0.1:9978/proxy?do=diydanmu'}
return {"parse": 0, "playUrl": "", "url": ""}
except Exception as e:
print(f"播放解析失败: {e}")
return {"parse": 0, "playUrl": "", "url": ""}
def isVideoFormat(self, url):
video_formats = ['.m3u8', '.mp4', '.avi', '.mkv', '.flv', '.ts']
return any(url.lower().endswith(fmt) for fmt in video_formats)
def manualVideoCheck(self):
pass
def localProxy(self, params):
return None
def get_cached_data(self, cache_key, data, path):
current_time = time.time()
if cache_key in self.cache:
cached_data, timestamp = self.cache[cache_key]
if current_time - timestamp < self.cache_timeout:
return cached_data
result = self.get_data(data, path)
if result:
self.cache[cache_key] = (result, current_time)
return result
if __name__ == '__main__':
pass
+330
View File
@@ -0,0 +1,330 @@
# -*- coding: utf-8 -*-
# by @嗷呜
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 ""
+1260
View File
@@ -0,0 +1,1260 @@
# -*- coding: utf-8 -*-
# by @星河
# 修复版本 - 参考最新三合一.js重构虎牙、斗鱼、B站直播逻辑
# 修复:虎牙清晰度选择,确保ratio参数正确传递码率值
# 修复:斗鱼切换分辨率只能播放1秒的问题(每次重新获取安全密钥和签名)
# 修复:B站使用特殊UA和WBI签名绕过-352风控 [^90^][^30^]
import json
import re
import sys
import time
import hashlib
import random
import urllib.parse
from base64 import b64decode, b64encode
from urllib.parse import parse_qs
import requests
from pyquery import PyQuery as pq
sys.path.append('..')
from base.spider import Spider
from concurrent.futures import ThreadPoolExecutor
class Spider(Spider):
def init(self, extend=""):
# 初始化B站WBI密钥
self.bili_wbi_keys = None
self.bili_wbi_expire = 0
pass
def getName(self):
return "直播"
def isVideoFormat(self, url):
pass
def manualVideoCheck(self):
pass
def destroy(self):
pass
headers = [
{
# 特殊UA绕过B站风控 [^90^]
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0"
},
{
"User-Agent": "Dart/3.4 (dart:io)"
}
]
excepturl = 'https://www.baidu.com'
hosts = {
"huya": ["https://www.huya.com", "https://mp.huya.com"],
"douyu": "https://www.douyu.com",
"wangyi": "https://cc.163.com",
"bili": ["https://api.live.bilibili.com", "https://api.bilibili.com"]
}
referers = {
"huya": "https://live.cdn.huya.com",
"douyu": "https://m.douyu.com",
"bili": "https://live.bilibili.com"
}
playheaders = {
"wangyi": {
"User-Agent": "ExoPlayer",
"Connection": "Keep-Alive",
"Icy-MetaData": "1"
},
"bili": {
'Accept': '*/*',
'Icy-MetaData': '1',
'referer': 'https://live.bilibili.com',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36'
},
'huya': {
'User-Agent': 'ExoPlayer',
'Connection': 'Keep-Alive',
'Icy-MetaData': '1'
},
'douyu': {
'User-Agent': 'libmpv',
'Icy-MetaData': '1'
}
}
# WBI签名相关常量 [^30^]
MIXIN_KEY_ENC_TAB = [
46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49,
33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40,
61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11,
36, 20, 34, 44, 52
]
def _get_bili_wbi_keys(self):
"""获取B站WBI密钥 [^30^]"""
try:
# 检查缓存
if self.bili_wbi_keys and time.time() < self.bili_wbi_expire:
return self.bili_wbi_keys
# 从导航接口获取 - 使用特殊UA [^90^]
resp = self.fetch(
'https://api.bilibili.com/x/web-interface/nav',
headers={
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36',
'Referer': 'https://www.bilibili.com/'
}
).json()
if resp.get('code') != 0:
return None
img_url = resp['data']['wbi_img']['img_url']
sub_url = resp['data']['wbi_img']['sub_url']
# 提取文件名作为key
img_key = img_url.rsplit('/', 1)[1].split('.')[0]
sub_key = sub_url.rsplit('/', 1)[1].split('.')[0]
self.bili_wbi_keys = (img_key, sub_key)
self.bili_wbi_expire = time.time() + 86400 # 24小时过期
return self.bili_wbi_keys
except Exception as e:
print(f"获取B站WBI密钥失败: {e}")
return None
def _get_mixin_key(self, orig: str):
"""生成mixin_key [^30^]"""
return ''.join([orig[i] for i in self.MIXIN_KEY_ENC_TAB])[:32]
def _enc_wbi(self, params: dict):
"""WBI签名 [^30^]"""
keys = self._get_bili_wbi_keys()
if not keys:
return params
img_key, sub_key = keys
mixin_key = self._get_mixin_key(img_key + sub_key)
# 添加时间戳
params['wts'] = round(time.time())
# 排序参数
params = dict(sorted(params.items()))
# 过滤特殊字符
params = {
k: ''.join(filter(lambda c: c not in "!'()*", str(v)))
for k, v in params.items()
}
# 计算签名
query = urllib.parse.urlencode(params)
w_rid = hashlib.md5((query + mixin_key).encode()).hexdigest()
params['w_rid'] = w_rid
return params
def process_bili(self):
"""获取B站分类列表 - 使用WBI签名 [^30^]"""
try:
# 尝试获取分类列表 - 使用特殊UA和WBI签名
params = {'need_entrance': 1, 'parent_id': 0}
signed_params = self._enc_wbi(params)
data = self.fetch(
f'{self.hosts["bili"][0]}/room/v1/Area/getList',
params=signed_params,
headers=self.headers[0]
).json()
if data.get('code') == 0 and data.get('data'):
# 保存分类数据供后续使用
self.bili_areas = data['data']
return ('bili', [{'key': 'cate', 'name': '分类',
'value': [{'n': i['name'], 'v': str(i['id'])}
for i in data['data']]}])
return 'bili', None
except Exception as e:
print(f"bili处理错误: {e}")
# 使用默认分类
return 'bili', [{'key': 'cate', 'name': '分类',
'value': [{'n': '网游', 'v': '2'}, {'n': '手游', 'v': '3'},
{'n': '单机', 'v': '6'}, {'n': '娱乐', 'v': '1'},
{'n': '电台', 'v': '5'}, {'n': '虚拟主播', 'v': '9'},
{'n': '生活', 'v': '10'}, {'n': '知识', 'v': '11'},
{'n': '赛事', 'v': '13'}]}]
def process_douyu(self):
try:
self.dyufdata = self.fetch(
f'{self.referers["douyu"]}/api/cate/list',
headers=self.headers[1]
).json()
return ('douyu', [{'key': 'cate', 'name': '分类',
'value': [{'n': i['cate1Name'], 'v': str(i['cate1Id'])}
for i in self.dyufdata['data']['cate1Info']]}])
except Exception as e:
print(f"douyu错误: {e}")
return 'douyu', None
def homeContent(self, filter):
result = {}
cateManual = {
"虎牙": "huya",
"斗鱼": "douyu",
"网易": "wangyi",
"B站": "bili"
}
classes = []
filters = {
'huya': [{'key': 'cate', 'name': '分类',
'value': [{'n': '网游', 'v': '1'}, {'n': '单机', 'v': '2'},
{'n': '娱乐', 'v': '8'}, {'n': '手游', 'v': '3'}]}]
}
with ThreadPoolExecutor(max_workers=2) as executor:
futures = {
executor.submit(self.process_bili): 'bili',
executor.submit(self.process_douyu): 'douyu'
}
for future in futures:
platform, filter_data = future.result()
if filter_data:
filters[platform] = filter_data
for k in cateManual:
classes.append({
'type_name': k,
'type_id': cateManual[k]
})
result['class'] = classes
result['filters'] = filters
return result
def homeVideoContent(self):
pass
def categoryContent(self, tid, pg, filter, extend):
vdata = []
result = {}
pagecount = 9999
result['page'] = pg
result['limit'] = 90
result['total'] = 999999
if tid == 'wangyi':
vdata, pagecount = self.wyccContent(tid, pg, filter, extend, vdata)
elif 'bili' in tid:
vdata, pagecount = self.biliContent(tid, pg, filter, extend, vdata)
elif 'huya' in tid:
vdata, pagecount = self.huyaContent(tid, pg, filter, extend, vdata)
elif 'douyu' in tid:
vdata, pagecount = self.douyuContent(tid, pg, filter, extend, vdata)
result['list'] = vdata
result['pagecount'] = pagecount
return result
def wyccContent(self, tid, pg, filter, extend, vdata):
params = {
'format': 'json',
'start': (int(pg) - 1) * 20,
'size': '20',
}
response = self.fetch(f'{self.hosts[tid]}/api/category/live/', params=params, headers=self.headers[0]).json()
for i in response['lives']:
if i.get('cuteid'):
bvdata = self.buildvod(
vod_id=f"{tid}@@{i['cuteid']}",
vod_name=i.get('title'),
vod_pic=i.get('cover'),
vod_remarks=i.get('nickname'),
style={"type": "rect", "ratio": 1.33}
)
vdata.append(bvdata)
return vdata, 9999
def biliContent(self, tid, pg, filter, extend, vdata):
"""B站分类内容 - 使用WBI签名绕过风控 [^30^][^90^]"""
try:
# 分类列表 - 显示子分类
if extend.get('cate') and pg == '1' and 'click' not in tid:
# 从已保存的分类数据中找到对应分类的子分类
if hasattr(self, 'bili_areas'):
for area in self.bili_areas:
if str(area['id']) == extend['cate']:
for sub_area in area.get('list', []):
v = self.buildvod(
vod_id=f"click_{tid}@@{extend['cate']}@@{sub_area['id']}",
vod_name=sub_area.get('name'),
vod_pic=sub_area.get('pic'),
vod_tag=1,
style={"type": "oval", "ratio": 1}
)
vdata.append(v)
return vdata, 1
# 如果没有找到子分类,直接返回空,让用户进入房间列表
return vdata, 1
# 房间列表 - 使用getList接口并添加WBI签名 [^30^]
if 'click' in tid:
# 子分类房间
ids = tid.split('_')[1].split('@@')
tid = ids[0]
parent_area_id = ids[1]
area_id = ids[2]
else:
# 默认使用分类ID作为parent_area_idarea_id为0表示该分类下所有
parent_area_id = extend.get('cate', '2') # 默认网游
area_id = 0
# 构建请求参数并添加WBI签名 [^30^]
params = {
'parent_area_id': parent_area_id,
'area_id': area_id,
'page': pg,
'platform': 'web',
'sort_type': 'online' # 按热度排序
}
signed_params = self._enc_wbi(params)
# 调用getList接口
api_url = f'{self.hosts[tid][0]}/xlive/web-interface/v1/second/getList'
data = self.fetch(api_url, params=signed_params, headers=self.headers[0]).json()
# 如果WBI签名失败,尝试不带签名
if data.get('code') == -352:
print("WBI签名失败,尝试无签名请求...")
params = {
'parent_area_id': parent_area_id,
'area_id': area_id,
'page': pg,
'platform': 'web',
'sort_type': 'online'
}
data = self.fetch(api_url, params=params, headers=self.headers[0]).json()
if data.get('code') == 0:
room_list = data.get('data', {}).get('list', [])
for room in room_list:
if room.get('roomid'):
# 处理在线人数显示
online = room.get('online', 0)
if online > 10000:
online_str = f"{online / 10000:.1f}"
else:
online_str = str(online)
v = self.buildvod(
f"{tid}@@{room['roomid']}",
room.get('title', '未知标题'),
room.get('cover') or room.get('system_cover'),
f"{online_str}",
0,
room.get('uname', ''),
style={"type": "rect", "ratio": 1.33}
)
vdata.append(v)
# 检查是否有更多数据
has_more = data.get('data', {}).get('has_more', 0)
if not has_more:
pagecount = int(pg)
else:
pagecount = 9999
else:
print(f"B站API返回错误: {data.get('message', '未知错误')} (code: {data.get('code')})")
pagecount = 1
return vdata, pagecount
except Exception as e:
print(f"B站内容获取错误: {e}")
return vdata, 1
def huyaContent(self, tid, pg, filter, extend, vdata):
if extend.get('cate') and pg == '1' and 'click' not in tid:
id = extend.get('cate')
data = self.fetch(f'{self.referers[tid]}/liveconfig/game/bussLive?bussType={id}',
headers=self.headers[1]).json()
for i in data['data']:
v = self.buildvod(
vod_id=f"click_{tid}@@{int(i['gid'])}",
vod_name=i.get('gameFullName'),
vod_pic=f'https://huyaimg.msstatic.com/cdnimage/game/{int(i["gid"])}-MS.jpg',
vod_tag=1,
style={"type": "oval", "ratio": 1}
)
vdata.append(v)
return vdata, 1
else:
gid = ''
if 'click' in tid:
ids = tid.split('_')[1].split('@@')
tid = ids[0]
gid = f'&gameId={ids[1]}'
data = self.fetch(f'{self.hosts[tid][0]}/cache.php?m=LiveList&do=getLiveListByPage&tagAll=0{gid}&page={pg}',
headers=self.headers[1]).json()
for i in data['data']['datas']:
if i.get('profileRoom'):
v = self.buildvod(
f"{tid}@@{i['profileRoom']}",
i.get('introduction'),
i.get('screenshot'),
str(int(i.get('totalCount', '1')) / 10000) + '',
0,
i.get('nick'),
style={"type": "rect", "ratio": 1.33}
)
vdata.append(v)
return vdata, 9999
def douyuContent(self, tid, pg, filter, extend, vdata):
if extend.get('cate') and pg == '1' and 'click' not in tid:
for i in self.dyufdata['data']['cate2Info']:
if str(i['cate1Id']) == extend['cate']:
v = self.buildvod(
vod_id=f"click_{tid}@@{i['cate2Id']}",
vod_name=i.get('cate2Name'),
vod_pic=i.get('icon'),
vod_remarks=i.get('count'),
vod_tag=1,
style={"type": "oval", "ratio": 1}
)
vdata.append(v)
return vdata, 1
else:
path = f'/japi/weblist/apinc/allpage/6/{pg}'
if 'click' in tid:
ids = tid.split('_')[1].split('@@')
tid = ids[0]
path = f'/gapi/rkc/directory/mixList/2_{ids[1]}/{pg}'
url = f'{self.hosts[tid]}{path}'
data = self.fetch(url, headers=self.headers[1]).json()
for i in data['data']['rl']:
v = self.buildvod(
vod_id=f"{tid}@@{i['rid']}",
vod_name=i.get('rn'),
vod_pic=i.get('rs16'),
vod_year=str(int(i.get('ol', 1)) / 10000) + '',
vod_remarks=i.get('nn'),
style={"type": "rect", "ratio": 1.33}
)
vdata.append(v)
return vdata, 9999
def detailContent(self, ids):
ids = ids[0].split('@@')
if ids[0] == 'wangyi':
vod = self.wyccDetail(ids)
elif ids[0] == 'bili':
vod = self.biliDetail(ids)
elif ids[0] == 'huya':
vod = self.huyaDetail(ids)
elif ids[0] == 'douyu':
vod = self.douyuDetail(ids)
return {'list': [vod]}
def wyccDetail(self, ids):
try:
vdata = self.getpq(f'{self.hosts[ids[0]]}/{ids[1]}', self.headers[0])('script').eq(-1).text()
def get_quality_name(vbr):
if vbr <= 600:
return "标清"
elif vbr <= 1000:
return "高清"
elif vbr <= 2000:
return "超清"
else:
return "蓝光"
data = json.loads(vdata)['props']['pageProps']['roomInfoInitData']
name = data['live'].get('title', ids[0])
vod = self.buildvod(vod_name=data.get('keywords_suffix'), vod_remarks=data['live'].get('title'),
vod_content=data.get('description_suffix'))
resolution_data = data['live']['quickplay']['resolution']
all_streams = {}
sorted_qualities = sorted(resolution_data.items(),
key=lambda x: x[1]['vbr'],
reverse=True)
for quality, data in sorted_qualities:
vbr = data['vbr']
quality_name = get_quality_name(vbr)
for cdn_name, url in data['cdn'].items():
if cdn_name not in all_streams and type(url) == str and url.startswith('http'):
all_streams[cdn_name] = []
if isinstance(url, str) and url.startswith('http'):
all_streams[cdn_name].extend([quality_name, url])
plists = []
names = []
for i, (cdn_name, stream_list) in enumerate(all_streams.items(), 1):
names.append(f'线路{i}')
pstr = f"{name}${ids[0]}@@{self.e64(json.dumps(stream_list))}"
plists.append(pstr)
vod['vod_play_from'] = "$$$".join(names)
vod['vod_play_url'] = "$$$".join(plists)
return vod
except Exception as e:
return self.handle_exception(e)
def biliDetail(self, ids):
"""
B站直播详情 - 使用playUrl接口获取多清晰度
"""
try:
room_id = ids[1]
# 获取房间信息
info_res = self.fetch(
f'{self.hosts["bili"][0]}/room/v1/Room/get_info?room_id={room_id}',
headers=self.headers[0]
).json()
if info_res.get('code') != 0:
return self.handle_exception(Exception("获取房间信息失败"))
room_info = info_res['data']
title = room_info.get('title', 'B站直播')
vod = self.buildvod(
vod_name=title,
type_name=f"{room_info.get('parent_area_name', '')}/{room_info.get('area_name', '')}",
vod_director=room_info.get('uname', ''),
vod_remarks=f"在线{room_info.get('online', 0)}"
)
# 获取播放地址信息
play_res = self.fetch(
f'{self.hosts["bili"][0]}/room/v1/Room/playUrl?cid={room_id}&qn=10000&platform=web',
headers={
**self.headers[0],
'Referer': 'https://live.bilibili.com/',
'Origin': 'https://live.bilibili.com'
}
).json()
if play_res.get('code') != 0:
return self.handle_exception(Exception("获取播放地址失败"))
play_data = play_res['data']
accept_quality = play_data.get('accept_quality', ['10000', '400', '250', '150'])
quality_desc = {item['qn']: item['desc'] for item in play_data.get('quality_description', [])}
# 构建清晰度列表
qualities = []
for qn in sorted([int(q) for q in accept_quality], reverse=True):
desc = quality_desc.get(qn, f'清晰度{qn}')
qualities.append(f"{desc}$bili@@{room_id}@@{qn}")
vod['vod_play_from'] = 'B站直播'
vod['vod_play_url'] = '#'.join(qualities)
return vod
except Exception as e:
print(f"B站详情错误: {e}")
return self.handle_exception(e)
def huyaDetail(self, ids):
"""
虎牙播放详情 - 参考最新三合一.js重构
支持多线路多清晰度选择
核心算法:通过房间信息API获取uid、streamName和rateArray,为每个清晰度生成签名URL
清晰度说明:
- 蓝光8M/6M/4M/10M = 8000/6000/4000/10000 kbps = 1080P+
- 蓝光 = 3000 kbps = 1080P
- 超清 = 2000 kbps = 1080P (官方标准)
- 高清 = 1200 kbps = 720P
- 标清/流畅 = 500-800 kbps = 480P/540P
"""
try:
room_id = ids[1]
# 1. 获取房间信息
api_url = f'{self.hosts[ids[0]][1]}/cache.php?m=Live&do=profileRoom&roomid={room_id}'
res = self.fetch(api_url, headers=self.headers[0])
if res.status_code != 200:
return self.handle_exception(Exception(f"API请求失败: {res.status_code}"))
data = res.json()
if not data or not data.get('data'):
return self.handle_exception(Exception("房间数据为空"))
room_data = data['data']
# 2. 提取关键信息
uid = room_data.get('profileInfo', {}).get('uid')
stream_info = room_data.get('stream', {})
live_data = room_data.get('liveData', {})
if not uid:
return self.handle_exception(Exception("缺少uid"))
# 3. 获取streamName和码率信息
base_stream_list = stream_info.get('baseSteamInfoList', [])
if not base_stream_list:
return self.handle_exception(Exception("无直播流信息"))
# 获取第一个CDN的streamName作为基准
base_stream = base_stream_list[0]
stream_name = base_stream.get('sStreamName')
if not stream_name:
return self.handle_exception(Exception("无法获取streamName"))
# 4. 构建VOD对象
vod = self.buildvod(
vod_name=live_data.get('introduction', '虎牙直播'),
type_name=live_data.get('gameFullName', ''),
vod_director=live_data.get('nick', ''),
vod_remarks=live_data.get('contentIntro', ''),
)
# 5. 获取所有CDN线路
cdn_list = []
for stream in base_stream_list:
cdn_type = stream.get('sCdnType', 'AL')
flv_url = stream.get('sFlvUrl', '')
hls_url = stream.get('sHlsUrl', '')
stream_name_cdn = stream.get('sStreamName', stream_name)
if flv_url:
cdn_list.append({
'cdn': cdn_type,
'flv_base': flv_url,
'hls_base': hls_url,
'stream_name': stream_name_cdn,
'priority': stream.get('iWebPriorityRate', 0)
})
# 按优先级排序
cdn_list.sort(key=lambda x: x['priority'], reverse=True)
# 6. 获取清晰度列表 (rateArray)
rate_array = stream_info.get('rateArray', [])
# 如果没有rateArray,尝试从vMultiStreamInfo获取
if not rate_array and 'vMultiStreamInfo' in room_data:
rate_array = room_data['vMultiStreamInfo']
# 如果仍然没有,使用默认清晰度(按虎牙官方标准)
if not rate_array:
rate_array = [
{'sDisplayName': '蓝光4M', 'iBitRate': 4000},
{'sDisplayName': '蓝光', 'iBitRate': 3000},
{'sDisplayName': '超清', 'iBitRate': 2000}, # 2000kbps = 1080P
{'sDisplayName': '高清', 'iBitRate': 1200}, # 1200kbps = 720P
{'sDisplayName': '流畅', 'iBitRate': 500}
]
# 过滤和排序清晰度
# 虎牙的rateArray中,iBitRate就是码率值,sDisplayName是显示名称
# 需要确保:超清=2000kbps(1080P),高清=1200kbps(720P)
filtered_rates = []
seen_bitrates = set()
for rate in rate_array:
bit_rate = rate.get('iBitRate', 0)
name = rate.get('sDisplayName', '')
# 跳过重复的码率
if bit_rate in seen_bitrates:
continue
# 修正清晰度名称,确保符合虎牙标准
# 2000kbps应该是超清(1080P),不是高清
if bit_rate == 2000 and ('高清' in name or '720' in name):
name = '超清' # 强制修正为超清
elif bit_rate == 1200 and ('标清' in name or '480' in name):
name = '高清' # 1200kbps对应高清
elif bit_rate == 2000 and name == '原画':
name = '超清' # 修正原画为超清
seen_bitrates.add(bit_rate)
filtered_rates.append({
'sDisplayName': name,
'iBitRate': bit_rate
})
# 按码率从高到低排序
sorted_rates = sorted(filtered_rates, key=lambda x: x['iBitRate'], reverse=True)
# 7. 为每个CDN生成各清晰度的播放URL
play_lines = []
line_names = []
for cdn_idx, cdn in enumerate(cdn_list[:3]): # 最多取3个CDN
cdn_name = cdn['cdn']
line_names.append(f"线路{cdn_idx + 1}({cdn_name})")
qualities = []
for rate in sorted_rates:
quality_name = rate['sDisplayName']
bit_rate = rate['iBitRate']
# 生成该清晰度的URL
quality_url = self._generate_huya_play_url(
cdn, uid, stream_name, bit_rate
)
qualities.extend([quality_name, quality_url])
# 编码该线路的所有清晰度
encoded_qualities = self.e64(json.dumps(qualities))
play_lines.append(f"{live_data.get('introduction', '直播')}${ids[0]}@@{encoded_qualities}")
# 8. 构建播放数据
vod['vod_play_from'] = "$$$".join(line_names)
vod['vod_play_url'] = "$$$".join(play_lines)
return vod
except Exception as e:
return self.handle_exception(e)
def _generate_huya_play_url(self, cdn, uid, stream_name, bit_rate):
"""
生成虎牙播放URL,参考最新三合一.js算法
关键:ratio参数必须正确设置为iBitRate值(如2000、4000等)
"""
# 基础URL构建
flv_base = cdn['flv_base']
stream = cdn['stream_name']
# 生成时间戳和签名参数
timestamp = int(time.time())
seqid = f"{uid}{timestamp}"
ss = hashlib.md5(f"{seqid}|huya_adr|102".encode()).hexdigest()
ws_time = hex(timestamp + 21600)[2:] # 16进制,有效期6小时
# 计算wsSecret
ws_secret = hashlib.md5(
f"DWq8BcJ3h6DJt6TY_{uid}_{stream_name}_{ss}_{ws_time}".encode()
).hexdigest()
# 构建基础URL
base_url = f"{flv_base}/{stream}.flv"
# 关键修复:ratio参数直接使用iBitRate值
# 超清=2000,高清=1200,蓝光=3000/4000/6000/8000
if bit_rate > 0:
ratio_param = f"ratio={bit_rate}"
else:
# 原画/0码率时,使用默认2000或从URL推断
ratio_param = "ratio=2000"
# 构建完整URL
play_url = (
f"{base_url}?{ratio_param}&wsSecret={ws_secret}&wsTime={ws_time}"
f"&ctype=huya_adr&seqid={seqid}&uid={uid}"
f"&fs=bgct&ver=1&t=102"
)
return play_url
def douyuDetail(self, ids):
"""
斗鱼播放详情 - 参考最新三合一.js重构
核心算法:设备ID生成 -> 获取加密密钥 -> 计算签名 -> 获取播放地址
修复:切换分辨率只能播放1秒的问题
方案:存储房间号和码率信息,在playerContent中实时获取对应码率的URL
"""
try:
channel = ids[1]
headers = self.gethr(0, zr=f'{self.hosts[ids[0]]}/{channel}')
# 1. 初始化会话和设备ID (参考JS中的initialize和setupDeviceId)
session = {}
# 请求首页获取Cookie
try:
home_res = self.fetch(f'{self.hosts[ids[0]]}/{channel}', headers=headers)
if home_res.headers.get('Set-Cookie'):
cookie_str = home_res.headers.get('Set-Cookie')
# 解析dy_did
did_match = re.search(r'dy_did=([a-f0-9]{32})', cookie_str)
if did_match:
device_id = did_match.group(1)
else:
device_id = self._generate_random_hex(32)
else:
device_id = self._generate_random_hex(32)
except:
device_id = self._generate_random_hex(32)
session['dy_did'] = device_id
session['mantine-color-scheme-value'] = 'light'
# 2. 获取房间基本信息
betard_res = self.fetch(f'{self.hosts[ids[0]]}/betard/{channel}', headers=headers).json()
if not betard_res or not betard_res.get('room'):
return self.handle_exception(Exception("获取房间信息失败"))
room_info = betard_res['room']
vname = room_info.get('room_name', '斗鱼直播')
vod = self.buildvod(
vod_name=vname,
vod_remarks=room_info.get('second_lvl_name', ''),
vod_director=room_info.get('nickname', ''),
)
# 3. 获取安全密钥 (参考JS中的getSecurityKey)
sec_url = f"{self.hosts[ids[0]]}/wgapi/livenc/liveweb/websec/getEncryption?did={device_id}"
sec_res = self.fetch(sec_url, headers=headers).json()
if not sec_res or sec_res.get('error') != 0:
return self.handle_exception(Exception("获取加密密钥失败"))
security_data = sec_res['data']
secret_key = security_data.get('key')
random_str = security_data.get('rand_str')
enc_time = security_data.get('enc_time', 1)
enc_data = security_data.get('enc_data')
# 4. 计算签名 (参考JS中的computeSignature)
current_time = int(time.time())
# 迭代计算MD5
current = random_str
for _ in range(enc_time):
current = hashlib.md5(f"{current}{secret_key}".encode()).hexdigest()
signature = hashlib.md5(f"{current}{secret_key}{channel}{current_time}".encode()).hexdigest()
# 5. 请求播放地址 (参考JS中的requestStreamData)
play_payload = {
'enc_data': enc_data,
'tt': str(current_time),
'did': device_id,
'auth': signature,
'cdn': '',
'rate': '',
'hevc': '0',
'fa': '0',
'ive': '0'
}
play_api = f"{self.hosts[ids[0]]}/lapi/live/getH5PlayV1/{channel}"
# 构建请求头带Cookie
play_headers = headers.copy()
cookie_str = '; '.join([f"{k}={v}" for k, v in session.items()])
play_headers['Cookie'] = cookie_str
play_headers['Content-Type'] = 'application/x-www-form-urlencoded'
play_res = requests.post(play_api, data=play_payload, headers=play_headers, timeout=10).json()
if not play_res or play_res.get('error') != 0:
# 尝试旧版API
play_res = self._try_legacy_douyu_api(channel, device_id, signature, current_time, play_headers)
if not play_res:
return self.handle_exception(Exception("获取播放地址失败"))
stream_info = play_res.get('data', {})
# 6. 检查并更新设备ID (参考JS中的checkAndUpdateDeviceId)
rtmp_live = stream_info.get('rtmp_live', '')
if rtmp_live:
did_match = re.search(r'did=([a-f0-9]{32})', rtmp_live)
if did_match and did_match.group(1) != device_id:
device_id = did_match.group(1)
session['dy_did'] = device_id
# 重新请求
play_payload['did'] = device_id
play_res = requests.post(play_api, data=play_payload, headers=play_headers, timeout=10).json()
if play_res and play_res.get('error') == 0:
stream_info = play_res.get('data', {})
# 7. 提取播放URL和多码率信息
stream_url = None
if stream_info.get('rtmp_url') and stream_info.get('rtmp_live'):
stream_url = f"{stream_info['rtmp_url']}/{stream_info['rtmp_live']}"
elif stream_info.get('hls_url'):
stream_url = stream_info['hls_url']
if not stream_url:
return self.handle_exception(Exception("无法获取播放地址"))
# 8. 构建多码率选项
multirates = stream_info.get('multirates', [])
# 关键修复:存储房间号和码率信息,而不是直接存储URL
# 这样在切换清晰度时可以重新获取对应码率的签名URL
qualities = []
if multirates:
# 按码率排序
sorted_rates = sorted(multirates, key=lambda x: x.get('bit', 0), reverse=True)
for rate in sorted_rates:
bit_rate = rate.get('rate', -1)
name = rate.get('name', f"{bit_rate}P")
# 存储格式:码率值,用于playerContent中重新获取URL
# 使用特殊标记#来区分这是码率值而不是URL
qualities.extend([name, f"#{bit_rate}"])
else:
# 只有原画
qualities = ['原画', '#-1']
# 同时存储房间号和设备信息,用于重新获取URL
# 格式:房间号|设备ID|签名信息(base64编码)
session_info = {
'channel': channel,
'device_id': device_id,
'secret_key': secret_key,
'random_str': random_str,
'enc_time': enc_time,
'enc_data': enc_data
}
encoded_session = self.e64(json.dumps(session_info))
# 9. 构建播放数据
# vod_play_url格式:房间名$平台@@base64(清晰度列表)@@base64(会话信息)
encoded_qualities = self.e64(json.dumps(qualities))
vod['vod_play_from'] = '斗鱼直播'
vod['vod_play_url'] = f"{vname}${ids[0]}@@{encoded_qualities}@@{encoded_session}"
return vod
except Exception as e:
return self.handle_exception(e)
def _generate_random_hex(self, length):
"""生成随机十六进制字符串"""
hex_chars = '0123456789abcdef'
return ''.join(random.choice(hex_chars) for _ in range(length))
def _try_legacy_douyu_api(self, channel, device_id, signature, timestamp, headers):
"""尝试使用旧版API获取播放地址"""
try:
legacy_payload = {
'did': device_id,
'tt': str(timestamp),
'sign': signature,
'cdn': '',
'rate': '-1',
'ver': 'Douyu_223061205',
'iar': '1',
'ive': '1',
'hevc': '0',
'fa': '0'
}
legacy_api = f"https://www.douyu.com/lapi/live/getH5Play/{channel}"
res = requests.post(legacy_api, data=legacy_payload, headers=headers, timeout=10)
return res.json() if res.status_code == 200 else None
except:
return None
def _get_douyu_play_url(self, channel, device_id, secret_key, random_str, enc_time, enc_data, rate):
"""
获取斗鱼指定码率的播放URL(带签名)
用于切换清晰度时重新获取URL
"""
try:
current_time = int(time.time())
# 重新计算签名
current = random_str
for _ in range(enc_time):
current = hashlib.md5(f"{current}{secret_key}".encode()).hexdigest()
signature = hashlib.md5(f"{current}{secret_key}{channel}{current_time}".encode()).hexdigest()
# 构建请求
play_payload = {
'enc_data': enc_data,
'tt': str(current_time),
'did': device_id,
'auth': signature,
'cdn': '',
'rate': str(rate) if rate > 0 else '',
'hevc': '0',
'fa': '0',
'ive': '0'
}
play_api = f"https://www.douyu.com/lapi/live/getH5PlayV1/{channel}"
headers = {
'User-Agent': self.headers[0]['User-Agent'],
'Referer': f'https://www.douyu.com/{channel}',
'Origin': 'https://www.douyu.com',
'Cookie': f'dy_did={device_id}; mantine-color-scheme-value=light',
'Content-Type': 'application/x-www-form-urlencoded'
}
play_res = requests.post(play_api, data=play_payload, headers=headers, timeout=10).json()
if not play_res or play_res.get('error') != 0:
# 尝试旧版API
return self._get_douyu_play_url_legacy(channel, device_id, signature, current_time, rate)
stream_info = play_res.get('data', {})
# 检查设备ID是否匹配
if stream_info.get('rtmp_live'):
did_match = re.search(r'did=([a-f0-9]{32})', stream_info['rtmp_live'])
if did_match and did_match.group(1) != device_id:
# 设备ID不匹配,使用新设备ID重新获取
return self._get_douyu_play_url(channel, did_match.group(1), secret_key, random_str, enc_time, enc_data, rate)
if stream_info.get('rtmp_url') and stream_info.get('rtmp_live'):
return f"{stream_info['rtmp_url']}/{stream_info['rtmp_live']}"
elif stream_info.get('hls_url'):
return stream_info['hls_url']
return None
except Exception as e:
print(f"获取斗鱼播放URL失败: {e}")
return None
def _get_douyu_play_url_legacy(self, channel, device_id, signature, timestamp, rate):
"""使用旧版API获取斗鱼播放URL"""
try:
legacy_payload = {
'did': device_id,
'tt': str(timestamp),
'sign': signature,
'cdn': '',
'rate': str(rate) if rate > 0 else '-1',
'ver': 'Douyu_223061205',
'iar': '1',
'ive': '1',
'hevc': '0',
'fa': '0'
}
legacy_api = f"https://www.douyu.com/lapi/live/getH5Play/{channel}"
headers = {
'User-Agent': self.headers[0]['User-Agent'],
'Referer': f'https://www.douyu.com/{channel}',
'Cookie': f'dy_did={device_id}',
'Content-Type': 'application/x-www-form-urlencoded'
}
res = requests.post(legacy_api, data=legacy_payload, headers=headers, timeout=10)
if res.status_code == 200:
data = res.json()
if data.get('error') == 0:
stream_info = data.get('data', {})
if stream_info.get('rtmp_url') and stream_info.get('rtmp_live'):
return f"{stream_info['rtmp_url']}/{stream_info['rtmp_live']}"
return None
except:
return None
def searchContent(self, key, quick, pg="1"):
pass
def playerContent(self, flag, id, vipFlags):
try:
ids = id.split('@@')
p = 1
if ids[0] in ['wangyi']:
p, url = 0, json.loads(self.d64(ids[1]))
elif ids[0] == 'bili':
p, url = self.biliplay(ids)
elif ids[0] == 'huya':
p, url = self.huyaplay(ids)
elif ids[0] == 'douyu':
p, url = self.douyuplay(ids)
return {'parse': p, 'url': url, 'header': self.playheaders[ids[0]]}
except Exception as e:
return {'parse': 1, 'url': self.excepturl, 'header': self.headers[0]}
def biliplay(self, ids):
"""
B站播放解析 - 使用playUrl接口获取指定清晰度直播流
ids: [平台, 房间号, 清晰度qn]
支持多线路返回
"""
try:
room_id = ids[1]
qn = ids[2] if len(ids) > 2 else '10000'
# 使用playUrl接口获取直播流
play_url = f'{self.hosts["bili"][0]}/room/v1/Room/playUrl?cid={room_id}&qn={qn}&platform=web'
data = self.fetch(play_url, headers={
**self.headers[0],
'Referer': 'https://live.bilibili.com/',
'Origin': 'https://live.bilibili.com'
}).json()
if data.get('code') != 0:
return 1, self.excepturl
play_data = data['data']
durl_list = play_data.get('durl', [])
if not durl_list:
return 1, self.excepturl
# 构建多线路结果 [线路1, URL1, 线路2, URL2, ...]
urls = []
for idx, item in enumerate(durl_list, 1):
url = item.get('url')
if url:
urls.extend([f'线路{idx}', url])
# 如果只有一条线路,直接返回URL
if len(urls) == 2:
return 0, urls[1] # 直接返回URL字符串
return 0, urls
except Exception as e:
print(f"B站播放错误: {e}")
return 1, self.excepturl
def huyaplay(self, ids):
"""
虎牙播放解析 - 返回所有清晰度选项供用户选择
ids[1] 格式: base64编码的 [清晰度名称1, URL1, 清晰度名称2, URL2, ...]
"""
try:
# ids[1] 是编码后的播放地址列表 [名称1, URL1, 名称2, URL2, ...]
decoded = json.loads(self.d64(ids[1]))
# decoded 是一个列表,奇数索引是名称,偶数索引是URL
return 0, decoded
except Exception as e:
print(f"虎牙播放解析错误: {e}")
return 1, self.excepturl
def douyuplay(self, ids):
"""
斗鱼播放解析 - 实时获取对应码率的播放URL
ids格式: [平台, base64(清晰度列表), base64(会话信息)]
清晰度列表: [名称1, #码率1, 名称2, #码率2, ...]
#表示这是码率值,需要重新获取URL
"""
try:
if len(ids) < 3:
# 兼容旧格式
decoded = json.loads(self.d64(ids[1]))
return 0, decoded
# 解析清晰度列表和会话信息
qualities = json.loads(self.d64(ids[1]))
session_info = json.loads(self.d64(ids[2]))
channel = session_info['channel']
device_id = session_info['device_id']
secret_key = session_info['secret_key']
random_str = session_info['random_str']
enc_time = session_info['enc_time']
enc_data = session_info['enc_data']
# 为每个清晰度实时获取播放URL
result = []
for i in range(0, len(qualities), 2):
name = qualities[i]
rate_marker = qualities[i + 1]
# 解析码率值(去掉#前缀)
if rate_marker.startswith('#'):
rate = int(rate_marker[1:])
else:
rate = -1
# 实时获取对应码率的URL
play_url = self._get_douyu_play_url(
channel, device_id, secret_key, random_str,
enc_time, enc_data, rate
)
if play_url:
result.extend([name, play_url])
if not result:
return 1, self.excepturl
return 0, result
except Exception as e:
print(f"斗鱼播放解析错误: {e}")
return 1, self.excepturl
def localProxy(self, param):
pass
def e64(self, text):
try:
text_bytes = text.encode('utf-8')
encoded_bytes = b64encode(text_bytes)
return encoded_bytes.decode('utf-8')
except Exception as e:
print(f"Base64编码错误: {str(e)}")
return ""
def d64(self, encoded_text):
try:
encoded_bytes = encoded_text.encode('utf-8')
decoded_bytes = b64decode(encoded_bytes)
return decoded_bytes.decode('utf-8')
except Exception as e:
print(f"Base64解码错误: {str(e)}")
return ""
def josn_to_params(self, params, skip_empty=False):
query = []
for k, v in params.items():
if skip_empty and not v:
continue
query.append(f"{k}={v}")
return "&".join(query)
def params_to_json(self, query_string):
parsed_data = parse_qs(query_string)
result = {key: value[0] for key, value in parsed_data.items()}
return result
def buildvod(self, vod_id='', vod_name='', vod_pic='', vod_year='', vod_tag='', vod_remarks='', style='',
type_name='', vod_area='', vod_actor='', vod_director='',
vod_content='', vod_play_from='', vod_play_url=''):
vod = {
'vod_id': vod_id,
'vod_name': vod_name,
'vod_pic': vod_pic,
'vod_year': vod_year,
'vod_tag': 'folder' if vod_tag else '',
'vod_remarks': vod_remarks,
'style': style,
'type_name': type_name,
'vod_area': vod_area,
'vod_actor': vod_actor,
'vod_director': vod_director,
'vod_content': vod_content,
'vod_play_from': vod_play_from,
'vod_play_url': vod_play_url
}
vod = {key: value for key, value in vod.items() if value}
return vod
def getpq(self, url, headers=None, cookies=None):
data = self.fetch(url, headers=headers, cookies=cookies).text
try:
return pq(data)
except Exception as e:
print(f"解析页面错误: {str(e)}")
return pq(data.encode('utf-8'))
def gethr(self, index, rf='', zr=''):
headers = self.headers[index]
if zr:
headers['referer'] = zr
else:
headers['referer'] = f"{self.referers[rf]}/"
return headers
def handle_exception(self, e):
print(f"报错: {str(e)}")
return {'vod_play_from': '哎呀翻车啦', 'vod_play_url': f'翻车啦${self.excepturl}'}
+343
View File
@@ -0,0 +1,343 @@
# coding = utf-8
# !/usr/bin/python
"""
"""
from Crypto.Util.Padding import unpad
from Crypto.Util.Padding import pad
from urllib.parse import unquote
from Crypto.Cipher import ARC4
from urllib.parse import quote
from base.spider import Spider
from Crypto.Cipher import AES
from bs4 import BeautifulSoup
from base64 import b64decode
import urllib.request
import urllib.parse
import binascii
import requests
import base64
import json
import time
import sys
import re
import os
sys.path.append('..')
xurl = "https://app.whjzjx.cn"
headers = {
'User-Agent': 'Linux; Android 12; Pixel 3 XL) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.101 Mobile Safari/537.36'
}
headerf = {
"platform": "1",
"user_agent": "Mozilla/5.0 (Linux; Android 9; V1938T Build/PQ3A.190705.08211809; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/91.0.4472.114 Safari/537.36",
"content-type": "application/json; charset=utf-8"
}
times = int(time.time() * 1000)
data = {
"device": "2a50580e69d38388c94c93605241fb306",
"package_name": "com.jz.xydj",
"android_id": "ec1280db12795506",
"install_first_open": True,
"first_install_time": 1752505243345,
"last_update_time": 1752505243345,
"report_link_url": "",
"authorization": "",
"timestamp": times
}
plain_text = json.dumps(data, separators=(',', ':'), ensure_ascii=False)
key = "B@ecf920Od8A4df7"
key_bytes = key.encode('utf-8')
plain_bytes = plain_text.encode('utf-8')
cipher = AES.new(key_bytes, AES.MODE_ECB)
padded_data = pad(plain_bytes, AES.block_size)
ciphertext = cipher.encrypt(padded_data)
encrypted = base64.b64encode(ciphertext).decode('utf-8')
response = requests.post("https://u.shytkjgs.com/user/v3/account/login", headers=headerf, data=encrypted)
response_data = response.json()
Authorization = response_data['data']['token']
headerx = {
'authorization': Authorization,
'platform': '1',
'version_name': '3.8.3.1'
}
class Spider(Spider):
global xurl
global headerx
global headers
def getName(self):
return "首页"
def init(self, extend):
pass
def isVideoFormat(self, url):
pass
def manualVideoCheck(self):
pass
def extract_middle_text(self, text, start_str, end_str, pl, start_index1: str = '', end_index2: str = ''):
if pl == 3:
plx = []
while True:
start_index = text.find(start_str)
if start_index == -1:
break
end_index = text.find(end_str, start_index + len(start_str))
if end_index == -1:
break
middle_text = text[start_index + len(start_str):end_index]
plx.append(middle_text)
text = text.replace(start_str + middle_text + end_str, '')
if len(plx) > 0:
purl = ''
for i in range(len(plx)):
matches = re.findall(start_index1, plx[i])
output = ""
for match in matches:
match3 = re.search(r'(?:^|[^0-9])(\d+)(?:[^0-9]|$)', match[1])
if match3:
number = match3.group(1)
else:
number = 0
if 'http' not in match[0]:
output += f"#{match[1]}${number}{xurl}{match[0]}"
else:
output += f"#{match[1]}${number}{match[0]}"
output = output[1:]
purl = purl + output + "$$$"
purl = purl[:-3]
return purl
else:
return ""
else:
start_index = text.find(start_str)
if start_index == -1:
return ""
end_index = text.find(end_str, start_index + len(start_str))
if end_index == -1:
return ""
if pl == 0:
middle_text = text[start_index + len(start_str):end_index]
return middle_text.replace("\\", "")
if pl == 1:
middle_text = text[start_index + len(start_str):end_index]
matches = re.findall(start_index1, middle_text)
if matches:
jg = ' '.join(matches)
return jg
if pl == 2:
middle_text = text[start_index + len(start_str):end_index]
matches = re.findall(start_index1, middle_text)
if matches:
new_list = [f'{item}' for item in matches]
jg = '$$$'.join(new_list)
return jg
def homeContent(self, filter):
result = {}
result = {"class": [{"type_id": "1", "type_name": "剧场"},
{"type_id": "3", "type_name": "新剧"},
{"type_id": "2", "type_name": "热播"},
{"type_id": "7", "type_name": "星选"},
{"type_id": "5", "type_name": "阳光"}],
}
return result
def homeVideoContent(self):
videos = []
url= f'{xurl}/v1/theater/home_page?theater_class_id=1&class2_id=4&page_num=1&page_size=24'
detail = requests.get(url=url, headers=headerx)
detail.encoding = "utf-8"
if detail.status_code == 200:
data = detail.json()
for vod in data['data']['list']:
name = vod['theater']['title']
id = vod['theater']['id']
pic = vod['theater']['cover_url']
remark = vod['theater']['play_amount_str']
video = {
"vod_id": id,
"vod_name": name,
"vod_pic": pic,
"vod_remarks": remark
}
videos.append(video)
result = {'list': videos}
return result
def categoryContent(self, cid, pg, filter, ext):
result = {}
videos = []
url = f'{xurl}/v1/theater/home_page?theater_class_id={cid}&page_num={pg}&page_size=24'
detail = requests.get(url=url,headers=headerx)
detail.encoding = "utf-8"
if detail.status_code == 200:
data = detail.json()
for vod in data['data']['list']:
name = vod['theater']['title']
id = vod['theater']['id']
pic = vod['theater']['cover_url']
remark = vod['theater']['theme']
video = {
"vod_id": id,
"vod_name": name,
"vod_pic": pic,
"vod_remarks": remark
}
videos.append(video)
result = {'list': videos}
result['page'] = pg
result['pagecount'] = 9999
result['limit'] = 90
result['total'] = 999999
return result
def detailContent(self, ids):
did = ids[0]
result = {}
videos = []
xianlu = ''
bofang = ''
url = f'{xurl}/v2/theater_parent/detail?theater_parent_id={did}'
detail = requests.get(url=url, headers=headerx)
detail.encoding = "utf-8"
if detail.status_code == 200:
data = detail.json()
url = 'https://fs-im-kefu.7moor-fs1.com/ly/4d2c3f00-7d4c-11e5-af15-41bf63ae4ea0/1732707176882/jiduo.txt'
response = requests.get(url)
response.encoding = 'utf-8'
code = response.text
name = self.extract_middle_text(code, "s1='", "'", 0)
Jumps = self.extract_middle_text(code, "s2='", "'", 0)
content = '剧情:' + data['data']['introduction']
area = data['data']['desc_tags'][0]
remarks = data['data']['filing']
# 修复剧集只有一集的问题 - 检查theaters数据是否存在且不为空
if 'theaters' in data['data'] and data['data']['theaters']:
for sou in data['data']['theaters']:
id = sou['son_video_url']
name = sou['num']
bofang = bofang + str(name) + '$' + id + '#'
bofang = bofang[:-1] if bofang.endswith('#') else bofang
xianlu = '星芽'
else:
# 如果没有theaters数据,检查是否有单个视频URL
if 'video_url' in data['data'] and data['data']['video_url']:
bofang = '1$' + data['data']['video_url']
xianlu = '星芽'
else:
bofang = Jumps
xianlu = '1'
videos.append({
"vod_id": did,
"vod_content": content,
"vod_remarks": remarks,
"vod_area": area,
"vod_play_from": xianlu,
"vod_play_url": bofang
})
result['list'] = videos
return result
def playerContent(self, flag, id, vipFlags):
result = {}
result["parse"] = 0
result["playUrl"] = ''
result["url"] = id
result["header"] = headers
return result
def searchContentPage(self, key, quick, page):
result = {}
videos = []
payload = {
"text": key
}
url = f"{xurl}/v3/search"
detail = requests.post(url=url, headers=headerx, json=payload)
if detail.status_code == 200:
detail.encoding = "utf-8"
data = detail.json()
for vod in data['data']['theater']['search_data']:
name = vod['title']
id = vod['id']
pic = vod['cover_url']
remark = vod['score_str']
video = {
"vod_id": id,
"vod_name": name,
"vod_pic": pic,
"vod_remarks": remark
}
videos.append(video)
result['list'] = videos
result['page'] = page
result['pagecount'] = 9999
result['limit'] = 90
result['total'] = 999999
return result
def searchContent(self, key, quick, pg="1"):
return self.searchContentPage(key, quick, '1')
def localProxy(self, params):
if params['type'] == "m3u8":
return self.proxyM3u8(params)
elif params['type'] == "media":
return self.proxyMedia(params)
elif params['type'] == "ts":
return self.proxyTs(params)
return None
+509
View File
@@ -0,0 +1,509 @@
# coding=utf-8
import base64
import json
import re
import requests
import sys
from functools import wraps
from urllib.parse import quote, unquote, urljoin, urlparse
from lxml import etree
sys.path.append("..")
from base.spider import Spider as BaseSpider
LOG_TAG = "[威视TV_DEBUG]"
def trace_interface(function):
@wraps(function)
def wrapper(self, *args, **kwargs):
interface = function.__name__
self._debug(
"接口开始",
interface=interface,
arguments=self._summarize_arguments(interface, args, kwargs),
)
try:
result = function(self, *args, **kwargs)
except Exception as error:
self._debug(
"接口异常",
interface=interface,
error_type=type(error).__name__,
error=str(error),
)
raise
self._debug("接口完成", interface=interface, **self._summarize_result(interface, result))
return result
return wrapper
class Spider(BaseSpider):
DEFAULT_HOST = "https://weishitv.xyz"
UA = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/126.0.0.0 Safari/537.36"
)
CLASSES = [
{"type_id": "1", "type_name": "电影"},
{"type_id": "2", "type_name": "剧集"},
{"type_id": "3", "type_name": "综艺"},
{"type_id": "4", "type_name": "动漫"},
]
@trace_interface
def getName(self):
return "威视TV"
@trace_interface
def init(self, extend=""):
candidate = extend
try:
if isinstance(extend, str) and extend.strip().startswith("{"):
candidate = json.loads(extend)
if isinstance(candidate, dict):
candidate = candidate.get("host") or candidate.get("url") or candidate.get("site") or ""
except Exception as error:
self._debug("配置解析失败", error_type=type(error).__name__, error=str(error))
candidate = ""
value = str(candidate or "").strip().rstrip("/")
self.host = value if re.match(r"^https?://", value, re.I) else self.DEFAULT_HOST
self.headers = {
"User-Agent": self.UA,
"Referer": self.host + "/",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.9",
}
self.session = requests.Session()
self.session.trust_env = False
self.session.headers.update(self.headers)
@trace_interface
def homeContent(self, filter):
result = {"class": list(self.CLASSES)}
if filter:
result["filters"] = {}
return result
@trace_interface
def homeVideoContent(self):
return {"list": self._parse_list(self._get(self.host + "/"))[:60]}
@trace_interface
def categoryContent(self, tid, pg, filter, extend):
page = self._page(pg)
html = self._get(f"{self.host}/index.php/vod/show/id/{quote(str(tid))}/page/{page}.html")
items = self._parse_list(html)
pagecount = self._page_count(html, page + 1 if items else page)
return {
"list": items,
"page": page,
"pagecount": pagecount,
"limit": len(items),
"total": pagecount * max(1, len(items)),
}
@trace_interface
def detailContent(self, ids):
if not ids:
return {"list": []}
vod = self._parse_detail(str(ids[0]), self._get(self._absolute(ids[0])))
return {"list": [vod] if vod else []}
@trace_interface
def searchContent(self, key, quick, pg="1"):
page = self._page(pg)
suffix = f"&page={page}" if page > 1 else ""
html = self._get(f"{self.host}/index.php/vod/search.html?wd={quote(str(key))}{suffix}")
items = self._parse_list(html)
return {
"list": items,
"page": page,
"pagecount": self._page_count(html, page + 1 if items else page),
}
@trace_interface
def playerContent(self, flag, id, vipFlags):
value = str(id or "")
if self._is_media(value):
return {"parse": 0, "url": value, "header": self._player_headers()}
url = self._decode_player(self._get(self._absolute(value)))
if not url:
return {"parse": 1, "url": "", "header": self._player_headers()}
return {"parse": 0 if self._is_media(url) else 1, "url": url, "header": self._player_headers()}
def _get(self, url):
self._debug("HTTP请求", method="GET", url=self._safe_url(url))
try:
response = self.session.get(url, timeout=15)
response.encoding = "utf-8"
text = response.text or ""
status = getattr(response, "status_code", 200)
final_url = str(getattr(response, "url", "") or url)
response_headers = getattr(response, "headers", {}) or {}
title = self._page_title(text)
self._debug(
"HTTP响应",
status=status,
final_url=self._safe_url(final_url),
content_length=len(text),
content_type=str(response_headers.get("Content-Type") or ""),
title=title,
page_kind=self._page_kind(text, title),
)
if status >= 400:
self._debug("HTTP状态异常", status=status, url=self._safe_url(final_url))
return ""
return text
except Exception as error:
self._debug(
"HTTP异常",
url=self._safe_url(url),
error_type=type(error).__name__,
error=str(error),
)
return ""
def _absolute(self, value):
text = str(value or "").strip()
if not text:
return self.host + "/"
if text.startswith("//"):
return "https:" + text
if re.match(r"^https?://", text, re.I):
parsed = urlparse(text)
if "/index.php/" in parsed.path:
return self.host + parsed.path + (("?" + parsed.query) if parsed.query else "")
return text
return urljoin(self.host + "/", text)
def _relative(self, value):
text = str(value or "").strip()
if not text:
return ""
if re.match(r"^https?://", text, re.I):
parsed = urlparse(text)
return parsed.path + (("?" + parsed.query) if parsed.query else "")
return text if text.startswith("/") else "/" + text
def _parse_list(self, html):
doc = self._html(html)
raw_list_markers = len(re.findall(r"public-list-box", html or "", re.I))
raw_detail_markers = len(re.findall(r"vod/detail", html or "", re.I))
skipped = {
"missing_link": 0,
"missing_id": 0,
"missing_name": 0,
"duplicate": 0,
"errors": 0,
}
if doc is None:
self._debug(
"列表解析",
html_parse_ok=False,
raw_list_markers=raw_list_markers,
raw_detail_markers=raw_detail_markers,
containers=0,
detail_links=0,
parsed=0,
skipped=skipped,
samples=[],
html_length=len(html or ""),
)
return []
items = []
seen = set()
boxes = doc.xpath("//div[contains(concat(' ', normalize-space(@class), ' '), ' public-list-box ')]")
detail_links = len(doc.xpath("//a[contains(@href, '/index.php/vod/detail/')]"))
for box in boxes:
try:
links = box.xpath(".//a[contains(concat(' ', normalize-space(@class), ' '), ' public-list-exp ') and contains(@href, '/index.php/vod/detail/')]")
if not links:
skipped["missing_link"] += 1
continue
link = links[0]
vod_id = self._relative(link.get("href"))
if not vod_id:
skipped["missing_id"] += 1
continue
name = (link.get("title") or "").strip()
if not name:
alt = link.xpath("string(.//img[1]/@alt)").strip()
name = re.sub(r"封面图$", "", alt).strip()
if not name:
skipped["missing_name"] += 1
continue
if vod_id in seen:
skipped["duplicate"] += 1
continue
seen.add(vod_id)
pic = link.xpath("string(.//img[1]/@data-src)") or link.xpath("string(.//img[1]/@data-original)") or link.xpath("string(.//img[1]/@src)")
pic = "" if str(pic).startswith("data:image/") else self._absolute(pic)
remarks = self._first_text(box, ".//*[contains(concat(' ', normalize-space(@class), ' '), ' public-list-prb ')]")
if not remarks:
remarks = self._first_text(box, ".//*[contains(concat(' ', normalize-space(@class), ' '), ' public-prt ')]")
items.append({"vod_id": vod_id, "vod_name": name, "vod_pic": pic, "vod_remarks": remarks})
except Exception as error:
skipped["errors"] += 1
self._debug(
"列表条目解析失败",
error_type=type(error).__name__,
error=str(error),
)
self._debug(
"列表解析",
html_parse_ok=True,
raw_list_markers=raw_list_markers,
raw_detail_markers=raw_detail_markers,
containers=len(boxes),
detail_links=detail_links,
parsed=len(items),
skipped=skipped,
samples=[item["vod_name"] for item in items[:3]],
html_length=len(html or ""),
)
return items
def _parse_detail(self, vod_id, html):
doc = self._html(html)
if doc is None:
return None
name = self._first_text(doc, "//*[contains(concat(' ', normalize-space(@class), ' '), ' this-desc-title ')]")
if not name:
return None
style = doc.xpath("string((//*[contains(concat(' ', normalize-space(@class), ' '), ' this-pic-bj ')])[1]/@style)")
match = re.search(r"url\(['\"]?([^'\")]+)", style or "", re.I)
pic = self._absolute(match.group(1)) if match else ""
info = [self._clean("".join(node.itertext())) for node in doc.xpath("//*[contains(concat(' ', normalize-space(@class), ' '), ' this-desc-info ')]//span")]
info = [item for item in info if item]
year = next((item for item in info if re.match(r"^(19|20)\d{2}$", item)), "")
area = next((item for item in info if item != year and not re.search(r"更新|集|完结|分$", item)), "")
remarks = next((item for item in info if re.search(r"更新|集|完结|HD|正片", item)), "")
type_name = " / ".join(filter(None, [self._clean("".join(node.itertext())) for node in doc.xpath("//*[contains(concat(' ', normalize-space(@class), ' '), ' this-desc-tags ')]//span")]))
director = ""
actor = ""
for node in doc.xpath("//*[contains(concat(' ', normalize-space(@class), ' '), ' this-info ')]"):
label = self._first_text(node, ".//strong").replace(" ", "")
value = ",".join(filter(None, [self._clean("".join(item.itertext())) for item in node.xpath(".//a")]))
if "导演" in label:
director = value
if "演员" in label or "主演" in label:
actor = value
content = self._first_text(doc, "//*[@id='height_limit']//*[contains(concat(' ', normalize-space(@class), ' '), ' text ')]")
content = re.sub(r"^\s*简介[:]?\s*", "", content).strip()
line_nodes = doc.xpath("//*[contains(concat(' ', normalize-space(@class), ' '), ' anthology-tab ')]//*[contains(concat(' ', normalize-space(@class), ' '), ' line-btn ')]")
blocks = doc.xpath("//*[contains(concat(' ', normalize-space(@class), ' '), ' anthology-list ')]//*[contains(concat(' ', normalize-space(@class), ' '), ' anthology-list-box ')]")
play_from = []
play_url = []
for index in range(min(len(line_nodes), len(blocks))):
line_texts = line_nodes[index].xpath(".//text()[not(ancestor::span[contains(concat(' ', normalize-space(@class), ' '), ' badge ')])]")
line_name = self._clean("".join(line_texts)) or f"线路{index + 1}"
episodes = []
episode_nodes = blocks[index].xpath(".//a[contains(concat(' ', normalize-space(@class), ' '), ' episode-btn ') and @href]")
for episode_index, episode in enumerate(episode_nodes):
episode_id = self._relative(episode.get("href"))
if episode_id:
episode_name = self._clean("".join(episode.itertext())) or f"{episode_index + 1}"
episodes.append(f"{episode_name}${episode_id}")
if episodes:
play_from.append(line_name)
play_url.append("#".join(episodes))
return {
"vod_id": self._relative(vod_id),
"vod_name": name,
"vod_pic": pic,
"vod_remarks": remarks,
"vod_year": year,
"vod_area": area,
"type_name": type_name,
"vod_director": director,
"vod_actor": actor,
"vod_content": content,
"vod_play_from": "$$$".join(play_from),
"vod_play_url": "$$$".join(play_url),
}
def _decode_player(self, html):
match = re.search(r"var\s+player_aaaa\s*=\s*(\{[\s\S]*?\})\s*</script>", html or "", re.I)
if not match:
return ""
try:
player = json.loads(match.group(1))
value = str(player.get("url") or "")
encrypt = int(player.get("encrypt") or 0)
if encrypt == 1:
value = unquote(value)
elif encrypt == 2:
value = unquote(base64.b64decode(value).decode("utf-8"))
return value if re.match(r"^https?://", value, re.I) else ""
except Exception as error:
self._debug("播放数据解析失败", error_type=type(error).__name__, error=str(error))
return ""
def _player_headers(self):
return {"User-Agent": self.UA, "Referer": self.host + "/"}
@staticmethod
def _preview(value, limit=180):
try:
text = json.dumps(value, ensure_ascii=False, default=str)
except Exception:
text = str(value)
text = re.sub(r"\s+", " ", text).strip()
return text if len(text) <= limit else text[:limit] + "..."
def _debug(self, event, **fields):
try:
payload = {"event": event}
payload.update(fields)
self.log(f"{LOG_TAG} {json.dumps(payload, ensure_ascii=False, default=str)}")
except Exception:
pass
def _summarize_arguments(self, interface, args, kwargs):
if interface == "playerContent":
value = args[1] if len(args) > 1 else kwargs.get("id")
flags = args[2] if len(args) > 2 else kwargs.get("vipFlags")
return {
"flag": self._preview(args[0] if args else kwargs.get("flag"), 60),
"id": self._resource_hint(value),
"vip_flags_count": len(flags) if isinstance(flags, (list, tuple, set)) else 0,
}
if interface == "isVideoFormat":
value = args[0] if args else kwargs.get("url")
return {"url": self._resource_hint(value)}
if interface == "init":
value = args[0] if args else kwargs.get("extend", "")
return {"extend": self._preview(value, 120)}
return self._preview({"args": args, "kwargs": kwargs})
def _resource_hint(self, value):
text = str(value or "").strip()
parsed = urlparse(text)
if parsed.scheme in ("http", "https") and parsed.netloc:
return {"kind": "url", "host": parsed.netloc, "media": bool(self._is_media(text))}
return {"kind": "path", "value": self._preview(text, 120)}
def _safe_url(self, value):
text = str(value or "").strip()
try:
parsed = urlparse(text)
if parsed.scheme not in ("http", "https") or not parsed.netloc:
return self._preview(text, 180)
if self._is_media(text):
return f"{parsed.scheme}://{parsed.netloc}/<media>"
query = "?<redacted>" if parsed.query else ""
return f"{parsed.scheme}://{parsed.netloc}{parsed.path}{query}"
except Exception:
return "<invalid-url>"
def _summarize_result(self, interface, result):
summary = {"result_type": type(result).__name__}
if not isinstance(result, dict):
summary["result"] = self._preview(result)
return summary
items = result.get("list")
classes = result.get("class")
if isinstance(items, list):
summary["list_count"] = len(items)
summary["samples"] = [
str(item.get("vod_name") or "")
for item in items[:3]
if isinstance(item, dict)
]
if isinstance(classes, list):
summary["class_count"] = len(classes)
for key in ("page", "pagecount", "limit", "total", "parse"):
if key in result:
summary[key] = result[key]
if interface == "playerContent":
value = str(result.get("url") or "")
summary["has_url"] = bool(value)
summary["media"] = self._is_media(value)
summary["url_host"] = urlparse(value).netloc if value else ""
return summary
@classmethod
def _page_title(cls, html):
match = re.search(r"<title[^>]*>([\s\S]*?)</title>", html or "", re.I)
if not match:
return ""
return cls._clean(re.sub(r"<[^>]+>", "", match.group(1)))
@staticmethod
def _page_kind(html, title):
text = f"{title}\n{str(html or '')[:50000]}".lower()
markers = (
"just a moment",
"cf-chl-",
"captcha",
"人机验证",
"访问验证",
"安全验证",
)
if any(marker in text for marker in markers):
return "challenge"
return "html" if html else "empty"
def _html(self, html):
try:
if not html:
return None
payload = html.encode("utf-8") if isinstance(html, str) else html
parser = etree.HTMLParser(encoding="utf-8", recover=True)
return etree.HTML(payload, parser=parser)
except Exception as error:
self._debug(
"HTML解析失败",
error_type=type(error).__name__,
error=str(error),
html_length=len(html or ""),
)
return None
@staticmethod
def _clean(value):
return re.sub(r"\s+", " ", str(value or "")).strip()
def _first_text(self, node, xpath):
values = node.xpath(xpath)
if not values:
return ""
value = values[0]
return self._clean("".join(value.itertext()) if hasattr(value, "itertext") else value)
@staticmethod
def _page(value):
try:
return max(1, int(value))
except Exception:
return 1
@staticmethod
def _page_count(html, fallback):
pages = [int(value) for value in re.findall(r"/page/(\d+)\.html", html or "")]
return max([int(fallback)] + pages)
@staticmethod
def _is_media(url):
return re.match(r"^https?://", str(url or ""), re.I) is not None and re.search(r"\.(m3u8|mp4|flv|mkv|ts|mpd)(?:[?#]|$)", str(url), re.I) is not None
@trace_interface
def isVideoFormat(self, url):
return False
@trace_interface
def manualVideoCheck(self):
return False
@trace_interface
def destroy(self):
pass
@trace_interface
def localProxy(self, param):
return None
+1078
View File
@@ -0,0 +1,1078 @@
# 蜜桃视频 类型爬虫
# 网站: https://www.nht966hht.vip:9527
# API: AES-128-CBC (ZeroPadding) + MD5 签名加密
# coding=utf-8
# !/usr/bin/python
import sys
sys.path.append('..')
from base.spider import BaseSpider
import requests
import json
import base64
import hashlib
import time
import re
import os
import string
import random
import threading
from urllib.parse import quote, unquote
from Crypto.Cipher import AES
TIMEOUT = 10
# ============================================================
# 站点配置(多站点备用)
# ============================================================
SITES = [
{'name': 'nht966', 'host': 'https://www.nht966hht.vip:9527'},
{'name': 'httre666', 'host': 'https://www.newhttestre666.cc'},
]
# ============================================================
# 加密常量(从 JS bundle 中提取)
# ============================================================
SIGN_KEY = 'opum3_Loily$SV^6H'
BUNDLE_ID = 'com.ht9.web20.video'
BRAND_ID = 'hongtao'
VERSION = '1.0.0'
PROJECT_ID = '1'
PROXY_TYPE = 'mitao_img'
class Spider(BaseSpider):
# ---- 基础信息 ----
def getName(self):
return "蜜桃视频"
def isVideoFormat(self, url):
return url and ('.mp4' in url or '.m3u8' in url or '.ts' in url)
def manualVideoCheck(self):
return False
# ---- 类变量 ----
filterable = True
searchable = True
host = SITES[0]['host']
session = requests.Session()
headers = {
"User-Agent": "Mozilla/5.0 (Linux; Android 13; SM-G991B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36",
"Accept": "application/json, text/plain, */*",
"Accept-Language": "zh-CN,zh;q=0.9",
"lang": "cn",
"deviceType": "H5-android",
}
# 测速缓存
_speed_cache_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), '.mitao_cache.json')
_speed_cache_ttl = 1800
_lock = threading.Lock()
_speed_test_done = False
# 会话状态
_user_id = ''
_session_id = ''
_device_id = ''
_session_inited = False
# 分类缓存 (从 initH5_1 typeTitleList)
_categories = []
# 视频类型列表 (从 appConfig videoTypeList,用于构建筛选)
_video_type_list = []
# 会话缓存(避免重复 deviceLogin 触发 429 限流)
_session_cache_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), '.mitao_session.json')
_session_cache_ttl = 1800 # 30 分钟
# ============================================================
# 多站点测速
# ============================================================
def _get_cached_site(self):
try:
if os.path.exists(self._speed_cache_file):
with open(self._speed_cache_file, 'r') as f:
data = json.loads(f.read())
age = time.time() - data.get('ts', 0)
host = data.get('host', '')
if age < self._speed_cache_ttl and host:
return host, True
except Exception:
pass
return '', False
def _save_cached_site(self, host):
try:
with open(self._speed_cache_file, 'w') as f:
f.write(json.dumps({'host': host, 'ts': time.time()}))
except Exception:
pass
def _test_site_speed(self, site, results):
try:
start = time.time()
r = requests.get(site['host'], headers=self.headers, timeout=TIMEOUT, verify=False)
elapsed = time.time() - start
if r.status_code == 200:
with self._lock:
results[site['name']] = elapsed
except Exception:
with self._lock:
results[site['name']] = 999
def _select_best_site(self):
if self._speed_test_done:
return
cached_host, valid = self._get_cached_site()
if valid:
self.host = cached_host
self._speed_test_done = True
return
results = {}
threads = []
for s in SITES:
t = threading.Thread(target=self._test_site_speed, args=(s, results))
t.daemon = True
t.start()
threads.append(t)
for t in threads:
t.join(1.5)
valid_sites = [s for s in SITES if results.get(s['name'], 999) < TIMEOUT]
best = min(valid_sites, key=lambda x: results[x['name']])['host'] if valid_sites else SITES[0]['host']
self.host = best
self._speed_test_done = True
self._save_cached_site(best)
# ============================================================
# 会话缓存(持久化到文件,避免重复 init 触发 429 限流)
# ============================================================
def _save_session_cache(self):
"""将会话状态写入缓存文件"""
try:
data = {
'ts': time.time(),
'user_id': self._user_id,
'session_id': self._session_id,
'device_id': self._device_id,
'categories': self._categories,
'video_type_list': self._video_type_list,
}
with open(self._session_cache_file, 'w') as f:
f.write(json.dumps(data, ensure_ascii=False))
except Exception:
pass
def _load_session_cache(self):
"""从缓存文件恢复会话状态,返回 True 表示缓存有效"""
try:
if not os.path.exists(self._session_cache_file):
return False
with open(self._session_cache_file, 'r') as f:
data = json.loads(f.read())
age = time.time() - data.get('ts', 0)
if age >= self._session_cache_ttl:
return False
self._user_id = data.get('user_id', '')
self._session_id = data.get('session_id', '')
self._device_id = data.get('device_id', '')
self._categories = data.get('categories', [])
self._video_type_list = data.get('video_type_list', [])
# 关键字段缺失视为缓存无效, 避免无认证请求被服务器拒绝
if not self._user_id or not self._session_id:
return False
return True
except Exception:
return False
# ============================================================
# AES 加解密(匹配 CryptoJS ZeroPadding
# ============================================================
@staticmethod
def _zero_pad(data, block_size=16):
pad_len = block_size - (len(data) % block_size)
if pad_len == block_size:
return data
return data + b'\x00' * pad_len
@staticmethod
def _zero_unpad(data):
return data.rstrip(b'\x00')
def _gen_key(self, timestamp):
"""生成 AES-128 密钥: timestamp后6位 + signKey前4 + bundleId前6"""
ts = str(timestamp)
return ts[-6:] + SIGN_KEY[:4] + BUNDLE_ID[:6]
def _gen_iv(self):
"""生成 AES-128 IV: bundleId后6 + signKey后4 + deviceId前6"""
return BUNDLE_ID[-6:] + SIGN_KEY[-4:] + self._device_id[:6]
def _aes_encrypt(self, plaintext, key_str, iv_str):
"""AES-128-CBC 加密 (ZeroPadding, 输出 Base64)"""
key = key_str.encode('utf-8')
iv = iv_str.encode('utf-8')
cipher = AES.new(key, AES.MODE_CBC, iv)
data = plaintext.encode('utf-8')
padded = self._zero_pad(data)
encrypted = cipher.encrypt(padded)
return base64.b64encode(encrypted).decode('utf-8')
def _aes_decrypt(self, ciphertext_b64, key_str, iv_str):
"""AES-128-CBC 解密 (ZeroPadding, 输入 Base64)"""
key = key_str.encode('utf-8')
iv = iv_str.encode('utf-8')
cipher = AES.new(key, AES.MODE_CBC, iv)
# 移除空白字符(匹配 JS 端 replace(/\s/g,"")
cleaned = re.sub(r'\s', '', ciphertext_b64)
encrypted = base64.b64decode(cleaned)
decrypted = cipher.decrypt(encrypted)
unpadded = self._zero_unpad(decrypted)
return unpadded.decode('utf-8', errors='replace')
def _generate_sign(self, params, api_path):
"""MD5 签名: 参数值排序拼接 + signKey + API路径 → MD5 大写"""
sorted_keys = sorted(params.keys())
concat = ''
for k in sorted_keys:
concat += str(params[k])
raw = concat + SIGN_KEY + api_path
return hashlib.md5(raw.encode('utf-8')).hexdigest().upper()
# ============================================================
# 客户端 deviceId 生成(匹配 JS: "H5-" + 随机串)
# ============================================================
@staticmethod
def _generate_device_id():
"""生成 H5 设备 ID,格式: H5- + 32位随机小写hex"""
rand = ''.join(random.choices(string.ascii_lowercase + string.digits, k=32))
return 'H5-' + rand
# ============================================================
# 通用请求 params (Ne)
# ============================================================
def _common_params(self):
# channelId2 = window.location.host (含端口,如 www.nht950hht.vip:9527)
hostname = self.host.replace('https://', '').replace('http://', '')
return {
'timezone': 'Asia/Karachi',
'version': VERSION,
'channelId': 67, # 必须是整数! JS: __xyz_cid_ = 67, JSON.stringify 后为 67 而非 "67"
'channelId2': hostname,
'brandId': BRAND_ID,
}
# ============================================================
# API 请求(支持加密/明文双模式)
# ============================================================
def _api_request(self, endpoint, params=None, skip_encrypt=False, _t=None):
"""
发送 AES 加密 API 请求
endpoint: e.g. '/ht/content/homeH5'
params: 请求参数 dict
skip_encrypt: True = 发送明文 JSON (调试用, 部分 init 端点不需加密)
_t: 可选, 复用外部时间戳 (initH5_1/2 共用)
"""
if params is None:
params = {}
# 毫秒时间戳 (支持外部传入, 匹配浏览器 initH5_1/2 共用 t 的行为)
timestamp = str(_t) if _t else str(int(time.time() * 1000))
key_str = self._gen_key(timestamp)
iv_str = self._gen_iv()
# 构建完整参数: Ne() + {t} + 业务参数
full_params = self._common_params()
full_params['t'] = timestamp
full_params.update(params)
# 签名: ze(params, endpoint) = MD5(sorted_values + signKey + path).upper()
full_params['sign'] = self._generate_sign(full_params, endpoint)
api_url = self.host + endpoint
headers = dict(self.headers)
headers['t'] = timestamp
if self._user_id:
headers['userId'] = self._user_id
if self._session_id:
headers['sessionId'] = self._session_id
# 必填请求头 (JS Ve 拦截器会设置这些)
headers['deviceId'] = self._device_id or ''
headers['bundleId'] = BUNDLE_ID
# 明文或加密
if skip_encrypt:
body = json.dumps(full_params, ensure_ascii=False, separators=(',', ':'))
headers['Content-Type'] = 'application/json'
headers['encrypt'] = 'false'
else:
plain = json.dumps(full_params, ensure_ascii=False, separators=(',', ':'))
body = self._aes_encrypt(plain, key_str, iv_str)
headers['Content-Type'] = 'text/plain'
headers['encrypt'] = 'true'
try:
r = self.session.post(api_url, data=body,
headers=headers, timeout=TIMEOUT, verify=False)
resp = r.json()
# code=10000 表示成功,解密响应 data(仅加密请求需解密)
if resp.get('code') == 10000 and isinstance(resp.get('data'), str) and resp['data']:
try:
decrypted = self._aes_decrypt(resp['data'], key_str, iv_str)
resp['data'] = json.loads(decrypted)
except Exception:
pass
return resp
except requests.exceptions.Timeout:
return None
except requests.exceptions.ConnectionError:
return None
except json.JSONDecodeError:
return None
except Exception:
return None
# ============================================================
# 会话初始化(匹配 JS 端流程)
# ============================================================
def _ensure_session(self):
"""
初始化会话: 优先从文件缓存恢复 → 否则 appConfig → 生成 deviceId → initH5_1 → initH5_2 → deviceLogin
真实浏览器流程: appConfig 最先调,initH5_1/2 共用同一个 t 时间戳
缓存策略: 避免 T3 新建实例时重复 deviceLogin 触发 429 限流
"""
if self._session_inited:
return
# 优先从缓存恢复(跳过整个 init 流程,避免 429)
if self._load_session_cache():
self._session_inited = True
# 旧缓存可能没有 video_type_list,补一次 appConfig 请求
if not self._video_type_list:
appcfg = self._api_request('/ht/users/appConfig')
if appcfg and appcfg.get('code') == 10000:
ac_data = appcfg.get('data', {})
if isinstance(ac_data, dict) and ac_data.get('appConfig'):
ac_cfg = ac_data['appConfig']
if isinstance(ac_cfg, dict) and ac_cfg.get('videoTypeList'):
self._video_type_list = ac_cfg['videoTypeList']
return
# 0. 生成 deviceId (JS 端 $.getDeviceId() 在页面加载时就执行)
if not self._device_id:
self._device_id = self._generate_device_id()
# 0.5 appConfig — 真实浏览器第一个调的就是它,获取 videoTypeList 供筛选
appcfg = self._api_request('/ht/users/appConfig')
if appcfg and appcfg.get('code') == 10000:
ac_data = appcfg.get('data', {})
if isinstance(ac_data, dict) and ac_data.get('appConfig'):
ac_cfg = ac_data['appConfig']
if isinstance(ac_cfg, dict) and ac_cfg.get('videoTypeList'):
self._video_type_list = ac_cfg['videoTypeList']
# 1. initH5_1 + initH5_2 共用一个 t (匹配浏览器行为)
shared_t = int(time.time() * 1000)
resp1 = self._api_request('/ht/users/initH5_1', _t=shared_t)
if resp1 and resp1.get('code') == 10000:
data = resp1.get('data', {})
if data.get('deviceId'):
self._device_id = data['deviceId']
# 保存分类列表供 homeContent 使用
if data.get('typeTitleList'):
self._categories = data['typeTitleList']
# 2. initH5_2 (复用 shared_t)
self._api_request('/ht/users/initH5_2', _t=shared_t)
# 3. deviceLogin → 获取 userId / sessionId
resp = self._api_request('/ht/users/deviceLogin', {
'bundleId': BUNDLE_ID,
'brandId': BRAND_ID,
'projectId': PROJECT_ID,
})
if resp and resp.get('code') == 10000:
data = resp.get('data', {})
self._user_id = data.get('userId', '')
self._session_id = data.get('sessionId', '')
self._session_inited = True
self._save_session_cache()
# ============================================================
# 图片代理
# ============================================================
def get_proxy_image_url(self, img_url):
if not img_url:
return ''
base_proxy = self.getProxyUrl()
if not base_proxy:
base_proxy = 'http://127.0.0.1:9980/proxy?do=py'
return base_proxy + '&type=' + PROXY_TYPE + '&url=' + quote(img_url, safe='')
def _fmt_duration(self, seconds):
try:
s = int(seconds or 0)
except (TypeError, ValueError):
return ''
if s <= 0:
return ''
m, s = divmod(s, 60)
return f"{m}:{s:02d}"
# ============================================================
# 初始化
# ============================================================
def init(self, extend=""):
cached_host, valid = self._get_cached_site()
if valid:
self.host = cached_host
self._speed_test_done = True
# ============================================================
# 首页
# ============================================================
# T3 首页统一入口: 同时返回分类列表 + 首页视频数据
# ============================================================
_CATEGORY_BLACKLIST = {'成人游戏', '漫画', '小说', '蜜穴女友', '一键脱衣', '春药商城', '同城交友', '吃瓜', '成人漫画'}
def homeContent(self, filter):
self._select_best_site()
self._ensure_session()
classes = []
filters = {}
# 动态加载真实分类(来自 initH5_1 typeTitleList),过滤掉不需要的
for cat in self._categories:
cid = str(cat.get('contentId', ''))
title = cat.get('title', '')
if not cid or not title or title in self._CATEGORY_BLACKLIST:
continue
classes.append({'type_id': cid, 'type_name': title})
# ---- 构建该分类的筛选器 ----
cat_filters = []
# 1. 二级分类 (videoTypeList 中 typePid == contentId 的子项)
sub_cats = [v for v in self._video_type_list if str(v.get('typePid', '')) == cid]
if sub_cats:
sub_values = [{'n': '全部', 'v': ''}]
for sc in sub_cats:
sc_id = str(sc.get('typeId', ''))
sc_name = sc.get('typeName', '')
if sc_id and sc_name:
sub_values.append({'n': sc_name, 'v': sc_id})
if len(sub_values) > 1:
cat_filters.append({'key': 'label', 'name': '分类', 'value': sub_values})
# 2. 标签 (尝试从 videoTypeList 中匹配该 contentId 对应一级类型的 tags)
# 一级类型 typePid==0 且 typeId 可能等于 contentId
first_level = [v for v in self._video_type_list
if str(v.get('typePid', '')) == '0' and str(v.get('typeId', '')) == cid]
if first_level:
tags_str = first_level[0].get('tags', '')
if tags_str:
tag_list = [t.strip() for t in tags_str.split(',') if t.strip()]
if tag_list:
tag_values = [{'n': '全部', 'v': ''}]
for t in tag_list:
tag_values.append({'n': t, 'v': t})
cat_filters.append({'key': 'tag', 'name': '标签', 'value': tag_values})
# 3. 排序 (JS sortList: ["最近更新","最多播放","最多收藏"] → 索引 0/1/2)
cat_filters.append({'key': 'sort', 'name': '排序', 'value': [
{'n': '最近更新', 'v': '0'},
{'n': '最多播放', 'v': '1'},
{'n': '最多收藏', 'v': '2'},
]})
if cat_filters:
filters[cid] = cat_filters
# ---- 添加特殊分类: 女优 (actor) ----
classes.append({'type_id': 'actor', 'type_name': '女优'})
# 动态生成筛选值 (API 只接受单值精确匹配)
_actors_filters = []
# 身高: 150-164cm
_actors_filters.append({'key': 'height', 'name': '身高', 'value': [
{'n': '身高', 'v': ''},
] + [{'n': f'{h}cm', 'v': str(h)} for h in range(150, 165)]})
# 罩杯: A-G
_actors_filters.append({'key': 'cup', 'name': '罩杯', 'value': [
{'n': '罩杯', 'v': ''},
] + [{'n': f'{c}罩杯', 'v': c} for c in 'ABCDEFG']})
# 年龄: 1976-2002 (出生年份)
_actors_filters.append({'key': 'birthday', 'name': '年龄', 'value': [
{'n': '年龄', 'v': ''},
] + [{'n': f'{y}', 'v': str(y)} for y in range(2002, 1975, -1)]})
# 出道: 2001-2025
_actors_filters.append({'key': 'debut', 'name': '出道', 'value': [
{'n': '出道', 'v': ''},
] + [{'n': f'{y}', 'v': str(y)} for y in range(2025, 2000, -1)]})
filters['actor'] = _actors_filters
# ---- 添加特殊分类: 专题 (topic) ----
classes.append({'type_id': 'topic', 'type_name': '专题'})
# 同时返回首页推荐视频列表 (兼容 T3 统一返回模式)
home_videos = self.categoryContent('home', 1, '', {})
return {
'class': classes,
'filters': filters,
'type': '影视',
'list': home_videos.get('list', []),
'page': home_videos.get('page', 1),
'pagecount': home_videos.get('pagecount', 1),
'limit': home_videos.get('limit', 0),
'total': home_videos.get('total', 0),
}
def homeVideoContent(self, tid, pg, filter, extend):
return self.categoryContent(tid or 'home', pg, filter, extend)
# ============================================================
# 分类列表
# ============================================================
def categoryContent(self, tid, pg, filter, extend):
tid = str(tid)
pg = int(pg)
self._select_best_site()
self._ensure_session()
vod_list = []
# ---- @ folder 模式: 点击文件夹 → 获取视频列表 ----
if '@' in tid:
real_tid = tid.replace('@', '')
if real_tid.startswith('actor_'):
actor_id = real_tid[len('actor_'):]
# 先查演员名
detail_resp = self._api_request('/ht/content/queryActorDetail', {
'actorId': actor_id,
})
actor_name = ''
if detail_resp and detail_resp.get('code') == 10000:
detail_data = detail_resp.get('data', {})
actor_info = (detail_data.get('actorDetail') or detail_data or {})
actor_name = (actor_info.get('actorName') or actor_info.get('actor_name') or '')
# 用演员名搜索视频
if actor_name:
resp = self._api_request('/ht/content/search', {
'keywords': actor_name,
'pageNo': str(pg - 1),
'pageSize': '20',
})
else:
# 降级: 用 actorId 尝试 queryTypeVideosH5
resp = self._api_request('/ht/content/queryTypeVideosH5', {
'actorId': actor_id,
'pageNo': str(pg - 1),
'pageSize': '20',
'type': '1',
})
if not resp or resp.get('code') != 10000:
return {'list': [], 'page': pg, 'pagecount': 1, 'limit': 0, 'total': 0}
data = resp.get('data', {})
vod_list = self._extract_videos_from_data(data)
total_page = int(data.get('totalPage') or data.get('total_page') or 1)
return {'list': vod_list, 'page': pg, 'pagecount': max(total_page, 1),
'limit': len(vod_list), 'total': max(total_page, 1) * 20}
elif real_tid.startswith('topic_'):
topic_id = real_tid[len('topic_'):]
resp = self._api_request('/ht/content/queryOriTopicVideos', {
'topicId': topic_id,
'pageNo': str(pg - 1),
'pageSize': '20',
})
if not resp or resp.get('code') != 10000:
return {'list': [], 'page': pg, 'pagecount': 1, 'limit': 0, 'total': 0}
data = resp.get('data', {})
vod_list = self._extract_videos_from_data(data)
total_page = int(data.get('totalPage') or data.get('total_page') or 1)
return {'list': vod_list, 'page': pg, 'pagecount': max(total_page, 1),
'limit': len(vod_list), 'total': max(total_page, 1) * 20}
else:
return {'list': [], 'page': pg, 'pagecount': 1, 'limit': 0, 'total': 0}
# ---- 女优列表 (folder 模式) ----
if tid == 'actor':
# 构建 API 参数, 映射 extend 中的筛选 key → API 参数名
api_params = {
'pageNo': str(pg - 1),
'pageSize': '20',
}
if isinstance(extend, dict):
_actor_filter_map = {
'height': 'actorHeight',
'cup': 'cupSize',
'birthday': 'actorBirthday',
'debut': 'actorDebut',
}
for ek, ak in _actor_filter_map.items():
val = extend.get(ek, '')
if val:
api_params[ak] = val
resp = self._api_request('/ht/content/getActors', api_params)
if not resp or resp.get('code') != 10000:
return {'list': [], 'page': pg, 'pagecount': 1, 'limit': 0, 'total': 0}
data = resp.get('data', {})
vod_list = self._parse_actor_list(data)
total_page = int(data.get('totalPage') or 1)
return {'list': vod_list, 'page': pg, 'pagecount': total_page,
'limit': len(vod_list), 'total': total_page * 20}
# ---- 专题列表 (folder 模式) ----
if tid == 'topic':
resp = self._api_request('/ht/content/getOriTopicList', {
'pageNo': str(pg - 1),
'pageSize': '20',
})
if not resp or resp.get('code') != 10000:
return {'list': [], 'page': pg, 'pagecount': 1, 'limit': 0, 'total': 0}
data = resp.get('data', {})
vod_list = self._parse_topic_list(data)
return {'list': vod_list, 'page': pg, 'pagecount': 50, 'limit': len(vod_list),
'total': len(vod_list) * 50}
if tid in ('home', 'new', 'hot'):
# 首页/最新/热门 → 使用 queryTypeVideosH5
# homeH5 端点始终返回 20001,改用已验证通的 queryTypeVideosH5
sort_map = {'home': '1', 'new': '1', 'hot': '2'}
resp = self._api_request('/ht/content/queryTypeVideosH5', {
'pageNo': str(pg - 1),
'pageSize': '20',
'sort': sort_map.get(tid, '1'),
'type': '1',
})
if not resp or resp.get('code') != 10000:
return {'list': [], 'page': pg, 'pagecount': 1, 'limit': 0, 'total': 0}
data = resp.get('data', {})
items = (data.get('typeVideoList') or data.get('list') or data.get('data') or data.get('videoList') or [])
if isinstance(items, list):
for v in items:
parsed = self._parse_video(v)
if parsed:
vod_list.append(parsed)
else:
# 数值分类 (contentId) → queryTypeVideosH5
# T3 通过 extend dict 传递筛选和排序参数
# extend: {'label': '子分类id', 'tag': '标签名', 'sort': '排序值'}
api_params = {
'pageNo': str(pg - 1),
'pageSize': '20',
'typeId': tid, # 按分类过滤(queryTypeVideosH5 → typeId
'type': '1', # 媒体类型 1=视频(home 分支也带,缺少会导致 API 返回默认列表)
}
if isinstance(extend, dict):
for key in ('label', 'tag', 'sort'):
val = extend.get(key, '')
if val:
api_params[key] = val
resp = self._api_request('/ht/content/queryTypeVideosH5', api_params)
if not resp or resp.get('code') != 10000:
return {'list': [], 'page': pg, 'pagecount': 1, 'limit': 0, 'total': 0}
data = resp.get('data', {})
items = (data.get('typeVideoList') or data.get('list') or data.get('data') or data.get('videoList') or [])
if isinstance(items, list):
for v in items:
parsed = self._parse_video(v)
if parsed:
vod_list.append(parsed)
# 使用 API 返回的真实 totalPagepageSize 固定 20
total_page = int(data.get('totalPage') or 1)
return {
'list': vod_list,
'page': pg,
'pagecount': total_page,
'limit': len(vod_list),
'total': total_page * 20,
}
# ============================================================
# 辅助: 从 data 提取视频列表
# ============================================================
def _extract_videos_from_data(self, data):
"""从响应 data 中提取视频列表(多种格式兼容)"""
# data 可能是 dict 或 list
if isinstance(data, list):
items = data
elif not isinstance(data, dict):
return []
else:
items = (data.get('videoList') or data.get('list') or data.get('data')
or data.get('videos') or data.get('typeVideoList')
or data.get('topicVideoIdList') or data.get('searchList')
or data.get('contentList') or data.get('records')
or data.get('pageData') or [])
if not isinstance(items, list):
return []
return [p for v in items if (p := self._parse_video(v))]
# ============================================================
# 辅助: 从 dict item 中尝试获取字段值(多种命名兼容)
# ============================================================
@staticmethod
def _try_get(item, *keys):
"""依次尝试多个字段名, 返回第一个非空值"""
for k in keys:
v = item.get(k)
if v is not None and v != '':
return v
return ''
# ============================================================
# 解析女优列表(getActors API 响应 → folder list, vod_id + '@'
# ============================================================
def _parse_actor_list(self, data):
"""解析 getActors 返回的演员列表,生成带 @ 后缀的 folder 条目"""
# data 可能是 dict 或 list
if isinstance(data, list):
items = data
elif not isinstance(data, dict):
return []
else:
items = (data.get('actorList') or data.get('actors') or data.get('list')
or data.get('data') or [])
if not isinstance(items, list):
return []
results = []
seen = set()
for item in items:
if not isinstance(item, dict):
continue
actor_id = str(self._try_get(item,
'actorId', 'contentId', 'id', 'artId', 'actor_id', 'userId'))
actor_name = str(self._try_get(item,
'actorName', 'name', 'title', 'artName', 'actor_name', 'actor'))
actor_img = str(self._try_get(item,
'actorPic', 'actorImg', 'img', 'avatar', 'cover',
'imageUrl', 'headImg', 'head', 'photo', 'image', 'pic', 'actor_img'))
actor_count = str(self._try_get(item,
'videoCount', 'contentCount', 'count', 'totalCount',
'total', 'video_count'))
if not actor_id:
continue
if actor_id in seen:
continue
seen.add(actor_id)
# 兜底: 无图时用 favicon 保证 item 可见
if not actor_img:
actor_img = self.host + '/favicon.ico'
remarks = f'{actor_count}' if actor_count else ''
results.append({
'vod_id': 'actor_' + actor_id + '@',
'vod_name': actor_name or ('演员' + actor_id),
'vod_pic': self.get_proxy_image_url(actor_img),
'vod_tag': 'folder',
'vod_remarks': remarks,
})
return results
# ============================================================
# 解析专题列表(getOriTopicList API 响应 → folder list, vod_id + '@'
# ============================================================
def _parse_topic_list(self, data):
"""解析 getOriTopicList 返回的专题列表,生成带 @ 后缀的 folder 条目"""
# data 可能是 dict 或 list
if isinstance(data, list):
items = data
elif not isinstance(data, dict):
return []
else:
items = (data.get('topicList') or data.get('oriTopicList') or data.get('list')
or data.get('data') or data.get('topics') or [])
if not isinstance(items, list):
return []
results = []
seen = set()
for item in items:
if not isinstance(item, dict):
continue
topic_id = str(self._try_get(item,
'topicId', 'id', 'contentId', 'oriTopicId', 'topic_id'))
topic_name = str(self._try_get(item,
'topicName', 'name', 'title', 'oriTopicName', 'topic_name', 'topic'))
topic_img = str(self._try_get(item,
'topicPic', 'topicImg', 'img', 'cover', 'imageUrl', 'pic',
'thumb', 'image', 'topic_img', 'oriTopicImg'))
topic_count = str(self._try_get(item,
'videoCount', 'count', 'contentCount', 'totalCount',
'total', 'video_count'))
if not topic_id:
continue
if topic_id in seen:
continue
seen.add(topic_id)
# 兜底: 无图时用 favicon 保证 item 可见
if not topic_img:
topic_img = self.host + '/favicon.ico'
remarks = f'{topic_count}' if topic_count else ''
results.append({
'vod_id': 'topic_' + topic_id + '@',
'vod_name': topic_name or ('专题' + topic_id),
'vod_pic': self.get_proxy_image_url(topic_img),
'vod_tag': 'folder',
'vod_remarks': remarks,
})
return results
# ============================================================
# 解析视频条目
# ============================================================
def _parse_video(self, item):
# 过滤广告 (contentType=3, 带 jumpScheme 跳转链接)
if item.get('contentType') != 1:
return None
vid = str(item.get('contentId') or item.get('id') or item.get('videoId') or '')
title = item.get('title') or item.get('name') or item.get('videoTitle') or ''
pic = item.get('img') or item.get('cover') or item.get('coverUrl') or item.get('pic') or item.get('imageUrl') or ''
remarks = item.get('duration') or item.get('playCount') or item.get('remark') or ''
# 时长格式化
if remarks and str(remarks).isdigit():
remarks = self._fmt_duration(remarks)
return {
'vod_id': vid,
'vod_name': title,
'vod_pic': self.get_proxy_image_url(pic) if pic else '',
'vod_remarks': str(remarks) if remarks else '',
}
# ============================================================
# 详情页
# ============================================================
def detailContent(self, ids):
did = ids[0] if isinstance(ids, list) else ids
self._select_best_site()
self._ensure_session()
resp = self._api_request('/ht/content/detail', {'contentId': str(did)})
if not resp or resp.get('code') != 10000:
return {'list': []}
detail = resp.get('data', {})
if not detail:
return {'list': []}
# 兼容多种字段名
title = (detail.get('title') or detail.get('name') or
detail.get('videoTitle') or '未知标题')
pic = (detail.get('cover') or detail.get('coverUrl') or
detail.get('img') or detail.get('imageUrl') or '')
desc = detail.get('description') or detail.get('desc') or detail.get('intro') or ''
duration = detail.get('duration', 0)
actor = detail.get('actor') or detail.get('actors') or ''
# 播放地址: videoUrl / playUrl / m3u8
play_url = (detail.get('videoUrl') or detail.get('playUrl') or
detail.get('url') or detail.get('m3u8Url') or
detail.get('sl') or '')
vod_play_url = '播放$' + str(did)
if play_url:
vod_play_url = '播放$' + play_url
return {'list': [{
'vod_id': str(did),
'vod_name': title,
'vod_pic': self.get_proxy_image_url(pic) if pic else '',
'vod_actor': str(actor) if actor else '',
'vod_director': '',
'vod_content': desc,
'vod_year': '',
'vod_area': '',
'vod_remarks': self._fmt_duration(duration),
'vod_play_from': '蜜桃视频',
'vod_play_url': vod_play_url,
'type': 'video',
}]}
# ============================================================
# 搜索
# ============================================================
def searchContent(self, key, quick, pg=1):
self._select_best_site()
self._ensure_session()
pg = int(pg)
resp = self._api_request('/ht/content/search', {
'keywords': key,
'pageNo': pg - 1,
'pageSize': 20,
})
if not resp or resp.get('code') != 10000:
return {'list': [], 'page': pg, 'pagecount': 1, 'limit': 0, 'total': 0}
data = resp.get('data', {})
# 兼容多种 data 形态:list / dict
if isinstance(data, list):
items = data
total_n = len(data)
elif isinstance(data, dict):
items = (data.get('searchList')
or data.get('list')
or data.get('data')
or data.get('videoList')
or data.get('records')
or data.get('resultList')
or data.get('content')
or [])
total_n = data.get('total') or data.get('totalCount') or data.get('totalNum') or 0
else:
return {'list': [], 'page': pg, 'pagecount': 1, 'limit': 0, 'total': 0}
if not isinstance(items, list):
return {'list': [], 'page': pg, 'pagecount': 1, 'limit': 0, 'total': 0}
vod_list = [p for v in items if (p := self._parse_video(v))]
total_page = int(data.get('totalPage') or 1) if isinstance(data, dict) else max(1, len(vod_list) // 20)
return {
'list': vod_list,
'page': pg,
'pagecount': total_page,
'limit': len(vod_list),
'total': total_page * 20,
}
# ============================================================
# 播放解析
# ============================================================
def playerContent(self, flag, id, vipFlags=None):
url = id.split('$')[-1]
# 如果已经是完整 URL
if url.startswith('http'):
return {
'parse': 0,
'url': url,
'jx': 0,
'header': {
'User-Agent': self.headers['User-Agent'],
'Referer': self.host + '/',
},
}
# 否则作为 videoId 重新获取
self._select_best_site()
self._ensure_session()
resp = self._api_request('/ht/content/detail', {'contentId': url})
if not resp or resp.get('code') != 10000:
return {'parse': 0, 'url': '', 'jx': 0}
detail = resp.get('data', {})
play_url = (detail.get('videoUrl') or detail.get('playUrl') or
detail.get('url') or detail.get('m3u8Url') or
detail.get('sl') or '')
return {
'parse': 0,
'url': play_url,
'jx': 0,
'header': {
'User-Agent': self.headers['User-Agent'],
'Referer': self.host + '/',
},
}
# ============================================================
# 图片代理
# ============================================================
def localProxy(self, params):
try:
if params.get('type') != PROXY_TYPE:
return [404, 'text/plain', 'not found']
img_url = params.get('url', '')
if not img_url:
return [400, 'text/plain', 'missing url']
img_url = unquote(img_url)
r = requests.get(img_url, headers={
'User-Agent': self.headers['User-Agent'],
'Referer': self.host + '/',
}, timeout=TIMEOUT, verify=False)
if r.status_code != 200:
return [404, 'text/plain', 'image not found']
data = r.content
# 尝试 XOR 0x88 解密 (蜜桃图片防盗链, _xfile.jpg 全部 XOR)
if data[:2] != b'\xff\xd8' and data[:4] != b'\x89PNG' \
and not (data[:4] == b'RIFF' and data[8:12] == b'WEBP'):
decoded = bytes(b ^ 0x88 for b in data)
if decoded[:2] == b'\xff\xd8' or decoded[:4] == b'\x89PNG' \
or (decoded[:4] == b'RIFF' and decoded[8:12] == b'WEBP'):
data = decoded
if data[:2] == b'\xff\xd8':
return [200, 'image/jpeg', data, {'Content-Length': str(len(data))}]
elif data[:4] == b'\x89PNG':
return [200, 'image/png', data, {'Content-Length': str(len(data))}]
elif data[:4] == b'RIFF' and data[8:12] == b'WEBP':
return [200, 'image/webp', data, {'Content-Length': str(len(data))}]
else:
mime = r.headers.get('Content-Type', 'image/jpeg')
if mime.startswith('image/'):
return [200, mime, data, {'Content-Length': str(len(data))}]
return [404, 'text/plain', 'invalid image format']
except Exception:
return [500, 'text/plain', 'proxy error']
+464
View File
@@ -0,0 +1,464 @@
# -*- coding: utf-8 -*-
import sys
import hashlib
import time
import requests
import re
import json
sys.path.append('..')
from base.spider import Spider
class Spider(Spider):
def getName(self):
return "Aidianying"
def init(self, extend):
self.home_url = 'https://m.sdzhgt.com/'
self.ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
self.error_url = "https://sf1-cdn-tos.huoshanstatic.com/obj/media-fe/xgplayer_doc_video/mp4/xgplayer-demo-720p.mp4"
def getDependence(self):
return []
def isVideoFormat(self, url):
pass
def manualVideoCheck(self):
pass
def homeContent(self, filter):
return {
'class': [{'type_id': '1', 'type_name': '电影'},
{'type_id': '2', 'type_name': '电视剧'},
{'type_id': '3', 'type_name': '综艺'},
{'type_id': '4', 'type_name': '动漫'}],
'filters': {
'1': [
{'key': 'type',
'name': '类型',
'value': [{'n': '全部', 'v': ''},
{'n': '喜剧', 'v': '/type/22'},
{'n': '动作', 'v': '/type/23'},
{'n': '科幻', 'v': '/type/30'},
{'n': '爱情', 'v': '/type/26'},
{'n': '悬疑', 'v': '/type/27'},
{'n': '奇幻', 'v': '/type/87'},
{'n': '剧情', 'v': '/type/37'},
{'n': '恐怖', 'v': '/type/36'},
{'n': '犯罪', 'v': '/type/35'},
{'n': '动画', 'v': '/type/33'},
{'n': '惊悚', 'v': '/type/34'},
{'n': '战争', 'v': '/type/25'},
{'n': '冒险', 'v': '/type/31'},
{'n': '灾难', 'v': '/type/81'},
{'n': '伦理', 'v': '/type/83'},
{'n': '其他', 'v': '/type/43'}]},
{'key': 'area',
'name': '地区',
'value': [{'n': '全部', 'v': ''},
{'n': '中国大陆', 'v': '/area/中国大陆'},
{'n': '中国香港', 'v': '/area/中国香港'},
{'n': '中国台湾', 'v': '/area/中国台湾'},
{'n': '美国', 'v': '/area/美国'},
{'n': '日本', 'v': '/area/日本'},
{'n': '韩国', 'v': '/area/韩国'},
{'n': '印度', 'v': '/area/印度'},
{'n': '泰国', 'v': '/area/泰国'},
{'n': '其他', 'v': '/area/其他'}]},
{'key': 'year',
'name': '年份',
'value': [{'n': '全部', 'v': ''},
{'n': '2026', 'v': '/year/2026'},
{'n': '2025', 'v': '/year/2025'},
{'n': '2024', 'v': '/year/2024'},
{'n': '2023', 'v': '/year/2023'},
{'n': '2022', 'v': '/year/2022'},
{'n': '2021', 'v': '/year/2021'},
{'n': '2020', 'v': '/year/2020'},
{'n': '2019', 'v': '/year/2019'},
{'n': '2018', 'v': '/year/2018'},
{'n': '2017', 'v': '/year/2017'},
{'n': '2016', 'v': '/year/2016'},
{'n': '2015', 'v': '/year/2015'},
{'n': '2014', 'v': '/year/2014'},
{'n': '2013', 'v': '/year/2013'},
{'n': '2012', 'v': '/year/2012'},
{'n': '2011', 'v': '/year/2011'},
{'n': '2010', 'v': '/year/2010'},
{'n': '2009~2000', 'v': '/year/2009~2000'}]},
{'key': 'lang',
'name': '语言',
'value': [{'n': '全部', 'v': ''},
{'n': '国语', 'v': '/lang/国语'},
{'n': '英语', 'v': '/lang/英语'},
{'n': '粤语', 'v': '/lang/粤语'},
{'n': '韩语', 'v': '/lang/韩语'},
{'n': '日语', 'v': '/lang/日语'},
{'n': '其他', 'v': '/lang/其他'}]},
{'key': 'by',
'name': '排序',
'value': [{'n': '上映时间', 'v': '/sortType/1/sortOrder/0'},
{'n': '人气高低', 'v': '/sortType/3/sortOrder/0'},
{'n': '评分高低', 'v': '/sortType/4/sortOrder/0'}]}
],
'2': [
{'key': 'type',
'name': '类型',
'value': [{'n': '全部', 'v': ''},
{'n': '国产剧', 'v': '/type/14'},
{'n': '欧美剧', 'v': '/type/15'},
{'n': '港台剧', 'v': '/type/16'},
{'n': '日韩剧', 'v': '/type/62'},
{'n': '其他剧', 'v': '/type/68'}]},
{'key': 'class',
'name': '剧情',
'value': [{'n': '全部', 'v': ''},
{'n': '古装', 'v': '/class/古装'},
{'n': '战争', 'v': '/class/战争'},
{'n': '喜剧', 'v': '/class/喜剧'},
{'n': '家庭', 'v': '/class/家庭'},
{'n': '犯罪', 'v': '/class/犯罪'},
{'n': '动作', 'v': '/class/动作'},
{'n': '奇幻', 'v': '/class/奇幻'},
{'n': '剧情', 'v': '/class/剧情'},
{'n': '历史', 'v': '/class/历史'},
{'n': '短片', 'v': '/class/短片'}]},
{'key': 'area',
'name': '地区',
'value': [{'n': '全部', 'v': ''},
{'n': '中国大陆', 'v': '/area/中国大陆'},
{'n': '中国香港', 'v': '/area/中国香港'},
{'n': '中国台湾', 'v': '/area/中国台湾'},
{'n': '日本', 'v': '/area/日本'},
{'n': '韩国', 'v': '/area/韩国'},
{'n': '美国', 'v': '/area/美国'},
{'n': '泰国', 'v': '/area/泰国'},
{'n': '其他', 'v': '/area/其他'}]},
{'key': 'year',
'name': '时间',
'value': [{'n': '全部', 'v': ''},
{'n': '2026', 'v': '/year/2026'},
{'n': '2025', 'v': '/year/2025'},
{'n': '2024', 'v': '/year/2024'},
{'n': '2023', 'v': '/year/2023'},
{'n': '2022', 'v': '/year/2022'},
{'n': '2021', 'v': '/year/2021'},
{'n': '2020', 'v': '/year/2020'},
{'n': '2019', 'v': '/year/2019'},
{'n': '2018', 'v': '/year/2018'},
{'n': '2017', 'v': '/year/2017'},
{'n': '2016', 'v': '/year/2016'},
{'n': '2015', 'v': '/year/2015'},
{'n': '2014', 'v': '/year/2014'},
{'n': '2013', 'v': '/year/2013'},
{'n': '2012', 'v': '/year/2012'},
{'n': '2011', 'v': '/year/2011'},
{'n': '2010', 'v': '/year/2010'}]},
{'key': 'lang',
'name': '语言',
'value': [{'n': '全部', 'v': ''},
{'n': '普通话', 'v': '/lang/普通话'},
{'n': '英语', 'v': '/lang/英语'},
{'n': '粤语', 'v': '/lang/粤语'},
{'n': '韩语', 'v': '/lang/韩语'},
{'n': '日语', 'v': '/lang/日语'},
{'n': '泰语', 'v': '/lang/泰语'},
{'n': '其他', 'v': '/lang/其他'}, ]},
{'key': 'by',
'name': '排序',
'value': [{'n': '最近更新', 'v': '/sortType/1/sortOrder/0'},
{'n': '添加时间', 'v': '/sortType/2/sortOrder/0'},
{'n': '人气高低', 'v': '/sortType/3/sortOrder/0'},
{'n': '评分高低', 'v': '/sortType/4/sortOrder/0'}]}
],
'3': [
{'key': 'type',
'name': '类型',
'value': [{'n': '全部', 'v': ''},
{'n': '国产综艺', 'v': '/type/69'},
{'n': '港台综艺', 'v': '/type/70'},
{'n': '日韩综艺', 'v': '/type/72'},
{'n': '欧美综艺', 'v': '/type/73'}]},
{'key': 'class',
'name': '剧情',
'value': [{'n': '全部', 'v': ''},
{'n': '真人秀', 'v': '/class/真人秀'},
{'n': '音乐', 'v': '/class/音乐'},
{'n': '脱口秀', 'v': '/class/脱口秀'}]},
{'key': 'area',
'name': '地区',
'value': [{'n': '全部', 'v': ''},
{'n': '中国大陆', 'v': '/area/中国大陆'},
{'n': '中国香港', 'v': '/area/中国香港'},
{'n': '中国台湾', 'v': '/area/中国台湾'},
{'n': '日本', 'v': '/area/日本'},
{'n': '韩国', 'v': '/area/韩国'},
{'n': '美国', 'v': '/area/美国'},
{'n': '其他', 'v': '/area/其他'}]},
{'key': 'year',
'name': '时间',
'value': [{'n': '全部', 'v': ''},
{'n': '2024', 'v': '/year/2024'},
{'n': '2023', 'v': '/year/2023'},
{'n': '2022', 'v': '/year/2022'},
{'n': '2021', 'v': '/year/2021'},
{'n': '2020', 'v': '/year/2020'}]},
{'key': 'lang',
'name': '语言',
'value': [{'n': '全部', 'v': ''},
{'n': '国语', 'v': '/lang/国语'},
{'n': '英语', 'v': '/lang/英语'},
{'n': '粤语', 'v': '/lang/粤语'},
{'n': '韩语', 'v': '/lang/韩语'},
{'n': '日语', 'v': '/lang/日语'},
{'n': '其他', 'v': '/lang/其他'}, ]},
{'key': 'by',
'name': '排序',
'value': [{'n': '最近更新', 'v': '/sortType/1/sortOrder/0'},
{'n': '添加时间', 'v': '/sortType/2/sortOrder/0'},
{'n': '人气高低', 'v': '/sortType/3/sortOrder/0'},
{'n': '评分高低', 'v': '/sortType/4/sortOrder/0'}]}
],
'4': [
{'key': 'type',
'name': '类型',
'value': [{'n': '全部', 'v': ''},
{'n': '国产动漫', 'v': '/type/75'},
{'n': '日韩动漫', 'v': '/type/76'},
{'n': '欧美动漫', 'v': '/type/77'}]},
{'key': 'class',
'name': '剧情',
'value': [{'n': '全部', 'v': ''},
{'n': '喜剧', 'v': '/class/喜剧'},
{'n': '科幻', 'v': '/class/科幻'},
{'n': '热血', 'v': '/class/热血'},
{'n': '冒险', 'v': '/class/冒险'},
{'n': '动作', 'v': '/class/动作'},
{'n': '运动', 'v': '/class/运动'},
{'n': '战争', 'v': '/class/战争'},
{'n': '儿童', 'v': '/class/儿童'}]},
{'key': 'area',
'name': '地区',
'value': [{'n': '全部', 'v': ''},
{'n': '中国大陆', 'v': '/area/中国大陆'},
{'n': '日本', 'v': '/area/日本'},
{'n': '美国', 'v': '/area/美国'},
{'n': '其他', 'v': '/area/其他'}]},
{'key': 'year',
'name': '时间',
'value': [{'n': '全部', 'v': ''},
{'n': '2024', 'v': '/year/2024'},
{'n': '2023', 'v': '/year/2023'},
{'n': '2022', 'v': '/year/2022'},
{'n': '2021', 'v': '/year/2021'},
{'n': '2020', 'v': '/year/2020'},
{'n': '2019', 'v': '/year/2019'},
{'n': '2018', 'v': '/year/2018'},
{'n': '2017', 'v': '/year/2017'},
{'n': '2016', 'v': '/year/2016'},
{'n': '2015', 'v': '/year/2015'},
{'n': '2014', 'v': '/year/2014'},
{'n': '2013', 'v': '/year/2013'},
{'n': '2012', 'v': '/year/2012'},
{'n': '2011', 'v': '/year/2011'},
{'n': '2010', 'v': '/year/2010'}]},
{'key': 'lang',
'name': '语言',
'value': [{'n': '全部', 'v': ''},
{'n': '国语', 'v': '/lang/国语'},
{'n': '英语', 'v': '/lang/英语'},
{'n': '日语', 'v': '/lang/日语'},
{'n': '其他', 'v': '/lang/其他'}]},
{'key': 'by',
'name': '排序',
'value': [{'n': '最近更新', 'v': '/sortType/1/sortOrder/0'},
{'n': '添加时间', 'v': '/sortType/2/sortOrder/0'},
{'n': '人气高低', 'v': '/sortType/3/sortOrder/0'},
{'n': '评分高低', 'v': '/sortType/4/sortOrder/0'}]}
]
}
}
def homeVideoContent(self):
video_list = []
t = str(int(time.time() * 1000))
# t = '1723292093234'
data = f'key=cb808529bae6b6be45ecfab29a4889bc&t={t}'
data_md5 = hashlib.md5(data.encode()).hexdigest()
data_sha1 = hashlib.sha1(data_md5.encode()).hexdigest()
h = {
"User-Agent": self.ua,
'referer': self.home_url, 't': t, 'sign': data_sha1}
try:
res = requests.get(f'{self.home_url}/api/mw-movie/anonymous/home/hotSearch', headers=h)
data_list = res.json()['data']
for i in data_list:
video_list.append(
{
'vod_id': i['vodId'],
'vod_name': i['vodName'],
'vod_pic': i['vodPic'],
'vod_remarks': i['vodVersion'] if i['typeId1'] == 1 else i['vodRemarks']
}
)
except requests.RequestException as e:
return {
'list': [],
'parse': 0,
'jx': 0
}
return {
'list': video_list,
'parse': 0,
'jx': 0
}
def categoryContent(self, cid, page, filter, ext):
t = cid
_type = ext.get('type') if ext.get('type') else ''
__class = ext.get('class') if ext.get('class') else ''
_area = ext.get('area') if ext.get('area') else ''
_year = ext.get('year') if ext.get('year') else ''
_lang = ext.get('lang') if ext.get('lang') else ''
_by = ext.get('by') if ext.get('by') else ''
video_list = []
h = {
"User-Agent": self.ua,
'referer': self.home_url,
}
try:
res = requests.get(
f'{self.home_url}/vod/show/id/{t}{_type}{__class}{_area}{_year}{_lang}{_by}/page/{page}',
headers=h)
aa = re.findall(r'\\"list\\":(.*?)}}}]', res.text)
if not aa:
return {'list': [], 'parse': 0, 'jx': 0}
bb = aa[0].replace('\\"', '"')
data_list = json.loads(bb)
for i in data_list:
video_list.append(
{
'vod_id': i['vodId'],
'vod_name': i['vodName'],
'vod_pic': i['vodPic'],
'vod_remarks': i['vodVersion'] if i['typeId1'] == 1 else i['vodRemarks']
}
)
except requests.RequestException as e:
return {'list': [], 'msg': e}
return {'list': video_list, 'parse': 0, 'jx': 0}
def detailContent(self, did):
ids = did[0]
video_list = []
t = str(int(time.time() * 1000))
# t = '1723292093234'
data = f'id={ids}&key=cb808529bae6b6be45ecfab29a4889bc&t={t}'
data_md5 = hashlib.md5(data.encode()).hexdigest()
data_sha1 = hashlib.sha1(data_md5.encode()).hexdigest()
h = {
"User-Agent": self.ua,
'referer': self.home_url,
't': t, 'sign': data_sha1
}
try:
res = requests.get(f'{self.home_url}/api/mw-movie/anonymous/video/detail?id={ids}', headers=h)
data = res.json()['data']
play_list = data['episodeList']
vod_play_url = []
for i in play_list:
name = i['name']
url = ids + '/' + str(i['nid'])
vod_play_url.append(name + '$' + url)
video_list.append(
{
'type_name': data['typeName'],
'vod_id': ids,
'vod_name': data['vodName'],
'vod_remarks': data['vodRemarks'],
'vod_year': data['vodYear'],
'vod_area': data['vodArea'],
'vod_actor': data['vodActor'],
'vod_director': data['vodDirector'],
'vod_content': data['vodContent'],
'vod_play_from': '老僧酿酒',
'vod_play_url': '#'.join(vod_play_url)
}
)
except requests.RequestException as e:
return {'list': [], 'msg': e}
return {"list": video_list, 'parse': 0, 'jx': 0}
def searchContent(self, key, quick, page='1'):
wd = key
video_list = []
t = str(int(time.time() * 1000))
data = f'keyword={wd}&pageNum={page}&pageSize=12&key=cb808529bae6b6be45ecfab29a4889bc&t={t}'
data_md5 = hashlib.md5(data.encode()).hexdigest()
data_sha1 = hashlib.sha1(data_md5.encode()).hexdigest()
h = {
"User-Agent": self.ua,
'referer': self.home_url,
't': t, 'sign': data_sha1
}
try:
response = requests.get(
f'{self.home_url}/api/mw-movie/anonymous/video/searchByWord?keyword={wd}&pageNum={page}&pageSize=12',
headers=h,
)
data_list = response.json()['data']['result']['list']
for i in data_list:
video_list.append(
{
'vod_id': i['vodId'],
'vod_name': i['vodName'],
'vod_pic': i['vodPic'],
'vod_remarks': i['vodVersion'] if i['typeId1'] == 1 else i['vodRemarks']
}
)
except requests.RequestException as e:
return {'list': [], 'msg': e}
return {'list': video_list, 'parse': 0, 'jx': 0}
def playerContent(self, flag, pid, vipFlags):
url = pid
play_url = self.error_url
data = url.split('/')
_id = data[0]
_nid = data[1]
t = str(int(time.time() * 1000))
# t = '1723292093234'
data = f'id={_id}&nid={_nid}&key=cb808529bae6b6be45ecfab29a4889bc&t={t}'
data_md5 = hashlib.md5(data.encode()).hexdigest()
data_sha1 = hashlib.sha1(data_md5.encode()).hexdigest()
h = {
"User-Agent": self.ua,
'referer': self.home_url,
't': t, 'sign': data_sha1
}
h2 = {
"User-Agent": self.ua,
}
try:
res = requests.get(
f'{self.home_url}/api/mw-movie/anonymous/v2/video/episode/url?id={_id}&nid={_nid}',
headers=h)
play_url = res.json()['data']['list'][0]['url']
except requests.RequestException as e:
return {"url": play_url, "header": h2, "parse": 0, "jx": 0}
return {"url": play_url, "header": h2, "parse": 0, "jx": 0}
def localProxy(self, params):
pass
def destroy(self):
return '正在Destroy'
if __name__ == '__main__':
pass