1
0
mirror of https://github.com/KubaPro010/fm-dx-webserver.git synced 2026-02-26 14:11:59 +01:00

Compare commits

...

4 Commits

25 changed files with 978 additions and 512 deletions

View File

@@ -31,9 +31,7 @@ FM-DX Webserver is a cross-platform web server designed for FM DXers who want to
This project utilizes these libraries:
- [3LAS](https://github.com/jojobond/3LAS) library by JoJoBond for Low Latency Audio Streaming.
- [flat-flags](https://github.com/luishdez/flat-flags) library by luishdez for RDS country flags.
- [librdsparser](https://github.com/kkonradpl/librdsparser) library by Konrad Kosmatka for RDS parsing.
All of these libraries are already bundled with the webserver.

7
package-lock.json generated
View File

@@ -16,7 +16,6 @@
"express-session": "1.18.2",
"ffmpeg-static": "5.2.0",
"http": "0.0.1-security",
"koffi": "2.7.2",
"net": "1.0.2",
"serialport": "12.0.0",
"ws": "8.18.1"
@@ -1181,12 +1180,6 @@
"node": ">=10"
}
},
"node_modules/koffi": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/koffi/-/koffi-2.7.2.tgz",
"integrity": "sha512-AWcsEKETQuELxK0Wq/aXDkDiNFFY41TxZQSrKm2Nd6HO/KTHeohPOOIlh2OfQnBXJbRjx5etpWt8cbqMUZo2sg==",
"hasInstallScript": true
},
"node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",

View File

@@ -19,7 +19,6 @@
"express-session": "1.18.2",
"ffmpeg-static": "5.2.0",
"http": "0.0.1-security",
"koffi": "2.7.2",
"net": "1.0.2",
"serialport": "12.0.0",
"ws": "8.18.1"

View File

@@ -23,10 +23,7 @@ function createChatServer(storage) {
storage.chatHistory.forEach((message) => {
const historyMessage = { ...message, history: true };
if (!request.session?.isAdminAuthenticated) {
delete historyMessage.ip;
}
if (!request.session?.isAdminAuthenticated) delete historyMessage.ip;
ws.send(JSON.stringify(historyMessage));
});

View File

@@ -39,9 +39,7 @@ const logMessage = (type, messages, verbose = false) => {
console.log(logMessage);
}
if(type !== 'FFMPEG') {
appendLogToBuffer(logMessage);
}
if(type !== 'FFMPEG') appendLogToBuffer(logMessage);
};
const logDebug = (...messages) => logMessage('DEBUG', messages, verboseMode);

View File

