添加 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 json
import base64
import zlib
import requests
from pyquery import PyQuery as pq
from bs4 import BeautifulSoup
from Crypto.Cipher import AES
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):
return 'TVB云播'
return 'HKTV影视'
def init(self, extend=""):
pass
if extend and extend.strip():
self.host = extend.strip().rstrip('/')
return self.host
def __init__(self):
self.name = 'TVB云播'
self.host = 'http://www.viptvb06.com'
self.name = 'HKTV影视'
self.host = 'http://app.tuxianimg.com'
self.timeout = 25
# 注:Host / Content-Length 由 requests 按 URL 与 body 自动生成,不在此手动设置。
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',
}
# 分类映射
self.class_names = '电影&电视剧&综艺&动漫&日韩剧&国产剧&欧美剧&港台剧'.split('&')
self.class_urls = '1&2&3&4&16&13&15&14'.split('&')
# 过滤条件映射
self.filter_map = {
"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"}]},
{"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/网络电影"}]},
{"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"}]},
{"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/其它"}]},
{"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"}]}
],
"2": [
{"key": "cateId", "name": "类型", "value": [{"n": "全部", "v": "2"}, {"n": "国产剧", "v": "13"}, {"n": "港台剧", "v": "14"}, {"n": "日韩剧", "v": "15"}, {"n": "欧美剧", "v": "16"}]},
{"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/其他"}]},
{"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"}]},
{"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": "lang", "name": "语言", "value": [{"n": "全部", "v": ""}, {"n": "国语", "v": "/lang/国语"}, {"n": "英语", "v": "/lang/英语"}, {"n": "粤语", "v": "/lang/粤语"}, {"n": "韩语", "v": "/lang/韩语"}, {"n": "日语", "v": "/lang/日语"}, {"n": "其它", "v": "/lang/其它"}]},
{"key": "by", "name": "排序", "value": [{"n": "全部", "v": ""}, {"n": "时间", "v": "/by/time"}, {"n": "人气", "v": "/by/hits"}, {"n": "评分", "v": "/by/score"}]}
],
"3": [
{"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/求职"}]},
{"key": "area", "name": "地区", "value": [{"n": "全部", "v": ""}, {"n": "内地", "v": "/area/内地"}, {"n": "港台", "v": "/area/港台"}, {"n": "日韩", "v": "/area/日韩"}, {"n": "欧美", "v": "/area/欧美"}]},
{"key": "lang", "name": "语言", "value": [{"n": "全部", "v": ""}, {"n": "国语", "v": "/lang/国语"}, {"n": "英语", "v": "/lang/英语"}, {"n": "粤语", "v": "/lang/粤语"}, {"n": "韩语", "v": "/lang/韩语"}, {"n": "日语", "v": "/lang/日语"}, {"n": "其它", "v": "/lang/其它"}]},
{"key": "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"}]}
],
"4": [
{"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/其他"}]},
{"key": "area", "name": "地区", "value": [{"n": "全部", "v": ""}, {"n": "国产", "v": "/area/国产"}, {"n": "日本", "v": "/area/日本"}, {"n": "欧美", "v": "/area/欧美"}, {"n": "Other", "v": "/area/Other"}]},
{"key": "lang", "name": "语言", "value": [{"n": "全部", "v": ""}, {"n": "国语", "v": "/lang/国语"}, {"n": "英语", "v": "/lang/英语"}, {"n": "粤语", "v": "/lang/粤语"}, {"n": "韩语", "v": "/lang/韩语"}, {"n": "日语", "v": "/lang/日语"}, {"n": "其它", "v": "/lang/其它"}]},
{"key": "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"}]}
]
# 由 playerContent 作为“全局播放 header”返回,框架会透传到所有直链请求。
# 注:host 字段由 requests/框架按 URL 自动生成,不在此手动设置(否则跨域名会错)。
self.play_header = {
'User-Agent': 'Dalvik/2.1.0 (Linux; U; Android 15; ZXV10S100V7 Build/b5cc949.1)',
'accept-encoding': 'gzip',
'allowcrossprotocolredirects': 'true',
'connection': 'Keep-Alive',
}
# 运行时由 init 接口填充
self.class_names = []
self.class_urls = []
self.type_extend = {} # type_id -> {class/area/lang/year/sort 选项}
# ------------------------------------------------------------------ #
# 内部:统一 API 请求 + 解密
# ------------------------------------------------------------------ #
def _api(self, endpoint, params=None):
url = f"{self.host}/api/vod/{endpoint}"
try:
resp = requests.post(url, data=params or {}, headers=self.header,
timeout=self.timeout, verify=False)
resp.encoding = 'utf-8'
if resp.status_code != 200:
print(f"[hktv] {endpoint} HTTP {resp.status_code}")
return None
try:
obj = resp.json()
except Exception:
m = re.search(r'\{.*\}', resp.text, re.S)
obj = json.loads(m.group(0)) if m else None
if not obj:
return None
plain = _decrypt_data(obj.get('data'))
if not plain:
return None
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):
"""
获取首页内容
"""
try:
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):
result['class'].append({
'type_name': name,
'type_id': cid
})
result['class'].append({'type_name': name, 'type_id': cid})
# 如果启用了筛选功能,添加筛选条件
if filter:
result['filters'] = {}
for cid in self.class_urls:
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
except Exception as e:
print(f"Error in homeContent: {str(e)}")
print(f"Error in homeContent: {e}")
import traceback
traceback.print_exc()
return {'class': [], 'list': []}
def get_filter_data(self, tid):
"""
获取筛选数据,与JS规则一致,动态从页面获取筛选条件
筛选维度来自 init 的 type_extend(真实数据);无则用预定义兜底。
维度:class(类型)/area(地区)/lang(语言)/year(年份)/sort(排序)
"""
try:
# 根据JS中的filter_map直接返回对应的筛选条件
# 因为JS规则中是预定义的筛选条件,所以我们根据分类ID返回对应的预定义条件
if tid in self.filter_map:
return self.filter_map[tid]
elif tid == '13': # 国产剧
return self.filter_map["2"]
elif tid == '14': # 港台剧
return self.filter_map["2"]
elif tid == '15': # 日韩剧
return self.filter_map["2"]
elif tid == '16': # 欧美剧
return self.filter_map["2"]
else:
# 默认返回电影的筛选条件
return self.filter_map["1"]
ext = self.type_extend.get(str(tid))
if ext:
out = []
label_map = {'class': '类型', 'area': '地区', 'lang': '语言',
'year': '年份', 'sort': '排序', 'star': '明星',
'director': '导演', 'state': '状态', 'version': '版本'}
for key, label in label_map.items():
vals = ext.get(key)
if not vals:
continue
items = [{'n': '全部', 'v': ''}]
for v in vals.split(','):
v = v.strip()
if v:
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:
print(f"Error in get_filter_data: {str(e)}")
print(f"Error in get_filter_data: {e}")
return []
def categoryContent(self, tid, pg, filter, extend):
"""
获取分类内容
对应JS中的url: 'vod/show/id/fyfilter.html'
"""
try:
# 构建过滤URL
filter_str = ""
params = {'type_id': tid, 'page': pg}
if extend:
for key, value in extend.items():
if value:
filter_str += value
for key in ('class', 'area', 'lang', 'year', 'sort'):
val = extend.get(key)
if val:
params[key] = val
url = f"{self.host}/index.php/vod/show/id/{tid}{filter_str}/page/{pg}.html"
response = self.fetch(url)
if not response:
return {'list': [], 'page': pg, 'pagecount': 0, 'limit': 0, 'total': 0}
data = self._api('typeFilterVodList', params)
if not data:
return {'list': [], 'page': int(pg), 'pagecount': 0,
'limit': 0, 'total': 0}
data = pq(response)
videos = []
rows = data.get('recommend_list') or []
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
items = data('.myui-vodlist__box, ul.myui-vodlist li')
for item in items.items():
title = item('a').attr('title') or item('a').text()
pic = item('.lazyload').attr('data-original')
if pic and not pic.startswith('http'):
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
})
seen = set()
uniq = []
for v in videos:
if v['vod_id'] not in seen:
seen.add(v['vod_id'])
uniq.append(v)
total = data.get('total', len(uniq))
pagecount = data.get('page_count', data.get('pagecount', 999))
return {
'list': videos,
'list': uniq,
'page': int(pg),
'pagecount': 999, # 假设很多页
'limit': len(videos),
'total': 999999
'pagecount': pagecount,
'limit': len(uniq),
'total': total,
}
except Exception as e:
print(f"Error in categoryContent: {str(e)}")
print(f"Error in categoryContent: {e}")
import traceback
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):
"""
获取详情内容
对应JS中的二级规则
"""
try:
if not ids or not ids[0]:
return {'list': []}
vid = ids[0]
url = f"{self.host}/index.php/vod/detail/id/{vid}.html"
response = self.fetch(url)
if not response:
data = self._api('vodDetail', {'vod_id': vid})
if not data:
return {'list': []}
data = pq(response)
v = data.get('vod') or {}
if not isinstance(v, dict):
return {'list': []}
# 解析详情页
vod = {
'vod_id': vid,
'vod_name': data('h1:first').text() or data('.data a:first').text(),
'vod_pic': '',
'vod_remarks': '',
'vod_year': '',
'vod_area': '',
'vod_director': '',
'vod_actor': '',
'vod_content': ''
'vod_id': str(v.get('vod_id', vid)),
'vod_name': v.get('vod_name', '') or '',
'vod_pic': self._pic(v.get('vod_pic') or v.get('vod_pic_slide') or ''),
'vod_remarks': v.get('vod_remarks', '') or '',
'vod_year': v.get('vod_year', '') or '',
'vod_area': v.get('vod_area', '') or '',
'vod_actor': v.get('vod_actor', '') or '',
'vod_director': v.get('vod_director', '') or '',
'vod_content': v.get('vod_content', '') or '',
}
# 图片
img = data('.lazyload').attr('data-original')
if img:
if not img.startswith('http'):
img = self.host + img
vod['vod_pic'] = img
# 线路:from_list 名 + url_list 集(url 在 vodParse 中解密)
from_list = data.get('vod_play_from_list') or []
url_list = data.get('vod_play_url_list') or []
src_list = data.get('player_source_list') or []
# player_code -> player_source_id
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_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))
# 查找所有播放源标签
tabs = data('.myui-panel__head h3')
for tab in tabs.items():
tab_text = tab.text().strip()
if tab_text and any(keyword in tab_text.lower() for keyword in ['播放', '线路', '云播', '在线', '资源', '高清', '极速', '备用', '专享', '秒播']):
play_from.append(tab_text)
# 获取所有播放列表
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]}
vod['vod_play_from'] = '$$$'.join(play_from)
vod['vod_play_url'] = '$$$'.join(play_url)
# 保存解析所需的 id 映射,供 playerContent 使用
vod['_player_source'] = {src: code2id.get(src) for src in play_from}
vod['_vod_id'] = vid
return {'list': [vod]}
except Exception as e:
print(f"Error in detailContent: {str(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)}")
print(f"Error in detailContent: {e}")
import traceback
traceback.print_exc()
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:
# 直接返回播放页URL,让播放器自行处理
return {
'parse': 1,
'url': id,
'header': self.header,
'playUrl': ''
}
data = self._api('searchSuggestions', {'keyword': keyword})
if not data:
return []
out = []
for it in data.get('list', []) or []:
if it.get('vod_name'):
out.append(it['vod_name'])
return out
except Exception as e:
print(f"Error in playerContent: {str(e)}")
return {
'parse': 0,
'url': '',
'header': {},
'playUrl': ''
}
print(f"Error in searchSuggestions: {e}")
return []
def playerContent(self, flag, id, vipFlags):
"""
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):
"""
发送HTTP请求
"""
try:
response = requests.get(url, headers=self.header, timeout=self.timeout)
response.encoding = 'utf-8'
if response.status_code == 200:
return response.text
return None
return response.text if response.status_code == 200 else None
except Exception as e:
print(f"Error fetching {url}: {str(e)}")
print(f"Error fetching {url}: {e}")
return None
def get_id_from_href(self, href):
"""
从链接中提取ID
"""
# 例如从 /index.php/vod/detail/id/123.html 提取 123
match = re.search(r'/id/(\d+)', href)
if match:
return match.group(1)
# 或者从 /vod-detail-id-123.html 提取 123
match = re.search(r'-id-(\d+)', href)
if match:
return match.group(1)
return href
if not href:
return href
m = re.search(r'[?&]id=(\d+)', href)
if m:
return m.group(1)
m = re.search(r'/id/(\d+)', href)
if m:
return m.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":"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": "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"}},
+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":"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": "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": "电影天堂","name": "影视 | 电影天堂","type": 1,"api": "http://caiji.dyttzyapi.com/api.php/provide/vod","searchable": 1,"quickSearch": 1,"filterable": 1,"changeable": 1,"playerType":2},