diff --git a/server/plugins.js b/server/plugins.js index 1b26022..31bd52a 100644 --- a/server/plugins.js +++ b/server/plugins.js @@ -22,24 +22,41 @@ function parsePluginConfig(filePath) { // Check if pluginConfig has frontEndPath defined if (pluginConfig.frontEndPath) { const sourcePath = path.join(path.dirname(filePath), pluginConfig.frontEndPath); - const destinationDir = path.join(path.dirname(filePath), '../web/js/plugins', pluginConfig.frontEndPath, '..'); + const destinationDir = path.join(__dirname, '../web/js/plugins', path.dirname(pluginConfig.frontEndPath)); + + // Check if the source path exists + if (!fs.existsSync(sourcePath)) { + console.error(`Error: source path ${sourcePath} does not exist.`); + return pluginConfig; + } // Check if the destination directory exists, if not, create it if (!fs.existsSync(destinationDir)) { fs.mkdirSync(destinationDir, { recursive: true }); // Create directory recursively } - // Copy the file to the destination directory const destinationFile = path.join(destinationDir, path.basename(sourcePath)); - fs.copyFileSync(sourcePath, destinationFile); - setTimeout(function() { - consoleCmd.logInfo(`Plugin ${pluginConfig.name} ${pluginConfig.version} initialized successfully.`); - }, 500) + + // Platform-specific handling for symlinks/junctions + if (process.platform !== 'win32') { + // On Linux, create a symlink + try { + if (fs.existsSync(destinationFile)) { + fs.unlinkSync(destinationFile); // Remove existing file/symlink + } + fs.symlinkSync(sourcePath, destinationFile); + setTimeout(function() { + consoleCmd.logInfo(`Plugin ${pluginConfig.name} ${pluginConfig.version} initialized successfully.`); + }, 500) + } catch (err) { + console.error(`Error creating symlink at ${destinationFile}: ${err.message}`); + } + } } else { console.error(`Error: frontEndPath is not defined in ${filePath}`); } } catch (err) { - console.error(`Error parsing plugin config from ${filePath}: ${err}`); + console.error(`Error parsing plugin config from ${filePath}: ${err.message}`); } return pluginConfig; @@ -62,9 +79,37 @@ function collectPluginConfigs() { return pluginConfigs; } +// Ensure the web/js/plugins directory exists +const webJsPluginsDir = path.join(__dirname, '../web/js/plugins'); +if (!fs.existsSync(webJsPluginsDir)) { + fs.mkdirSync(webJsPluginsDir, { recursive: true }); +} + +// Main function to create symlinks/junctions for plugins +function createLinks() { + const pluginsDir = path.join(__dirname, '../plugins'); + const destinationPluginsDir = path.join(__dirname, '../web/js/plugins'); + + if (process.platform === 'win32') { + // On Windows, create a junction + try { + if (fs.existsSync(destinationPluginsDir)) { + fs.rmSync(destinationPluginsDir, { recursive: true }); + } + fs.symlinkSync(pluginsDir, destinationPluginsDir, 'junction'); + setTimeout(function() { + consoleCmd.logInfo(`Plugin ${pluginConfig.name} ${pluginConfig.version} initialized successfully.`); + }, 500) + } catch (err) { + console.error(`Error creating junction at ${destinationPluginsDir}: ${err.message}`); + } + } +} + // Usage example const allPluginConfigs = collectPluginConfigs(); +createLinks(); module.exports = { allPluginConfigs -} \ No newline at end of file +}; diff --git a/server/tx_search.js b/server/tx_search.js index 60e08b7..4d6119a 100644 --- a/server/tx_search.js +++ b/server/tx_search.js @@ -6,6 +6,9 @@ let cachedData = {}; let lastFetchTime = 0; const fetchInterval = 3000; +const esSwitchCache = {"lastCheck":0, "esSwitch":false}; +const esFetchInterval = 300000; + // Fetch data from maps function fetchTx(freq, piCode, rdsPs) { const now = Date.now(); @@ -46,6 +49,7 @@ function processData(data, piCode, rdsPs) { let maxScore = -Infinity; // Initialize maxScore with a very low value let txAzimuth; let maxDistance; + let esMode = checkEs(); for (const cityId in data.locations) { const city = data.locations[cityId]; @@ -53,7 +57,11 @@ function processData(data, piCode, rdsPs) { for (const station of city.stations) { if (station.pi === piCode.toUpperCase() && !station.extra && station.ps && station.ps.toLowerCase().includes(rdsPs.replace(/ /g, '_').replace(/^_*(.*?)_*$/, '$1').toLowerCase())) { const distance = haversine(serverConfig.identification.lat, serverConfig.identification.lon, city.lat, city.lon); - const score = (10*Math.log10(station.erp*1000)) / distance.distanceKm; // Calculate score + let weightDistance = distance.distanceKm + if (esMode && (distance.distanceKm > 200)) { + weightDistance = Math.abs(distance.distanceKm-1500); + } + const score = (10*Math.log10(station.erp*1000)) / weightDistance; // Calculate score if (score > maxScore) { maxScore = score; txAzimuth = distance.azimuth; @@ -82,6 +90,37 @@ function processData(data, piCode, rdsPs) { } } +function checkEs() { + const now = Date.now(); + const url = "https://fmdx.org/includes/tools/get_muf.php"; + let esSwitch = false; + + if (now - esSwitchCache.lastCheck < esFetchInterval) { + esSwitch = esSwitchCache.esSwitch; + } else { + esSwitchCache.lastCheck = now; + fetch(url) + .then(response => response.json()) + .then(data => { + if (serverConfig.identification.lon < -32) { + if (data.north_america.max_frequency != "No data") { + esSwitch = true; + } + } else { + if (data.europe.max_frequency != "No data") { + esSwitch = true; + } + } + esSwitchCache.esSwitch = esSwitch; + }) + .catch(error => { + console.error("Error fetching data:", error); + }); + } + + return esSwitch; +} + function haversine(lat1, lon1, lat2, lon2) { const R = 6371; // Earth radius in kilometers const dLat = deg2rad(lat2 - lat1); diff --git a/web/index.ejs b/web/index.ejs index a382ad7..37f0595 100644 --- a/web/index.ejs +++ b/web/index.ejs @@ -433,7 +433,7 @@ FM-DX Webserver

by Noobish, kkonradpl & the OpenRadio community.

- [Receiver Map] + [Receiver Map]

<% if(ownerContact){ %> @@ -486,6 +486,7 @@ + <% plugins?.forEach(function(plugin) { %> diff --git a/web/js/3las/3las.js b/web/js/3las/3las.js index 3ec297f..bfc2f6c 100644 --- a/web/js/3las/3las.js +++ b/web/js/3las/3las.js @@ -45,6 +45,7 @@ var _3LAS = /** @class */ (function () { }; _3LAS.prototype.Start = function () { this.ConnectivityFlag = false; + this.Stop(); // Attempt to mitigate the 0.5x speed/multiple stream bug // This is stupid, but required for Android.... thanks Google :( if (this.WakeLock) @@ -128,11 +129,50 @@ var _3LAS = /** @class */ (function () { this.ConnectivityFlag = false; if (this.ConnectivityCallback) this.ConnectivityCallback(false); + } + + if (shouldReconnect) { + if (!this.ConnectivityFlag) { + console.log("Initial reconnect attempt..."); + this.Stop(); // Attempt to mitigate the 0.5x speed/multiple stream bug + this.Start(); } - this.Start(); - }; + + // Delay launch of subsequent reconnect attempts by 3 seconds + setTimeout(() => { + + let streamReconnecting = false; + + let intervalReconnect = setInterval(() => { + if (this.ConnectivityFlag || typeof Stream === 'undefined' || Stream === null) { + console.log("Reconnect attempts aborted."); + clearInterval(intervalReconnect); + } else if (!streamReconnecting) { + streamReconnecting = true; + console.log("Attempting to restart stream..."); + this.Stop(); // Attempt to mitigate the 0.5x speed/multiple stream bug + this.Start(); + // Wait for reconnect attempt + setTimeout(() => { + streamReconnecting = false; + }, 3000); + } + // Restore user set volume + if (Stream && typeof newVolumeGlobal !== 'undefined' && newVolumeGlobal !== null) { + Stream.Volume = newVolumeGlobal; + console.log(`User volume restored: ${Math.round(newVolumeGlobal * 100)}%`); + } + }, 3000); + + }, 3000); + + } else { + this.Logger.Log("Reconnection is disabled."); + } +}; + _3LAS.prototype.OnSocketDataReady = function (data) { this.Fallback.OnSocketDataReady(data); }; return _3LAS; -}()); \ No newline at end of file +}()); diff --git a/web/js/3las/main.js b/web/js/3las/main.js index 4812bed..9e716cb 100644 --- a/web/js/3las/main.js +++ b/web/js/3las/main.js @@ -1,43 +1,69 @@ const DefaultVolume = 0.5; let Stream; +let shouldReconnect = true; +let newVolumeGlobal = 1; function Init(_ev) { - try { - const settings = new _3LAS_Settings(); - if (!Stream) { // Ensure Stream is not re-initialized - Stream = new _3LAS(null, settings); - } - } catch (error) { - console.log(error); - return; - } - - Stream.ConnectivityCallback = OnConnectivityCallback; $(".playbutton").off('click').on('click', OnPlayButtonClick); // Ensure only one event handler is attached $("#volumeSlider").off("input").on("input", updateVolume); // Ensure only one event handler is attached } +function createStream() { + try { + const settings = new _3LAS_Settings(); + Stream = new _3LAS(null, settings); + Stream.ConnectivityCallback = OnConnectivityCallback; + } catch (error) { + console.error("Initialization Error: ", error); + } +} + +function destroyStream() { + if (Stream) { + Stream.Stop(); + Stream = null; + } +} + function OnConnectivityCallback(isConnected) { - Stream.Volume = isConnected ? 1.0 : DefaultVolume; + console.log("Connectivity changed:", isConnected); + if (Stream) { + Stream.Volume = isConnected ? 1.0 : DefaultVolume; + } else { + console.warn("Stream is not initialized."); + } } function OnPlayButtonClick(_ev) { const $playbutton = $('.playbutton'); - $playbutton.find('.fa-solid').toggleClass('fa-play fa-stop'); - - if (Stream.ConnectivityFlag) { - Stream.Stop(); + if (Stream) { + console.log("Stopping stream..."); + shouldReconnect = false; + destroyStream(); + $playbutton.find('.fa-solid').toggleClass('fa-stop fa-play'); } else { + console.log("Starting stream..."); + shouldReconnect = true; + createStream(); Stream.Start(); - $playbutton.addClass('bg-gray').prop('disabled', true); - setTimeout(() => { - $playbutton.removeClass('bg-gray').prop('disabled', false); - }, 3000); + $playbutton.find('.fa-solid').toggleClass('fa-play fa-stop'); } + $playbutton.addClass('bg-gray').prop('disabled', true); + setTimeout(() => { + $playbutton.removeClass('bg-gray').prop('disabled', false); + }, 3000); } function updateVolume() { - Stream.Volume = $(this).val(); + if (Stream) { + const newVolume = $(this).val(); + newVolumeGlobal = newVolume; + console.log("Volume updated to:", newVolume); + Stream.Volume = newVolume; + } else { + console.warn("Stream is not initialized."); + } } -$(document).ready(Init); \ No newline at end of file +$(document).ready(Init); + diff --git a/web/js/main.js b/web/js/main.js index 99c5e74..f219828 100644 --- a/web/js/main.js +++ b/web/js/main.js @@ -1,7 +1,7 @@ -var url = new URL('text', window.location.href); -url.protocol = url.protocol.replace('http', 'ws'); -var socketAddress = url.href; -var socket = new WebSocket(socketAddress); +// WebSocket connection located in ./websocket.js + + + var parsedData, signalChart, previousFreq; var signalData = []; var data = []; @@ -10,7 +10,7 @@ let updateCounter = 0; const europe_programmes = [ "No PTY", "News", "Current Affairs", "Info", "Sport", "Education", "Drama", "Culture", "Science", "Varied", - "Pop M", "Rock M", "Easy Listening", "Light Classical", + "Pop Music", "Rock Music", "Easy Listening", "Light Classical", "Serious Classical", "Other Music", "Weather", "Finance", "Children's Programmes", "Social Affairs", "Religion", "Phone-in", "Travel", "Leisure", "Jazz Music", "Country Music", "National Music", @@ -873,7 +873,7 @@ function initTooltips() { // Add a delay of 500 milliseconds before creating and appending the tooltip $(this).data('timeout', setTimeout(() => { var tooltip = $('
').html(tooltipText); - $('body').append(tooltip); + if ($('.tooltiptext').length === 0) { $('body').append(tooltip); } // Don't allow more than one tooltip var posX = e.pageX; var posY = e.pageY; @@ -888,6 +888,7 @@ function initTooltips() { // Clear the timeout if the mouse leaves before the delay completes clearTimeout($(this).data('timeout')); $('.tooltiptext').remove(); + setTimeout(() => { $('.tooltiptext').remove(); }, 500); // Ensure no tooltips remain stuck }).mousemove(function(e){ var tooltipWidth = $('.tooltiptext').outerWidth(); var tooltipHeight = $('.tooltiptext').outerHeight(); diff --git a/web/js/websocket.js b/web/js/websocket.js new file mode 100644 index 0000000..d965d33 --- /dev/null +++ b/web/js/websocket.js @@ -0,0 +1,30 @@ +var url = new URL('text', window.location.href); +url.protocol = url.protocol.replace('http', 'ws'); +var socketAddress = url.href; +var socket = new WebSocket(socketAddress); + +const socketPromise = new Promise((resolve, reject) => { + // Event listener for when the WebSocket connection is open + socket.addEventListener('open', () => { + console.log('WebSocket connection open'); + resolve(socket); // Resolve the promise with the WebSocket instance + }); + + // Event listener for WebSocket errors + socket.addEventListener('error', (error) => { + console.error('WebSocket error', error); + reject(error); // Reject the promise on error + }); + + // Event listener for WebSocket connection closure + socket.addEventListener('close', () => { + console.warn('WebSocket connection closed'); + reject(new Error('WebSocket connection closed')); // Reject with closure warning + }); +}); + +// Assign the socketPromise to window.socketPromise for global access +window.socketPromise = socketPromise; + +// Assign the socket instance to window.socket for global access +window.socket = socket;