mirror of
https://github.com/fishtank-dashboard/fishtank-dashboard.git
synced 2026-05-02 04:52:04 -04:00
minor bug fixes
This commit is contained in:
committed by
GitHub
parent
46e2c58ba8
commit
e398ad69c3
118
server.js
118
server.js
@@ -3,11 +3,13 @@ const https = require('https');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const url = require('url');
|
||||
const zlib = require('zlib');
|
||||
const WebSocket = require('ws');
|
||||
|
||||
const PORT = 3000;
|
||||
const API_TARGET = 'api.fishtank.live';
|
||||
const CAM_TARGET = 'epyc.goran.jetzt';
|
||||
const CAM_PORT = 443;
|
||||
const FT_WS_URL = 'wss://ws.fishtank.live/socket.io/?EIO=4&transport=websocket';
|
||||
|
||||
// ── Local WS clients (dashboard connections) ────────────────
|
||||
@@ -304,6 +306,51 @@ function connectFishtankWS(token) {
|
||||
}
|
||||
|
||||
// ── HTTP helpers ─────────────────────────────────────────────
|
||||
// Per-slug cookie store — tkn cookie from port 444 responses
|
||||
const camCookies = {};
|
||||
|
||||
function fetchCam(slug, subPath, callback) {
|
||||
// Step 1: request from port 443 (follows redirect to 444 manually)
|
||||
const path443 = `/hls/${slug}/${subPath}`;
|
||||
const cookie = camCookies[slug] ? `tkn=${camCookies[slug]}` : '';
|
||||
|
||||
const doRequest = (port, reqPath, reqCookie) => {
|
||||
const opts = {
|
||||
hostname: CAM_TARGET, port, path: reqPath, method: 'GET',
|
||||
headers: {
|
||||
'host': port === 443 ? CAM_TARGET : `${CAM_TARGET}:444`,
|
||||
'user-agent': 'VLC/3.0.20 LibVLC/3.0.20',
|
||||
'accept': '*/*',
|
||||
...(reqCookie ? { 'cookie': reqCookie } : {}),
|
||||
},
|
||||
};
|
||||
const req = https.request(opts, (res) => {
|
||||
const chunks = [];
|
||||
res.on('data', c => chunks.push(c));
|
||||
res.on('end', () => {
|
||||
const body = Buffer.concat(chunks);
|
||||
// Store cookie from Set-Cookie header
|
||||
const setCookie = res.headers['set-cookie'];
|
||||
if (setCookie) {
|
||||
const match = (Array.isArray(setCookie) ? setCookie.join(';') : setCookie).match(/tkn=(\d+)/);
|
||||
if (match) camCookies[slug] = match[1];
|
||||
}
|
||||
// Follow redirect to port 444
|
||||
if (res.statusCode === 302 && res.headers.location) {
|
||||
const loc = res.headers.location;
|
||||
const m = loc.match(/epyc\.goran\.jetzt:444(\/.*)/);
|
||||
if (m) { doRequest(444, m[1], camCookies[slug] ? `tkn=${camCookies[slug]}` : ''); return; }
|
||||
}
|
||||
callback(null, res, body);
|
||||
});
|
||||
});
|
||||
req.on('error', err => callback(err));
|
||||
req.end();
|
||||
};
|
||||
|
||||
doRequest(443, path443, cookie);
|
||||
}
|
||||
|
||||
function fetchRemote(hostname, targetPath, callback) {
|
||||
const req = https.request({
|
||||
hostname, port: 443, path: targetPath, method: 'GET',
|
||||
@@ -324,14 +371,15 @@ function fetchRemote(hostname, targetPath, callback) {
|
||||
}
|
||||
|
||||
function rewriteM3u8(body, basePath) {
|
||||
// basePath examples: "dirc-5/" or "dirc-5/0_1/"
|
||||
// Never starts with slash — strip it just in case
|
||||
const base = basePath.replace(/^\//, '');
|
||||
return body.split('\n').map(line => {
|
||||
const t = line.trim();
|
||||
if (!t || t.startsWith('#')) {
|
||||
return t.replace(/URI="([^"]+)"/g, (m, uri) =>
|
||||
uri.startsWith('http') ? m : `URI="http://localhost:${PORT}/cam${basePath}${uri}"`);
|
||||
}
|
||||
if (!t || t.startsWith('#')) return t;
|
||||
if (t.startsWith('http')) return t;
|
||||
return `http://localhost:${PORT}/cam${basePath}${t}`;
|
||||
if (t.startsWith('/')) return `http://localhost:${PORT}/cam${t}`;
|
||||
return `http://localhost:${PORT}/cam/${base}${t}`;
|
||||
}).join('\n');
|
||||
}
|
||||
|
||||
@@ -399,18 +447,60 @@ const server = http.createServer((req, res) => {
|
||||
}
|
||||
|
||||
if (parsed.pathname.startsWith('/cam/')) {
|
||||
const targetPath = parsed.pathname.replace('/cam', '') + (parsed.search || '');
|
||||
if (!targetPath.includes('.m3u8')) {
|
||||
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;
|
||||
// Extract slug from any cam path variant
|
||||
const camPart = parsed.pathname.replace('/cam', '').replace('/hls', '').replace(/^\//, '');
|
||||
const parts = camPart.split('/');
|
||||
const slug = parts[0];
|
||||
|
||||
// Is this a media segment request? e.g. /cam/dirc-5/0_1/seg001.ts
|
||||
const isSegment = parsed.pathname.match(/\.(ts|fmp4|mp4|m4s)(\?|$)/i);
|
||||
const isSubPlaylist = parts.length > 2 && !isSegment; // e.g. dirc-5/0_1/index.m3u8
|
||||
|
||||
if (isSegment || isSubPlaylist) {
|
||||
// Sub-playlist or segment — reconstruct path with cookie token
|
||||
let subPath = parts.slice(1).join('/');
|
||||
const tkn = camCookies[slug];
|
||||
if (tkn) subPath += (subPath.includes('?') ? '&' : '?') + 'tkn=' + tkn;
|
||||
const opts = {
|
||||
hostname: CAM_TARGET, port: 444,
|
||||
path: '/hls/' + slug + '/' + subPath, method: 'GET',
|
||||
headers: {
|
||||
'host': CAM_TARGET + ':444',
|
||||
'user-agent': 'VLC/3.0.20 LibVLC/3.0.20',
|
||||
'accept': '*/*',
|
||||
...(tkn ? { 'cookie': 'tkn=' + tkn } : {}),
|
||||
},
|
||||
};
|
||||
const preq = https.request(opts, (pres) => {
|
||||
const chunks = [];
|
||||
pres.on('data', c => chunks.push(c));
|
||||
pres.on('end', () => {
|
||||
const body = Buffer.concat(chunks);
|
||||
if (!isSegment && pres.statusCode === 200) {
|
||||
const rawText = body.toString('utf8');
|
||||
// Log first segment line to debug path format
|
||||
const firstSeg = rawText.split('\n').find(l => l.trim() && !l.startsWith('#'));
|
||||
console.log('[CAM] sub-playlist first line:', JSON.stringify(firstSeg));
|
||||
const basePath = slug + '/' + parts.slice(1, -1).join('/') + '/';
|
||||
const rewritten = rewriteM3u8(rawText, basePath);
|
||||
res.writeHead(200, { 'content-type': 'application/vnd.apple.mpegurl', 'access-control-allow-origin': '*', 'cache-control': 'no-cache' });
|
||||
res.end(rewritten);
|
||||
} else {
|
||||
res.writeHead(pres.statusCode, { ...pres.headers, 'access-control-allow-origin': '*' });
|
||||
res.end(body);
|
||||
}
|
||||
});
|
||||
});
|
||||
preq.on('error', err => { res.writeHead(502); res.end(err.message); });
|
||||
preq.end();
|
||||
return;
|
||||
}
|
||||
fetchRemote(CAM_TARGET, targetPath, (err, remoteRes, body) => {
|
||||
|
||||
// Master playlist — use fetchCam which handles redirect + cookie storage
|
||||
fetchCam(slug, 'index.m3u8', (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; }
|
||||
const basePath = targetPath.substring(0, targetPath.lastIndexOf('/') + 1);
|
||||
const basePath = slug + '/';
|
||||
const rewritten = rewriteM3u8(body.toString('utf8'), basePath);
|
||||
res.writeHead(200, { 'content-type': 'application/vnd.apple.mpegurl', 'access-control-allow-origin': '*', 'cache-control': 'no-cache' });
|
||||
res.end(rewritten);
|
||||
|
||||
Reference in New Issue
Block a user