diff --git a/py/央视网.py b/py/央视网.py new file mode 100644 index 0000000..32c95b1 --- /dev/null +++ b/py/央视网.py @@ -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} \ No newline at end of file diff --git a/py/小皮影院.py b/py/小皮影院.py new file mode 100644 index 0000000..b0e8558 --- /dev/null +++ b/py/小皮影院.py @@ -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} \ No newline at end of file diff --git a/py/影库.py b/py/影库.py new file mode 100644 index 0000000..72dcd40 --- /dev/null +++ b/py/影库.py @@ -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 \ No newline at end of file diff --git a/py/熊猫秒播.py b/py/熊猫秒播.py new file mode 100644 index 0000000..55023ce --- /dev/null +++ b/py/熊猫秒播.py @@ -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 + + + + + + + + + + + + + diff --git a/py/稀饭动漫.py b/py/稀饭动漫.py new file mode 100644 index 0000000..2a95742 --- /dev/null +++ b/py/稀饭动漫.py @@ -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() \ No newline at end of file