Files
tvbox_collect/jaychouqq/yingshi/js3/T4Proxy.js
T
2026-06-29 03:41:14 +00:00

459 lines
15 KiB
JavaScript

#!/usr/bin/env node
// ============================================================================
// Spider API - Type 4 风格代理
// ============================================================================
const http = require('http');
const https = require('https');
const { URL } = require('url');
const os = require('os');
// ============================================================================
// 配置常量
// ============================================================================
const SPIDER_API_HOST = 'http://192.168.10.36:9978';
const DEFAULT_SITE_KEY = '';
const REQUEST_TIMEOUT = 30000; // 毫秒
// ============================================================================
// Spider API 客户端
// ============================================================================
class SpiderApiClient {
constructor(host, timeout = 30000) {
this.host = host.replace(/\/$/, '');
this.timeout = timeout;
}
configAction(action, extra = {}) {
return this.post('/spider/config', { action, ...extra });
}
execute(key, method, params = {}) {
return this.post('/spider', { key, method, ...params });
}
homeContent(key, filter = true) {
return this.execute(key, 'homeContent', { filter });
}
categoryContent(key, tid, page = 1, filter = true, extend = {}) {
return this.execute(key, 'categoryContent', {
tid, pg: page, filter, extend
});
}
detailContent(key, ids) {
return this.execute(key, 'detailContent', {
ids: Array.isArray(ids) ? ids : [ids]
});
}
searchContent(key, keyword, page = 1) {
return this.execute(key, 'searchContent', { wd: keyword, pg: page });
}
playerContent(key, flag, id, flags = []) {
return this.execute(key, 'playerContent', { flag, id, flags });
}
post(endpoint, data) {
return new Promise((resolve, reject) => {
const targetUrl = this.host + endpoint;
const parsedUrl = new URL(targetUrl);
const isHttps = parsedUrl.protocol === 'https:';
const client = isHttps ? https : http;
const postData = JSON.stringify(data);
const options = {
hostname: parsedUrl.hostname,
port: parsedUrl.port || (isHttps ? 443 : 80),
path: parsedUrl.pathname + parsedUrl.search,
method: 'POST',
headers: {
'Content-Type': 'application/json; charset=utf-8',
'Content-Length': Buffer.byteLength(postData)
},
timeout: this.timeout
};
const req = client.request(options, (res) => {
let body = '';
res.setEncoding('utf8');
res.on('data', (chunk) => body += chunk);
res.on('end', () => {
try {
resolve(JSON.parse(body));
} catch (e) {
resolve({ code: -1, msg: 'Invalid JSON response' });
}
});
});
req.on('error', (e) => {
resolve({ code: -1, msg: `Request failed: ${e.message}` });
});
req.on('timeout', () => {
req.destroy();
resolve({ code: -1, msg: 'Request timeout' });
});
req.write(postData);
req.end();
});
}
}
// ============================================================================
// Type 4 代理
// ============================================================================
class SpiderApiProxy {
constructor(client, defaultKey = '') {
this.client = client;
this.defaultKey = defaultKey;
}
async handle(req, res) {
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
if (req.method === 'OPTIONS') {
res.statusCode = 204;
res.end();
return;
}
try {
const result = await this.dispatch(req);
res.end(JSON.stringify(result, null, 2));
} catch (e) {
res.end(JSON.stringify({ error: e.message }));
}
}
async dispatch(req) {
const input = await this.getInput(req);
// 从 PATH_INFO 或 query 参数获取 key
let key = this.getKeyFromPath(req);
if (!key) {
key = input.key || this.defaultKey;
}
// 兼容处理:play 参数作为 id 的别名
if (input.play && !input.id) {
input.id = input.play;
}
// 根据参数特征判断接口类型
let ac = '';
if (input.wd) {
// 搜索接口
ac = 'search';
} else if (input.t) {
// 有 t 参数就是分类列表
ac = 'list';
} else if (input.ids) {
// 有 ids 参数就是详情
ac = 'detail';
} else if (input.flag && input.id) {
// 有 flag 和 id 就是播放
ac = 'play';
} else if (!key || key === 'config' || input.ac === 'config') {
// 配置接口
ac = 'config';
} else {
// 默认是首页/列表
ac = 'list';
}
// 处理配置接口
if (ac === 'config') {
return await this.getProxiedConfig(req);
}
// 验证 key
if (!key) {
return { error: '缺少 key 参数' };
}
// 调用 Spider API
let apiResult = null;
const pg = parseInt(input.pg) || 1;
// 路由分发
switch (ac) {
case 'search':
apiResult = await this.client.searchContent(key, input.wd, pg);
break;
case 'detail':
const ids = Array.isArray(input.ids) ? input.ids : input.ids.split(',');
apiResult = await this.client.detailContent(key, ids);
break;
case 'play':
const flags = input.flags ? input.flags.split(',') : [];
apiResult = await this.client.playerContent(key, input.flag, input.id, flags);
// 播放接口直接返回原始数据,不需要格式化
if (apiResult && apiResult.code === 0) {
return apiResult.data || apiResult;
}
return { error: apiResult?.msg || '请求失败' };
case 'list':
default:
const t = input.t || '';
const filter = !input.f || input.f === '1';
if (!t) {
apiResult = await this.client.homeContent(key, filter);
} else {
const extend = {};
['area', 'year', 'type', 'class', 'lang'].forEach(field => {
if (input[field]) {
extend[field] = input[field];
}
});
apiResult = await this.client.categoryContent(key, t, pg, filter, extend);
}
break;
}
// 返回苹果CMS标准格式
if (apiResult && apiResult.code === 0) {
return this.formatMacCmsResponse(apiResult.data, pg);
}
// 错误情况
return { error: apiResult?.msg || '请求失败' };
}
getKeyFromPath(req) {
const pathname = req.url.split('?')[0];
// 移除开头的斜杠并获取第一段路径
const pathParts = pathname.split('/').filter(p => p);
// 如果有路径段,返回第一个(假设是 key),并进行 URL 解码
if (pathParts.length > 0) {
try {
return decodeURIComponent(pathParts[0]);
} catch (e) {
return pathParts[0];
}
}
return '';
}
formatMacCmsResponse(data, currentPage = 1) {
// 获取 list
const list = data.list || [];
const listCount = list.length;
// 从返回数据中获取或动态计算
const page = data.page ? parseInt(data.page) : currentPage;
const limit = data.limit ? parseInt(data.limit) : (listCount > 0 ? listCount : 20);
// 计算总页数:优先使用返回值,否则写死 9999
const pagecount = data.pagecount ? parseInt(data.pagecount) : 9999;
// 计算总数:优先使用返回值,否则用 pagecount * limit
const total = data.total ? parseInt(data.total) : pagecount * limit;
// 苹果CMS标准返回格式
const response = {
page,
pagecount,
limit,
total,
list
};
if (data.class) {
response.class = data.class;
}
if (data.filters) {
response.filters = data.filters;
}
return response;
}
async getProxiedConfig(req) {
const status = await this.client.configAction('status');
if (status.code !== 0 || !status.data?.sites) {
return { error: '获取配置失败' };
}
const sites = status.data.sites;
const baseUrl = this.getBaseUrl(req);
const config = {
spider: '',
wallpaper: 'https://深色壁纸.xxooo.cf/',
warningText: '资源来自网络,仅供学习使用',
sites: [],
doh: this.getDefaultDoh(),
rules: this.getDefaultRules(),
lives: []
};
sites.forEach(site => {
config.sites.push({
key: site.key,
name: site.name,
api: `${baseUrl}/${site.key}`,
type: '4',
searchable: site.searchable ?? 1,
quickSearch: site.quickSearch ?? 1,
filterable: site.filterable ?? 1
});
});
return config;
}
getBaseUrl(req) {
const protocol = req.connection.encrypted ? 'https' : 'http';
const host = req.headers.host || 'localhost';
return `${protocol}://${host}`;
}
getDefaultDoh() {
return [
{ name: 'Google', url: 'https://dns.google/dns-query', ips: ['8.8.4.4', '8.8.8.8'] },
{ name: 'Cloudflare', url: 'https://cloudflare-dns.com/dns-query', ips: ['1.1.1.1', '1.0.0.1'] }
];
}
getDefaultRules() {
return [
{ name: 'lz', hosts: ['vip.lz', 'hd.lz'], regex: ['18.5333'] },
{ name: '非凡', hosts: ['vip.ffzy', 'hd.ffzy'], regex: ['25.0666'] }
];
}
async getInput(req) {
// 解析查询参数
const queryIndex = req.url.indexOf('?');
const input = {};
if (queryIndex !== -1) {
const searchParams = new URLSearchParams(req.url.slice(queryIndex + 1));
for (const [key, value] of searchParams) {
// 对参数进行 URL 解码
try {
input[key] = decodeURIComponent(value);
} catch (e) {
input[key] = value;
}
}
}
// 处理 POST body
if (req.method === 'POST') {
const body = await this.readBody(req);
try {
const jsonBody = JSON.parse(body);
Object.assign(input, jsonBody);
} catch (e) {
// 忽略非 JSON body
}
}
return input;
}
readBody(req) {
return new Promise((resolve) => {
let body = '';
req.on('data', chunk => body += chunk);
req.on('end', () => resolve(body));
req.on('error', () => resolve(''));
});
}
}
// ============================================================================
// 主入口
// ============================================================================
function main() {
// 解析命令行参数
const args = process.argv.slice(2);
let port = 10998;
const host = '0.0.0.0';
// 支持 --port 或 -p 参数
for (let i = 0; i < args.length; i++) {
if ((args[i] === '--port' || args[i] === '-p') && args[i + 1]) {
port = parseInt(args[i + 1]);
break;
}
}
// 创建客户端和代理
const client = new SpiderApiClient(SPIDER_API_HOST, REQUEST_TIMEOUT);
const proxy = new SpiderApiProxy(client, DEFAULT_SITE_KEY);
// 创建 HTTP 服务器
const server = http.createServer((req, res) => {
proxy.handle(req, res);
});
server.listen(port, host, () => {
console.log(`Spider API Type 4 代理服务器已启动`);
console.log(`监听地址: http://${host}:${port}`);
// 获取局域网IP地址
const networkInterfaces = os.networkInterfaces();
const lanIps = [];
for (const name of Object.keys(networkInterfaces)) {
for (const net of networkInterfaces[name]) {
// 跳过内部地址和非IPv4地址
if (net.family === 'IPv4' && !net.internal) {
lanIps.push(net.address);
}
}
}
if (lanIps.length > 0) {
console.log(`局域网地址:`);
lanIps.forEach(ip => {
console.log(` http://${ip}:${port}`);
});
}
console.log(`Spider API: ${SPIDER_API_HOST}`);
console.log(`\n使用示例:`);
console.log(` 配置接口: http://localhost:${port}?ac=config`);
console.log(` 首页接口: http://localhost:${port}/站源key`);
console.log(` 分类接口: http://localhost:${port}/站源key?t=分类ID&pg=页码`);
console.log(` 详情接口: http://localhost:${port}/站源key?ids=视频ID`);
console.log(` 搜索接口: http://localhost:${port}/站源key?wd=关键词&pg=页码`);
console.log(` 播放接口: http://localhost:${port}/站源key?flag=播放源&id=播放ID`);
console.log(` 或: http://localhost:${port}/站源key?flag=播放源&play=播放ID`);
console.log(`\n提示: ac参数可省略,系统会根据其他参数自动识别接口类型`);
console.log(`筛选参数: area=地区&year=年份&type=类型&class=剧情&lang=语言`);
});
server.on('error', (e) => {
if (e.code === 'EADDRINUSE') {
console.error(`错误: 端口 ${port} 已被占用,请强制结束app或更换端口后重新运行`);
} else {
console.error('服务器错误:', e.message);
}
process.exit(1);
});
}
if (require.main === module) {
main();
}
module.exports = { SpiderApiClient, SpiderApiProxy };