From 4f894c5cc8649582b7ff2018367b512994743717 Mon Sep 17 00:00:00 2001 From: XC-Pro <19840+xc-pro@noreply.example.org> Date: Thu, 13 Aug 2026 18:03:00 +0200 Subject: [PATCH] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E6=96=87=E4=BB=B6=E8=87=B3?= =?UTF-8?q?=E3=80=8Capi=E3=80=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/6v.py | 306 +++++++++++++++++++++++++++++ api/AppToV5.py | 187 ++++++++++++++++++ api/GZYS.py | 496 +++++++++++++++++++++++++++++++++++++++++++++++ api/erge_juhe.js | 310 +++++++++++++++++++++++++++++ 4 files changed, 1299 insertions(+) create mode 100644 api/6v.py create mode 100644 api/AppToV5.py create mode 100644 api/GZYS.py create mode 100644 api/erge_juhe.js diff --git a/api/6v.py b/api/6v.py new file mode 100644 index 0000000..ca83ecb --- /dev/null +++ b/api/6v.py @@ -0,0 +1,306 @@ +# coding=utf-8 +#!/usr/bin/python +import re +import sys +from html import unescape +from urllib.parse import urljoin, quote + +sys.path.append('..') +from base.spider import Spider + + +class Spider(Spider): + host = 'https://www.xb6v.org' + headers = { + 'User-Agent': 'Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 Chrome/120 Mobile Safari/537.36', + 'Referer': 'https://www.xb6v.org/', + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + 'Accept-Language': 'zh-CN,zh;q=0.9', + } + classes = [ + {'type_name': '首页', 'type_id': '/'}, + {'type_name': '喜剧片', 'type_id': '/xijupian/'}, + {'type_name': '动作片', 'type_id': '/dongzuopian/'}, + {'type_name': '爱情片', 'type_id': '/aiqingpian/'}, + {'type_name': '科幻片', 'type_id': '/kehuanpian/'}, + {'type_name': '恐怖片', 'type_id': '/kongbupian/'}, + {'type_name': '剧情片', 'type_id': '/juqingpian/'}, + {'type_name': '战争片', 'type_id': '/zhanzhengpian/'}, + {'type_name': '纪录片', 'type_id': '/jilupian/'}, + {'type_name': '动画片', 'type_id': '/donghuapian/'}, + {'type_name': '电视剧', 'type_id': '/dianshiju/'}, + {'type_name': '综艺', 'type_id': '/ZongYi/'}, + ] + + def getName(self): + return '6v影视' + + def init(self, extend=''): + pass + + def isVideoFormat(self, url): + pass + + def manualVideoCheck(self): + pass + + def destroy(self): + pass + + def homeContent(self, filter): + return {'class': self.classes} + + def homeVideoContent(self): + return {'list': self._parse_list(self._html(self.host + '/'))} + + # 兼容部分 Python Spider 壳的命名 + def homeVod(self): + return self.homeVideoContent() + + def categoryContent(self, tid, pg, filter, extend): + page = int(pg or 1) + path = tid or '/' + if page > 1: + if path.endswith('/'): + path = path + 'index_%d.html' % page + else: + path = path.rstrip('/') + '/index_%d.html' % page + html = self._html(self._abs(path)) + videos = self._parse_list(html) + return {'list': videos, 'page': page, 'pagecount': page + 1 if videos else page, 'limit': 18, 'total': 999999 if videos else 0} + + def detailContent(self, ids): + url = ids[0] if isinstance(ids, list) else ids + url = self._abs(url) + html = self._html(url) + title = self._first([r'\s*([^<]+?)(?:-|_|\|).*?', r']*>(.*?)'], html, '未知影片') + pic = self._first([r']+class=["\'][^"\']*thumbnail[^"\']*["\'][\s\S]*?]+(?:src|data-original|data-src)=["\']([^"\']+)["\']', r']+(?:src|data-original|data-src)=["\']([^"\']+\.(?:jpg|jpeg|png|webp)[^"\']*)["\']'], html, '') + desc = self._first([r'◎简\s*介\s*([\s\S]*?)(?:◎||$)', r']+name=["\']description["\'][^>]+content=["\']([^"\']+)["\']'], html, '暂无简介') + lines = self._parse_detail_lines(html) + play_from = '$$$'.join([x['name'] for x in lines]) or '详情页' + play_url = '$$$'.join(['#'.join(['%s$%s' % (ep['title'], ep['url']) for ep in x['episodes']]) for x in lines]) or ('打开详情页$' + url) + vod = { + 'vod_id': url, + 'vod_name': self._clean(title), + 'vod_pic': self._abs(pic), + 'type_name': '6v影视', + 'vod_year': self._year(title + ' ' + html), + 'vod_area': '', + 'vod_remarks': '', + 'vod_actor': '', + 'vod_director': '', + 'vod_content': self._clean(desc), + 'vod_play_from': play_from, + 'vod_play_url': play_url, + } + return {'list': [vod]} + + def searchContent(self, key, quick, pg='1'): + url = self.host + '/e/search/11index.php' + body = 'keyboard=%s&show=title&tempid=1&tbname=article&mid=1&dopost=search&submit=' % quote(key or '') + html = self._html(url, method='post', data=body, extra_headers={'Content-Type': 'application/x-www-form-urlencoded'}) + return {'list': self._parse_list(html)} + + def playerContent(self, flag, id, vipFlags): + url = self._abs(id) + low = url.lower() + if low.startswith('magnet:') or any(x in low for x in ['pan.quark.cn', 'pan.baidu.com', 'xunlei.com', 'aliyundrive.com', 'alipan.com', 'cloud.189.cn', 'yun.139.com', '123pan']): + return {'parse': 0, 'jx': 0, 'url': url, 'header': self.headers} + real = self._resolve_play_url(url) + return {'parse': 0, 'jx': 0, 'url': real or url, 'header': {'User-Agent': self.headers['User-Agent'], 'Referer': self.host + '/'}} + + def localProxy(self, param): + return None + + def _html(self, url, method='get', data=None, extra_headers=None): + headers = dict(self.headers) + if extra_headers: + headers.update(extra_headers) + try: + if method.lower() == 'post': + res = self.fetch(url, headers=headers, data=data, method='post', timeout=20) + else: + res = self.fetch(url, headers=headers, timeout=20) + return self._res_text(res) + except Exception: + # 某些壳不支持 method 参数,POST 搜索失败时返回空;分类/首页不受影响 + try: + if method.lower() == 'post': + res = self.fetch(url, headers=headers, postData=data, timeout=20) + return self._res_text(res) + except Exception: + pass + return '' + + def _res_text(self, res): + if res is None: + return '' + if isinstance(res, str): + return res + if isinstance(res, bytes): + return self._decode(res) + if isinstance(res, dict): + val = res.get('content') or res.get('body') or res.get('data') or res.get('text') or '' + if isinstance(val, bytes): + return self._decode(val) + return str(val or '') + if hasattr(res, 'content'): + val = getattr(res, 'content') + if isinstance(val, bytes): + return self._decode(val) + return str(val or '') + if hasattr(res, 'text'): + return str(getattr(res, 'text') or '') + return str(res) + + def _decode(self, data): + for enc in ('utf-8', 'gbk', 'gb18030'): + try: + txt = data.decode(enc) + if '锟斤拷' not in txt and '\ufffd' not in txt: + return txt + except Exception: + pass + try: + return data.decode('utf-8', errors='ignore') + except Exception: + return '' + + def _abs(self, url): + if not url: + return '' + if str(url).startswith('magnet:'): + return url + return urljoin(self.host + '/', str(url).replace('&', '&')) + + def _clean(self, text): + text = re.sub(r'|', '', str(text or ''), flags=re.I) + text = re.sub(r'<[^>]+>', ' ', text) + text = unescape(text) + text = re.sub(r'"?>\s*', ' ', text) + text = re.sub(r'[\ue000-\uf8ff]', '', text) + return re.sub(r'\s+', ' ', text).strip() + + def _first(self, patterns, html, default=''): + for pat in patterns: + m = re.search(pat, html or '', flags=re.I) + if m: + return self._clean(m.group(1)) + return default + + def _year(self, text): + m = re.search(r'\b((?:19|20)\d{2})\b', text or '') + return m.group(1) if m else '' + + def _parse_list(self, html): + html = html or '' + videos = [] + seen = set() + # 6v 实测列表:
  • ... thumbnail ... h2 ...
  • + blocks = re.findall(r']+class=["\'][^"\']*\bpost\b[^"\']*["\'][^>]*>[\s\S]*?', html, flags=re.I) + for block in blocks: + href = self._first_raw([r'

    [\s\S]*?]+href=["\']([^"\']+\.html)["\']', r']+href=["\']([^"\']+\.html)["\'][^>]*class=["\'][^"\']*zoom'], block) + title = self._first_raw([r'

    [\s\S]*?]*>([\s\S]*?)', r'

    [\s\S]*?]*title=["\']([^"\']+)["\']', r']*title=["\']([^"\']+)["\']'], block) + pic = self._first_raw([r']+(?:src|data-original|data-src)=["\']([^"\']+)["\']'], block) + item = self._item(href, title, pic) + if item and item['vod_id'] not in seen: + seen.add(item['vod_id']) + videos.append(item) + if videos: + return videos + # 兜底:全页 anchor,只收真正详情页,避免导航链接 + for href, inner in re.findall(r']*href=["\']([^"\']+\.html)["\'][^>]*>([\s\S]*?)', html, flags=re.I): + if '/e/' in href or href in ('/', '/index.html'): + continue + if not re.search(r'/[A-Za-z0-9_-]+/\d+\.html|/\d+\.html', href): + continue + title = self._clean(inner) + pic = self._first_raw([r']+(?:src|data-original|data-src)=["\']([^"\']+)["\']'], inner) + item = self._item(href, title, pic) + if item and item['vod_id'] not in seen: + seen.add(item['vod_id']) + videos.append(item) + return videos + + def _first_raw(self, patterns, text): + for pat in patterns: + m = re.search(pat, text or '', flags=re.I) + if m: + return m.group(1) + return '' + + def _item(self, href, title, pic=''): + title = self._clean(title) + if not href or len(title) < 2: + return None + url = self._abs(href) + return {'vod_id': url, 'vod_name': title, 'vod_pic': self._abs(pic), 'vod_remarks': self._year(title) or '点击查看'} + + def _parse_detail_lines(self, html): + lines = [] + h3s = list(re.finditer(r']*>([^<]*播放地址[^<]*)

    ', html or '', flags=re.I)) + for i, h3 in enumerate(h3s): + section = html[h3.start():(h3s[i + 1].start() if i + 1 < len(h3s) else len(html))] + eps = self._parse_eps(section, play=True) + if eps: + lines.append({'name': self._clean(h3.group(1)) or '在线播放', 'episodes': eps}) + if not lines: + eps = self._parse_eps(html, play=True) + if eps: + lines.append({'name': '在线播放', 'episodes': eps}) + idx = (html or '').find('【下载地址】') + if idx >= 0: + sec = html[idx:] + cut_points = [x for x in [sec.find(']*>(.*?)' + else: + pat = r']*?\s+)?href\s*=\s*["\']([^"\']+)["\'][^>]*>(.*?)' + for href, title in re.findall(pat, html or '', flags=re.I): + title = self._clean(title) + if href in seen or not title: + continue + if not play and ('#respond' in href or 'category' in href): + continue + seen.add(href) + if not play and (title == '链接' or len(title) < 2): + low = href.lower() + title = '夸克网盘' if 'quark' in low else ('迅雷网盘' if 'xunlei' in low else ('磁力链接' if low.startswith('magnet:') else '下载')) + eps.append({'title': title, 'url': self._abs(href)}) + return eps + + def _resolve_play_url(self, play_url): + html = self._html(play_url) + media = self._find_media(html, play_url) + if media: + return media + iframe = re.search(r']+src\s*=\s*["\']([^"\']+)["\']', html or '', flags=re.I) + if iframe: + iframe_url = urljoin(play_url, iframe.group(1)) + media = self._find_media(self._html(iframe_url), iframe_url) + if media: + return media + return None + + def _find_media(self, html, base_url): + patterns = [ + r'https?://[^\s"\'<>]+\.(?:m3u8|mp4)[^\s"\'<>]*', + r'const\s+url\s*=\s*["\']([^"\']+\.(?:m3u8|mp4)[^"\']*)["\']', + r'url\s*[:=]\s*["\']([^"\']+\.(?:m3u8|mp4)[^"\']*)["\']', + r'["\']url["\']\s*:\s*["\']([^"\']+\.(?:m3u8|mp4)[^"\']*)["\']', + ] + for pat in patterns: + m = re.search(pat, html or '', flags=re.I) + if m: + val = m.group(1) if m.lastindex else m.group(0) + return urljoin(base_url, val.replace('\\/', '/')) + return None diff --git a/api/AppToV5.py b/api/AppToV5.py new file mode 100644 index 0000000..76a95d4 --- /dev/null +++ b/api/AppToV5.py @@ -0,0 +1,187 @@ +# -*- coding: utf-8 -*- +# 本资源来源于互联网公开渠道,仅可用于个人学习及爬虫技术交流。 +# 严禁将其用于任何商业用途,下载后请于 24 小时内删除,搜索结果均来自源站,本人不承担任何责任。 +""" +{ + "key": "xxx", + "name": "xxx", + "type": 3, + "api": "./ApptoV5无加密.py", + "ext": "http://domain.com" +} +""" + +import re,sys,uuid +from base.spider import Spider +sys.path.append('..') + +class Spider(Spider): + host,config,local_uuid,parsing_config = '','','',[] + headers = { + 'User-Agent': "Dart/2.19 (dart:io)", + 'Accept-Encoding': "gzip", + 'appto-local-uuid': local_uuid + } + + def init(self, extend=''): + try: + host = extend.strip() + if not host.startswith('http'): + return {} + if not re.match(r'^https?://[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*(:\d+)?/?$', host): + host_=self.fetch(host).json() + self.host = host_['domain'] + else: + self.host = host + self.local_uuid = str(uuid.uuid4()) + response = self.fetch(f'{self.host}/apptov5/v1/config/get?p=android&__platform=android', headers=self.headers).json() + config = response['data'] + self.config = config + parsing_conf = config['get_parsing']['lists'] + parsing_config = {} + for i in parsing_conf: + if len(i['config']) != 0: + label = [] + for j in i['config']: + if j['type'] == 'json': + label.append(j['label']) + parsing_config.update({i['key']:label}) + self.parsing_config = parsing_config + return None + except Exception as e: + print(f'初始化异常:{e}') + return {} + + def detailContent(self, ids): + response = self.fetch(f"{self.host}/apptov5/v1/vod/getVod?id={ids[0]}",headers=self.headers).json() + data3 = response['data'] + videos = [] + vod_play_url = '' + vod_play_from = '' + for i in data3['vod_play_list']: + play_url = '' + for j in i['urls']: + play_url += f"{j['name']}${i['player_info']['from']}@{j['url']}#" + vod_play_from += i['player_info']['show'] + '$$$' + vod_play_url += play_url.rstrip('#') + '$$$' + vod_play_url = vod_play_url.rstrip('$$$') + vod_play_from = vod_play_from.rstrip('$$$') + videos.append({ + 'vod_id': data3.get('vod_id'), + 'vod_name': data3.get('vod_name'), + 'vod_content': data3.get('vod_content'), + 'vod_remarks': data3.get('vod_remarks'), + 'vod_director': data3.get('vod_director'), + 'vod_actor': data3.get('vod_actor'), + 'vod_year': data3.get('vod_year'), + 'vod_area': data3.get('vod_area'), + 'vod_play_from': vod_play_from, + 'vod_play_url': vod_play_url + }) + return {'list': videos} + + def searchContent(self, key, quick, pg='1'): + url = f"{self.host}/apptov5/v1/search/lists?wd={key}&page={pg}&type=&__platform=android" + response = self.fetch(url, headers=self.headers).json() + data = response['data']['data'] + for i in data: + if i.get('vod_pic').startswith('mac://'): + i['vod_pic'] = i['vod_pic'].replace('mac://', 'http://', 1) + return {'list': data, 'page': pg, 'total': response['data']['total']} + + def playerContent(self, flag, id, vipflags): + default_ua = 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1' + parsing_config = self.parsing_config + parts = id.split('@') + if len(parts) != 2: + return {'parse': 0, 'url': id, 'header': {'User-Agent': default_ua}} + playfrom, rawurl = parts + label_list = parsing_config.get(playfrom) + if not label_list: + return {'parse': 0, 'url': rawurl, 'header': {'User-Agent': default_ua}} + result = {'parse': 1, 'url': rawurl, 'header': {'User-Agent': default_ua}} + for label in label_list: + payload = { + 'play_url': rawurl, + 'label': label, + 'key': playfrom + } + try: + response = self.post( + f"{self.host}/apptov5/v1/parsing/proxy?__platform=android", + data=payload, + headers=self.headers + ).json() + except Exception as e: + print(f"请求异常: {e}") + continue + if not isinstance(response, dict): + continue + if response.get('code') == 422: + continue + data = response.get('data') + if not isinstance(data, dict): + continue + url = data.get('url') + if not url: + continue + ua = data.get('UA') or data.get('UserAgent') or default_ua + result = { + 'parse': 0, + 'url': url, + 'header': {'User-Agent': ua} + } + break + return result + + def homeContent(self, filter): + config = self.config + if not config: + return {} + home_cate = config['get_home_cate'] + classes = [] + for i in home_cate: + if isinstance(i.get('extend', []),dict): + classes.append({'type_id': i['cate'], 'type_name': i['title']}) + return {'class': classes} + + def homeVideoContent(self): + response = self.fetch(f'{self.host}/apptov5/v1/home/data?id=1&mold=1&__platform=android',headers=self.headers).json() + data = response['data'] + vod_list = [] + for i in data['sections']: + for j in i['items']: + vod_pic = j.get('vod_pic') + if vod_pic.startswith('mac://'): + vod_pic = vod_pic.replace('mac://', 'http://', 1) + vod_list.append({ + "vod_id": j.get('vod_id'), + "vod_name": j.get('vod_name'), + "vod_pic": vod_pic, + "vod_remarks": j.get('vod_remarks') + }) + return {'list': vod_list} + + def categoryContent(self, tid, pg, filter, extend): + response = self.fetch(f"{self.host}/apptov5/v1/vod/lists?area={extend.get('area','')}&lang={extend.get('lang','')}&year={extend.get('year','')}&order={extend.get('sort','time')}&type_id={tid}&type_name=&page={pg}&pageSize=21&__platform=android", headers=self.headers).json() + data = response['data'] + data2 = data['data'] + for i in data['data']: + if i.get('vod_pic','').startswith('mac://'): + i['vod_pic'] = i['vod_pic'].replace('mac://', 'http://', 1) + return {'list': data2, 'page': pg, 'total': data['total']} + + def getName(self): + pass + + def isVideoFormat(self, url): + pass + + def manualVideoCheck(self): + pass + + def destroy(self): + pass + + def localProxy(self, param): + pass \ No newline at end of file diff --git a/api/GZYS.py b/api/GZYS.py new file mode 100644 index 0000000..220e1fc --- /dev/null +++ b/api/GZYS.py @@ -0,0 +1,496 @@ +# coding = utf-8 +#!/usr/bin/python +import re +import sys +import json +import time +import base64 +import hashlib +import random +import string +import urllib.parse +from Crypto.Cipher import AES +from Crypto.Util.Padding import pad, unpad +from Crypto.PublicKey import RSA +from Crypto.Cipher import PKCS1_v1_5 +from base.spider import Spider + +sys.path.append('..') + +class Spider(Spider): + def __init__(self): + self.name = "瓜子" + self.hosts = [ + 'https://apinew.uozvr.com', + 'https://api.w32z7vtd.com', + 'https://api.6a7nnf7.com', + 'https://api.umygrx3.com', + 'https://api.rmedphk.com' + ] + self.host_index = 0 + self.host = self.hosts[self.host_index] + + # AES 固定密钥(与Java版一致) + self.AES_KEY = 'OITxa5OqAYjhswxx' + self.AES_IV = 'rCMNwZASNBKZ8mXV' + + # RSA 公钥/私钥 + self.RSA_PUBLIC_KEY = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDUM5+/y8sPsWkd1/RQS64X259EUwxFXFE5HlA65MqrxnPs0JqoSRojSDy5QhwvROlaD6TwRQHKMY2OAZ6SnQeUJsChTEFIR9qUkwrs3/MVUMxjsv6JS6Oe/juclyJGTgVmDhB55EafXsD0SQYVj/QXXsxR6ewR5E2kL52yAAD4yQIDAQAB" + self.RSA_PRIVATE_KEY = """-----BEGIN RSA PRIVATE KEY----- +MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGAe6hKrWLi1zQmjTT1 +ozbE4QdFeJGNxubxld6GrFGximxfMsMB6BpJhpcTouAqywAFppiKetUBBbXwYsYU +1wNr648XVmPmCMCy4rY8vdliFnbMUj086DU6Z+/oXBdWU3/b1G0DN3E9wULRSwcK +ZT3wj/cCI1vsCm3gj2R5SqkA9Y0CAwEAAQKBgAJH+4CxV0/zBVcLiBCHvSANm0l7 +HetybTh/j2p0Y1sTXro4ALwAaCTUeqdBjWiLSo9lNwDHFyq8zX90+gNxa7c5EqcW +V9FmlVXr8VhfBzcZo1nXeNdXFT7tQ2yah/odtdcx+vRMSGJd1t/5k5bDd9wAvYdI +DblMAg+wiKKZ5KcdAkEA1cCakEN4NexkF5tHPRrR6XOY/XHfkqXxEhMqmNbB9U34 +saTJnLWIHC8IXys6Qmzz30TtzCjuOqKRRy+FMM4TdwJBAJQZFPjsGC+RqcG5UvVM +iMPhnwe/bXEehShK86yJK/g/UiKrO87h3aEu5gcJqBygTq3BBBoH2md3pr/W+hUM +WBsCQQChfhTIrdDinKi6lRxrdBnn0Ohjg2cwuqK5zzU9p/N+S9x7Ck8wUI53DKm8 +jUJE8WAG7WLj/oCOWEh+ic6NIwTdAkEAj0X8nhx6AXsgCYRql1klbqtVmL8+95KZ +K7PnLWG/IfjQUy3pPGoSaZ7fdquG8bq8oyf5+dzjE/oTXcByS+6XRQJAP/5ciy1b +L3NhUhsaOVy55MHXnPjdcTX0FaLi+ybXZIfIQ2P4rb19mVq1feMbCXhz+L1rG8oa +t5lYKfpe8k83ZA== +-----END RSA PRIVATE KEY-----""" + + self.DEVICE_OLD_KEY = "aLFBMWpxBrIDAD1Si/KVvm41" + + # 设备信息(随机生成) + self.deviceId = str(864150060000000 + random.randint(0, 9999)) + self.deviceKey = ''.join(random.choices('0123456789ABCDEF', k=40)) # 20字节hex大写 + self.token = "" + self.token_id = "" + self.registered = False + + self.header = { + 'User-Agent': 'Lavf/57.83.100', + 'code': 'GZ0369', + 'deviceId': self.deviceId, + 'lang': 'zh_cn', + 'Cache-Control': 'no-cache', + 'Content-Type': 'application/x-www-form-urlencoded', + 'Version': '2604028', + 'PackageName': 'com.ae06aebdbb.y286327f5a.ofe849883320260517', + 'Ver': '3.0.3.2', + 'api-ver': '3.0.3.2', + 'Referer': self.host + } + + self.cache = {} + self.cache_timeout = 300 + + # 初始化token + self.init_token() + + def getName(self): + return self.name + + def init(self, extend=''): + pass + + # ---------- 设备注册与认证 ---------- + def init_token(self): + """初始化token:注册设备 -> 刷新""" + print("===== 初始化设备认证 =====") + try: + if not self.registered: + self.sign_up() + # 刷新获取最终token + self.refresh_token() + except Exception as e: + print(f"初始化token失败: {e}") + # 兜底使用原有硬编码(几乎没用) + self.token = '024212ef0975c5306a1434e113a46463.bc77313e11a248558a6ca244ca980944ec3421fa480c50e0229ad91f1cb15aea582603202cd71796885c9e5163e500f1b72f737059aff1ddb8beea47c5a331d6760540345b7f88b2302a0e6e09589f9dcf3ff9175d8c905f990203f5fc04748008ea7a366571cbf5b09509a873dcfba3cf1d5590385f5f7ef6e01d1850974aa220eb5178c89e61c24411af9b9a19435e.06fde789ece48d9b33c5dc857e04e9b5838f08264d928b87237d3476c4484b46' + + def sign_up(self): + """注册设备""" + print("注册新设备...") + params = { + "new_key": self.deviceKey, + "old_key": self.DEVICE_OLD_KEY, + "phone_type": 1, + "code": "" + } + result = self._auth_request('/App/Authentication/Device/signUp', params) + self._apply_auth(result) + self.registered = True + + def sign_in(self): + """登录设备""" + print("设备登录...") + params = { + "new_key": self.deviceKey, + "old_key": self.DEVICE_OLD_KEY + } + result = self._auth_request('/App/Authentication/Device/signIn', params) + self._apply_auth(result) + + def _apply_auth(self, result): + """从认证响应中提取token""" + new_token = result.get('token', '') + if not new_token: + raise Exception("认证失败,无token返回: {}".format(result)) + self.token = new_token + new_token_id = result.get('app_user_id', '') + if new_token_id: + self.token_id = new_token_id + print(f"获取token成功, token前缀: {self.token[:30]}...") + + def refresh_token(self): + """刷新token""" + print("刷新token...") + result = self._auth_request('/App/Authentication/Authenticator/refresh', {}) + self._apply_auth(result) + + def _auth_request(self, path, params): + """认证类请求(不需要ensure_token)""" + return self._send_encrypted_request(params, path, is_auth=True) + + # ---------- 业务请求核心(修复加密与签名) ---------- + def ensure_token(self): + """确保token有效,如未就绪则重新获取""" + if not self.token or not self.token_id: + if self.registered: + self.sign_in() + else: + self.sign_up() + self.refresh_token() + + def _send_encrypted_request(self, data, path, is_auth=False): + """ + 发送加密请求,返回解密后的字典 + :param data: 业务参数字典 + :param path: 请求路径 + :param is_auth: 是否为认证类请求(signUp/signIn/refresh),此时不使用ensure_token + """ + try: + if not is_auth: + self.ensure_token() + + # 1. 将参数转为JSON并AES加密 + json_params = json.dumps(data) + encrypted = self.aes_encrypt(json_params, self.AES_KEY, self.AES_IV) + request_key = encrypted.upper() # Java中是bytesToHex(encrypted).toUpperCase() + + # 2. 生成keys (RSA加密 iv/key JSON) + key_json = json.dumps({"iv": self.AES_IV, "key": self.AES_KEY}) + keys = self.rsa_encrypt(key_json, self.RSA_PUBLIC_KEY) + + # 3. 生成签名 + t = str(int(time.time())) + sign_str = f"token_id=,token={self.token},phone_type=1,request_key={request_key},app_id=1,time={t},keys={keys}*&zvdvdvddbfikkkumtmdwqppp?|4Y!s!2br" + signature = self.get_md5(sign_str) # 已改为大写 + + # 4. 构建请求体 + body = { + 'token': self.token, + 'token_id': '', + 'phone_type': '1', + 'time': t, + 'phone_model': 'xiaomi-25031', # 与Java版保持一致 + 'keys': keys, + 'request_key': request_key, + 'signature': signature, + 'app_id': '1', + 'ad_version': '1' + } + + # 5. 发送请求 + url = f"{self.host}{path}" + response = self.post(url, headers=self.header, data=body, timeout=10) + + if response.status_code != 200: + raise Exception(f"HTTP {response.status_code}") + + resp_json = response.json() + # 检查业务code(若不为200可能token过期) + if 'code' in resp_json and resp_json['code'] != 200: + print(f"业务错误码: {resp_json['code']}, 信息: {resp_json}") + # 如果不是认证请求,尝试重新获取token后重试一次(这里简单处理,外层get_data已有重试) + raise Exception("业务错误") + + data_section = resp_json.get('data') + if not data_section: + raise Exception("响应缺少data字段") + + encrypted_response = data_section.get('response_key', '') + encrypted_keys = data_section.get('keys', '') + + # 6. 解密响应 + decrypted_keys_json = self.rsa_decrypt(encrypted_keys, self.RSA_PRIVATE_KEY) + key_info = json.loads(decrypted_keys_json) + resp_key = key_info['key'] + resp_iv = key_info['iv'] + decrypted_data = self.aes_decrypt(encrypted_response, resp_key, resp_iv) + return json.loads(decrypted_data) + + except Exception as e: + print(f"请求失败 [{path}]: {e}") + return None + + def get_data(self, data, path, use_cache=True): + """带重试和域名轮询的数据获取(保持原框架)""" + try: + cache_key = f"{path}_{hash(str(data))}" if use_cache else None + if use_cache and cache_key in self.cache: + cached_data, timestamp = self.cache[cache_key] + if time.time() - timestamp < self.cache_timeout: + return cached_data + + for attempt in range(3): + tried = 0 + while tried < len(self.hosts): + self.host = self.hosts[self.host_index] + self.header['Referer'] = self.host + result = self._send_encrypted_request(data, path) + if result is not None: + print(f"请求成功: {path}, 域名: {self.host}") + if use_cache and cache_key: + self.cache[cache_key] = (result, time.time()) + return result + + # 切换到下一个域名 + self.host_index = (self.host_index + 1) % len(self.hosts) + tried += 1 + + # 所有域名失败,尝试重新认证并重试 + if attempt < 2: + print("所有域名失败,尝试重新认证...") + try: + self.ensure_token() + except: + pass + self.host_index = 0 + else: + break + return None + except Exception as e: + print(f"get_data异常: {e}") + return None + + # ---------- 加解密工具 ---------- + def aes_encrypt(self, text, key, iv): + try: + key_bytes = key.encode('utf-8') + iv_bytes = iv.encode('utf-8') + cipher = AES.new(key_bytes, AES.MODE_CBC, iv_bytes) + encrypted = cipher.encrypt(pad(text.encode('utf-8'), AES.block_size)) + return encrypted.hex().upper() + except Exception as e: + print(f"AES加密失败: {e}") + return "" + + def aes_decrypt(self, text, key, iv): + try: + key_bytes = key.encode('utf-8') + iv_bytes = iv.encode('utf-8') + cipher = AES.new(key_bytes, AES.MODE_CBC, iv_bytes) + encrypted_bytes = bytes.fromhex(text) + decrypted = unpad(cipher.decrypt(encrypted_bytes), AES.block_size) + return decrypted.decode('utf-8') + except Exception as e: + print(f"AES解密失败: {e}") + return "" + + def rsa_encrypt(self, text, public_key_str): + """RSA公钥加密(PKCS1v1.5)""" + try: + key = RSA.import_key("-----BEGIN PUBLIC KEY-----\n" + public_key_str + "\n-----END PUBLIC KEY-----") + cipher = PKCS1_v1_5.new(key) + encrypted = cipher.encrypt(text.encode('utf-8')) + return base64.b64encode(encrypted).decode('utf-8') + except Exception as e: + print(f"RSA加密失败: {e}") + return "" + + def rsa_decrypt(self, encrypted_data, private_key_str): + """RSA私钥解密""" + try: + encrypted_bytes = base64.b64decode(encrypted_data) + rsa_key = RSA.import_key(private_key_str) + cipher = PKCS1_v1_5.new(rsa_key) + decrypted = cipher.decrypt(encrypted_bytes, None) + return decrypted.decode('utf-8') if decrypted else "" + except Exception as e: + print(f"RSA解密失败: {e}") + return "" + + def get_md5(self, text): + return hashlib.md5(text.encode()).hexdigest().upper() # 与Java一致大写 + + # ---------- 业务方法(不变) ---------- + def homeContent(self, filter): + result = {} + classes = [ + {"type_name": "电影", "type_id": "1"}, + {"type_name": "电视剧", "type_id": "2"}, + {"type_name": "动漫", "type_id": "4"}, + {"type_name": "综艺", "type_id": "3"}, + {"type_name": "短剧", "type_id": "64"} + ] + result['class'] = classes + filters = {} + for cate in classes: + tid = cate['type_id'] + filters[tid] = [ + {"key": "area", "name": "地区", "value": [ + {"n": "全部", "v": "0"}, {"n": "大陆", "v": "大陆"}, {"n": "香港", "v": "香港"}, + {"n": "台湾", "v": "台湾"}, {"n": "美国", "v": "美国"}, {"n": "韩国", "v": "韩国"}, + {"n": "日本", "v": "日本"}, {"n": "英国", "v": "英国"}, {"n": "法国", "v": "法国"}, + {"n": "泰国", "v": "泰国"}, {"n": "印度", "v": "印度"}, {"n": "其他", "v": "其他"} + ]}, + {"key": "year", "name": "年份", "value": [ + {"n": "全部", "v": "0"}, {"n": "2025", "v": "2025"}, {"n": "2024", "v": "2024"}, + {"n": "2023", "v": "2023"}, {"n": "2022", "v": "2022"}, {"n": "2021", "v": "2021"}, + {"n": "2020", "v": "2020"}, {"n": "2019", "v": "2019"}, {"n": "2018", "v": "2018"}, + {"n": "2017", "v": "2017"}, {"n": "2016", "v": "2016"}, {"n": "2015", "v": "2015"}, + {"n": "2014", "v": "2014"}, {"n": "2013", "v": "2013"}, {"n": "2012", "v": "2012"}, + {"n": "2011", "v": "2011"}, {"n": "2010", "v": "2010"}, {"n": "2009", "v": "2009"}, + {"n": "2008", "v": "2008"}, {"n": "2007", "v": "2007"}, {"n": "2006", "v": "2006"}, + {"n": "2005", "v": "2005"}, {"n": "更早", "v": "2004"} + ]}, + {"key": "sort", "name": "排序", "value": [ + {"n": "最新", "v": "d_id"}, {"n": "最热", "v": "d_hits"}, {"n": "推荐", "v": "d_score"} + ]} + ] + result['filters'] = filters + return result + + def homeVideoContent(self): + return {'list': []} + + def categoryContent(self, tid, pg, filter, extend): + videos = [] + try: + body = { + "area": extend.get('area', '0'), + "year": extend.get('year', '0'), + "pageSize": "30", + "sort": extend.get('sort', 'd_id'), + "page": str(pg), + "tid": tid + } + cache_key = f"category_{tid}_{pg}_{hash(str(body))}" + data = self.get_cached_data(cache_key, body, '/App/IndexList/indexList') + if data and 'list' in data: + for item in data['list']: + vod_continu = item.get('vod_continu', 0) + remarks = '电影' if vod_continu == 0 else f'更新至{vod_continu}集' + video = { + "vod_id": f"{item.get('vod_id', '')}/{vod_continu}", + "vod_name": item.get('vod_name', ''), + "vod_pic": item.get('vod_pic', ''), + "vod_remarks": remarks + } + videos.append(video) + except Exception as e: + print(f"获取分类内容失败: {e}") + return {'list': videos, 'page': int(pg), 'pagecount': 9999, 'limit': 30, 'total': 999999} + + def detailContent(self, ids): + try: + vod_id = ids[0].split('/')[0] + t = str(int(time.time())) + body1 = {"token_id": self.token_id, "vod_id": vod_id, "mobile_time": t, "token": self.token} + qdata = self.get_data(body1, '/App/IndexPlay/playInfo') + body2 = {"vurl_cloud_id": "2", "vod_d_id": vod_id} + jdata = self.get_data(body2, '/App/Resource/Vurl/show') + if not qdata or 'vodInfo' not in qdata: + return {'list': []} + vod = qdata['vodInfo'] + video_detail = { + "vod_id": vod_id, + "vod_name": vod.get('vod_name', ''), + "vod_pic": vod.get('vod_pic', ''), + "vod_year": vod.get('vod_year', ''), + "vod_area": vod.get('vod_area', ''), + "vod_actor": vod.get('vod_actor', ''), + "vod_director": vod.get('vod_director', ''), + "vod_content": vod.get('vod_use_content', '').strip(), + "vod_play_from": "瓜子影视" + } + play_list = [] + if jdata and 'list' in jdata: + for index, item in enumerate(jdata['list']): + if 'play' in item: + n, p = [], [] + for key, value in item['play'].items(): + if 'param' in value and value['param']: + n.append(key) + p.append(value['param']) + if p: + play_name = str(index + 1) if len(jdata['list']) != 1 else vod.get('vod_name', '') + play_url = f"{p[-1]}||{'@'.join(n)}" + play_list.append(f"{play_name}${play_url}") + video_detail["vod_play_url"] = "#".join(play_list) + return {'list': [video_detail]} + except Exception as e: + print(f"获取详情失败: {e}") + return {'list': []} + + def searchContent(self, key, quick, pg=1): + videos = [] + try: + body = {"keywords": key, "order_val": "1", "page": str(pg)} + data = self.get_data(body, '/App/Index/findMoreVod', use_cache=False) + if data and 'list' in data: + for item in data['list']: + vod_continu = item.get('vod_continu', 0) + remarks = '电影' if vod_continu == 0 else f'更新至{vod_continu}集' + videos.append({ + "vod_id": f"{item.get('vod_id', '')}/{vod_continu}", + "vod_name": item.get('vod_name', ''), + "vod_pic": item.get('vod_pic', ''), + "vod_remarks": remarks + }) + except Exception as e: + print(f"搜索失败: {e}") + return {'list': videos, 'page': int(pg), 'pagecount': 9999, 'limit': 30, 'total': 999999} + + def playerContent(self, flag, id, vipFlags): + try: + parts = id.split('||') + if len(parts) < 2: + return {"parse": 0, "playUrl": "", "url": ""} + param_str = parts[0] + resolutions = parts[1].split('@') if len(parts) > 1 else [] + params = {} + for pair in param_str.split('&'): + if '=' in pair: + key, value = pair.split('=', 1) + params[key] = value + if resolutions: + resolutions.sort(key=lambda x: int(x) if x.isdigit() else 0, reverse=True) + params['resolution'] = resolutions[0] + data = self.get_data(params, '/App/Resource/VurlDetail/showOne', use_cache=False) + if data and 'url' in data: + return {"parse": 0, "playUrl": "", "url": data['url'], + "header": json.dumps({"User-Agent": "Lavf/57.83.100", "Referer": "http://WJiZxLXA2.com/"}), 'danmaku': 'http://127.0.0.1:9978/proxy?do=diydanmu'} + return {"parse": 0, "playUrl": "", "url": ""} + except Exception as e: + print(f"播放解析失败: {e}") + return {"parse": 0, "playUrl": "", "url": ""} + + def isVideoFormat(self, url): + video_formats = ['.m3u8', '.mp4', '.avi', '.mkv', '.flv', '.ts'] + return any(url.lower().endswith(fmt) for fmt in video_formats) + + def manualVideoCheck(self): + pass + + def localProxy(self, params): + return None + + def get_cached_data(self, cache_key, data, path): + current_time = time.time() + if cache_key in self.cache: + cached_data, timestamp = self.cache[cache_key] + if current_time - timestamp < self.cache_timeout: + return cached_data + result = self.get_data(data, path) + if result: + self.cache[cache_key] = (result, current_time) + return result + +if __name__ == '__main__': + pass \ No newline at end of file diff --git a/api/erge_juhe.js b/api/erge_juhe.js new file mode 100644 index 0000000..6b65871 --- /dev/null +++ b/api/erge_juhe.js @@ -0,0 +1,310 @@ +/* +@header({ + searchable: 1, + filterable: 1, + quickSearch: 1, + title: '聚合儿歌[儿]', + lang: 'cat' +}) +*/ + +let siteName = '聚合儿歌', siteKey = '', siteType = 0; + +const platformList = [ + { name: '贝乐虎', id: 'beilehu' }, + { name: '兔小贝', id: 'tuxiaobei' } +]; + +const headers = { + 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1' +}; + +const rule = { + beilehu: { + host: 'https://vd.ubestkid.com', + api: '/api/v1/bv/video' + }, + tuxiaobei: { + host: 'https://www.tuxiaobei.com', + listApi: '/list/mip-data', + playUrl: '/play/', + searchApi: '/search/' + } +}; + +const filterOptions = { + beilehu: [{ + key: "area", name: "分类", + value: [ + { "n": "最新上架", "v": "65" }, { "n": "人气热播", "v": "113" }, { "n": "经典童谣", "v": "56" }, + { "n": "开心贝乐虎", "v": "137" }, { "n": "律动儿歌", "v": "53" }, { "n": "经典儿歌", "v": "59" }, + { "n": "超级汽车1", "v": "101" }, { "n": "超级汽车第二季", "v": "119" }, { "n": "超级汽车第三季", "v": "136" }, + { "n": "三字经", "v": "95" }, { "n": "幼儿手势舞", "v": "133" }, { "n": "哄睡儿歌", "v": "117" }, + { "n": "英文儿歌", "v": "70" }, { "n": "节日与节气", "v": "116" }, { "n": "恐龙世界", "v": "97" }, + { "n": "动画片儿歌", "v": "55" }, { "n": "流行歌曲", "v": "57" }, { "n": "贝乐虎入园记", "v": "118" }, + { "n": "贝乐虎大百科", "v": "106" }, { "n": "经典古诗", "v": "62" }, { "n": "经典故事", "v": "63" }, + { "n": "萌虎学功夫", "v": "128" }, { "n": "绘本故事", "v": "100" }, { "n": "开心贝乐虎英文版", "v": "121" }, + { "n": "嗨贝乐虎情商动画", "v": "96" }, { "n": "动物音乐派对", "v": "108" }, { "n": "动物音乐派对英文版", "v": "126" }, + { "n": "奇妙的身体", "v": "105" }, { "n": "奇妙的身体英文版", "v": "124" }, { "n": "认知卡片", "v": "64" }, + { "n": "趣味简笔画", "v": "109" }, { "n": "数字儿歌", "v": "78" }, { "n": "识字体验版", "v": "120" }, + { "n": "启蒙系列体验版", "v": "127" } + ] + }], + tuxiaobei: [{ + key: "area", name: "分类", + value: [ + { "n": "全部", "v": "" }, { "n": "儿歌", "v": "2" }, { "n": "故事", "v": "3" }, + { "n": "公益", "v": "27" }, { "n": "十万个为什么", "v": "9" }, { "n": "安全教育", "v": "28" }, + { "n": "动物奇缘", "v": "29" }, { "n": "弟子规", "v": "7" }, { "n": "古诗", "v": "5" }, + { "n": "三字经", "v": "6" }, { "n": "千字文", "v": "8" }, { "n": "数学", "v": "11" }, + { "n": "英语", "v": "25" }, { "n": "折纸", "v": "24" } + ] + }] +}; + +const ruleFilterDef = { + beilehu: { area: '56' }, + tuxiaobei: { area: '2' } +}; + +function init(cfg) { + siteName = cfg.skey?.split('_')[1] || cfg.skey || '聚合儿歌'; + siteKey = cfg.skey; + siteType = cfg.stype; +} + +function safeJSONParse(str, defaultValue = {}) { + if (!str || typeof str === 'object') return str || defaultValue; + try { + return JSON.parse(str); + } catch { + return defaultValue; + } +} + +async function request(url, options = {}) { + const reqHeaders = { ...headers, ...options.headers }; + let postType = reqHeaders['Content-Type']?.includes('json') ? 'json' : + reqHeaders['Content-Type']?.includes('form') ? 'form' : ''; + + try { + const response = await req(url, { + method: options.method || 'GET', + headers: reqHeaders, + data: options.data, + postType: postType, + timeout: options.timeout || 15000 + }); + return response?.content || response?.data || response; + } catch { + return null; + } +} + +function getPlatList() { + return platformList; +} + +async function getBeilehuList(typeId, page) { + let videos = []; + try { + const postData = { + age: 1, + appver: "6.1.9", + egvip_status: 0, + svip_status: 0, + vps: 60, + subcateId: parseInt(typeId), + p: page + }; + + const html = await request(rule.beilehu.host + rule.beilehu.api, { + method: 'POST', + headers: { ...headers, 'Content-Type': 'application/json' }, + data: postData + }); + + const json = safeJSONParse(html); + const items = json.result?.items || []; + + videos = items.map(item => ({ + vod_id: `beilehu@${item.url}`, + vod_name: item.title || '未知视频', + vod_pic: item.image || '', + vod_remarks: `贝乐虎 | 播放:${item.viewcount || 0}`, + vod_content: item.description || '' + })); + } catch (e) {} + return videos; +} + +async function getTuxiaobeiList(typeId, page) { + let videos = []; + try { + const url = `${rule.tuxiaobei.host}${rule.tuxiaobei.listApi}?typeId=${typeId}&page=${page}&callback=`; + const html = await request(url, { headers }); + + const match = html.match(/\((.*?)\);/); + if (!match) return videos; + + const data = safeJSONParse(match[1]).data; + const items = data.items || []; + + videos = items.map(item => ({ + vod_id: `tuxiaobei@${item.video_id}`, + vod_name: item.name || '未知视频', + vod_pic: item.image || '', + vod_remarks: `兔小贝 | ${item.root_category_name || ''} ${item.duration_string || ''}`, + vod_content: item.description || '' + })); + } catch (e) {} + return videos; +} + +async function getBeilehuDetail(url) { + return { + vod_id: url, + vod_name: '贝乐虎视频', + vod_remarks: '贝乐虎', + vod_play_from: '贝乐虎', + vod_play_url: `点击播放$${url}` + }; +} + +async function getTuxiaobeiDetail(id) { + return { + vod_id: id, + vod_name: '兔小贝视频', + vod_remarks: '兔小贝', + vod_play_from: '兔小贝', + vod_play_url: `点击播放$${rule.tuxiaobei.host}${rule.tuxiaobei.playUrl}${id}` + }; +} + +async function home(filter) { + const platForms = getPlatList(); + const classes = platForms.map(item => ({ type_name: item.name, type_id: item.id })); + const filters = {}; + platForms.forEach(item => { if (filterOptions[item.id]) filters[item.id] = filterOptions[item.id]; }); + return JSON.stringify({ class: classes, filters: filters }); +} + +async function homeVod() { + try { + const platForms = getPlatList(); + const randomPlat = platForms[Math.floor(Math.random() * platForms.length)]; + const randomArea = ruleFilterDef[randomPlat.id]?.area || ''; + const categoryResult = await category(randomPlat.id, 1, { area: randomArea }, {}); + const categoryList = safeJSONParse(categoryResult).list || []; + return JSON.stringify({ list: categoryList.slice(0, 12) }); + } catch (e) { + return JSON.stringify({ list: [] }); + } +} + +async function category(tid, pg, filter, extend) { + const page = pg || 1; + extend = extend || {}; + + const platformItem = platformList.find(p => p.id === tid); + if (!platformItem) { + return JSON.stringify({ list: [], page, pagecount: 1, limit: 0, total: 0 }); + } + + const searchKeyword = extend?.custom; + if (searchKeyword) { + return await cfs(tid, searchKeyword, pg); + } + + const area = filter?.area || extend?.area || ruleFilterDef[tid]?.area || ''; + const videos = []; + + try { + switch (tid) { + case 'beilehu': + videos.push(...await getBeilehuList(area, page)); + break; + case 'tuxiaobei': + videos.push(...await getTuxiaobeiList(area, page)); + break; + } + } catch (e) {} + + return JSON.stringify({ + list: videos, + page: page, + pagecount: page + 1, + limit: videos.length, + total: videos.length * (page + 1) + }); +} + +async function detail(id) { + try { + const parts = id.split('@'); + const platform = parts[0]; + const did = parts.slice(1).join('@'); + + let vod = {}; + if (platform === 'beilehu') { + vod = await getBeilehuDetail(did); + } else if (platform === 'tuxiaobei') { + vod = await getTuxiaobeiDetail(did); + } + return JSON.stringify({ list: [vod] }); + } catch (e) { + return JSON.stringify({ list: [] }); + } +} + +async function play(flag, id, flags) { + try { + if (flag.includes('贝乐虎')) { + return JSON.stringify({ parse: 0, url: id, header: headers }); + } + + if (flag.includes('兔小贝')) { + try { + const html = await request(id, { headers }); + let videoUrl = ''; + + const srcMatch = html.match(/video-src=["']([^"']+)["']/); + if (srcMatch) videoUrl = srcMatch[1]; + + if (!videoUrl) { + const sourceMatch = html.match(/]*src=["']([^"']+)["']/); + if (sourceMatch) videoUrl = sourceMatch[1]; + } + + if (!videoUrl) { + const m3u8Match = html.match(/https?:\/\/[^"']+\.m3u8[^"']*/); + if (m3u8Match) videoUrl = m3u8Match[0]; + } + + if (!videoUrl) { + return JSON.stringify({ parse: 0, url: id, msg: '未找到播放地址' }); + } + + return JSON.stringify({ parse: 0, url: videoUrl, header: headers }); + } catch (e) { + return JSON.stringify({ parse: 0, url: id, msg: `播放失败: ${e.message}` }); + } + } + + return JSON.stringify({ parse: 0, url: id }); + } catch (e) { + return JSON.stringify({ parse: 0, url: id, msg: `播放失败: ${e.message}` }); + } +} + +async function cfs(siteId, wd, pg) { + return JSON.stringify({ list: [], page: pg || 1, pagecount: 1, limit: 0, total: 0 }); +} + +async function search(wd, quick, pg) { + return JSON.stringify({ list: [], page: pg || 1, pagecount: 1, limit: 0, total: 0 }); +} + +export function __jsEvalReturn() { + return { init, home, homeVod, category, detail, play, search }; +} \ No newline at end of file