@@ -1,213 +1,8 @@
/* Libraries / Imports */
const fs = require('fs');
const https = require('https');
const koffi = require('koffi');
const path = require('path');
const os = require('os');
const platform = os.platform();
const cpuArchitecture = os.arch();
const { configName, serverConfig, configUpdate, configSave } = require('./server_config');
let unicode_type;
let shared_Library;
const RDSDecoder = require("./rds.js");
const { serverConfig } = require('./server_config');
if (platform === 'win32') {
unicode_type = 'int16_t';
arch_type = (cpuArchitecture === 'x64' ? 'mingw64' : 'mingw32');
shared_Library=path.join(__dirname, "libraries", arch_type, "librdsparser.dll");
} else if (platform === 'linux') {
unicode_type = 'int32_t';
arch_type = (cpuArchitecture === 'x64' ? 'x86_64' :
(cpuArchitecture === 'ia32' ? 'x86' :
(cpuArchitecture === 'arm64' ? 'aarch64' : cpuArchitecture)));
shared_Library=path.join(__dirname, "libraries", arch_type, "librdsparser.so");
} else if (platform === 'darwin') {
unicode_type = 'int32_t';
shared_Library=path.join(__dirname, "libraries", "macos", "librdsparser.dylib");
}
const lib = koffi.load(shared_Library);
const { fetchTx } = require('./tx_search.js');
koffi.proto('void callback_pi(void *rds, void *user_data)');
koffi.proto('void callback_pty(void *rds, void *user_data)');
koffi.proto('void callback_tp(void *rds, void *user_data)');
koffi.proto('void callback_ta(void *rds, void *user_data)');
koffi.proto('void callback_ms(void *rds, void *user_data)');
koffi.proto('void callback_ecc(void *rds, void *user_data)');
koffi.proto('void callback_country(void *rds, void *user_data)');
koffi.proto('void callback_af(void *rds, uint32_t af, void *user_data)');
koffi.proto('void callback_ps(void *rds, void *user_data)');
koffi.proto('void callback_rt(void *rds, int flag, void *user_data)');
koffi.proto('void callback_ptyn(void *rds, void *user_data)');
koffi.proto('void callback_ct(void *rds, void *ct, void *user_data)');
const rdsparser = {
new: lib.func('void* rdsparser_new()'),
free: lib.func('void rdsparser_free(void *rds)'),
clear: lib.func('void rdsparser_clear(void *rds)'),
parse_string: lib.func('bool rdsparser_parse_string(void *rds, const char *input)'),
set_text_correction: lib.func('void rdsparser_set_text_correction(void *rds, uint8_t text, uint8_t type, uint8_t error)'),
set_text_progressive: lib.func('void rdsparser_set_text_progressive(void *rds, uint8_t string, uint8_t state)'),
get_pi: lib.func('int32_t rdsparser_get_pi(void *rds)'),
get_pty: lib.func('int8_t rdsparser_get_pty(void *rds)'),
get_tp: lib.func('int8_t rdsparser_get_tp(void *rds)'),
get_ta: lib.func('int8_t rdsparser_get_ta(void *rds)'),
get_ms: lib.func('int8_t rdsparser_get_ms(void *rds)'),
get_ecc: lib.func('int16_t rdsparser_get_ecc(void *rds)'),
get_country: lib.func('int rdsparser_get_country(void *rds)'),
get_ps: lib.func('void* rdsparser_get_ps(void *rds)'),
get_rt: lib.func('void* rdsparser_get_rt(void *rds, int flag)'),
get_ptyn: lib.func('void* rdsparser_get_ptyn(void *rds)'),
register_pi: lib.func('void rdsparser_register_pi(void *rds, void *cb)'),
register_pty: lib.func('void rdsparser_register_pty(void *rds, void *cb)'),
register_tp: lib.func('void rdsparser_register_tp(void *rds, void *cb)'),
register_ta: lib.func('void rdsparser_register_ta(void *rds, void *cb)'),
register_ms: lib.func('void rdsparser_register_ms(void *rds, void *cb)'),
register_ecc: lib.func('void rdsparser_register_ecc(void *rds, void *cb)'),
register_country: lib.func('void rdsparser_register_country(void *rds, void *cb)'),
register_af: lib.func('void rdsparser_register_af(void *rds, void *cb)'),
register_ps: lib.func('void rdsparser_register_ps(void *rds, void *cb)'),
register_rt: lib.func('void rdsparser_register_rt(void *rds, void *cb)'),
register_ptyn: lib.func('void rdsparser_register_ptyn(void *rds, void *cb)'),
register_ct: lib.func('void rdsparser_register_ct(void *rds, void *cb)'),
string_get_content: lib.func(unicode_type + '* rdsparser_string_get_content(void *string)'),
string_get_errors: lib.func('uint8_t* rdsparser_string_get_errors(void *string)'),
string_get_length: lib.func('uint8_t rdsparser_string_get_length(void *string)'),
ct_get_year: lib.func('uint16_t rdsparser_ct_get_year(void *ct)'),
ct_get_month: lib.func('uint8_t rdsparser_ct_get_month(void *ct)'),
ct_get_day: lib.func('uint8_t rdsparser_ct_get_day(void *ct)'),
ct_get_hour: lib.func('uint8_t rdsparser_ct_get_hour(void *ct)'),
ct_get_minute: lib.func('uint8_t rdsparser_ct_get_minute(void *ct)'),
ct_get_offset: lib.func('int8_t rdsparser_ct_get_offset(void *ct)'),
pty_lookup_short: lib.func('const char* rdsparser_pty_lookup_short(int8_t pty, bool rbds)'),
pty_lookup_long: lib.func('const char* rdsparser_pty_lookup_long(int8_t pty, bool rbds)'),
country_lookup_name: lib.func('const char* rdsparser_country_lookup_name(int country)'),
country_lookup_iso: lib.func('const char* rdsparser_country_lookup_iso(int country)')
}
const callbacks = {
pi: koffi.register(rds => (
value = rdsparser.get_pi(rds)
//console.log('PI: ' + value.toString(16).toUpperCase())
), 'callback_pi*'),
pty: koffi.register(rds => (
value = rdsparser.get_pty(rds),
dataToSend.pty = value
), 'callback_pty*'),
tp: koffi.register(rds => (
value = rdsparser.get_tp(rds),
dataToSend.tp = value
), 'callback_tp*'),
ta: koffi.register(rds => (
value = rdsparser.get_ta(rds),
dataToSend.ta = value
), 'callback_ta*'),
ms: koffi.register(rds => (
value = rdsparser.get_ms(rds),
dataToSend.ms = value
), 'callback_ms*'),
af: koffi.register((rds, value) => (
dataToSend.af.push(value)
), 'callback_af*'),
ecc: koffi.register(rds => (
value = rdsparser.get_ecc(rds),
dataToSend.ecc = value
), 'callback_ecc*'),
country: koffi.register(rds => (
value = rdsparser.get_country(rds),
display = rdsparser.country_lookup_name(value),
iso = rdsparser.country_lookup_iso(value),
dataToSend.country_name = display,
dataToSend.country_iso = iso
), 'callback_country*'),
ps: koffi.register(rds => (
ps = rdsparser.get_ps(rds),
dataToSend.ps = decode_unicode(ps),
dataToSend.ps_errors = decode_errors(ps)
), 'callback_ps*'),
rt: koffi.register((rds, flag) => {
const rt = rdsparser.get_rt(rds, flag);
if (flag === 0) {
dataToSend.rt0 = decode_unicode(rt);
dataToSend.rt0_errors = decode_errors(rt);
}
if (flag === 1) {
dataToSend.rt1 = decode_unicode(rt);
dataToSend.rt1_errors = decode_errors(rt);
}
dataToSend.rt_flag = flag;
}, 'callback_rt*'),
ptyn: koffi.register((rds, flag) => (
value = decode_unicode(rdsparser.get_ptyn(rds))
/*console.log('PTYN: ' + value)*/
), 'callback_ptyn*'),
ct: koffi.register((rds, ct) => (
year = rdsparser.ct_get_year(ct),
month = String(rdsparser.ct_get_month(ct)).padStart(2, '0'),
day = String(rdsparser.ct_get_day(ct)).padStart(2, '0'),
hour = String(rdsparser.ct_get_hour(ct)).padStart(2, '0'),
minute = String(rdsparser.ct_get_minute(ct)).padStart(2, '0'),
offset = rdsparser.ct_get_offset(ct),
tz_sign = (offset >= 0 ? '+' : '-'),
tz_hour = String(Math.abs(Math.floor(offset / 60))).padStart(2, '0'),
tz_minute = String(Math.abs(offset % 60)).padStart(2, '0')
//console.log('CT: ' + year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ' (' + tz_sign + tz_hour + ':' + tz_minute + ')')
), 'callback_ct*')
};
let rds = rdsparser.new()
rdsparser.set_text_correction(rds, 0, 0, 2);
rdsparser.set_text_correction(rds, 0, 1, 2);
rdsparser.set_text_correction(rds, 1, 0, 2);
rdsparser.set_text_correction(rds, 1, 1, 2);
rdsparser.set_text_progressive(rds, 0, 1)
rdsparser.set_text_progressive(rds, 1, 1)
rdsparser.register_pi(rds, callbacks.pi);
rdsparser.register_pty(rds, callbacks.pty);
rdsparser.register_tp(rds, callbacks.tp);
rdsparser.register_ta(rds, callbacks.ta);
rdsparser.register_ms(rds, callbacks.ms);
rdsparser.register_ecc(rds, callbacks.ecc);
rdsparser.register_country(rds, callbacks.country);
rdsparser.register_af(rds, callbacks.af);
rdsparser.register_ps(rds, callbacks.ps);
rdsparser.register_rt(rds, callbacks.rt);
rdsparser.register_ptyn(rds, callbacks.ptyn);
rdsparser.register_ct(rds, callbacks.ct);
const decode_unicode = function(string) {
let length = rdsparser.string_get_length(string);
if (length) {
let content = rdsparser.string_get_content(string);
let array = koffi.decode(content, unicode_type + ' [' + length + ']');
return String.fromCodePoint.apply(String, array);
}
return '';
};
const decode_errors = function(string) {
let length = rdsparser.string_get_length(string);
if (length) {
let errors = rdsparser.string_get_errors(string);
let array = koffi.decode(errors, 'uint8_t [' + length + ']');
return Uint8Array.from(array).toString();
}
return '';
};
const updateInterval = 75;
// Initialize the data object
@@ -251,6 +46,8 @@ var dataToSend = {
users: 0,
};
const rdsdec = new RDSDecoder(dataToSend);
const filterMappings = {
'G11': { eq: 1, ims: 1 },
'G01': { eq: 0, ims: 1 },
@@ -264,8 +61,6 @@ var lastUpdateTime = Date.now();
const initialData = { ...dataToSend };
const resetToDefault = dataToSend => Object.assign(dataToSend, initialData);
// Serialport reconnect variables
const ServerStartTime = process.hrtime();
var serialportUpdateTime = process.hrtime();
let checkSerialport = false;
let rdsTimeoutTimer = null;
@@ -283,7 +78,7 @@ function rdsReceived() {
function rdsReset() {
resetToDefault(dataToSend);
dataToSend.af.length = 0;
rdsparser.clear(rds);
rdsdec.clear();
if (rdsTimeoutTimer) {
clearTimeout(rdsTimeoutTimer);
rdsTimeoutTimer = null;
@@ -307,17 +102,13 @@ function handleData(wss, receivedData, rdsWss) {
rdsReceived();
modifiedData = receivedLine.slice(1);
legacyRdsPiBuffer = modifiedData;
if (dataToSend.pi.length >= modifiedData.length || dataToSend.pi == '?') {
dataToSend.pi = modifiedData;
}
if (dataToSend.pi.length >= modifiedData.length || dataToSend.pi == '?') dataToSend.pi = modifiedData;
break;
case receivedLine.startsWith('T'): // Frequency
modifiedData = receivedLine.substring(1).split(",")[0];
rdsReset();
if((modifiedData / 1000).toFixed(3) == dataToSend.freq) {
return; // Prevent tune spamming using scrollwheel
}
if((modifiedData / 1000).toFixed(3) == dataToSend.freq) return; // Prevent tune spamming using scrollwheel
parsedValue = parseFloat(modifiedData);
@@ -372,8 +163,7 @@ function handleData(wss, receivedData, rdsWss) {
var errorsNew = 0;
var pi;
if (legacyRdsPiBuffer !== null &&
legacyRdsPiBuffer.length >= 4) {
if(legacyRdsPiBuffer !== null && legacyRdsPiBuffer.length >= 4) {
pi = legacyRdsPiBuffer.slice(0, 4);
// PI message does not carry explicit information about
// error correction, but this is a good substitute.
@@ -404,7 +194,7 @@ function handleData(wss, receivedData, rdsWss) {
client.send(finalBuffer);
});
rdsparser.parse_string(rds, modifiedData);
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));
legacyRdsPiBuffer = null;
break;
}
@@ -469,10 +259,7 @@ async function checkSerialPortStatus() {
while (!checkSerialport) {
const ServerElapsedSeconds = process.hrtime(ServerStartTime)[0];
if (ServerElapsedSeconds > 10) {
checkSerialport = true;
}
if (ServerElapsedSeconds > 10) checkSerialport = true;
await new Promise(resolve => setTimeout(resolve, 100));
}
}
@@ -507,13 +294,10 @@ function processSignal(receivedData, st, stForced) {
// Convert highestSignal to a number for comparison
var highestSignal = parseFloat(dataToSend.sigTop);
if (signal > highestSignal) {
dataToSend.sigTop = signal.toString(); // Convert back to string for consistency
if (signal > highestSignal) dataToSend.sigTop = signal.toString(); // Convert back to string for consistency
}
}
}
module.exports = {
handleData, showOnlineUsers, dataToSend, initialData, resetToDefault, state
};

View File

@@ -463,6 +463,7 @@ router.get('/tunnelservers', async (req, res) => {
{ value: "eu", host: "eu.fmtuner.org", label: "Europe" },
{ value: "us", host: "us.fmtuner.org", label: "Americas" },
{ value: "sg", host: "sg.fmtuner.org", label: "Asia & Oceania" },
{ value: "pldx", host: "pldx.fmtuner.org", label: "Poland (k201)" },
];
const results = await Promise.all(

View File

@@ -1,8 +1,7 @@
/* Libraries / Imports */
const fs = require('fs');
const fetch = require('node-fetch');
const { logDebug, logError, logInfo, logWarn } = require('./console');
const { serverConfig, configUpdate, configSave } = require('./server_config');
const { logDebug, logInfo, logWarn } = require('./console');
const { serverConfig, configSave } = require('./server_config');
var pjson = require('../package.json');
var os = require('os');
@@ -23,23 +22,15 @@ function send(request) {
fetch(url, options)
.then(response => response.json())
.then(data => {
if (data.success && data.token)
{
if (data.success && data.token) {
if (!serverConfig.identification.token)
{
logInfo("Registered to FM-DX Server Map successfully.");
serverConfig.identification.token = data.token;
configSave();
}
else
{
logDebug("FM-DX Server Map update successful.");
}
}
else
{
logWarn("Failed to update FM-DX Server Map: " + (data.error ? data.error : 'unknown error'));
}
else logDebug("FM-DX Server Map update successful.");
} else logWarn("Failed to update FM-DX Server Map: " + (data.error ? data.error : 'unknown error'));
})
.catch(error => {
logWarn("Failed to update FM-DX Server Map: " + error);
@@ -47,10 +38,7 @@ function send(request) {
}
function sendKeepalive() {
if (!serverConfig.identification.token)
{
return;
}
if (!serverConfig.identification.token) return;
const request = {
token: serverConfig.identification.token,
@@ -64,9 +52,7 @@ function sendUpdate() {
let currentOs = os.type() + ' ' + os.release();
let bwLimit = '';
if (serverConfig.webserver.tuningLimit === true) {
bwLimit = serverConfig.webserver.tuningLowerLimit + ' - ' + serverConfig.webserver.tuningUpperLimit + ' MHz';
}
if (serverConfig.webserver.tuningLimit === true) bwLimit = serverConfig.webserver.tuningLowerLimit + ' - ' + serverConfig.webserver.tuningUpperLimit + ' MHz';
const request = {
status: ((serverConfig.lockToAdmin == 'true' || serverConfig.publicTuner == 'false') ? 2 : 1),
@@ -82,37 +68,20 @@ function sendUpdate() {
version: pjson.version
};
if (serverConfig.identification.token)
{
request.token = serverConfig.identification.token;
}
if (serverConfig.identification.token) request.token = serverConfig.identification.token;
if (serverConfig.identification.proxyIp.length)
{
request.url = serverConfig.identification.proxyIp;
}
else
{
request.port = serverConfig.webserver.webserverPort;
}
if (serverConfig.identification.proxyIp.length) request.url = serverConfig.identification.proxyIp;
else request.port = serverConfig.webserver.webserverPort;
send(request);
}
function update() {
if (timeoutID !== null) {
clearTimeout(timeoutID);
}
if (!serverConfig.identification.broadcastTuner)
{
return;
}
if (timeoutID !== null) clearTimeout(timeoutID);
if (!serverConfig.identification.broadcastTuner) return;
sendUpdate();
timeoutID = setInterval(sendKeepalive, 5 * 60 * 1000);
}
module.exports = {
update
};
module.exports.update = update;

View File

@@ -137,9 +137,7 @@ function processConnection(clientIp, locationInfo, currentUsers, ws, callback) {
const normalizedClientIp = clientIp?.replace(/^::ffff:/, '');
fetchBannedAS((error, bannedAS) => {
if (error) {
console.error("Error fetching banned AS list:", error);
}
if (error) console.error("Error fetching banned AS list:", error);
if (bannedAS.some((as) => locationInfo.as?.includes(as))) {
const now = Date.now();
@@ -165,9 +163,7 @@ function processConnection(clientIp, locationInfo, currentUsers, ws, callback) {
instance: ws,
});
consoleCmd.logInfo(
`Web client \x1b[32mconnected\x1b[0m (${normalizedClientIp}) \x1b[90m[${currentUsers}]\x1b[0m Location: ${userLocation}`
);
consoleCmd.logInfo(`Web client \x1b[32mconnected\x1b[0m (${normalizedClientIp}) \x1b[90m[${currentUsers}]\x1b[0m Location: ${userLocation}`);
callback("User allowed");
});
@@ -200,13 +196,9 @@ function resolveDataBuffer(data, wss, rdsWss) {
incompleteDataBuffer = receivedData.slice(position + 1);
receivedData = receivedData.slice(0, position + 1);
}
} else {
incompleteDataBuffer = '';
}
} else incompleteDataBuffer = '';
if (receivedData.length) {
dataHandler.handleData(wss, receivedData, rdsWss);
};
if (receivedData.length) dataHandler.handleData(wss, receivedData, rdsWss);
}
function kickClient(ipAddress) {
@@ -221,9 +213,7 @@ function kickClient(ipAddress) {
targetClient.instance.close();
consoleCmd.logInfo(`Web client kicked (${ipAddress})`);
}, 500);
} else {
consoleCmd.logInfo(`Kicking client ${ipAddress} failed. No suitable client found.`);
}
} else consoleCmd.logInfo(`Kicking client ${ipAddress} failed. No suitable client found.`);
}
function checkIPv6Support(callback) {
@@ -232,11 +222,8 @@ function checkIPv6Support(callback) {
server.listen(0, '::1', () => {
server.close(() => callback(true));
}).on('error', (error) => {
if (error.code === 'EADDRNOTAVAIL') {
callback(false);
} else {
callback(false);
}
if (error.code === 'EADDRNOTAVAIL') callback(false);
else callback(false);
});
}
@@ -272,9 +259,7 @@ function antispamProtection(message, clientIp, ws, userCommands, lastWarn, userC
if (endpointName === 'text') consoleCmd.logDebug(`Command received from \x1b[90m${clientIp}\x1b[0m: ${command}`);
// Initialize user command history if not present
if (!userCommandHistory[clientIp]) {
userCommandHistory[clientIp] = [];
}
if (!userCommandHistory[clientIp]) userCommandHistory[clientIp] = [];
// Record the current timestamp for the user
userCommandHistory[clientIp].push(now);

View File

@@ -79,6 +79,7 @@ const terminalWidth = readline.createInterface({
}).output.columns;
// Couldn't get figlet.js or something like that?
console.log(`\x1b[32m
_____ __ __ ______ __ __ __ _
| ___| \\/ | | _ \\ \\/ / \\ \\ / /__| |__ ___ ___ _ ____ _____ _ __
@@ -124,20 +125,16 @@ setInterval(() => {
logWarn('Communication lost from ' + serverConfig.xdrd.comPort + ', force closing serialport.');
setTimeout(() => {
serialport.close((err) => {
if (err) {
logError('Error closing serialport: ', err.message);
}
if (err) logError('Error closing serialport: ', err.message);
});
}, 1000);
} else {
logWarn('Communication lost from ' + serverConfig.xdrd.comPort + '.');
}
} else logWarn('Communication lost from ' + serverConfig.xdrd.comPort + '.');
}
}, 2000);
// Serial Connection
function connectToSerial() {
if (serverConfig.xdrd.wirelessConnection === false) {
if (serverConfig.xdrd.wirelessConnection === true) return;
// Configure the SerialPort with DTR and RTS options
serialport = new SerialPort({
@@ -175,9 +172,7 @@ if (serverConfig.xdrd.wirelessConnection === false) {
dataHandler.dataToSend.freq = Number(serverConfig.defaultFreq).toFixed(3);
} else if (dataHandler.state.lastFrequencyAlive && dataHandler.state.isSerialportRetrying) { // Serialport retry code when port is open but communication is lost
serialport.write('T' + (dataHandler.state.lastFrequencyAlive * 1000) + '\n');
} else {
serialport.write('T87500\n');
}
} else serialport.write('T87500\n');
dataHandler.state.isSerialportRetrying = false;
serialport.write('A0\n');
@@ -185,19 +180,12 @@ if (serverConfig.xdrd.wirelessConnection === false) {
serialport.write('W0\n');
serverConfig.webserver.rdsMode ? serialport.write('D1\n') : serialport.write('D0\n');
// cEQ and iMS combinations
if (serverConfig.ceqStartup === "0" && serverConfig.imsStartup === "0") {
serialport.write("G00\n"); // Both Disabled
} else if (serverConfig.ceqStartup === "1" && serverConfig.imsStartup === "0") {
serialport.write(`G10\n`);
} else if (serverConfig.ceqStartup === "0" && serverConfig.imsStartup === "1") {
serialport.write(`G01\n`);
} else if (serverConfig.ceqStartup === "1" && serverConfig.imsStartup === "1") {
serialport.write("G11\n"); // Both Enabled
}
if (serverConfig.ceqStartup === "0" && serverConfig.imsStartup === "0") serialport.write("G00\n"); // Both Disabled
else if (serverConfig.ceqStartup === "1" && serverConfig.imsStartup === "0") serialport.write(`G10\n`);
else if (serverConfig.ceqStartup === "0" && serverConfig.imsStartup === "1") serialport.write(`G01\n`);
else if (serverConfig.ceqStartup === "1" && serverConfig.imsStartup === "1") serialport.write("G11\n"); // Both Enabled
// Handle stereo mode
if (serverConfig.stereoStartup === "1") {
serialport.write("B1\n"); // Mono
}
if (serverConfig.stereoStartup === "1") serialport.write("B1\n"); // Mono
serverConfig.audio.startupVolume
? serialport.write('Y' + (serverConfig.audio.startupVolume * 100).toFixed(0) + '\n')
: serialport.write('Y100\n');
@@ -222,7 +210,6 @@ if (serverConfig.xdrd.wirelessConnection === false) {
});
return serialport;
}
}
// xdrd connection
let authFlags = {};
@@ -266,9 +253,8 @@ client.on('data', (data) => {
if (line.startsWith('a')) {
authFlags.authMsg = true;
logWarn('Authentication with xdrd failed. Is your password set correctly?');
} else if (line.startsWith('o1,')) {
authFlags.firstClient = true;
} else if (line.startsWith('T') && line.length <= 7) {
} else if (line.startsWith('o1,')) authFlags.firstClient = true;
else if (line.startsWith('T') && line.length <= 7) {
const freq = line.slice(1) / 1000;
dataHandler.dataToSend.freq = freq.toFixed(3);
} else if (line.startsWith('OK')) {
@@ -306,9 +292,7 @@ client.on('close', () => {
setTimeout(function () {
connectToXdrd();
}, 2000)
} else {
logWarn('Disconnected from xdrd.');
}
} else logWarn('Disconnected from xdrd.');
});
client.on('error', (err) => {
@@ -369,9 +353,7 @@ wss.on('connection', (ws, request) => {
return;
}
if (clientIp && clientIp.includes(',')) {
clientIp = clientIp.split(',')[0].trim();
}
if (clientIp && clientIp.includes(',')) clientIp = clientIp.split(',')[0].trim();
// Per-IP limit connection open
if (clientIp) {
@@ -435,9 +417,7 @@ wss.on('connection', (ws, request) => {
}
}
if (command.includes("\'")) {
return;
}
if (command.includes("\'")) return;
const { isAdminAuthenticated, isTuneAuthenticated } = request.session || {};
@@ -466,14 +446,10 @@ wss.on('connection', (ws, request) => {
const tuneFreq = Number(command.slice(1)) / 1000;
const { tuningLimit, tuningLowerLimit, tuningUpperLimit } = serverConfig.webserver;
if (tuningLimit && (tuneFreq < tuningLowerLimit || tuneFreq > tuningUpperLimit) || isNaN(tuneFreq)) {
return;
}
if (tuningLimit && (tuneFreq < tuningLowerLimit || tuneFreq > tuningUpperLimit) || isNaN(tuneFreq)) return;
}
if ((serverConfig.publicTuner && !serverConfig.lockToAdmin) || isAdminAuthenticated || (!serverConfig.publicTuner && !serverConfig.lockToAdmin && isTuneAuthenticated)) {
output.write(`${command}\n`);
}
if ((serverConfig.publicTuner && !serverConfig.lockToAdmin) || isAdminAuthenticated || (!serverConfig.publicTuner && !serverConfig.lockToAdmin && isTuneAuthenticated)) output.write(`${command}\n`);
});
ws.on('close', (code, reason) => {
@@ -499,55 +475,34 @@ wss.on('connection', (ws, request) => {
dataHandler.showOnlineUsers(currentUsers);
const index = storage.connectedUsers.findIndex(user => user.ip === clientIp);
if (index !== -1) {
storage.connectedUsers.splice(index, 1);
}
if (index !== -1) storage.connectedUsers.splice(index, 1);
if (currentUsers === 0) {
storage.connectedUsers = [];
if (serverConfig.bwAutoNoUsers === "1") {
output.write("W0\n"); // Auto BW 'Enabled'
}
if (serverConfig.bwAutoNoUsers === "1") output.write("W0\n"); // Auto BW 'Enabled'
// cEQ and iMS combinations
if (serverConfig.ceqNoUsers === "1" && serverConfig.imsNoUsers === "1") {
output.write("G00\n"); // Both Disabled
} else if (serverConfig.ceqNoUsers === "1" && serverConfig.imsNoUsers === "0") {
output.write(`G0${dataHandler.dataToSend.ims}\n`);
} else if (serverConfig.ceqNoUsers === "0" && serverConfig.imsNoUsers === "1") {
output.write(`G${dataHandler.dataToSend.eq}0\n`);
} else if (serverConfig.ceqNoUsers === "2" && serverConfig.imsNoUsers === "0") {
output.write(`G1${dataHandler.dataToSend.ims}\n`);
} else if (serverConfig.ceqNoUsers === "0" && serverConfig.imsNoUsers === "2") {
output.write(`G${dataHandler.dataToSend.eq}1\n`);
} else if (serverConfig.ceqNoUsers === "2" && serverConfig.imsNoUsers === "1") {
output.write("G10\n"); // Only cEQ enabled
} else if (serverConfig.ceqNoUsers === "1" && serverConfig.imsNoUsers === "2") {
output.write("G01\n"); // Only iMS enabled
} else if (serverConfig.ceqNoUsers === "2" && serverConfig.imsNoUsers === "2") {
output.write("G11\n"); // Both Enabled
}
if (serverConfig.ceqNoUsers === "1" && serverConfig.imsNoUsers === "1") output.write("G00\n"); // Both Disabled
else if (serverConfig.ceqNoUsers === "1" && serverConfig.imsNoUsers === "0") output.write(`G0${dataHandler.dataToSend.ims}\n`);
else if (serverConfig.ceqNoUsers === "0" && serverConfig.imsNoUsers === "1") output.write(`G${dataHandler.dataToSend.eq}0\n`);
else if (serverConfig.ceqNoUsers === "2" && serverConfig.imsNoUsers === "0") output.write(`G1${dataHandler.dataToSend.ims}\n`);
else if (serverConfig.ceqNoUsers === "0" && serverConfig.imsNoUsers === "2") output.write(`G${dataHandler.dataToSend.eq}1\n`);
else if (serverConfig.ceqNoUsers === "2" && serverConfig.imsNoUsers === "1") output.write("G10\n"); // Only cEQ enabled
else if (serverConfig.ceqNoUsers === "1" && serverConfig.imsNoUsers === "2") output.write("G01\n"); // Only iMS enabled
else if (serverConfig.ceqNoUsers === "2" && serverConfig.imsNoUsers === "2") output.write("G11\n"); // Both Enabled
// Handle stereo mode
if (serverConfig.stereoNoUsers === "1") {
output.write("B0\n");
} else if (serverConfig.stereoNoUsers === "2") {
output.write("B1\n");
}
if (serverConfig.stereoNoUsers === "1") output.write("B0\n");
else if (serverConfig.stereoNoUsers === "2") output.write("B1\n");
// Handle Antenna selection
if (timeoutAntenna) clearTimeout(timeoutAntenna);
timeoutAntenna = setTimeout(() => {
if (serverConfig.antennaNoUsers === "1") {
output.write("Z0\n");
} else if (serverConfig.antennaNoUsers === "2") {
output.write("Z1\n");
} else if (serverConfig.antennaNoUsers === "3") {
output.write("Z2\n");
} else if (serverConfig.antennaNoUsers === "4") {
output.write("Z3\n");
}
if (serverConfig.antennaNoUsers === "1") output.write("Z0\n");
else if (serverConfig.antennaNoUsers === "2") output.write("Z1\n");
else if (serverConfig.antennaNoUsers === "3") output.write("Z2\n");
else if (serverConfig.antennaNoUsers === "4") output.write("Z3\n");
}, serverConfig.antennaNoUsersDelay ? 15000 : 0);
}
@@ -570,13 +525,9 @@ wss.on('connection', (ws, request) => {
}, 10000);
}
if (currentUsers === 0 && serverConfig.autoShutdown === true && serverConfig.xdrd.wirelessConnection === true) {
client.write('X\n');
}
if (currentUsers === 0 && serverConfig.autoShutdown === true && serverConfig.xdrd.wirelessConnection === true) client.write('X\n');
if (code !== 1008) {
logInfo(`Web client \x1b[31mdisconnected\x1b[0m (${normalizedClientIp}) \x1b[90m[${currentUsers}]`);
}
if (code !== 1008) logInfo(`Web client \x1b[31mdisconnected\x1b[0m (${normalizedClientIp}) \x1b[90m[${currentUsers}]`);
});
ws.on('error', console.error);
@@ -600,7 +551,7 @@ pluginsWss.on('connection', (ws, request) => {
let messageData;
try {
try { // JS Requires the try statement to have braces, unlike the if statement. This extends the huge list of proofs that this is a fucking toy language
messageData = JSON.parse(message); // Attempt to parse the JSON
} catch (error) {
// console.error("Failed to parse message:", error); // Log the error
@@ -611,9 +562,7 @@ pluginsWss.on('connection', (ws, request) => {
// Broadcast the message to all other clients
pluginsWss.clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send(modifiedMessage); // Send the message to all clients
}
if (client.readyState === WebSocket.OPEN) client.send(modifiedMessage); // Send the message to all clients
});
});
@@ -671,9 +620,7 @@ httpServer.on('upgrade', (request, socket, head) => {
pluginsWss.emit('connection', ws, request);
});
});
} else {
socket.destroy();
}
} else socket.destroy();
});
app.use(express.static(path.join(__dirname, '../web'))); // Serve the entire web folder to the user
@@ -691,18 +638,13 @@ helpers.checkIPv6Support((isIPv6Supported) => {
const startServer = (address, isIPv6) => {
httpServer.listen(port, address, () => {
if (!isIPv6 && !configExists()) {
logInfo(`Open your browser and proceed to \x1b[34mhttp://${address}:${port}\x1b[0m to continue with setup.`);
} else {
logServerStart(address, isIPv6);
}
if (!isIPv6 && !configExists()) logInfo(`Open your browser and proceed to \x1b[34mhttp://${address}:${port}\x1b[0m to continue with setup.`);
else logServerStart(address, isIPv6);
});
};
if (isIPv6Supported) {
startServer(ipv4Address, false); // Start on IPv4
startServer(ipv6Address, true); // Start on IPv6
} else {
startServer(ipv4Address, false); // Start only on IPv4
}
} else startServer(ipv4Address, false); // Start only on IPv4
});

Binary file not shown.

Binary file not shown.

184
server/rds.js Normal file
View File

@@ -0,0 +1,184 @@
const { rdsEccLookup, iso, countries } = require("./rds_country.js")
class RDSDecoder {
constructor(data) {
this.data = data;
this.clear()
}
clear() {
this.data.pi = '?';
this.ps = Array(8).fill(' ');
this.ps_errors = Array(8).fill("0");
this.rt0 = Array(64).fill(' ');
this.rt0_errors = Array(64).fill("0");
this.rt1 = Array(64).fill(' ');
this.rt1_errors = Array(64).fill("0");
this.data.ps = '';
this.data.rt1 = '';
this.data.rt0 = '';
this.data.pty = 0;
this.data.tp = 0;
this.data.ta = 0;
this.data.ms = -1;
this.data.rt_flag = 0;
this.rt1_to_clear = false;
this.rt0_to_clear = false;
this.data.ecc = null;
this.data.country_name = ""
this.data.country_iso = "UN"
this.af_len = 0;
this.data.af = []
this.af_am_follows = false;
this.last_pi_error = 0;
}
decodeGroup(blockA, blockB, blockC, blockD, error) {
const a_error = (error & 0xC0) >> 6;
const b_error = (error & 0x30) >> 4;
const c_error = (error & 0xc) >> 2;
const d_error = error & 3;
if(this.last_pi_error > a_error) {
this.data.pi = blockA.toString(16).toUpperCase().padStart(4, '0');
this.last_pi_error = a_error;
}
if(b_error != 0) return; // B chooses what group this is, if this has errors, we are screwed
const group = (blockB >> 12) & 0xF;
const version = (blockB >> 11) & 0x1;
this.data.tp = Number((blockB >> 10) & 1);
this.data.pty = (blockB >> 5) & 0b11111;
if (group === 0) {
this.data.ta = (blockB >> 4) & 1;
this.data.ms = (blockB >> 3) & 1;
if(version === 0 && c_error !== 3) {
var af_high = blockC >> 8;
var af_low = blockC & 0xFF;
var BASE = 224;
var FILLER = 205;
var AM_FOLLOWS = 250;
if(af_high >= BASE && af_high <= (BASE+25)) {
this.af_len = af_high-BASE;
if(this.af_len !== this.data.af.length) {
this.data.af = [];
this.af_am_follows = false;
if(af_low != FILLER && af_low != AM_FOLLOWS) this.data.af.push((af_low+875)*100)
else if(af_low == AM_FOLLOWS) this.af_am_follows = true;
}
} else if(this.data.af.length != this.af_len) {
if(!(af_high == AM_FOLLOWS || this.af_am_follows)) {
var freq = (af_high+875)*100;
if(!this.data.af.includes(freq)) this.data.af.push(freq);
}
if(this.af_am_follows) this.af_am_follows = false;
if(!(af_high == AM_FOLLOWS || af_low == FILLER || af_low == AM_FOLLOWS)) {
var freq = (af_low+875)*100;
if(!this.data.af.includes(freq)) this.data.af.push(freq);
}
if(af_low == AM_FOLLOWS) this.af_am_follows = true;
}
}
if(d_error === 3) return; // Don't risk it
const idx = blockB & 0x3;
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.data.ps = this.ps.join('');
this.data.ps_errors = this.ps_errors.join(',');
} else if (group === 1 && version === 0) {
var variant_code = (blockC >> 12) & 0x7;
switch (variant_code) {
case 0:
this.data.ecc = blockC & 0xff;
this.data.country_name = rdsEccLookup(blockA, this.data.ecc);
if(this.data.country_name.length === 0) this.data.country_iso = "UN";
else this.data.country_iso = iso[countries.indexOf(this.data.country_name)]
break;
default: break;
}
} else if (group === 2) {
const idx = blockB & 0b1111;
this.rt_ab = Boolean((blockB >> 4) & 1);
var multiplier = (version == 0) ? 4 : 2;
if(this.rt_ab) {
if(this.rt1_to_clear) {
this.rt1 = Array(64).fill(' ');
this.rt1_errors = Array(64).fill("0");
this.rt1_to_clear = false;
}
if(c_error !== 3 && 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;
}
if(d_error !== 3) {
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;
}
var i = this.rt1.indexOf("\r")
while(i != -1) {
this.rt1[i] = " ";
i = this.rt1.indexOf("\r");
}
this.data.rt1 = this.rt1.join('');
this.data.rt1_errors = this.rt1_errors.join(',');
this.data.rt_flag = 1;
this.rt0_to_clear = true;
} else {
if(this.rt0_to_clear) {
this.rt0 = Array(64).fill(' ');
this.rt0_errors = Array(64).fill("0");
this.rt0_to_clear = false;
}
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;
}
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;
}
var i = this.rt0.indexOf("\r");
while(i != -1) {
this.rt0[i] = " ";
i = this.rt0.indexOf("\r");
}
this.data.rt0 = this.rt0.join('');
this.data.rt0_errors = this.rt0_errors.join(',');
this.data.rt_flag = 0;
this.rt1_to_clear = true;
}
} else {
// console.log(group, version)
}
}
}
module.exports = RDSDecoder;

632
server/rds_country.js Normal file
View File

@@ -0,0 +1,632 @@
var countries = [
"Albania",
"Estonia",
"Algeria",
"Ethiopia",
"Andorra",
"Angola",
"Finland",
"Armenia",
"France",
"Ascension Island",
"Gabon",
"Austria",
"Gambia",
"Azerbaijan",
"Georgia",
"Germany",
"Bahrein",
"Ghana",
"Belarus",
"Gibraltar",
"Belgium",
"Greece",
"Benin",
"Guinea",
"Bosnia Herzegovina",
"Guinea-Bissau",
"Botswana",
"Hungary",
"Bulgaria",
"Iceland",
"Burkina Faso",
"Iraq",
"Burundi",
"Ireland",
"Cabinda",
"Israel",
"Cameroon",
"Italy",
"Jordan",
"Cape Verde",
"Kazakhstan",
"Central African Republic",
"Kenya",
"Chad",
"Kosovo",
"Comoros",
"Kuwait",
"DR Congo",
"Kyrgyzstan",
"Republic of Congo",
"Latvia",
"Cote d'Ivoire",
"Lebanon",
"Croatia",
"Lesotho",
"Cyprus",
"Liberia",
"Czechia",
"Libya",
"Denmark",
"Liechtenstein",
"Djiboutia",
"Lithuania",
"Egypt",
"Luxembourg",
"Equatorial Guinea",
"Macedonia",
"Eritrea",
"Madagascar",
"Seychelles",
"Malawi",
"Sierra Leone",
"Mali",
"Slovakia",
"Malta",
"Slovenia",
"Mauritania",
"Somalia",
"Mauritius",
"South Africa",
"Moldova",
"South Sudan",
"Monaco",
"Spain",
"Mongolia",
"Sudan",
"Montenegro",
"Swaziland",
"Morocco",
"Sweden",
"Mozambique",
"Switzerland",
"Namibia",
"Syria",
"Netherlands",
"Tajikistan",
"Niger",
"Tanzania",
"Nigeria",
"Togo",
"Norway",
"Tunisia",
"Oman",
"Turkey",
"Palestine",
"Turkmenistan",
"Poland",
"Uganda",
"Portugal",
"Ukraine",
"Qatar",
"United Arab Emirates",
"Romania",
"United Kingdom",
"Russia",
"Uzbekistan",
"Rwanda",
"Vatican",
"San Marino",
"Western Sahara",
"Sao Tome and Principe",
"Yemen",
"Saudi Arabia",
"Zambia",
"Senegal",
"Zimbabwe",
"Serbia",
"Anguilla",
"Guyana",
"Antigua and Barbuda",
"Haiti",
"Argentina",
"Honduras",
"Aruba",
"Jamaica",
"Bahamas",
"Martinique",
"Barbados",
"Mexico",
"Belize",
"Montserrat",
"Brazil/Bermuda",
"Brazil/AN",
"Bolivia",
"Nicaragua",
"Brazil",
"Panama",
"Canada",
"Paraguay",
"Cayman Islands",
"Peru",
"Chile",
"USA/VI/PR",
"Colombia",
"St. Kitts",
"Costa Rica",
"St. Lucia",
"Cuba",
"St. Pierre and Miquelon",
"Dominica",
"St. Vincent",
"Dominican Republic",
"Suriname",
"El Salvador",
"Trinidad and Tobago",
"Turks and Caicos islands",
"Falkland Islands",
"Greenland",
"Uruguay",
"Grenada",
"Venezuela",
"Guadeloupe",
"Virgin Islands",
"Guatemala",
"Afghanistan",
"South Korea",
"Laos",
"Australia Capital Territory",
"Macao",
"Australia New South Wales",
"Malaysia",
"Australia Victoria",
"Maldives",
"Australia Queensland",
"Marshall Islands",
"Australia South Australia",
"Micronesia",
"Australia Western Australia",
"Myanmar",
"Australia Tasmania",
"Nauru",
"Australia Northern Territory",
"Nepal",
"Bangladesh",
"New Zealand",
"Bhutan",
"Pakistan",
"Brunei Darussalam",
"Papua New Guinea",
"Cambodia",
"Philippines",
"China",
"Samoa",
"Singapore",
"Solomon Islands",
"Fiji",
"Sri Lanka",
"Hong Kong",
"Taiwan",
"India",
"Thailand",
"Indonesia",
"Tonga",
"Iran",
"Vanuatu",
"Japan",
"Vietnam",
"Kiribati",
"North Korea",
"Brazil/Equator"
]
var iso = [
"AL",
"EE",
"DZ",
"ET",
"AD",
"AO",
"FI",
"AM",
"FR",
"SH",
"GA",
"AT",
"GM",
"AZ",
"GE",
"DE",
"BH",
"GH",
"BY",
"GI",
"BE",
"GR",
"BJ",
"GN",
"BA",
"GW",
"BW",
"HU",
"BG",
"IS",
"BF",
"IQ",
"BI",
"IE",
"--",
"IL",
"CM",
"IT",
"JO",
"CV",
"KZ",
"CF",
"KE",
"TD",
"XK",
"KM",
"KW",
"CD",
"KG",
"CG",
"LV",
"CI",
"LB",
"HR",
"LS",
"CY",
"LR",
"CZ",
"LY",
"DK",
"LI",
"DJ",
"LT",
"EG",
"LU",
"GQ",
"MK",
"ER",
"MG",
"SC",
"MW",
"SL",
"ML",
"SK",
"MT",
"SI",
"MR",
"SO",
"MU",
"ZA",
"MD",
"SS",
"MC",
"ES",
"MN",
"SD",
"ME",
"SZ",
"MA",
"SE",
"MZ",
"CH",
"NA",
"SY",
"NL",
"TJ",
"NE",
"TZ",
"NG",
"TG",
"NO",
"TN",
"OM",
"TR",
"PS",
"TM",
"PL",
"UG",
"PT",
"UA",
"QA",
"AE",
"RO",
"GB",
"RU",
"UZ",
"RW",
"VA",
"SM",
"EH",
"ST",
"YE",
"SA",
"ZM",
"SN",
"ZW",
"RS",
"AI",
"GY",
"AG",
"HT",
"AR",
"HN",
"AW",
"JM",
"BS",
"MQ",
"BB",
"MX",
"BZ",
"MS",
"--",
"--",
"BO",
"NI",
"BR",
"PA",
"CA",
"PY",
"KY",
"PE",
"CL",
"--",
"CO",
"KN",
"CR",
"LC",
"CU",
"PM",
"DM",
"VC",
"DO",
"SR",
"SN",
"TT",
"TB",
"FK",
"GL",
"UY",
"GD",
"VE",
"GP",
"VG",
"GT",
"AF",
"KR",
"LA",
"AU",
"MO",
"AU",
"MY",
"AU",
"MV",
"AU",
"MH",
"AU",
"FM",
"AU",
"MM",
"AU",
"NR",
"AU",
"NP",
"BD",
"NZ",
"BT",
"PK",
"BN",
"PG",
"KH",
"PH",
"CN",
"WS",
"SG",
"SB",
"FJ",
"LK",
"HK",
"TW",
"IN",
"TH",
"ID",
"TO",
"IR",
"VU",
"JP",
"VN",
"KI",
"KP",
"--"
]
// RDS ECC Lookup Tables - Converted from C to JavaScript
const rdsEccA0A6Lut = [
// A0
[
"USA/VI/PR", "USA/VI/PR", "USA/VI/PR", "USA/VI/PR", "USA/VI/PR",
"USA/VI/PR", "USA/VI/PR", "USA/VI/PR", "USA/VI/PR", "USA/VI/PR",
"USA/VI/PR", null, "USA/VI/PR", "USA/VI/PR", null
],
// A1
[
null, null, null, null, null,
null, null, null, null, null,
"Canada", "Canada", "Canada", "Canada", "Greenland"
],
// A2
[
"Anguilla", "Antigua and Barbuda", "Brazil/Equator", "Falkland Islands", "Barbados",
"Belize", "Cayman Islands", "Costa Rica", "Cuba", "Argentina",
"Brazil", "Brazil/Bermuda", "Brazil/AN", "Guadeloupe", "Bahamas"
],
// A3
[
"Bolivia", "Colombia", "Jamaica", "Martinique", null,
"Paraguay", "Nicaragua", null, "Panama", "Dominica",
"Dominican Republic", "Chile", "Grenada", "Turks and Caicos islands", "Guyana"
],
// A4
[
"Guatemala", "Honduras", "Aruba", null, "Montserrat",
"Trinidad and Tobago", "Peru", "Suriname", "Uruguay", "St. Kitts",
"St. Lucia", "El Salvador", "Haiti", "Venezuela", "Virgin Islands"
],
// A5
[
null, null, null, null, null,
null, null, null, null, null,
"Mexico", "St. Vincent", "Mexico", "Mexico", "Mexico"
],
// A6
[
null, null, null, null, null,
null, null, null, null, null,
null, null, null, null, "St. Pierre and Miquelon"
]
];
const rdsEccD0D4Lut = [
// D0
[
"Cameroon", "Central African Republic", "Djiboutia", "Madagascar", "Mali",
"Angola", "Equatorial Guinea", "Gabon", "Guinea", "South Africa",
"Burkina Faso", "Republic of Congo", "Togo", "Benin", "Malawi"
],
// D1
[
"Namibia", "Liberia", "Ghana", "Mauritania", "Sao Tome and Principe",
"Cape Verde", "Senegal", "Gambia", "Burundi", "Ascension Island",
"Botswana", "Comoros", "Tanzania", "Ethiopia", "Nigeria"
],
// D2
[
"Sierra Leone", "Zimbabwe", "Mozambique", "Uganda", "Swaziland",
"Kenya", "Somalia", "Niger", "Chad", "Guinea-Bissau",
"DR Congo", "Cote d'Ivoire", null, "Zambia", "Eritrea"
],
// D3
[
null, null, "Western Sahara", "Cabinda", "Rwanda",
"Lesotho", null, "Seychelles", null, "Mauritius",
null, "Sudan", null, null, null
],
// D4
[
null, null, null, null, null,
null, null, null, null, "South Sudan",
null, null, null, null, null
]
];
const rdsEccE0E5Lut = [
// E0
[
"Germany", "Algeria", "Andorra", "Israel", "Italy",
"Belgium", "Russia", "Palestine", "Albania", "Austria",
"Hungary", "Malta", "Germany", null, "Egypt"
],
// E1
[
"Greece", "Cyprus", "San Marino", "Switzerland", "Jordan",
"Finland", "Luxembourg", "Bulgaria", "Denmark", "Gibraltar",
"Iraq", "United Kingdom", "Libya", "Romania", "France"
],
// E2
[
"Morocco", "Czechia", "Poland", "Vatican", "Slovakia",
"Syria", "Tunisia", null, "Liechtenstein", "Iceland",
"Monaco", "Lithuania", "Serbia", "Spain", "Norway"
],
// E3
[
"Montenegro", "Ireland", "Turkey", null, "Tajikistan",
null, null, "Netherlands", "Latvia", "Lebanon",
"Azerbaijan", "Croatia", "Kazakhstan", "Sweden", "Belarus"
],
// E4
[
"Moldova", "Estonia", "Macedonia", null, null,
"Ukraine", "Kosovo", "Portugal", "Slovenia", "Armenia",
"Uzbekistan", "Georgia", null, "Turkmenistan", "Bosnia Herzegovina"
],
// E5
[
null, null, "Kyrgyzstan", null, null,
null, null, null, null, null,
null, null, null, null, null
]
];
const rdsEccF0F4Lut = [
// F0
[
"Australia Capital Territory", "Australia New South Wales", "Australia Victoria", "Australia Queensland", "Australia South Australia",
"Australia Western Australia", "Australia Tasmania", "Australia Northern Territory", "Saudi Arabia", "Afghanistan",
"Myanmar", "China", "North Korea", "Bahrein", "Malaysia"
],
// F1
[
"Kiribati", "Bhutan", "Bangladesh", "Pakistan", "Fiji",
"Oman", "Nauru", "Iran", "New Zealand", "Solomon Islands",
"Brunei Darussalam", "Sri Lanka", "Taiwan", "South Korea", "Hong Kong"
],
// F2
[
"Kuwait", "Qatar", "Cambodia", "Samoa", "India",
"Macao", "Vietnam", "Philippines", "Japan", "Singapore",
"Maldives", "Indonesia", "United Arab Emirates", "Nepal", "Vanuatu"
],
// F3
[
"Laos", "Thailand", "Tonga", null, null,
null, null, "China", "Papua New Guinea", null,
"Yemen", null, null, "Micronesia", "Mongolia"
],
// F4
[
null, null, null, null, null,
null, null, null, "China", null,
"Marshall Islands", null, null, null, null
]
];
function rdsEccLookup(pi, ecc) {
const PI_UNKNOWN = -1;
const piCountry = (pi >> 12) & 0xF;
if (pi === PI_UNKNOWN || piCountry === 0) {
return ""
}
const piId = piCountry - 1;
const eccRanges = [
{ min: 0xA0, max: 0xA6, lut: rdsEccA0A6Lut },
{ min: 0xD0, max: 0xD4, lut: rdsEccD0D4Lut },
{ min: 0xE0, max: 0xE5, lut: rdsEccE0E5Lut },
{ min: 0xF0, max: 0xF4, lut: rdsEccF0F4Lut }
];
// Check each range
for (const range of eccRanges) {
if (ecc >= range.min && ecc <= range.max) {
const eccId = ecc - range.min;
return range.lut[eccId][piId];
}
}
return ""
}
module.exports = {
rdsEccLookup,
iso,
countries
};

View File

@@ -94,7 +94,7 @@ let serverConfig = {
enabled: false,
username: "",
token: "",
region: "eu",
region: "pldx",
lowLatencyMode: false,
subdomain: "",
httpName: "",

View File

@@ -11,11 +11,8 @@ function checkFFmpeg() {
});
checkFFmpegProcess.on('exit', (code) => {
if (code === 0) {
resolve('ffmpeg');
} else {
resolve(require('ffmpeg-static'));
}
if (code === 0) resolve('ffmpeg');
else resolve(require('ffmpeg-static'));
});
});
}

View File

@@ -74,9 +74,7 @@ function parseAudioDevice(options, callback) {
if (platform === 'win32' && line.search(/Alternative\sname/) > -1) {
const lastDevice = deviceList[deviceList.length - 1];
const alt = line.match(alternativeName);
if (lastDevice && alt) {
lastDevice.alternativeName = alt[1];
}
if (lastDevice && alt) lastDevice.alternativeName = alt[1];
return;
}
@@ -107,11 +105,8 @@ function parseAudioDevice(options, callback) {
}
};
if (callbackExists) {
execute();
} else {
return new Promise(execute);
}
if (callbackExists) execute();
else return new Promise(execute);
}
module.exports = { parseAudioDevice };

View File

@@ -25,9 +25,7 @@ async function connect() {
try {
const res = await fetch('https://fmtuner.org/binaries/' + frpcFileName);
if (res.status === 404) {
throw new Error('404 error');
}
if (res.status === 404) throw new Error('404 error');
const stream = fs2.createWriteStream(frpcPath);
await finished(Readable.fromWeb(res.body).pipe(stream));
} catch (err) {
@@ -41,7 +39,7 @@ async function connect() {
}
const cfg = ejs.render(frpcConfigTemplate, {
cfg: serverConfig.tunnel,
host: serverConfig.tunnel.community.enabled ? serverConfig.tunnel.community.host : serverConfig.tunnel.region + ".fmtuner.org",
host: serverConfig.tunnel.community.enabled ? serverConfig.tunnel.community.host : ((serverConfig.tunnel.region == "pldx") ? "pldx.duckdns.org" : (serverConfig.tunnel.region + ".fmtuner.org")),
server: {
port: serverConfig.webserver.webserverPort
}
@@ -62,15 +60,10 @@ async function connect() {
if (line.includes('connect to server error')) {
const reason = line.substring(line.indexOf(': ')+2);
logError('Failed to connect to tunnel, reason: ' + reason);
} else if (line.includes('invalid user or token')) {
logError('Failed to connect to tunnel, reason: invalid user or token');
} else if (line.includes('start proxy success')) {
logInfo('Tunnel established successfully');
} else if (line.includes('login to server success')) {
logInfo('Connection to tunnel server was successful');
} else {
logDebug('Tunnel log:', line);
}
} else if (line.includes('invalid user or token')) logError('Failed to connect to tunnel, reason: invalid user or token');
else if (line.includes('start proxy success')) logInfo('Tunnel established successfully');
else if (line.includes('login to server success')) logInfo('Connection to tunnel server was successful');
else logDebug('Tunnel log:', line);
});
child.on('error', (err) => {

View File

@@ -116,11 +116,8 @@ function initBanlist() {
data: { ip: ipAddress, reason: reason },
success: function(response) {
// Refresh the page if the request was successful
if (response.success) {
location.reload();
} else {
console.error('Failed to add to banlist');
}
if (response.success) location.reload();
else console.error('Failed to add to banlist');
},
error: function() {
console.error('Error occurred during the request');

View File

@@ -699,7 +699,8 @@
<div class="panel-100 p-bottom-20">
<h3>Tunnel</h3>
<p>When you become an <a href="https://buymeacoffee.com/fmdx" target="_blank"><strong>FMDX.org supporter</strong></a>, you can host your webserver without the need of a public IP address & port forwarding.<br>
When you become a supporter, you can message the Founders on Discord for your login details.</p>
When you become a supporter, you can message the Founders on Discord for your login details.</p><br>
<p>You can also get an tunnel from kuba201 discord, one of the contributors of this version of the application.</p>
<h4>Main tunnel settings</h4>
<%- include('_components', {component: 'checkbox', cssClass: 'm-right-10', label: 'Enable tunnel', id: 'tunnel-enabled'}) %><br>
<%- include('_components', { component: 'dropdown', id: 'tunnel-server', inputId: 'tunnel-serverSelect', label: 'Official server region', cssClass: '', placeholder: 'Europe',
@@ -707,6 +708,7 @@
{ value: 'eu', label: 'Europe' },
{ value: 'us', label: 'Americas' },
{ value: 'sg', label: 'Asia & Oceania' },
{ value: 'pldx', label: 'Poland (k201)' },
]
}) %>
<%- include('_components', {component: 'text', cssClass: 'w-150 br-15', placeholder: '', label: 'Username', id: 'tunnel-username'}) %>