添加 TVB云播

This commit is contained in:
qist
2026-08-08 15:30:00 +08:00
parent bed70bca27
commit 9ac1d3118f
3 changed files with 313 additions and 336 deletions
+311 -336
View File
@@ -5,422 +5,397 @@ TVB云播
""" """
import re import re
import json import json
import base64
import zlib
import requests import requests
from pyquery import PyQuery as pq from Crypto.Cipher import AES
from bs4 import BeautifulSoup
from base.spider import Spider # 继承基础Spider类 from base.spider import Spider # 继承基础Spider类
class Spider(Spider): # 直接继承Spider基类 # ------------------------------------------------------------------ #
# 响应解密(AES-128-CBC + gzip),key/iv 来自 libnmmp.so
# ------------------------------------------------------------------ #
_AES_KEY = b"MickwrKuMickwrKu"
_AES_IV = b"MickwrKuMickwrKu"
def _decrypt_data(b64text):
"""data 字段(base64) -> 明文 JSON 字符串"""
if not b64text:
return None
try:
raw = base64.b64decode(b64text)
pt = AES.new(_AES_KEY, AES.MODE_CBC, _AES_IV).decrypt(raw)
pad = pt[-1]
if 1 <= pad <= 16:
pt = pt[:-pad]
try:
return zlib.decompress(pt, 16 + zlib.MAX_WBITS).decode('utf-8', 'ignore')
except Exception:
return pt.decode('utf-8', 'ignore')
except Exception as e:
print(f"[hktv] decrypt fail: {e}")
return None
# 线路名 -> 中文显示名(源站返回的是英文/拼音缩写)
_LINE_CN = {
'mp4': 'MP4 高清',
'YYNB': '优优牛播',
'wsym3u8': '微视云 M3U8',
'bfzym3u8': '暴风影视 M3U8',
'lzm3u8': '量子 M3U8',
'hkm3u8': '港剧 M3U8',
'1080zyk': '1080 自愈库',
}
def _line_cn(name):
"""线路名转中文显示;未知线路保留原名。"""
return _LINE_CN.get(name, name)
class Spider(Spider):
def getName(self): def getName(self):
return 'TVB云播' return 'HKTV影视'
def init(self, extend=""): def init(self, extend=""):
pass if extend and extend.strip():
self.host = extend.strip().rstrip('/')
return self.host
def __init__(self): def __init__(self):
self.name = 'TVB云播' self.name = 'HKTV影视'
self.host = 'http://www.viptvb06.com' self.host = 'http://app.tuxianimg.com'
self.timeout = 25 self.timeout = 25
# 注:Host / Content-Length 由 requests 按 URL 与 body 自动生成,不在此手动设置。
self.header = { self.header = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 'App-Device-Id': '2c84565e46955353d875e3d964d87e1c6',
'App-Os-Type': 'android',
'App-Ui-Mode': 'light',
'App-Version-Code': '100',
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'Connection': 'Keep-Alive',
'Accept-Encoding': 'gzip',
'User-Agent': 'okhttp/3.14.9',
} }
# 由 playerContent 作为“全局播放 header”返回,框架会透传到所有直链请求。
# 分类映射 # 注:host 字段由 requests/框架按 URL 自动生成,不在此手动设置(否则跨域名会错)。
self.class_names = '电影&电视剧&综艺&动漫&日韩剧&国产剧&欧美剧&港台剧'.split('&') self.play_header = {
self.class_urls = '1&2&3&4&16&13&15&14'.split('&') 'User-Agent': 'Dalvik/2.1.0 (Linux; U; Android 15; ZXV10S100V7 Build/b5cc949.1)',
'accept-encoding': 'gzip',
# 过滤条件映射 'allowcrossprotocolredirects': 'true',
self.filter_map = { 'connection': 'Keep-Alive',
"1": [ }
{"key": "cateId", "name": "类型", "value": [{"n": "全部", "v": "1"}, {"n": "动作片", "v": "6"}, {"n": "喜剧片", "v": "7"}, {"n": "爱情片", "v": "8"}, {"n": "科幻片", "v": "9"}, {"n": "恐怖片", "v": "10"}, {"n": "剧情片", "v": "11"}, {"n": "战争片", "v": "12"}]}, # 运行时由 init 接口填充
{"key": "class", "name": "剧情", "value": [{"n": "全部", "v": ""}, {"n": "喜剧", "v": "/class/喜剧"}, {"n": "爱情", "v": "/class/爱情"}, {"n": "恐怖", "v": "/class/恐怖"}, {"n": "动作", "v": "/class/动作"}, {"n": "科幻", "v": "/class/科幻"}, {"n": "剧情", "v": "/class/剧情"}, {"n": "战争", "v": "/class/战争"}, {"n": "警匪", "v": "/class/警匪"}, {"n": "犯罪", "v": "/class/犯罪"}, {"n": "动画", "v": "/class/动画"}, {"n": "奇幻", "v": "/class/奇幻"}, {"n": "武侠", "v": "/class/武侠"}, {"n": "冒险", "v": "/class/冒险"}, {"n": "枪战", "v": "/class/枪战"}, {"n": "悬疑", "v": "/class/悬疑"}, {"n": "惊悚", "v": "/class/惊悚"}, {"n": "经典", "v": "/class/经典"}, {"n": "青春", "v": "/class/青春"}, {"n": "文艺", "v": "/class/文艺"}, {"n": "微电影", "v": "/class/微电影"}, {"n": "古装", "v": "/class/古装"}, {"n": "历史", "v": "/class/历史"}, {"n": "运动", "v": "/class/运动"}, {"n": "农村", "v": "/class/农村"}, {"n": "儿童", "v": "/class/儿童"}, {"n": "网络电影", "v": "/class/网络电影"}]}, self.class_names = []
{"key": "area", "name": "地区", "value": [{"n": "全部", "v": ""}, {"n": "大陆", "v": "/area/大陆"}, {"n": "香港", "v": "/area/香港"}, {"n": "台湾", "v": "/area/台湾"}, {"n": "美国", "v": "/area/美国"}, {"n": "法国", "v": "/area/法国"}, {"n": "英国", "v": "/area/英国"}, {"n": "日本", "v": "/area/日本"}, {"n": "韩国", "v": "/area/韩国"}, {"n": "德国", "v": "/area/Germany"}, {"n": "泰国", "v": "/area/Thailand"}, {"n": "India", "v": "/area/India"}, {"n": "Italy", "v": "/area/Italy"}, {"n": "Spain", "v": "/area/Spain"}, {"n": "Canada", "v": "/area/Canada"}, {"n": "Other", "v": "/area/Other"}]}, self.class_urls = []
{"key": "lang", "name": "语言", "value": [{"n": "全部", "v": ""}, {"n": "国语", "v": "/lang/国语"}, {"n": "英语", "v": "/lang/英语"}, {"n": "粤语", "v": "/lang/粤语"}, {"n": "韩语", "v": "/lang/韩语"}, {"n": "日语", "v": "/lang/日语"}, {"n": "法语", "v": "/lang/法语"}, {"n": "德语", "v": "/lang/德语"}, {"n": "其它", "v": "/lang/其它"}]}, self.type_extend = {} # type_id -> {class/area/lang/year/sort 选项}
{"key": "year", "name": "年份", "value": [{"n": "全部", "v": ""}, {"n": "2026", "v": "/year/2026"}, {"n": "2025", "v": "/year/2025"}, {"n": "2024", "v": "/year/2024"}, {"n": "2023", "v": "/year/2023"}, {"n": "2022", "v": "/year/2022"}, {"n": "2021", "v": "/year/2021"}, {"n": "2020", "v": "/year/2020"}, {"n": "2019", "v": "/year/2019"}, {"n": "2018", "v": "/year/2018"}, {"n": "2017", "v": "/year/2017"}, {"n": "2016", "v": "/year/2016"}, {"n": "2015", "v": "/year/2015"}, {"n": "2014", "v": "/year/2014"}, {"n": "2013", "v": "/year/2013"}, {"n": "2012", "v": "/year/2012"}, {"n": "2011", "v": "/year/2011"}, {"n": "2010", "v": "/year/2010"}, {"n": "2009", "v": "/year/2009"}, {"n": "2008", "v": "/year/2008"}, {"n": "2007", "v": "/year/2007"}, {"n": "2006", "v": "/year/2006"}, {"n": "2005", "v": "/year/2005"}, {"n": "2004", "v": "/year/2004"}]},
{"key": "by", "name": "排序", "value": [{"n": "全部", "v": ""}, {"n": "时间", "v": "/by/time"}, {"n": "人气", "v": "/by/hits"}, {"n": "评分", "v": "/by/score"}]} # ------------------------------------------------------------------ #
], # 内部:统一 API 请求 + 解密
"2": [ # ------------------------------------------------------------------ #
{"key": "cateId", "name": "类型", "value": [{"n": "全部", "v": "2"}, {"n": "国产剧", "v": "13"}, {"n": "港台剧", "v": "14"}, {"n": "日韩剧", "v": "15"}, {"n": "欧美剧", "v": "16"}]}, def _api(self, endpoint, params=None):
{"key": "class", "name": "剧情", "value": [{"n": "全部", "v": ""}, {"n": "古装", "v": "/class/古装"}, {"n": "青春", "v": "/class/青春"}, {"n": "偶像", "v": "/class/偶像"}, {"n": "喜剧", "v": "/class/喜剧"}, {"n": "家庭", "v": "/class/家庭"}, {"n": "犯罪", "v": "/class/犯罪"}, {"n": "动作", "v": "/class/动作"}, {"n": "奇幻", "v": "/class/奇幻"}, {"n": "剧情", "v": "/class/剧情"}, {"n": "历史", "v": "/class/历史"}, {"n": "经典", "v": "/class/经典"}, {"n": "乡村", "v": "/class/乡村"}, {"n": "情景", "v": "/class/情景"}, {"n": "商战", "v": "/class/商战"}, {"n": "网剧", "v": "/class/网剧"}, {"n": "其他", "v": "/class/其他"}]}, url = f"{self.host}/api/vod/{endpoint}"
{"key": "area", "name": "地区", "value": [{"n": "全部", "v": ""}, {"n": "内地", "v": "/area/内地"}, {"n": "韩国", "v": "/area/韩国"}, {"n": "香港", "v": "/area/香港"}, {"n": "台湾", "v": "/area/台湾"}, {"n": "日本", "v": "/area/日本"}, {"n": "美国", "v": "/area/美国"}, {"n": "泰国", "v": "/area/泰国"}, {"n": "英国", "v": "/area/英国"}, {"n": "Singapore", "v": "/area/Singapore"}, {"n": "Other", "v": "/area/Other"}]}, try:
{"key": "year", "name": "年份", "value": [{"n": "全部", "v": ""}, {"n": "2026", "v": "/year/2026"}, {"n": "2025", "v": "/year/2025"}, {"n": "2024", "v": "/year/2024"}, {"n": "2023", "v": "/year/2023"}, {"n": "2022", "v": "/year/2022"}, {"n": "2021", "v": "/year/2021"}, {"n": "2020", "v": "/year/2020"}, {"n": "2019", "v": "/year/2019"}, {"n": "2018", "v": "/year/2018"}, {"n": "2017", "v": "/year/2017"}, {"n": "2016", "v": "/year/2016"}, {"n": "2015", "v": "/year/2015"}, {"n": "2014", "v": "/year/2014"}, {"n": "2013", "v": "/year/2013"}, {"n": "2012", "v": "/year/2012"}, {"n": "2011", "v": "/year/2011"}, {"n": "2010", "v": "/year/2010"}, {"n": "2009", "v": "/year/2009"}, {"n": "2008", "v": "/year/2008"}, {"n": "2007", "v": "/year/2007"}, {"n": "2006", "v": "/year/2006"}, {"n": "2005", "v": "/year/2005"}, {"n": "2004", "v": "/year/2004"}]}, resp = requests.post(url, data=params or {}, headers=self.header,
{"key": "lang", "name": "语言", "value": [{"n": "全部", "v": ""}, {"n": "国语", "v": "/lang/国语"}, {"n": "英语", "v": "/lang/英语"}, {"n": "粤语", "v": "/lang/粤语"}, {"n": "韩语", "v": "/lang/韩语"}, {"n": "日语", "v": "/lang/日语"}, {"n": "其它", "v": "/lang/其它"}]}, timeout=self.timeout, verify=False)
{"key": "by", "name": "排序", "value": [{"n": "全部", "v": ""}, {"n": "时间", "v": "/by/time"}, {"n": "人气", "v": "/by/hits"}, {"n": "评分", "v": "/by/score"}]} resp.encoding = 'utf-8'
], if resp.status_code != 200:
"3": [ print(f"[hktv] {endpoint} HTTP {resp.status_code}")
{"key": "class", "name": "剧情", "value": [{"n": "全部", "v": ""}, {"n": "选秀", "v": "/class/选秀"}, {"n": "情感", "v": "/class/情感"}, {"n": "访谈", "v": "/class/访谈"}, {"n": "播报", "v": "/class/播报"}, {"n": "旅游", "v": "/class/旅游"}, {"n": "音乐", "v": "/class/音乐"}, {"n": "美食", "v": "/class/美食"}, {"n": "纪实", "v": "/class/纪实"}, {"n": "曲艺", "v": "/class/曲艺"}, {"n": "生活", "v": "/class/生活"}, {"n": "游戏互动", "v": "/class/游戏互动"}, {"n": "财经", "v": "/class/财经"}, {"n": "求职", "v": "/class/求职"}]}, return None
{"key": "area", "name": "地区", "value": [{"n": "全部", "v": ""}, {"n": "内地", "v": "/area/内地"}, {"n": "港台", "v": "/area/港台"}, {"n": "日韩", "v": "/area/日韩"}, {"n": "欧美", "v": "/area/欧美"}]}, try:
{"key": "lang", "name": "语言", "value": [{"n": "全部", "v": ""}, {"n": "国语", "v": "/lang/国语"}, {"n": "英语", "v": "/lang/英语"}, {"n": "粤语", "v": "/lang/粤语"}, {"n": "韩语", "v": "/lang/韩语"}, {"n": "日语", "v": "/lang/日语"}, {"n": "其它", "v": "/lang/其它"}]}, obj = resp.json()
{"key": "year", "name": "年份", "value": [{"n": "全部", "v": ""}, {"n": "2026", "v": "/year/2026"}, {"n": "2025", "v": "/year/2025"}, {"n": "2024", "v": "/year/2024"}, {"n": "2023", "v": "/year/2023"}, {"n": "2022", "v": "/year/2022"}, {"n": "2021", "v": "/year/2021"}, {"n": "2020", "v": "/year/2020"}, {"n": "2019", "v": "/year/2019"}, {"n": "2018", "v": "/year/2018"}, {"n": "2017", "v": "/year/2017"}, {"n": "2016", "v": "/year/2016"}, {"n": "2015", "v": "/year/2015"}, {"n": "2014", "v": "/year/2014"}, {"n": "2013", "v": "/year/2013"}, {"n": "2012", "v": "/year/2012"}, {"n": "2011", "v": "/year/2011"}, {"n": "2010", "v": "/year/2010"}, {"n": "2009", "v": "/year/2009"}, {"n": "2008", "v": "/year/2008"}, {"n": "2007", "v": "/year/2007"}, {"n": "2006", "v": "/year/2006"}, {"n": "2005", "v": "/year/2005"}, {"n": "2004", "v": "/year/2004"}]}, except Exception:
{"key": "by", "name": "排序", "value": [{"n": "全部", "v": ""}, {"n": "时间", "v": "/by/time"}, {"n": "人气", "v": "/by/hits"}, {"n": "评分", "v": "/by/score"}]} m = re.search(r'\{.*\}', resp.text, re.S)
], obj = json.loads(m.group(0)) if m else None
"4": [ if not obj:
{"key": "class", "name": "剧情", "value": [{"n": "全部", "v": ""}, {"n": "情感", "v": "/class/情感"}, {"n": "科幻", "v": "/class/科幻"}, {"n": "热血", "v": "/class/热血"}, {"n": "推理", "v": "/class/推理"}, {"n": "搞笑", "v": "/class/搞笑"}, {"n": "冒险", "v": "/class/冒险"}, {"n": "萝莉", "v": "/class/萝莉"}, {"n": "校园", "v": "/class/校园"}, {"n": "动作", "v": "/class/动作"}, {"n": "机战", "v": "/class/机战"}, {"n": "运动", "v": "/class/运动"}, {"n": "战争", "v": "/class/战争"}, {"n": "少年", "v": "/class/少年"}, {"n": "少女", "v": "/class/少女"}, {"n": "社会", "v": "/class/社会"}, {"n": "原创", "v": "/class/原创"}, {"n": "亲子", "v": "/class/亲子"}, {"n": "益智", "v": "/class/益智"}, {"n": "励志", "v": "/class/励志"}, {"n": "其它", "v": "/class/其他"}]}, return None
{"key": "area", "name": "地区", "value": [{"n": "全部", "v": ""}, {"n": "国产", "v": "/area/国产"}, {"n": "日本", "v": "/area/日本"}, {"n": "欧美", "v": "/area/欧美"}, {"n": "Other", "v": "/area/Other"}]}, plain = _decrypt_data(obj.get('data'))
{"key": "lang", "name": "语言", "value": [{"n": "全部", "v": ""}, {"n": "国语", "v": "/lang/国语"}, {"n": "英语", "v": "/lang/英语"}, {"n": "粤语", "v": "/lang/粤语"}, {"n": "韩语", "v": "/lang/韩语"}, {"n": "日语", "v": "/lang/日语"}, {"n": "其它", "v": "/lang/其它"}]}, if not plain:
{"key": "year", "name": "年份", "value": [{"n": "全部", "v": ""}, {"n": "2026", "v": "/year/2026"}, {"n": "2025", "v": "/year/2025"}, {"n": "2024", "v": "/year/2024"}, {"n": "2023", "v": "/year/2023"}, {"n": "2022", "v": "/year/2022"}, {"n": "2021", "v": "/year/2021"}, {"n": "2020", "v": "/year/2020"}, {"n": "2019", "v": "/year/2019"}, {"n": "2018", "v": "/year/2018"}, {"n": "2017", "v": "/year/2017"}, {"n": "2016", "v": "/year/2016"}, {"n": "2015", "v": "/year/2015"}, {"n": "2014", "v": "/year/2014"}, {"n": "2013", "v": "/year/2013"}, {"n": "2012", "v": "/year/2012"}, {"n": "2011", "v": "/year/2011"}, {"n": "2010", "v": "/year/2010"}, {"n": "2009", "v": "/year/2009"}, {"n": "2008", "v": "/year/2008"}, {"n": "2007", "v": "/year/2007"}, {"n": "2006", "v": "/year/2006"}, {"n": "2005", "v": "/year/2005"}, {"n": "2004", "v": "/year/2004"}]}, return None
{"key": "by", "name": "排序", "value": [{"n": "全部", "v": ""}, {"n": "时间", "v": "/by/time"}, {"n": "人气", "v": "/by/hits"}, {"n": "评分", "v": "/by/score"}]} return json.loads(plain)
] except Exception as e:
print(f"[hktv] request {endpoint} failed: {e}")
return None
@staticmethod
def _pic(pic):
if not pic:
return ''
return pic if pic.startswith('http') else pic
@staticmethod
def _vod_item(v):
pic = v.get('vod_pic') or v.get('vod_pic_slide') or ''
return {
'vod_id': str(v.get('vod_id', '') or ''),
'vod_name': v.get('vod_name', '') or '',
'vod_pic': Spider._pic(pic),
'vod_remarks': v.get('vod_remarks', '') or '',
} }
# ------------------------------------------------------------------ #
# 框架接口(与 tvb.py 完全一致)
# ------------------------------------------------------------------ #
def homeContent(self, filter): def homeContent(self, filter):
"""
获取首页内容
"""
try: try:
result = {'class': [], 'list': []} result = {'class': [], 'list': []}
init = self._api('init')
# 添加分类 if init:
# 分类
for c in init.get('type_list', []):
tid = str(c.get('type_id', ''))
tname = c.get('type_name', '')
if not tid or not tname:
continue
if tid == '0' and tname == '全部':
continue # 跳过“全部”虚拟项
self.class_names.append(tname)
self.class_urls.append(tid)
# 解析筛选维度
ext = c.get('type_extend', '')
if ext:
try:
self.type_extend[tid] = json.loads(ext)
except Exception:
pass
# 首页推荐
for v in init.get('recommend_list', []):
item = self._vod_item(v)
if item['vod_id'] and item['vod_name']:
result['list'].append(item)
for name, cid in zip(self.class_names, self.class_urls): for name, cid in zip(self.class_names, self.class_urls):
result['class'].append({ result['class'].append({'type_name': name, 'type_id': cid})
'type_name': name,
'type_id': cid
})
# 如果启用了筛选功能,添加筛选条件
if filter: if filter:
result['filters'] = {} result['filters'] = {}
for cid in self.class_urls: for cid in self.class_urls:
result['filters'][cid] = self.get_filter_data(cid) result['filters'][cid] = self.get_filter_data(cid)
# 获取推荐内容
url = f"{self.host}/index.php/vod/show/id/1.html"
response = self.fetch(url)
if response:
data = pq(response)
# 使用推荐选择器: ul.myui-vodlist;li;*;*;*;*
ul_items = data('ul.myui-vodlist li')
for item in ul_items.items():
title = item('a:first').attr('title') or ''
pic = item('.lazyload').attr('data-original') or ''
if pic and not pic.startswith('http'):
pic = self.host + pic
href = item('a:first').attr('href') or ''
if href and not href.startswith('http'):
href = self.host + href
remarks = item('.tag').text() or ''
if title and href:
result['list'].append({
'vod_id': self.get_id_from_href(href),
'vod_name': title,
'vod_pic': pic,
'vod_remarks': remarks
})
return result return result
except Exception as e: except Exception as e:
print(f"Error in homeContent: {str(e)}") print(f"Error in homeContent: {e}")
import traceback import traceback
traceback.print_exc() traceback.print_exc()
return {'class': [], 'list': []} return {'class': [], 'list': []}
def get_filter_data(self, tid): def get_filter_data(self, tid):
""" """
获取筛选数据,与JS规则一致,动态从页面获取筛选条件 筛选维度来自 init 的 type_extend(真实数据);无则用预定义兜底。
维度:class(类型)/area(地区)/lang(语言)/year(年份)/sort(排序)
""" """
try: try:
# 根据JS中的filter_map直接返回对应的筛选条件 ext = self.type_extend.get(str(tid))
# 因为JS规则中是预定义的筛选条件,所以我们根据分类ID返回对应的预定义条件 if ext:
if tid in self.filter_map: out = []
return self.filter_map[tid] label_map = {'class': '类型', 'area': '地区', 'lang': '语言',
elif tid == '13': # 国产剧 'year': '年份', 'sort': '排序', 'star': '明星',
return self.filter_map["2"] 'director': '导演', 'state': '状态', 'version': '版本'}
elif tid == '14': # 港台剧 for key, label in label_map.items():
return self.filter_map["2"] vals = ext.get(key)
elif tid == '15': # 日韩剧 if not vals:
return self.filter_map["2"] continue
elif tid == '16': # 欧美剧 items = [{'n': '全部', 'v': ''}]
return self.filter_map["2"] for v in vals.split(','):
else: v = v.strip()
# 默认返回电影的筛选条件 if v:
return self.filter_map["1"] items.append({'n': v, 'v': v})
out.append({'key': key, 'name': label, 'value': items})
if out:
return out
# 兜底
return [
{'key': 'class', 'name': '类型', 'value': [{'n': '全部', 'v': ''}, {'n': '动作', 'v': '动作'}, {'n': '喜剧', 'v': '喜剧'}, {'n': '爱情', 'v': '爱情'}, {'n': '科幻', 'v': '科幻'}, {'n': '剧情', 'v': '剧情'}, {'n': '犯罪', 'v': '犯罪'}, {'n': '奇幻', 'v': '奇幻'}]},
{'key': 'area', 'name': '地区', 'value': [{'n': '全部', 'v': ''}, {'n': '大陆', 'v': '大陆'}, {'n': '香港', 'v': '香港'}, {'n': '台湾', 'v': '台湾'}, {'n': '美国', 'v': '美国'}, {'n': '日本', 'v': '日本'}, {'n': '韩国', 'v': '韩国'}]},
{'key': 'lang', 'name': '语言', 'value': [{'n': '全部', 'v': ''}, {'n': '国语', 'v': '国语'}, {'n': '英语', 'v': '英语'}, {'n': '粤语', 'v': '粤语'}]},
{'key': 'year', 'name': '年份', 'value': [{'n': '全部', 'v': ''}, {'n': '2026', 'v': '2026'}, {'n': '2025', 'v': '2025'}, {'n': '2024', 'v': '2024'}, {'n': '2023', 'v': '2023'}]},
{'key': 'sort', 'name': '排序', 'value': [{'n': '全部', 'v': ''}, {'n': '时间', 'v': 'time'}, {'n': '人气', 'v': 'hits'}, {'n': '评分', 'v': 'score'}]},
]
except Exception as e: except Exception as e:
print(f"Error in get_filter_data: {str(e)}") print(f"Error in get_filter_data: {e}")
return [] return []
def categoryContent(self, tid, pg, filter, extend): def categoryContent(self, tid, pg, filter, extend):
"""
获取分类内容
对应JS中的url: 'vod/show/id/fyfilter.html'
"""
try: try:
# 构建过滤URL params = {'type_id': tid, 'page': pg}
filter_str = ""
if extend: if extend:
for key, value in extend.items(): for key in ('class', 'area', 'lang', 'year', 'sort'):
if value: val = extend.get(key)
filter_str += value if val:
params[key] = val
url = f"{self.host}/index.php/vod/show/id/{tid}{filter_str}/page/{pg}.html" data = self._api('typeFilterVodList', params)
response = self.fetch(url) if not data:
return {'list': [], 'page': int(pg), 'pagecount': 0,
if not response: 'limit': 0, 'total': 0}
return {'list': [], 'page': pg, 'pagecount': 0, 'limit': 0, 'total': 0}
data = pq(response) rows = data.get('recommend_list') or []
videos = [] videos = [self._vod_item(v) for v in rows
if v.get('vod_id') and v.get('vod_name')]
# 使用一级选择器: .myui-vodlist__box;a&&title;.lazyload&&data-original;.tag&&Text;a&&href seen = set()
items = data('.myui-vodlist__box, ul.myui-vodlist li') uniq = []
for item in items.items(): for v in videos:
title = item('a').attr('title') or item('a').text() if v['vod_id'] not in seen:
pic = item('.lazyload').attr('data-original') seen.add(v['vod_id'])
if pic and not pic.startswith('http'): uniq.append(v)
pic = self.host + pic
remarks = item('.tag').text() or item('.pic-text').text() or ''
href = item('a').attr('href')
if href and not href.startswith('http'):
href = self.host + href
if title and href:
# 检查是否已有相同ID的视频,避免重复
vid = self.get_id_from_href(href)
if not any(v['vod_id'] == vid for v in videos):
videos.append({
'vod_id': vid,
'vod_name': title,
'vod_pic': pic,
'vod_remarks': remarks
})
total = data.get('total', len(uniq))
pagecount = data.get('page_count', data.get('pagecount', 999))
return { return {
'list': videos, 'list': uniq,
'page': int(pg), 'page': int(pg),
'pagecount': 999, # 假设很多页 'pagecount': pagecount,
'limit': len(videos), 'limit': len(uniq),
'total': 999999 'total': total,
} }
except Exception as e: except Exception as e:
print(f"Error in categoryContent: {str(e)}") print(f"Error in categoryContent: {e}")
import traceback import traceback
traceback.print_exc() traceback.print_exc()
return {'list': [], 'page': pg, 'pagecount': 0, 'limit': 0, 'total': 0} return {'list': [], 'page': int(pg), 'pagecount': 0,
'limit': 0, 'total': 0}
def detailContent(self, ids): def detailContent(self, ids):
"""
获取详情内容
对应JS中的二级规则
"""
try: try:
if not ids or not ids[0]: if not ids or not ids[0]:
return {'list': []} return {'list': []}
vid = ids[0] vid = ids[0]
url = f"{self.host}/index.php/vod/detail/id/{vid}.html" data = self._api('vodDetail', {'vod_id': vid})
response = self.fetch(url) if not data:
if not response:
return {'list': []} return {'list': []}
data = pq(response) v = data.get('vod') or {}
if not isinstance(v, dict):
return {'list': []}
# 解析详情页
vod = { vod = {
'vod_id': vid, 'vod_id': str(v.get('vod_id', vid)),
'vod_name': data('h1:first').text() or data('.data a:first').text(), 'vod_name': v.get('vod_name', '') or '',
'vod_pic': '', 'vod_pic': self._pic(v.get('vod_pic') or v.get('vod_pic_slide') or ''),
'vod_remarks': '', 'vod_remarks': v.get('vod_remarks', '') or '',
'vod_year': '', 'vod_year': v.get('vod_year', '') or '',
'vod_area': '', 'vod_area': v.get('vod_area', '') or '',
'vod_director': '', 'vod_actor': v.get('vod_actor', '') or '',
'vod_actor': '', 'vod_director': v.get('vod_director', '') or '',
'vod_content': '' 'vod_content': v.get('vod_content', '') or '',
} }
# 图片 # 线路:from_list 名 + url_list 集(url 在 vodParse 中解密)
img = data('.lazyload').attr('data-original') from_list = data.get('vod_play_from_list') or []
if img: url_list = data.get('vod_play_url_list') or []
if not img.startswith('http'): src_list = data.get('player_source_list') or []
img = self.host + img # player_code -> player_source_id
vod['vod_pic'] = img code2id = {s.get('player_code'): s.get('id') for s in src_list if s.get('player_code')}
# 描述信息
data_items = data('.data').items()
data_list = list(data_items)
if len(data_list) >= 1:
# 提取各种信息
data_text = data('.data').text()
# 年份、地区、导演、演员等信息通常在.data内
# 根据JS规则: ";.data:eq(0) a:eq(2)&&Text;.data:eq(0) a:eq(1)&&Text;.data:eq(2)&&Text;.data:eq(3)&&Text"
if len(data_list) > 0:
links = data_list[0]('a')
if len(links) > 2:
# 假设第3个链接是年代,第2个是地区,第1个是演员(反向对应)
if len(links) > 2:
vod['vod_year'] = links.eq(2).text()
if len(links) > 1:
vod['vod_area'] = links.eq(1).text()
if len(links) > 0:
vod['vod_actor'] = links.eq(0).text()
# 内容描述
content_elem = data('.text-collapse span')
if content_elem:
vod['vod_content'] = content_elem.text()
# 处理播放线路和播放列表,确保每个线路分开
play_from = [] play_from = []
play_urls = [] play_url = []
for idx, src in enumerate(from_list):
psid = code2id.get(src, '')
episodes = []
if idx < len(url_list):
for ep in url_list[idx].get('urls', []):
# 编码: 名称$player_source_id@episode_index@vod_id
episodes.append(f"{ep.get('name','')}${psid}@{ep.get('episode_index','')}@{vid}")
play_from.append(_line_cn(src))
play_url.append('#'.join(episodes))
# 查找所有播放源标签 vod['vod_play_from'] = '$$$'.join(play_from)
tabs = data('.myui-panel__head h3') vod['vod_play_url'] = '$$$'.join(play_url)
for tab in tabs.items(): # 保存解析所需的 id 映射,供 playerContent 使用
tab_text = tab.text().strip() vod['_player_source'] = {src: code2id.get(src) for src in play_from}
if tab_text and any(keyword in tab_text.lower() for keyword in ['播放', '线路', '云播', '在线', '资源', '高清', '极速', '备用', '专享', '秒播']): vod['_vod_id'] = vid
play_from.append(tab_text) return {'list': [vod]}
# 获取所有播放列表
all_lists = data('.myui-content__list')
for idx in range(len(play_from)):
urls = []
list_items = all_lists.eq(idx)('li') if idx < len(all_lists) else []
for item in list_items.items():
a = item('a')
href = a.attr('href')
if href and not href.startswith('http'):
href = self.host + href
title = a.text().strip()
if title and href:
urls.append(f"{title}${href}")
play_urls.append('#'.join(urls))
# 如果没有找到任何播放线路,尝试获取所有列表作为一个播放线路
if not play_from or not any(play_urls):
urls = []
list_items = data('.myui-content__list:first li')
for item in list_items.items():
a = item('a')
href = a.attr('href')
if href and not href.startswith('http'):
href = self.host + href
title = a.text().strip()
if title and href:
urls.append(f"{title}${href}")
if urls:
play_from = ['播放源']
play_urls = ['#'.join(urls)]
# 补齐长度,确保一一对应
if len(play_urls) < len(play_from):
while len(play_urls) < len(play_from):
play_urls.append('')
elif len(play_from) < len(play_urls):
play_urls = play_urls[:len(play_from)]
# 过滤掉空线路和空地址,确保前端能正确识别独立线路
play_from_clean = []
play_urls_clean = []
for f, u in zip(play_from, play_urls):
if f.strip() and u.strip():
play_from_clean.append(f.strip())
play_urls_clean.append(u.strip())
if play_from_clean and play_urls_clean:
vod['vod_play_from'] = '$$$'.join(play_from_clean)
vod['vod_play_url'] = '$$$'.join(play_urls_clean)
else:
vod['vod_play_from'] = 'TVB云播'
vod['vod_play_url'] = '播放$' + url
return {"list": [vod]}
except Exception as e: except Exception as e:
print(f"Error in detailContent: {str(e)}") print(f"Error in detailContent: {e}")
import traceback
traceback.print_exc()
return {"list": []}
def searchContent(self, key, quick, pg="1"):
"""
搜索内容
"""
try:
url = f"{self.host}/index.php/vod/search.html?wd={key}&submit="
response = self.fetch(url)
if not response:
return {'list': []}
data = pq(response)
videos = []
# 使用搜索选择器: 'ul.myui-vodlist__media li;*;*;*;*'
items = data('ul.myui-vodlist__media li, ul.myui-vodlist li')
for item in items.items():
title = item('a:first').attr('title') or item('a:first').text()
pic = item('.lazyload').attr('data-original')
if pic and not pic.startswith('http'):
pic = self.host + pic
href = item('a:first').attr('href')
if href and not href.startswith('http'):
href = self.host + href
remarks = item('.tag').text() or item('.pic-text').text() or ''
if title and href:
# 检查是否已有相同ID的视频,避免重复
vid = self.get_id_from_href(href)
if not any(v['vod_id'] == vid for v in videos):
videos.append({
'vod_id': vid,
'vod_name': title,
'vod_pic': pic,
'vod_remarks': remarks
})
return {'list': videos}
except Exception as e:
print(f"Error in searchContent: {str(e)}")
import traceback import traceback
traceback.print_exc() traceback.print_exc()
return {'list': []} return {'list': []}
def playerContent(self, flag, id, vipFlags): def searchContent(self, key, quick, pg="1"):
try:
# 真实搜索接口:searchList,参数 keywords / type_id / page
data = self._api('searchList', {'keywords': key, 'type_id': '0', 'page': pg})
if not data:
return {'list': []}
rows = data.get('search_list') or data.get('recommend_list') or []
videos = []
seen = set()
for v in rows:
item = self._vod_item(v)
if item['vod_id'] and item['vod_name'] and item['vod_id'] not in seen:
seen.add(item['vod_id'])
videos.append(item)
return {'list': videos}
except Exception as e:
print(f"Error in searchContent: {e}")
import traceback
traceback.print_exc()
return {'list': []}
def searchSuggestions(self, keyword):
""" """
获取播放URL 搜索联想(实时补全)。参数 keyword。
与 searchContent 共用同一套解密;返回建议名称列表。
""" """
try: try:
# 直接返回播放页URL,让播放器自行处理 data = self._api('searchSuggestions', {'keyword': keyword})
return { if not data:
'parse': 1, return []
'url': id, out = []
'header': self.header, for it in data.get('list', []) or []:
'playUrl': '' if it.get('vod_name'):
} out.append(it['vod_name'])
return out
except Exception as e: except Exception as e:
print(f"Error in playerContent: {str(e)}") print(f"Error in searchSuggestions: {e}")
return { return []
'parse': 0,
'url': '', def playerContent(self, flag, id, vipFlags):
'header': {}, """
'playUrl': '' detailContent 中每集被编码为 "name$psid@epidx@vodid"
} 框架传入的 id 形态为 "psid@epidx@vodid"(不含名称)。
经 vodParse 接口用 (vod_id, player_source_id, episode_index) 换取真实 play_url。
"""
try:
if not id or id.count('@') < 2:
return {'parse': 0, 'url': '', 'header': {}, 'playUrl': ''}
psid, ep_idx, vid = id.split('@', 2)
data = self._api('vodParse', {
'vod_id': vid,
'player_source_id': psid,
'episode_index': ep_idx,
})
if data and data.get('play_url'):
return {
'parse': 1 if data['play_url'].startswith('http') else 0,
'url': data['play_url'],
'header': self.play_header,
'playUrl': '',
}
return {'parse': 0, 'url': '', 'header': {}, 'playUrl': ''}
except Exception as e:
print(f"Error in playerContent: {e}")
return {'parse': 0, 'url': '', 'header': {}, 'playUrl': ''}
def fetch(self, url): def fetch(self, url):
"""
发送HTTP请求
"""
try: try:
response = requests.get(url, headers=self.header, timeout=self.timeout) response = requests.get(url, headers=self.header, timeout=self.timeout)
response.encoding = 'utf-8' response.encoding = 'utf-8'
if response.status_code == 200: return response.text if response.status_code == 200 else None
return response.text
return None
except Exception as e: except Exception as e:
print(f"Error fetching {url}: {str(e)}") print(f"Error fetching {url}: {e}")
return None return None
def get_id_from_href(self, href): def get_id_from_href(self, href):
""" if not href:
从链接中提取ID return href
""" m = re.search(r'[?&]id=(\d+)', href)
# 例如从 /index.php/vod/detail/id/123.html 提取 123 if m:
match = re.search(r'/id/(\d+)', href) return m.group(1)
if match: m = re.search(r'/id/(\d+)', href)
return match.group(1) if m:
# 或者从 /vod-detail-id-123.html 提取 123 return m.group(1)
match = re.search(r'-id-(\d+)', href) return href
if match:
return match.group(1)
return href
+1
View File
@@ -14,6 +14,7 @@
{"key":"fyyy","name":"飞宇影院","type":3,"api":"csp_XBPQ","searchable":1,"quickSearch":1,"filterable":1,"changeable":1,"ext":{"分类url":"http://ntfeiyu.com/nt/{cateId}/area/{area}/by/{by}/class/{class}/lang/{lang}/page/{catePg}/year/{year}.html","分类":"电影$1#电视剧$2#综艺$3#动漫$4"},"header":{"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0"}}, {"key":"fyyy","name":"飞宇影院","type":3,"api":"csp_XBPQ","searchable":1,"quickSearch":1,"filterable":1,"changeable":1,"ext":{"分类url":"http://ntfeiyu.com/nt/{cateId}/area/{area}/by/{by}/class/{class}/lang/{lang}/page/{catePg}/year/{year}.html","分类":"电影$1#电视剧$2#综艺$3#动漫$4"},"header":{"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0"}},
{"key":"cupfox_in","name":"茶杯狐┃cupfox.in","type":3,"api":"./lib/drpy2.min.js","ext":"./js/茶杯狐.js","searchable":1,"quickSearch":1,"filterable":0,"changeable":1,"header":{"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0"}}, {"key":"cupfox_in","name":"茶杯狐┃cupfox.in","type":3,"api":"./lib/drpy2.min.js","ext":"./js/茶杯狐.js","searchable":1,"quickSearch":1,"filterable":0,"changeable":1,"header":{"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0"}},
{"key":"italkbbtv","name":"ITalkBB | 外","type":3,"api":"./py/ITalkBBTV.py","searchable":1,"quickSearch":1,"filterable":0,"changeable":1}, {"key":"italkbbtv","name":"ITalkBB | 外","type":3,"api":"./py/ITalkBBTV.py","searchable":1,"quickSearch":1,"filterable":0,"changeable":1},
{"key": "TVB云播","name": "TVB云播","type": 3,"api": "./py/TVB云播.py","searchable": 1,"quickSearch": 1,"filterable": 0,"changeable": 1},
{"key":"qiletv","name":"奇乐影视","type":3,"api":"csp_XBPQ","searchable":1,"quickSearch":1,"filterable":1,"changeable":1,"ext":"./json/奇乐.json","header":{"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0"}}, {"key":"qiletv","name":"奇乐影视","type":3,"api":"csp_XBPQ","searchable":1,"quickSearch":1,"filterable":1,"changeable":1,"ext":"./json/奇乐.json","header":{"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0"}},
+1
View File
@@ -15,6 +15,7 @@
{"key":"fyyy","name":"飞宇影院","type":3,"api":"csp_XBPQ","searchable":1,"quickSearch":1,"filterable":1,"changeable":1,"ext":{"分类url":"http://ntfeiyu.com/nt/{cateId}/area/{area}/by/{by}/class/{class}/lang/{lang}/page/{catePg}/year/{year}.html","分类":"电影$1#电视剧$2#综艺$3#动漫$4"},"header":{"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0"}}, {"key":"fyyy","name":"飞宇影院","type":3,"api":"csp_XBPQ","searchable":1,"quickSearch":1,"filterable":1,"changeable":1,"ext":{"分类url":"http://ntfeiyu.com/nt/{cateId}/area/{area}/by/{by}/class/{class}/lang/{lang}/page/{catePg}/year/{year}.html","分类":"电影$1#电视剧$2#综艺$3#动漫$4"},"header":{"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0"}},
{"key":"cupfox_in","name":"茶杯狐┃cupfox.in","type":3,"api":"./lib/drpy2.min.js","ext":"./js/茶杯狐.js","searchable":1,"quickSearch":1,"filterable":0,"changeable":1,"header":{"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0"}}, {"key":"cupfox_in","name":"茶杯狐┃cupfox.in","type":3,"api":"./lib/drpy2.min.js","ext":"./js/茶杯狐.js","searchable":1,"quickSearch":1,"filterable":0,"changeable":1,"header":{"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0"}},
{"key":"italkbbtv","name":"ITalkBB|外","type":3,"api":"./py/ITalkBBTV.py","searchable":1,"quickSearch":1,"filterable":0,"changeable":1}, {"key":"italkbbtv","name":"ITalkBB|外","type":3,"api":"./py/ITalkBBTV.py","searchable":1,"quickSearch":1,"filterable":0,"changeable":1},
{"key": "TVB云播","name": "TVB云播","type": 3,"api": "./py/TVB云播.py","searchable": 1,"quickSearch": 1,"filterable": 0,"changeable": 1},
{"key":"qiletv","name":"奇乐影视","type":3,"api":"csp_XBPQ","searchable":1,"quickSearch":1,"filterable":1,"changeable":1,"ext":"./json/奇乐.json","header":{"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0"}}, {"key":"qiletv","name":"奇乐影视","type":3,"api":"csp_XBPQ","searchable":1,"quickSearch":1,"filterable":1,"changeable":1,"ext":"./json/奇乐.json","header":{"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0"}},
{"key": "电影天堂","name": "影视 | 电影天堂","type": 1,"api": "http://caiji.dyttzyapi.com/api.php/provide/vod","searchable": 1,"quickSearch": 1,"filterable": 1,"changeable": 1,"playerType":2}, {"key": "电影天堂","name": "影视 | 电影天堂","type": 1,"api": "http://caiji.dyttzyapi.com/api.php/provide/vod","searchable": 1,"quickSearch": 1,"filterable": 1,"changeable": 1,"playerType":2},