|
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 })); |
|
|
|
|
|
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> |
|
`; |
|
|
|
|
|
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) { |
|
|
|
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()) { |
|
|
|
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); |
|
|
|
|
|
if (req.url.includes('/api.php/provide/vod/')) { |
|
proxyReq.setHeader('Content-Type', 'application/json; charset=utf-8'); |
|
} |
|
}, |
|
|
|
onProxyRes: (proxyRes, req, res) => { |
|
|
|
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'; |
|
|
|
|
|
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); |
|
|
|
|
|
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) => { |
|
|
|
if (path.endsWith('.html')) { |
|
res.setHeader('Content-Type', 'text/html; charset=utf-8'); |
|
} |
|
|
|
else if (path.endsWith('.css')) { |
|
res.setHeader('Content-Type', 'text/css; charset=utf-8'); |
|
} |
|
|
|
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} 访问`); |
|
}); |