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