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;