some changes

This commit is contained in:
2026-07-10 22:02:51 +02:00
parent 7bd28794d6
commit 6e6217e1e7
33 changed files with 757 additions and 2305 deletions
+15 -66
View File
@@ -6,8 +6,6 @@ const { SerialPort } = require('serialport')
const path = require('path');
const https = require('https');
// File Imports
const parseAudioDevice = require('./stream/parser');
const { configName, serverConfig, configUpdate, configSave, configExists, configPath } = require('./server_config');
const helpers = require('./helpers');
const storage = require('./storage');
@@ -70,7 +68,11 @@ router.get('/403', (req, res) => {
router.get('/audioonly', (req, res) => res.render('audioonly'))
router.get('/setup', (req, res) => {
let serialPorts;
if(!helpers.isAdmin(req)) {
res.render('login');
return;
}
function loadConfig() {
if (fs.existsSync(configPath)) {
const configFileContents = fs.readFileSync(configPath, 'utf8');
@@ -79,45 +81,17 @@ router.get('/setup', (req, res) => {
return serverConfig;
}
if(!helpers.isAdmin(req)) {
res.render('login');
return;
}
const processUptimeInSeconds = Math.floor(process.uptime());
const formattedProcessUptime = helpers.formatUptime(processUptimeInSeconds);
SerialPort.list().then((deviceList) => {
serialPorts = deviceList.map(port => ({
path: port.path,
friendlyName: port.friendlyName,
}));
parseAudioDevice((result) => {
const processUptimeInSeconds = Math.floor(process.uptime());
const formattedProcessUptime = helpers.formatUptime(processUptimeInSeconds);
const updatedConfig = loadConfig(); // Reload the config every time
res.render('setup', {
isAdminAuthenticated: helpers.isAdmin(req),
videoDevices: result.audioDevices,
audioDevices: result.videoDevices,
serialPorts: serialPorts,
memoryUsage: (process.memoryUsage.rss() / 1024 / 1024).toFixed(1) + ' MB',
memoryHeap: (process.memoryUsage().heapUsed / 1024 / 1024).toFixed(1) + ' MB',
processUptime: formattedProcessUptime,
consoleOutput: logs,
plugins: require('./plugins'),
enabledPlugins: updatedConfig.plugins,
onlineUsers: dataHandler.dataToSend.users,
connectedUsers: storage.connectedUsers,
device: serverConfig.device,
banlist: updatedConfig.webserver.banlist, // Updated banlist from the latest config
tunerProfiles: tunerProfiles.map((profile) => ({
id: profile.id,
label: profile.label,
detailsHtml: helpers.parseMarkdown(profile.details || '')
}))
});
});
})
const updatedConfig = loadConfig(); // Reload the config every time
res.render('setup', {
isAdminAuthenticated: true,
memoryUsage: (process.memoryUsage.rss() / 1024 / 1024).toFixed(1) + ' MB',
memoryHeap: (process.memoryUsage().heapUsed / 1024 / 1024).toFixed(1) + ' MB',
processUptime: formattedProcessUptime,
consoleOutput: logs,
});
});
router.get('/api', (req, res) => {
@@ -254,11 +228,6 @@ router.get('/getData', (req, res) => {
}
});
router.get('/getDevices', (req, res) => {
if (helpers.isAdmin(req) || !fs.existsSync(configName + '.json')) parseAudioDevice(res.json);
else res.status(403).json({ error: 'Unauthorized' });
});
/* Static data are being sent through here on connection - these don't change when the server is running */
router.get('/static_data', (req, res) => {
res.json({
@@ -382,24 +351,4 @@ router.get('/log_fmlist', (req, res) => {
request.end();
});
router.get('/tunnelservers', async (req, res) => {
const servers = [
{ 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" }
];
const results = await Promise.all(
servers.map(async s => {
const latency = await helpers.checkLatency(s.host);
return {
value: s.value,
label: `${s.label} (${latency ? latency + ' ms' : 'offline'})` // From my tests, the latency via HTTP ping is roughly 2x higher than regular ping
};
})
);
res.json(results);
});
module.exports = router;
+1 -5
View File
@@ -1,7 +1,3 @@
const tunnel = require('./tunnel');
tunnel.download();
require('./stream/index');
require("./device");
const { serverConfig } = require('./server_config');
@@ -12,7 +8,7 @@ const { serverConfig } = require('./server_config');
if (plugins.length > 0) setTimeout(helpers.startPluginsWithDelay, 3000, plugins, 3000);
}
require('./stream/index');
require("./web")(serverConfig.webserver.webserverIp);
tunnel.connect();
require('./server_list')();
+24 -47
View File
@@ -1,52 +1,29 @@
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",
"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", "-",
"Cameroon", "Italy",
"Jordan", "Cape Verde",
"Kazakhstan",
"Central African Republic",
"Kenya",
"Chad",
"Kosovo",
"Comoros",
"Kuwait",
"DR Congo",
"Kenya", "Chad",
"Kosovo", "Comoros",
"Kuwait", "DR Congo",
"Kyrgyzstan",
"Republic of Congo",
"Latvia",
@@ -402,7 +379,7 @@ const rdsEccD0D4Lut = [
const rdsEccE0E5Lut = [
// E0
[
"Germany", "Algeria", "Andorra", "Israel", "Italy",
"Germany", "Algeria", "Andorra", "-", "Italy",
"Belgium", "Russia", "Palestine", "Albania", "Austria",
"Hungary", "Malta", "Germany", "", "Egypt"
],
-14
View File
@@ -89,20 +89,6 @@ let serverConfig = {
si47xx: {
agcControl: false
},
tunnel: {
enabled: false,
username: "",
token: "",
region: "eu",
lowLatencyMode: false,
subdomain: "",
httpName: "",
httpPassword: "",
community: {
enabled: false,
host: ""
}
},
plugins: [],
device: 'tef',
defaultFreq: 87.5,
-18
View File
@@ -1,18 +0,0 @@
const { spawn } = require('child_process');
module.exports = function() {
return new Promise((resolve, reject) => {
const checkFFmpegProcess = spawn('ffmpeg', ['-version'], {
stdio: ['ignore', 'ignore', 'ignore'],
});
checkFFmpegProcess.on('error', () => {
resolve(require('ffmpeg-static'));
});
checkFFmpegProcess.on('exit', (code) => {
if (code === 0) resolve('ffmpeg');
else resolve(require('ffmpeg-static'));
});
});
}
+18 -6
View File
@@ -7,7 +7,23 @@ if (!configExists() || !serverConfig.audio.audioDevice) return;
const { spawn } = require('child_process');
const { logDebug, logError, logInfo, logWarn } = require('../console');
const checkFFmpeg = require('./checkFFmpeg');
const checkFFmpeg = function() {
return new Promise((resolve, reject) => {
const checkFFmpegProcess = spawn('ffmpeg', ['-version'], {
stdio: ['ignore', 'ignore', 'ignore'],
});
checkFFmpegProcess.on('error', () => {
resolve(require('ffmpeg-static'));
});
checkFFmpegProcess.on('exit', (code) => {
if (code === 0) resolve('ffmpeg');
else resolve(require('ffmpeg-static'));
});
});
}
const consoleLogTitle = '[Audio Stream]';
@@ -24,8 +40,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); // Maybe even do 32 khz, we do not need higher than 15 khz precision
const sampleRate = Number(serverConfig.audio.sampleRate || 32000);
const channels = Number(serverConfig.audio.audioChannels || 2);
let ffmpeg = null;
@@ -80,8 +95,6 @@ checkFFmpeg().then((ffmpegPath) => {
ffmpeg.stdout.pipe(audio_pipe, { end: false });
connectMessage(`${consoleLogTitle} Connected FFmpeg → MP3 → audioWss`);
ffmpeg.stderr.on('data', (data) => {
const msg = data.toString();
@@ -136,7 +149,6 @@ checkFFmpeg().then((ffmpegPath) => {
});
launchFFmpeg();
}).catch((err) => {
logError(`${consoleLogTitle} Error: ${err.message}`);
});
-110
View File
@@ -1,110 +0,0 @@
'use strict';
const exec = require('child_process').exec;
const fs = require('fs').promises; // Use the Promise-based fs API
const ffmpeg = require('ffmpeg-static');
const filePath = '/proc/asound/cards';
const platform = process.platform;
function parseAudioDevice(options, callback) {
let videoDevices = [];
let audioDevices = [];
let isVideo = true;
if (typeof options === 'function') {
callback = options;
options = null;
}
options = options || {};
const ffmpegPath = `"${ffmpeg.replace(/\\/g, '\\\\')}"`;
const callbackExists = typeof callback === 'function';
const execute = async (fulfill, reject) => {
try {
if (platform === 'linux') {
try {
const data = await fs.readFile(filePath, 'utf8');
const regex = /\[([^\]]+)\]/g;
const matches = (data.match(regex) || []).map(match => 'hw:' + match.replace(/\s+/g, '').slice(1, -1));
matches.forEach(match => {
if (typeof match === 'string') audioDevices.push({ name: match });
});
} catch (err) {
console.error(`Error reading file: ${err.message}`);
}
// Linux doesn't support the `-list_devices` ffmpeg command like macOS/Windows,
// so skip the ffmpeg exec for Linux
const result = { videoDevices: [], audioDevices };
if (callbackExists) return callback(result);
return fulfill(result);
}
let inputDevice, prefix, audioSeparator, alternativeName, deviceParams;
switch (platform) {
case 'win32':
inputDevice = 'dshow';
prefix = /\[dshow/;
audioSeparator = /DirectShow\saudio\sdevices/;
alternativeName = /Alternative\sname\s*?"(.*?)"/;
deviceParams = /"(.*?)"/;
break;
case 'darwin':
inputDevice = 'avfoundation';
prefix = /^\[AVFoundation/;
audioSeparator = /AVFoundation\saudio\sdevices/;
deviceParams = /^\[AVFoundation.*?]\s\[(\d+)]\s(.*)$/;
break;
}
exec(`${ffmpegPath} -f ${inputDevice} -list_devices true -i ""`, (err, stdout, stderr) => {
stderr.split("\n")
.filter(line => line.search(prefix) > -1)
.forEach(line => {
const deviceList = isVideo ? videoDevices : audioDevices;
if (line.search(audioSeparator) > -1) {
isVideo = false;
return;
}
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];
return;
}
const params = line.match(deviceParams);
if (params) {
let device;
switch (platform) {
case 'win32':
device = { name: params[1] };
break;
case 'darwin':
device = { id: parseInt(params[1]), name: params[2] };
break;
}
deviceList.push(device);
}
});
audioDevices = audioDevices.filter(device => device.name !== undefined);
const result = { videoDevices, audioDevices };
if (callbackExists) return callback(result);
return fulfill(result);
});
} catch (err) {
console.error('Unexpected error:', err);
if (callbackExists) callback({ videoDevices: [], audioDevices: [] });
else reject(err);
}
};
if (callbackExists) execute();
else return new Promise(execute);
}
module.exports = parseAudioDevice;
-101
View File
@@ -1,101 +0,0 @@
const { logDebug, logError, logInfo, logWarn } = require('./console');
const { serverConfig } = require('./server_config');
const { Readable } = require('stream');
const { finished } = require('stream/promises');
const fs = require('fs/promises');
const fs2 = require('fs');
const path = require('path');
const os = require('os');
const ejs = require('ejs');
const { spawn } = require('child_process');
const readline = require('readline');
const fileExists = path => new Promise(resolve => fs.access(path, fs.constants.F_OK).then(() => resolve(true)).catch(() => resolve(false)));
async function download() {
if (serverConfig.tunnel?.enabled === true) {
const librariesDir = path.resolve(__dirname, '../libraries');
if (!await fileExists(librariesDir)) await fs.mkdir(librariesDir);
const frpcPath = path.resolve(librariesDir, 'frpc' + (os.platform() === 'win32' ? '.exe' : ''));
if (!await fileExists(frpcPath)) {
logInfo('frpc binary, required for tunnel is not available. Downloading now...');
const frpcFileName = `frpc_${os.platform}_${os.arch}` + (os.platform() === 'win32' ? '.exe' : '');
try {
const res = await fetch('https://fmtuner.org/binaries/' + frpcFileName);
if (res.status === 404) throw new Error('404 error');
const stream = fs2.createWriteStream(frpcPath);
await finished(Readable.fromWeb(res.body).pipe(stream));
} catch (err) {
logError('Failed to download frpc, reason: ' + err);
return;
}
logInfo('Downloading of frpc is completed.')
if (os.platform() === 'linux' || os.platform() === 'darwin') await fs.chmod(frpcPath, 0o770);
}
}
}
async function connect() {
if (serverConfig.tunnel?.enabled === true) {
const librariesDir = path.resolve(__dirname, '../libraries');
const frpcPath = path.resolve(librariesDir, 'frpc' + (os.platform() === 'win32' ? '.exe' : ''));
const cfg = ejs.render(frpcConfigTemplate, {
cfg: serverConfig.tunnel,
host: serverConfig.tunnel.community.enabled ? serverConfig.tunnel.community.host : (serverConfig.tunnel.region + ".fmtuner.org"),
server: {
port: serverConfig.webserver.webserverPort
}
});
const cfgPath = path.resolve(librariesDir, 'frpc.toml');
await fs.writeFile(cfgPath, cfg);
const child = spawn(frpcPath, ['-c', cfgPath]);
process.on('exit', () => child.kill());
const rl = readline.createInterface({
input: child.stdout,
terminal: false
});
rl.on('line', (line) => {
if (line.includes('connect to server error')) logError('Failed to connect to tunnel, reason: ' + line.substring(line.indexOf(': ')+2));
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) => {
logError('Failed to start tunnel process:', err);
});
child.on('close', (code) => {
logInfo(`Tunnel process exited with code ${code}`);
});
}
}
const frpcConfigTemplate = `
serverAddr = "<%= host %>"
serverPort = 7000
loginFailExit = false
log.disablePrintColor = true
user = "<%= cfg.username %>"
metadatas.token = "<%= cfg.token %>"
<% if (cfg.lowLatencyMode) { %>
transport.protocol = "kcp"
<% } %>
[[proxies]]
name = "web"
type = "http"
localPort = <%= server.port %>
subdomain = "<%= cfg.subdomain %>"
<% if (cfg.httpName != "") { %>
httpUser = "<%= cfg.httpName %>"
httpPassword = "<%= cfg.httpPassword %>"
<% } %>
`;
module.exports = { connect, download };
-1
View File
@@ -83,7 +83,6 @@ wss.on('connection', (ws, request) => {
}
if(currentUsers >= MAX_USERS) {
// TODO: give a nice fucking unwelcome message on the ui (we kindly ask you to FUCK OFF?)
ws.send("a0"); // This means not authenticated in XDR
ws.close(1008, 'Too many users using this server at the moment.');
return;