上传文件至「py」
This commit is contained in:
@@ -0,0 +1,649 @@
|
||||
import sys
|
||||
import re
|
||||
import json
|
||||
import time
|
||||
import random
|
||||
import struct
|
||||
import requests
|
||||
from base64 import b64encode, b64decode
|
||||
from urllib.parse import urlencode
|
||||
from urllib3 import disable_warnings
|
||||
from base.spider import Spider
|
||||
|
||||
disable_warnings()
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
# ==================== 加密常量 ====================
|
||||
DELTA = 0x9e3779b9
|
||||
ROUNDS = 16
|
||||
LOG_ROUNDS = 4
|
||||
SALT_LEN = 2
|
||||
ZERO_LEN = 7
|
||||
TEA_CKEY = bytes.fromhex('59b2f7cf725ef43c34fdd7c123411ed3')
|
||||
GUARD_TEA_KEY = bytes.fromhex('110DBEC10C23E7D2E56A1CAD6914EF1B')
|
||||
|
||||
XOR_KEY = [0x84, 0x2E, 0xED, 0x08, 0xF0, 0x66, 0xE6, 0xEA,
|
||||
0x48, 0xB4, 0xCA, 0xA9, 0x91, 0xED, 0x6F, 0xF3]
|
||||
GUARD_XOR_KEY = [0xB3, 0xC9, 0x53, 0xA0, 0x69, 0x13, 0xAD, 0x4D]
|
||||
|
||||
STANDARD_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='
|
||||
CUSTOM_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-='
|
||||
|
||||
# ==================== 频道图标 ====================
|
||||
# 使用 wanglindl/TVlogo 小尺寸台标源(jsdelivr CDN,已验证可用)
|
||||
# 图标尺寸较小,适合TVBox/直播列表显示
|
||||
LOGOS = {
|
||||
'cctv1': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV1.png',
|
||||
'cctv2': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV2.png',
|
||||
'cctv3': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV3.png',
|
||||
'cctv4': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV4.png',
|
||||
'cctv5': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV5.png',
|
||||
'cctv5p': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV5plus.png',
|
||||
'cctv6': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV6.png',
|
||||
'cctv7': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV7.png',
|
||||
'cctv8': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV8.png',
|
||||
'cctv9': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV9.png',
|
||||
'cctv10': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV10.png',
|
||||
'cctv11': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV11.png',
|
||||
'cctv12': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV12.png',
|
||||
'cctv13': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV13.png',
|
||||
'cctv14': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV14.png',
|
||||
'cctv15': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV15.png',
|
||||
'cctv16': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV16.png',
|
||||
'cctv164k': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV16.png',
|
||||
'cctv17': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV17.png',
|
||||
'cctv4k': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV4K.png',
|
||||
'cctv8k': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV8K.png',
|
||||
'cgtn': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CGTN.png',
|
||||
'cgtnfy': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CGTNfy.png',
|
||||
'cgtney': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CGTNey.png',
|
||||
'cgtnalby': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CGTNalby.png',
|
||||
'cgtnxby': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CGTNxbyy.png',
|
||||
'cgtnwyjl': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CGTNjilu.png',
|
||||
'cctvfyjc': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTVfyjc.png',
|
||||
'cctvdyjc': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTVdyjc.png',
|
||||
'cctvhjjc': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTVhjjc.png',
|
||||
'cctvsjdl': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTVsjdl.png',
|
||||
'cctvfyyy': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTVfyyy.png',
|
||||
'cctvbqkj': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTVbqkj.png',
|
||||
'cctvfyzq': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTVfyzq.png',
|
||||
'cctvgeqwq': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTVgefwq.png',
|
||||
'cctvnxss': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTVnxss.png',
|
||||
'cctvyswhjp': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTVyswhjp.png',
|
||||
'cctvystq': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTVystq.png',
|
||||
'cctvdszn': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTVdszn.png',
|
||||
'cctvwsjk': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTVwsjk.png',
|
||||
'bjws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Beijing.png',
|
||||
'jsws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Jiangsu.png',
|
||||
'dfws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Dongfang.png',
|
||||
'zjws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Zhejiang.png',
|
||||
'hnws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Hunan.png',
|
||||
'hbws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Hubei.png',
|
||||
'gdws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Guangdong.png',
|
||||
'gxws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Guangxi.png',
|
||||
'hljws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Heilongjiang.png',
|
||||
'hnws2': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Hainan.png',
|
||||
'cqws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Chongqing.png',
|
||||
'szws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Shenzhen.png',
|
||||
'scws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Sichuan.png',
|
||||
'henanws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Henan.png',
|
||||
'fjdnhz': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Dongnan.png',
|
||||
'gzhws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Guizhou.png',
|
||||
'jxws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Jiangxi.png',
|
||||
'lnws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Liaoning.png',
|
||||
'ahws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Anhui.png',
|
||||
'hbws2': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Hebei.png',
|
||||
'sdws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Shandong.png',
|
||||
'tjws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Tianjin.png',
|
||||
'jlws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Jilin.png',
|
||||
'shanxiws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Shanxi.png',
|
||||
'nxws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Ningxia.png',
|
||||
'nmgws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Neimeng.png',
|
||||
'ynws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Yunnan.png',
|
||||
'shanxiws2': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Shanxi_.png',
|
||||
'qhws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Qinghai.png',
|
||||
'xzws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Xizang.png',
|
||||
'xjws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Xinjiang.png',
|
||||
'cetv1': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CETV1.png',
|
||||
'gxpd': '',
|
||||
}
|
||||
|
||||
# ==================== 频道数据 ====================
|
||||
# {key: [cnlid, livepid, defn, display_name]}
|
||||
CHANNELS = {
|
||||
'cctv1': ['2024078201', '600001859', 'fhd', 'CCTV-1'],
|
||||
'cctv2': ['2024075401', '600001800', 'fhd', 'CCTV-2'],
|
||||
'cctv3': ['2024068501', '600001801', 'fhd', 'CCTV-3'],
|
||||
'cctv4': ['2029797101', '600001814', 'fhd', 'CCTV-4'],
|
||||
'cctv5': ['2024078401', '600001818', 'fhd', 'CCTV-5'],
|
||||
'cctv5p': ['2024078001', '600001817', 'fhd', 'CCTV-5+'],
|
||||
'cctv6': ['2013693901', '600108442', 'fhd', 'CCTV-6'],
|
||||
'cctv7': ['2024072001', '600004092', 'fhd', 'CCTV-7'],
|
||||
'cctv8': ['2029793001', '600001803', 'fhd', 'CCTV-8'],
|
||||
'cctv9': ['2024078601', '600004078', 'fhd', 'CCTV-9'],
|
||||
'cctv10': ['2024078701', '600001805', 'fhd', 'CCTV-10'],
|
||||
'cctv11': ['2027248701', '600001806', 'fhd', 'CCTV-11'],
|
||||
'cctv12': ['2027248801', '600001807', 'fhd', 'CCTV-12'],
|
||||
'cctv13': ['2029797201', '600001811', 'fhd', 'CCTV-13'],
|
||||
'cctv14': ['2027248901', '600001809', 'fhd', 'CCTV-14'],
|
||||
'cctv15': ['2027249001', '600001815', 'fhd', 'CCTV-15'],
|
||||
'cctv16': ['2027249101', '600098637', 'fhd', 'CCTV-16'],
|
||||
'cctv164k': ['2027249301', '600099502', 'fhd', 'CCTV-16(4K)'],
|
||||
'cctv17': ['2027249401', '600001810', 'fhd', 'CCTV-17'],
|
||||
'cctv4k': ['2029810301', '600002264', 'fhd', 'CCTV-4K'],
|
||||
'cctv8k': ['2026774101', '600156816', 'fhd', 'CCTV-8K'],
|
||||
'cgtn': ['2024181701', '600014550', 'fhd', 'CGTN'],
|
||||
'cgtnfy': ['2024181801', '600084704', 'fhd', 'CGTN法语'],
|
||||
'cgtney': ['2024181901', '600084758', 'fhd', 'CGTN俄语'],
|
||||
'cgtnalby': ['2024182001', '600084782', 'fhd', 'CGTN阿拉伯语'],
|
||||
'cgtnxby': ['2024182101', '600084744', 'fhd', 'CGTN西班牙语'],
|
||||
'cgtnwyjl': ['2024182301', '600084781', 'fhd', 'CGTN外语纪录'],
|
||||
'cctvfyjc': ['2025637103', '600099658', 'shd', '风云剧场'],
|
||||
'cctvdyjc': ['2026874203', '600099655', 'shd', '第一剧场'],
|
||||
'cctvhjjc': ['2026874303', '600099620', 'shd', '怀旧剧场'],
|
||||
'cctvsjdl': ['2026874403', '600099637', 'shd', '世界地理'],
|
||||
'cctvfyyy': ['2026874503', '600099660', 'shd', '风云音乐'],
|
||||
'cctvbqkj': ['2026874603', '600099649', 'shd', '兵器科技'],
|
||||
'cctvfyzq': ['2026966203', '600099636', 'shd', '风云足球'],
|
||||
'cctvgeqwq': ['2026874703', '600099659', 'shd', '高尔夫·网球'],
|
||||
'cctvnxss': ['2026874803', '600099650', 'shd', '女性时尚'],
|
||||
'cctvyswhjp': ['2026874903', '600099653', 'shd', '央视文化精品'],
|
||||
'cctvystq': ['2026875003', '600099652', 'shd', '央视台球'],
|
||||
'cctvdszn': ['2026875103', '600099656', 'shd', '电视指南'],
|
||||
'cctvwsjk': ['2025637003', '600099651', 'shd', '卫生健康'],
|
||||
'bjws': ['2024052703', '600002309', 'fhd', '北京卫视'],
|
||||
'jsws': ['2024171103', '600002521', 'fhd', '江苏卫视'],
|
||||
'dfws': ['2024054503', '600002483', 'fhd', '东方卫视'],
|
||||
'zjws': ['2024054703', '600002520', 'fhd', '浙江卫视'],
|
||||
'hnws': ['2024054803', '600002475', 'fhd', '湖南卫视'],
|
||||
'hbws': ['2024171203', '600002508', 'fhd', '湖北卫视'],
|
||||
'gdws': ['2024060903', '600002485', 'fhd', '广东卫视'],
|
||||
'gxws': ['2024060703', '600002509', 'fhd', '广西卫视'],
|
||||
'hljws': ['2029797003', '600002498', 'fhd', '黑龙江卫视'],
|
||||
'hnws2': ['2024055603', '600002506', 'fhd', '海南卫视'],
|
||||
'cqws': ['2024061103', '600002531', 'fhd', '重庆卫视'],
|
||||
'szws': ['2024061303', '600002481', 'fhd', '深圳卫视'],
|
||||
'scws': ['2024061403', '600002516', 'fhd', '四川卫视'],
|
||||
'henanws': ['2029797303', '600002525', 'fhd', '河南卫视'],
|
||||
'fjdnhz': ['2024061503', '600002484', 'fhd', '福建东南卫视'],
|
||||
'gzhws': ['2024061603', '600002490', 'fhd', '贵州卫视'],
|
||||
'jxws': ['2024061703', '600002503', 'fhd', '江西卫视'],
|
||||
'lnws': ['2024171303', '600002505', 'fhd', '辽宁卫视'],
|
||||
'ahws': ['2024171403', '600002532', 'fhd', '安徽卫视'],
|
||||
'hbws2': ['2024171503', '600002493', 'fhd', '河北卫视'],
|
||||
'sdws': ['2029787903', '600002513', 'fhd', '山东卫视'],
|
||||
'tjws': ['2019927003', '600152137', 'fhd', '天津卫视'],
|
||||
'jlws': ['2025561503', '600190405', 'fhd', '吉林卫视'],
|
||||
'shanxiws': ['2029795103', '600190400', 'fhd', '陕西卫视'],
|
||||
'nxws': ['2025608503', '600190737', 'fhd', '宁夏卫视'],
|
||||
'nmgws': ['2025561203', '600190401', 'fhd', '内蒙古卫视'],
|
||||
'ynws': ['2025561303', '600190402', 'fhd', '云南卫视'],
|
||||
'shanxiws2': ['2025560803', '600190407', 'fhd', '山西卫视'],
|
||||
'qhws': ['2025559103', '600190406', 'fhd', '青海卫视'],
|
||||
'xzws': ['2025558003', '600190403', 'fhd', '西藏卫视'],
|
||||
'xjws': ['2019927403', '600152138', 'fhd', '新疆卫视'],
|
||||
'cetv1': ['2022823801', '600171827', 'fhd', '中国教育电视台'],
|
||||
'gxpd': ['2029360403', '600213139', 'fhd', '国学频道'],
|
||||
}
|
||||
|
||||
# ==================== 分类定义 ====================
|
||||
CATS = [
|
||||
{
|
||||
'name': '央视',
|
||||
'ids': [
|
||||
'cctv1', 'cctv2', 'cctv3', 'cctv4', 'cctv5', 'cctv5p',
|
||||
'cctv6', 'cctv7', 'cctv8', 'cctv9', 'cctv10', 'cctv11',
|
||||
'cctv12', 'cctv13', 'cctv14', 'cctv15', 'cctv16', 'cctv164k',
|
||||
'cctv17', 'cctv4k', 'cctv8k',
|
||||
]
|
||||
},
|
||||
{
|
||||
'name': 'CGTN',
|
||||
'ids': ['cgtn', 'cgtnfy', 'cgtney', 'cgtnalby', 'cgtnxby', 'cgtnwyjl']
|
||||
},
|
||||
{
|
||||
'name': '央视付费',
|
||||
'ids': [
|
||||
'cctvfyjc', 'cctvdyjc', 'cctvhjjc', 'cctvsjdl', 'cctvfyyy',
|
||||
'cctvbqkj', 'cctvfyzq', 'cctvgeqwq', 'cctvnxss', 'cctvyswhjp',
|
||||
'cctvystq', 'cctvdszn', 'cctvwsjk',
|
||||
]
|
||||
},
|
||||
{
|
||||
'name': '卫视',
|
||||
'ids': [
|
||||
'bjws', 'jsws', 'dfws', 'zjws', 'hnws', 'hbws', 'gdws',
|
||||
'gxws', 'hljws', 'hnws2', 'cqws', 'szws', 'scws', 'henanws',
|
||||
'fjdnhz', 'gzhws', 'jxws', 'lnws', 'ahws', 'hbws2', 'sdws',
|
||||
'tjws', 'jlws', 'shanxiws', 'nxws', 'nmgws', 'ynws',
|
||||
'shanxiws2', 'qhws', 'xzws', 'xjws',
|
||||
]
|
||||
},
|
||||
{
|
||||
'name': '其他',
|
||||
'ids': ['cetv1', 'gxpd']
|
||||
},
|
||||
]
|
||||
|
||||
# ==================== 初始化 ====================
|
||||
|
||||
def init(self, extend=""):
|
||||
self.guid = self._gen_guid()
|
||||
self.session = requests.Session()
|
||||
self.session.verify = False
|
||||
self.session.headers.update({
|
||||
'User-Agent': 'qqlive',
|
||||
'Connection': 'Keep-Alive',
|
||||
'Accept': 'application/json',
|
||||
})
|
||||
|
||||
def getName(self):
|
||||
return "央视频"
|
||||
|
||||
# ==================== 蜘蛛壳接口 ====================
|
||||
|
||||
def homeContent(self, filter):
|
||||
classes = [
|
||||
{'type_id': str(i), 'type_name': c['name']}
|
||||
for i, c in enumerate(self.CATS)
|
||||
]
|
||||
return {'class': classes}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
cat = self.CATS[int(tid)]
|
||||
vod_list = []
|
||||
for cid in cat['ids']:
|
||||
ch = self.CHANNELS.get(cid)
|
||||
if ch:
|
||||
vod_list.append({
|
||||
'vod_id': cid,
|
||||
'vod_name': ch[3],
|
||||
'vod_pic': self.LOGOS.get(cid, ''),
|
||||
'vod_remarks': ch[2].upper(),
|
||||
})
|
||||
return {
|
||||
'page': 1,
|
||||
'pagecount': 1,
|
||||
'limit': len(vod_list),
|
||||
'total': len(vod_list),
|
||||
'list': vod_list,
|
||||
}
|
||||
|
||||
def detailContent(self, ids):
|
||||
cid = ids[0]
|
||||
ch = self.CHANNELS.get(cid)
|
||||
if not ch:
|
||||
return {'list': []}
|
||||
vod = {
|
||||
'vod_id': cid,
|
||||
'vod_name': ch[3],
|
||||
'vod_pic': self.LOGOS.get(cid, ''),
|
||||
'vod_play_from': '央视频',
|
||||
'vod_play_url': f'直播${cid}',
|
||||
'vod_content': f'{ch[3]} 高清直播',
|
||||
}
|
||||
return {'list': [vod]}
|
||||
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
ch = self.CHANNELS.get(id)
|
||||
if not ch:
|
||||
return {'parse': 0, 'url': '', 'header': ''}
|
||||
play_url = self._get_play_url(ch[0], ch[1], ch[2])
|
||||
if play_url:
|
||||
return {'parse': 0, 'url': play_url, 'header': ''}
|
||||
return {'parse': 0, 'url': '', 'header': ''}
|
||||
|
||||
# ==================== 播放地址获取 ====================
|
||||
|
||||
def _get_play_url(self, cnlid, livepid, defn):
|
||||
try:
|
||||
self.guid = self._gen_guid()
|
||||
ck = self._gen_ckey(cnlid)
|
||||
flowid = self._gen_flowid()
|
||||
|
||||
params = {
|
||||
"atime": "120",
|
||||
"livepid": livepid,
|
||||
"cnlid": cnlid,
|
||||
"appVer": "V8.22.1035.3031",
|
||||
"app_version": "300090",
|
||||
"caplv": "1",
|
||||
"cmd": "2",
|
||||
"defn": defn,
|
||||
"device": "iPhone",
|
||||
"encryptVer": "4.2",
|
||||
"getpreviewinfo": "0",
|
||||
"hevclv": "33",
|
||||
"lang": "zh-Hans_JP",
|
||||
"livequeue": "0",
|
||||
"logintype": "1",
|
||||
"nettype": "1",
|
||||
"newnettype": "1",
|
||||
"newplatform": "4330403",
|
||||
"platform": "4330403",
|
||||
"sdtfrom": "v3021",
|
||||
"spacode": "23",
|
||||
"spaudio": "1",
|
||||
"spdemuxer": "6",
|
||||
"spdrm": "2",
|
||||
"spdynamicrange": "7",
|
||||
"spflv": "1",
|
||||
"spflvaudio": "1",
|
||||
"sphdrfps": "60",
|
||||
"sphttps": "0",
|
||||
"spvcode": "MSgzMDoyMTYwLDYwOjIxNjB8MzA6MjE2MCw2MDoyMTYwKTsyKDMwOjIxNjAsNjA6MjE2MHwzMDoyMTYwLDYwOjIxNjAp",
|
||||
"spvideo": "4",
|
||||
"stream": "1",
|
||||
"system": "1",
|
||||
"sysver": "ios18.2.1",
|
||||
"uhd_flag": "4",
|
||||
"cKey": ck['ckey'],
|
||||
"guid": self.guid,
|
||||
"fntick": ck['params']['Timestamp'],
|
||||
"flowid": flowid,
|
||||
"playbacktime": "0",
|
||||
}
|
||||
|
||||
resp = self.session.get(
|
||||
"https://bkliveinfo.ysp.cctv.cn",
|
||||
params=params,
|
||||
timeout=15,
|
||||
)
|
||||
data = resp.json()
|
||||
if data.get('iretcode') == 0 and data.get('playurl'):
|
||||
return data['playurl']
|
||||
except Exception as e:
|
||||
print(f"[央视频] 获取播放地址失败: {e}")
|
||||
return None
|
||||
|
||||
# ==================== GUID / FlowID ====================
|
||||
|
||||
def _gen_guid(self):
|
||||
return '{:08x}{:04x}{:04x}{:04x}{:012x}'.format(
|
||||
random.randint(0, 0xffffffff),
|
||||
random.randint(0, 0xffff),
|
||||
random.randint(0, 0xffff),
|
||||
random.randint(0, 0xffff),
|
||||
random.randint(0, 0xffffffffffff),
|
||||
)
|
||||
|
||||
def _gen_flowid(self):
|
||||
p = [
|
||||
random.randint(0, 0xffff), random.randint(0, 0xffff),
|
||||
random.randint(0, 0xffff),
|
||||
random.randint(0, 0x0fff) | 0x4000,
|
||||
random.randint(0, 0x3fff) | 0x8000,
|
||||
random.randint(0, 0xffff), random.randint(0, 0xffff),
|
||||
random.randint(0, 0xffff),
|
||||
]
|
||||
return '{:04X}{:04X}-{:04X}-{:04X}-{:04X}-{:04X}{:04X}{:04X}_4330403'.format(*p)
|
||||
|
||||
# ==================== 签名 ====================
|
||||
|
||||
def _calc_sig(self, buf):
|
||||
s = 0
|
||||
for b in buf:
|
||||
s = (0x83 * s + (b & 0xFF)) & 0x7FFFFFFF
|
||||
return s
|
||||
|
||||
# ==================== 自定义 Base64 ====================
|
||||
|
||||
def _b64enc(self, data):
|
||||
enc = b64encode(data).decode()
|
||||
return enc.translate(str.maketrans(self.STANDARD_ALPHABET, self.CUSTOM_ALPHABET)).rstrip('=')
|
||||
|
||||
def _b64dec(self, text):
|
||||
text = text.rstrip('=')
|
||||
if len(text) % 4:
|
||||
text += '=' * (4 - len(text) % 4)
|
||||
return b64decode(text.translate(str.maketrans(self.CUSTOM_ALPHABET, self.STANDARD_ALPHABET)))
|
||||
|
||||
# ==================== XOR ====================
|
||||
|
||||
def _xor(self, arr):
|
||||
return [arr[i] ^ self.XOR_KEY[i & 0xF] for i in range(len(arr))]
|
||||
|
||||
# ==================== TEA ====================
|
||||
|
||||
def _tea_enc(self, data, key):
|
||||
if len(data) < 8:
|
||||
data = data.ljust(8, b'\0')
|
||||
y, z = struct.unpack('>II', data[:8])
|
||||
k = struct.unpack('>4I', key[:16])
|
||||
s = 0
|
||||
for _ in range(self.ROUNDS):
|
||||
s = (s + self.DELTA) & 0xFFFFFFFF
|
||||
y = (y + (((z << 4) + k[0]) ^ (z + s) ^ ((z >> 5) + k[1]))) & 0xFFFFFFFF
|
||||
z = (z + (((y << 4) + k[2]) ^ (y + s) ^ ((y >> 5) + k[3]))) & 0xFFFFFFFF
|
||||
return struct.pack('>II', y, z)
|
||||
|
||||
def _tea_dec(self, data, key):
|
||||
y, z = struct.unpack('>II', data[:8])
|
||||
k = struct.unpack('>4I', key[:16])
|
||||
s = (self.DELTA << self.LOG_ROUNDS) & 0xFFFFFFFF
|
||||
for _ in range(self.ROUNDS):
|
||||
z = (z - (((y << 4) + k[2]) ^ (y + s) ^ ((y >> 5) + k[3]))) & 0xFFFFFFFF
|
||||
y = (y - (((z << 4) + k[0]) ^ (z + s) ^ ((z >> 5) + k[1]))) & 0xFFFFFFFF
|
||||
s = (s - self.DELTA) & 0xFFFFFFFF
|
||||
return struct.pack('>II', y, z)
|
||||
|
||||
# ==================== CBC 加密 ====================
|
||||
|
||||
def _cbc_enc(self, p_in, n_len, p_key):
|
||||
pad_salt_zero = n_len + 1 + self.SALT_LEN + self.ZERO_LEN
|
||||
n_pad = pad_salt_zero % 8
|
||||
if n_pad:
|
||||
n_pad = 8 - n_pad
|
||||
|
||||
out = b''
|
||||
src = [0] * 8
|
||||
src[0] = (random.randint(0, 255) & 0xF8) | n_pad
|
||||
si = 1
|
||||
|
||||
while n_pad:
|
||||
src[si] = random.randint(0, 255)
|
||||
si += 1
|
||||
n_pad -= 1
|
||||
|
||||
iv_p = [0] * 8
|
||||
iv_c = [0] * 8
|
||||
|
||||
# salt
|
||||
i = 0
|
||||
while i < self.SALT_LEN:
|
||||
if si < 8:
|
||||
src[si] = random.randint(0, 255)
|
||||
si += 1
|
||||
i += 1
|
||||
if si == 8:
|
||||
for j in range(8):
|
||||
src[j] ^= iv_c[j]
|
||||
tb = list(self._tea_enc(bytes(src), p_key))
|
||||
for j in range(8):
|
||||
tb[j] ^= iv_p[j]
|
||||
iv_p = list(src)
|
||||
iv_c = list(tb)
|
||||
out += bytes(tb)
|
||||
si = 0
|
||||
|
||||
# body
|
||||
pi = 0
|
||||
while n_len:
|
||||
if si < 8:
|
||||
src[si] = p_in[pi]
|
||||
pi += 1
|
||||
si += 1
|
||||
n_len -= 1
|
||||
if si == 8:
|
||||
for j in range(8):
|
||||
src[j] ^= iv_c[j]
|
||||
tb = list(self._tea_enc(bytes(src), p_key))
|
||||
for j in range(8):
|
||||
tb[j] ^= iv_p[j]
|
||||
iv_p = list(src)
|
||||
iv_c = list(tb)
|
||||
out += bytes(tb)
|
||||
si = 0
|
||||
|
||||
# zero
|
||||
i = 0
|
||||
while i < self.ZERO_LEN:
|
||||
if si < 8:
|
||||
src[si] = 0
|
||||
si += 1
|
||||
i += 1
|
||||
if si == 8:
|
||||
for j in range(8):
|
||||
src[j] ^= iv_c[j]
|
||||
tb = list(self._tea_enc(bytes(src), p_key))
|
||||
for j in range(8):
|
||||
tb[j] ^= iv_p[j]
|
||||
iv_p = list(src)
|
||||
iv_c = list(tb)
|
||||
out += bytes(tb)
|
||||
si = 0
|
||||
|
||||
# last
|
||||
if si > 0:
|
||||
for j in range(si, 8):
|
||||
src[j] = 0
|
||||
for j in range(8):
|
||||
src[j] ^= iv_c[j]
|
||||
tb = list(self._tea_enc(bytes(src), p_key))
|
||||
for j in range(8):
|
||||
tb[j] ^= iv_p[j]
|
||||
out += bytes(tb)
|
||||
|
||||
return out
|
||||
|
||||
# ==================== CBC 解密 ====================
|
||||
|
||||
def _cbc_dec(self, p_in, n_len, p_key):
|
||||
if n_len % 8 != 0 or n_len < 16:
|
||||
return None
|
||||
dest = list(self._tea_dec(p_in[:8], p_key))
|
||||
n_pad = dest[0] & 0x07
|
||||
out_len = n_len - 1 - n_pad - self.SALT_LEN - self.ZERO_LEN
|
||||
if out_len < 0:
|
||||
return None
|
||||
|
||||
iv_pre = [0] * 8
|
||||
iv_cur = list(p_in[:8])
|
||||
off = 8
|
||||
di = 1 + n_pad
|
||||
|
||||
# skip salt
|
||||
sc = 1
|
||||
while sc <= self.SALT_LEN:
|
||||
if di < 8:
|
||||
di += 1
|
||||
sc += 1
|
||||
elif di == 8:
|
||||
iv_pre = list(iv_cur)
|
||||
if off + 8 > n_len:
|
||||
return None
|
||||
iv_cur = list(p_in[off:off + 8])
|
||||
for j in range(8):
|
||||
dest[j] ^= iv_cur[j]
|
||||
dest = list(self._tea_dec(bytes(dest), p_key))
|
||||
off += 8
|
||||
di = 0
|
||||
|
||||
# copy plain
|
||||
plain = []
|
||||
rem = out_len
|
||||
while rem > 0:
|
||||
if di < 8:
|
||||
plain.append(dest[di] ^ iv_pre[di])
|
||||
di += 1
|
||||
rem -= 1
|
||||
elif di == 8:
|
||||
iv_pre = list(iv_cur)
|
||||
if off + 8 > n_len:
|
||||
return None
|
||||
iv_cur = list(p_in[off:off + 8])
|
||||
for j in range(8):
|
||||
dest[j] ^= iv_cur[j]
|
||||
dest = list(self._tea_dec(bytes(dest), p_key))
|
||||
off += 8
|
||||
di = 0
|
||||
return bytes(plain)
|
||||
|
||||
# ==================== Guard Time ====================
|
||||
|
||||
def _last5(self, v):
|
||||
v = str(v)
|
||||
return v[-5:] if len(v) >= 5 else ''
|
||||
|
||||
def _gen_guard_time(self, ts, guid):
|
||||
body = struct.pack('>I', ts)
|
||||
for part in [self._last5(guid), self._last5('null'), self._last5('null'), '-1']:
|
||||
pb = part.encode()
|
||||
body += struct.pack('>H', len(pb)) + pb
|
||||
|
||||
plain = struct.pack('>H', len(body)) + body
|
||||
chk = self._calc_sig(list(plain))
|
||||
|
||||
enc = self._cbc_enc(plain, len(plain), self.GUARD_TEA_KEY) + struct.pack('>I', chk)
|
||||
el = list(enc)
|
||||
for i in range(len(el)):
|
||||
el[i] ^= self.GUARD_XOR_KEY[i & 7]
|
||||
return bytes(el).hex().upper()
|
||||
|
||||
# ==================== CKey ====================
|
||||
|
||||
def _encrypt_ckey(self, data):
|
||||
chk = self._calc_sig(list(data))
|
||||
enc = self._cbc_enc(data, len(data), self.TEA_CKEY) + struct.pack('>I', chk)
|
||||
return '--01' + self._b64enc(bytes(self._xor(list(enc))))
|
||||
|
||||
def _build_pkt(self, params):
|
||||
d = b''
|
||||
d += bytes.fromhex('0000004200000004000004d2') # 12-byte header
|
||||
d += struct.pack('>I', params['Platform'])
|
||||
d += struct.pack('>I', 0) # sig placeholder
|
||||
d += struct.pack('>I', params['Timestamp'])
|
||||
|
||||
for k in ['Sdtfrom', 'randFlag', 'appVer', 'vid', 'guid']:
|
||||
v = params[k].encode()
|
||||
d += struct.pack('>H', len(v)) + v
|
||||
|
||||
d += struct.pack('>I', 1) # part1
|
||||
d += struct.pack('>I', 1) # isDlna
|
||||
|
||||
for v in [b'2622783A', b'nil']:
|
||||
d += struct.pack('>H', len(v)) + v
|
||||
|
||||
uuid4 = params['uuid4'].encode()
|
||||
d += struct.pack('>H', len(uuid4)) + uuid4
|
||||
|
||||
d += struct.pack('>H', 3) + b'nil' # bundleID1
|
||||
|
||||
for v in [b'v0.1.000', b'com.cctv.yangshipin.app.iphone',
|
||||
b'4330403', b'ex_json_bus', b'ex_json_vs']:
|
||||
d += struct.pack('>H', len(v)) + v
|
||||
|
||||
cgt = params['ck_guard_time'].encode()
|
||||
d += struct.pack('>H', len(cgt)) + cgt
|
||||
|
||||
buf = struct.pack('>H', len(d)) + d
|
||||
sig = self._calc_sig(list(buf))
|
||||
return buf[:18] + struct.pack('>I', sig) + buf[22:]
|
||||
|
||||
def _gen_ckey(self, cnlid):
|
||||
ts = int(time.time())
|
||||
cgt = self._gen_guard_time(ts, self.guid)
|
||||
params = {
|
||||
'Platform': 4330403,
|
||||
'Timestamp': ts,
|
||||
'Sdtfrom': 'dcgh',
|
||||
'vid': cnlid,
|
||||
'guid': self.guid,
|
||||
'appVer': 'V8.22.1035.3031',
|
||||
'randFlag': '_zj1A5Gh6QYcxWjIUGos2w==',
|
||||
'uuid4': '57eab0c4-2c58-44c6-8ae9-dd2757525dc5',
|
||||
'ck_guard_time': cgt,
|
||||
}
|
||||
pkt = self._build_pkt(params)
|
||||
return {'ckey': self._encrypt_ckey(pkt), 'params': params}
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
import re, requests, urllib.parse
|
||||
from lxml import etree
|
||||
from base.spider import Spider
|
||||
|
||||
class Spider(Spider):
|
||||
def getName(self): return "小皮影院"
|
||||
def init(self, extend=""):
|
||||
self.host = "https://www.xptv.cc"
|
||||
self.headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Referer": self.host + "/"}
|
||||
self.categories = [
|
||||
{"type_id": "dianying", "type_name": "电影"},
|
||||
{"type_id": "dianshiju", "type_name": "电视剧"},
|
||||
{"type_id": "dongman", "type_name": "动漫"}
|
||||
]
|
||||
self.filters = {
|
||||
"dianying": [
|
||||
{"key": "year", "name": "年份", "value": [
|
||||
{"n": "全部", "v": ""}, {"n": "2026", "v": "2026"}, {"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": "更早", "v": "1800-1979"}
|
||||
]},
|
||||
{"key": "area", "name": "地区", "value": [
|
||||
{"n": "全部", "v": ""}, {"n": "内地", "v": "%E5%86%85%E5%9C%B0"}, {"n": "中国香港", "v": "%E4%B8%AD%E5%9B%BD%E9%A6%99%E6%B8%AF"},
|
||||
{"n": "中国台湾", "v": "%E4%B8%AD%E5%9B%BD%E5%8F%B0%E6%B9%BE"}, {"n": "韩国", "v": "%E9%9F%A9%E5%9B%BD"}, {"n": "日本", "v": "%E6%97%A5%E6%9C%AC"},
|
||||
{"n": "美国", "v": "%E7%BE%8E%E5%9B%BD"}, {"n": "法国", "v": "%E6%B3%95%E5%9B%BD"}, {"n": "英国", "v": "%E8%8B%B1%E5%9B%BD"},
|
||||
{"n": "泰国", "v": "%E6%B3%B0%E5%9B%BD"}, {"n": "印度", "v": "%E5%8D%B0%E5%BA%A6"}, {"n": "其它", "v": "%E5%85%B6%E5%AE%83"}
|
||||
]},
|
||||
{"key": "class", "name": "类型", "value": [
|
||||
{"n": "全部", "v": ""}, {"n": "剧情", "v": "%E5%89%A7%E6%83%85"}, {"n": "喜剧", "v": "%E5%96%9C%E5%89%A7"}, {"n": "爱情", "v": "%E7%88%B1%E6%83%85"},
|
||||
{"n": "动作", "v": "%E5%8A%A8%E4%BD%9C"}, {"n": "犯罪", "v": "%E7%8A%AF%E7%BD%AA"}, {"n": "惊悚", "v": "%E6%83%8A%E6%82%9A"}, {"n": "悬疑", "v": "%E6%82%AC%E7%96%91"},
|
||||
{"n": "科幻", "v": "%E7%A7%91%E5%B9%BB"}, {"n": "恐怖", "v": "%E6%81%90%E6%80%96"}, {"n": "奇幻", "v": "%E5%A5%87%E5%B9%BB"}, {"n": "冒险", "v": "%E5%86%92%E9%99%A9"},
|
||||
{"n": "古装", "v": "%E5%8F%A4%E8%A3%85"}, {"n": "动画", "v": "%E5%8A%A8%E7%94%BB"}, {"n": "传记", "v": "%E4%BC%A0%E8%AE%B0"}, {"n": "纪录", "v": "%E7%BA%AA%E5%BD%95"}
|
||||
]},
|
||||
{"key": "by", "name": "排序", "value": [
|
||||
{"n": "按时间", "v": "time"}, {"n": "按热度", "v": "hits"}, {"n": "按评分", "v": "score"}
|
||||
]}
|
||||
],
|
||||
"dianshiju": [
|
||||
{"key": "year", "name": "年份", "value": [
|
||||
{"n": "全部", "v": ""}, {"n": "2026", "v": "2026"}, {"n": "2025", "v": "2025"}, {"n": "2024", "v": "2024"},
|
||||
{"n": "2023", "v": "2023"}, {"n": "2022", "v": "2022"}, {"n": "更早", "v": "1800-1979"}
|
||||
]},
|
||||
{"key": "area", "name": "地区", "value": [
|
||||
{"n": "全部", "v": ""}, {"n": "内地", "v": "%E5%86%85%E5%9C%B0"}, {"n": "中国香港", "v": "%E4%B8%AD%E5%9B%BD%E9%A6%99%E6%B8%AF"},
|
||||
{"n": "中国台湾", "v": "%E4%B8%AD%E5%9B%BD%E5%8F%B0%E6%B9%BE"}, {"n": "韩国", "v": "%E9%9F%A9%E5%9B%BD"}, {"n": "日本", "v": "%E6%97%A5%E6%9C%AC"},
|
||||
{"n": "美国", "v": "%E7%BE%8E%E5%9B%BD"}, {"n": "其它", "v": "%E5%85%B6%E5%AE%83"}
|
||||
]},
|
||||
{"key": "class", "name": "类型", "value": [
|
||||
{"n": "全部", "v": ""}, {"n": "剧情", "v": "%E5%89%A7%E6%83%85"}, {"n": "喜剧", "v": "%E5%96%9C%E5%89%A7"}, {"n": "爱情", "v": "%E7%88%B1%E6%83%85"},
|
||||
{"n": "动作", "v": "%E5%8A%A8%E4%BD%9C"}, {"n": "犯罪", "v": "%E7%8A%AF%E7%BD%AA"}, {"n": "悬疑", "v": "%E6%82%AC%E7%96%91"},
|
||||
{"n": "古装", "v": "%E5%8F%A4%E8%A3%85"}, {"n": "历史", "v": "%E5%8E%86%E5%8F%B2"}, {"n": "战争", "v": "%E6%88%98%E4%BA%89"},
|
||||
{"n": "动画", "v": "%E5%8A%A8%E7%94%BB"}, {"n": "纪录", "v": "%E7%BA%AA%E5%BD%95"}
|
||||
]},
|
||||
{"key": "by", "name": "排序", "value": [
|
||||
{"n": "按时间", "v": "time"}, {"n": "按热度", "v": "hits"}, {"n": "按评分", "v": "score"}
|
||||
]}
|
||||
],
|
||||
"dongman": [
|
||||
{"key": "year", "name": "年份", "value": [
|
||||
{"n": "全部", "v": ""}, {"n": "2026", "v": "2026"}, {"n": "2025", "v": "2025"}, {"n": "2024", "v": "2024"},
|
||||
{"n": "2023", "v": "2023"}, {"n": "2022", "v": "2022"}, {"n": "更早", "v": "1800-1979"}
|
||||
]},
|
||||
{"key": "isend", "name": "状态", "value": [
|
||||
{"n": "全部", "v": ""}, {"n": "完结", "v": "%E5%AE%8C%E7%BB%93"}, {"n": "连载", "v": "%E8%BF%9E%E8%BD%BD"}
|
||||
]}
|
||||
]
|
||||
}
|
||||
def _get(self, url):
|
||||
try:
|
||||
r = requests.get(url, headers=self.headers, timeout=8)
|
||||
r.encoding = r.apparent_encoding or "utf-8"
|
||||
return r.text
|
||||
except: return None
|
||||
def _fix(self, u):
|
||||
if not u: return ""
|
||||
if u.startswith("//"): return "https:" + u
|
||||
if u.startswith("/"): return self.host + u
|
||||
if u.startswith("http"): return u
|
||||
return ""
|
||||
def _parse_list(self, text):
|
||||
if not text: return []
|
||||
tree = etree.HTML(text); res, seen = [], set()
|
||||
for a in tree.xpath('//a[contains(@href,"/v/")]'):
|
||||
try:
|
||||
h = a.get("href", "") or ""
|
||||
m = re.search(r"/v/(\d+)\.html", h)
|
||||
if not m or m.group(1) in seen: continue
|
||||
seen.add(m.group(1))
|
||||
t = "".join(a.xpath('.//h3/text()')).strip() or a.get("title", "") or ""
|
||||
imgs = a.xpath('.//img')
|
||||
p = ""
|
||||
if imgs:
|
||||
for attr in ["data-original", "data-src", "src"]:
|
||||
p = imgs[0].get(attr, "") or ""
|
||||
if p: break
|
||||
res.append({"vod_id": m.group(1), "vod_name": t, "vod_pic": self._fix(p)})
|
||||
except: continue
|
||||
return res
|
||||
def homeContent(self, filter):
|
||||
h = self._get(self.host + "/")
|
||||
return {"class": self.categories, "list": self._parse_list(h) if h else [], "filters": self.filters}
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
parts = []
|
||||
if extend:
|
||||
for k in ["year", "area", "class", "by", "isend"]:
|
||||
v = (extend.get(k, "") or "").strip()
|
||||
if v: parts.append(f"{k}/{v}")
|
||||
if parts:
|
||||
url = self.host + f"/show/{tid}/" + "/".join(parts) + ("/page/" + pg if pg != "1" else "") + ".html"
|
||||
else:
|
||||
url = self.host + f"/show/{tid}.html" if pg == "1" else self.host + f"/show/{tid}/page/{pg}.html"
|
||||
h = self._get(url)
|
||||
if not h: return {"page": int(pg), "pagecount": 99, "limit": 24, "total": 999, "list": []}
|
||||
tree = etree.HTML(h)
|
||||
pc = 1
|
||||
for x in tree.xpath('//div[contains(@class,"font-mono")]//text()'):
|
||||
if "/" in x:
|
||||
try: pc = int(x.split("/")[-1].strip()); break
|
||||
except: pass
|
||||
return {"page": int(pg), "pagecount": pc, "limit": 24, "total": 999, "list": self._parse_list(h)}
|
||||
def detailContent(self, ids):
|
||||
res = {"list": []}
|
||||
for vid in ids:
|
||||
try:
|
||||
h = self._get(f"{self.host}/v/{vid}.html")
|
||||
if not h: continue
|
||||
tree = etree.HTML(h)
|
||||
name = "".join(tree.xpath('//h1/text()')).strip()
|
||||
pic = self._fix("".join(tree.xpath('//main//img[1]/@src')))
|
||||
grids = tree.xpath('//div[contains(@class,"playlist-grid")]')
|
||||
if not grids:
|
||||
grids = tree.xpath('//div[contains(@class,"playlist")]//div[contains(@class,"grid")]')
|
||||
srcs, eps = [], []
|
||||
for grid in grids:
|
||||
items = grid.xpath('.//a[contains(@href,"/p/")]')
|
||||
el = [f'{"".join(a.xpath(".//text()")).strip()}${self._fix(a.get("href",""))}' for a in items]
|
||||
if el: srcs.append(f"节点{len(srcs)+1}" if len(grids) > 1 else "云播"); eps.append("#".join(el))
|
||||
if not srcs:
|
||||
items = tree.xpath('//a[contains(@href,"/p/")]')
|
||||
el = list(dict.fromkeys([f'{"".join(a.xpath(".//text()")).strip()}${self._fix(a.get("href",""))}' for a in items]))
|
||||
if el: srcs.append("云播"); eps.append("#".join(el))
|
||||
res["list"].append({"vod_id": vid, "vod_name": name, "vod_pic": pic, "vod_play_from": "$$$".join(srcs), "vod_play_url": "$$$".join(eps)})
|
||||
except: continue
|
||||
return res
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
return {"list": [], "page": int(pg)}
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
url = id if id.startswith("http") else self._fix(id)
|
||||
try:
|
||||
h = self._get(url)
|
||||
if h and 'var player_aaaa=' in h:
|
||||
p = h.find('var player_aaaa=') + len('var player_aaaa=')
|
||||
depth, end, i = 0, p, p
|
||||
while i < len(h):
|
||||
if h[i] == '{': depth += 1
|
||||
elif h[i] == '}':
|
||||
depth -= 1
|
||||
if depth == 0: end = i + 1; break
|
||||
i += 1
|
||||
import json
|
||||
data = json.loads(h[p:end])
|
||||
play_id = urllib.parse.unquote(data.get('url', ''))
|
||||
if data.get('from') == 'juhe' and play_id:
|
||||
parse_url = 'https://api.apiimg.com/show/super.php?id=' + urllib.parse.quote(play_id, safe='=')
|
||||
ph = self._get(parse_url)
|
||||
if ph:
|
||||
ms = re.findall(r'https?://[^"\'<>]+?\.(?:m3u8|mp4)[^"\'<>]*', ph, re.I)
|
||||
ms += re.findall(r'https?:\\/\\/[^"\'<>]+?\.(?:m3u8|mp4)[^"\'<>]*', ph, re.I)
|
||||
ms = list(dict.fromkeys([x.replace('\\/', '/') for x in ms if 'api.apiimg.com' not in x and 'playbg.php' not in x and x.lower().startswith('http')]))
|
||||
if ms:
|
||||
hd = {"User-Agent": self.headers["User-Agent"], "Referer": parse_url}
|
||||
return {"parse": 0, "url": ms[0], "header": hd}
|
||||
except: pass
|
||||
return {"parse": 1, "url": url, "header": self.headers}
|
||||
@@ -0,0 +1,255 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
tvbox 插件 - 蛋挞TV(兼容基类修正版)
|
||||
站点: https://www.dantatv.cc
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
from base64 import b64encode, b64decode
|
||||
|
||||
sys.path.append('..')
|
||||
from base.spider import Spider
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
|
||||
def init(self, extend=""):
|
||||
self.host = "https://www.dantatv.cc"
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
# ---------- 辅助函数 ----------
|
||||
def getheader(self, content_type=None):
|
||||
headers = {
|
||||
'Unique-Origin': 'B9A378A8C39BDA1277D2D6185FCE2695',
|
||||
'User-Agent': 'okhttp/4.1.0/luob.app',
|
||||
'Connection': 'Keep-Alive',
|
||||
'Accept-Encoding': 'gzip',
|
||||
}
|
||||
if content_type:
|
||||
headers['Content-Type'] = content_type
|
||||
return headers
|
||||
|
||||
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 homeContent(self, filter):
|
||||
classes = [
|
||||
{"type_name": "剧集", "type_id": "1"},
|
||||
{"type_name": "电影", "type_id": "2"},
|
||||
{"type_name": "动漫", "type_id": "3"},
|
||||
{"type_name": "短剧", "type_id": "4"},
|
||||
{"type_name": "综艺", "type_id": "5"},
|
||||
{"type_name": "体育赛事", "type_id": "29"},
|
||||
]
|
||||
return {"class": classes, "filters": {}}
|
||||
|
||||
# ---------- 首页推荐视频 ----------
|
||||
def homeVideoContent(self):
|
||||
url = f"{self.host}/api/index"
|
||||
headers = self.getheader()
|
||||
try:
|
||||
resp = self.fetch(url, headers=headers)
|
||||
data = resp.json()
|
||||
vod_list = []
|
||||
if data.get('data'):
|
||||
for item in data['data'][0].get('vodList', []):
|
||||
vod_list.append(self._vod_to_common(item))
|
||||
except Exception as e:
|
||||
print(f"首页推荐请求失败: {e}")
|
||||
vod_list = []
|
||||
return {'list': vod_list}
|
||||
|
||||
# ---------- 分类页(POST请求改用 self.post) ----------
|
||||
def categoryContent(self, tid, pg, filter, extend=None):
|
||||
if extend is None:
|
||||
extend = {}
|
||||
body = {
|
||||
"typeId1": int(tid),
|
||||
"pageNum": int(pg),
|
||||
"pageSize": 12,
|
||||
"sortField": "vod_time",
|
||||
"vodClass": extend.get('class', ''),
|
||||
"vodArea": extend.get('area', ''),
|
||||
"vodYear": extend.get('year', '')
|
||||
}
|
||||
url = f"{self.host}/api/search/type"
|
||||
headers = self.getheader(content_type='application/json')
|
||||
try:
|
||||
# 使用基类的 post 方法(若基类无 post,可改为 requests.post 自行实现)
|
||||
resp = self.post(url, headers=headers, data=json.dumps(body).encode('utf-8'))
|
||||
data = resp.json()
|
||||
vod_list = [self._vod_to_common(item) for item in data.get('data', [])]
|
||||
except Exception as e:
|
||||
print(f"分类页请求失败: {e}")
|
||||
vod_list = []
|
||||
return {
|
||||
'list': vod_list,
|
||||
'page': pg,
|
||||
'pagecount': 9999,
|
||||
'limit': 12,
|
||||
'total': 999999
|
||||
}
|
||||
|
||||
# ---------- 详情页 ----------
|
||||
def detailContent(self, ids):
|
||||
vod_id = ids[0]
|
||||
url = f"{self.host}/api/vod/play?vodId={vod_id}"
|
||||
headers = self.getheader()
|
||||
try:
|
||||
resp = self.fetch(url, headers=headers)
|
||||
data = resp.json()
|
||||
vod = data.get('data', {}).get('dantaVod', {})
|
||||
except Exception as e:
|
||||
print(f"详情请求失败: {e}")
|
||||
return {'list': []}
|
||||
|
||||
if not vod:
|
||||
return {'list': []}
|
||||
|
||||
info = {
|
||||
'vod_id': vod.get('vodId'),
|
||||
'vod_name': vod.get('vodName'),
|
||||
'vod_pic': self._fix_pic(vod.get('vodPic')),
|
||||
'vod_actor': vod.get('vodActor'),
|
||||
'vod_director': vod.get('vodDirector'),
|
||||
'vod_blurb': vod.get('vodContent') or vod.get('vodBlurb'),
|
||||
'vod_area': vod.get('vodArea'),
|
||||
'vod_year': vod.get('vodYear'),
|
||||
'vod_remarks': vod.get('vodRemarks'),
|
||||
'vod_lang': vod.get('vodLang'),
|
||||
'vod_class': vod.get('vodClass'),
|
||||
}
|
||||
|
||||
sources = vod.get('sources', [])
|
||||
vod_play_from = []
|
||||
vod_play_url = []
|
||||
for idx, src in enumerate(sources):
|
||||
collect_id = src.get('collectId')
|
||||
raw_url = src.get('vodPlayUrl', '')
|
||||
if not raw_url:
|
||||
continue
|
||||
|
||||
items = raw_url.split('#')
|
||||
encoded_items = []
|
||||
for part in items:
|
||||
if '$' not in part:
|
||||
continue
|
||||
name, link = part.split('$', 1)
|
||||
payload = {"collectId": collect_id, "url": link}
|
||||
enc = self.e64(json.dumps(payload, ensure_ascii=False))
|
||||
encoded_items.append(f"{name}${enc}")
|
||||
|
||||
if encoded_items:
|
||||
line_name = src.get('collectName') or src.get('vodPlayFrom') or f"线路{idx+1}"
|
||||
vod_play_from.append(line_name)
|
||||
vod_play_url.append('#'.join(encoded_items))
|
||||
|
||||
info['vod_play_from'] = '$$$'.join(vod_play_from)
|
||||
info['vod_play_url'] = '$$$'.join(vod_play_url)
|
||||
return {'list': [info]}
|
||||
|
||||
# ---------- 搜索 ----------
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
url = f"{self.host}/api/search/keyword"
|
||||
params = {"keyword": key, "pageNum": pg, "pageSize": 10}
|
||||
headers = self.getheader()
|
||||
try:
|
||||
resp = self.fetch(url, headers=headers, params=params)
|
||||
data = resp.json()
|
||||
vod_list = [self._vod_to_common(item) for item in data.get('data', [])]
|
||||
except Exception as e:
|
||||
print(f"搜索失败: {e}")
|
||||
vod_list = []
|
||||
return {'list': vod_list, 'page': pg}
|
||||
|
||||
# ---------- 播放解析 ----------
|
||||
def playerContent(self, flag, id, vipFlags):
|
||||
try:
|
||||
payload = json.loads(self.d64(id))
|
||||
collect_id = payload.get('collectId')
|
||||
raw_url = payload.get('url')
|
||||
if not collect_id or not raw_url:
|
||||
raise ValueError("缺少参数")
|
||||
except:
|
||||
return {
|
||||
'parse': 1,
|
||||
'url': '',
|
||||
'header': {'User-Agent': 'okhttp/4.1.0/luob.app'}
|
||||
}
|
||||
|
||||
parse_url = f"{self.host}/api/vod/parse"
|
||||
params = {"collectId": collect_id, "url": raw_url}
|
||||
headers = self.getheader()
|
||||
try:
|
||||
resp = self.fetch(parse_url, headers=headers, params=params)
|
||||
if resp.status_code == 200:
|
||||
result = resp.json()
|
||||
final_url = result.get('data') or result.get('url')
|
||||
if final_url:
|
||||
return {
|
||||
'parse': 0,
|
||||
'url': final_url,
|
||||
'header': {'User-Agent': 'okhttp/4.1.0/luob.app'}
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"解析失败: {e}")
|
||||
|
||||
return {
|
||||
'parse': 1,
|
||||
'url': raw_url,
|
||||
'header': {'User-Agent': 'okhttp/4.1.0/luob.app'}
|
||||
}
|
||||
|
||||
# ---------- 内部辅助:字段映射 ----------
|
||||
def _vod_to_common(self, item):
|
||||
remark = item.get('vodRemarks', '')
|
||||
color = item.get('vodColor')
|
||||
if color:
|
||||
remark = f"[{color}] {remark}" if remark else color
|
||||
|
||||
return {
|
||||
'vod_id': item.get('vodId'),
|
||||
'vod_name': item.get('vodName'),
|
||||
'vod_pic': self._fix_pic(item.get('vodPic')),
|
||||
'vod_remarks': remark,
|
||||
'vod_year': item.get('vodYear'),
|
||||
'vod_area': item.get('vodArea'),
|
||||
'vod_actor': item.get('vodActor'),
|
||||
'vod_director': item.get('vodDirector'),
|
||||
'vod_lang': item.get('vodLang'),
|
||||
'vod_class': item.get('vodClass'),
|
||||
}
|
||||
|
||||
def _fix_pic(self, pic):
|
||||
"""补全相对路径图片"""
|
||||
if pic and pic.startswith('/'):
|
||||
return self.host + pic
|
||||
return pic
|
||||
+253
@@ -0,0 +1,253 @@
|
||||
# coding=utf-8
|
||||
# !/usr/bin/python
|
||||
import sys
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
import re
|
||||
from base.spider import Spider
|
||||
import json
|
||||
sys.path.append('..')
|
||||
xurl = "https://ee55ff.com/video.html"
|
||||
headerx = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.87 Safari/537.36'
|
||||
}
|
||||
class Spider(Spider):
|
||||
global xurl
|
||||
global headerx
|
||||
|
||||
def getName(self):
|
||||
return "首页"
|
||||
|
||||
def init(self, extend):
|
||||
pass
|
||||
|
||||
def isVideoFormat(self, url):
|
||||
pass
|
||||
|
||||
def manualVideoCheck(self):
|
||||
pass
|
||||
|
||||
def homeContent(self, filter):
|
||||
# https://yaselulu.autos/?page_id=9
|
||||
|
||||
data = {"name": "John", "age": 31, "city": "New York"}
|
||||
res = requests.post('https://spiderscloudcn2.51111666.com/getDataInit', headers=headerx, json=data)
|
||||
res.encoding = "utf-8"
|
||||
json_dict = json.loads(res.text)
|
||||
menu0ListMap = json_dict["data"]["menu0ListMap"]
|
||||
result = {}
|
||||
result['class'] = []
|
||||
for item in menu0ListMap:
|
||||
if item['typeName'] == "传媒" or item['typeName'] == "视频" or item['typeName'] == "电影":
|
||||
for item1 in item['menu2List']:
|
||||
result['class'].append({'type_id': item1['typeId2'], 'type_name': item1['typeName2']})
|
||||
|
||||
return result
|
||||
|
||||
def homeVideoContent(self):
|
||||
videos = []
|
||||
try:
|
||||
data = {
|
||||
"command": "WEB_GET_INFO",
|
||||
"pageNumber": 1,
|
||||
"RecordsPage": 20,
|
||||
"typeId": "24",
|
||||
"typeMid": "1",
|
||||
"languageType": "CN",
|
||||
"content": ""
|
||||
}
|
||||
res = requests.post('https://spiderscloudcn2.51111666.com/forward', headers=headerx, json=data)
|
||||
res.encoding = "utf-8"
|
||||
json_dict = json.loads(res.text)
|
||||
menu0ListMap = json_dict["data"]["resultList"]
|
||||
for item in menu0ListMap:
|
||||
name1 = item['vod_name'].replace("yy8ycom", "")
|
||||
pattern = r'(.*?)-(.*?)-\d+\s+'
|
||||
name = re.sub(pattern, '', name1)
|
||||
id = item['id']
|
||||
pic = item['vod_pic']
|
||||
id2 = item['vod_server_id']
|
||||
|
||||
video = {
|
||||
"vod_id": str(id) + '#' + str(id2),
|
||||
"vod_name": name,
|
||||
"vod_pic": pic,
|
||||
"vod_remarks": ''
|
||||
}
|
||||
videos.append(video)
|
||||
result = {'list': videos}
|
||||
return result
|
||||
except:
|
||||
pass
|
||||
|
||||
def categoryContent(self, cid, pg, filter, ext):
|
||||
result = {}
|
||||
videos = []
|
||||
if not pg:
|
||||
pg = 1
|
||||
|
||||
# https://yaselulu.autos/?cat=3754&paged=1
|
||||
|
||||
videos = []
|
||||
try:
|
||||
data = {
|
||||
"command": "WEB_GET_INFO",
|
||||
"pageNumber": pg,
|
||||
"RecordsPage": 20,
|
||||
"typeId": cid,
|
||||
"typeMid": "1",
|
||||
"languageType": "CN",
|
||||
"content": ""
|
||||
}
|
||||
res = requests.post('https://spiderscloudcn2.51111666.com/forward', headers=headerx, json=data)
|
||||
res.encoding = "utf-8"
|
||||
json_dict = json.loads(res.text)
|
||||
menu0ListMap = json_dict["data"]["resultList"]
|
||||
for item in menu0ListMap:
|
||||
name1 = item['vod_name'].replace("yy8ycom", "")
|
||||
pattern = r'(.*?)-(.*?)-\d+\s+'
|
||||
name = re.sub(pattern, '', name1)
|
||||
id = item['id']
|
||||
pic = item['vod_pic']
|
||||
id2 = item['vod_server_id']
|
||||
|
||||
video = {
|
||||
"vod_id": str(id) + '#' + str(id2),
|
||||
"vod_name": name,
|
||||
"vod_pic": pic,
|
||||
"vod_remarks": ''
|
||||
}
|
||||
videos.append(video)
|
||||
except:
|
||||
pass
|
||||
|
||||
result['list'] = videos
|
||||
result['page'] = pg
|
||||
result['pagecount'] = 9999
|
||||
result['limit'] = 90
|
||||
result['total'] = 999999
|
||||
return result
|
||||
|
||||
def detailContent(self, ids):
|
||||
data2 = {"name": "John", "age": 31, "city": "New York"}
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.87 Safari/537.36',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
res1 = requests.post('https://spiderscloudcn2.51111666.com/getDataInit', headers=headers, json=data2)
|
||||
|
||||
js1 = json.loads(res1.text)
|
||||
|
||||
did = ids[0]
|
||||
cid, svid = did.split("#")
|
||||
videos = []
|
||||
result = {}
|
||||
data = {
|
||||
"command": "WEB_GET_INFO_DETAIL",
|
||||
"type_Mid": "1",
|
||||
"id": cid,
|
||||
"languageType": "CN"
|
||||
}
|
||||
res = requests.post('https://spiderscloudcn2.51111666.com/forward', headers=headerx, json=data)
|
||||
res.encoding = "utf-8"
|
||||
|
||||
json_dict = json.loads(res.text)
|
||||
if svid:
|
||||
purl = js1['data']['macVodLinkMap'][svid]['LINK_2'] + json_dict['data']["result"]["vod_url"]
|
||||
else:
|
||||
purl = json_dict['data']["result"]["vod_url"]
|
||||
|
||||
videos.append({
|
||||
"vod_id": '',
|
||||
"vod_name": '',
|
||||
"vod_pic": "",
|
||||
"type_name": "ぃぅおか🍬 คิดถึง",
|
||||
"vod_year": "",
|
||||
"vod_area": "",
|
||||
"vod_remarks": "",
|
||||
"vod_actor": "",
|
||||
"vod_director": "",
|
||||
"vod_content": "",
|
||||
"vod_play_from": "直链播放",
|
||||
"vod_play_url": purl
|
||||
})
|
||||
|
||||
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, page):
|
||||
# https://yaselulu.autos/?s=%E6%88%91%E7%9A%84&paged=2
|
||||
|
||||
result = {}
|
||||
videos = []
|
||||
if not page:
|
||||
page = 1
|
||||
|
||||
data = {
|
||||
"command": "WEB_GET_INFO",
|
||||
"pageNumber": page,
|
||||
"RecordsPage": 20,
|
||||
"typeId": "0",
|
||||
"typeMid": "1",
|
||||
"languageType": "CN",
|
||||
"content": key,
|
||||
"type": "1"
|
||||
}
|
||||
res = requests.post('https://spiderscloudcn2.51111666.com/forward', headers=headerx, json=data)
|
||||
res.encoding = "utf-8"
|
||||
json_dict = json.loads(res.text)
|
||||
menu0ListMap = json_dict["data"]["resultList"]
|
||||
for item in menu0ListMap:
|
||||
name = item['vod_name'].replace("yy8ycom", "")
|
||||
id = item['id']
|
||||
pic = item['vod_pic']
|
||||
id2 = item['vod_server_id']
|
||||
|
||||
video = {
|
||||
"vod_id": str(id) + '#' + str(id2),
|
||||
"vod_name": name,
|
||||
"vod_pic": pic,
|
||||
"vod_remarks": ''
|
||||
}
|
||||
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):
|
||||
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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
+296
@@ -0,0 +1,296 @@
|
||||
# 本资源来源于互联网公开渠道,仅可用于个人学习爬虫技术。
|
||||
# 严禁将其用于任何商业用途,下载后请于 24 小时内删除,搜索结果均来自源站,本人不承担任何责任。
|
||||
# junyouyun
|
||||
|
||||
import re
|
||||
import json
|
||||
from urllib.parse import quote, urljoin, parse_qs
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
from base.spider import Spider
|
||||
|
||||
BASE = "https://anime.xifanacg.com"
|
||||
HEADERS = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
|
||||
"Referer": BASE,
|
||||
}
|
||||
|
||||
CLASS_OPTIONS = {
|
||||
"1": ["搞笑", "原创", "轻小说改", "恋爱", "百合", "漫改", "校园", "战斗", "治愈", "奇幻",
|
||||
"日常", "青春", "乙女向", "悬疑", "后宫", "科幻", "冒险", "热血", "异世界", "游戏改",
|
||||
"音乐", "偶像", "美食", "耽美"],
|
||||
"2": ["搞笑", "原创", "轻小说改", "恋爱", "百合", "漫改", "校园", "战斗", "治愈", "奇幻",
|
||||
"日常", "青春", "乙女向", "悬疑", "后宫", "科幻", "冒险", "热血", "异世界", "游戏改",
|
||||
"音乐", "偶像", "美食", "耽美", "2026年1月", "2025年10月"],
|
||||
"3": [],
|
||||
"21": [],
|
||||
}
|
||||
|
||||
AREA_OPTIONS = {
|
||||
"1": ["日本"],
|
||||
"2": [],
|
||||
"3": ["日本"],
|
||||
"21": [],
|
||||
}
|
||||
|
||||
YEAR_RANGE = [str(y) for y in range(2026, 2004, -1)]
|
||||
|
||||
ORDER_OPTIONS = [
|
||||
{"n": "按最新", "v": "time"},
|
||||
{"n": "按最热", "v": "hits"},
|
||||
{"n": "按评分", "v": "score"},
|
||||
]
|
||||
|
||||
|
||||
class Spider(Spider):
|
||||
def init(self, extend=""):
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update(HEADERS)
|
||||
|
||||
def _get_decoded_html(self, url):
|
||||
r = self.session.get(url, timeout=15)
|
||||
r.encoding = r.apparent_encoding if r.apparent_encoding else 'utf-8'
|
||||
return r.text
|
||||
|
||||
def _parse_vod_cards(self, html):
|
||||
soup = BeautifulSoup(html, 'html.parser')
|
||||
vod_list = []
|
||||
for box in soup.select('.public-list-box.public-pic-b'):
|
||||
link = box.select_one('a.public-list-exp')
|
||||
if not link:
|
||||
continue
|
||||
href = link.get('href', '')
|
||||
vid = href.split('/')[-1].replace('.html', '')
|
||||
title = link.get('title', '')
|
||||
img = box.select_one('img.gen-movie-img')
|
||||
pic = img.get('data-src', '') if img else ''
|
||||
prb = box.select_one('.public-list-prb')
|
||||
remarks = prb.get_text(strip=True) if prb else ''
|
||||
vod_list.append({
|
||||
"vod_id": vid,
|
||||
"vod_name": title,
|
||||
"vod_pic": pic,
|
||||
"vod_remarks": remarks
|
||||
})
|
||||
return vod_list
|
||||
|
||||
def homeContent(self, filter):
|
||||
classes = [
|
||||
{"type_name": "连载新番", "type_id": "1"},
|
||||
{"type_name": "完结旧番", "type_id": "2"},
|
||||
{"type_name": "剧场版", "type_id": "3"},
|
||||
{"type_name": "美漫", "type_id": "21"},
|
||||
]
|
||||
filters = {}
|
||||
for tid in ["1", "2", "3", "21"]:
|
||||
f_list = []
|
||||
if CLASS_OPTIONS[tid]:
|
||||
f_list.append({
|
||||
"key": "class", "name": "类型",
|
||||
"value": [{"n": "全部", "v": ""}] + [{"n": c, "v": c} for c in CLASS_OPTIONS[tid]]
|
||||
})
|
||||
if AREA_OPTIONS[tid]:
|
||||
f_list.append({
|
||||
"key": "area", "name": "地区",
|
||||
"value": [{"n": "全部", "v": ""}] + [{"n": a, "v": a} for a in AREA_OPTIONS[tid]]
|
||||
})
|
||||
if tid != "21":
|
||||
f_list.append({
|
||||
"key": "year", "name": "年份",
|
||||
"value": [{"n": "全部", "v": ""}] + [{"n": y, "v": y} for y in YEAR_RANGE]
|
||||
})
|
||||
f_list.append({"key": "by", "name": "排序", "value": ORDER_OPTIONS})
|
||||
filters[tid] = f_list
|
||||
return {"class": classes, "filters": filters}
|
||||
|
||||
def homeVideoContent(self):
|
||||
try:
|
||||
html = self._get_decoded_html(BASE)
|
||||
return {"list": self._parse_vod_cards(html)}
|
||||
except Exception:
|
||||
data = self.categoryContent("1", "1", False, {})
|
||||
return {"list": data.get("list", [])}
|
||||
|
||||
def categoryContent(self, tid, pg, filter, extend):
|
||||
"""
|
||||
分类列表:POST 请求 /index.php/ds_api/vod
|
||||
传递完整表单参数(包含空字段 lang/version/state/letter/time/level/weekday)
|
||||
"""
|
||||
# 2. 构建完整表单数据(必须包含所有字段,空值用空字符串)
|
||||
form_data = {
|
||||
'type': tid,
|
||||
'class': extend.get("class", ""),
|
||||
'area': extend.get("area", ""),
|
||||
'year': extend.get("year", ""),
|
||||
'lang': extend.get("lang", ""),
|
||||
'version': extend.get("version", ""),
|
||||
'state': extend.get("state", ""),
|
||||
'letter': extend.get("letter", ""),
|
||||
'time': extend.get("time", ""),
|
||||
'level': extend.get("level", "0"),
|
||||
'weekday': extend.get("weekday", ""),
|
||||
'by': extend.get("by", "time"),
|
||||
'page': pg,
|
||||
}
|
||||
url = f"{BASE}/index.php/ds_api/vod"
|
||||
try:
|
||||
r = self.session.post(url, data=form_data, timeout=15)
|
||||
data = r.json()
|
||||
except Exception:
|
||||
return {"list": [], "page": int(pg), "pagecount": 1, "limit": 40, "total": 0}
|
||||
|
||||
vod_list = []
|
||||
for item in data.get("list", []):
|
||||
vod_id = str(item.get("vod_id", ""))
|
||||
actor_raw = item.get("vod_actor", "")
|
||||
actor = ','.join([a for a in actor_raw.split(',') if a.strip()]) if actor_raw else ''
|
||||
vod_list.append({
|
||||
"vod_id": vod_id,
|
||||
"vod_name": item.get("vod_name", ""),
|
||||
"vod_pic": item.get("vod_pic", ""),
|
||||
"vod_remarks": item.get("vod_remarks", ""),
|
||||
"vod_actor": actor,
|
||||
"vod_douban_score": str(item.get("vod_douban_score", "")) if item.get("vod_douban_score") else "",
|
||||
})
|
||||
return {
|
||||
"list": vod_list,
|
||||
"page": int(data.get("page", pg)),
|
||||
"pagecount": int(data.get("pagecount", 1)),
|
||||
"limit": int(data.get("limit", 40)),
|
||||
"total": int(data.get("total", 0)),
|
||||
}
|
||||
|
||||
def detailContent(self, ids):
|
||||
vid = ids[0] if isinstance(ids, list) else ids
|
||||
url = f"{BASE}/bangumi/{vid}.html"
|
||||
try:
|
||||
html = self._get_decoded_html(url)
|
||||
except Exception:
|
||||
return {"list": [{"vod_id": vid, "vod_name": "获取失败"}]}
|
||||
|
||||
soup = BeautifulSoup(html, 'html.parser')
|
||||
name = soup.select_one('h3.slide-info-title')
|
||||
name = name.text.strip() if name else ''
|
||||
pic_tag = soup.select_one('.detail-pic img.lazy')
|
||||
pic = pic_tag.get('data-src', '') if pic_tag else ''
|
||||
score_tag = soup.select_one('.fraction')
|
||||
score = score_tag.text.strip() if score_tag else ''
|
||||
remarks_tag = soup.select_one('.slide-info-remarks')
|
||||
remarks = remarks_tag.text.strip() if remarks_tag else ''
|
||||
|
||||
director = actor = area = year = desc = ''
|
||||
param_box = soup.select_one('.info-parameter')
|
||||
if param_box:
|
||||
for li in param_box.find_all('li'):
|
||||
em = li.find('em')
|
||||
if not em:
|
||||
continue
|
||||
key = em.get_text(strip=True).rstrip(':').rstrip(':')
|
||||
li_clone = BeautifulSoup(str(li), 'html.parser')
|
||||
for e in li_clone.find_all('em'):
|
||||
e.decompose()
|
||||
value = li_clone.get_text(strip=True)
|
||||
if '导演' in key:
|
||||
director = value
|
||||
elif '主演' in key:
|
||||
actor = value
|
||||
elif '地区' in key:
|
||||
area = value
|
||||
elif '年份' in key:
|
||||
year = value
|
||||
elif '简介' in key:
|
||||
desc = value
|
||||
|
||||
if not director:
|
||||
d_tag = soup.find('strong', string=re.compile('导演'))
|
||||
if d_tag and d_tag.find_next('a'):
|
||||
director = d_tag.find_next('a').text.strip()
|
||||
if not actor:
|
||||
a_tag = soup.find('strong', string=re.compile('演员'))
|
||||
if a_tag:
|
||||
actor_links = a_tag.find_next_siblings('a')
|
||||
actor = ','.join([a.text.strip() for a in actor_links]) if actor_links else ''
|
||||
if not desc:
|
||||
desc_div = soup.select_one('#height_limit')
|
||||
desc = desc_div.text.strip() if desc_div else ''
|
||||
|
||||
play_from, play_url = [], []
|
||||
tab_links = soup.select('.anthology-tab .swiper-slide')
|
||||
list_boxes = soup.select('.anthology-list-box')
|
||||
for idx, box in enumerate(list_boxes):
|
||||
line_name = f"线路{idx+1}"
|
||||
if idx < len(tab_links):
|
||||
raw = tab_links[idx].get_text(strip=True)
|
||||
badge = tab_links[idx].find('span', class_='badge')
|
||||
if badge:
|
||||
raw = raw.replace(badge.text, '').strip()
|
||||
if raw:
|
||||
line_name = raw
|
||||
play_from.append(line_name)
|
||||
eps = [f"{a.text.strip()}${urljoin(BASE, a['href'])}" for a in box.select('li a.this-link')]
|
||||
play_url.append("#".join(eps))
|
||||
|
||||
vod = {
|
||||
"vod_id": str(vid), "vod_name": name, "vod_pic": pic,
|
||||
"vod_score": score, "vod_remarks": remarks,
|
||||
"vod_year": year, "vod_area": area, "vod_actor": actor,
|
||||
"vod_director": director, "vod_content": desc,
|
||||
"vod_play_from": "$$$".join(play_from) if play_from else "",
|
||||
"vod_play_url": "$$$".join(play_url) if play_url else "",
|
||||
}
|
||||
return {"list": [vod]}
|
||||
|
||||
def searchContent(self, key, quick, pg="1"):
|
||||
# 优先使用 AJAX suggest 接口
|
||||
try:
|
||||
url = f"{BASE}/index.php/ajax/suggest?mid=1&wd={quote(key)}"
|
||||
r = self.session.get(url, timeout=10)
|
||||
data = r.json()
|
||||
vod_list = []
|
||||
for item in data.get("list", []):
|
||||
vod_list.append({
|
||||
"vod_id": str(item.get("id", "")),
|
||||
"vod_name": item.get("name", ""),
|
||||
"vod_pic": item.get("pic", ""),
|
||||
})
|
||||
if vod_list:
|
||||
return {"list": vod_list, "page": int(pg)}
|
||||
except Exception:
|
||||
pass # 接口失败则回退到页面解析
|
||||
|
||||
# 回退:静态搜索页面
|
||||
try:
|
||||
search_url = f"{BASE}/search.html?wd={quote(key)}"
|
||||
html = self._get_decoded_html(search_url)
|
||||
vod_list = self._parse_vod_cards(html)
|
||||
return {"list": vod_list, "page": int(pg)}
|
||||
except Exception:
|
||||
return {"list": [], "page": int(pg)}
|
||||
|
||||
def playerContent(self, flag, video_id, vipFlags):
|
||||
try:
|
||||
html = self._get_decoded_html(video_id)
|
||||
except Exception:
|
||||
return {"parse": 0, "url": ""}
|
||||
m = re.search(r'var\s+player_\w+\s*=\s*({[^;]+})', html, re.DOTALL)
|
||||
if not m:
|
||||
direct = re.search(r'"url":"(https?://[^"]+\.(?:mp4|m3u8)[^"]*)"', html)
|
||||
return {"parse": 0, "url": direct.group(1)} if direct else {"parse": 0, "url": ""}
|
||||
try:
|
||||
data = json.loads(m.group(1))
|
||||
except json.JSONDecodeError:
|
||||
raw = m.group(1)
|
||||
fallback = re.search(r'"url":"(https?://[^"]+)"', raw)
|
||||
return {"parse": 0, "url": fallback.group(1)} if fallback else {"parse": 0, "url": ""}
|
||||
mp4_url = data.get("url", "")
|
||||
if data.get("encrypt") == 1 and data.get("from") in ("xfy2", "CS"):
|
||||
mp4_url = f"https://player.moedot.net/player/index.php?code=xfdm1&from=cf&url={mp4_url}"
|
||||
return {"parse": 1, "url": mp4_url}
|
||||
return {"parse": 0, "url": mp4_url}
|
||||
|
||||
def getName(self):
|
||||
return "xifanacg"
|
||||
|
||||
def destroy(self):
|
||||
if hasattr(self, "session"):
|
||||
self.session.close()
|
||||
Reference in New Issue
Block a user