Compare commits

...
6 Commits
Author SHA1 Message Date
kuba c0d1fee257 "too dig of a bick" 2026-02-24 15:27:39 +01:00
kuba 5d524eba56 some changes 2026-02-24 15:03:56 +01:00
kuba 648ef00bed sync to upstream 2026-02-24 14:44:48 +01:00
kuba 8a53bf1027 oh brother 2026-02-24 14:17:30 +01:00
kuba 722277c41f whoops 2026-02-24 14:16:50 +01:00
kuba ee25214160 some changes again 2026-02-24 14:15:52 +01:00
32 changed files with 528 additions and 541 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
node_modules/
/*.json
/serverlog.txt
/serverlog*.txt
/web/js/plugins/
/libraries/
/plugins/*
+1 -1
View File
@@ -3,7 +3,7 @@ require('./server/index.js');
/**
* FM-DX Webserver
*
* Github repo: https://github.com/NoobishSVK/fm-dx-webserver
* Github repo: https://github.com/KubaPro010/fm-dx-webserver
* Server files: /server
* Client files (web): /web
* Plugin files: /plugins
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "fm-dx-webserver",
"version": "1.4.0",
"version": "1.4.0a",
"description": "FM DX Webserver",
"main": "index.js",
"scripts": {
+35 -14
View File
@@ -3,12 +3,19 @@ const { serverConfig } = require('./server_config');
const { logChat } = require('./console');
const helpers = require('./helpers');
function heartbeat() { // WebSocket heartbeat helper
this.isAlive = true;
}
function createChatServer(storage) {
if (!serverConfig.webserver.chatEnabled) return null;
const chatWss = new WebSocket.Server({ noServer: true });
chatWss.on('connection', (ws, request) => {
ws.isAlive = true;
ws.on('pong', heartbeat);
const clientIp = request.headers['x-forwarded-for'] || request.socket.remoteAddress;
const userCommandHistory = {};
@@ -25,19 +32,18 @@ function createChatServer(storage) {
ws.send(JSON.stringify(historyMessage));
});
const ipMessage = {
ws.send(JSON.stringify({
type: 'clientIp',
ip: clientIp,
admin: request.session?.isAdminAuthenticated
};
}));
ws.send(JSON.stringify(ipMessage));
const userCommands = {};
let lastWarn = { time: 0 };
ws.on('message', (message) => {
helpers.antispamProtection(
message = helpers.antispamProtection(
message,
clientIp,
ws,
@@ -45,9 +51,12 @@ function createChatServer(storage) {
lastWarn,
userCommandHistory,
'5',
'chat'
'chat',
512
);
if(!message) return;
let messageData;
try {
@@ -57,23 +66,16 @@ function createChatServer(storage) {
return;
}
console.log("Chat message:", messageData);
delete messageData.admin;
delete messageData.ip;
delete messageData.time;
if (messageData.nickname != null) {
messageData.nickname = helpers.escapeHtml(String(messageData.nickname));
}
if (messageData.nickname != null) messageData.nickname = helpers.escapeHtml(String(messageData.nickname));
messageData.ip = clientIp;
const now = new Date();
messageData.time =
String(now.getHours()).padStart(2, '0') +
":" +
String(now.getMinutes()).padStart(2, '0');
messageData.time = String(now.getHours()).padStart(2, '0') + ":" + String(now.getMinutes()).padStart(2, '0');
if (serverConfig.webserver.banlist?.includes(clientIp)) return;
@@ -95,6 +97,25 @@ function createChatServer(storage) {
}
});
});
ws.on('close', () => {
ws.isAlive = false;
});
});
/**
* We will not always be receiving data, so some proxies may terminate the connection, this prevents it.
*/
const interval = setInterval(() => {
chatWss.clients.forEach((ws) => {
if (ws.isAlive === false) return ws.terminate();
ws.isAlive = false;
ws.ping();
});
}, 30000);
chatWss.on('close', () => {
clearInterval(interval);
});
return chatWss;
+3 -1
View File
@@ -3,7 +3,9 @@ const fs = require('fs').promises;
const verboseMode = process.argv.includes('--debug');
const verboseModeFfmpeg = process.argv.includes('--ffmpegdebug');
const LOG_FILE = 'serverlog.txt';
const LOG_FILE = process.argv.includes('--config') && process.argv[process.argv.indexOf('--config') + 1]
? `serverlog_${process.argv[process.argv.indexOf('--config') + 1]}.txt`
: 'serverlog.txt';
const ANSI_ESCAPE_CODE_PATTERN = /\x1b\[[0-9;]*m/g;
const MAX_LOG_LINES = 5000;
const FLUSH_INTERVAL = 60000;
+2 -3
View File
@@ -2,7 +2,7 @@
const RDSDecoder = require("./rds.js");
const { serverConfig } = require('./server_config');
const { fetchTx } = require('./tx_search.js');
const fetchTx = require('./tx_search.js');
const updateInterval = 75;
// Initialize the data object
@@ -193,8 +193,7 @@ function handleData(wss, receivedData, rdsWss) {
data += (((errors & 0x03) == 0) ? modifiedData.slice(12, 16) : '----');
const newDataString = "G:\r\n" + data + "\r\n\r\n";
const finalBuffer = Buffer.from(newDataString, 'utf-8');
client.send(finalBuffer);
client.send(newDataString);
});
rdsdec.decodeGroup(parseInt(modifiedData.slice(0, 4), 16), parseInt(modifiedData.slice(4, 8), 16), parseInt(modifiedData.slice(8, 12), 16), parseInt(modifiedData.slice(12, 16), 16));
+3 -2
View File
@@ -15,7 +15,7 @@ const tunerProfiles = require('./tuner_profiles');
const { logInfo, logs } = require('./console');
const dataHandler = require('./datahandler');
const fmdxList = require('./fmdx_list');
const { allPluginConfigs } = require('./plugins');
const allPluginConfigs = require('./plugins');
// Endpoints
router.get('/', (req, res) => {
@@ -87,7 +87,8 @@ router.get('/', (req, res) => {
});
router.get('/403', (req, res) => {
res.render('403');
const reason = req.query.reason || null;
res.render('403', { reason });
})
router.get('/wizard', (req, res) => {
+51 -19
View File
@@ -1,3 +1,5 @@
const fs = require('fs');
const path = require('path');
const http = require('http');
const https = require('https');
const net = require('net');
@@ -5,7 +7,7 @@ const crypto = require('crypto');
const dataHandler = require('./datahandler');
const storage = require('./storage');
const consoleCmd = require('./console');
const { serverConfig, configExists, configSave } = require('./server_config');
const { serverConfig, configSave } = require('./server_config');
function parseMarkdown(parsed) {
parsed = parsed.replace(/<\/?[^>]+(>|$)/g, '');
@@ -93,9 +95,7 @@ let bannedASCache = { data: null, timestamp: 0 };
function fetchBannedAS(callback) {
const now = Date.now();
if (bannedASCache.data && now - bannedASCache.timestamp < 10 * 60 * 1000) {
return callback(null, bannedASCache.data);
}
if (bannedASCache.data && now - bannedASCache.timestamp < 10 * 60 * 1000) return callback(null, bannedASCache.data);
const req = https.get("https://fmdx.org/banned_as.json", { family: 4 }, (banResponse) => {
let banData = "";
@@ -152,9 +152,7 @@ function processConnection(clientIp, locationInfo, currentUsers, ws, callback) {
}
const userLocation =
locationInfo.country === undefined
? "Unknown"
: `${locationInfo.city}, ${locationInfo.regionName}, ${locationInfo.countryCode}`;
locationInfo.country === undefined ? "Unknown" : `${locationInfo.city}, ${locationInfo.regionName}, ${locationInfo.countryCode}`;
storage.connectedUsers.push({
ip: clientIp,
@@ -252,12 +250,18 @@ function checkLatency(host, port = 80, timeout = 2000) {
});
}
function antispamProtection(message, clientIp, ws, userCommands, lastWarn, userCommandHistory, lengthCommands, endpointName) {
const command = message.toString();
function antispamProtection(message, clientIp, ws, userCommands, lastWarn, userCommandHistory, lengthCommands, endpointName, maxPayloadSize = 1024 * 1024) {
const rawCommand = message.toString();
const command = rawCommand.replace(/[\r\n]+/g, '');
const now = Date.now();
const normalizedClientIp = clientIp?.replace(/^::ffff:/, '');
if (endpointName === 'text') consoleCmd.logDebug(`Command received from \x1b[90m${clientIp}\x1b[0m: ${command}`);
if (command.length > maxPayloadSize) {
consoleCmd.logWarn(`Command from \x1b[90m${normalizedClientIp}\x1b[0m on \x1b[90m/${endpointName}\x1b[0m exceeded maximum payload size (${parseInt(command.length / 1024)} KB / ${parseInt(maxPayloadSize / 1024)} KB).`);
return "";
}
// Initialize user command history if not present
if (!userCommandHistory[clientIp]) userCommandHistory[clientIp] = [];
@@ -289,9 +293,7 @@ function antispamProtection(message, clientIp, ws, userCommands, lastWarn, userC
lastMessageTime = now;
// Initialize command history for rate-limiting checks
if (!userCommands[command]) {
userCommands[command] = [];
}
if (!userCommands[command]) userCommands[command] = [];
// Record the current timestamp for this command
userCommands[command].push(now);
@@ -313,15 +315,45 @@ function antispamProtection(message, clientIp, ws, userCommands, lastWarn, userC
}
const escapeHtml = (unsafe) => {
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
return unsafe.replace(/&/g, "&amp;")
.replace(/</g, "&lt;").replace(/>/g, "&gt;")
.replace(/"/g, "&quot;").replace(/'/g, "&#039;");
};
// Start plugins with delay
function startPluginsWithDelay(plugins, delay) {
plugins.forEach((pluginPath, index) => {
setTimeout(() => {
const pluginName = path.basename(pluginPath, '.js'); // Extract plugin name from path
consoleCmd.logInfo(`-----------------------------------------------------------------`);
consoleCmd.logInfo(`Plugin ${pluginName} loaded successfully!`);
require(pluginPath);
}, delay * index);
});
// Add final log line after all plugins are loaded
setTimeout(() => {
consoleCmd.logInfo(`-----------------------------------------------------------------`);
}, delay * plugins.length);
}
// Function to find server files based on the plugins listed in config
function findServerFiles(plugins) {
let results = [];
plugins.forEach(plugin => {
// Remove .js extension if present
if (plugin.endsWith('.js')) plugin = plugin.slice(0, -3);
const pluginPath = path.join(__dirname, '..', 'plugins', `${plugin}_server.js`);
if (fs.existsSync(pluginPath) && fs.statSync(pluginPath).isFile()) results.push(pluginPath);
});
return results;
}
module.exports = {
authenticateWithXdrd, parseMarkdown, handleConnect, removeMarkdown, formatUptime, resolveDataBuffer, kickClient, checkIPv6Support, checkLatency, antispamProtection, escapeHtml
authenticateWithXdrd, parseMarkdown, handleConnect,
removeMarkdown, formatUptime, resolveDataBuffer,
kickClient, checkIPv6Support, checkLatency,
antispamProtection, escapeHtml, findServerFiles,
startPluginsWithDelay
}
+11 -53
View File
@@ -1,4 +1,3 @@
// Library imports
const express = require('express');
const endpoints = require('./endpoints');
const session = require('express-session');
@@ -8,21 +7,16 @@ const readline = require('readline');
const app = express();
const httpServer = http.createServer(app);
const WebSocket = require('ws');
const wss = new WebSocket.Server({ noServer: true, perMessageDeflate: true });
const rdsWss = new WebSocket.Server({ noServer: true });
const pluginsWss = new WebSocket.Server({ noServer: true, perMessageDeflate: true });
const fs = require('fs');
const path = require('path');
const net = require('net');
const client = new net.Socket();
const { SerialPort } = require('serialport');
const tunnel = require('./tunnel');
const { createChatServer } = require('./chat');
const { createAudioServer } = require('./stream/ws.js');
const figlet = require('figlet');
// File imports
const helpers = require('./helpers');
const { findServerFiles, startPluginsWithDelay } = helpers;
const dataHandler = require('./datahandler');
const fmdxList = require('./fmdx_list');
const { logError, logInfo, logWarn } = require('./console');
@@ -31,35 +25,10 @@ const { serverConfig, configExists } = require('./server_config');
const pluginsApi = require('./plugins_api');
const pjson = require('../package.json');
// Function to find server files based on the plugins listed in config
function findServerFiles(plugins) {
let results = [];
plugins.forEach(plugin => {
// Remove .js extension if present
if (plugin.endsWith('.js')) plugin = plugin.slice(0, -3);
const pluginPath = path.join(__dirname, '..', 'plugins', `${plugin}_server.js`);
if (fs.existsSync(pluginPath) && fs.statSync(pluginPath).isFile()) results.push(pluginPath);
});
return results;
}
// Start plugins with delay
function startPluginsWithDelay(plugins, delay) {
plugins.forEach((pluginPath, index) => {
setTimeout(() => {
const pluginName = path.basename(pluginPath, '.js'); // Extract plugin name from path
logInfo(`-----------------------------------------------------------------`);
logInfo(`Plugin ${pluginName} loaded successfully!`);
require(pluginPath);
}, delay * index);
});
// Add final log line after all plugins are loaded
setTimeout(() => {
logInfo(`-----------------------------------------------------------------`);
}, delay * plugins.length);
}
const client = new net.Socket();
const wss = new WebSocket.Server({ noServer: true, perMessageDeflate: true });
const rdsWss = new WebSocket.Server({ noServer: true });
const pluginsWss = new WebSocket.Server({ noServer: true, perMessageDeflate: true });
// Get all plugins from config and find corresponding server files
const plugins = findServerFiles(serverConfig.plugins);
@@ -76,22 +45,12 @@ const terminalWidth = readline.createInterface({
output: process.stdout
}).output.columns;
figlet("FM-DX Webserver", function (err, data) {
if (err) {
console.log("Something went wrong...");
console.dir(err);
return;
}
console.log('\x1b[32m' + data);
});
console.log('\x1b[32m' + figlet.textSync("FM-DX Webserver"));
console.log('\x1b[32m\x1b[2mby Noobish @ \x1b[4mFMDX.org\x1b[0m');
console.log("v" + pjson.version)
console.log('\x1b[90m' + '─'.repeat(terminalWidth - 1) + '\x1b[0m');
const chatWss = createChatServer(storage);
const audioWss = createAudioServer();
// Start ffmpeg
const audioWss = require('./stream/ws.js');
require('./stream/index');
require('./plugins');
@@ -107,6 +66,7 @@ const sessionMiddleware = session({
});
app.use(sessionMiddleware);
app.use(bodyParser.json());
const chatWss = createChatServer(storage);
connectToXdrd();
connectToSerial();
@@ -237,9 +197,7 @@ client.on('data', (data) => {
const { xdrd } = serverConfig;
helpers.resolveDataBuffer(data, wss, rdsWss);
if (authFlags.authMsg == true && authFlags.messageCount > 1) {
return;
}
if (authFlags.authMsg == true && authFlags.messageCount > 1) return;
authFlags.messageCount++;
const receivedData = data.toString();
@@ -398,7 +356,7 @@ wss.on('connection', (ws, request) => {
let lastWarn = { time: 0 };
ws.on('message', (message) => {
const command = helpers.antispamProtection(message, clientIp, ws, userCommands, lastWarn, userCommandHistory, '18', 'text');
const command = helpers.antispamProtection(message, clientIp, ws, userCommands, lastWarn, userCommandHistory, '18', 'text', 16 * 1024);
if (!clientIp.includes("127.0.0.1")) {
if (((command.startsWith('X') || command.startsWith('Y')) && !request.session.isAdminAuthenticated) ||
+1 -3
View File
@@ -93,6 +93,4 @@ function createLinks() {
const allPluginConfigs = collectPluginConfigs();
createLinks();
module.exports = {
allPluginConfigs
};
module.exports = allPluginConfigs;
+10 -10
View File
@@ -93,8 +93,8 @@ class RDSDecoder {
this.ps[idx * 2] = String.fromCharCode(blockD >> 8);
this.ps[idx * 2 + 1] = String.fromCharCode(blockD & 0xFF);
this.ps_errors[idx * 2] = error;
this.ps_errors[idx * 2 + 1] = error;
this.ps_errors[idx * 2] = Math.ceil(d_error * (10/3));
this.ps_errors[idx * 2 + 1] = Math.ceil(d_error * (10/3));
this.data.ps = this.ps.join('');
this.data.ps_errors = this.ps_errors.join(',');
@@ -124,15 +124,15 @@ class RDSDecoder {
if(c_error < 2 && multiplier !== 2) {
this.rt1[idx * multiplier] = String.fromCharCode(blockC >> 8);
this.rt1[idx * multiplier + 1] = String.fromCharCode(blockC & 0xFF);
this.rt1_errors[idx * multiplier] = error;
this.rt1_errors[idx * multiplier + 1] = error;
this.rt1_errors[idx * multiplier] = Math.ceil(c_error * (10/3));
this.rt1_errors[idx * multiplier + 1] = Math.ceil(c_error * (10/3));
}
if(d_error < 2) {
var offset = (multiplier == 2) ? 0 : 2;
this.rt1[idx * multiplier + offset] = String.fromCharCode(blockD >> 8);
this.rt1[idx * multiplier + offset + 1] = String.fromCharCode(blockD & 0xFF);
this.rt1_errors[idx * multiplier + offset] = error;
this.rt1_errors[idx * multiplier + offset + 1] = error;
this.rt1_errors[idx * multiplier + offset] = Math.ceil(d_error * (10/3));
this.rt1_errors[idx * multiplier + offset + 1] = Math.ceil(d_error * (10/3));
}
var i = this.rt1.indexOf("\r")
@@ -155,15 +155,15 @@ class RDSDecoder {
if(c_error !== 3 && multiplier !== 2) {
this.rt0[idx * multiplier] = String.fromCharCode(blockC >> 8);
this.rt0[idx * multiplier + 1] = String.fromCharCode(blockC & 0xFF);
this.rt0_errors[idx * multiplier] = error;
this.rt0_errors[idx * multiplier + 1] = error;
this.rt0_errors[idx * multiplier] = Math.ceil(c_error * (10/3));
this.rt0_errors[idx * multiplier + 1] = Math.ceil(c_error * (10/3));
}
if(d_error !== 3) {
var offset = (multiplier == 2) ? 0 : 2;
this.rt0[idx * multiplier + offset] = String.fromCharCode(blockD >> 8);
this.rt0[idx * multiplier + offset + 1] = String.fromCharCode(blockD & 0xFF);
this.rt0_errors[idx * multiplier + offset] = error;
this.rt0_errors[idx * multiplier + offset + 1] = error;
this.rt0_errors[idx * multiplier + offset] = Math.ceil(d_error * (10/3));
this.rt0_errors[idx * multiplier + offset + 1] = Math.ceil(d_error * (10/3));
}
var i = this.rt0.indexOf("\r");
+2 -2
View File
@@ -21,7 +21,7 @@ checkFFmpeg().then((ffmpegPath) => {
logInfo(`${consoleLogTitle} Using ${ffmpegPath === 'ffmpeg' ? 'system-installed FFmpeg' : 'ffmpeg-static'}`);
logInfo(`${consoleLogTitle} Starting audio stream on device: \x1b[35m${serverConfig.audio.audioDevice}\x1b[0m`);
const sampleRate = Number(serverConfig.audio.sampleRate || 44100) + Number(serverConfig.audio.samplerateOffset || 0);
const sampleRate = Number(serverConfig.audio.sampleRate || 44100) + Number(serverConfig.audio.samplerateOffset || 0); // Maybe even do 32 khz, we do not need higher than 15 khz precision
const channels = Number(serverConfig.audio.audioChannels || 2);
@@ -139,4 +139,4 @@ checkFFmpeg().then((ffmpegPath) => {
logError(`${consoleLogTitle} Error: ${err.message}`);
});
module.exports.audio_pipe = audio_pipe;
module.exports = audio_pipe;
+10 -14
View File
@@ -1,32 +1,28 @@
const WebSocket = require('ws');
const { serverConfig } = require('../server_config');
const { audio_pipe } = require('./index.js');
const audio_pipe = require('./index.js');
function createAudioServer() {
const audioWss = new WebSocket.Server({ noServer: true });
const audioWss = new WebSocket.Server({ noServer: true, skipUTF8Validation: true });
audioWss.on('connection', (ws, request) => {
audioWss.on('connection', (ws, request) => {
const clientIp = request.headers['x-forwarded-for'] || request.socket.remoteAddress;
if (serverConfig.webserver.banlist?.includes(clientIp)) {
ws.close(1008, 'Banned IP');
return;
}
});
});
audio_pipe.on('data', (chunk) => {
audio_pipe.on('data', (chunk) => {
audioWss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) client.send(chunk, {binary: true, compress: false });
});
if (client.readyState === WebSocket.OPEN) client.send(chunk, {binary: true, compress: false});
});
});
audio_pipe.on('end', () => {
audio_pipe.on('end', () => {
audioWss.clients.forEach((client) => {
client.close(1001, "Audio stream ended");
});
});
});
return audioWss;
}
module.exports = { createAudioServer };
module.exports = audioWss;
+6 -18
View File
@@ -80,9 +80,7 @@ async function buildTxDatabase() {
consoleCmd.logInfo('Fetching transmitter database...');
const response = await fetch(`https://maps.fmdx.org/api?qth=${serverConfig.identification.lat},${serverConfig.identification.lon}`, {
method: 'GET',
headers: {
'Accept': 'application/json'
}
headers: {'Accept': 'application/json'}
});
if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`);
localDb = await response.json();
@@ -169,9 +167,7 @@ function getStateForCoordinates(lat, lon) {
for (const feature of usStatesGeoJson.features) {
const boundingBox = getStateBoundingBox(feature.geometry.coordinates);
if (isCityInState(lat, lon, boundingBox)) {
return feature.properties.name; // Return the state's name if city is inside bounding box
}
if (isCityInState(lat, lon, boundingBox)) return feature.properties.name; // Return the state's name if city is inside bounding box
}
return null;
}
@@ -208,22 +204,16 @@ function validPsCompare(rdsPs, stationPs) {
for (let i = 0; i < standardizedRdsPs.length; i++) {
// Skip this position if the character in standardizedRdsPs is an underscore.
if (standardizedRdsPs[i] === '_') continue;
if (token[i] === standardizedRdsPs[i]) {
matchCount++;
}
}
if (matchCount >= minMatchLen) {
return true;
if (token[i] === standardizedRdsPs[i]) matchCount++;
}
if (matchCount >= minMatchLen) return true;
}
return false;
}
function evaluateStation(station, esMode) {
let weightDistance = station.distanceKm;
if (esMode && station.distanceKm > 700) {
weightDistance = Math.abs(station.distanceKm - 1500) + 200;
}
if (esMode && station.distanceKm > 700) weightDistance = Math.abs(station.distanceKm - 1500) + 200;
let erp = station.erp && station.erp > 0 ? station.erp : 1;
let extraWeight = erp > weightedErp && station.distanceKm <= weightDistance ? 0.3 : 0;
let score = 0;
@@ -394,6 +384,4 @@ function deg2rad(deg) {
return deg * (Math.PI / 180);
}
module.exports = {
fetchTx
};
module.exports = fetchTx;
+9 -2
View File
@@ -3,8 +3,11 @@
<head>
<title>Unauthorized - FM-DX Webserver</title>
<link href="css/entry.css" type="text/css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css" type="text/css" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js" integrity="sha512-v2CJ7UaYy4JwqLDIrZUI/4hqeoQieOmAZNXBeQyjo21dadnwR+8ZaIJVT8EE2iyI61OV8e6M8PP2/4hpQINQ/g==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css" type="text/css"
rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"
integrity="sha512-v2CJ7UaYy4JwqLDIrZUI/4hqeoQieOmAZNXBeQyjo21dadnwR+8ZaIJVT8EE2iyI61OV8e6M8PP2/4hpQINQ/g=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<link rel="icon" type="image/png" href="favicon2.png" />
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
@@ -20,6 +23,10 @@
<p>
There's a possibility you were kicked by the system.<br>
Please try again later.</p>
<% if (reason) { %>
<p><strong>Reason:</strong> too dig of a bick</p>
<% } %>
</div>
</div>
</div>
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

+24
View File
@@ -0,0 +1,24 @@
<svg width="128" height="128" viewBox="0 0 128 128"
xmlns="http://www.w3.org/2000/svg">
<!-- Outer hollow circle -->
<circle
cx="64"
cy="64"
r="54"
fill="none"
stroke="#A7A88B"
stroke-width="20"
/>
<!-- Inner hollow circle -->
<circle
cx="64"
cy="64"
r="22"
fill="none"
stroke="#FFFFFF"
stroke-width="18"
/>
</svg>

After

Width:  |  Height:  |  Size: 382 B

+1 -1
View File
@@ -15,7 +15,7 @@
<script src="js/libs/chartjs-adapter-luxon.umd.min.js"></script>
<script src="js/libs/chartjs-plugin-streaming.min.js"></script>
<link rel="icon" type="image/png" href="favicon.png" />
<link rel="icon" type="image/svg+xml" href="favicon.svg" id="favicon" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta property="og:title" content="FM-DX WebServer [<%= tunerName %>]">
+5 -14
View File
@@ -27,11 +27,8 @@ function destroyStream() {
function OnConnectivityCallback(isConnected) {
console.log("Connectivity changed:", isConnected);
if (Stream) {
Stream.Volume = $('#volumeSlider').val();
} else {
console.warn("Stream is not initialized.");
}
if (Stream) Stream.Volume = $('#volumeSlider').val();
else console.warn("Stream is not initialized.");
}
@@ -44,18 +41,14 @@ function OnPlayButtonClick(_ev) {
shouldReconnect = false;
destroyStream();
$playbutton.find('.fa-solid').toggleClass('fa-stop fa-play');
if (isAppleiOS && 'audioSession' in navigator) {
navigator.audioSession.type = "none";
}
if (isAppleiOS && 'audioSession' in navigator) navigator.audioSession.type = "none";
} else {
console.log("Starting stream...");
shouldReconnect = true;
createStream();
Stream.Start();
$playbutton.find('.fa-solid').toggleClass('fa-play fa-stop');
if (isAppleiOS && 'audioSession' in navigator) {
navigator.audioSession.type = "playback";
}
if (isAppleiOS && 'audioSession' in navigator) navigator.audioSession.type = "playback";
}
$playbutton.addClass('bg-gray').prop('disabled', true);
@@ -70,9 +63,7 @@ function updateVolume() {
newVolumeGlobal = newVolume;
console.log("Volume updated to:", newVolume);
Stream.Volume = newVolume;
} else {
console.warn("Stream is not initialized.");
}
} else console.warn("Stream is not initialized.");
}
$(document).ready(Init);
+11 -21
View File
@@ -3,18 +3,13 @@ function tuneUp() {
if (socket.readyState === WebSocket.OPEN) {
getCurrentFreq();
let addVal = 0;
if (currentFreq < 0.52) {
addVal = 9 - (Math.round(currentFreq*1000) % 9);
} else if (currentFreq < 1.71) {
if (currentFreq < 0.52) addVal = 9 - (Math.round(currentFreq*1000) % 9);
else if (currentFreq < 1.71) {
// TODO: Rework to replace 9 with 9 or 10 based on regionalisation setting
addVal = 9 - (Math.round(currentFreq*1000) % 9);
} else if (currentFreq < 29.6) {
addVal = 5 - (Math.round(currentFreq*1000) % 5);
} else if (currentFreq >= 65.9 && currentFreq < 74) {
addVal = 30 - ((Math.round(currentFreq*1000) - 65900) % 30);
} else {
addVal = 100 - (Math.round(currentFreq*1000) % 100);
}
} else if (currentFreq < 29.6) addVal = 5 - (Math.round(currentFreq*1000) % 5);
else if (currentFreq >= 65.9 && currentFreq < 74) addVal = 30 - ((Math.round(currentFreq*1000) - 65900) % 30);
else addVal = 100 - (Math.round(currentFreq*1000) % 100);
socket.send("T" + (Math.round(currentFreq*1000) + addVal));
}
}
@@ -23,18 +18,13 @@ function tuneDown() {
if (socket.readyState === WebSocket.OPEN) {
getCurrentFreq();
let subVal = 0;
if (currentFreq < 0.52) {
if (currentFreq < 0.52) subVal = (Math.round(currentFreq*1000) % 9 == 0) ? 9 : (Math.round(currentFreq*1000) % 9);
else if (currentFreq < 1.71) {
// TODO: Rework to replace 9 with 9 or 10 based on regionalisation setting (Americans use 10, because of dumbfuckinstan)
subVal = (Math.round(currentFreq*1000) % 9 == 0) ? 9 : (Math.round(currentFreq*1000) % 9);
} else if (currentFreq < 1.71) {
// TODO: Rework to replace 9 with 9 or 10 based on regionalisation setting
subVal = (Math.round(currentFreq*1000) % 9 == 0) ? 9 : (Math.round(currentFreq*1000) % 9);
} else if (currentFreq < 29.6) {
subVal = (Math.round(currentFreq*1000) % 5 == 0) ? 5 : (Math.round(currentFreq*1000) % 5);
} else if (currentFreq > 65.9 && currentFreq <= 74) {
subVal = ((Math.round(currentFreq*1000) - 65900) % 30 == 0) ? 30 : ((Math.round(currentFreq*1000) - 65900) % 30);
} else {
subVal = (Math.round(currentFreq*1000) % 100 == 0) ? 100 : (Math.round(currentFreq*1000) % 100);
}
} else if (currentFreq < 29.6) subVal = (Math.round(currentFreq*1000) % 5 == 0) ? 5 : (Math.round(currentFreq*1000) % 5);
else if (currentFreq > 65.9 && currentFreq <= 74) subVal = ((Math.round(currentFreq*1000) - 65900) % 30 == 0) ? 30 : ((Math.round(currentFreq*1000) - 65900) % 30);
else subVal = (Math.round(currentFreq*1000) % 100 == 0) ? 100 : (Math.round(currentFreq*1000) % 100);
socket.send("T" + (Math.round(currentFreq*1000) - subVal));
}
}
+2 -6
View File
@@ -19,9 +19,7 @@ $(document).ready(function() {
function generateRandomString(length) {
const characters = 'ABCDEFGHJKMNOPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * characters.length));
}
for (let i = 0; i < length; i++) result += characters.charAt(Math.floor(Math.random() * characters.length));
return result;
}
@@ -87,9 +85,7 @@ $(document).ready(function() {
});
chatSendInput.keypress(function(event) {
if (event.which === 13) {
sendMessage();
}
if (event.which === 13) sendMessage();
});
function sendMessage() {
+9 -25
View File
@@ -38,9 +38,7 @@ function fetchConfig() {
function populateFields(data, prefix = "") {
$.each(data, (key, value) => {
if (value === null) {
value = ""; // Convert null to an empty string
}
if (value === null) value = ""; // Convert null to an empty string
let id = `${prefix}${prefix ? "-" : ""}${key}`;
const $element = $(`#${id}`);
@@ -50,11 +48,8 @@ function populateFields(data, prefix = "") {
$element.find('option').each(function() {
const $option = $(this);
const dataName = $option.data('name');
if (value.includes(dataName)) {
$option.prop('selected', true);
} else {
$option.prop('selected', false);
}
if (value.includes(dataName)) $option.prop('selected', true);
else $option.prop('selected', false);
});
$element.trigger('change');
@@ -68,11 +63,8 @@ function populateFields(data, prefix = "") {
const arrayId = `${id}-${index + 1}`;
const $arrayElement = $(`#${arrayId}`);
if ($arrayElement.length) {
$arrayElement.val(item);
} else {
console.log(`Element with id ${arrayId} not found`);
}
if ($arrayElement.length) $arrayElement.val(item);
else console.log(`Element with id ${arrayId} not found`);
});
return;
} else {
@@ -92,9 +84,7 @@ function populateFields(data, prefix = "") {
const $dropdownOption = $element.siblings('ul.options').find(`li[data-value="${value}"]`);
$element.val($dropdownOption.length ? $dropdownOption.text() : value);
$element.attr('data-value', value);
} else {
$element.val(value);
}
} else $element.val(value);
});
}
@@ -111,9 +101,7 @@ function updateConfigData(data, prefix = "") {
if ($presetElement.length) {
data[key].push($presetElement.val() || null); // Allow null if necessary
index++;
} else {
break;
}
} else break;
}
return;
}
@@ -123,16 +111,12 @@ function updateConfigData(data, prefix = "") {
const $selectedOptions = $element.find('option:selected');
$selectedOptions.each(function() {
const dataName = $(this).attr('data-name');
if (dataName) {
data[key].push(dataName);
}
if (dataName) data[key].push(dataName);
});
return;
}
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
return updateConfigData(value, id);
}
if (typeof value === "object" && value !== null && !Array.isArray(value)) return updateConfigData(value, id);
if ($element.length) {
const newValue = $element.attr("data-value") ?? $element.val() ?? null;
+1 -3
View File
@@ -106,9 +106,7 @@ $(document).ready(function() {
$listOfOptions.on('click', selectOption);
$dropdowns.on('click', 'input', toggleDropdown);
$dropdowns.on('keydown', 'input', function(event) {
if (event.key === 'Enter') {
toggleDropdown(event);
}
if (event.key === 'Enter') toggleDropdown(event);
});
$dropdowns.on('keydown', '.option', navigateOptions);
+3 -10
View File
@@ -1028,15 +1028,10 @@ const updateDataElements = throttle(function(parsedData) {
updateHtmlIfChanged($('#alternative-txes'), altTxInfo);
updateTextIfChanged($('#data-station-distance'), txDistance);
$dataStationContainer.css('display', 'block');
} else {
$dataStationContainer.removeAttr('style');
}
} else $dataStationContainer.removeAttr('style');
if(parsedData.txInfo.tx.length > 1 && parsedData.txInfo.dist > 150 && parsedData.txInfo.dist < 4000) {
$('.log-fmlist').removeAttr('disabled').removeClass('btn-disabled cursor-disabled');
} else {
$('.log-fmlist').attr('disabled', 'true').addClass('btn-disabled cursor-disabled');
}
if(parsedData.txInfo.tx.length > 1 && parsedData.txInfo.dist > 150 && parsedData.txInfo.dist < 4000) $('.log-fmlist').removeAttr('disabled').removeClass('btn-disabled cursor-disabled');
else $('.log-fmlist').attr('disabled', 'true').addClass('btn-disabled cursor-disabled');
updateHtmlIfChanged($('#data-regular-pi'), parsedData.txInfo.reg === true ? parsedData.txInfo.pi : '&nbsp;');
if (updateCounter % 8 === 0) {
@@ -1084,7 +1079,6 @@ function updatePanels(parsedData) {
}
if (updateCounter % 3 === 0) {
updateButtonState("data-eq", parsedData.eq);
updateButtonState("data-ims", parsedData.ims);
@@ -1288,4 +1282,3 @@ function initTooltips(target = null) {
$('#preset1').parent().hide();
}
}
+4 -9
View File
@@ -31,15 +31,11 @@ $(document).ready(function() {
$(document).on("click", function(event) { // Close the modal when clicking outside of it
if ($(event.target).is(modal)) {
closeModal();
}
if ($(event.target).is(modal)) closeModal();
});
$(document).on("keydown", function(event) { // Close the modal when pressing ESC key
if (event.key === "Escape") {
closeModal();
}
if (event.key === "Escape") closeModal();
});
$(".tuner-mobile-settings").on("click", function () {
@@ -69,9 +65,8 @@ function initPopups() {
function togglePopup(targetSelector) {
const $target = $(targetSelector);
if ($target.is(":visible")) {
$target.fadeOut(200);
} else {
if ($target.is(":visible")) $target.fadeOut(200);
else {
$(".popup-window").fadeOut(200);
$target.fadeIn(200);
}
+23 -3
View File
@@ -75,6 +75,27 @@ function getQueryParameter(name) {
return urlParams.get(name);
}
function updateFavicon(color) {
function rgbToHex(rgb) {
const result = rgb.match(/\d+/g);
return "#" + result.slice(0, 3).map(x =>(+x).toString(16).padStart(2, "0")).join("");
}
const hex = rgbToHex(color);
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
<circle cx="64" cy="64" r="54" fill="none" stroke="${hex}" stroke-width="20"/>
<circle cx="64" cy="64" r="22" fill="none" stroke="white" stroke-width="18"/>
</svg>`;
const base64 = btoa(svg);
$('#favicon').attr(
'href',
`data:image/svg+xml;base64,${base64}`
);
}
function setTheme(themeName) {
const themeColors = themes[themeName];
if (themeColors) {
@@ -94,6 +115,7 @@ function setTheme(themeName) {
$(':root').css('--color-text', themeColors[2]);
$(':root').css('--color-text-2', textColor2);
$('.wrapper-outer').css('background-color', backgroundColorWithOpacity);
updateFavicon(themeColors[1]);
}
}
@@ -167,9 +189,7 @@ function loadInitialSettings() {
if(signalParameter && !localStorage.getItem('signalUnit')) {
signalSelector.find('input').val(signalSelector.find('.option[data-value="' + signalParameter + '"]').text());
localStorage.setItem('signalUnit', signalParameter);
} else {
signalSelector.find('input').val(signalSelector.find('.option[data-value="' + savedUnit + '"]').text());
}
} else signalSelector.find('input').val(signalSelector.find('.option[data-value="' + savedUnit + '"]').text());
signalSelector.on('click', '.option', (event) => {
const selectedSignalUnit = $(event.target).data('value');
+2 -6
View File
@@ -27,9 +27,7 @@ function mapCreate() {
zoom: 3,
worldCopyJump: true
});
} else {
map.setZoom(3).panTo([40, 0]);
}
} else map.setZoom(3).panTo([40, 0]);
L.tileLayer(tilesURL, {
attribution: mapAttrib,
@@ -272,9 +270,7 @@ function checkTunnelServers() {
// If this li is the currently selected one, update input text too
// Note: input.val() holds the label, so match by label is safer
if ($li.text() === selectedValue || server.value === selectedValue) {
$input.val(server.label);
}
if ($li.text() === selectedValue || server.value === selectedValue) $input.val(server.label);
}
});
},
+2 -2
View File
@@ -1,2 +1,2 @@
const versionDate = new Date('Feb 24, 2026 01:00:00');
const currentVersion = `v1.4.0 [${versionDate.getDate()}/${versionDate.getMonth() + 1}/${versionDate.getFullYear()}]`;
const versionDate = new Date('Feb 24, 2026 15:00:00');
const currentVersion = `v1.4.0a [${versionDate.getDate()}/${versionDate.getMonth() + 1}/${versionDate.getFullYear()}]`;
+1 -3
View File
@@ -27,8 +27,6 @@ function navigateStep(isNext) {
currentStep.hide();
targetStep.show();
updateProgressBar(targetStep);
} else if (isNext) {
submitConfig();
}
} else if (isNext) submitConfig();
updateWizardContent();
}
+2 -2
View File
@@ -8,7 +8,7 @@
<script src="js/libs/jquery.min.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.css" type="text/css" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js"></script>
<link rel="icon" type="image/png" href="favicon.png" />
<link rel="icon" type="image/svg+xml" href="favicon.svg" id="favicon" />
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
@@ -16,7 +16,7 @@
<div class="wrapper-outer wrapper-full">
<div id="wrapper">
<div class="panel-100 no-bg">
<img class="top-25" src="favicon.png" height="64px">
<img class="top-25" src="favicon.svg" height="64px">
<p>You are currently not logged in as an administrator and therefore can't change the settings.</p>
<p>Please login below.</p>
</div>
+1 -1
View File
@@ -8,7 +8,7 @@
<script src="js/libs/jquery.min.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.css" type="text/css" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js"></script>
<link rel="icon" type="image/png" href="favicon.png" />
<link rel="icon" type="image/svg+xml" href="favicon.svg" id="favicon" />
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
+1 -1
View File
@@ -8,7 +8,7 @@
<script src="js/libs/jquery.min.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.css" type="text/css" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js"></script>
<link rel="icon" type="image/png" href="favicon.png" />
<link rel="icon" type="image/svg+xml" href="favicon.svg" id="favicon" />
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>