diff --git a/.gitignore b/.gitignore index 1b3b2af..03f42a7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules/ /*.json /ffmpeg.exe -/serverlog.txt \ No newline at end of file +/serverlog.txt +/web/js/plugins/ \ No newline at end of file diff --git a/package.json b/package.json index 815b6e5..ad308c0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fm-dx-webserver", - "version": "1.1.9", + "version": "1.2.0", "description": "FM DX Webserver", "main": "index.js", "scripts": { diff --git a/server/console.js b/server/console.js index 578ba21..b7e819f 100644 --- a/server/console.js +++ b/server/console.js @@ -4,7 +4,7 @@ const verboseMode = process.argv.includes('--debug'); const verboseModeFfmpeg = process.argv.includes('--ffmpegdebug'); const ANSI_ESCAPE_CODE_PATTERN = /\x1b\[[0-9;]*m/g; -const MAX_LOG_LINES = 100000; +const MAX_LOG_LINES = 5000; const getCurrentTime = () => { const currentTime = new Date(); @@ -90,7 +90,8 @@ const logWarn = (...messages) => { }; function appendLogToFile(logMessage) { - const cleanLogMessage = removeANSIEscapeCodes(logMessage); + const date = new Date(); + const cleanLogMessage = date.toLocaleDateString() + ' | ' + removeANSIEscapeCodes(logMessage); fs.appendFile('serverlog.txt', cleanLogMessage + '\n', (err) => { if (err) { diff --git a/server/endpoints.js b/server/endpoints.js index 2b4bc41..758e41a 100644 --- a/server/endpoints.js +++ b/server/endpoints.js @@ -13,6 +13,7 @@ const storage = require('./storage'); const { logInfo, logDebug, logWarn, logError, logFfmpeg, logs } = require('./console'); const dataHandler = require('./datahandler'); const fmdxList = require('./fmdx_list'); +const { allPluginConfigs } = require('./plugins'); // Endpoints router.get('/', (req, res) => { @@ -56,6 +57,7 @@ router.get('/', (req, res) => { tuningUpperLimit: serverConfig.webserver.tuningUpperLimit, chatEnabled: serverConfig.webserver.chatEnabled, device: serverConfig.device, + plugins: serverConfig.plugins, bwSwitch: serverConfig.bwSwitch ? serverConfig.bwSwitch : false }); } @@ -108,6 +110,7 @@ router.get('/setup', (req, res) => { memoryUsage: (process.memoryUsage.rss() / 1024 / 1024).toFixed(1) + ' MB', processUptime: formattedProcessUptime, consoleOutput: logs, + plugins: allPluginConfigs, onlineUsers: dataHandler.dataToSend.users, connectedUsers: storage.connectedUsers }); @@ -154,6 +157,17 @@ router.get('/logout', (req, res) => { }); }); +router.get('/kick', (req, res) => { + const ipAddress = req.query.ip; // Extract the IP address parameter from the query string + // Terminate the WebSocket connection for the specified IP address + if(req.session.isAdminAuthenticated) { + helpers.kickClient(ipAddress); + } + setTimeout(() => { + res.redirect('/setup'); + }, 500); +}); + router.post('/saveData', (req, res) => { const data = req.body; let firstSetup; @@ -215,7 +229,8 @@ router.get('/static_data', (req, res) => { qthLongitude: serverConfig.identification.lon, presets: serverConfig.webserver.presets || [], defaultTheme: serverConfig.webserver.defaultTheme || 'theme1', - bgImage: serverConfig.webserver.bgImage || '' + bgImage: serverConfig.webserver.bgImage || '', + rdsMode: serverConfig.webserver.rdsMode || false, }); }); diff --git a/server/helpers.js b/server/helpers.js index 8729e68..6c27aa9 100644 --- a/server/helpers.js +++ b/server/helpers.js @@ -1,5 +1,7 @@ const WebSocket = require('ws'); const dataHandler = require('./datahandler'); +const storage = require('./storage'); +const consoleCmd = require('./console'); function parseMarkdown(parsed) { parsed = parsed.replace(/<\/?[^>]+(>|$)/g, ''); @@ -79,6 +81,23 @@ function resolveDataBuffer(data, wss) { } } +function kickClient(ipAddress) { + // Find the entry in connectedClients associated with the provided IP address + const targetClient = storage.connectedUsers.find(client => client.ip === ipAddress); + if (targetClient && targetClient.instance) { + // Send a termination message to the client + targetClient.instance.send('KICK'); + + // Close the WebSocket connection after a short delay to allow the client to receive the message + setTimeout(() => { + targetClient.instance.close(); + consoleCmd.logInfo(`Web client kicked (${ipAddress})`); + }, 500); + } else { + consoleCmd.logInfo(`Kicking client ${ipAddress} failed. No suitable client found.`); + } +} + module.exports = { - parseMarkdown, removeMarkdown, formatUptime, resolveDataBuffer + parseMarkdown, removeMarkdown, formatUptime, resolveDataBuffer, kickClient } \ No newline at end of file diff --git a/server/index.js b/server/index.js index e9ee676..a2b0c3f 100644 --- a/server/index.js +++ b/server/index.js @@ -38,6 +38,7 @@ console.log('\x1b[90m――――――――――――――――――― // Start ffmpeg require('./stream/index'); +require('./plugins'); // Create a WebSocket proxy instance const proxy = httpProxy.createProxyServer({ @@ -253,7 +254,7 @@ wss.on('connection', (ws, request) => { const connectionTime = new Date().toLocaleString([], options); if(locationInfo.country === undefined) { - const userData = { ip: clientIp, location: 'Unknown', time: connectionTime }; + const userData = { ip: clientIp, location: 'Unknown', time: connectionTime, instance: ws }; storage.connectedUsers.push(userData); logInfo(`Web client \x1b[32mconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]\x1b[0m`); } else { diff --git a/server/server_config.js b/server/server_config.js index 2b76fbf..38d7a43 100644 --- a/server/server_config.js +++ b/server/server_config.js @@ -45,6 +45,7 @@ let serverConfig = { tunePass: "", adminPass: "" }, + plugins: [], device: 'tef', defaultFreq: 87.5, publicTuner: true, @@ -65,9 +66,10 @@ function deepMerge(target, source) } function configUpdate(newConfig) { - if (newConfig.webserver && newConfig.webserver.banlist !== undefined) { + if (newConfig.webserver && (newConfig.webserver.banlist !== undefined || newConfig.plugins !== undefined)) { // If new banlist is provided, replace the existing one serverConfig.webserver.banlist = newConfig.webserver.banlist; + serverConfig.plugins = newConfig.plugins; delete newConfig.webserver.banlist; // Remove banlist from newConfig to avoid merging } diff --git a/web/css/entry.css b/web/css/entry.css index aecf56a..f08ac8d 100644 --- a/web/css/entry.css +++ b/web/css/entry.css @@ -7,4 +7,5 @@ @import url("panels.css"); /* Different panels and their sizes */ @import url("modal.css"); /* Modal window */ @import url("setup.css"); /* Web setup interface */ +@import url("multiselect.css"); /* Multiselect tags */ @import url("helpers.css"); /* Stuff that is used often such as text changers etc */ \ No newline at end of file diff --git a/web/index.ejs b/web/index.ejs index 1a1362b..119e76d 100644 --- a/web/index.ejs +++ b/web/index.ejs @@ -468,5 +468,8 @@ +<% plugins.forEach(function(plugin) { %> + +<% }); %> diff --git a/web/js/3las/main.js b/web/js/3las/main.js index e2f9346..dd0d195 100644 --- a/web/js/3las/main.js +++ b/web/js/3las/main.js @@ -20,6 +20,8 @@ function OnConnectivityCallback(isConnected) { } function OnPlayButtonClick(_ev) { + const $playbutton = $('.playbutton'); + $playbutton.find('.fa-solid').toggleClass('fa-play fa-stop'); try { if (Stream.ConnectivityFlag) { Stream.Stop(); @@ -29,8 +31,6 @@ function OnPlayButtonClick(_ev) { setTimeout(() => { $playbutton.removeClass('bg-gray').prop('disabled', false); }, 3000); - const $playbutton = $('.playbutton'); - $playbutton.find('.fa-solid').toggleClass('fa-play fa-stop'); } } catch (error) { console.error(error); diff --git a/web/js/confighandler.js b/web/js/confighandler.js index 1f1bc35..e6d87f3 100644 --- a/web/js/confighandler.js +++ b/web/js/confighandler.js @@ -11,6 +11,7 @@ function submitData() { const defaultTheme = themeDataValue; const bgImage = $("#bg-image").val() || ''; + const rdsMode = $('#rds-mode').is(":checked") || false; const ant1enabled = $('#ant1-enabled').is(":checked") || false; const ant2enabled = $('#ant2-enabled').is(":checked") || false; @@ -71,6 +72,11 @@ function submitData() { const tunePass = $('#tune-pass').val(); const adminPass = $('#admin-pass').val(); + let plugins = []; + $('#plugin-list option:selected').each(function() { + plugins.push($(this).data('name')); + }); + const publicTuner = $("#tuner-public").is(":checked"); const lockToAdmin = $("#tuner-lock").is(":checked"); const autoShutdown = $("#shutdown-tuner").is(":checked") || false; @@ -88,7 +94,8 @@ function submitData() { defaultTheme, presets, banlist, - bgImage + bgImage, + rdsMode, }, antennas: { enabled: antennasEnabled, @@ -136,6 +143,7 @@ function submitData() { tunePass, adminPass, }, + plugins, device, publicTuner, lockToAdmin, @@ -181,6 +189,7 @@ function submitData() { $("#chat-switch").prop("checked", data.webserver.chatEnabled || false); $('#selected-theme').val(data.webserver.defaultTheme || 'Default'); + $('#rds-mode').prop("checked", data.webserver.rdsMode || false); var selectedTheme = $(".option[data-value='" + data.webserver.defaultTheme + "']"); @@ -273,6 +282,14 @@ function submitData() { $("#shutdown-tuner").prop("checked", data.autoShutdown); $("#antenna-switch").prop("checked", data.antennas?.enabled); + data.plugins.forEach(function(name) { + // Find the option with the corresponding data-name attribute and mark it as selected + $('#plugin-list option[data-name="' + name + '"]').prop('selected', true); + }); + + // Update the multi-select element to reflect the changes + $('#plugin-list').trigger('change'); + // Check if latitude and longitude are present in the data if (data.identification.lat && data.identification.lon) { // Set the map's center to the received coordinates diff --git a/web/js/dropdown.js b/web/js/dropdown.js index 1632655..0be1700 100644 --- a/web/js/dropdown.js +++ b/web/js/dropdown.js @@ -54,3 +54,18 @@ const closeDropdownFromOutside = (event) => { $(document).on('click', closeDropdownFromOutside); $listOfOptions.on('click', selectOption); $dropdowns.on('click', toggleDropdown); + +// MULTISELECT +$('.multiselect option').mousedown(function(e) { + e.preventDefault(); + var originalScrollTop = $(this).parent().scrollTop(); + console.log(originalScrollTop); + $(this).prop('selected', $(this).prop('selected') ? false : true); + var self = this; + $(this).parent().focus(); + setTimeout(function() { + $(self).parent().scrollTop(originalScrollTop); + }, 0); + + return false; +}); \ No newline at end of file diff --git a/web/js/init.js b/web/js/init.js index dbcd4a3..688b283 100644 --- a/web/js/init.js +++ b/web/js/init.js @@ -3,7 +3,7 @@ var day = currentDate.getDate(); var month = currentDate.getMonth() + 1; // Months are zero-indexed, so add 1 var year = currentDate.getFullYear(); var formattedDate = day + '/' + month + '/' + year; -var currentVersion = 'v1.1.9c [' + formattedDate + ']'; +var currentVersion = 'v1.2.0 [' + formattedDate + ']'; getInitialSettings(); @@ -20,6 +20,7 @@ function getInitialSettings() { localStorage.setItem('preset3', data.presets[2]); localStorage.setItem('preset4', data.presets[3]); localStorage.setItem('bgImage', data.bgImage); + localStorage.setItem('rdsMode', data.rdsMode); }, error: function (error) { console.error('Error:', error); diff --git a/web/js/main.js b/web/js/main.js index 9feb569..36870fa 100644 --- a/web/js/main.js +++ b/web/js/main.js @@ -25,6 +25,8 @@ const usa_programmes = [ "Spanish Talk", "Spanish Music", "Hip Hop", "", "", "Weather", "Emergency Test", "Emergency" ]; +const rdsMode = localStorage.getItem('rdsMode'); + $(document).ready(function () { var canvas = $('#signal-canvas')[0]; @@ -359,7 +361,16 @@ function updateCanvas(parsedData, signalChart) { } socket.onmessage = (event) => { + if (event.data == 'KICK') { + console.log('Kick iniitiated.') + setTimeout(() => { + window.location.href = '/403'; + }, 500); // Adjust the delay as needed + return; + } + parsedData = JSON.parse(event.data); + updatePanels(parsedData); if(localStorage.getItem("smoothSignal") == 'true') { const sum = signalData.reduce((acc, strNum) => acc + parseFloat(strNum), 0); @@ -701,7 +712,7 @@ const updateDataElements = throttle(function(parsedData) { updateHtmlIfChanged($dataRt0, processString(parsedData.rt0, parsedData.rt0_errors)); updateHtmlIfChanged($dataRt1, processString(parsedData.rt1, parsedData.rt1_errors)); - updateTextIfChanged($dataPty, europe_programmes[parsedData.pty]); + updateTextIfChanged($dataPty, rdsMode == 'true' ? usa_programmes[parsedData.pty] : europe_programmes[parsedData.pty]); if (parsedData.rds === true) { $flagDesktopCointainer.css('background-color', 'var(--color-2)'); diff --git a/web/setup.ejs b/web/setup.ejs index bc7a249..4e7029d 100644 --- a/web/setup.ejs +++ b/web/setup.ejs @@ -25,6 +25,7 @@
  • Connection
  • Audio
  • Webserver
  • +
  • Plugins
  • Info & Map
  • @@ -59,7 +60,7 @@ IP Address Location Online since - + @@ -69,7 +70,7 @@ <%= user.ip %> <%= user.location %> <%= user.time %> - + Kick <% }); %> <% } else { %> @@ -78,7 +79,32 @@ <% } %> - + + +

    Maintenance

    +
    +
    + + +
    +
    + + +
    +
    + + +

    +
    + +
    + + +
    +
    + + +

    Console

    <% if (consoleOutput && consoleOutput.length > 0) { %> @@ -91,37 +117,8 @@

    No console output available.

    <% } %> -

    Maintenance

    -
    -
    -
    - - -
    -
    - - -
    -
    -
    -
    - - -

    -
    -
    - -
    - - -
    -
    - - -

    -

    Version:

    -

    Check for the latest source codeSupport the developer

    +

    Check for the latest source codeSupport the developer

    @@ -355,6 +352,13 @@
    +

    RDS Mode

    +

    You can switch between American (RBDS) / Global (RDS) mode here.

    +
    + + +
    +

    Presets

    You can set up to 4 presets.
    These presets are accessible with the F1-F4 buttons.
    Enter frequencies in MHz.

    @@ -399,6 +403,21 @@
    +
    +

    Plugins

    +

    Any compatible .js plugin, which is in the "plugins" folder, will be listed here.
    + Click on the individual plugins to enable/disable them.

    +

    + Download new plugins here! + +
    +

    Tuner Specific Settings