TV / server.js
samlax12's picture
Update server.js
240ee8f verified
raw
history blame contribute delete
10.4 kB
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const path = require('path');
const crypto = require('crypto');
const fs = require('fs');
const app = express();
const PORT = process.env.PORT || 8080;
// 中间件:解析请求体
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// 中间件:处理HTML文件中的环境变量注入和强制设置脚本
app.use((req, res, next) => {
if (req.path.endsWith('.html') || req.path === '/' || req.path.endsWith('/')) {
const filePath = req.path === '/' || req.path.endsWith('/')
? path.join(__dirname, 'index.html')
: path.join(__dirname, req.path);
if (fs.existsSync(filePath)) {
let content = fs.readFileSync(filePath, 'utf8');
// 替换密码占位符
const password = process.env.PASSWORD || '';
let passwordHash = '';
if (password) {
const hash = crypto.createHash('sha256');
hash.update(password);
passwordHash = hash.digest('hex');
}
content = content.replace(
'window.__ENV__.PASSWORD = "{{PASSWORD}}";',
`window.__ENV__.PASSWORD = "${passwordHash}"; // SHA-256 hash`
);
// 注入强制设置脚本
const forcedSettingsScript = `
<script>
// 页面加载后立即执行
document.addEventListener('DOMContentLoaded', function() {
console.log('正在应用强制设置...');
// 选择所有API源
if (typeof API_SITES !== 'undefined') {
// 获取所有API源的键(不包括自定义API和aggregated)
window.selectedAPIs = Object.keys(API_SITES).filter(key => key !== 'aggregated' && key !== 'custom');
localStorage.setItem('selectedAPIs', JSON.stringify(window.selectedAPIs));
// 延迟执行,确保界面元素加载完成
setTimeout(function() {
// 选择所有API复选框
const apiCheckboxes = document.querySelectorAll('#apiCheckboxes input[type="checkbox"]');
apiCheckboxes.forEach(checkbox => {
checkbox.checked = true;
});
// 选择所有自定义API复选框
const customApiCheckboxes = document.querySelectorAll('#customApisList input[type="checkbox"]');
customApiCheckboxes.forEach(checkbox => {
checkbox.checked = true;
// 获取自定义API索引并添加到selectedAPIs
const customIndex = checkbox.dataset.customIndex;
if (customIndex) {
const customApiId = 'custom_' + customIndex;
if (!window.selectedAPIs.includes(customApiId)) {
window.selectedAPIs.push(customApiId);
}
}
});
// 更新selectedAPIs到localStorage
localStorage.setItem('selectedAPIs', JSON.stringify(window.selectedAPIs));
// 如果存在updateSelectedApiCount函数,更新API计数显示
if (typeof updateSelectedApiCount === 'function') {
updateSelectedApiCount();
}
}, 500);
}
// 默认关闭黄色内容过滤
localStorage.setItem('yellowFilterEnabled', 'false');
// 默认开启分片广告过滤
localStorage.setItem('adFilteringEnabled', 'true');
// 默认开启豆瓣热门推荐
localStorage.setItem('doubanEnabled', 'true');
// 如果页面上有相关元素,直接更新UI
setTimeout(function() {
// 更新黄色内容过滤开关
const yellowFilterToggle = document.getElementById('yellowFilterToggle');
if (yellowFilterToggle) {
yellowFilterToggle.checked = false;
}
// 更新分片广告过滤开关
const adFilterToggle = document.getElementById('adFilterToggle');
if (adFilterToggle) {
adFilterToggle.checked = true;
}
// 更新豆瓣热门开关
const doubanToggle = document.getElementById('doubanToggle');
if (doubanToggle) {
doubanToggle.checked = true;
}
// 尝试更新豆瓣区域可见性
if (typeof updateDoubanVisibility === 'function') {
updateDoubanVisibility();
} else if (window.updateDoubanVisibility) {
window.updateDoubanVisibility();
}
// 尝试调用全选API的函数
if (typeof selectAllAPIs === 'function') {
selectAllAPIs(true);
}
console.log('已应用强制设置');
}, 800);
});
</script>
`;
// 在</body>前插入脚本
content = content.replace('</body>', forcedSettingsScript + '</body>');
res.setHeader('Content-Type', 'text/html; charset=utf-8');
return res.send(content);
}
}
next();
});
// 创建代理中间件函数
function createDynamicProxy(req, res, next) {
// 从URL参数获取目标URL
const targetUrl = decodeURIComponent(req.params.url);
if (!targetUrl || !targetUrl.match(/^https?:\/\/.+/i)) {
return res.status(400).json({
success: false,
error: '无效的目标URL'
});
}
// 提取主机和协议
try {
const urlObj = new URL(targetUrl);
const target = `${urlObj.protocol}//${urlObj.host}`;
// 确保路径和查询参数包含的中文正确编码
let pathToProxy = urlObj.pathname;
// 处理查询参数,确保中文字符被正确编码
if (urlObj.search) {
// 分析查询参数
const searchParams = new URLSearchParams(urlObj.search);
// 重新创建查询字符串,确保中文编码正确
const encodedParams = new URLSearchParams();
for (const [key, value] of searchParams.entries()) {
// 对于特定参数(如wd=搜索词),确保正确编码
if (key === 'wd' || key === 'ids') {
// 确保中文搜索词被正确编码 - 先解码确保不重复编码,再重新编码
const decodedValue = decodeURIComponent(value);
encodedParams.append(key, decodedValue);
} else {
encodedParams.append(key, value);
}
}
// 重建查询字符串
pathToProxy += `?${encodedParams.toString()}`;
}
// 创建代理
const proxy = createProxyMiddleware({
target,
changeOrigin: true,
pathRewrite: () => pathToProxy,
secure: false,
onProxyReq: (proxyReq, req, res) => {
// 设置请求头
proxyReq.setHeader('User-Agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36');
proxyReq.setHeader('Accept', req.headers.accept || '*/*');
proxyReq.setHeader('Accept-Encoding', 'gzip, deflate');
proxyReq.setHeader('Accept-Language', 'zh-CN,zh;q=0.9,en;q=0.8');
proxyReq.setHeader('Referer', req.headers.referer || target);
// 如果是API请求,明确指定内容类型和编码
if (req.url.includes('/api.php/provide/vod/')) {
proxyReq.setHeader('Content-Type', 'application/json; charset=utf-8');
}
},
onProxyRes: (proxyRes, req, res) => {
// 设置CORS头
proxyRes.headers['access-control-allow-origin'] = '*';
proxyRes.headers['access-control-allow-methods'] = 'GET, HEAD, OPTIONS';
proxyRes.headers['access-control-allow-headers'] = '*';
// 设置缓存策略
proxyRes.headers['cache-control'] = 'public, max-age=86400';
// 确保API响应的内容类型正确包含编码
if (req.url.includes('/api.php/provide/vod/')) {
proxyRes.headers['content-type'] = 'application/json; charset=utf-8';
}
},
// 错误处理
onError: (err, req, res) => {
console.error(`[代理错误] ${err.message}`);
res.writeHead(500, {
'Content-Type': 'application/json; charset=utf-8'
});
res.end(JSON.stringify({
error: `代理请求失败: ${err.message}`
}));
}
});
proxy(req, res, next);
} catch (error) {
console.error(`代理错误: ${error.message}`);
return res.status(500).json({
success: false,
error: `代理请求失败: ${error.message}`
});
}
}
// 设置代理路由
app.use('/proxy/:url(*)', createDynamicProxy);
// OPTIONS请求处理
app.options('/proxy/:url(*)', (req, res) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', '*');
res.setHeader('Access-Control-Max-Age', '86400');
res.status(204).end();
});
// 确保所有响应使用正确的编码
app.use((req, res, next) => {
if (!res.headersSent && !req.path.startsWith('/proxy/')) {
res.setHeader('Content-Type', 'text/html; charset=utf-8');
}
next();
});
// 静态文件服务 - 所有其他请求
app.use(express.static(path.join(__dirname), {
maxAge: '1d',
setHeaders: (res, path) => {
// 为HTML文件设置正确的编码
if (path.endsWith('.html')) {
res.setHeader('Content-Type', 'text/html; charset=utf-8');
}
// 为CSS文件设置正确的编码
else if (path.endsWith('.css')) {
res.setHeader('Content-Type', 'text/css; charset=utf-8');
}
// 为JS文件设置正确的编码
else if (path.endsWith('.js')) {
res.setHeader('Content-Type', 'text/javascript; charset=utf-8');
}
}
}));
// 错误处理中间件
app.use((err, req, res, next) => {
console.error(`服务器错误: ${err.stack}`);
res.status(500).send('服务器内部错误');
});
// 启动服务器
app.listen(PORT, () => {
console.log(`LibreTV 服务器已启动,运行在 http://localhost:${PORT}`);
console.log(`代理服务可通过 http://localhost:${PORT}/proxy/{URL} 访问`);
});