mirror of
https://github.com/fishtank-dashboard/fishtank-dashboard.git
synced 2026-05-02 05:02:03 -04:00
162 lines
4.8 KiB
JavaScript
162 lines
4.8 KiB
JavaScript
const http = require('http');
|
|
const https = require('https');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const url = require('url');
|
|
|
|
const PORT = 3000;
|
|
const API_TARGET = 'api.fishtank.live';
|
|
const CAM_TARGET = 'epyc.goran.jetzt';
|
|
|
|
function fetchRemote(hostname, targetPath, callback) {
|
|
const options = {
|
|
hostname,
|
|
port: 443,
|
|
path: targetPath,
|
|
method: 'GET',
|
|
headers: {
|
|
'host': hostname,
|
|
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
|
'accept': '*/*',
|
|
'origin': 'https://www.fishtank.live',
|
|
'referer': 'https://www.fishtank.live/',
|
|
},
|
|
};
|
|
|
|
const req = https.request(options, (remoteRes) => {
|
|
const chunks = [];
|
|
remoteRes.on('data', chunk => chunks.push(chunk));
|
|
remoteRes.on('end', () => callback(null, remoteRes, Buffer.concat(chunks)));
|
|
});
|
|
req.on('error', err => callback(err));
|
|
req.end();
|
|
}
|
|
|
|
function rewriteM3u8(body, basePath) {
|
|
// basePath = e.g. /dirc-5/
|
|
return body.split('\n').map(line => {
|
|
const trimmed = line.trim();
|
|
if (!trimmed || trimmed.startsWith('#')) {
|
|
// Rewrite URI= attributes inside tag lines e.g. URI="tracks-a1/index.fmp4.m3u8"
|
|
return trimmed.replace(/URI="([^"]+)"/g, (match, uri) => {
|
|
if (uri.startsWith('http')) return match;
|
|
return `URI="http://localhost:${PORT}/cam${basePath}${uri}"`;
|
|
});
|
|
}
|
|
if (trimmed.startsWith('http')) return trimmed;
|
|
return `http://localhost:${PORT}/cam${basePath}${trimmed}`;
|
|
}).join('\n');
|
|
}
|
|
|
|
function proxyApi(req, res, targetPath) {
|
|
const options = {
|
|
hostname: API_TARGET,
|
|
port: 443,
|
|
path: targetPath,
|
|
method: req.method,
|
|
headers: {
|
|
...req.headers,
|
|
host: API_TARGET,
|
|
origin: 'https://www.fishtank.live',
|
|
referer: 'https://www.fishtank.live/',
|
|
},
|
|
};
|
|
delete options.headers['accept-encoding'];
|
|
|
|
const proxy = https.request(options, (apiRes) => {
|
|
res.writeHead(apiRes.statusCode, {
|
|
...apiRes.headers,
|
|
'access-control-allow-origin': '*',
|
|
'access-control-allow-headers': 'Authorization, Content-Type',
|
|
});
|
|
apiRes.pipe(res);
|
|
});
|
|
proxy.on('error', err => { res.writeHead(502); res.end(err.message); });
|
|
req.pipe(proxy);
|
|
}
|
|
|
|
const server = http.createServer((req, res) => {
|
|
const parsed = url.parse(req.url);
|
|
|
|
if (req.method === 'OPTIONS') {
|
|
res.writeHead(204, {
|
|
'access-control-allow-origin': '*',
|
|
'access-control-allow-methods': 'GET, POST, OPTIONS',
|
|
'access-control-allow-headers': 'Authorization, Content-Type',
|
|
});
|
|
res.end();
|
|
return;
|
|
}
|
|
|
|
// Serve dashboard
|
|
if (parsed.pathname === '/' || parsed.pathname === '/dashboard') {
|
|
const file = path.join(__dirname, 'fishtank-dashboard.html');
|
|
fs.readFile(file, (err, data) => {
|
|
if (err) { res.writeHead(404); res.end('Dashboard not found'); return; }
|
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
res.end(data);
|
|
});
|
|
return;
|
|
}
|
|
|
|
// API proxy
|
|
if (parsed.pathname.startsWith('/api/')) {
|
|
proxyApi(req, res, parsed.pathname.replace('/api', '') + (parsed.search || ''));
|
|
return;
|
|
}
|
|
|
|
// Camera proxy
|
|
if (parsed.pathname.startsWith('/cam/')) {
|
|
const targetPath = parsed.pathname.replace('/cam', '') + (parsed.search || '');
|
|
const isM3u8 = targetPath.includes('.m3u8');
|
|
|
|
if (!isM3u8) {
|
|
// Raw segment — pipe directly
|
|
fetchRemote(CAM_TARGET, targetPath, (err, remoteRes, body) => {
|
|
if (err) { res.writeHead(502); res.end(err.message); return; }
|
|
res.writeHead(remoteRes.statusCode, {
|
|
...remoteRes.headers,
|
|
'access-control-allow-origin': '*',
|
|
});
|
|
res.end(body);
|
|
});
|
|
return;
|
|
}
|
|
|
|
// M3u8 — fetch and rewrite relative URLs
|
|
fetchRemote(CAM_TARGET, targetPath, (err, remoteRes, body) => {
|
|
if (err) { res.writeHead(502); res.end(err.message); return; }
|
|
if (remoteRes.statusCode !== 200) {
|
|
res.writeHead(remoteRes.statusCode);
|
|
res.end(body);
|
|
return;
|
|
}
|
|
|
|
// Base path = directory containing this m3u8
|
|
// e.g. /dirc-5/tracks-v2/index.fmp4.m3u8 → /dirc-5/tracks-v2/
|
|
const basePath = targetPath.substring(0, targetPath.lastIndexOf('/') + 1);
|
|
const rewritten = rewriteM3u8(body.toString('utf8'), basePath);
|
|
|
|
console.log(`✓ m3u8 ${targetPath} (${rewritten.split('\n').length} lines)`);
|
|
|
|
res.writeHead(200, {
|
|
'content-type': 'application/vnd.apple.mpegurl',
|
|
'access-control-allow-origin': '*',
|
|
'cache-control': 'no-cache',
|
|
});
|
|
res.end(rewritten);
|
|
});
|
|
return;
|
|
}
|
|
|
|
res.writeHead(404);
|
|
res.end('Not found');
|
|
});
|
|
|
|
server.listen(PORT, () => {
|
|
console.log('✓ Dashboard → http://localhost:' + PORT);
|
|
console.log('✓ Test cam → http://localhost:' + PORT + '/cam/dirc-5/index.m3u8');
|
|
console.log('\nPress Ctrl+C to stop');
|
|
});
|
|
// This line intentionally left blank
|