mirror of
https://github.com/fishtank-dashboard/fishtank-dashboard.git
synced 2026-04-30 10:42:02 -04:00
Add files via upload
This commit is contained in:
168
README.md
Normal file
168
README.md
Normal file
@@ -0,0 +1,168 @@
|
||||
# Fishtank Monitor
|
||||
|
||||
A local dashboard for fishtank.live that shows live camera feeds, polls, TTS messages, stock prices, and more — all in one place without needing the main site open.
|
||||
|
||||
---
|
||||
|
||||
## What You Need
|
||||
|
||||
- A Windows, Mac, or Linux computer
|
||||
- A fishtank.live account
|
||||
- Node.js installed (free, takes 2 minutes)
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — Install Node.js
|
||||
|
||||
Node.js is a small program that lets your computer run the server that powers this dashboard.
|
||||
|
||||
1. Go to **https://nodejs.org**
|
||||
2. Click the big green **"LTS"** download button (the recommended version)
|
||||
3. Run the installer and click through — all default options are fine
|
||||
4. When it's done, you're ready
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — Set Up the Files
|
||||
|
||||
You should have two files:
|
||||
|
||||
- `fishtank-dashboard.html`
|
||||
- `server.js`
|
||||
|
||||
Put both files in the **same folder** anywhere on your computer (e.g. your Desktop or a folder called `fishtank`).
|
||||
|
||||
---
|
||||
|
||||
## Step 3 — Start the Server
|
||||
|
||||
**On Windows:**
|
||||
1. Open the folder where you put the files
|
||||
2. Click the address bar at the top of the folder window and type `cmd`, then press Enter
|
||||
3. A black window will appear. Type the following and press Enter:
|
||||
```
|
||||
node server.js
|
||||
```
|
||||
|
||||
**On Mac:**
|
||||
1. Open the Terminal app (search for it in Spotlight)
|
||||
2. Drag your folder into the Terminal window — this sets the location
|
||||
3. Type the following and press Enter:
|
||||
```
|
||||
node server.js
|
||||
```
|
||||
|
||||
You should see:
|
||||
```
|
||||
✓ Dashboard → http://localhost:3000
|
||||
✓ API proxy → http://localhost:3000/api/v1/...
|
||||
✓ Cam proxy → http://localhost:3000/cam/<stream>/index.m3u8
|
||||
```
|
||||
|
||||
**Leave this window open** — closing it stops the dashboard.
|
||||
|
||||
---
|
||||
|
||||
## Step 4 — Open the Dashboard
|
||||
|
||||
Open your browser and go to:
|
||||
|
||||
```
|
||||
http://localhost:3000
|
||||
```
|
||||
|
||||
The dashboard will load with camera feeds starting automatically.
|
||||
|
||||
---
|
||||
|
||||
## Step 5 — Link Your Account (Optional)
|
||||
|
||||
Linking your fishtank.live account unlocks polls, TTS messages, stock prices, and other live data. The cameras work without it.
|
||||
|
||||
**To get your token:**
|
||||
1. Go to **fishtank.live** and log in
|
||||
2. Press **F12** to open Developer Tools
|
||||
3. Go to **Storage** (Firefox) or **Application** (Chrome) tab
|
||||
4. Click **Cookies** → `https://www.fishtank.live`
|
||||
5. Find the cookie named `sb-wcsaaupukpdmqdjcgaoo-auth-token`
|
||||
6. Copy the entire **Value** field (it starts with `%5B%22eyJ...`)
|
||||
|
||||
**To link it:**
|
||||
1. Click the **LINK API** button in the top-left of the dashboard
|
||||
2. Paste the value you copied into the input field
|
||||
3. Press Enter or click **CONNECT**
|
||||
4. The dot turns green and shows **API LIVE** when connected
|
||||
|
||||
Your token is saved automatically — you won't need to do this again unless you log out or clear your browser data. The dashboard also refreshes the token automatically so it stays connected.
|
||||
|
||||
---
|
||||
|
||||
## Stopping the Server
|
||||
|
||||
In the terminal window where the server is running, press:
|
||||
```
|
||||
Ctrl + C
|
||||
```
|
||||
|
||||
If you closed the terminal window and need to stop it:
|
||||
|
||||
- **Windows:** Open Task Manager → find `node.exe` → End Task
|
||||
- **Mac/Linux:** Open Terminal and run `pkill node`
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
### 📹 Camera Feeds
|
||||
- **17 live camera feeds** from the fishtank house displayed as thumbnails
|
||||
- **Director Mode** plays as the main featured feed by default
|
||||
- **Click any thumbnail** to switch it to the featured view — Director Mode takes its place in the grid
|
||||
- **Click the cell showing Director Mode** in the grid to return to Director Mode
|
||||
- **Double-click** the main feed to go fullscreen. Press Escape or double-click again to exit
|
||||
- **Cameraman** feed displayed in its own panel next to the stocks chart, same switching behaviour
|
||||
- **THUMBS / LIVE toggle** — Thumbs mode refreshes a snapshot every 30 seconds (saves bandwidth). Live mode streams all cameras simultaneously
|
||||
|
||||
### 📊 Polls
|
||||
- Shows the current active poll with live vote bars
|
||||
- Displays the winner of the last poll
|
||||
- Narrative polls are labelled with a **NARRATIVE POLL** badge
|
||||
|
||||
### 💬 TTS Messages
|
||||
- Accumulates TTS messages as they play in the house
|
||||
- Shows the sender, voice used, room, status, cost, and timestamp
|
||||
- **Click any message** to jump to the camera for that room
|
||||
- A green/red dot next to the panel title shows whether TTS is currently enabled on the site
|
||||
|
||||
### 📈 Stocks
|
||||
- Line chart showing all contestant stock prices over time
|
||||
- Toggle between **1D**, **1H**, and **1W** views
|
||||
- Each stock has its own colour — click a stock in the legend to hide/show it
|
||||
- Percentage change shown next to each ticker with up/down arrows
|
||||
|
||||
### 📷 Capture Tools (in the camera panel header)
|
||||
- **📷 SNAP** — takes a full resolution PNG screenshot of the current featured camera
|
||||
- **✂ CLIP** — saves the last ~60 seconds of buffered footage as a WebM file
|
||||
- **⏺ REC / ⏹ STOP** — records forward from when you click. A live file size counter shows how large the recording is getting
|
||||
- **3.9MB LIMIT toggle** — when enabled, recording auto-stops before hitting 4MB (useful for sites with upload limits)
|
||||
- **🔇 CLIPS MUTED / 🔊 AUDIO toggle** — controls whether clips and recordings include audio
|
||||
|
||||
### ⚙️ Other Controls
|
||||
- **VOL slider** — controls the volume of the featured camera
|
||||
- **Interval selector** (5s / 10s / 30s / 60s) — how often the API data refreshes
|
||||
- **LOGOUT** — clears your stored token and disconnects from the API
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Cameras not loading:**
|
||||
Make sure the server is running (`node server.js`) and you're visiting `http://localhost:3000` — not opening the HTML file directly.
|
||||
|
||||
**"LINK API" not working:**
|
||||
Make sure you copied the full cookie value starting with `%5B%22eyJ`. Just the token alone (starting with `eyJ`) also works but won't enable auto-refresh.
|
||||
|
||||
**Server won't start:**
|
||||
Make sure Node.js is installed. Open a terminal and type `node --version` — if it prints a version number, Node is installed. If not, go back to Step 1.
|
||||
|
||||
**Port already in use:**
|
||||
Something else is using port 3000. Either stop that program or edit `server.js` and change `const PORT = 3000` to another number like `3001`, then visit `http://localhost:3001` instead.
|
||||
2067
fishtank-dashboard.html
Normal file
2067
fishtank-dashboard.html
Normal file
File diff suppressed because it is too large
Load Diff
161
server.js
Normal file
161
server.js
Normal file
@@ -0,0 +1,161 @@
|
||||
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
|
||||
Reference in New Issue
Block a user