diff --git a/py/TVB云播.py b/py/TVB云播.py index ab5416ff..c46bea3f 100644 --- a/py/TVB云播.py +++ b/py/TVB云播.py @@ -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 \ No newline at end of file + 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 diff --git a/tools/dianshi.json b/tools/dianshi.json index b8df304b..9156270f 100644 --- a/tools/dianshi.json +++ b/tools/dianshi.json @@ -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"}}, diff --git a/tools/jsm.json b/tools/jsm.json index 62bf500b..6b12a4c2 100644 --- a/tools/jsm.json +++ b/tools/jsm.json @@ -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},